@mentra/bluetooth-sdk 3.2.1-beta.506 → 3.2.1-beta.512
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/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +37 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +59 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +26 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +31 -19
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +16 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +44 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +82 -13
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +94 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +15 -9
- package/build/_private/BluetoothSdkModule.d.ts +2 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +4 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +23 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/sgcs/Nimo.swift +41 -5
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +20 -6
- package/ios/Source/sgcs/NimoCanvasSession.swift +30 -14
- package/ios/Tests/DeviceManagerReconnectTests.swift +26 -1
- package/ios/Tests/NimoCanvasSessionTests.swift +74 -5
- package/ios/Tests/NimoCanvasTransportTests.swift +25 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/package.json +2 -2
- package/src/_private/BluetoothSdkModule.ts +2 -0
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -40,6 +40,8 @@ enum NimoProtocol {
|
|
|
40
40
|
static let CMD_SET_PARAMETER = 0x03
|
|
41
41
|
static let CMD_INSTRUCTION_REPORT = 0x06
|
|
42
42
|
static let CMD_CONTROL_INSTRUCTION = 0x07
|
|
43
|
+
static let CMD_CONTROL_FACTORY = 0x08
|
|
44
|
+
static let FACTORY_RECOVER = 0x03
|
|
43
45
|
static let CMD_CONTROL_NOTIFICATION = 0x09
|
|
44
46
|
|
|
45
47
|
// get parameter keys
|
|
@@ -58,6 +60,9 @@ enum NimoProtocol {
|
|
|
58
60
|
static let SET_DISPLAY_OFF = 0x0F
|
|
59
61
|
static let SET_PHONE_TYPE = 0x14
|
|
60
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
|
|
61
66
|
|
|
62
67
|
// control instruction keys
|
|
63
68
|
static let CTRL_ENTER_APP = 0x01
|
|
@@ -742,6 +747,27 @@ class Nimo: NSObject, SGCManager {
|
|
|
742
747
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
743
748
|
}
|
|
744
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
|
+
|
|
745
771
|
func forget() {
|
|
746
772
|
Bridge.log("NIMO: forget()")
|
|
747
773
|
disconnect()
|
|
@@ -1287,6 +1313,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1287
1313
|
needsAck: false
|
|
1288
1314
|
)
|
|
1289
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])))
|
|
1290
1319
|
getBatteryStatus()
|
|
1291
1320
|
requestVersionInfo()
|
|
1292
1321
|
|
|
@@ -1296,7 +1325,10 @@ class Nimo: NSObject, SGCManager {
|
|
|
1296
1325
|
DeviceStore.shared.apply("glasses", "fullyBooted", true)
|
|
1297
1326
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.CONNECTED)
|
|
1298
1327
|
startTimers()
|
|
1299
|
-
|
|
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)
|
|
1300
1332
|
}
|
|
1301
1333
|
|
|
1302
1334
|
private func handshakeFailed() {
|
|
@@ -1365,6 +1397,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1365
1397
|
private func handleRxPacket(_ packet: Data) {
|
|
1366
1398
|
if packet.count >= 10, packet[8] == 7, [1, 3, 4].contains(packet[9]) {
|
|
1367
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)") }
|
|
1368
1401
|
canvas.response(key: response.key, payload: response.payload)
|
|
1369
1402
|
}
|
|
1370
1403
|
return
|
|
@@ -1398,9 +1431,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1398
1431
|
Bridge.log("NIMO: app state report appId=\(appId) phase=\(phase)")
|
|
1399
1432
|
switch phase {
|
|
1400
1433
|
case NimoProtocol.STATE_ENTER:
|
|
1401
|
-
|
|
1434
|
+
canvas.nativeApp(appId, entered: true)
|
|
1402
1435
|
case NimoProtocol.STATE_EXIT:
|
|
1403
|
-
|
|
1436
|
+
canvas.nativeApp(appId, entered: false)
|
|
1404
1437
|
default:
|
|
1405
1438
|
break
|
|
1406
1439
|
}
|
|
@@ -1430,7 +1463,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1430
1463
|
peerCompanionReady = v[9] != 0
|
|
1431
1464
|
onTwsState(Int(v[8]) >= 1)
|
|
1432
1465
|
if handshakeState == .ready {
|
|
1433
|
-
canvas.readiness(twsConnected && peerCompanionReady
|
|
1466
|
+
canvas.readiness(twsConnected && peerCompanionReady != false, confirmed: true)
|
|
1434
1467
|
}
|
|
1435
1468
|
}
|
|
1436
1469
|
case NimoProtocol.BUSINESS_BATTERY:
|
|
@@ -1453,7 +1486,7 @@ class Nimo: NSObject, SGCManager {
|
|
|
1453
1486
|
if !connected, handshakeState == .ready {
|
|
1454
1487
|
Bridge.log("NIMO: TWS service dropped mid-session (arm removed/off?)")
|
|
1455
1488
|
}
|
|
1456
|
-
if handshakeState == .ready { canvas.readiness(connected && peerCompanionReady
|
|
1489
|
+
if handshakeState == .ready { canvas.readiness(connected && peerCompanionReady != false) }
|
|
1457
1490
|
}
|
|
1458
1491
|
|
|
1459
1492
|
private func handleInputEvent(_ code: Int) {
|
|
@@ -1489,6 +1522,9 @@ class Nimo: NSObject, SGCManager {
|
|
|
1489
1522
|
}
|
|
1490
1523
|
|
|
1491
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
|
+
}
|
|
1492
1528
|
resolvePendingAck(cmd: cmd, key: key, success: statusCode == 0)
|
|
1493
1529
|
|
|
1494
1530
|
guard cmd == NimoProtocol.CMD_GET_PARAMETER, statusCode == 0 else { return }
|
|
@@ -21,6 +21,7 @@ final class NimoCanvasCoordinator {
|
|
|
21
21
|
private let rejected: (Int) -> Void
|
|
22
22
|
private let session = NimoCanvasSession()
|
|
23
23
|
private var flight: Flight?
|
|
24
|
+
private var cancelRetry: (() -> Void)?
|
|
24
25
|
private var cancelDeadline: (() -> Void)?
|
|
25
26
|
|
|
26
27
|
init(schedule: @escaping NimoScheduler, writeCapacity: @escaping () -> Int,
|
|
@@ -35,23 +36,26 @@ final class NimoCanvasCoordinator {
|
|
|
35
36
|
run(session.offer(bytes, scope: scope, force: force))
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
func activate() {
|
|
40
|
+
run(session.activate())
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
func readiness(_ value: Bool, confirmed: Bool = false) {
|
|
44
|
+
if !value { cancelRetry?(); cancelRetry = nil }
|
|
39
45
|
run(session.readiness(value, confirmed: confirmed))
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
func exit() {
|
|
49
|
+
cancelRetry?(); cancelRetry = nil
|
|
43
50
|
run(session.exit())
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
func nativeApp(_ appId: Int, entered: Bool) -> Bool {
|
|
48
|
-
let takeover = (appId == NimoCanvasCodec.appId && !entered) || (appId != NimoCanvasCodec.appId && entered)
|
|
49
|
-
let expectedExit = appId == NimoCanvasCodec.appId && !entered && flight?.key == 3
|
|
53
|
+
func nativeApp(_ appId: Int, entered: Bool) {
|
|
50
54
|
run(session.nativeApp(appId, entered: entered))
|
|
51
|
-
return takeover && !expectedExit
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
func disconnected() {
|
|
58
|
+
cancelRetry?(); cancelRetry = nil
|
|
55
59
|
cancelDeadline?(); cancelDeadline = nil
|
|
56
60
|
flight = nil
|
|
57
61
|
session.disconnected()
|
|
@@ -72,6 +76,7 @@ final class NimoCanvasCoordinator {
|
|
|
72
76
|
for action in actions {
|
|
73
77
|
switch action {
|
|
74
78
|
case let .send(key, frame, ticket):
|
|
79
|
+
cancelRetry?(); cancelRetry = nil
|
|
75
80
|
cancelDeadline?()
|
|
76
81
|
let current = Flight(key: key)
|
|
77
82
|
flight = current
|
|
@@ -93,7 +98,16 @@ final class NimoCanvasCoordinator {
|
|
|
93
98
|
disconnected(); reconnect("Canvas framing failed: \(error)")
|
|
94
99
|
}
|
|
95
100
|
case let .reconnect(reason): disconnected(); reconnect(reason)
|
|
96
|
-
case let .rejected(status):
|
|
101
|
+
case let .rejected(status):
|
|
102
|
+
rejected(status)
|
|
103
|
+
if status == 7 {
|
|
104
|
+
cancelRetry?()
|
|
105
|
+
cancelRetry = schedule(1) { [weak self] in
|
|
106
|
+
guard let self else { return }
|
|
107
|
+
self.cancelRetry = nil
|
|
108
|
+
self.run(self.session.retryNotReady())
|
|
109
|
+
}
|
|
110
|
+
}
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
}
|
|
@@ -15,12 +15,14 @@ final class NimoCanvasSession {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
private var ready = false
|
|
18
|
+
private var transportReady = false
|
|
18
19
|
private var active = false
|
|
19
20
|
private var locked = false
|
|
20
21
|
private var launchBlocked = false
|
|
21
22
|
private var waitingReadiness = false
|
|
22
23
|
private var observedNotReady = false
|
|
23
24
|
private var readinessRetries = 0
|
|
25
|
+
private var probeRetries = 0
|
|
24
26
|
private var exitRequested = false
|
|
25
27
|
private var desired: Data?
|
|
26
28
|
private var accepted: Data?
|
|
@@ -39,9 +41,27 @@ final class NimoCanvasSession {
|
|
|
39
41
|
return pump()
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/// Enter a blank canvas unless a newer host scene survived the connection change.
|
|
45
|
+
func activate() -> [Action] {
|
|
46
|
+
if desired == nil { desired = Data([0, 0, 1]) }
|
|
47
|
+
exitRequested = false
|
|
48
|
+
return pump()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The firmware validates peer readiness on Launch. Only an explicit NOT_READY
|
|
52
|
+
/// permits a bounded retry; an ambiguous ACK still requires a new transport.
|
|
53
|
+
func retryNotReady() -> [Action] {
|
|
54
|
+
guard transportReady, waitingReadiness, desired != nil, probeRetries < 3 else { return [] }
|
|
55
|
+
probeRetries += 1
|
|
56
|
+
waitingReadiness = false
|
|
57
|
+
ready = true
|
|
58
|
+
return pump()
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
/// Only a fresh heartbeat samples both TWS and peer Companion readiness.
|
|
43
62
|
func readiness(_ value: Bool, confirmed: Bool = false) -> [Action] {
|
|
44
|
-
|
|
63
|
+
transportReady = value
|
|
64
|
+
if !value { readinessRetries = 0; probeRetries = 0 }
|
|
45
65
|
if !value, flight != nil { return resetLink("Readiness lost during canvas command") }
|
|
46
66
|
if waitingReadiness {
|
|
47
67
|
if !value { observedNotReady = true }
|
|
@@ -58,8 +78,8 @@ final class NimoCanvasSession {
|
|
|
58
78
|
|
|
59
79
|
/// Keep only the latest desired scene across a new transport generation.
|
|
60
80
|
func disconnected() {
|
|
61
|
-
ready = false; active = false; locked = false; launchBlocked = false
|
|
62
|
-
waitingReadiness = false; observedNotReady = false; readinessRetries = 0
|
|
81
|
+
ready = false; transportReady = false; active = false; locked = false; launchBlocked = false
|
|
82
|
+
waitingReadiness = false; observedNotReady = false; readinessRetries = 0; probeRetries = 0
|
|
63
83
|
accepted = nil; rejected = nil; flight = nil
|
|
64
84
|
ticket &+= 1; forceRevision &+= 1
|
|
65
85
|
}
|
|
@@ -69,16 +89,12 @@ final class NimoCanvasSession {
|
|
|
69
89
|
return pump()
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
/// Stock UI reports do not relinquish host ownership. Replay after the current ACK.
|
|
72
93
|
func nativeApp(_ appId: Int, entered: Bool) -> [Action] {
|
|
73
94
|
if (appId == NimoCanvasCodec.appId && !entered) || (appId != NimoCanvasCodec.appId && entered) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return []
|
|
78
|
-
}
|
|
79
|
-
let ambiguous = flight != nil
|
|
80
|
-
desired = nil; accepted = nil; rejected = nil; active = false; exitRequested = false; scope = nil
|
|
81
|
-
if ambiguous { return resetLink("Native app transition interrupted canvas command") }
|
|
95
|
+
active = false
|
|
96
|
+
accepted = nil
|
|
97
|
+
return pump()
|
|
82
98
|
}
|
|
83
99
|
return []
|
|
84
100
|
}
|
|
@@ -112,8 +128,8 @@ final class NimoCanvasSession {
|
|
|
112
128
|
return [.rejected(status)] + pump()
|
|
113
129
|
}
|
|
114
130
|
switch key {
|
|
115
|
-
case 1: active = true
|
|
116
|
-
case 4: accepted = current.frame; rejected = nil; readinessRetries = 0
|
|
131
|
+
case 1: active = true; accepted = nil
|
|
132
|
+
case 4: accepted = current.frame; rejected = nil; readinessRetries = 0; probeRetries = 0
|
|
117
133
|
case 3: active = false; accepted = nil; exitRequested = false
|
|
118
134
|
default: break
|
|
119
135
|
}
|
|
@@ -126,7 +142,7 @@ final class NimoCanvasSession {
|
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
private func resetLink(_ reason: String) -> [Action] {
|
|
129
|
-
ready = false; active = false; accepted = nil; flight = nil
|
|
145
|
+
ready = false; transportReady = false; active = false; accepted = nil; flight = nil
|
|
130
146
|
ticket &+= 1; forceRevision &+= 1
|
|
131
147
|
return [.reconnect(reason)]
|
|
132
148
|
}
|
|
@@ -5,6 +5,25 @@ import XCTest
|
|
|
5
5
|
final class DeviceManagerReconnectTests: XCTestCase {
|
|
6
6
|
private let requests = ["should_send_lc3", "should_send_pcm", "should_send_transcript", "local_stt_fallback_active"]
|
|
7
7
|
|
|
8
|
+
func testDelayedReconnectReplayPreservesNimoDepthEndpoints() {
|
|
9
|
+
for depth in [0, 10] {
|
|
10
|
+
withRecordingDevice { manager, store in
|
|
11
|
+
let device = manager.sgc as! ReconnectRecordingDevice
|
|
12
|
+
device.type = DeviceTypes.NIMO
|
|
13
|
+
store.set("bluetooth", "dashboard_height", 7)
|
|
14
|
+
store.set("bluetooth", "dashboard_depth", depth)
|
|
15
|
+
let replayed = expectation(description: "Delayed NIMO depth \(depth)")
|
|
16
|
+
replayed.assertForOverFulfill = false
|
|
17
|
+
device.onPosition = { replayed.fulfill() }
|
|
18
|
+
manager.handleDeviceReady()
|
|
19
|
+
XCTAssertTrue(device.positions.isEmpty)
|
|
20
|
+
wait(for: [replayed], timeout: 3)
|
|
21
|
+
XCTAssertEqual(device.positions.last?.0, 7)
|
|
22
|
+
XCTAssertEqual(device.positions.last?.1, depth)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
8
27
|
private func withRecordingDevice(_ body: (DeviceManager, DeviceStore) -> Void) {
|
|
9
28
|
let manager = DeviceManager.shared
|
|
10
29
|
let store = DeviceStore.shared
|
|
@@ -265,6 +284,8 @@ private final class ReconnectRecordingDevice: SGCManager {
|
|
|
265
284
|
let hasMic = true
|
|
266
285
|
let showConnectionConfirmation = false
|
|
267
286
|
var micChanges: [Bool] = []
|
|
287
|
+
var positions: [(Int, Int)] = []
|
|
288
|
+
var onPosition: (() -> Void)?
|
|
268
289
|
var isMicSuspendedForAudio = false
|
|
269
290
|
|
|
270
291
|
func clearSceneElements(_: [String]) async {}
|
|
@@ -300,7 +321,11 @@ private final class ReconnectRecordingDevice: SGCManager {
|
|
|
300
321
|
}
|
|
301
322
|
|
|
302
323
|
func showDashboard() {}
|
|
303
|
-
func setDashboardPosition(_: Int, _: Int) {
|
|
324
|
+
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
325
|
+
positions.append((height, depth))
|
|
326
|
+
onPosition?()
|
|
327
|
+
}
|
|
328
|
+
|
|
304
329
|
func setHeadUpAngle(_: Int) {}
|
|
305
330
|
func getBatteryStatus() {}
|
|
306
331
|
func setSilentMode(_: Bool) {}
|
|
@@ -102,11 +102,80 @@ final class NimoCanvasSessionTests: XCTestCase {
|
|
|
102
102
|
XCTAssertEqual(frame, b)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
func
|
|
105
|
+
func testNativeTakeoverReplaysWithoutNewHostFrame() {
|
|
106
106
|
let session = NimoCanvasSession(); connected(session)
|
|
107
|
-
|
|
108
|
-
XCTAssertTrue(session.
|
|
109
|
-
|
|
110
|
-
XCTAssertEqual(
|
|
107
|
+
XCTAssertEqual(sentKey(session.nativeApp(0, entered: true)), 1)
|
|
108
|
+
XCTAssertTrue(session.nativeApp(0, entered: true).isEmpty)
|
|
109
|
+
guard case let .send(_, frame, _) = ack(session, 1).first else { return XCTFail() }
|
|
110
|
+
XCTAssertEqual(frame, a)
|
|
111
|
+
XCTAssertTrue(session.nativeApp(0xFD, entered: false).isEmpty)
|
|
112
|
+
XCTAssertEqual(sentKey(ack(session, 4)), 1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func testTakeoverDuringUpdateReplaysIdenticalFrameAfterRelaunch() {
|
|
116
|
+
let session = NimoCanvasSession()
|
|
117
|
+
_ = session.offer(a, scope: "app:1"); _ = session.readiness(true)
|
|
118
|
+
XCTAssertEqual(sentKey(ack(session, 1)), 4)
|
|
119
|
+
XCTAssertTrue(session.nativeApp(0, entered: true).isEmpty)
|
|
120
|
+
XCTAssertEqual(sentKey(ack(session, 4)), 1)
|
|
121
|
+
guard case let .send(_, frame, _) = ack(session, 1).first else { return XCTFail("Missing replay Update") }
|
|
122
|
+
XCTAssertEqual(frame, a)
|
|
123
|
+
XCTAssertTrue(ack(session, 4).isEmpty)
|
|
124
|
+
XCTAssertTrue(session.offer(a, scope: "app:1").isEmpty)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func testStartupStockUiPreservesReconnectScene() {
|
|
128
|
+
let session = NimoCanvasSession(); connected(session)
|
|
129
|
+
session.disconnected()
|
|
130
|
+
XCTAssertTrue(session.nativeApp(0, entered: true).isEmpty)
|
|
131
|
+
XCTAssertTrue(session.activate().isEmpty)
|
|
132
|
+
XCTAssertEqual(sentKey(session.readiness(true)), 1)
|
|
133
|
+
guard case let .send(_, frame, _) = ack(session, 1).first else { return XCTFail() }
|
|
134
|
+
XCTAssertEqual(frame, a)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func testActivationEntersBlankCanvasBeforeAnyMiniapp() {
|
|
138
|
+
let session = NimoCanvasSession()
|
|
139
|
+
XCTAssertTrue(session.activate().isEmpty)
|
|
140
|
+
XCTAssertEqual(sentKey(session.readiness(true)), 1)
|
|
141
|
+
guard case let .send(_, frame, _) = ack(session, 1).first else { return XCTFail() }
|
|
142
|
+
XCTAssertEqual(frame, Data([0, 0, 1]))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func testExplicitExitStopsRestorationUntilAnotherConnection() {
|
|
146
|
+
let session = NimoCanvasSession(); connected(session)
|
|
147
|
+
XCTAssertEqual(sentKey(session.exit()), 3)
|
|
148
|
+
XCTAssertTrue(session.nativeApp(0, entered: true).isEmpty)
|
|
149
|
+
XCTAssertTrue(ack(session, 3).isEmpty)
|
|
150
|
+
XCTAssertTrue(session.nativeApp(0xFD, entered: false).isEmpty)
|
|
151
|
+
session.disconnected()
|
|
152
|
+
_ = session.activate()
|
|
153
|
+
XCTAssertEqual(sentKey(session.readiness(true)), 1)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func testSuccessfulUpdateRestoresProbeBudgetForNextRecovery() {
|
|
157
|
+
let session = NimoCanvasSession()
|
|
158
|
+
_ = session.activate(); _ = session.readiness(true)
|
|
159
|
+
for episode in 0 ..< 2 {
|
|
160
|
+
if episode > 0 { XCTAssertEqual(sentKey(session.nativeApp(0, entered: true)), 1) }
|
|
161
|
+
for _ in 0 ..< 3 {
|
|
162
|
+
_ = ack(session, 1, status: 7)
|
|
163
|
+
XCTAssertEqual(sentKey(session.retryNotReady()), 1)
|
|
164
|
+
}
|
|
165
|
+
XCTAssertEqual(sentKey(ack(session, 1)), 4)
|
|
166
|
+
XCTAssertTrue(ack(session, 4).isEmpty)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func testProbesAreBoundedAndHeartbeatCanStillRecover() {
|
|
171
|
+
let session = NimoCanvasSession()
|
|
172
|
+
_ = session.activate(); _ = session.readiness(true)
|
|
173
|
+
for _ in 0 ..< 3 {
|
|
174
|
+
_ = ack(session, 1, status: 7)
|
|
175
|
+
XCTAssertEqual(sentKey(session.retryNotReady()), 1)
|
|
176
|
+
}
|
|
177
|
+
_ = ack(session, 1, status: 7)
|
|
178
|
+
XCTAssertTrue(session.retryNotReady().isEmpty)
|
|
179
|
+
XCTAssertEqual(sentKey(session.readiness(true, confirmed: true)), 1)
|
|
111
180
|
}
|
|
112
181
|
}
|
|
@@ -33,6 +33,31 @@ private final class NimoTestClock {
|
|
|
33
33
|
final class NimoCanvasTransportTests: XCTestCase {
|
|
34
34
|
private let frame = Data([0, 0, 1])
|
|
35
35
|
|
|
36
|
+
func testNotReadyProbesAreTimedBoundedAndCancelled() {
|
|
37
|
+
for stop in ["budget", "disconnect", "exit", "notReady"] {
|
|
38
|
+
let clock = NimoTestClock()
|
|
39
|
+
var keys: [Int] = []
|
|
40
|
+
let canvas = NimoCanvasCoordinator(schedule: clock.schedule, writeCapacity: { 512 }, enqueue: { frames, start, complete in
|
|
41
|
+
keys.append(Int(frames[0][9])); start(); complete(); return true
|
|
42
|
+
}, reconnect: { XCTFail($0) }, rejected: { _ in })
|
|
43
|
+
canvas.activate(); canvas.readiness(true)
|
|
44
|
+
for attempt in 0 ..< 4 {
|
|
45
|
+
canvas.response(key: 1, payload: Data([7]))
|
|
46
|
+
if stop == "disconnect" { canvas.disconnected() }
|
|
47
|
+
if stop == "exit" { canvas.exit() }
|
|
48
|
+
if stop == "notReady" { canvas.readiness(false) }
|
|
49
|
+
let before = keys.count
|
|
50
|
+
clock.advance(0.999)
|
|
51
|
+
XCTAssertEqual(keys.count, before)
|
|
52
|
+
clock.advance(0.001)
|
|
53
|
+
XCTAssertEqual(keys.count, before + (stop == "budget" && attempt < 3 ? 1 : 0))
|
|
54
|
+
if stop != "budget" { break }
|
|
55
|
+
}
|
|
56
|
+
clock.advance(60)
|
|
57
|
+
XCTAssertEqual(keys.count, stop == "budget" ? 4 : 1)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
36
61
|
func testEarlyAckOnlyAdvancesAfterFinalWriteCompletes() {
|
|
37
62
|
let clock = NimoTestClock()
|
|
38
63
|
var chains: [[Data]] = [], starts: [() -> Void] = [], completes: [() -> Void] = []
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
@testable import MentraBluetoothSDK
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class NimoDeviceCommandsTests: XCTestCase {
|
|
6
|
+
func testSystemEnglishAndFactoryResetMatchFirmwareWireCommands() {
|
|
7
|
+
func hex(_ bytes: Data) -> String {
|
|
8
|
+
bytes.map { String(format: "%02X", $0) }.joined()
|
|
9
|
+
}
|
|
10
|
+
XCTAssertEqual(hex(NimoFrameCodec.encodeFrame(cmd: NimoProtocol.CMD_SET_PARAMETER,
|
|
11
|
+
key: NimoProtocol.SET_SYSTEM_LANGUAGE, payload: Data([NimoProtocol.LANGUAGE_ENGLISH]))),
|
|
12
|
+
"BF020500702500000324010001")
|
|
13
|
+
XCTAssertEqual(hex(NimoFrameCodec.encodeFrame(cmd: NimoProtocol.CMD_CONTROL_FACTORY,
|
|
14
|
+
key: NimoProtocol.FACTORY_RECOVER, payload: Data())), "BF0204005358000008030000")
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.2.1-beta.
|
|
3
|
+
"version": "3.2.1-beta.512",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"registry": "https://registry.npmjs.org/"
|
|
115
115
|
},
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@mentra/cloud-protocol": "3.2.1-beta.
|
|
117
|
+
"@mentra/cloud-protocol": "3.2.1-beta.512"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@types/node": "^25.9.3",
|
|
@@ -109,6 +109,8 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
109
109
|
connectSimulated(): Promise<void>
|
|
110
110
|
disconnect(): Promise<void>
|
|
111
111
|
forget(): Promise<void>
|
|
112
|
+
/** Explicit Unpair also resets connected NIMO glasses before forgetting. */
|
|
113
|
+
unpair(): Promise<void>
|
|
112
114
|
forgetController(): Promise<void>
|
|
113
115
|
showDashboard(): Promise<void>
|
|
114
116
|
setBrightness(level: number, autoMode?: boolean | null): Promise<void>
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.2.1",
|
|
15
|
-
"releaseIdentity": "3.2.1-beta.
|
|
16
|
-
"releaseSetId": "mentra-3.2.1-beta.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.1-beta.512",
|
|
16
|
+
"releaseSetId": "mentra-3.2.1-beta.512",
|
|
17
|
+
"sourceCommit": "e09a07ee7b71972e75a5051003ec561c12e6cb9b",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.512.json",
|
|
19
|
+
"otaManifestSha256": "bd24cbd8279319912d643699eeb435df3ca88582194953583491a3625799ec01"
|
|
20
20
|
})
|