Skip to content
Open
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
private static let pelagicGen1TxCharacteristic = CBUUID(
string: "6606AB42-89D5-4A00-A8CE-4EB5E1414EE0"
)
private static let nordicUartServiceUUID = CBUUID(
string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
)
private static let nordicUartServiceRxCharacteristic = CBUUID(
string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
)
private static let nordicUartServiceNotifyCharacteristic = CBUUID(
string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
)
private static let notifySettleDelaySeconds: TimeInterval = 0.3
private static let bleIoctlType: UInt32 = UInt32(Character("b").asciiValue!)
private static let bleIoctlGetNameNumber: UInt32 = 0
Expand All @@ -44,13 +53,16 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
private let centralManager: CBCentralManager

private var writeCharacteristic: CBCharacteristic?
// Kept only for the "Discovery ready" log message.
private var notifyCharacteristic: CBCharacteristic?
private var subscribedCharacteristics: [CBCharacteristic] = []
Comment thread
ericgriffin marked this conversation as resolved.
private var bestCandidate: CharacteristicCandidate?
private var remainingServiceDiscoveries = 0
private var discoverySignaled = false

private let readLock = NSLock()
private var readBuffer = Data()
private var readPackets: [Data] = []
private var partialReadBuffer = Data()
private let readSemaphore = DispatchSemaphore(value: 0)
private let writeSemaphore = DispatchSemaphore(value: 0)
private let writeReadySemaphore = DispatchSemaphore(value: 0)
Expand All @@ -74,6 +86,11 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
/// Set by DiveComputerHostApiImpl before download starts.
var onPinCodeRequired: ((String) -> Void)?

private var isLikelyCrestDevice: Bool {
let name = peripheral.name?.uppercased() ?? ""
return name.hasPrefix("CREST")
}

