@mentra/bluetooth-sdk 3.1.0-dev.7 → 3.1.0-dev.71
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 +40 -34
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +0 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +13 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +81 -69
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/build/BluetoothSdk.types.d.ts +26 -5
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +44 -0
- package/build/changelogs.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +6 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +8 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +1 -6
- package/ios/Source/DeviceManager.swift +13 -8
- package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +17 -0
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/sgcs/MentraLive.swift +93 -80
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +6 -1
- package/scripts/public-ota-api.test.mjs +37 -0
- package/src/BluetoothSdk.types.ts +28 -5
- package/src/changelogs.ts +44 -0
- package/src/generated/changelogCatalog.ts +9 -0
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +24 -0
- package/src/ota-transport/index.ts +116 -0
|
@@ -25,6 +25,35 @@ struct MentraLiveDevice {
|
|
|
25
25
|
let address: String
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
enum MentraLivePendingPairingTarget {
|
|
29
|
+
static func matches(
|
|
30
|
+
connectedName: String,
|
|
31
|
+
connectedIdentifier: String,
|
|
32
|
+
pendingName: String,
|
|
33
|
+
pendingIdentifier: String
|
|
34
|
+
) -> Bool {
|
|
35
|
+
if !pendingIdentifier.isEmpty {
|
|
36
|
+
return connectedIdentifier.caseInsensitiveCompare(pendingIdentifier) == .orderedSame
|
|
37
|
+
}
|
|
38
|
+
return !pendingName.isEmpty && connectedName == pendingName
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func shouldRecover(
|
|
42
|
+
isConnected: Bool,
|
|
43
|
+
connectedName: String,
|
|
44
|
+
connectedIdentifier: String,
|
|
45
|
+
pendingName: String,
|
|
46
|
+
pendingIdentifier: String
|
|
47
|
+
) -> Bool {
|
|
48
|
+
isConnected && matches(
|
|
49
|
+
connectedName: connectedName,
|
|
50
|
+
connectedIdentifier: connectedIdentifier,
|
|
51
|
+
pendingName: pendingName,
|
|
52
|
+
pendingIdentifier: pendingIdentifier
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
28
57
|
// MARK: - BlePhotoUploadService
|
|
29
58
|
|
|
30
59
|
class BlePhotoUploadService {
|
|
@@ -873,9 +902,10 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
873
902
|
if !isReconnectTarget && advertisesPairingFlag(advertisementData)
|
|
874
903
|
&& !isPairingDiscoverable(advertisementData)
|
|
875
904
|
{
|
|
876
|
-
//
|
|
905
|
+
// Keep nearby secure units visible with pairing-mode guidance, while RN blocks
|
|
906
|
+
// the connection until a pairable advertisement arrives.
|
|
877
907
|
Bridge.log(
|
|
878
|
-
"LIVE: Nearby \(name) is secure firmware not in pairing mode —
|
|
908
|
+
"LIVE: Nearby \(name) is secure firmware not in pairing mode — exposing as non-pairable"
|
|
879
909
|
)
|
|
880
910
|
discoveredPeripherals[name] = peripheral
|
|
881
911
|
cacheAdvPairing(
|
|
@@ -942,11 +972,32 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
942
972
|
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
943
973
|
DispatchQueue.main.async { [weak self] in
|
|
944
974
|
guard let self else { return }
|
|
975
|
+
let matchesActiveAttempt = self.connectingPeripheral === peripheral
|
|
976
|
+
guard MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
|
|
977
|
+
pairingYieldActive: self.pairingYieldActive,
|
|
978
|
+
matchesActiveAttempt: matchesActiveAttempt
|
|
979
|
+
) else {
|
|
980
|
+
Bridge.log(
|
|
981
|
+
"LIVE: Rejecting stale connection callback during pairing yield or after attempt replacement: \(peripheral.identifier)"
|
|
982
|
+
)
|
|
983
|
+
self.stopConnectionTimeout()
|
|
984
|
+
if self.connectingPeripheral === peripheral {
|
|
985
|
+
self.connectingPeripheral = nil
|
|
986
|
+
}
|
|
987
|
+
if self.connectedPeripheral === peripheral {
|
|
988
|
+
self.connectedPeripheral = nil
|
|
989
|
+
}
|
|
990
|
+
self.isConnecting = false
|
|
991
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
992
|
+
return
|
|
993
|
+
}
|
|
945
994
|
Bridge.log("Connected to GATT server, discovering services...")
|
|
946
995
|
|
|
947
996
|
self.stopConnectionTimeout()
|
|
948
997
|
self.isConnecting = false
|
|
998
|
+
self.connectingPeripheral = nil
|
|
949
999
|
self.connectedPeripheral = peripheral
|
|
1000
|
+
self.emitConnectedPendingDeviceForPairingScan()
|
|
950
1001
|
|
|
951
1002
|
// Save device name and address for future reconnection
|
|
952
1003
|
if let name = peripheral.name {
|
|
@@ -1345,56 +1396,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1345
1396
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1346
1397
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1347
1398
|
private static let loudnessGateSwitchType = 10
|
|
1348
|
-
private
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
private let advPairingDiscoverable: UInt8 = 0x01
|
|
1356
|
-
|
|
1357
|
-
// CoreBluetooth returns manufacturer data prefixed with the 2-byte company id
|
|
1358
|
-
// (little-endian) and does NOT filter by company id itself (unlike Android's
|
|
1359
|
-
// getManufacturerSpecificData(companyId)), so every reader of this data must
|
|
1360
|
-
// verify the company id before trusting any flag/trailer byte.
|
|
1361
|
-
private func mentraManufacturerData(_ advertisementData: [String: Any]) -> Data? {
|
|
1362
|
-
guard let manufData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
1363
|
-
manufData.count >= advManufCompanyIdLength
|
|
1364
|
-
else {
|
|
1365
|
-
return nil
|
|
1366
|
-
}
|
|
1367
|
-
let companyId = UInt16(manufData[0]) | (UInt16(manufData[1]) << 8)
|
|
1368
|
-
guard companyId == mentraManufacturerId else {
|
|
1369
|
-
return nil
|
|
1370
|
-
}
|
|
1371
|
-
return manufData
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/// OS-1615 ads append `flag | version | capability | code_lo | code_hi` after the
|
|
1375
|
-
/// connected byte. Field firmware uses the same 0xB822 company id but then writes
|
|
1376
|
-
/// the XOR'd Classic MAC at those offsets. Length alone is not a pairing flag.
|
|
1377
|
-
private func hasSecurePairingTrailer(_ advertisementData: [String: Any]) -> Bool {
|
|
1378
|
-
let flagIndex = advManufCompanyIdLength + advManufPairingFlagOffset
|
|
1379
|
-
let trailerBase = flagIndex + 1
|
|
1380
|
-
guard let manufData = mentraManufacturerData(advertisementData),
|
|
1381
|
-
manufData.count >= trailerBase + 4
|
|
1382
|
-
else {
|
|
1383
|
-
return false
|
|
1384
|
-
}
|
|
1385
|
-
let version = Int(manufData[trailerBase])
|
|
1386
|
-
let capability = Int(manufData[trailerBase + 1])
|
|
1387
|
-
return (1...15).contains(version) && (capability & 0x01) != 0
|
|
1399
|
+
private func pairingAdvertisement(
|
|
1400
|
+
_ advertisementData: [String: Any]
|
|
1401
|
+
) -> MentraLivePairingAdvertisement? {
|
|
1402
|
+
MentraLivePairingAdvertisement.parse(
|
|
1403
|
+
coreBluetoothManufacturerData:
|
|
1404
|
+
advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
|
1405
|
+
)
|
|
1388
1406
|
}
|
|
1389
1407
|
|
|
1390
1408
|
private func isPairingDiscoverable(_ advertisementData: [String: Any]) -> Bool {
|
|
1391
|
-
|
|
1392
|
-
let manufData = mentraManufacturerData(advertisementData)
|
|
1393
|
-
else {
|
|
1394
|
-
return false
|
|
1395
|
-
}
|
|
1396
|
-
let flagIndex = advManufCompanyIdLength + advManufPairingFlagOffset
|
|
1397
|
-
return manufData[flagIndex] == advPairingDiscoverable
|
|
1409
|
+
pairingAdvertisement(advertisementData)?.pairingMode == true
|
|
1398
1410
|
}
|
|
1399
1411
|
|
|
1400
1412
|
private struct SecurePairingTrailer {
|
|
@@ -1403,24 +1415,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1403
1415
|
let secureCapable: Bool
|
|
1404
1416
|
}
|
|
1405
1417
|
|
|
1406
|
-
/// Trailer immediately after pairing flag: version | capability | code_lo | code_hi
|
|
1407
1418
|
private func parseSecurePairingTrailer(_ advertisementData: [String: Any]) -> SecurePairingTrailer {
|
|
1408
|
-
guard
|
|
1409
|
-
let manufData = mentraManufacturerData(advertisementData)
|
|
1410
|
-
else {
|
|
1419
|
+
guard let advertisement = pairingAdvertisement(advertisementData) else {
|
|
1411
1420
|
return SecurePairingTrailer(pairingMode: false, pairingCode: nil, secureCapable: false)
|
|
1412
1421
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
let code = String(format: "%02X%02X", codeHi, codeLo)
|
|
1419
|
-
return SecurePairingTrailer(pairingMode: pairingMode, pairingCode: code, secureCapable: true)
|
|
1422
|
+
return SecurePairingTrailer(
|
|
1423
|
+
pairingMode: advertisement.pairingMode,
|
|
1424
|
+
pairingCode: advertisement.pairingCode,
|
|
1425
|
+
secureCapable: true
|
|
1426
|
+
)
|
|
1420
1427
|
}
|
|
1421
1428
|
|
|
1422
1429
|
private func advertisesPairingFlag(_ advertisementData: [String: Any]) -> Bool {
|
|
1423
|
-
|
|
1430
|
+
pairingAdvertisement(advertisementData) != nil
|
|
1424
1431
|
}
|
|
1425
1432
|
|
|
1426
1433
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
@@ -1642,6 +1649,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1642
1649
|
|
|
1643
1650
|
// State Tracking
|
|
1644
1651
|
private var isScanning = false
|
|
1652
|
+
private var manualDiscoveryActive = false
|
|
1645
1653
|
private var isConnecting = false
|
|
1646
1654
|
private var isKilled = false
|
|
1647
1655
|
/// Glasses opened pairing window — stand down without forgetting identity/bonds.
|
|
@@ -1796,7 +1804,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1796
1804
|
// clear the saved device name:
|
|
1797
1805
|
UserDefaults.standard.set("", forKey: PREFS_DEVICE_NAME)
|
|
1798
1806
|
|
|
1807
|
+
manualDiscoveryActive = true
|
|
1799
1808
|
startScan()
|
|
1809
|
+
emitConnectedPendingDeviceForPairingScan()
|
|
1800
1810
|
}
|
|
1801
1811
|
}
|
|
1802
1812
|
|
|
@@ -2388,10 +2398,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2388
2398
|
|
|
2389
2399
|
centralManager?.scanForPeripherals(withServices: nil, options: scanOptions)
|
|
2390
2400
|
|
|
2391
|
-
// Fresh advertisements refill pairing metadata. Re-emitting the last scan's
|
|
2392
|
-
// cache would keep a unit pairable after it left pairing mode.
|
|
2393
|
-
emitConnectedDeviceForPairingScan()
|
|
2394
|
-
|
|
2395
2401
|
// var dName = DeviceManager.shared.deviceName
|
|
2396
2402
|
// if dName.isEmpty {
|
|
2397
2403
|
// dName = "MENTRA_LIVE"
|
|
@@ -2409,6 +2415,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2409
2415
|
}
|
|
2410
2416
|
|
|
2411
2417
|
func stopScan() {
|
|
2418
|
+
manualDiscoveryActive = false
|
|
2412
2419
|
guard isScanning else { return }
|
|
2413
2420
|
|
|
2414
2421
|
centralManager?.stopScan()
|
|
@@ -2914,25 +2921,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2914
2921
|
let windowMs = max(5_000, min(180_000, json["window_ms"] as? Int ?? 120_000))
|
|
2915
2922
|
Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
|
|
2916
2923
|
enterPairingYield(windowMs: windowMs)
|
|
2917
|
-
|
|
2924
|
+
let body: [String: Any] = [
|
|
2918
2925
|
"window_ms": windowMs,
|
|
2919
2926
|
"reason": json["reason"] as? String ?? "user_gesture",
|
|
2920
2927
|
]
|
|
2921
|
-
if let txn = json["txn"] {
|
|
2922
|
-
body["txn"] = txn
|
|
2923
|
-
}
|
|
2924
2928
|
Bridge.sendTypedMessage("entering_pairing_mode", body: body)
|
|
2925
2929
|
|
|
2926
2930
|
case "pairing_info":
|
|
2927
2931
|
Bridge.sendPairingInfo(
|
|
2928
2932
|
hadPreviousBond: json["had_previous_bond"] as? Bool ?? false,
|
|
2929
|
-
transferId: json["transfer_id"] as? String,
|
|
2930
2933
|
pairingCode: json["pairing_code"] as? String,
|
|
2931
2934
|
classicBondReady: json["classic_bond_ready"] as? Bool ?? false,
|
|
2932
2935
|
// Legacy firmware that omits this field is not secure-capable.
|
|
2933
2936
|
securePairingCapable: json["secure_pairing_capable"] as? Bool ?? false,
|
|
2934
|
-
protocolVersion: json["protocol_version"] as? Int ?? 1
|
|
2935
|
-
binding: json["binding"] as? String
|
|
2937
|
+
protocolVersion: json["protocol_version"] as? Int ?? 1
|
|
2936
2938
|
)
|
|
2937
2939
|
|
|
2938
2940
|
case "imu_response", "imu_stream_response", "imu_gesture_response",
|
|
@@ -5531,22 +5533,33 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5531
5533
|
|
|
5532
5534
|
// MARK: - Event Emission
|
|
5533
5535
|
|
|
5534
|
-
///
|
|
5535
|
-
///
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5536
|
+
/// A selected pairing target can stop advertising once GATT connects, before readiness
|
|
5537
|
+
/// promotes it to the default device. Re-emit only that explicit pending target; an
|
|
5538
|
+
/// established owner's connected glasses have no pending identity and remain hidden.
|
|
5539
|
+
private func emitConnectedPendingDeviceForPairingScan() {
|
|
5540
|
+
guard manualDiscoveryActive, !pairingYieldActive, let peripheral = connectedPeripheral,
|
|
5541
|
+
let name = peripheral.name,
|
|
5542
|
+
MentraLivePendingPairingTarget.shouldRecover(
|
|
5543
|
+
isConnected: peripheral.state == .connected,
|
|
5544
|
+
connectedName: name,
|
|
5545
|
+
connectedIdentifier: peripheral.identifier.uuidString,
|
|
5546
|
+
pendingName: DeviceStore.shared.get("bluetooth", "pending_device_name") as? String ?? "",
|
|
5547
|
+
pendingIdentifier: DeviceStore.shared.get("bluetooth", "pending_device_address") as? String ?? ""
|
|
5548
|
+
)
|
|
5540
5549
|
else {
|
|
5541
5550
|
return
|
|
5542
5551
|
}
|
|
5543
|
-
|
|
5552
|
+
|
|
5553
|
+
Bridge.log("LIVE: Pairing scan: recovering connected pending target \(name) (\(peripheral.identifier))")
|
|
5544
5554
|
emitDiscoveredDevice(
|
|
5545
5555
|
name,
|
|
5546
5556
|
identifier: peripheral.identifier.uuidString,
|
|
5547
5557
|
pairingMode: true,
|
|
5548
5558
|
pairingCode: nil,
|
|
5549
|
-
securePairingCapable:
|
|
5559
|
+
securePairingCapable: DeviceStore.shared.get(
|
|
5560
|
+
"bluetooth",
|
|
5561
|
+
"pending_device_secure_pairing_capable"
|
|
5562
|
+
) as? Bool
|
|
5550
5563
|
)
|
|
5551
5564
|
}
|
|
5552
5565
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct MentraLivePairingAdvertisement: Equatable {
|
|
4
|
+
let pairingMode: Bool
|
|
5
|
+
let pairingCode: String
|
|
6
|
+
|
|
7
|
+
private static let manufacturerId: UInt16 = 0xB822
|
|
8
|
+
private static let companyIdLength = 2
|
|
9
|
+
private static let pairingFlagOffset = 5
|
|
10
|
+
private static let pairingDiscoverable: UInt8 = 0x01
|
|
11
|
+
private static let protocolVersionOffset = pairingFlagOffset + 1
|
|
12
|
+
private static let capabilityOffset = protocolVersionOffset + 1
|
|
13
|
+
private static let codeLowOffset = capabilityOffset + 1
|
|
14
|
+
private static let codeHighOffset = codeLowOffset + 1
|
|
15
|
+
private static let magicFirstOffset = codeHighOffset + 1
|
|
16
|
+
private static let magicSecondOffset = magicFirstOffset + 1
|
|
17
|
+
private static let protocolVersionRange = 2 ... 15
|
|
18
|
+
private static let securePairingCapability = 0x01
|
|
19
|
+
private static let magicFirst: UInt8 = 0x4D // M
|
|
20
|
+
private static let magicSecond: UInt8 = 0x50 // P
|
|
21
|
+
|
|
22
|
+
/// Parses CoreBluetooth manufacturer data, including its two-byte company-id prefix.
|
|
23
|
+
///
|
|
24
|
+
/// Legacy firmware stores an XOR'd Classic MAC where the original pairing implementation
|
|
25
|
+
/// expected version and capability bytes. Requiring the `MP` marker makes the formats
|
|
26
|
+
/// unambiguous instead of probabilistically classifying MAC bytes as a secure trailer.
|
|
27
|
+
static func parse(coreBluetoothManufacturerData data: Data?) -> MentraLivePairingAdvertisement? {
|
|
28
|
+
guard let data, data.count > companyIdLength + magicSecondOffset else {
|
|
29
|
+
return nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let companyId = UInt16(data[0]) | (UInt16(data[1]) << 8)
|
|
33
|
+
guard companyId == manufacturerId else {
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let payloadBase = companyIdLength
|
|
38
|
+
let version = Int(data[payloadBase + protocolVersionOffset])
|
|
39
|
+
let capability = Int(data[payloadBase + capabilityOffset])
|
|
40
|
+
guard protocolVersionRange.contains(version),
|
|
41
|
+
capability & securePairingCapability != 0,
|
|
42
|
+
data[payloadBase + magicFirstOffset] == magicFirst,
|
|
43
|
+
data[payloadBase + magicSecondOffset] == magicSecond
|
|
44
|
+
else {
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let codeLow = Int(data[payloadBase + codeLowOffset])
|
|
49
|
+
let codeHigh = Int(data[payloadBase + codeHighOffset])
|
|
50
|
+
return MentraLivePairingAdvertisement(
|
|
51
|
+
pairingMode: data[payloadBase + pairingFlagOffset] == pairingDiscoverable,
|
|
52
|
+
pairingCode: String(format: "%02X%02X", codeHigh, codeLow)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
enum MentraLiveConnectionAttemptPolicy {
|
|
58
|
+
static func shouldAcceptDidConnect(
|
|
59
|
+
pairingYieldActive: Bool,
|
|
60
|
+
matchesActiveAttempt: Bool
|
|
61
|
+
) -> Bool {
|
|
62
|
+
!pairingYieldActive && matchesActiveAttempt
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
@testable import MentraBluetoothSDK
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class MentraLivePairingAdvertisementTests: XCTestCase {
|
|
6
|
+
func testConnectedPairingRecoveryRequiresExplicitPendingTarget() {
|
|
7
|
+
XCTAssertFalse(
|
|
8
|
+
MentraLivePendingPairingTarget.matches(
|
|
9
|
+
connectedName: "MENTRA_LIVE_BLE_OWNER",
|
|
10
|
+
connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
11
|
+
pendingName: "",
|
|
12
|
+
pendingIdentifier: ""
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
XCTAssertTrue(
|
|
16
|
+
MentraLivePendingPairingTarget.matches(
|
|
17
|
+
connectedName: "MENTRA_LIVE_BLE_TARGET",
|
|
18
|
+
connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
19
|
+
pendingName: "MENTRA_LIVE_BLE_TARGET",
|
|
20
|
+
pendingIdentifier: ""
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
XCTAssertTrue(
|
|
24
|
+
MentraLivePendingPairingTarget.matches(
|
|
25
|
+
connectedName: "MENTRA_LIVE_BLE_TARGET",
|
|
26
|
+
connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
27
|
+
pendingName: "OTHER",
|
|
28
|
+
pendingIdentifier: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
XCTAssertFalse(
|
|
32
|
+
MentraLivePendingPairingTarget.matches(
|
|
33
|
+
connectedName: "MENTRA_LIVE_BLE_TARGET",
|
|
34
|
+
connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
35
|
+
pendingName: "MENTRA_LIVE_BLE_TARGET",
|
|
36
|
+
pendingIdentifier: "11111111-2222-3333-4444-555555555555"
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testConnectedPairingRecoveryRequiresActiveGattConnection() {
|
|
42
|
+
XCTAssertFalse(
|
|
43
|
+
MentraLivePendingPairingTarget.shouldRecover(
|
|
44
|
+
isConnected: false,
|
|
45
|
+
connectedName: "MENTRA_LIVE_BLE_TARGET",
|
|
46
|
+
connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
47
|
+
pendingName: "MENTRA_LIVE_BLE_TARGET",
|
|
48
|
+
pendingIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
XCTAssertTrue(
|
|
52
|
+
MentraLivePendingPairingTarget.shouldRecover(
|
|
53
|
+
isConnected: true,
|
|
54
|
+
connectedName: "MENTRA_LIVE_BLE_TARGET",
|
|
55
|
+
connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
56
|
+
pendingName: "MENTRA_LIVE_BLE_TARGET",
|
|
57
|
+
pendingIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func testConnectionCallbackPolicyRejectsPairingYieldAndStaleAttempts() {
|
|
63
|
+
XCTAssertFalse(
|
|
64
|
+
MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
|
|
65
|
+
pairingYieldActive: true,
|
|
66
|
+
matchesActiveAttempt: true
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
XCTAssertFalse(
|
|
70
|
+
MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
|
|
71
|
+
pairingYieldActive: false,
|
|
72
|
+
matchesActiveAttempt: false
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
XCTAssertTrue(
|
|
76
|
+
MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
|
|
77
|
+
pairingYieldActive: false,
|
|
78
|
+
matchesActiveAttempt: true
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func testParsesMarkedSecurePairingAdvertisement() {
|
|
84
|
+
let result = MentraLivePairingAdvertisement.parse(
|
|
85
|
+
coreBluetoothManufacturerData: securePayload(pairingFlag: 1)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
XCTAssertEqual(result?.pairingCode, "1234")
|
|
89
|
+
XCTAssertEqual(result?.pairingMode, true)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func testParsesMarkedOwnedAdvertisement() {
|
|
93
|
+
let result = MentraLivePairingAdvertisement.parse(
|
|
94
|
+
coreBluetoothManufacturerData: securePayload(pairingFlag: 0)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
XCTAssertEqual(result?.pairingCode, "1234")
|
|
98
|
+
XCTAssertEqual(result?.pairingMode, false)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func testRejectsLegacyPayloadThatMatchesOldVersionCapabilityHeuristic() {
|
|
102
|
+
var payload = legacyPayload()
|
|
103
|
+
payload[2 + 5] = 0
|
|
104
|
+
payload[2 + 6] = 1
|
|
105
|
+
payload[2 + 7] = 1
|
|
106
|
+
payload[2 + 8] = 0x34
|
|
107
|
+
payload[2 + 9] = 0x12
|
|
108
|
+
|
|
109
|
+
XCTAssertNil(
|
|
110
|
+
MentraLivePairingAdvertisement.parse(coreBluetoothManufacturerData: payload)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func testRejectsUnmarkedFirstGenerationSecureTrailer() {
|
|
115
|
+
var payload = securePayload(pairingFlag: 1)
|
|
116
|
+
payload[2 + 10] = 0x11
|
|
117
|
+
payload[2 + 11] = 0x22
|
|
118
|
+
|
|
119
|
+
XCTAssertNil(
|
|
120
|
+
MentraLivePairingAdvertisement.parse(coreBluetoothManufacturerData: payload)
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func testRejectsEveryLegacyVersionAndCapabilityCombination() {
|
|
125
|
+
for version in UInt8.min ... UInt8.max {
|
|
126
|
+
for capability in UInt8.min ... UInt8.max {
|
|
127
|
+
var payload = legacyPayload()
|
|
128
|
+
payload[2 + 5] = 1
|
|
129
|
+
payload[2 + 6] = version
|
|
130
|
+
payload[2 + 7] = capability
|
|
131
|
+
payload[2 + 10] = 0x4D
|
|
132
|
+
// Offset 11 is padding in the legacy format, so it cannot contain the second
|
|
133
|
+
// non-zero marker byte.
|
|
134
|
+
payload[2 + 11] = 0
|
|
135
|
+
XCTAssertNil(
|
|
136
|
+
MentraLivePairingAdvertisement.parse(coreBluetoothManufacturerData: payload)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func legacyPayload() -> Data {
|
|
143
|
+
var data = Data(repeating: 0, count: 29)
|
|
144
|
+
data[0] = 0x22
|
|
145
|
+
data[1] = 0xB8
|
|
146
|
+
return data
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func securePayload(pairingFlag: UInt8) -> Data {
|
|
150
|
+
var data = legacyPayload()
|
|
151
|
+
data[2 + 5] = pairingFlag
|
|
152
|
+
data[2 + 6] = 2
|
|
153
|
+
data[2 + 7] = 1
|
|
154
|
+
data[2 + 8] = 0x34
|
|
155
|
+
data[2 + 9] = 0x12
|
|
156
|
+
data[2 + 10] = 0x4D
|
|
157
|
+
data[2 + 11] = 0x50
|
|
158
|
+
return data
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class ReleaseChangelogTests: XCTestCase {
|
|
5
|
+
func testIncludesTargetNotesForTransitionWithinOneReleaseTrain() throws {
|
|
6
|
+
let changelogs = try ReleaseChangelogCatalog.select(
|
|
7
|
+
fromVersion: "3.1.0-dev.2",
|
|
8
|
+
toVersion: "3.1.0-beta.8"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
XCTAssertEqual(changelogs.map(\.version), ["3.1.0"])
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.1.0-dev.
|
|
3
|
+
"version": "3.1.0-dev.71",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"types": "./build/photo-receiver/index.d.ts",
|
|
33
33
|
"default": "./build/photo-receiver/index.js"
|
|
34
34
|
},
|
|
35
|
+
"./ota-transport": {
|
|
36
|
+
"react-native": "./src/ota-transport/index.ts",
|
|
37
|
+
"types": "./build/ota-transport/index.d.ts",
|
|
38
|
+
"default": "./build/ota-transport/index.js"
|
|
39
|
+
},
|
|
35
40
|
"./debug": {
|
|
36
41
|
"react-native": "./src/debug.ts",
|
|
37
42
|
"types": "./build/debug.d.ts",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import {readFileSync} from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import test from "node:test"
|
|
5
|
+
import {fileURLToPath} from "node:url"
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
8
|
+
|
|
9
|
+
test("publishes the supported OTA transport entrypoint", () => {
|
|
10
|
+
const manifest = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"))
|
|
11
|
+
assert.deepEqual(manifest.exports["./ota-transport"], {
|
|
12
|
+
"react-native": "./src/ota-transport/index.ts",
|
|
13
|
+
"types": "./build/ota-transport/index.d.ts",
|
|
14
|
+
"default": "./build/ota-transport/index.js",
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const source = readFileSync(path.join(packageRoot, "src/ota-transport/index.ts"), "utf8")
|
|
18
|
+
assert.match(source, /export const otaLocalNetwork/)
|
|
19
|
+
assert.match(source, /export const otaServer/)
|
|
20
|
+
assert.doesNotMatch(source, /export \{default as Mentra(LocalNetwork|OtaServer)/)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("keeps restart signals and correlated OTA status on the public root", () => {
|
|
24
|
+
const source = readFileSync(path.join(packageRoot, "src/index.ts"), "utf8")
|
|
25
|
+
for (const required of [
|
|
26
|
+
'"glasses_session_changed"',
|
|
27
|
+
'"mtk_update_complete"',
|
|
28
|
+
"queryOtaStatus",
|
|
29
|
+
"subscribeGlassesStatus",
|
|
30
|
+
"subscribeBluetoothStatus",
|
|
31
|
+
"setSystemTime",
|
|
32
|
+
"ping",
|
|
33
|
+
]) {
|
|
34
|
+
assert.ok(source.includes(required), `missing public OTA primitive: ${required}`)
|
|
35
|
+
}
|
|
36
|
+
assert.match(source, /Unsupported BluetoothSdk event/)
|
|
37
|
+
})
|
|
@@ -652,20 +652,15 @@ export type PairFailureEvent = {
|
|
|
652
652
|
|
|
653
653
|
export type PairingInfoEvent = {
|
|
654
654
|
had_previous_bond: boolean
|
|
655
|
-
/** 16-char uppercase hex transfer id when secure pairing is active. */
|
|
656
|
-
transfer_id?: string
|
|
657
655
|
pairing_code?: string
|
|
658
656
|
classic_bond_ready?: boolean
|
|
659
657
|
secure_pairing_capable?: boolean
|
|
660
658
|
protocol_version?: number
|
|
661
|
-
/** Credential binding mode negotiated for this transfer, when reported by the glasses. */
|
|
662
|
-
binding?: "ctkd" | "temporal" | "none" | string
|
|
663
659
|
}
|
|
664
660
|
|
|
665
661
|
export type EnteringPairingModeEvent = {
|
|
666
662
|
window_ms: number
|
|
667
663
|
reason?: string
|
|
668
|
-
txn?: number
|
|
669
664
|
}
|
|
670
665
|
|
|
671
666
|
export type OwnerReplacedEvent = {
|
|
@@ -1048,6 +1043,10 @@ export type BluetoothSdkEventMap = {
|
|
|
1048
1043
|
mic_lc3: MicLc3Event
|
|
1049
1044
|
mic_health: MicHealthEvent
|
|
1050
1045
|
stream_status: StreamStatusEvent
|
|
1046
|
+
/** Mentra Live MTK updater completed and the glasses are about to restart. */
|
|
1047
|
+
mtk_update_complete: MtkUpdateCompleteEvent
|
|
1048
|
+
/** The ASG process restarted while the BES kept the BLE connection alive. */
|
|
1049
|
+
glasses_session_changed: GlassesSessionChangedEvent
|
|
1051
1050
|
ota_start_ack: OtaStartAckEvent
|
|
1052
1051
|
ota_status: OtaStatusEvent
|
|
1053
1052
|
ar99_ota_status: Ar99OtaStatusEvent
|
|
@@ -1073,6 +1072,15 @@ export interface BluetoothSdkPublicModule {
|
|
|
1073
1072
|
listener: BluetoothSdkEventListener<EventName>,
|
|
1074
1073
|
): BluetoothSdkSubscription
|
|
1075
1074
|
|
|
1075
|
+
/** Read an immutable snapshot of the current glasses state. */
|
|
1076
|
+
getGlassesStatus(): Promise<PublicGlassesStatus>
|
|
1077
|
+
/** Read an immutable snapshot of the phone Bluetooth adapter state. */
|
|
1078
|
+
getBluetoothStatus(): Promise<PublicBluetoothStatus>
|
|
1079
|
+
/** Observe immutable glasses-state patches. Returns an unsubscribe function. */
|
|
1080
|
+
subscribeGlassesStatus(listener: (changed: Partial<PublicGlassesStatus>) => void): () => void
|
|
1081
|
+
/** Observe immutable Bluetooth-state patches. Returns an unsubscribe function. */
|
|
1082
|
+
subscribeBluetoothStatus(listener: (changed: Partial<PublicBluetoothStatus>) => void): () => void
|
|
1083
|
+
|
|
1076
1084
|
getDefaultDevice(): Promise<Device | null>
|
|
1077
1085
|
setDefaultDevice(device: Device | null): Promise<void>
|
|
1078
1086
|
clearDefaultDevice(): Promise<void>
|
|
@@ -1094,11 +1102,15 @@ export interface BluetoothSdkPublicModule {
|
|
|
1094
1102
|
setHeadUpAngle(angleDegrees: number): Promise<void>
|
|
1095
1103
|
setImuEnabled(enabled: boolean): Promise<void>
|
|
1096
1104
|
setScreenDisabled(disabled: boolean): Promise<void>
|
|
1105
|
+
/** Keep legacy Mentra Live OTA sessions awake. Modern sessions normally do not require this. */
|
|
1106
|
+
ping(): Promise<void>
|
|
1097
1107
|
|
|
1098
1108
|
requestWifiScan(): Promise<WifiSearchResult[]>
|
|
1099
1109
|
sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
|
|
1100
1110
|
forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
|
|
1101
1111
|
setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
|
|
1112
|
+
/** Set the glasses clock from the phone after an OTA clock-skew failure. */
|
|
1113
|
+
setSystemTime(timestampMs: number): Promise<void>
|
|
1102
1114
|
/** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
|
|
1103
1115
|
setWifiAdbState(enabled: boolean): Promise<void>
|
|
1104
1116
|
|
|
@@ -1187,8 +1199,12 @@ export interface BluetoothSdkPublicModule {
|
|
|
1187
1199
|
getOtaVersionUrl(): string
|
|
1188
1200
|
/** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
|
|
1189
1201
|
checkForOtaUpdate(): Promise<boolean>
|
|
1202
|
+
/** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
|
|
1203
|
+
getReleaseChangelogs(fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[]
|
|
1190
1204
|
/** Start OTA from the configured or explicitly supplied manifest URL. */
|
|
1191
1205
|
startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
|
|
1206
|
+
/** Query the active OTA session and return the correlated status response. */
|
|
1207
|
+
queryOtaStatus(): Promise<OtaQueryResult>
|
|
1192
1208
|
startAr99OtaFromFile(path: string): Promise<boolean>
|
|
1193
1209
|
cancelAr99Ota(): Promise<void>
|
|
1194
1210
|
sendAr99FactoryReset(): Promise<void>
|
|
@@ -1267,6 +1283,13 @@ export interface OtaUpdateInfo {
|
|
|
1267
1283
|
isDowngrade?: boolean
|
|
1268
1284
|
}
|
|
1269
1285
|
|
|
1286
|
+
export interface ReleaseChangelog {
|
|
1287
|
+
/** Base production version, for example `3.1.0`. */
|
|
1288
|
+
version: string
|
|
1289
|
+
/** Markdown body authored in `/changelogs/<version>.md`. */
|
|
1290
|
+
markdown: string
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1270
1293
|
export interface OtaProgress {
|
|
1271
1294
|
stage: OtaStage
|
|
1272
1295
|
status: OtaProgressStatus
|