@mentra/bluetooth-sdk 3.2.1-beta.315 → 3.2.1-beta.354
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +67 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +64 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +73 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +257 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +225 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +168 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +557 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +308 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +270 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +194 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +199 -0
- package/build/BluetoothSdk.types.d.ts +11 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +5 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +23 -10
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +4 -0
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/G2.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +66 -3
- package/ios/Source/sgcs/Nimo.swift +182 -371
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +219 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +150 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +112 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +161 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +11 -0
- package/src/_private/BluetoothSdkModule.ts +6 -0
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
import AVFoundation
|
|
10
|
-
import Compression
|
|
11
10
|
import CoreBluetooth
|
|
12
11
|
import Foundation
|
|
13
|
-
import CoreGraphics
|
|
14
12
|
|
|
15
13
|
// MARK: - Nimo BLE Constants
|
|
16
14
|
|
|
@@ -23,7 +21,7 @@ enum NimoBLE {
|
|
|
23
21
|
static let CHAR_MIC = CBUUID(string: "00002025-0000-1000-8000-00805F9B34FB")
|
|
24
22
|
|
|
25
23
|
static let NAME_PREFIX = "nimo"
|
|
26
|
-
|
|
24
|
+
/// iOS ANCS side-channel devices advertise "<name>_ble" — never the data channel.
|
|
27
25
|
static let BLE_NAME_SUFFIX = "_ble"
|
|
28
26
|
|
|
29
27
|
static let CHUNK_SIZE = 501
|
|
@@ -31,6 +29,7 @@ enum NimoBLE {
|
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
// MARK: - Nimo Protocol Constants
|
|
32
|
+
|
|
34
33
|
// Byte values follow the glasses firmware protocol and must not be changed.
|
|
35
34
|
|
|
36
35
|
enum NimoProtocol {
|
|
@@ -69,7 +68,7 @@ enum NimoProtocol {
|
|
|
69
68
|
static let CTRL_QUIT_APP = 0x03
|
|
70
69
|
static let CTRL_UPDATE_CONTENT = 0x04
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
/// notification keys
|
|
73
72
|
static let NOTIFICATION_SEND = 0x01
|
|
74
73
|
|
|
75
74
|
// report keys (cmd 0x06)
|
|
@@ -108,7 +107,7 @@ enum NimoProtocol {
|
|
|
108
107
|
static let APP_ID_PROMPTER = 0x06
|
|
109
108
|
static let APP_ID_AI_TALK = 0x07
|
|
110
109
|
|
|
111
|
-
|
|
110
|
+
/// enterApp modes
|
|
112
111
|
static let APP_MODE_STANDALONE = 0x00
|
|
113
112
|
|
|
114
113
|
// widget resTypes
|
|
@@ -126,7 +125,7 @@ enum NimoProtocol {
|
|
|
126
125
|
static let NAV_LARGE_MAP_WIDTH = 452
|
|
127
126
|
static let NAV_LARGE_MAP_HEIGHT = 170
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
/// phone types
|
|
130
129
|
static let PHONE_TYPE_IOS = 0x01
|
|
131
130
|
|
|
132
131
|
// image header
|
|
@@ -570,6 +569,9 @@ actor NimoReconnectionManager {
|
|
|
570
569
|
class Nimo: NSObject, SGCManager {
|
|
571
570
|
var type: String = DeviceTypes.NIMO
|
|
572
571
|
let hasMic = true
|
|
572
|
+
let showBrightnessConfirmation = false
|
|
573
|
+
let showConnectionConfirmation = false
|
|
574
|
+
let sceneHandoffRequiresClear = false
|
|
573
575
|
|
|
574
576
|
private static let _bluetoothQueue = DispatchQueue(label: "BluetoothNimo", qos: .userInitiated)
|
|
575
577
|
|
|
@@ -578,19 +580,10 @@ class Nimo: NSObject, SGCManager {
|
|
|
578
580
|
static let ackTimeoutSeconds: TimeInterval = 5
|
|
579
581
|
static let pairingTimeoutSeconds: TimeInterval = 15
|
|
580
582
|
static let batteryPollSeconds: TimeInterval = 30
|
|
581
|
-
|
|
582
|
-
static let writeWatchdogSeconds: TimeInterval = 1
|
|
583
|
-
// Fall back to the other ear if the preferred one goes quiet for this long.
|
|
583
|
+
/// Fall back to the other ear if the preferred one goes quiet for this long.
|
|
584
584
|
static let micSideFallbackSeconds: TimeInterval = 2
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
-
// The text surface every sendTextWall renders into. The ASR note view (appId 0x04,
|
|
588
|
-
// note text resId 0x00) is a plain text page that renders pushed text directly;
|
|
589
|
-
// Prompter (0x06) was tried first but did not display pushed text on hardware.
|
|
590
|
-
private let textAppId = NimoProtocol.APP_ID_ASR_NOTE
|
|
591
|
-
private let textLayoutId = 0
|
|
592
|
-
private let textResId = 0
|
|
593
|
-
|
|
594
587
|
// BLE
|
|
595
588
|
private var centralManager: CBCentralManager?
|
|
596
589
|
private var peripheral: CBPeripheral?
|
|
@@ -599,7 +592,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
599
592
|
private var micChar: CBCharacteristic?
|
|
600
593
|
private var isDisconnecting = false
|
|
601
594
|
|
|
602
|
-
|
|
595
|
+
/// Device search
|
|
603
596
|
private var DEVICE_SEARCH_ID = "NOT_SET"
|
|
604
597
|
|
|
605
598
|
private var lastDeviceName: String? {
|
|
@@ -615,7 +608,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
615
608
|
private let reconnectionManager = NimoReconnectionManager()
|
|
616
609
|
private let receiveAssembler = NimoReceiveAssembler()
|
|
617
610
|
|
|
618
|
-
|
|
611
|
+
/// Handshake
|
|
619
612
|
private enum HandshakeState {
|
|
620
613
|
case idle
|
|
621
614
|
case awaitingTws
|
|
@@ -625,10 +618,12 @@ class Nimo: NSObject, SGCManager {
|
|
|
625
618
|
|
|
626
619
|
private var handshakeState = HandshakeState.idle
|
|
627
620
|
private var twsConnected = false
|
|
621
|
+
private var peerCompanionReady: Bool?
|
|
622
|
+
private var setTimeAttempts = 0
|
|
628
623
|
private var twsTimeoutItem: DispatchWorkItem?
|
|
629
624
|
private var pairingTimeoutItem: DispatchWorkItem?
|
|
630
625
|
|
|
631
|
-
|
|
626
|
+
/// Pending acks keyed by (cmd << 8) | key
|
|
632
627
|
private struct PendingAck {
|
|
633
628
|
let onResult: (Bool) -> Void
|
|
634
629
|
let timeoutItem: DispatchWorkItem
|
|
@@ -636,26 +631,36 @@ class Nimo: NSObject, SGCManager {
|
|
|
636
631
|
|
|
637
632
|
private var pendingAcks: [Int: PendingAck] = [:]
|
|
638
633
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
634
|
+
private static func scheduler(_ queue: DispatchQueue) -> NimoScheduler {
|
|
635
|
+
{ delay, task in
|
|
636
|
+
let item = DispatchWorkItem(block: task)
|
|
637
|
+
queue.asyncAfter(deadline: .now() + delay, execute: item)
|
|
638
|
+
return { item.cancel() }
|
|
639
|
+
}
|
|
644
640
|
}
|
|
645
641
|
|
|
646
|
-
private
|
|
647
|
-
private var
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
642
|
+
private let encoderQueue = DispatchQueue(label: "NimoCanvasEncoder", qos: .userInitiated)
|
|
643
|
+
private lazy var canvasEncoder = NimoCanvasEncoder(
|
|
644
|
+
main: Self.scheduler(.main), worker: Self.scheduler(encoderQueue),
|
|
645
|
+
failure: { Bridge.log("NIMO: canvas encoding failed; retaining last valid scene: \($0)") }
|
|
646
|
+
)
|
|
647
|
+
private lazy var writes = NimoWriteQueue<CBCharacteristic>(
|
|
648
|
+
schedule: Self.scheduler(.main), write: { [weak self] characteristic, bytes in
|
|
649
|
+
guard let self, let peripheral = self.peripheral, peripheral.state == .connected,
|
|
650
|
+
bytes.count <= peripheral.maximumWriteValueLength(for: .withResponse) else { return false }
|
|
651
|
+
peripheral.writeValue(bytes, for: characteristic, type: .withResponse)
|
|
652
|
+
return true
|
|
653
|
+
}, failure: { [weak self] in self?.abortTransport($0) }
|
|
654
|
+
)
|
|
655
|
+
private lazy var canvas = NimoCanvasCoordinator(
|
|
656
|
+
schedule: Self.scheduler(.main),
|
|
657
|
+
writeCapacity: { [weak self] in min(512, self?.peripheral?.maximumWriteValueLength(for: .withResponse) ?? 20) },
|
|
658
|
+
enqueue: { [weak self] frames, started, completed in
|
|
659
|
+
self?.enqueueFrames(frames, finalStarted: started, completed: completed) ?? false
|
|
660
|
+
}, reconnect: { [weak self] in self?.abortTransport($0) },
|
|
661
|
+
rejected: { Bridge.log("NIMO: canvas rejected status=\($0); not a render ACK") }
|
|
662
|
+
)
|
|
651
663
|
private var batteryPollTimer: Timer?
|
|
652
|
-
private var textQueueTimer: Timer?
|
|
653
|
-
|
|
654
|
-
// Text rendering
|
|
655
|
-
private var pendingText: String?
|
|
656
|
-
private var textAppEntered = false
|
|
657
|
-
private var navAppEntered = false
|
|
658
|
-
private var currentGlassesAppId = -1
|
|
659
664
|
|
|
660
665
|
// Battery
|
|
661
666
|
private var lastBatteryLevel = -1
|
|
@@ -804,8 +809,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
804
809
|
}
|
|
805
810
|
|
|
806
811
|
func clearDisplay() {
|
|
807
|
-
|
|
808
|
-
pendingText = " "
|
|
812
|
+
encodeCanvas(scope: "legacy") { try NimoCanvasCodec.replace([]) }
|
|
809
813
|
}
|
|
810
814
|
|
|
811
815
|
func sendText(_ text: String) async {
|
|
@@ -813,92 +817,61 @@ class Nimo: NSObject, SGCManager {
|
|
|
813
817
|
}
|
|
814
818
|
|
|
815
819
|
func sendTextWall(_ text: String) async {
|
|
816
|
-
|
|
817
|
-
|
|
820
|
+
encodeCanvas(scope: "legacy") {
|
|
821
|
+
try NimoCanvasCodec.replace(NimoCanvasCodec.text(text, 0, 0, 500, 220, border: 0, radius: 0))
|
|
822
|
+
}
|
|
818
823
|
}
|
|
819
824
|
|
|
820
825
|
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
821
826
|
await sendTextWall(top + "\n\n" + bottom)
|
|
822
827
|
}
|
|
823
828
|
|
|
824
|
-
func sendPositionedText(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
guard let imageData = Data(base64Encoded: base64ImageData),
|
|
849
|
-
let image = SdkImage(data: imageData)
|
|
850
|
-
else {
|
|
851
|
-
Bridge.log("NIMO: displayBitmap — could not decode image")
|
|
829
|
+
func sendPositionedText(_ text: String, x: Int32, y: Int32, width: Int32, height: Int32,
|
|
830
|
+
borderWidth: Int32, borderRadius: Int32) async
|
|
831
|
+
{
|
|
832
|
+
encodeCanvas(scope: "legacy") {
|
|
833
|
+
try NimoCanvasCodec.replace(NimoCanvasCodec.text(text, Int(x), Int(y), Int(width), Int(height),
|
|
834
|
+
border: Int(borderWidth), radius: Int(borderRadius)))
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
func displayBitmap(base64ImageData: String, x: Int32?, y: Int32?, width: Int32?, height: Int32?) async -> Bool {
|
|
839
|
+
do {
|
|
840
|
+
let left = Int(x ?? 0), top = Int(y ?? 0)
|
|
841
|
+
let w = width.map(Int.init) ?? (NimoCanvasCodec.width - left)
|
|
842
|
+
let h = height.map(Int.init) ?? (NimoCanvasCodec.height - top)
|
|
843
|
+
try NimoCanvasCodec.region(left, top, w, h)
|
|
844
|
+
// True acknowledges bounded local validation, never visible rendering.
|
|
845
|
+
let source = try NimoCanvasImage.source(base64ImageData)
|
|
846
|
+
encodeCanvas(scope: "legacy") {
|
|
847
|
+
try NimoCanvasCodec.replace([NimoCanvasCodec.bitmap(left, top, w, h,
|
|
848
|
+
gray: NimoCanvasImage.grayscale(source, width: w, height: h))])
|
|
849
|
+
}
|
|
850
|
+
return true
|
|
851
|
+
} catch {
|
|
852
|
+
Bridge.log("NIMO: displayBitmap failed: \(error)")
|
|
852
853
|
return false
|
|
853
854
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
func applySceneFrame(_ frame: SceneFrame) async {
|
|
858
|
+
encodeCanvas(scope: "\(frame.appId):\(frame.epoch)", force: frame.replay) {
|
|
859
|
+
try NimoCanvasCodec.scene(frame.elements) { try NimoCanvasImage.decode($0, width: $1, height: $2) }
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
func clearSceneElements(_: [String]) async {
|
|
864
|
+
clearDisplay()
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private func encodeCanvas(scope: String, force: Bool = false, encode: @escaping () throws -> Data) {
|
|
868
|
+
canvasEncoder.submit(encode: encode) { [weak self] bytes in
|
|
869
|
+
self?.canvas.offer(bytes, scope: scope, force: force)
|
|
868
870
|
}
|
|
869
|
-
let content =
|
|
870
|
-
NimoFrameCodec.imageHeader(
|
|
871
|
-
width: targetWidth,
|
|
872
|
-
height: targetHeight,
|
|
873
|
-
formatBpp: NimoProtocol.FORMAT_2BPP,
|
|
874
|
-
compression: compression,
|
|
875
|
-
originalSize: packed.count,
|
|
876
|
-
compressedSize: compression == NimoProtocol.COMPRESSION_NONE ? 0 : payload.count
|
|
877
|
-
) + payload
|
|
878
|
-
let frames = NimoFrameCodec.updateContentFrames(
|
|
879
|
-
appId: NimoProtocol.APP_ID_NAV,
|
|
880
|
-
layoutId: 0,
|
|
881
|
-
resId: NimoProtocol.NAV_RES_LARGE_MAP,
|
|
882
|
-
resType: NimoProtocol.WIDGET_PICTURE,
|
|
883
|
-
content: content
|
|
884
|
-
)
|
|
885
|
-
enqueueFrames(frames)
|
|
886
|
-
return true
|
|
887
871
|
}
|
|
888
872
|
|
|
889
873
|
func showDashboard() {
|
|
890
|
-
|
|
891
|
-
textAppEntered = false
|
|
892
|
-
navAppEntered = false
|
|
893
|
-
sendFrame(
|
|
894
|
-
NimoFrameCodec.encodeFrame(
|
|
895
|
-
cmd: NimoProtocol.CMD_CONTROL_INSTRUCTION,
|
|
896
|
-
key: NimoProtocol.CTRL_ENTER_APP,
|
|
897
|
-
payload: Data([
|
|
898
|
-
UInt8(NimoProtocol.APP_ID_DASHBOARD), UInt8(NimoProtocol.APP_MODE_STANDALONE),
|
|
899
|
-
])
|
|
900
|
-
)
|
|
901
|
-
)
|
|
874
|
+
exit()
|
|
902
875
|
}
|
|
903
876
|
|
|
904
877
|
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
@@ -963,17 +936,8 @@ class Nimo: NSObject, SGCManager {
|
|
|
963
936
|
}
|
|
964
937
|
|
|
965
938
|
func exit() {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
textAppEntered = false
|
|
969
|
-
navAppEntered = false
|
|
970
|
-
sendFrame(
|
|
971
|
-
NimoFrameCodec.encodeFrame(
|
|
972
|
-
cmd: NimoProtocol.CMD_CONTROL_INSTRUCTION,
|
|
973
|
-
key: NimoProtocol.CTRL_QUIT_APP,
|
|
974
|
-
payload: Data([UInt8(appId & 0xFF)])
|
|
975
|
-
)
|
|
976
|
-
)
|
|
939
|
+
canvasEncoder.invalidate()
|
|
940
|
+
canvas.exit()
|
|
977
941
|
}
|
|
978
942
|
|
|
979
943
|
func sendShutdown() {
|
|
@@ -1020,7 +984,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1020
984
|
if bytes.count > maxLen {
|
|
1021
985
|
// Truncate on a UTF-8 boundary
|
|
1022
986
|
var end = maxLen
|
|
1023
|
-
while end > 0, bytes[end] & 0xC0 == 0x80 {
|
|
987
|
+
while end > 0, bytes[end] & 0xC0 == 0x80 {
|
|
988
|
+
end -= 1
|
|
989
|
+
}
|
|
1024
990
|
bytes = bytes.subdata(in: 0 ..< end)
|
|
1025
991
|
}
|
|
1026
992
|
dest.replaceSubrange(offset ..< offset + bytes.count, with: bytes)
|
|
@@ -1070,7 +1036,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1070
1036
|
func sendWifiCredentials(_: String, _: String) {}
|
|
1071
1037
|
func forgetWifiNetwork(_: String) {}
|
|
1072
1038
|
func sendHotspotState(_: Bool) {}
|
|
1073
|
-
func sendOtaStart(otaVersionUrl: String?) {}
|
|
1039
|
+
func sendOtaStart(otaVersionUrl _: String?) {}
|
|
1074
1040
|
func sendOtaQueryStatus() {}
|
|
1075
1041
|
|
|
1076
1042
|
// MARK: - SGCManager: User Context / Gallery / Version
|
|
@@ -1163,44 +1129,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1163
1129
|
|
|
1164
1130
|
// MARK: - Write Queue
|
|
1165
1131
|
|
|
1166
|
-
private func
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
guard let peripheral else {
|
|
1170
|
-
writeQueue.removeAll()
|
|
1171
|
-
return
|
|
1172
|
-
}
|
|
1173
|
-
let item = writeQueue.removeFirst()
|
|
1174
|
-
writeInFlight = true
|
|
1175
|
-
peripheral.writeValue(item.bytes, for: item.char, type: .withResponse)
|
|
1176
|
-
|
|
1177
|
-
// Watchdog: if the write callback never arrives, unblock the queue.
|
|
1178
|
-
writeWatchdogItem?.cancel()
|
|
1179
|
-
let watchdog = DispatchWorkItem { [weak self] in
|
|
1180
|
-
guard let self else { return }
|
|
1181
|
-
if self.writeInFlight {
|
|
1182
|
-
Bridge.log("NIMO: write watchdog fired — forcing queue drain")
|
|
1183
|
-
self.writeInFlight = false
|
|
1184
|
-
self.drainWriteQueue()
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
writeWatchdogItem = watchdog
|
|
1188
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + Const.writeWatchdogSeconds, execute: watchdog)
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
private func onWriteCompleted() {
|
|
1192
|
-
DispatchQueue.main.asyncAfter(
|
|
1193
|
-
deadline: .now() + .milliseconds(NimoBLE.INTER_FRAME_DELAY_MS)
|
|
1194
|
-
) { [weak self] in
|
|
1195
|
-
guard let self else { return }
|
|
1196
|
-
self.writeInFlight = false
|
|
1197
|
-
self.drainWriteQueue()
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
private func enqueueWrite(_ char: CBCharacteristic, _ bytes: Data) {
|
|
1202
|
-
writeQueue.append(QueuedWrite(char: char, bytes: bytes))
|
|
1203
|
-
drainWriteQueue()
|
|
1132
|
+
private func enqueueWrite(_ characteristic: CBCharacteristic, _ bytes: Data) {
|
|
1133
|
+
guard let peripheral else { return }
|
|
1134
|
+
writes.enqueue(peripheral, characteristic: characteristic, frames: [bytes])
|
|
1204
1135
|
}
|
|
1205
1136
|
|
|
1206
1137
|
private func sendFrame(_ frame: Data) {
|
|
@@ -1208,12 +1139,18 @@ class Nimo: NSObject, SGCManager {
|
|
|
1208
1139
|
enqueueWrite(txChar, frame)
|
|
1209
1140
|
}
|
|
1210
1141
|
|
|
1211
|
-
private func enqueueFrames(_ frames: [Data])
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1142
|
+
private func enqueueFrames(_ frames: [Data], finalStarted: @escaping () -> Void,
|
|
1143
|
+
completed: @escaping () -> Void) -> Bool
|
|
1144
|
+
{
|
|
1145
|
+
guard let peripheral, let txChar else { return false }
|
|
1146
|
+
return writes.enqueue(peripheral, characteristic: txChar, frames: frames,
|
|
1147
|
+
finalStarted: finalStarted, completed: completed)
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private func abortTransport(_ reason: String) {
|
|
1151
|
+
Bridge.log("NIMO: retiring transport: \(reason)")
|
|
1152
|
+
resetSessionState()
|
|
1153
|
+
if let peripheral { centralManager?.cancelPeripheralConnection(peripheral) }
|
|
1217
1154
|
}
|
|
1218
1155
|
|
|
1219
1156
|
// MARK: - Pending ACKs
|
|
@@ -1286,18 +1223,24 @@ class Nimo: NSObject, SGCManager {
|
|
|
1286
1223
|
cancelTwsTimeout()
|
|
1287
1224
|
handshakeState = .awaitingTimeAck
|
|
1288
1225
|
Bridge.log("NIMO: TWS OK — sending setTime (awaiting ACK)")
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1226
|
+
setTimeAttempts = 0
|
|
1227
|
+
attemptSetTime()
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
private func attemptSetTime() {
|
|
1231
|
+
guard handshakeState == .awaitingTimeAck, let expected = peripheral else { return }
|
|
1232
|
+
setTimeAttempts += 1
|
|
1233
|
+
sendAwaitingAck(cmd: NimoProtocol.CMD_SET_PARAMETER, key: NimoProtocol.SET_TIME,
|
|
1234
|
+
payload: NimoFrameCodec.encodeDeviceTime())
|
|
1235
|
+
{ [weak self] ok in
|
|
1236
|
+
guard let self, self.peripheral === expected, self.handshakeState == .awaitingTimeAck else { return }
|
|
1237
|
+
if ok { self.finishHandshake() }
|
|
1238
|
+
else if self.setTimeAttempts < 3 {
|
|
1239
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
1240
|
+
guard let self, self.peripheral === expected else { return }
|
|
1241
|
+
self.attemptSetTime()
|
|
1242
|
+
}
|
|
1243
|
+
} else { self.handshakeFailed() }
|
|
1301
1244
|
}
|
|
1302
1245
|
}
|
|
1303
1246
|
|
|
@@ -1321,6 +1264,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1321
1264
|
DeviceStore.shared.apply("glasses", "fullyBooted", true)
|
|
1322
1265
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.CONNECTED)
|
|
1323
1266
|
startTimers()
|
|
1267
|
+
canvas.readiness(twsConnected && peerCompanionReady == true)
|
|
1324
1268
|
}
|
|
1325
1269
|
|
|
1326
1270
|
private func handshakeFailed() {
|
|
@@ -1333,16 +1277,13 @@ class Nimo: NSObject, SGCManager {
|
|
|
1333
1277
|
|
|
1334
1278
|
private func resetSessionState() {
|
|
1335
1279
|
handshakeState = .idle
|
|
1280
|
+
cancelTwsTimeout()
|
|
1336
1281
|
twsConnected = false
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
pendingText = nil
|
|
1282
|
+
peerCompanionReady = nil
|
|
1283
|
+
canvasEncoder.invalidate()
|
|
1284
|
+
canvas.disconnected()
|
|
1341
1285
|
receiveAssembler.reset()
|
|
1342
|
-
|
|
1343
|
-
writeInFlight = false
|
|
1344
|
-
writeWatchdogItem?.cancel()
|
|
1345
|
-
writeWatchdogItem = nil
|
|
1286
|
+
writes.reset()
|
|
1346
1287
|
failAllPendingAcks()
|
|
1347
1288
|
stopTimers()
|
|
1348
1289
|
}
|
|
@@ -1366,7 +1307,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1366
1307
|
}
|
|
1367
1308
|
}
|
|
1368
1309
|
|
|
1369
|
-
// MARK: - Timers (battery poll keepalive
|
|
1310
|
+
// MARK: - Timers (battery poll keepalive)
|
|
1370
1311
|
|
|
1371
1312
|
private func startTimers() {
|
|
1372
1313
|
stopTimers()
|
|
@@ -1379,62 +1320,29 @@ class Nimo: NSObject, SGCManager {
|
|
|
1379
1320
|
self?.getBatteryStatus()
|
|
1380
1321
|
}
|
|
1381
1322
|
}
|
|
1382
|
-
textQueueTimer = Timer.scheduledTimer(
|
|
1383
|
-
withTimeInterval: Const.textQueueTickSeconds, repeats: true
|
|
1384
|
-
) { [weak self] _ in
|
|
1385
|
-
Task { @MainActor in
|
|
1386
|
-
self?.drainTextQueue()
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
1323
|
}
|
|
1390
1324
|
|
|
1391
1325
|
private func stopTimers() {
|
|
1392
1326
|
batteryPollTimer?.invalidate()
|
|
1393
1327
|
batteryPollTimer = nil
|
|
1394
|
-
textQueueTimer?.invalidate()
|
|
1395
|
-
textQueueTimer = nil
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// MARK: - Text Rendering
|
|
1399
|
-
|
|
1400
|
-
private func drainTextQueue() {
|
|
1401
|
-
guard let text = pendingText, handshakeState == .ready else { return }
|
|
1402
|
-
pendingText = nil
|
|
1403
|
-
|
|
1404
|
-
// All text (text_wall / double_text_wall / reference_card / positioned_text) renders on
|
|
1405
|
-
// the ASR note page — the only text surface confirmed to render on hardware.
|
|
1406
|
-
if !textAppEntered {
|
|
1407
|
-
sendFrame(
|
|
1408
|
-
NimoFrameCodec.encodeFrame(
|
|
1409
|
-
cmd: NimoProtocol.CMD_CONTROL_INSTRUCTION,
|
|
1410
|
-
key: NimoProtocol.CTRL_ENTER_APP,
|
|
1411
|
-
payload: Data([UInt8(textAppId), UInt8(NimoProtocol.APP_MODE_STANDALONE)])
|
|
1412
|
-
)
|
|
1413
|
-
)
|
|
1414
|
-
// Optimistic; corrected by app-state reports if the glasses refuse/exit.
|
|
1415
|
-
textAppEntered = true
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
let frames = NimoFrameCodec.updateContentFrames(
|
|
1419
|
-
appId: textAppId,
|
|
1420
|
-
layoutId: textLayoutId,
|
|
1421
|
-
resId: textResId,
|
|
1422
|
-
resType: NimoProtocol.WIDGET_TEXT_NEW,
|
|
1423
|
-
content: Data(text.utf8)
|
|
1424
|
-
)
|
|
1425
|
-
enqueueFrames(frames)
|
|
1426
1328
|
}
|
|
1427
1329
|
|
|
1428
1330
|
// MARK: - Incoming Data
|
|
1429
1331
|
|
|
1430
1332
|
private func handleRxPacket(_ packet: Data) {
|
|
1333
|
+
if packet.count >= 10, packet[8] == 7, [1, 3, 4].contains(packet[9]) {
|
|
1334
|
+
if let response = NimoCanvasCodec.response(packet) {
|
|
1335
|
+
canvas.response(key: response.key, payload: response.payload)
|
|
1336
|
+
}
|
|
1337
|
+
return
|
|
1338
|
+
}
|
|
1431
1339
|
receiveAssembler.cleanup()
|
|
1432
1340
|
for frame in receiveAssembler.ingest(packet) {
|
|
1433
1341
|
guard let decoded = NimoFrameCodec.decode(frame),
|
|
1434
1342
|
let cmd = decoded.cmd, let key = decoded.key
|
|
1435
1343
|
else { continue }
|
|
1436
1344
|
if cmd == NimoProtocol.CMD_INSTRUCTION_REPORT {
|
|
1437
|
-
handleReport(key: key, data: decoded.data ?? Data())
|
|
1345
|
+
if decoded.statusCode == 0 { handleReport(key: key, data: decoded.data ?? Data()) }
|
|
1438
1346
|
} else {
|
|
1439
1347
|
handleResponse(
|
|
1440
1348
|
cmd: cmd, key: key, statusCode: decoded.statusCode ?? 1,
|
|
@@ -1457,13 +1365,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1457
1365
|
Bridge.log("NIMO: app state report appId=\(appId) phase=\(phase)")
|
|
1458
1366
|
switch phase {
|
|
1459
1367
|
case NimoProtocol.STATE_ENTER:
|
|
1460
|
-
|
|
1461
|
-
if appId != textAppId { textAppEntered = false }
|
|
1462
|
-
if appId != NimoProtocol.APP_ID_NAV { navAppEntered = false }
|
|
1368
|
+
if canvas.nativeApp(appId, entered: true) { canvasEncoder.invalidate() }
|
|
1463
1369
|
case NimoProtocol.STATE_EXIT:
|
|
1464
|
-
if appId
|
|
1465
|
-
if appId == textAppId { textAppEntered = false }
|
|
1466
|
-
if appId == NimoProtocol.APP_ID_NAV { navAppEntered = false }
|
|
1370
|
+
if canvas.nativeApp(appId, entered: false) { canvasEncoder.invalidate() }
|
|
1467
1371
|
default:
|
|
1468
1372
|
break
|
|
1469
1373
|
}
|
|
@@ -1490,7 +1394,11 @@ class Nimo: NSObject, SGCManager {
|
|
|
1490
1394
|
case NimoProtocol.BUSINESS_HEARTBEAT:
|
|
1491
1395
|
// [leftMv(2)][rightMv(2)][btSysStatus(4)][twsStatus(1)][slaveGatt(1)]
|
|
1492
1396
|
if v.count >= 10 {
|
|
1397
|
+
peerCompanionReady = v[9] != 0
|
|
1493
1398
|
onTwsState(Int(v[8]) >= 1)
|
|
1399
|
+
if handshakeState == .ready {
|
|
1400
|
+
canvas.readiness(twsConnected && peerCompanionReady == true, confirmed: true)
|
|
1401
|
+
}
|
|
1494
1402
|
}
|
|
1495
1403
|
case NimoProtocol.BUSINESS_BATTERY:
|
|
1496
1404
|
if v.count >= 4 {
|
|
@@ -1512,6 +1420,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1512
1420
|
if !connected, handshakeState == .ready {
|
|
1513
1421
|
Bridge.log("NIMO: TWS service dropped mid-session (arm removed/off?)")
|
|
1514
1422
|
}
|
|
1423
|
+
if handshakeState == .ready { canvas.readiness(connected && peerCompanionReady == true) }
|
|
1515
1424
|
}
|
|
1516
1425
|
|
|
1517
1426
|
private func handleInputEvent(_ code: Int) {
|
|
@@ -1573,7 +1482,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1573
1482
|
}
|
|
1574
1483
|
case NimoProtocol.GET_VERSION_DETAIL:
|
|
1575
1484
|
var end = bytes.count
|
|
1576
|
-
while end > 0, bytes[end - 1] == 0 {
|
|
1485
|
+
while end > 0, bytes[end - 1] == 0 {
|
|
1486
|
+
end -= 1
|
|
1487
|
+
}
|
|
1577
1488
|
firmwareVersionDetail =
|
|
1578
1489
|
String(bytes: bytes[0 ..< end], encoding: .utf8)?
|
|
1579
1490
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
@@ -1636,121 +1547,6 @@ class Nimo: NSObject, SGCManager {
|
|
|
1636
1547
|
}
|
|
1637
1548
|
}
|
|
1638
1549
|
}
|
|
1639
|
-
|
|
1640
|
-
// MARK: - Bitmap Helpers
|
|
1641
|
-
|
|
1642
|
-
/// Aspect-fit the image into `width`x`height` on black, then convert to L8 grayscale.
|
|
1643
|
-
private func bitmapToGrayscale(_ image: SdkImage, width: Int, height: Int) -> Data? {
|
|
1644
|
-
guard let cgImage = image.cgImage else { return nil }
|
|
1645
|
-
let colorSpace = CGColorSpaceCreateDeviceGray()
|
|
1646
|
-
guard let context = CGContext(
|
|
1647
|
-
data: nil,
|
|
1648
|
-
width: width,
|
|
1649
|
-
height: height,
|
|
1650
|
-
bitsPerComponent: 8,
|
|
1651
|
-
bytesPerRow: width,
|
|
1652
|
-
space: colorSpace,
|
|
1653
|
-
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
1654
|
-
) else { return nil }
|
|
1655
|
-
|
|
1656
|
-
context.setFillColor(gray: 0, alpha: 1)
|
|
1657
|
-
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
|
1658
|
-
let scale = min(CGFloat(width) / CGFloat(cgImage.width), CGFloat(height) / CGFloat(cgImage.height))
|
|
1659
|
-
let dw = CGFloat(cgImage.width) * scale
|
|
1660
|
-
let dh = CGFloat(cgImage.height) * scale
|
|
1661
|
-
let rect = CGRect(
|
|
1662
|
-
x: (CGFloat(width) - dw) / 2, y: (CGFloat(height) - dh) / 2, width: dw, height: dh
|
|
1663
|
-
)
|
|
1664
|
-
context.interpolationQuality = .medium
|
|
1665
|
-
context.draw(cgImage, in: rect)
|
|
1666
|
-
|
|
1667
|
-
guard let buffer = context.data else { return nil }
|
|
1668
|
-
// CGContext rows are top-down here; copy row by row in case bytesPerRow != width.
|
|
1669
|
-
// Invert luminance (255 - value): MentraOS app bitmaps follow the platform convention
|
|
1670
|
-
// of dark content on a light background, but Nimo's additive display lights up high
|
|
1671
|
-
// 2bpp values (3 = white). Without inverting, the background lights the whole lens and
|
|
1672
|
-
// the content stays dark — the inverted look. Flipping renders content lit on a dark lens.
|
|
1673
|
-
var gray = Data(capacity: width * height)
|
|
1674
|
-
let bytesPerRow = context.bytesPerRow
|
|
1675
|
-
let base = buffer.assumingMemoryBound(to: UInt8.self)
|
|
1676
|
-
for row in 0 ..< height {
|
|
1677
|
-
let rowOffset = row * bytesPerRow
|
|
1678
|
-
for col in 0 ..< width {
|
|
1679
|
-
gray.append(255 - base[rowOffset + col])
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return gray
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
/// L8 → 2bpp: thresholds 0x40/0x80/0xC0, 4 pixels per byte, MSB first.
|
|
1686
|
-
private func packL8To2bpp(_ l8: Data) -> Data {
|
|
1687
|
-
let bytes = [UInt8](l8)
|
|
1688
|
-
let outBytes = (bytes.count + 3) >> 2
|
|
1689
|
-
var dst = Data(count: outBytes)
|
|
1690
|
-
for i in 0 ..< outBytes {
|
|
1691
|
-
var packed: UInt8 = 0
|
|
1692
|
-
for j in 0 ..< 4 {
|
|
1693
|
-
let idx = i * 4 + j
|
|
1694
|
-
var value: UInt8 = 0
|
|
1695
|
-
if idx < bytes.count {
|
|
1696
|
-
let px = bytes[idx]
|
|
1697
|
-
if px < 0x40 {
|
|
1698
|
-
value = 0
|
|
1699
|
-
} else if px < 0x80 {
|
|
1700
|
-
value = 1
|
|
1701
|
-
} else if px < 0xC0 {
|
|
1702
|
-
value = 2
|
|
1703
|
-
} else {
|
|
1704
|
-
value = 3
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
packed |= (value & 0x03) << UInt8(6 - 2 * j)
|
|
1708
|
-
}
|
|
1709
|
-
dst[i] = packed
|
|
1710
|
-
}
|
|
1711
|
-
return dst
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
/// zlib-compress (RFC1950); falls back to uncompressed when not smaller.
|
|
1715
|
-
/// Apple's Compression framework emits raw deflate, so the 2-byte zlib header
|
|
1716
|
-
/// and adler32 trailer the firmware expects are added manually.
|
|
1717
|
-
private func compressAdaptive(_ data: Data) -> (Data, Int) {
|
|
1718
|
-
guard !data.isEmpty, let compressed = zlibCompress(data), compressed.count < data.count
|
|
1719
|
-
else { return (data, NimoProtocol.COMPRESSION_NONE) }
|
|
1720
|
-
return (compressed, NimoProtocol.COMPRESSION_ZLIB)
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
private func zlibCompress(_ data: Data) -> Data? {
|
|
1724
|
-
let dstCapacity = data.count + data.count / 16 + 256
|
|
1725
|
-
var deflated = Data(count: dstCapacity)
|
|
1726
|
-
let written = deflated.withUnsafeMutableBytes { (dst: UnsafeMutableRawBufferPointer) -> Int in
|
|
1727
|
-
data.withUnsafeBytes { (src: UnsafeRawBufferPointer) -> Int in
|
|
1728
|
-
compression_encode_buffer(
|
|
1729
|
-
dst.bindMemory(to: UInt8.self).baseAddress!, dstCapacity,
|
|
1730
|
-
src.bindMemory(to: UInt8.self).baseAddress!, data.count,
|
|
1731
|
-
nil, COMPRESSION_ZLIB
|
|
1732
|
-
)
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
guard written > 0 else { return nil }
|
|
1736
|
-
deflated.removeSubrange(written ..< deflated.count)
|
|
1737
|
-
|
|
1738
|
-
var out = Data([0x78, 0x9C]) // zlib header, default compression
|
|
1739
|
-
out.append(deflated)
|
|
1740
|
-
// adler32 over the uncompressed data, big-endian
|
|
1741
|
-
var a: UInt32 = 1
|
|
1742
|
-
var b: UInt32 = 0
|
|
1743
|
-
for byte in data {
|
|
1744
|
-
a = (a + UInt32(byte)) % 65521
|
|
1745
|
-
b = (b + a) % 65521
|
|
1746
|
-
}
|
|
1747
|
-
let adler = (b << 16) | a
|
|
1748
|
-
out.append(contentsOf: [
|
|
1749
|
-
UInt8((adler >> 24) & 0xFF), UInt8((adler >> 16) & 0xFF),
|
|
1750
|
-
UInt8((adler >> 8) & 0xFF), UInt8(adler & 0xFF),
|
|
1751
|
-
])
|
|
1752
|
-
return out
|
|
1753
|
-
}
|
|
1754
1550
|
}
|
|
1755
1551
|
|
|
1756
1552
|
// MARK: - CBCentralManagerDelegate
|
|
@@ -1775,7 +1571,7 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1775
1571
|
) {
|
|
1776
1572
|
guard
|
|
1777
1573
|
let name = peripheral.name
|
|
1778
|
-
|
|
1574
|
+
?? advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
|
1779
1575
|
else { return }
|
|
1780
1576
|
let rssiValue = rssi.intValue
|
|
1781
1577
|
|
|
@@ -1803,6 +1599,8 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1803
1599
|
DispatchQueue.main.async { [weak self] in
|
|
1804
1600
|
guard let self else { return }
|
|
1805
1601
|
Bridge.log("NIMO: Connected to \(peripheral.name ?? "unknown")")
|
|
1602
|
+
guard self.peripheral === peripheral else { return }
|
|
1603
|
+
self.writes.connected(peripheral)
|
|
1806
1604
|
self.lastDeviceUUID = peripheral.identifier.uuidString
|
|
1807
1605
|
peripheral.discoverServices([NimoBLE.SERVICE_UUID])
|
|
1808
1606
|
}
|
|
@@ -1816,18 +1614,19 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1816
1614
|
Bridge.log(
|
|
1817
1615
|
"NIMO: Failed to connect \(peripheral.name ?? "unknown"): \(error?.localizedDescription ?? "unknown")"
|
|
1818
1616
|
)
|
|
1617
|
+
guard self.peripheral === peripheral else { return }
|
|
1819
1618
|
self.peripheral = nil
|
|
1820
1619
|
self.startReconnectionTimer()
|
|
1821
1620
|
}
|
|
1822
1621
|
}
|
|
1823
1622
|
|
|
1824
1623
|
nonisolated func centralManager(
|
|
1825
|
-
_: CBCentralManager, didDisconnectPeripheral
|
|
1624
|
+
_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?
|
|
1826
1625
|
) {
|
|
1827
1626
|
DispatchQueue.main.async { [weak self] in
|
|
1828
1627
|
guard let self else { return }
|
|
1829
1628
|
Bridge.log("NIMO: Disconnected: \(error?.localizedDescription ?? "clean")")
|
|
1830
|
-
if self.isDisconnecting { return }
|
|
1629
|
+
if self.isDisconnecting || self.peripheral !== peripheral { return }
|
|
1831
1630
|
|
|
1832
1631
|
self.peripheral = nil
|
|
1833
1632
|
self.txChar = nil
|
|
@@ -1846,15 +1645,19 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1846
1645
|
// MARK: - CBPeripheralDelegate
|
|
1847
1646
|
|
|
1848
1647
|
extension Nimo: CBPeripheralDelegate {
|
|
1849
|
-
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices
|
|
1850
|
-
|
|
1851
|
-
|
|
1648
|
+
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
1649
|
+
DispatchQueue.main.async { [weak self] in
|
|
1650
|
+
guard let self, self.peripheral === peripheral else { return }
|
|
1651
|
+
guard error == nil, let service = peripheral.services?.first(where: { $0.uuid == NimoBLE.SERVICE_UUID }) else {
|
|
1652
|
+
self.abortTransport("NIMO service discovery failed")
|
|
1653
|
+
return
|
|
1654
|
+
}
|
|
1852
1655
|
peripheral.discoverCharacteristics(nil, for: service)
|
|
1853
1656
|
}
|
|
1854
1657
|
}
|
|
1855
1658
|
|
|
1856
1659
|
nonisolated func peripheral(
|
|
1857
|
-
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error
|
|
1660
|
+
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?
|
|
1858
1661
|
) {
|
|
1859
1662
|
guard service.uuid == NimoBLE.SERVICE_UUID,
|
|
1860
1663
|
let characteristics = service.characteristics
|
|
@@ -1862,6 +1665,8 @@ extension Nimo: CBPeripheralDelegate {
|
|
|
1862
1665
|
|
|
1863
1666
|
DispatchQueue.main.async { [weak self] in
|
|
1864
1667
|
guard let self else { return }
|
|
1668
|
+
guard self.peripheral === peripheral else { return }
|
|
1669
|
+
guard error == nil else { self.abortTransport("NIMO characteristic discovery failed"); return }
|
|
1865
1670
|
for char in characteristics {
|
|
1866
1671
|
switch char.uuid {
|
|
1867
1672
|
case NimoBLE.CHAR_TX:
|
|
@@ -1876,6 +1681,10 @@ extension Nimo: CBPeripheralDelegate {
|
|
|
1876
1681
|
break
|
|
1877
1682
|
}
|
|
1878
1683
|
}
|
|
1684
|
+
guard self.txChar?.properties.contains(.write) == true, self.rxChar != nil, self.micChar != nil else {
|
|
1685
|
+
self.abortTransport("Required NIMO characteristics unavailable")
|
|
1686
|
+
return
|
|
1687
|
+
}
|
|
1879
1688
|
Bridge.log(
|
|
1880
1689
|
"NIMO: chars tx=\(self.txChar != nil) rx=\(self.rxChar != nil) mic=\(self.micChar != nil)"
|
|
1881
1690
|
)
|
|
@@ -1883,46 +1692,48 @@ extension Nimo: CBPeripheralDelegate {
|
|
|
1883
1692
|
}
|
|
1884
1693
|
|
|
1885
1694
|
nonisolated func peripheral(
|
|
1886
|
-
_: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
1695
|
+
_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
1887
1696
|
error: Error?
|
|
1888
1697
|
) {
|
|
1889
1698
|
DispatchQueue.main.async { [weak self] in
|
|
1890
1699
|
guard let self else { return }
|
|
1700
|
+
guard self.peripheral === peripheral else { return }
|
|
1891
1701
|
if let error {
|
|
1702
|
+
self.abortTransport("Notification subscription failed")
|
|
1892
1703
|
Bridge.log("NIMO: notify enable failed: \(error.localizedDescription)")
|
|
1893
1704
|
return
|
|
1894
1705
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1706
|
+
guard characteristic === self.rxChar || characteristic === self.micChar else { return }
|
|
1707
|
+
guard characteristic.isNotifying else { self.abortTransport("NIMO notifications disabled"); return }
|
|
1708
|
+
if self.rxChar?.isNotifying == true, self.micChar?.isNotifying == true,
|
|
1709
|
+
self.txChar?.properties.contains(.write) == true, self.handshakeState == .idle
|
|
1710
|
+
{
|
|
1897
1711
|
self.startHandshake()
|
|
1898
1712
|
}
|
|
1899
1713
|
}
|
|
1900
1714
|
}
|
|
1901
1715
|
|
|
1902
1716
|
nonisolated func peripheral(
|
|
1903
|
-
_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
1717
|
+
_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
1904
1718
|
) {
|
|
1905
1719
|
guard let data = characteristic.value, error == nil else { return }
|
|
1906
1720
|
let uuid = characteristic.uuid
|
|
1907
1721
|
DispatchQueue.main.async { [weak self] in
|
|
1908
1722
|
guard let self else { return }
|
|
1909
|
-
|
|
1723
|
+
guard self.peripheral === peripheral else { return }
|
|
1724
|
+
if uuid == NimoBLE.CHAR_RX, characteristic === self.rxChar {
|
|
1910
1725
|
self.handleRxPacket(data)
|
|
1911
|
-
} else if uuid == NimoBLE.CHAR_MIC {
|
|
1726
|
+
} else if uuid == NimoBLE.CHAR_MIC, characteristic === self.micChar {
|
|
1912
1727
|
self.handleMicPacket(data)
|
|
1913
1728
|
}
|
|
1914
1729
|
}
|
|
1915
1730
|
}
|
|
1916
1731
|
|
|
1917
1732
|
nonisolated func peripheral(
|
|
1918
|
-
_: CBPeripheral, didWriteValueFor
|
|
1733
|
+
_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?
|
|
1919
1734
|
) {
|
|
1920
1735
|
DispatchQueue.main.async { [weak self] in
|
|
1921
|
-
|
|
1922
|
-
if let error {
|
|
1923
|
-
Bridge.log("NIMO: Write error: \(error.localizedDescription)")
|
|
1924
|
-
}
|
|
1925
|
-
self.onWriteCompleted()
|
|
1736
|
+
self?.writes.written(peripheral, characteristic: characteristic, success: error == nil)
|
|
1926
1737
|
}
|
|
1927
1738
|
}
|
|
1928
1739
|
}
|