init(peripheral: CBPeripheral, centralManager: CBCentralManager) {
self.peripheral = peripheral
self.centralManager = centralManager
Expand Down Expand Up @@ -138,6 +155,7 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
bestCandidate = nil
writeCharacteristic = nil
notifyCharacteristic = nil
subscribedCharacteristics = []
hasSeenNotify = false
writeWithoutResponsePreferred = false
consecutiveReadTimeouts = 0
Expand Down Expand Up @@ -261,9 +279,9 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
private static let cPoll: libdc_io_poll_fn = { userdata, timeout in
let stream = Unmanaged<BleIoStream>.fromOpaque(userdata!).takeUnretainedValue()
stream.readLock.lock()
let available = stream.readBuffer.count
let available = !stream.partialReadBuffer.isEmpty || !stream.readPackets.isEmpty
stream.readLock.unlock()
if available > 0 { return Int32(LIBDC_STATUS_SUCCESS) }
if available { return Int32(LIBDC_STATUS_SUCCESS) }

if timeout == 0 { return Int32(LIBDC_STATUS_TIMEOUT) }

Expand All @@ -286,13 +304,25 @@ class BleIoStream: NSObject, CBPeripheralDelegate {

while true {
readLock.lock()
let available = readBuffer.count
if available > 0 {
let bytesToRead = min(size, readBuffer.count)
_ = readBuffer.withUnsafeBytes { ptr in
if partialReadBuffer.isEmpty, !readPackets.isEmpty {
partialReadBuffer = readPackets.removeFirst()
}
Comment thread
ericgriffin marked this conversation as resolved.
if partialReadBuffer.count > size {
let packetSize = partialReadBuffer.count
partialReadBuffer.removeAll(keepingCapacity: true)
readLock.unlock()
actual.pointee = 0
NativeLogger.e("BleIoStream", category: "BLE",
"Received BLE packet larger than requested read size"
+ " (packet=\(packetSize) requested=\(size)); dropping packet")
return Int32(LIBDC_STATUS_IO)
}
if !partialReadBuffer.isEmpty {
let bytesToRead = min(size, partialReadBuffer.count)
_ = partialReadBuffer.withUnsafeBytes { ptr in
memcpy(data, ptr.baseAddress!, bytesToRead)
}
readBuffer.removeFirst(bytesToRead)
partialReadBuffer.removeFirst(bytesToRead)
Comment thread
ericgriffin marked this conversation as resolved.
readLock.unlock()
actual.pointee = bytesToRead
return Int32(LIBDC_STATUS_SUCCESS)
Comment thread
ericgriffin marked this conversation as resolved.
Expand Down Expand Up @@ -392,7 +422,8 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
}

readLock.lock()
readBuffer.removeAll(keepingCapacity: true)
readPackets.removeAll(keepingCapacity: true)
partialReadBuffer.removeAll(keepingCapacity: true)
readLock.unlock()
drainSemaphore(readSemaphore)
return Int32(LIBDC_STATUS_SUCCESS)
Expand Down Expand Up @@ -431,6 +462,11 @@ class BleIoStream: NSObject, CBPeripheralDelegate {

private func initialWriteWithoutResponsePreference(for characteristic: CBCharacteristic) -> Bool {
let properties = characteristic.properties
// CR5L needs the NUS path to stay Crest-gated. If we later want to
// generalize support for more NUS devices, start by widening this gate.
if isLikelyCrestDevice && characteristic.uuid == Self.nordicUartServiceRxCharacteristic {
return false
}
if characteristic.uuid == Self.pelagicGen1TxCharacteristic {
return true
}
Expand Down Expand Up @@ -626,18 +662,25 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
+ " \(error.localizedDescription)")
return
}
if let notify = notifyCharacteristic, characteristic.uuid != notify.uuid {
guard subscribedCharacteristics.contains(where: { $0.uuid == characteristic.uuid }) else {
Comment thread
ericgriffin marked this conversation as resolved.
NativeLogger.w("BleIoStream", category: "BLE",
"Ignoring notify from \(characteristic.uuid.uuidString)"
+ " (subscribed count=\(subscribedCharacteristics.count))")
return
}
guard let value = characteristic.value, !value.isEmpty else {
NativeLogger.w("BleIoStream", category: "BLE",
"Ignoring empty notify from \(characteristic.uuid.uuidString)")
return
}
guard let value = characteristic.value else { return }
hasSeenNotify = true
consecutiveReadTimeouts = 0
NativeLogger.d("BleIoStream", category: "BLE",
"notify \(characteristic.uuid.uuidString)"
+ " bytes=\(value.count)"
+ " data=\(Self.hexString(value, maxBytes: Self.maxLogBytes))")
readLock.lock()
readBuffer.append(value)
readPackets.append(value)
Comment thread
ericgriffin marked this conversation as resolved.
readLock.unlock()
readSemaphore.signal()
}
Expand All @@ -660,29 +703,37 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
guard let notify = notifyCharacteristic, characteristic.uuid == notify.uuid else {
guard subscribedCharacteristics.contains(where: { $0.uuid == characteristic.uuid }) else {
return
}
guard waitingForNotifyEnable else { return }
waitingForNotifyEnable = false

if let error {
let pendingCount = subscribedCharacteristics.filter { !$0.isNotifying }.count
NativeLogger.e("BleIoStream", category: "BLE",
"Failed enabling notify for \(characteristic.uuid.uuidString):"
+ " \(error.localizedDescription)")
+ " \(error.localizedDescription)"
+ " (pending=\(pendingCount))")
waitingForNotifyEnable = false
signalDiscoveryReady()
return
}
if !characteristic.isNotifying {
let pendingCount = subscribedCharacteristics.filter { !$0.isNotifying }.count
NativeLogger.w("BleIoStream", category: "BLE",
"Notify not active for \(characteristic.uuid.uuidString)")
"Notify not active for \(characteristic.uuid.uuidString)"
+ " (pending=\(pendingCount))")
waitingForNotifyEnable = false
signalDiscoveryReady()
return
}
NativeLogger.d("BleIoStream", category: "BLE",
"Notify enabled for \(characteristic.uuid.uuidString)")
isReady = true
signalDiscoveryReady()
if subscribedCharacteristics.allSatisfy(\.isNotifying) {
waitingForNotifyEnable = false
isReady = true
signalDiscoveryReady()
}
Comment thread
ericgriffin marked this conversation as resolved.
}

private func signalDiscoveryReady() {
Expand All @@ -703,6 +754,9 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
if props.contains(.writeWithoutResponse) { writeScore += 4 }
if props.contains(.write) { writeScore += 2 }
if Self.preferredWriteUUIDs.contains(characteristic.uuid) { writeScore += 1000 }
if isLikelyCrestDevice && characteristic.uuid == Self.nordicUartServiceRxCharacteristic {
writeScore += 1000
}
if bestWrite == nil || writeScore > bestWrite!.score {
bestWrite = (characteristic, writeScore)
}
Expand All @@ -713,6 +767,11 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
if props.contains(.notify) { notifyScore += 4 }
if props.contains(.indicate) { notifyScore += 2 }
if Self.preferredNotifyUUIDs.contains(characteristic.uuid) { notifyScore += 1000 }
if isLikelyCrestDevice &&
characteristic.uuid == Self.nordicUartServiceNotifyCharacteristic
{
notifyScore += 1000
}
if bestNotify == nil || notifyScore > bestNotify!.score {
bestNotify = (characteristic, notifyScore)
}
Expand All @@ -727,6 +786,9 @@ class BleIoStream: NSObject, CBPeripheralDelegate {
if Self.preferredServiceUUIDs.contains(service.uuid) {
serviceScore += 1000
}
if isLikelyCrestDevice && service.uuid == Self.nordicUartServiceUUID {
serviceScore += 1000
}

if let existing = bestCandidate, existing.score >= serviceScore {
return
Expand All @@ -749,23 +811,55 @@ class BleIoStream: NSObject, CBPeripheralDelegate {

writeCharacteristic = candidate.write
notifyCharacteristic = candidate.notify
let serviceCharacteristics = candidate.write.service?.characteristics ?? []
if isLikelyCrestDevice && candidate.write.uuid == Self.nordicUartServiceRxCharacteristic {
subscribedCharacteristics = serviceCharacteristics.filter {
$0.properties.contains(.notify) || $0.properties.contains(.indicate)
}
if subscribedCharacteristics.isEmpty {
let writeService = candidate.write.service?.uuid.uuidString ?? "nil"
let notifyService = candidate.notify.service?.uuid.uuidString ?? "nil"
NativeLogger.w("BleIoStream", category: "BLE",
"NUS write characteristic found but no notify/indicate characteristics"
+ " on its service (writeService=\(writeService));"
+ " falling back to candidate.notify=\(candidate.notify.uuid.uuidString)"
+ " (notifyService=\(notifyService))")
subscribedCharacteristics = [candidate.notify]
}
if candidate.notify.service !== candidate.write.service {
let writeService = candidate.write.service?.uuid.uuidString ?? "nil"
let notifyService = candidate.notify.service?.uuid.uuidString ?? "nil"
NativeLogger.w("BleIoStream", category: "BLE",
"Selected write/notify characteristics from different services"
+ " for Crest/NUS path"
+ " (writeService=\(writeService)"
+ " notifyService=\(notifyService))")
}
} else {
subscribedCharacteristics = [candidate.notify]
}
Comment thread
ericgriffin marked this conversation as resolved.
writeWithoutResponsePreferred = initialWriteWithoutResponsePreference(for: candidate.write)
NativeLogger.d("BleIoStream", category: "BLE",
"Selected write=\(candidate.write.uuid.uuidString)"
+ " (\(Self.propertySummary(candidate.write.properties)))"
+ " notify=\(candidate.notify.uuid.uuidString)"
+ " (\(Self.propertySummary(candidate.notify.properties)))"
+ " (score=\(candidate.score))")
NativeLogger.d("BleIoStream", category: "BLE",
"Subscribed characteristics:"
+ " \(subscribedCharacteristics.map { $0.uuid.uuidString }.joined(separator: ","))")
NativeLogger.d("BleIoStream", category: "BLE",
"Initial write mode preference:"
+ " \(writeWithoutResponsePreferred ? "withoutResponse" : "withResponse")")
if candidate.notify.isNotifying {
if !subscribedCharacteristics.isEmpty && subscribedCharacteristics.allSatisfy(\.isNotifying) {
isReady = true
signalDiscoveryReady()
return
}
waitingForNotifyEnable = true
peripheral.setNotifyValue(true, for: candidate.notify)
for characteristic in subscribedCharacteristics where !characteristic.isNotifying {
peripheral.setNotifyValue(true, for: characteristic)
}
}
}

Expand Down
Loading