Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for L2CAP and refactor state management #32

Merged
merged 20 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ disabled_rules:
- cyclomatic_complexity
- todo
- file_length
- function_body_length
- type_body_length
- force_try
- non_optional_string_data_conversion
184 changes: 184 additions & 0 deletions Sources/MobileSdk/BLEConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Derived from MIT-licensed work by Paul Wilkinson: https://github.com/paulw11/L2Cap

import CoreBluetooth
import Foundation

/// The base BLE connection, only intended for subclassing.
class BLEInternalL2CAPConnection: NSObject, StreamDelegate {
var channel: CBL2CAPChannel?

private var outputData = Data()
private var outputDelivered = false
private var incomingData = Data()
private var incomingTime = Date(timeIntervalSinceNow: 0)
private var incomingDelivered = false
private var openCount = 0
private var totalBytesWritten = 0

/// Handle stream events. Many of these we hand to local methods which the child classes are expected to
/// override.
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case Stream.Event.openCompleted:
// TODO: This is a bit of a hack, but it'll do for now. There are two streams, one input, one
// output, and we get notified about both. We really only want to start doing things when
// both are available.
openCount += 1

if openCount == 2 {
streamIsOpen()
}

case Stream.Event.endEncountered:
openCount -= 1
streamEnded()

case Stream.Event.hasBytesAvailable:
streamBytesAvailable()
if let stream = aStream as? InputStream {
readBytes(from: stream)
}

case Stream.Event.hasSpaceAvailable:
streamSpaceAvailable()
send()

case Stream.Event.errorOccurred:
streamError()

default:
streamUnknownEvent()
}
}

/// Public send() interface.
public func send(data: Data) {
if !outputDelivered {
outputDelivered = true
outputData = data
totalBytesWritten = 0
send()
}
}

/// Internal send() interface.
private func send() {
guard let ostream = channel?.outputStream, !outputData.isEmpty, ostream.hasSpaceAvailable else {
return
}
let bytesWritten = ostream.write(outputData)

totalBytesWritten += bytesWritten

// The isEmpty guard above should prevent div0 errors here.
let fracDone = Double(totalBytesWritten) / Double(outputData.count)

streamSentData(bytes: bytesWritten, total: totalBytesWritten, fraction: fracDone)

if bytesWritten < outputData.count {
outputData = outputData.advanced(by: bytesWritten)
} else {
outputData.removeAll()
}
}

/// Close the stream.
public func close() {
if let chn = channel {
chn.outputStream.close()
chn.inputStream.close()
chn.inputStream.remove(from: .main, forMode: .default)
chn.outputStream.remove(from: .main, forMode: .default)
chn.inputStream.delegate = nil
chn.outputStream.delegate = nil
openCount = 0
}

channel = nil
}

/// Read from the stream.
private func readBytes(from stream: InputStream) {
let bufLength = 1024
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufLength)
defer {
buffer.deallocate()
}
let bytesRead = stream.read(buffer, maxLength: bufLength)
incomingData.append(buffer, count: bytesRead)

// This is an awful hack to work around a hairy problem. L2CAP is a stream protocol; there's
sbihel marked this conversation as resolved.
Show resolved Hide resolved
// no framing on data, so there's no way to signal that the data exchange is complete. In principle
// we could build a framing protocol on top, or we could use the State characteristics to signal out
// of band, but neither of those are specified by the spec, so we'd be out of compliance. The State
// signalling is what the non-L2CAP flow uses, but the spec explicitly says it's not used with L2CAP.
//
// Another thing we could do would be close the connection, but there are two problems with that;
// the first is we'd be out of spec compliance again, and the second is that we actually have two
// messages going, one in each direction, serially. If we closed to indicate the length of the first,
// we'd have no connection for the second.
//
// So, we have data coming in, and we don't know how much. The stream lets us know when more data
// has arrived, the data comes in chunks. What we do, then, is timestamp when we receive some data,
// and then half a second later see if we got any more. Hopefully the half second delay is small
// enough not to annoy the user and large enough to account for noisy radio environments, but the
// numbers here are a heuristic, and may need to be tuned. If we have no recent data, we assume
// everything is ok, and declare the transmission complete.

incomingTime = Date(timeIntervalSinceNow: 0)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Half second delay.
if self.incomingDelivered {
return
}

let timeSinceLastData = -self.incomingTime.timeIntervalSinceNow // Make it positive.
let complete = timeSinceLastData > 0.25

if complete {
self.streamReceivedData(self.incomingData)
self.incomingDelivered = true
}
}

if stream.hasBytesAvailable {
readBytes(from: stream)
}
}

/// Methods to be overridden by child classes.
func streamIsOpen() { print("The stream is open.") }
func streamEnded() { print("The stream has ended.") }
func streamBytesAvailable() { print("The stream has bytes available.") }
func streamSpaceAvailable() { print("The stream has space available.") }
func streamError() { print("Stream error.") }
func streamUnknownEvent() { print("Stream unknown event.") }
func streamSentData(bytes _: Int, total _: Int, fraction _: Double) { print("Stream sent data.") }
func streamReceivedData(_: Data) { print("Stream received data.") }
}

/// A UInt16 from Data extension.
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}

/// A Data from UInt16 extension.
extension Data {
var uint16: UInt16 {
let i16array = withUnsafeBytes { $0.load(as: UInt16.self) }
return i16array
}
}

