-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for L2CAP and refactor state management (#32)
This is a fairly large change; both the holder and the reader have been extended to support L2CAP. In service of this, the original flow has been refactored into more explicit state machines, and the L2CAP flow has been added to those state machines. Both the reader and holder have `useL2CAP` booleans; if either is `false`, the reader and holder will use the old flow to communicate. This should also mean that they will use the old flow when working with other readers or holders that do not support L2CAP. Some of this work (notably the `*Connection.swift` files) is derived from Paul Wilkinson's MIT-licensed L2Cap library: https://github.com/paulw11/L2Cap This repo is MIT-licensed as well, so the licenses are compatible. We aren't using the L2Cap library as-is for a variety of reasons, but the main reason is that the behaviour we need differs significantly from the behaviour L2Cap (the library) offers. There are a couple of places in this change that are hacking around impedance mismatches: - we seem to need to have `notify` on the L2CAP characteristic in order to propagate the PSM to central, though 18013-5 Annex A claims the only required property is `read` -- we're not out of spec, since we're offering an optional property, but it remains to be seen how that will interact with 3rd party centrals - 18013-5 specifies no framing for the request or response sent over the L2CAP stream, so we have to infer when the data has arrived from timing; this seems like a fragile method particularly if confronted by noisy radio environments
- Loading branch information
1 parent
8b80370
commit e1e8d12
Showing
10 changed files
with
1,034 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// 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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.