@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -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/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- 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/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -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 +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -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/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -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 +233 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -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 +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +16 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- 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/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +272 -197
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +25 -1
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -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
|
|
|
@@ -22,15 +20,12 @@ enum NimoBLE {
|
|
|
22
20
|
static let CHAR_RX = CBUUID(string: "00002022-0000-1000-8000-00805F9B34FB")
|
|
23
21
|
static let CHAR_MIC = CBUUID(string: "00002025-0000-1000-8000-00805F9B34FB")
|
|
24
22
|
|
|
25
|
-
static let NAME_PREFIX = "nimo"
|
|
26
|
-
// iOS ANCS side-channel devices advertise "<name>_ble" — never the data channel.
|
|
27
|
-
static let BLE_NAME_SUFFIX = "_ble"
|
|
28
|
-
|
|
29
23
|
static let CHUNK_SIZE = 501
|
|
30
24
|
static let INTER_FRAME_DELAY_MS = 5
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
// MARK: - Nimo Protocol Constants
|
|
28
|
+
|
|
34
29
|
// Byte values follow the glasses firmware protocol and must not be changed.
|
|
35
30
|
|
|
36
31
|
enum NimoProtocol {
|
|
@@ -45,6 +40,8 @@ enum NimoProtocol {
|
|
|
45
40
|
static let CMD_SET_PARAMETER = 0x03
|
|
46
41
|
static let CMD_INSTRUCTION_REPORT = 0x06
|
|
47
42
|
static let CMD_CONTROL_INSTRUCTION = 0x07
|
|
43
|
+
static let CMD_CONTROL_FACTORY = 0x08
|
|
44
|
+
static let FACTORY_RECOVER = 0x03
|
|
48
45
|
static let CMD_CONTROL_NOTIFICATION = 0x09
|
|
49
46
|
|
|
50
47
|
// get parameter keys
|
|
@@ -63,13 +60,16 @@ enum NimoProtocol {
|
|
|
63
60
|
static let SET_DISPLAY_OFF = 0x0F
|
|
64
61
|
static let SET_PHONE_TYPE = 0x14
|
|
65
62
|
static let SET_HEIGHT_LEVEL = 0x17
|
|
63
|
+
// Dynamic-v1 firmware: 1-based UI locale; 1 selects English.
|
|
64
|
+
static let SET_SYSTEM_LANGUAGE = 0x24
|
|
65
|
+
static let LANGUAGE_ENGLISH: UInt8 = 0x01
|
|
66
66
|
|
|
67
67
|
// control instruction keys
|
|
68
68
|
static let CTRL_ENTER_APP = 0x01
|
|
69
69
|
static let CTRL_QUIT_APP = 0x03
|
|
70
70
|
static let CTRL_UPDATE_CONTENT = 0x04
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/// notification keys
|
|
73
73
|
static let NOTIFICATION_SEND = 0x01
|
|
74
74
|
|
|
75
75
|
// report keys (cmd 0x06)
|
|
@@ -108,7 +108,7 @@ enum NimoProtocol {
|
|
|
108
108
|
static let APP_ID_PROMPTER = 0x06
|
|
109
109
|
static let APP_ID_AI_TALK = 0x07
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
/// enterApp modes
|
|
112
112
|
static let APP_MODE_STANDALONE = 0x00
|
|
113
113
|
|
|
114
114
|
// widget resTypes
|
|
@@ -126,7 +126,7 @@ enum NimoProtocol {
|
|
|
126
126
|
static let NAV_LARGE_MAP_WIDTH = 452
|
|
127
127
|
static let NAV_LARGE_MAP_HEIGHT = 170
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
/// phone types
|
|
130
130
|
static let PHONE_TYPE_IOS = 0x01
|
|
131
131
|
|
|
132
132
|
// image header
|
|
@@ -570,6 +570,9 @@ actor NimoReconnectionManager {
|
|
|
570
570
|
class Nimo: NSObject, SGCManager {
|
|
571
571
|
var type: String = DeviceTypes.NIMO
|
|
572
572
|
let hasMic = true
|
|
573
|
+
let showBrightnessConfirmation = false
|
|
574
|
+
let showConnectionConfirmation = false
|
|
575
|
+
let sceneHandoffRequiresClear = false
|
|
573
576
|
|
|
574
577
|
private static let _bluetoothQueue = DispatchQueue(label: "BluetoothNimo", qos: .userInitiated)
|
|
575
578
|
|
|
@@ -578,19 +581,10 @@ class Nimo: NSObject, SGCManager {
|
|
|
578
581
|
static let ackTimeoutSeconds: TimeInterval = 5
|
|
579
582
|
static let pairingTimeoutSeconds: TimeInterval = 15
|
|
580
583
|
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.
|
|
584
|
+
/// Fall back to the other ear if the preferred one goes quiet for this long.
|
|
584
585
|
static let micSideFallbackSeconds: TimeInterval = 2
|
|
585
586
|
}
|
|
586
587
|
|
|
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
588
|
// BLE
|
|
595
589
|
private var centralManager: CBCentralManager?
|
|
596
590
|
private var peripheral: CBPeripheral?
|
|
@@ -598,8 +592,29 @@ class Nimo: NSObject, SGCManager {
|
|
|
598
592
|
private var rxChar: CBCharacteristic?
|
|
599
593
|
private var micChar: CBCharacteristic?
|
|
600
594
|
private var isDisconnecting = false
|
|
595
|
+
private var scanRequested = false
|
|
596
|
+
private lazy var discovery = NimoDiscovery<CBPeripheral>(
|
|
597
|
+
register: { [weak self] in
|
|
598
|
+
#if os(iOS)
|
|
599
|
+
self?.centralManager?.registerForConnectionEvents(options: [.serviceUUIDs: [NimoBLE.SERVICE_UUID]])
|
|
600
|
+
#endif
|
|
601
|
+
}, connected: { [weak self] in
|
|
602
|
+
self?.centralManager?.retrieveConnectedPeripherals(withServices: [NimoBLE.SERVICE_UUID]) ?? []
|
|
603
|
+
}, scan: { [weak self] in
|
|
604
|
+
self?.centralManager?.scanForPeripherals(
|
|
605
|
+
withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
|
606
|
+
)
|
|
607
|
+
}, stop: { [weak self] in
|
|
608
|
+
self?.centralManager?.stopScan()
|
|
609
|
+
#if os(iOS)
|
|
610
|
+
self?.centralManager?.registerForConnectionEvents(options: [:])
|
|
611
|
+
#endif
|
|
612
|
+
}, name: { $0.name }, found: { [weak self] peripheral, name, rssi in
|
|
613
|
+
self?.onDeviceFound(peripheral, name: name, rssi: rssi)
|
|
614
|
+
}
|
|
615
|
+
)
|
|
601
616
|
|
|
602
|
-
|
|
617
|
+
/// Device search
|
|
603
618
|
private var DEVICE_SEARCH_ID = "NOT_SET"
|
|
604
619
|
|
|
605
620
|
private var lastDeviceName: String? {
|
|
@@ -615,7 +630,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
615
630
|
private let reconnectionManager = NimoReconnectionManager()
|
|
616
631
|
private let receiveAssembler = NimoReceiveAssembler()
|
|
617
632
|
|
|
618
|
-
|
|
633
|
+
/// Handshake
|
|
619
634
|
private enum HandshakeState {
|
|
620
635
|
case idle
|
|
621
636
|
case awaitingTws
|
|
@@ -625,10 +640,12 @@ class Nimo: NSObject, SGCManager {
|
|
|
625
640
|
|
|
626
641
|
private var handshakeState = HandshakeState.idle
|
|
627
642
|
private var twsConnected = false
|
|
643
|
+
private var peerCompanionReady: Bool?
|
|
644
|
+
private var setTimeAttempts = 0
|
|
628
645
|
private var twsTimeoutItem: DispatchWorkItem?
|
|
629
646
|
private var pairingTimeoutItem: DispatchWorkItem?
|
|
630
647
|
|
|
631
|
-
|
|
648
|
+
/// Pending acks keyed by (cmd << 8) | key
|
|
632
649
|
private struct PendingAck {
|
|
633
650
|
let onResult: (Bool) -> Void
|
|
634
651
|
let timeoutItem: DispatchWorkItem
|
|
@@ -636,26 +653,36 @@ class Nimo: NSObject, SGCManager {
|
|
|
636
653
|
|
|
637
654
|
private var pendingAcks: [Int: PendingAck] = [:]
|
|
638
655
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
656
|
+
private static func scheduler(_ queue: DispatchQueue) -> NimoScheduler {
|
|
657
|
+
{ delay, task in
|
|
658
|
+
let item = DispatchWorkItem(block: task)
|
|
659
|
+
queue.asyncAfter(deadline: .now() + delay, execute: item)
|
|
660
|
+
return { item.cancel() }
|
|
661
|
+
}
|
|
644
662
|
}
|
|
645
663
|
|
|
646
|
-
private
|
|
647
|
-
private var
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
664
|
+
private let encoderQueue = DispatchQueue(label: "NimoCanvasEncoder", qos: .userInitiated)
|
|
665
|
+
private lazy var canvasEncoder = NimoCanvasEncoder(
|
|
666
|
+
main: Self.scheduler(.main), worker: Self.scheduler(encoderQueue),
|
|
667
|
+
failure: { Bridge.log("NIMO: canvas encoding failed; retaining last valid scene: \($0)") }
|
|
668
|
+
)
|
|
669
|
+
private lazy var writes = NimoWriteQueue<CBCharacteristic>(
|
|
670
|
+
schedule: Self.scheduler(.main), write: { [weak self] characteristic, bytes in
|
|
671
|
+
guard let self, let peripheral = self.peripheral, peripheral.state == .connected,
|
|
672
|
+
bytes.count <= peripheral.maximumWriteValueLength(for: .withResponse) else { return false }
|
|
673
|
+
peripheral.writeValue(bytes, for: characteristic, type: .withResponse)
|
|
674
|
+
return true
|
|
675
|
+
}, failure: { [weak self] in self?.abortTransport($0) }
|
|
676
|
+
)
|
|
677
|
+
private lazy var canvas = NimoCanvasCoordinator(
|
|
678
|
+
schedule: Self.scheduler(.main),
|
|
679
|
+
writeCapacity: { [weak self] in min(512, self?.peripheral?.maximumWriteValueLength(for: .withResponse) ?? 20) },
|
|
680
|
+
enqueue: { [weak self] frames, started, completed in
|
|
681
|
+
self?.enqueueFrames(frames, finalStarted: started, completed: completed) ?? false
|
|
682
|
+
}, reconnect: { [weak self] in self?.abortTransport($0) },
|
|
683
|
+
rejected: { Bridge.log("NIMO: canvas rejected status=\($0); not a render ACK") }
|
|
684
|
+
)
|
|
651
685
|
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
686
|
|
|
660
687
|
// Battery
|
|
661
688
|
private var lastBatteryLevel = -1
|
|
@@ -675,6 +702,8 @@ class Nimo: NSObject, SGCManager {
|
|
|
675
702
|
func findCompatibleDevices() {
|
|
676
703
|
Bridge.log("NIMO: findCompatibleDevices()")
|
|
677
704
|
DEVICE_SEARCH_ID = "NOT_SET"
|
|
705
|
+
isDisconnecting = false
|
|
706
|
+
scanRequested = true
|
|
678
707
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.SCANNING)
|
|
679
708
|
startScan()
|
|
680
709
|
}
|
|
@@ -682,6 +711,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
682
711
|
func connectById(_ id: String) {
|
|
683
712
|
Bridge.log("NIMO: connectById(\(id))")
|
|
684
713
|
DEVICE_SEARCH_ID = id
|
|
714
|
+
scanRequested = true
|
|
685
715
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.CONNECTING)
|
|
686
716
|
isDisconnecting = false
|
|
687
717
|
startPairingTimeout()
|
|
@@ -689,7 +719,8 @@ class Nimo: NSObject, SGCManager {
|
|
|
689
719
|
}
|
|
690
720
|
|
|
691
721
|
func stopScan() {
|
|
692
|
-
|
|
722
|
+
scanRequested = false
|
|
723
|
+
discovery.cancel()
|
|
693
724
|
}
|
|
694
725
|
|
|
695
726
|
func disconnect() {
|
|
@@ -716,6 +747,27 @@ class Nimo: NSObject, SGCManager {
|
|
|
716
747
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
717
748
|
}
|
|
718
749
|
|
|
750
|
+
/// Explicit Unpair only; passive forget/cleanup must never reset the glasses.
|
|
751
|
+
func resetForUnpair(onResult: @escaping (Bool) -> Void) {
|
|
752
|
+
guard peripheral != nil, txChar != nil, handshakeState == .ready else {
|
|
753
|
+
Bridge.log("NIMO: unpair while offline; remote reset unavailable")
|
|
754
|
+
onResult(false)
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
isDisconnecting = true
|
|
758
|
+
stopTimers()
|
|
759
|
+
Task { await reconnectionManager.stop() }
|
|
760
|
+
canvasEncoder.invalidate()
|
|
761
|
+
canvas.disconnected()
|
|
762
|
+
Bridge.log("NIMO: sending factory reset for explicit Unpair")
|
|
763
|
+
sendAwaitingAck(cmd: NimoProtocol.CMD_CONTROL_FACTORY, key: NimoProtocol.FACTORY_RECOVER,
|
|
764
|
+
payload: Data())
|
|
765
|
+
{ success in
|
|
766
|
+
Bridge.log("NIMO: factory reset acknowledged=\(success)")
|
|
767
|
+
onResult(success)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
719
771
|
func forget() {
|
|
720
772
|
Bridge.log("NIMO: forget()")
|
|
721
773
|
disconnect()
|
|
@@ -804,8 +856,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
804
856
|
}
|
|
805
857
|
|
|
806
858
|
func clearDisplay() {
|
|
807
|
-
|
|
808
|
-
pendingText = " "
|
|
859
|
+
encodeCanvas(scope: "legacy") { try NimoCanvasCodec.replace([]) }
|
|
809
860
|
}
|
|
810
861
|
|
|
811
862
|
func sendText(_ text: String) async {
|
|
@@ -813,92 +864,61 @@ class Nimo: NSObject, SGCManager {
|
|
|
813
864
|
}
|
|
814
865
|
|
|
815
866
|
func sendTextWall(_ text: String) async {
|
|
816
|
-
|
|
817
|
-
|
|
867
|
+
encodeCanvas(scope: "legacy") {
|
|
868
|
+
try NimoCanvasCodec.replace(NimoCanvasCodec.text(text, 0, 0, 500, 220, border: 0, radius: 0))
|
|
869
|
+
}
|
|
818
870
|
}
|
|
819
871
|
|
|
820
872
|
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
821
873
|
await sendTextWall(top + "\n\n" + bottom)
|
|
822
874
|
}
|
|
823
875
|
|
|
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")
|
|
876
|
+
func sendPositionedText(_ text: String, x: Int32, y: Int32, width: Int32, height: Int32,
|
|
877
|
+
borderWidth: Int32, borderRadius: Int32) async
|
|
878
|
+
{
|
|
879
|
+
encodeCanvas(scope: "legacy") {
|
|
880
|
+
try NimoCanvasCodec.replace(NimoCanvasCodec.text(text, Int(x), Int(y), Int(width), Int(height),
|
|
881
|
+
border: Int(borderWidth), radius: Int(borderRadius)))
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
func displayBitmap(base64ImageData: String, x: Int32?, y: Int32?, width: Int32?, height: Int32?) async -> Bool {
|
|
886
|
+
do {
|
|
887
|
+
let left = Int(x ?? 0), top = Int(y ?? 0)
|
|
888
|
+
let w = width.map(Int.init) ?? (NimoCanvasCodec.width - left)
|
|
889
|
+
let h = height.map(Int.init) ?? (NimoCanvasCodec.height - top)
|
|
890
|
+
try NimoCanvasCodec.region(left, top, w, h)
|
|
891
|
+
// True acknowledges bounded local validation, never visible rendering.
|
|
892
|
+
let source = try NimoCanvasImage.source(base64ImageData)
|
|
893
|
+
encodeCanvas(scope: "legacy") {
|
|
894
|
+
try NimoCanvasCodec.replace([NimoCanvasCodec.bitmap(left, top, w, h,
|
|
895
|
+
gray: NimoCanvasImage.grayscale(source, width: w, height: h))])
|
|
896
|
+
}
|
|
897
|
+
return true
|
|
898
|
+
} catch {
|
|
899
|
+
Bridge.log("NIMO: displayBitmap failed: \(error)")
|
|
852
900
|
return false
|
|
853
901
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
func applySceneFrame(_ frame: SceneFrame) async {
|
|
905
|
+
encodeCanvas(scope: "\(frame.appId):\(frame.epoch)", force: frame.replay) {
|
|
906
|
+
try NimoCanvasCodec.scene(frame.elements) { try NimoCanvasImage.decode($0, width: $1, height: $2) }
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
func clearSceneElements(_: [String]) async {
|
|
911
|
+
clearDisplay()
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private func encodeCanvas(scope: String, force: Bool = false, encode: @escaping () throws -> Data) {
|
|
915
|
+
canvasEncoder.submit(encode: encode) { [weak self] bytes in
|
|
916
|
+
self?.canvas.offer(bytes, scope: scope, force: force)
|
|
917
|
+
}
|
|
887
918
|
}
|
|
888
919
|
|
|
889
920
|
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
|
-
)
|
|
921
|
+
exit()
|
|
902
922
|
}
|
|
903
923
|
|
|
904
924
|
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
@@ -963,17 +983,8 @@ class Nimo: NSObject, SGCManager {
|
|
|
963
983
|
}
|
|
964
984
|
|
|
965
985
|
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
|
-
)
|
|
986
|
+
canvasEncoder.invalidate()
|
|
987
|
+
canvas.exit()
|
|
977
988
|
}
|
|
978
989
|
|
|
979
990
|
func sendShutdown() {
|
|
@@ -1020,7 +1031,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1020
1031
|
if bytes.count > maxLen {
|
|
1021
1032
|
// Truncate on a UTF-8 boundary
|
|
1022
1033
|
var end = maxLen
|
|
1023
|
-
while end > 0, bytes[end] & 0xC0 == 0x80 {
|
|
1034
|
+
while end > 0, bytes[end] & 0xC0 == 0x80 {
|
|
1035
|
+
end -= 1
|
|
1036
|
+
}
|
|
1024
1037
|
bytes = bytes.subdata(in: 0 ..< end)
|
|
1025
1038
|
}
|
|
1026
1039
|
dest.replaceSubrange(offset ..< offset + bytes.count, with: bytes)
|
|
@@ -1070,7 +1083,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1070
1083
|
func sendWifiCredentials(_: String, _: String) {}
|
|
1071
1084
|
func forgetWifiNetwork(_: String) {}
|
|
1072
1085
|
func sendHotspotState(_: Bool) {}
|
|
1073
|
-
func sendOtaStart(otaVersionUrl: String?) {}
|
|
1086
|
+
func sendOtaStart(otaVersionUrl _: String?) {}
|
|
1074
1087
|
func sendOtaQueryStatus() {}
|
|
1075
1088
|
|
|
1076
1089
|
// MARK: - SGCManager: User Context / Gallery / Version
|
|
@@ -1095,13 +1108,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1095
1108
|
|
|
1096
1109
|
// MARK: - BLE Scanning
|
|
1097
1110
|
|
|
1098
|
-
private func isNimoMainDevice(_ name: String) -> Bool {
|
|
1099
|
-
let lower = name.lowercased()
|
|
1100
|
-
return lower.hasPrefix(NimoBLE.NAME_PREFIX) && !lower.hasSuffix(NimoBLE.BLE_NAME_SUFFIX)
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
1111
|
@discardableResult
|
|
1104
1112
|
private func startScan() -> Bool {
|
|
1113
|
+
guard scanRequested, !isDisconnecting else { return false }
|
|
1105
1114
|
Bridge.log("NIMO: startScan()")
|
|
1106
1115
|
if centralManager == nil {
|
|
1107
1116
|
centralManager = CBCentralManager(
|
|
@@ -1117,17 +1126,32 @@ class Nimo: NSObject, SGCManager {
|
|
|
1117
1126
|
|
|
1118
1127
|
// Fast path: reconnect to the cached peripheral UUID.
|
|
1119
1128
|
if connectByUUID() {
|
|
1129
|
+
stopScan()
|
|
1120
1130
|
return true
|
|
1121
1131
|
}
|
|
1122
1132
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
)
|
|
1133
|
+
// NIMO's Companion service is GATT over BR/EDR. The main device must
|
|
1134
|
+
// first be paired in Settings; it does not appear in a BLE-only scan.
|
|
1135
|
+
discovery.start()
|
|
1127
1136
|
return true
|
|
1128
1137
|
}
|
|
1129
1138
|
|
|
1139
|
+
private func onDeviceFound(_ peripheral: CBPeripheral, name: String, rssi: Int?) {
|
|
1140
|
+
Bridge.log("NIMO: found Companion device: \(name)")
|
|
1141
|
+
Bridge.sendDiscoveredDevice(DeviceTypes.NIMO, name, rssi: rssi)
|
|
1142
|
+
guard DEVICE_SEARCH_ID != "NOT_SET", name == DEVICE_SEARCH_ID,
|
|
1143
|
+
self.peripheral == nil else { return }
|
|
1144
|
+
Bridge.log("NIMO: Connecting to \(name)")
|
|
1145
|
+
stopScan()
|
|
1146
|
+
lastDeviceName = name
|
|
1147
|
+
lastDeviceUUID = peripheral.identifier.uuidString
|
|
1148
|
+
self.peripheral = peripheral
|
|
1149
|
+
peripheral.delegate = self
|
|
1150
|
+
centralManager?.connect(peripheral, options: nil)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1130
1153
|
private func connectByUUID() -> Bool {
|
|
1154
|
+
guard peripheral == nil else { return false }
|
|
1131
1155
|
guard DEVICE_SEARCH_ID != "NOT_SET", !DEVICE_SEARCH_ID.isEmpty else { return false }
|
|
1132
1156
|
guard lastDeviceName == DEVICE_SEARCH_ID,
|
|
1133
1157
|
let uuidString = lastDeviceUUID,
|
|
@@ -1163,44 +1187,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1163
1187
|
|
|
1164
1188
|
// MARK: - Write Queue
|
|
1165
1189
|
|
|
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()
|
|
1190
|
+
private func enqueueWrite(_ characteristic: CBCharacteristic, _ bytes: Data) {
|
|
1191
|
+
guard let peripheral else { return }
|
|
1192
|
+
writes.enqueue(peripheral, characteristic: characteristic, frames: [bytes])
|
|
1204
1193
|
}
|
|
1205
1194
|
|
|
1206
1195
|
private func sendFrame(_ frame: Data) {
|
|
@@ -1208,12 +1197,18 @@ class Nimo: NSObject, SGCManager {
|
|
|
1208
1197
|
enqueueWrite(txChar, frame)
|
|
1209
1198
|
}
|
|
1210
1199
|
|
|
1211
|
-
private func enqueueFrames(_ frames: [Data])
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1200
|
+
private func enqueueFrames(_ frames: [Data], finalStarted: @escaping () -> Void,
|
|
1201
|
+
completed: @escaping () -> Void) -> Bool
|
|
1202
|
+
{
|
|
1203
|
+
guard let peripheral, let txChar else { return false }
|
|
1204
|
+
return writes.enqueue(peripheral, characteristic: txChar, frames: frames,
|
|
1205
|
+
finalStarted: finalStarted, completed: completed)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private func abortTransport(_ reason: String) {
|
|
1209
|
+
Bridge.log("NIMO: retiring transport: \(reason)")
|
|
1210
|
+
resetSessionState()
|
|
1211
|
+
if let peripheral { centralManager?.cancelPeripheralConnection(peripheral) }
|
|
1217
1212
|
}
|
|
1218
1213
|
|
|
1219
1214
|
// MARK: - Pending ACKs
|
|
@@ -1286,18 +1281,24 @@ class Nimo: NSObject, SGCManager {
|
|
|
1286
1281
|
cancelTwsTimeout()
|
|
1287
1282
|
handshakeState = .awaitingTimeAck
|
|
1288
1283
|
Bridge.log("NIMO: TWS OK — sending setTime (awaiting ACK)")
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1284
|
+
setTimeAttempts = 0
|
|
1285
|
+
attemptSetTime()
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
private func attemptSetTime() {
|
|
1289
|
+
guard handshakeState == .awaitingTimeAck, let expected = peripheral else { return }
|
|
1290
|
+
setTimeAttempts += 1
|
|
1291
|
+
sendAwaitingAck(cmd: NimoProtocol.CMD_SET_PARAMETER, key: NimoProtocol.SET_TIME,
|
|
1292
|
+
payload: NimoFrameCodec.encodeDeviceTime())
|
|
1293
|
+
{ [weak self] ok in
|
|
1294
|
+
guard let self, self.peripheral === expected, self.handshakeState == .awaitingTimeAck else { return }
|
|
1295
|
+
if ok { self.finishHandshake() }
|
|
1296
|
+
else if self.setTimeAttempts < 3 {
|
|
1297
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
1298
|
+
guard let self, self.peripheral === expected else { return }
|
|
1299
|
+
self.attemptSetTime()
|
|
1300
|
+
}
|
|
1301
|
+
} else { self.handshakeFailed() }
|
|
1301
1302
|
}
|
|
1302
1303
|
}
|
|
1303
1304
|
|
|
@@ -1312,6 +1313,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1312
1313
|
needsAck: false
|
|
1313
1314
|
)
|
|
1314
1315
|
)
|
|
1316
|
+
// Optional setting: older firmware may reject it without preventing connection.
|
|
1317
|
+
sendFrame(NimoFrameCodec.encodeFrame(cmd: NimoProtocol.CMD_SET_PARAMETER,
|
|
1318
|
+
key: NimoProtocol.SET_SYSTEM_LANGUAGE, payload: Data([NimoProtocol.LANGUAGE_ENGLISH])))
|
|
1315
1319
|
getBatteryStatus()
|
|
1316
1320
|
requestVersionInfo()
|
|
1317
1321
|
|
|
@@ -1321,6 +1325,10 @@ class Nimo: NSObject, SGCManager {
|
|
|
1321
1325
|
DeviceStore.shared.apply("glasses", "fullyBooted", true)
|
|
1322
1326
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.CONNECTED)
|
|
1323
1327
|
startTimers()
|
|
1328
|
+
// The Launch ACK validates peer readiness without waiting for the first
|
|
1329
|
+
// unsolicited heartbeat (which can arrive a minute after connection).
|
|
1330
|
+
canvas.activate()
|
|
1331
|
+
canvas.readiness(twsConnected && peerCompanionReady != false)
|
|
1324
1332
|
}
|
|
1325
1333
|
|
|
1326
1334
|
private func handshakeFailed() {
|
|
@@ -1333,16 +1341,13 @@ class Nimo: NSObject, SGCManager {
|
|
|
1333
1341
|
|
|
1334
1342
|
private func resetSessionState() {
|
|
1335
1343
|
handshakeState = .idle
|
|
1344
|
+
cancelTwsTimeout()
|
|
1336
1345
|
twsConnected = false
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
pendingText = nil
|
|
1346
|
+
peerCompanionReady = nil
|
|
1347
|
+
canvasEncoder.invalidate()
|
|
1348
|
+
canvas.disconnected()
|
|
1341
1349
|
receiveAssembler.reset()
|
|
1342
|
-
|
|
1343
|
-
writeInFlight = false
|
|
1344
|
-
writeWatchdogItem?.cancel()
|
|
1345
|
-
writeWatchdogItem = nil
|
|
1350
|
+
writes.reset()
|
|
1346
1351
|
failAllPendingAcks()
|
|
1347
1352
|
stopTimers()
|
|
1348
1353
|
}
|
|
@@ -1358,7 +1363,8 @@ class Nimo: NSObject, SGCManager {
|
|
|
1358
1363
|
}
|
|
1359
1364
|
Bridge.log("NIMO: Attempting reconnection...")
|
|
1360
1365
|
await MainActor.run {
|
|
1361
|
-
self.isDisconnecting
|
|
1366
|
+
guard !self.isDisconnecting else { return }
|
|
1367
|
+
self.scanRequested = true
|
|
1362
1368
|
self.startScan()
|
|
1363
1369
|
}
|
|
1364
1370
|
return false
|
|
@@ -1366,7 +1372,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1366
1372
|
}
|
|
1367
1373
|
}
|
|
1368
1374
|
|
|
1369
|
-
// MARK: - Timers (battery poll keepalive
|
|
1375
|
+
// MARK: - Timers (battery poll keepalive)
|
|
1370
1376
|
|
|
1371
1377
|
private func startTimers() {
|
|
1372
1378
|
stopTimers()
|
|
@@ -1379,62 +1385,30 @@ class Nimo: NSObject, SGCManager {
|
|
|
1379
1385
|
self?.getBatteryStatus()
|
|
1380
1386
|
}
|
|
1381
1387
|
}
|
|
1382
|
-
textQueueTimer = Timer.scheduledTimer(
|
|
1383
|
-
withTimeInterval: Const.textQueueTickSeconds, repeats: true
|
|
1384
|
-
) { [weak self] _ in
|
|
1385
|
-
Task { @MainActor in
|
|
1386
|
-
self?.drainTextQueue()
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
1388
|
}
|
|
1390
1389
|
|
|
1391
1390
|
private func stopTimers() {
|
|
1392
1391
|
batteryPollTimer?.invalidate()
|
|
1393
1392
|
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
1393
|
}
|
|
1427
1394
|
|
|
1428
1395
|
// MARK: - Incoming Data
|
|
1429
1396
|
|
|
1430
1397
|
private func handleRxPacket(_ packet: Data) {
|
|
1398
|
+
if packet.count >= 10, packet[8] == 7, [1, 3, 4].contains(packet[9]) {
|
|
1399
|
+
if let response = NimoCanvasCodec.response(packet) {
|
|
1400
|
+
if response.key == 1 { Bridge.log("NIMO: canvas Launch response status=\(response.payload.first.map(Int.init) ?? -1)") }
|
|
1401
|
+
canvas.response(key: response.key, payload: response.payload)
|
|
1402
|
+
}
|
|
1403
|
+
return
|
|
1404
|
+
}
|
|
1431
1405
|
receiveAssembler.cleanup()
|
|
1432
1406
|
for frame in receiveAssembler.ingest(packet) {
|
|
1433
1407
|
guard let decoded = NimoFrameCodec.decode(frame),
|
|
1434
1408
|
let cmd = decoded.cmd, let key = decoded.key
|
|
1435
1409
|
else { continue }
|
|
1436
1410
|
if cmd == NimoProtocol.CMD_INSTRUCTION_REPORT {
|
|
1437
|
-
handleReport(key: key, data: decoded.data ?? Data())
|
|
1411
|
+
if decoded.statusCode == 0 { handleReport(key: key, data: decoded.data ?? Data()) }
|
|
1438
1412
|
} else {
|
|
1439
1413
|
handleResponse(
|
|
1440
1414
|
cmd: cmd, key: key, statusCode: decoded.statusCode ?? 1,
|
|
@@ -1457,13 +1431,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1457
1431
|
Bridge.log("NIMO: app state report appId=\(appId) phase=\(phase)")
|
|
1458
1432
|
switch phase {
|
|
1459
1433
|
case NimoProtocol.STATE_ENTER:
|
|
1460
|
-
|
|
1461
|
-
if appId != textAppId { textAppEntered = false }
|
|
1462
|
-
if appId != NimoProtocol.APP_ID_NAV { navAppEntered = false }
|
|
1434
|
+
canvas.nativeApp(appId, entered: true)
|
|
1463
1435
|
case NimoProtocol.STATE_EXIT:
|
|
1464
|
-
|
|
1465
|
-
if appId == textAppId { textAppEntered = false }
|
|
1466
|
-
if appId == NimoProtocol.APP_ID_NAV { navAppEntered = false }
|
|
1436
|
+
canvas.nativeApp(appId, entered: false)
|
|
1467
1437
|
default:
|
|
1468
1438
|
break
|
|
1469
1439
|
}
|
|
@@ -1490,7 +1460,11 @@ class Nimo: NSObject, SGCManager {
|
|
|
1490
1460
|
case NimoProtocol.BUSINESS_HEARTBEAT:
|
|
1491
1461
|
// [leftMv(2)][rightMv(2)][btSysStatus(4)][twsStatus(1)][slaveGatt(1)]
|
|
1492
1462
|
if v.count >= 10 {
|
|
1463
|
+
peerCompanionReady = v[9] != 0
|
|
1493
1464
|
onTwsState(Int(v[8]) >= 1)
|
|
1465
|
+
if handshakeState == .ready {
|
|
1466
|
+
canvas.readiness(twsConnected && peerCompanionReady != false, confirmed: true)
|
|
1467
|
+
}
|
|
1494
1468
|
}
|
|
1495
1469
|
case NimoProtocol.BUSINESS_BATTERY:
|
|
1496
1470
|
if v.count >= 4 {
|
|
@@ -1512,6 +1486,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1512
1486
|
if !connected, handshakeState == .ready {
|
|
1513
1487
|
Bridge.log("NIMO: TWS service dropped mid-session (arm removed/off?)")
|
|
1514
1488
|
}
|
|
1489
|
+
if handshakeState == .ready { canvas.readiness(connected && peerCompanionReady != false) }
|
|
1515
1490
|
}
|
|
1516
1491
|
|
|
1517
1492
|
private func handleInputEvent(_ code: Int) {
|
|
@@ -1547,6 +1522,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1547
1522
|
}
|
|
1548
1523
|
|
|
1549
1524
|
private func handleResponse(cmd: Int, key: Int, statusCode: Int, data: Data) {
|
|
1525
|
+
if cmd == NimoProtocol.CMD_SET_PARAMETER, key == NimoProtocol.SET_SYSTEM_LANGUAGE {
|
|
1526
|
+
Bridge.log("NIMO: English menu setting acknowledged=\(statusCode == 0) status=\(statusCode)")
|
|
1527
|
+
}
|
|
1550
1528
|
resolvePendingAck(cmd: cmd, key: key, success: statusCode == 0)
|
|
1551
1529
|
|
|
1552
1530
|
guard cmd == NimoProtocol.CMD_GET_PARAMETER, statusCode == 0 else { return }
|
|
@@ -1573,7 +1551,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1573
1551
|
}
|
|
1574
1552
|
case NimoProtocol.GET_VERSION_DETAIL:
|
|
1575
1553
|
var end = bytes.count
|
|
1576
|
-
while end > 0, bytes[end - 1] == 0 {
|
|
1554
|
+
while end > 0, bytes[end - 1] == 0 {
|
|
1555
|
+
end -= 1
|
|
1556
|
+
}
|
|
1577
1557
|
firmwareVersionDetail =
|
|
1578
1558
|
String(bytes: bytes[0 ..< end], encoding: .utf8)?
|
|
1579
1559
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
@@ -1632,124 +1612,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1632
1612
|
guard let opusDecoder else { return }
|
|
1633
1613
|
for opusFrame in packet.opusFrames {
|
|
1634
1614
|
if let pcm = opusDecoder.decode(opusFrame), !pcm.isEmpty {
|
|
1635
|
-
DeviceManager.shared.
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
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])
|
|
1615
|
+
DeviceManager.shared.handleGlassesPcm(pcm)
|
|
1680
1616
|
}
|
|
1681
1617
|
}
|
|
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
1618
|
}
|
|
1754
1619
|
}
|
|
1755
1620
|
|
|
@@ -1768,41 +1633,35 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1768
1633
|
}
|
|
1769
1634
|
|
|
1770
1635
|
nonisolated func centralManager(
|
|
1771
|
-
_
|
|
1636
|
+
_: CBCentralManager,
|
|
1772
1637
|
didDiscover peripheral: CBPeripheral,
|
|
1773
1638
|
advertisementData: [String: Any],
|
|
1774
1639
|
rssi: NSNumber
|
|
1775
1640
|
) {
|
|
1776
|
-
|
|
1777
|
-
let name = peripheral.name
|
|
1778
|
-
?? advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
|
1779
|
-
else { return }
|
|
1641
|
+
let advertisedName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
|
1780
1642
|
let rssiValue = rssi.intValue
|
|
1781
|
-
|
|
1782
1643
|
DispatchQueue.main.async { [weak self] in
|
|
1783
|
-
|
|
1784
|
-
guard self.isNimoMainDevice(name) else { return }
|
|
1785
|
-
|
|
1786
|
-
Bridge.sendDiscoveredDevice(DeviceTypes.NIMO, name, rssi: rssiValue)
|
|
1787
|
-
|
|
1788
|
-
guard self.DEVICE_SEARCH_ID != "NOT_SET" else { return }
|
|
1789
|
-
guard name == self.DEVICE_SEARCH_ID else { return }
|
|
1790
|
-
guard self.peripheral == nil else { return }
|
|
1791
|
-
|
|
1792
|
-
Bridge.log("NIMO: Connecting to \(name)")
|
|
1793
|
-
self.stopScan()
|
|
1794
|
-
self.lastDeviceName = name
|
|
1795
|
-
self.lastDeviceUUID = peripheral.identifier.uuidString
|
|
1796
|
-
self.peripheral = peripheral
|
|
1797
|
-
peripheral.delegate = self
|
|
1798
|
-
central.connect(peripheral, options: nil)
|
|
1644
|
+
self?.discovery.receive(peripheral, advertisedName: advertisedName, rssi: rssiValue)
|
|
1799
1645
|
}
|
|
1800
1646
|
}
|
|
1801
1647
|
|
|
1648
|
+
#if os(iOS)
|
|
1649
|
+
nonisolated func centralManager(
|
|
1650
|
+
_: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral
|
|
1651
|
+
) {
|
|
1652
|
+
guard event == .peerConnected else { return }
|
|
1653
|
+
DispatchQueue.main.async { [weak self] in
|
|
1654
|
+
self?.discovery.receive(peripheral)
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
#endif
|
|
1658
|
+
|
|
1802
1659
|
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
1803
1660
|
DispatchQueue.main.async { [weak self] in
|
|
1804
1661
|
guard let self else { return }
|
|
1805
1662
|
Bridge.log("NIMO: Connected to \(peripheral.name ?? "unknown")")
|
|
1663
|
+
guard self.peripheral === peripheral else { return }
|
|
1664
|
+
self.writes.connected(peripheral)
|
|
1806
1665
|
self.lastDeviceUUID = peripheral.identifier.uuidString
|
|
1807
1666
|
peripheral.discoverServices([NimoBLE.SERVICE_UUID])
|
|
1808
1667
|
}
|
|
@@ -1816,18 +1675,19 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1816
1675
|
Bridge.log(
|
|
1817
1676
|
"NIMO: Failed to connect \(peripheral.name ?? "unknown"): \(error?.localizedDescription ?? "unknown")"
|
|
1818
1677
|
)
|
|
1678
|
+
guard self.peripheral === peripheral else { return }
|
|
1819
1679
|
self.peripheral = nil
|
|
1820
1680
|
self.startReconnectionTimer()
|
|
1821
1681
|
}
|
|
1822
1682
|
}
|
|
1823
1683
|
|
|
1824
1684
|
nonisolated func centralManager(
|
|
1825
|
-
_: CBCentralManager, didDisconnectPeripheral
|
|
1685
|
+
_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?
|
|
1826
1686
|
) {
|
|
1827
1687
|
DispatchQueue.main.async { [weak self] in
|
|
1828
1688
|
guard let self else { return }
|
|
1829
1689
|
Bridge.log("NIMO: Disconnected: \(error?.localizedDescription ?? "clean")")
|
|
1830
|
-
if self.isDisconnecting { return }
|
|
1690
|
+
if self.isDisconnecting || self.peripheral !== peripheral { return }
|
|
1831
1691
|
|
|
1832
1692
|
self.peripheral = nil
|
|
1833
1693
|
self.txChar = nil
|
|
@@ -1846,15 +1706,19 @@ extension Nimo: CBCentralManagerDelegate {
|
|
|
1846
1706
|
// MARK: - CBPeripheralDelegate
|
|
1847
1707
|
|
|
1848
1708
|
extension Nimo: CBPeripheralDelegate {
|
|
1849
|
-
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices
|
|
1850
|
-
|
|
1851
|
-
|
|
1709
|
+
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
1710
|
+
DispatchQueue.main.async { [weak self] in
|
|
1711
|
+
guard let self, self.peripheral === peripheral else { return }
|
|
1712
|
+
guard error == nil, let service = peripheral.services?.first(where: { $0.uuid == NimoBLE.SERVICE_UUID }) else {
|
|
1713
|
+
self.abortTransport("NIMO service discovery failed")
|
|
1714
|
+
return
|
|
1715
|
+
}
|
|
1852
1716
|
peripheral.discoverCharacteristics(nil, for: service)
|
|
1853
1717
|
}
|
|
1854
1718
|
}
|
|
1855
1719
|
|
|
1856
1720
|
nonisolated func peripheral(
|
|
1857
|
-
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error
|
|
1721
|
+
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?
|
|
1858
1722
|
) {
|
|
1859
1723
|
guard service.uuid == NimoBLE.SERVICE_UUID,
|
|
1860
1724
|
let characteristics = service.characteristics
|
|
@@ -1862,6 +1726,8 @@ extension Nimo: CBPeripheralDelegate {
|
|
|
1862
1726
|
|
|
1863
1727
|
DispatchQueue.main.async { [weak self] in
|
|
1864
1728
|
guard let self else { return }
|
|
1729
|
+
guard self.peripheral === peripheral else { return }
|
|
1730
|
+
guard error == nil else { self.abortTransport("NIMO characteristic discovery failed"); return }
|
|
1865
1731
|
for char in characteristics {
|
|
1866
1732
|
switch char.uuid {
|
|
1867
1733
|
case NimoBLE.CHAR_TX:
|
|
@@ -1876,6 +1742,10 @@ extension Nimo: CBPeripheralDelegate {
|
|
|
1876
1742
|
break
|
|
1877
1743
|
}
|
|
1878
1744
|
}
|
|
1745
|
+
guard self.txChar?.properties.contains(.write) == true, self.rxChar != nil, self.micChar != nil else {
|
|
1746
|
+
self.abortTransport("Required NIMO characteristics unavailable")
|
|
1747
|
+
return
|
|
1748
|
+
}
|
|
1879
1749
|
Bridge.log(
|
|
1880
1750
|
"NIMO: chars tx=\(self.txChar != nil) rx=\(self.rxChar != nil) mic=\(self.micChar != nil)"
|
|
1881
1751
|
)
|
|
@@ -1883,46 +1753,48 @@ extension Nimo: CBPeripheralDelegate {
|
|
|
1883
1753
|
}
|
|
1884
1754
|
|
|
1885
1755
|
nonisolated func peripheral(
|
|
1886
|
-
_: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
1756
|
+
_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
1887
1757
|
error: Error?
|
|
1888
1758
|
) {
|
|
1889
1759
|
DispatchQueue.main.async { [weak self] in
|
|
1890
1760
|
guard let self else { return }
|
|
1761
|
+
guard self.peripheral === peripheral else { return }
|
|
1891
1762
|
if let error {
|
|
1763
|
+
self.abortTransport("Notification subscription failed")
|
|
1892
1764
|
Bridge.log("NIMO: notify enable failed: \(error.localizedDescription)")
|
|
1893
1765
|
return
|
|
1894
1766
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1767
|
+
guard characteristic === self.rxChar || characteristic === self.micChar else { return }
|
|
1768
|
+
guard characteristic.isNotifying else { self.abortTransport("NIMO notifications disabled"); return }
|
|
1769
|
+
if self.rxChar?.isNotifying == true, self.micChar?.isNotifying == true,
|
|
1770
|
+
self.txChar?.properties.contains(.write) == true, self.handshakeState == .idle
|
|
1771
|
+
{
|
|
1897
1772
|
self.startHandshake()
|
|
1898
1773
|
}
|
|
1899
1774
|
}
|
|
1900
1775
|
}
|
|
1901
1776
|
|
|
1902
1777
|
nonisolated func peripheral(
|
|
1903
|
-
_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
1778
|
+
_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
1904
1779
|
) {
|
|
1905
1780
|
guard let data = characteristic.value, error == nil else { return }
|
|
1906
1781
|
let uuid = characteristic.uuid
|
|
1907
1782
|
DispatchQueue.main.async { [weak self] in
|
|
1908
1783
|
guard let self else { return }
|
|
1909
|
-
|
|
1784
|
+
guard self.peripheral === peripheral else { return }
|
|
1785
|
+
if uuid == NimoBLE.CHAR_RX, characteristic === self.rxChar {
|
|
1910
1786
|
self.handleRxPacket(data)
|
|
1911
|
-
} else if uuid == NimoBLE.CHAR_MIC {
|
|
1787
|
+
} else if uuid == NimoBLE.CHAR_MIC, characteristic === self.micChar {
|
|
1912
1788
|
self.handleMicPacket(data)
|
|
1913
1789
|
}
|
|
1914
1790
|
}
|
|
1915
1791
|
}
|
|
1916
1792
|
|
|
1917
1793
|
nonisolated func peripheral(
|
|
1918
|
-
_: CBPeripheral, didWriteValueFor
|
|
1794
|
+
_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?
|
|
1919
1795
|
) {
|
|
1920
1796
|
DispatchQueue.main.async { [weak self] in
|
|
1921
|
-
|
|
1922
|
-
if let error {
|
|
1923
|
-
Bridge.log("NIMO: Write error: \(error.localizedDescription)")
|
|
1924
|
-
}
|
|
1925
|
-
self.onWriteCompleted()
|
|
1797
|
+
self?.writes.written(peripheral, characteristic: characteristic, success: error == nil)
|
|
1926
1798
|
}
|
|
1927
1799
|
}
|
|
1928
1800
|
}
|