/// A write() on OutputStream extension.
extension OutputStream {
func write(_ data: Data) -> Int {
return data.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> Int in
let bufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
return self.write(bufferPointer.baseAddress!, maxLength: data.count)
}
}
}
8 changes: 6 additions & 2 deletions Sources/MobileSdk/Credentials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ public class CredentialStore {

// swiftlint:disable force_cast
public func presentMdocBLE(deviceEngagement: DeviceEngagement,
callback: BLESessionStateDelegate
callback: BLESessionStateDelegate,
useL2CAP: Bool = true
// , trustedReaders: TrustedReaders
) -> BLESessionManager? {
if let firstMdoc = self.credentials.first(where: {$0 is MDoc}) {
return BLESessionManager(mdoc: firstMdoc as! MDoc, engagement: DeviceEngagement.QRCode, callback: callback)
return BLESessionManager(mdoc: firstMdoc as! MDoc,
engagement: DeviceEngagement.QRCode,
callback: callback,
useL2CAP: useL2CAP)
} else {
return nil
}
Expand Down
8 changes: 6 additions & 2 deletions Sources/MobileSdk/MDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,20 @@ public class BLESessionManager {
var sessionManager: SessionManager?
var mdoc: MDoc
var bleManager: MDocHolderBLECentral!
var useL2CAP: Bool

init?(mdoc: MDoc, engagement: DeviceEngagement, callback: BLESessionStateDelegate) {
init?(mdoc: MDoc, engagement: DeviceEngagement, callback: BLESessionStateDelegate, useL2CAP: Bool) {
self.callback = callback
self.uuid = UUID()
self.mdoc = mdoc
self.useL2CAP = useL2CAP
do {
let sessionData = try SpruceIDMobileSdkRs.initialiseSession(document: mdoc.inner,
uuid: self.uuid.uuidString)
self.state = sessionData.state
bleManager = MDocHolderBLECentral(callback: self, serviceUuid: CBUUID(nsuuid: self.uuid))
bleManager = MDocHolderBLECentral(callback: self,
serviceUuid: CBUUID(nsuuid: self.uuid),
useL2CAP: useL2CAP)
self.callback.update(state: .engagingQRCode(sessionData.qrCodeUri.data(using: .ascii)!))
} catch {
print("\(error)")
Expand Down
63 changes: 63 additions & 0 deletions Sources/MobileSdk/MDocBLEUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,66 @@ enum MDocReaderBLECallback {
protocol MDocReaderBLEDelegate: AnyObject {
func callback(message: MDocReaderBLECallback)
}

/// Return a string describing a BLE characteristic property.
func MDocCharacteristicPropertyName(_ prop: CBCharacteristicProperties) -> String {
return switch prop {
case .broadcast: "broadcast"
case .read: "read"
case .writeWithoutResponse: "write without response"
case .write: "write"
case .notify: "notify"
case .indicate: "indicate"
case .authenticatedSignedWrites: "authenticated signed writes"
case .extendedProperties: "extended properties"
case .notifyEncryptionRequired: "notify encryption required"
case .indicateEncryptionRequired: "indicate encryption required"
default: "unknown property"
}
}

/// Return a string describing a BLE characteristic.
func MDocCharacteristicName(_ chr: CBCharacteristic) -> String {
return MDocCharacteristicNameFromUUID(chr.uuid)
}

/// Return a string describing a BLE characteristic given its UUID.
func MDocCharacteristicNameFromUUID(_ chr: CBUUID) -> String {
return switch chr {
case holderStateCharacteristicId: "Holder:State"
case holderClient2ServerCharacteristicId: "Holder:Client2Server"
case holderServer2ClientCharacteristicId: "Holder:Server2Client"
case holderL2CAPCharacteristicId: "Holder:L2CAP"
case readerStateCharacteristicId: "Reader:State"
case readerClient2ServerCharacteristicId: "Reader:Client2Server"
case readerServer2ClientCharacteristicId: "Reader:Server2Client"
case readerIdentCharacteristicId: "Reader:Ident"
case readerL2CAPCharacteristicId: "Reader:L2CAP"
default: "Unknown:\(chr)"
}
}

/// Print a description of a BLE characteristic.
func MDocDesribeCharacteristic(_ chr: CBCharacteristic) {
print(" \(MDocCharacteristicName(chr)) ( ", terminator: "")

if chr.properties.contains(.broadcast) { print("broadcast", terminator: " ") }
if chr.properties.contains(.read) { print("read", terminator: " ") }
if chr.properties.contains(.writeWithoutResponse) { print("writeWithoutResponse", terminator: " ") }
if chr.properties.contains(.write) { print("write", terminator: " ") }
if chr.properties.contains(.notify) { print("notify", terminator: " ") }
if chr.properties.contains(.indicate) { print("indicate", terminator: " ") }
if chr.properties.contains(.authenticatedSignedWrites) { print("authenticatedSignedWrites", terminator: " ") }
if chr.properties.contains(.extendedProperties) { print("extendedProperties", terminator: " ") }
if chr.properties.contains(.notifyEncryptionRequired) { print("notifyEncryptionRequired", terminator: " ") }
if chr.properties.contains(.indicateEncryptionRequired) { print("indicateEncryptionRequired", terminator: " ") }
print(")")

if let descriptors = chr.descriptors {
for desc in descriptors {
print(" : \(desc.uuid)")
}
} else {
print(" <no descriptors>")
}
}
Loading
Loading