@mentra/bluetooth-sdk 3.2.1-beta.396 → 3.2.1-beta.401
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 +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/sgcs/G1.swift +47 -32
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
package/README.md
CHANGED
|
@@ -446,9 +446,9 @@ console.log(ledAck.state)
|
|
|
446
446
|
|
|
447
447
|
## Dashboard Content
|
|
448
448
|
|
|
449
|
-
`setDashboardContent(content)` keeps the standard `$TIME12$ $DATE$ $GBATT
|
|
450
|
-
|
|
451
|
-
|
|
449
|
+
`setDashboardContent(content)` keeps the standard `$TIME12$ $DATE$ $GBATT$`
|
|
450
|
+
status header and places non-empty content below it after a blank line. Pass the
|
|
451
|
+
exact empty string to reset the dashboard to the status
|
|
452
452
|
header only. The template is held in memory for the SDK process/session, and its
|
|
453
453
|
status placeholders are refreshed each time the dashboard renders. Calling this
|
|
454
454
|
method does not open the dashboard; it updates an active contextual dashboard
|
|
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "3.2.1"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.2.1-beta.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.2.1-beta.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.2.1-beta.401"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.2.1-beta.401"
|
|
8
|
+
const val SOURCE_COMMIT: String = "86f368f9f9b543f397c0b66df76de2c46170ca3d"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.401.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "48700618e2ec25cf0d145f2157e0c98f07a0e902858325b851b71b987177925e"
|
|
11
11
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
3
|
internal object DashboardContentFormatter {
|
|
4
|
-
const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$
|
|
4
|
+
const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$"
|
|
5
5
|
|
|
6
6
|
fun template(content: String): String =
|
|
7
7
|
if (content.isEmpty()) STATUS_HEADER else "$STATUS_HEADER\n\n$content"
|
|
@@ -7,7 +7,7 @@ class DashboardContentFormatterTest {
|
|
|
7
7
|
@Test
|
|
8
8
|
fun emptyContentReturnsStatusHeaderOnly() {
|
|
9
9
|
assertEquals(
|
|
10
|
-
"\$TIME12$ \$DATE$ \$GBATT$
|
|
10
|
+
"\$TIME12$ \$DATE$ \$GBATT$",
|
|
11
11
|
DashboardContentFormatter.template(""),
|
|
12
12
|
)
|
|
13
13
|
}
|
|
@@ -15,7 +15,7 @@ class DashboardContentFormatterTest {
|
|
|
15
15
|
@Test
|
|
16
16
|
fun nonEmptyContentIsAppendedExactlyAfterBlankLine() {
|
|
17
17
|
assertEquals(
|
|
18
|
-
"\$TIME12$ \$DATE$ \$GBATT
|
|
18
|
+
"\$TIME12$ \$DATE$ \$GBATT$\n\n Next meeting\nRoom 2 ",
|
|
19
19
|
DashboardContentFormatter.template(" Next meeting\nRoom 2 "),
|
|
20
20
|
)
|
|
21
21
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "3.2.1",
|
|
4
|
-
"releaseIdentity": "3.2.1-beta.
|
|
5
|
-
"releaseSetId": "mentra-3.2.1-beta.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.
|
|
8
|
-
"otaManifestSha256": "
|
|
4
|
+
"releaseIdentity": "3.2.1-beta.401",
|
|
5
|
+
"releaseSetId": "mentra-3.2.1-beta.401",
|
|
6
|
+
"sourceCommit": "86f368f9f9b543f397c0b66df76de2c46170ca3d",
|
|
7
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.401.json",
|
|
8
|
+
"otaManifestSha256": "48700618e2ec25cf0d145f2157e0c98f07a0e902858325b851b71b987177925e"
|
|
9
9
|
});
|
|
10
10
|
//# sourceMappingURL=releaseMetadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,gBAAgB;IACnC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,kIAAkI;IACpJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.1\",\n \"releaseIdentity\": \"3.2.1-beta.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,gBAAgB;IACnC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,kIAAkI;IACpJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.1\",\n \"releaseIdentity\": \"3.2.1-beta.401\",\n \"releaseSetId\": \"mentra-3.2.1-beta.401\",\n \"sourceCommit\": \"86f368f9f9b543f397c0b66df76de2c46170ca3d\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.401.json\",\n \"otaManifestSha256\": \"48700618e2ec25cf0d145f2157e0c98f07a0e902858325b851b71b987177925e\"\n})\n"]}
|
|
@@ -16,7 +16,7 @@ enum BluetoothSdkDefaults {
|
|
|
16
16
|
/// silently disable it on every pair the app touches.
|
|
17
17
|
static let autoPowerOffEnabled = false
|
|
18
18
|
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
19
|
-
private static let swiftPackageSdkVersion = "3.2.1-beta.
|
|
19
|
+
private static let swiftPackageSdkVersion = "3.2.1-beta.401"
|
|
20
20
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
21
21
|
|
|
22
22
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.2.1"
|
|
6
|
-
static let releaseIdentity = "3.2.1-beta.
|
|
7
|
-
static let releaseSetId = "mentra-3.2.1-beta.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.2.1-beta.401"
|
|
7
|
+
static let releaseSetId = "mentra-3.2.1-beta.401"
|
|
8
|
+
static let sourceCommit = "86f368f9f9b543f397c0b66df76de2c46170ca3d"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.401.json"
|
|
10
|
+
static let otaManifestSha256 = "48700618e2ec25cf0d145f2157e0c98f07a0e902858325b851b71b987177925e"
|
|
11
11
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
enum DashboardContentFormatter {
|
|
4
|
-
static let statusHeader = "$TIME12$ $DATE$ $GBATT$
|
|
4
|
+
static let statusHeader = "$TIME12$ $DATE$ $GBATT$"
|
|
5
5
|
|
|
6
6
|
static func template(for content: String) -> String {
|
|
7
7
|
content.isEmpty ? statusHeader : statusHeader + "\n\n" + content
|
package/ios/Source/sgcs/G1.swift
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import Combine
|
|
9
9
|
import CoreBluetooth
|
|
10
|
-
import Foundation
|
|
11
10
|
import CoreGraphics
|
|
11
|
+
import Foundation
|
|
12
12
|
|
|
13
13
|
extension Data {
|
|
14
14
|
func chunked(into size: Int) -> [Data] {
|
|
@@ -232,7 +232,8 @@ actor ReconnectionManager {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
func start(onAttempt: @escaping @Sendable () async -> Bool) {
|
|
235
|
-
|
|
235
|
+
// Multiple command failures must not restart the delay between attempts.
|
|
236
|
+
guard !isRunning else { return }
|
|
236
237
|
attempts = 0
|
|
237
238
|
|
|
238
239
|
task = Task {
|
|
@@ -258,6 +259,9 @@ actor ReconnectionManager {
|
|
|
258
259
|
break
|
|
259
260
|
}
|
|
260
261
|
}
|
|
262
|
+
if !Task.isCancelled {
|
|
263
|
+
task = nil
|
|
264
|
+
}
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -312,7 +316,7 @@ class G1: NSObject, SGCManager {
|
|
|
312
316
|
|
|
313
317
|
func queryGalleryStatus() {}
|
|
314
318
|
|
|
315
|
-
func sendOtaStart(otaVersionUrl: String?) {}
|
|
319
|
+
func sendOtaStart(otaVersionUrl _: String?) {}
|
|
316
320
|
func sendOtaQueryStatus() {}
|
|
317
321
|
|
|
318
322
|
func ping() {}
|
|
@@ -450,13 +454,12 @@ class G1: NSObject, SGCManager {
|
|
|
450
454
|
}
|
|
451
455
|
|
|
452
456
|
private var centralManager: CBCentralManager?
|
|
457
|
+
private var scanRequested = false
|
|
453
458
|
|
|
454
459
|
private var leftPeripheral: CBPeripheral?
|
|
455
460
|
private var rightPeripheral: CBPeripheral?
|
|
456
461
|
private var connectedDevices: [String: (CBPeripheral?, CBPeripheral?)] = [:]
|
|
457
462
|
var lastConnectionTimestamp: Date = .distantPast
|
|
458
|
-
private var leftInitialized: Bool = false
|
|
459
|
-
private var rightInitialized: Bool = false
|
|
460
463
|
|
|
461
464
|
private var leftGlassUUID: UUID? {
|
|
462
465
|
get {
|
|
@@ -501,6 +504,8 @@ class G1: NSObject, SGCManager {
|
|
|
501
504
|
}
|
|
502
505
|
|
|
503
506
|
func forget() {
|
|
507
|
+
isDisconnecting = true
|
|
508
|
+
stopScan()
|
|
504
509
|
Task {
|
|
505
510
|
await heartbeatManager.stop()
|
|
506
511
|
await reconnectionManager.stop()
|
|
@@ -704,11 +709,13 @@ class G1: NSObject, SGCManager {
|
|
|
704
709
|
|
|
705
710
|
func connectById(_ id: String) {
|
|
706
711
|
DEVICE_SEARCH_ID = "_" + id + "_"
|
|
712
|
+
scanRequested = true
|
|
707
713
|
startScan()
|
|
708
714
|
}
|
|
709
715
|
|
|
710
716
|
func findCompatibleDevices() {
|
|
711
717
|
DEVICE_SEARCH_ID = "NOT_SET"
|
|
718
|
+
scanRequested = true
|
|
712
719
|
startScan()
|
|
713
720
|
}
|
|
714
721
|
|
|
@@ -736,8 +743,7 @@ class G1: NSObject, SGCManager {
|
|
|
736
743
|
)
|
|
737
744
|
stopScan()
|
|
738
745
|
|
|
739
|
-
//
|
|
740
|
-
getBatteryStatus()
|
|
746
|
+
// Battery requests start after both arms acknowledge initialization.
|
|
741
747
|
return true
|
|
742
748
|
}
|
|
743
749
|
|
|
@@ -985,11 +991,15 @@ class G1: NSObject, SGCManager {
|
|
|
985
991
|
connected = leftReady && rightReady
|
|
986
992
|
if fullyBooted {
|
|
987
993
|
stopReconnectionTimer()
|
|
994
|
+
if !prevLeftReady || !prevRightReady {
|
|
995
|
+
getBatteryStatus()
|
|
996
|
+
}
|
|
988
997
|
}
|
|
989
998
|
}
|
|
990
999
|
|
|
991
1000
|
func stopScan() {
|
|
992
|
-
|
|
1001
|
+
scanRequested = false
|
|
1002
|
+
centralManager?.stopScan()
|
|
993
1003
|
Bridge.log("G1: Stopped scanning for devices")
|
|
994
1004
|
}
|
|
995
1005
|
|
|
@@ -1011,6 +1021,7 @@ class G1: NSObject, SGCManager {
|
|
|
1011
1021
|
|
|
1012
1022
|
func disconnect() {
|
|
1013
1023
|
isDisconnecting = true
|
|
1024
|
+
stopScan()
|
|
1014
1025
|
leftGlassUUID = nil
|
|
1015
1026
|
rightGlassUUID = nil
|
|
1016
1027
|
stopReconnectionTimer()
|
|
@@ -1042,10 +1053,13 @@ class G1: NSObject, SGCManager {
|
|
|
1042
1053
|
let data = Data(chunks[0])
|
|
1043
1054
|
// CoreCommsService.log("SEND (\(side)) \(data.hexEncodedString())")
|
|
1044
1055
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1056
|
+
// A queued command can outlive the link that enqueued it. Missing
|
|
1057
|
+
// transport during discovery/pairing is not an ACK timeout and must
|
|
1058
|
+
// not start a reconnect loop before CoreBluetooth finishes connecting.
|
|
1059
|
+
let peripheral = side == "L" ? leftPeripheral : rightPeripheral
|
|
1060
|
+
guard !isDisconnecting, peripheral?.state == .connected,
|
|
1061
|
+
getWriteCharacteristic(for: peripheral) != nil
|
|
1062
|
+
else { return }
|
|
1049
1063
|
|
|
1050
1064
|
for i in 0 ..< chunks.count - 1 {
|
|
1051
1065
|
let chunk = chunks[i]
|
|
@@ -1506,18 +1520,9 @@ extension G1 {
|
|
|
1506
1520
|
/// don't call semaphore signals here as it's handled elswhere:
|
|
1507
1521
|
private func handleInitResponse(from peripheral: CBPeripheral, success: Bool) {
|
|
1508
1522
|
if peripheral == leftPeripheral {
|
|
1509
|
-
|
|
1510
|
-
// CoreCommsService.log("G1: Left arm initialized: \(success)")
|
|
1511
|
-
setReadiness(left: true, right: nil)
|
|
1523
|
+
setReadiness(left: success, right: nil)
|
|
1512
1524
|
} else if peripheral == rightPeripheral {
|
|
1513
|
-
|
|
1514
|
-
// CoreCommsService.log("G1: Right arm initialized: \(success)")
|
|
1515
|
-
setReadiness(left: nil, right: true)
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// Only proceed if both glasses are initialized
|
|
1519
|
-
if leftInitialized, rightInitialized {
|
|
1520
|
-
setReadiness(left: true, right: true)
|
|
1525
|
+
setReadiness(left: nil, right: success)
|
|
1521
1526
|
}
|
|
1522
1527
|
}
|
|
1523
1528
|
|
|
@@ -1737,6 +1742,7 @@ extension G1 {
|
|
|
1737
1742
|
}
|
|
1738
1743
|
|
|
1739
1744
|
func getBatteryStatus() {
|
|
1745
|
+
guard !isDisconnecting, leftReady, rightReady else { return }
|
|
1740
1746
|
Bridge.log("G1: getBatteryStatus()")
|
|
1741
1747
|
let command: [UInt8] = [Commands.BLE_REQ_BATTERY.rawValue, 0x01]
|
|
1742
1748
|
queueChunks([command])
|
|
@@ -2368,6 +2374,7 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
2368
2374
|
|
|
2369
2375
|
private func startReconnectionTimer() {
|
|
2370
2376
|
Task {
|
|
2377
|
+
guard !isDisconnecting else { return }
|
|
2371
2378
|
await reconnectionManager.start { [weak self] in
|
|
2372
2379
|
guard let self else { return false }
|
|
2373
2380
|
|
|
@@ -2381,6 +2388,8 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
2381
2388
|
|
|
2382
2389
|
// Attempt to reconnect
|
|
2383
2390
|
await MainActor.run {
|
|
2391
|
+
guard !self.isDisconnecting else { return }
|
|
2392
|
+
self.scanRequested = true
|
|
2384
2393
|
self.startScan()
|
|
2385
2394
|
}
|
|
2386
2395
|
|
|
@@ -2517,18 +2526,24 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
2517
2526
|
/// called whenever bluetooth is initialized / turned on or off:
|
|
2518
2527
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
2519
2528
|
DispatchQueue.main.async { [weak self] in
|
|
2520
|
-
|
|
2529
|
+
self?.handleBluetoothState(central.state)
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2521
2532
|
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2533
|
+
func handleBluetoothState(_ state: CBManagerState) {
|
|
2534
|
+
if state == .poweredOn {
|
|
2535
|
+
Bridge.log("G1: Bluetooth was powered on")
|
|
2536
|
+
setReadiness(left: false, right: false)
|
|
2525
2537
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2538
|
+
// Discovery has no search ID, but still needs to resume after the
|
|
2539
|
+
// central manager's initial .unknown state. Preserve selected-pair
|
|
2540
|
+
// reconnects after a power cycle, unless intentionally disconnected.
|
|
2541
|
+
let hasConnectionTarget = DEVICE_SEARCH_ID != "NOT_SET" && !DEVICE_SEARCH_ID.isEmpty
|
|
2542
|
+
if !isDisconnecting, scanRequested || hasConnectionTarget {
|
|
2543
|
+
startScan()
|
|
2531
2544
|
}
|
|
2545
|
+
} else {
|
|
2546
|
+
Bridge.log("G1: Bluetooth was turned off.")
|
|
2532
2547
|
}
|
|
2533
2548
|
}
|
|
2534
2549
|
|
|
@@ -5,14 +5,14 @@ final class DashboardContentFormatterTests: XCTestCase {
|
|
|
5
5
|
func testEmptyContentReturnsStatusHeaderOnly() {
|
|
6
6
|
XCTAssertEqual(
|
|
7
7
|
DashboardContentFormatter.template(for: ""),
|
|
8
|
-
"$TIME12$ $DATE$ $GBATT$
|
|
8
|
+
"$TIME12$ $DATE$ $GBATT$"
|
|
9
9
|
)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
func testNonEmptyContentIsAppendedExactlyAfterBlankLine() {
|
|
13
13
|
XCTAssertEqual(
|
|
14
14
|
DashboardContentFormatter.template(for: " Next meeting\nRoom 2 "),
|
|
15
|
-
"$TIME12$ $DATE$ $GBATT
|
|
15
|
+
"$TIME12$ $DATE$ $GBATT$\n\n Next meeting\nRoom 2 "
|
|
16
16
|
)
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import CoreBluetooth
|
|
2
|
+
@testable import MentraBluetoothSDK
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
final class G1PairingTests: XCTestCase {
|
|
7
|
+
/// Exercise the real discovery/readiness entry points without accessing a
|
|
8
|
+
/// radio. Battery requests are observed through the SDK diagnostics.
|
|
9
|
+
private class RecordingG1: G1 {
|
|
10
|
+
var scans = 0
|
|
11
|
+
override func startScan() -> Bool {
|
|
12
|
+
scans += 1
|
|
13
|
+
return false // Central manager is still initializing.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private func withGlasses(_ test: (RecordingG1) async throws -> Void) async rethrows {
|
|
18
|
+
let store = DeviceStore.shared.store
|
|
19
|
+
let saved = store.getCategory("glasses")
|
|
20
|
+
let keys = ["leftGlassUUID", "rightGlassUUID", "leftGlassSearchID", "rightGlassSearchID"]
|
|
21
|
+
let savedDefaults = keys.map { ($0, UserDefaults.standard.object(forKey: $0)) }
|
|
22
|
+
let glasses = RecordingG1()
|
|
23
|
+
defer {
|
|
24
|
+
glasses.forget()
|
|
25
|
+
for (key, value) in savedDefaults {
|
|
26
|
+
UserDefaults.standard.set(value, forKey: key)
|
|
27
|
+
}
|
|
28
|
+
for key in store.getCategory("glasses").keys where saved[key] == nil {
|
|
29
|
+
store.remove("glasses", key)
|
|
30
|
+
}
|
|
31
|
+
for (key, value) in saved {
|
|
32
|
+
store.set("glasses", key, value)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try await test(glasses)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func testFirstDiscoveryResumesWhenBluetoothPowersOn() async {
|
|
39
|
+
await withGlasses { glasses in
|
|
40
|
+
glasses.findCompatibleDevices()
|
|
41
|
+
XCTAssertEqual(glasses.DEVICE_SEARCH_ID, "NOT_SET")
|
|
42
|
+
XCTAssertEqual(glasses.scans, 1)
|
|
43
|
+
glasses.handleBluetoothState(.poweredOn)
|
|
44
|
+
XCTAssertEqual(glasses.scans, 2)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func testStoppedDiscoveryDoesNotResumeOnLatePowerCallback() async {
|
|
49
|
+
await withGlasses { glasses in
|
|
50
|
+
glasses.findCompatibleDevices()
|
|
51
|
+
glasses.stopScan()
|
|
52
|
+
glasses.handleBluetoothState(.poweredOn)
|
|
53
|
+
XCTAssertEqual(glasses.scans, 1)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func testSelectedPairResumesButCancelledPairDoesNot() async {
|
|
58
|
+
await withGlasses { glasses in
|
|
59
|
+
glasses.connectById("59")
|
|
60
|
+
glasses.handleBluetoothState(.poweredOn)
|
|
61
|
+
XCTAssertEqual(glasses.scans, 2)
|
|
62
|
+
XCTAssertEqual(glasses.DEVICE_SEARCH_ID, "_59_")
|
|
63
|
+
// Finding both arms stops scanning, but should not prevent a
|
|
64
|
+
// selected pair from reconnecting after Bluetooth is toggled.
|
|
65
|
+
glasses.stopScan()
|
|
66
|
+
glasses.handleBluetoothState(.poweredOff)
|
|
67
|
+
glasses.handleBluetoothState(.poweredOn)
|
|
68
|
+
XCTAssertEqual(glasses.scans, 3)
|
|
69
|
+
glasses.disconnect()
|
|
70
|
+
glasses.handleBluetoothState(.poweredOn)
|
|
71
|
+
XCTAssertEqual(glasses.scans, 3)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func testBatteryWaitsForBothArmsAndRefreshesAfterReconnect() async {
|
|
76
|
+
await withGlasses { glasses in
|
|
77
|
+
var batteryRequests = 0
|
|
78
|
+
let sink = Bridge.addEventSink { event, body in
|
|
79
|
+
if event == "log", body["message"] as? String == "G1: getBatteryStatus()" {
|
|
80
|
+
batteryRequests += 1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
defer { Bridge.removeEventSink(sink) }
|
|
84
|
+
glasses.getBatteryStatus()
|
|
85
|
+
XCTAssertEqual(batteryRequests, 0)
|
|
86
|
+
glasses.setReadiness(left: true, right: nil)
|
|
87
|
+
glasses.getBatteryStatus()
|
|
88
|
+
XCTAssertEqual(batteryRequests, 0)
|
|
89
|
+
glasses.setReadiness(left: nil, right: true)
|
|
90
|
+
XCTAssertEqual(batteryRequests, 1)
|
|
91
|
+
glasses.setReadiness(left: true, right: true)
|
|
92
|
+
XCTAssertEqual(batteryRequests, 1, "Duplicate init ACKs must not poll again")
|
|
93
|
+
glasses.setReadiness(left: false, right: false)
|
|
94
|
+
glasses.getBatteryStatus()
|
|
95
|
+
XCTAssertEqual(batteryRequests, 1)
|
|
96
|
+
glasses.setReadiness(left: nil, right: true)
|
|
97
|
+
glasses.setReadiness(left: true, right: nil)
|
|
98
|
+
XCTAssertEqual(batteryRequests, 2)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func testQueuedCommandWithoutTransportDoesNotReconnect() async throws {
|
|
103
|
+
try await withGlasses { glasses in
|
|
104
|
+
glasses.queueChunks([[Commands.BLE_REQ_BATTERY.rawValue, 0x01]])
|
|
105
|
+
// The original missing-characteristic path restarted scanning after
|
|
106
|
+
// five 100ms command attempts, before either arm could connect.
|
|
107
|
+
try await Task.sleep(nanoseconds: 1_000_000_000)
|
|
108
|
+
XCTAssertEqual(glasses.scans, 0)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
final class G1ReconnectionTests: XCTestCase {
|
|
114
|
+
func testSuccessfulReconnectAllowsALaterRetryLoop() async {
|
|
115
|
+
let manager = ReconnectionManager()
|
|
116
|
+
let connected = expectation(description: "Reconnect completes")
|
|
117
|
+
await manager.start {
|
|
118
|
+
connected.fulfill()
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
await fulfillment(of: [connected], timeout: 1)
|
|
122
|
+
let running = await manager.isRunning
|
|
123
|
+
XCTAssertFalse(running)
|
|
124
|
+
|
|
125
|
+
let retry = expectation(description: "A later disconnection can reconnect")
|
|
126
|
+
await manager.start {
|
|
127
|
+
retry.fulfill()
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
await fulfillment(of: [retry], timeout: 1)
|
|
131
|
+
await manager.stop()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func testRepeatedFailuresDoNotRestartTheRetryDelay() async {
|
|
135
|
+
let manager = ReconnectionManager(intervalSeconds: 60)
|
|
136
|
+
let firstAttempt = expectation(description: "First reconnect attempt")
|
|
137
|
+
await manager.start {
|
|
138
|
+
firstAttempt.fulfill()
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
await fulfillment(of: [firstAttempt], timeout: 1)
|
|
142
|
+
|
|
143
|
+
let restarted = expectation(description: "No immediate retry for another failed command")
|
|
144
|
+
restarted.isInverted = true
|
|
145
|
+
await manager.start {
|
|
146
|
+
restarted.fulfill()
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
await fulfillment(of: [restarted], timeout: 0.1)
|
|
150
|
+
let attempts = await manager.attemptCount
|
|
151
|
+
XCTAssertEqual(attempts, 1)
|
|
152
|
+
await manager.stop()
|
|
153
|
+
|
|
154
|
+
let nextAttempt = expectation(description: "A later connection can retry")
|
|
155
|
+
await manager.start {
|
|
156
|
+
nextAttempt.fulfill()
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
await fulfillment(of: [nextAttempt], timeout: 1)
|
|
160
|
+
await manager.stop()
|
|
161
|
+
}
|
|
162
|
+
}
|
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.401",
|
|
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.401"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@types/node": "^25.9.3",
|
|
@@ -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.401",
|
|
16
|
+
"releaseSetId": "mentra-3.2.1-beta.401",
|
|
17
|
+
"sourceCommit": "86f368f9f9b543f397c0b66df76de2c46170ca3d",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.401.json",
|
|
19
|
+
"otaManifestSha256": "48700618e2ec25cf0d145f2157e0c98f07a0e902858325b851b71b987177925e"
|
|
20
20
|
})
|