@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.7
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 +38 -2
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +198 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1174 -191
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +61 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +47 -3
- package/ios/Source/DeviceManager.swift +135 -10
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +74 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +439 -30
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/package.json +5 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +72 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +19 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
|
@@ -56,6 +56,9 @@ abstract class SGCManager {
|
|
|
56
56
|
abstract fun stopStream()
|
|
57
57
|
abstract fun sendStreamKeepAlive(message: MutableMap<String, Any>)
|
|
58
58
|
abstract fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean)
|
|
59
|
+
open fun queryVideoRecordingStatus(requestId: String) {
|
|
60
|
+
Bridge.log("SGC: queryVideoRecordingStatus operation not supported")
|
|
61
|
+
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Start video recording with optional per-recording resolution/fps. A width,
|
|
@@ -308,6 +311,11 @@ abstract class SGCManager {
|
|
|
308
311
|
Bridge.log("SGC: sendSetSystemTime not supported on $type")
|
|
309
312
|
}
|
|
310
313
|
|
|
314
|
+
/** Enable/disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
|
|
315
|
+
open fun sendWifiAdbState(enabled: Boolean) {
|
|
316
|
+
Bridge.log("SGC: sendWifiAdbState not supported on $type")
|
|
317
|
+
}
|
|
318
|
+
|
|
311
319
|
// User Context (for crash reporting)
|
|
312
320
|
abstract fun sendUserEmailToGlasses(email: String)
|
|
313
321
|
|
|
@@ -43,6 +43,7 @@ internal data class GlassesStatus(
|
|
|
43
43
|
val besFirmwareVersion: String,
|
|
44
44
|
val mtkFirmwareVersion: String,
|
|
45
45
|
val bluetoothMacAddress: String,
|
|
46
|
+
val wifiMacAddress: String,
|
|
46
47
|
val leftMacAddress: String,
|
|
47
48
|
val rightMacAddress: String,
|
|
48
49
|
val macAddress: String,
|
|
@@ -50,6 +51,7 @@ internal data class GlassesStatus(
|
|
|
50
51
|
val systemTimeMs: Long?,
|
|
51
52
|
val otaVersionUrl: String,
|
|
52
53
|
val appVersion: String,
|
|
54
|
+
val hotspotOtaVersion: Int,
|
|
53
55
|
val bluetoothName: String,
|
|
54
56
|
val serialNumber: String,
|
|
55
57
|
val style: String,
|
|
@@ -85,12 +87,14 @@ internal data class GlassesStatus(
|
|
|
85
87
|
"besFirmwareVersion" to besFirmwareVersion,
|
|
86
88
|
"mtkFirmwareVersion" to mtkFirmwareVersion,
|
|
87
89
|
"bluetoothMacAddress" to bluetoothMacAddress,
|
|
90
|
+
"wifiMacAddress" to wifiMacAddress,
|
|
88
91
|
"leftMacAddress" to leftMacAddress,
|
|
89
92
|
"rightMacAddress" to rightMacAddress,
|
|
90
93
|
"macAddress" to macAddress,
|
|
91
94
|
"buildNumber" to buildNumber,
|
|
92
95
|
"otaVersionUrl" to otaVersionUrl,
|
|
93
96
|
"appVersion" to appVersion,
|
|
97
|
+
"hotspotOtaVersion" to hotspotOtaVersion,
|
|
94
98
|
"bluetoothName" to bluetoothName,
|
|
95
99
|
"serialNumber" to serialNumber,
|
|
96
100
|
"style" to style,
|
|
@@ -134,6 +138,7 @@ internal data class GlassesStatus(
|
|
|
134
138
|
besFirmwareVersion = stringValue(values, "besFirmwareVersion") ?: "",
|
|
135
139
|
mtkFirmwareVersion = stringValue(values, "mtkFirmwareVersion") ?: "",
|
|
136
140
|
bluetoothMacAddress = stringValue(values, "bluetoothMacAddress") ?: "",
|
|
141
|
+
wifiMacAddress = stringValue(values, "wifiMacAddress") ?: "",
|
|
137
142
|
leftMacAddress = stringValue(values, "leftMacAddress") ?: "",
|
|
138
143
|
rightMacAddress = stringValue(values, "rightMacAddress") ?: "",
|
|
139
144
|
macAddress = stringValue(values, "macAddress") ?: "",
|
|
@@ -141,6 +146,7 @@ internal data class GlassesStatus(
|
|
|
141
146
|
systemTimeMs = longValue(values, "systemTimeMs"),
|
|
142
147
|
otaVersionUrl = stringValue(values, "otaVersionUrl") ?: "",
|
|
143
148
|
appVersion = stringValue(values, "appVersion") ?: "",
|
|
149
|
+
hotspotOtaVersion = numberValue(values, "hotspotOtaVersion") ?: 0,
|
|
144
150
|
bluetoothName = stringValue(values, "bluetoothName") ?: "",
|
|
145
151
|
serialNumber = stringValue(values, "serialNumber") ?: "",
|
|
146
152
|
style = stringValue(values, "style") ?: "",
|
|
@@ -173,6 +179,7 @@ data class VersionInfoResult(
|
|
|
173
179
|
val systemTimeMs: Long?,
|
|
174
180
|
val otaVersionUrl: String,
|
|
175
181
|
val appVersion: String,
|
|
182
|
+
val hotspotOtaVersion: Int,
|
|
176
183
|
) {
|
|
177
184
|
internal fun toMap(): Map<String, Any> =
|
|
178
185
|
buildMap {
|
|
@@ -184,6 +191,7 @@ data class VersionInfoResult(
|
|
|
184
191
|
systemTimeMs?.let { put("systemTimeMs", it) }
|
|
185
192
|
put("otaVersionUrl", otaVersionUrl)
|
|
186
193
|
put("appVersion", appVersion)
|
|
194
|
+
put("hotspotOtaVersion", hotspotOtaVersion)
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
companion object {
|
|
@@ -197,6 +205,7 @@ data class VersionInfoResult(
|
|
|
197
205
|
systemTimeMs = status.systemTimeMs,
|
|
198
206
|
otaVersionUrl = status.otaVersionUrl,
|
|
199
207
|
appVersion = status.appVersion,
|
|
208
|
+
hotspotOtaVersion = status.hotspotOtaVersion,
|
|
200
209
|
)
|
|
201
210
|
|
|
202
211
|
internal fun fromMap(values: Map<String, Any>): VersionInfoResult =
|
|
@@ -209,6 +218,8 @@ data class VersionInfoResult(
|
|
|
209
218
|
systemTimeMs = longValue(values, "systemTimeMs", "system_time_ms"),
|
|
210
219
|
otaVersionUrl = stringValue(values, "otaVersionUrl", "ota_version_url") ?: "",
|
|
211
220
|
appVersion = stringValue(values, "appVersion", "app_version") ?: "",
|
|
221
|
+
hotspotOtaVersion =
|
|
222
|
+
numberValue(values, "hotspotOtaVersion", "hotspot_ota_version") ?: 0,
|
|
212
223
|
)
|
|
213
224
|
}
|
|
214
225
|
}
|
|
@@ -373,12 +384,14 @@ internal data class GlassesStatusUpdate(
|
|
|
373
384
|
val besFirmwareVersion: String? = null,
|
|
374
385
|
val mtkFirmwareVersion: String? = null,
|
|
375
386
|
val bluetoothMacAddress: String? = null,
|
|
387
|
+
val wifiMacAddress: String? = null,
|
|
376
388
|
val leftMacAddress: String? = null,
|
|
377
389
|
val rightMacAddress: String? = null,
|
|
378
390
|
val macAddress: String? = null,
|
|
379
391
|
val buildNumber: String? = null,
|
|
380
392
|
val otaVersionUrl: String? = null,
|
|
381
393
|
val appVersion: String? = null,
|
|
394
|
+
val hotspotOtaVersion: Int? = null,
|
|
382
395
|
val bluetoothName: String? = null,
|
|
383
396
|
val serialNumber: String? = null,
|
|
384
397
|
val style: String? = null,
|
|
@@ -422,12 +435,14 @@ internal data class GlassesStatusUpdate(
|
|
|
422
435
|
putIfNotNull("besFirmwareVersion", besFirmwareVersion)
|
|
423
436
|
putIfNotNull("mtkFirmwareVersion", mtkFirmwareVersion)
|
|
424
437
|
putIfNotNull("bluetoothMacAddress", bluetoothMacAddress)
|
|
438
|
+
putIfNotNull("wifiMacAddress", wifiMacAddress)
|
|
425
439
|
putIfNotNull("leftMacAddress", leftMacAddress)
|
|
426
440
|
putIfNotNull("rightMacAddress", rightMacAddress)
|
|
427
441
|
putIfNotNull("macAddress", macAddress)
|
|
428
442
|
putIfNotNull("buildNumber", buildNumber)
|
|
429
443
|
putIfNotNull("otaVersionUrl", otaVersionUrl)
|
|
430
444
|
putIfNotNull("appVersion", appVersion)
|
|
445
|
+
putIfNotNull("hotspotOtaVersion", hotspotOtaVersion)
|
|
431
446
|
putIfNotNull("bluetoothName", bluetoothName)
|
|
432
447
|
putIfNotNull("serialNumber", serialNumber)
|
|
433
448
|
putIfNotNull("style", style)
|
|
@@ -470,12 +485,14 @@ internal data class GlassesStatusUpdate(
|
|
|
470
485
|
besFirmwareVersion = optionalStringValue(values, "besFirmwareVersion"),
|
|
471
486
|
mtkFirmwareVersion = optionalStringValue(values, "mtkFirmwareVersion"),
|
|
472
487
|
bluetoothMacAddress = optionalStringValue(values, "bluetoothMacAddress"),
|
|
488
|
+
wifiMacAddress = optionalStringValue(values, "wifiMacAddress"),
|
|
473
489
|
leftMacAddress = optionalStringValue(values, "leftMacAddress"),
|
|
474
490
|
rightMacAddress = optionalStringValue(values, "rightMacAddress"),
|
|
475
491
|
macAddress = optionalStringValue(values, "macAddress"),
|
|
476
492
|
buildNumber = optionalStringValue(values, "buildNumber"),
|
|
477
493
|
otaVersionUrl = optionalStringValue(values, "otaVersionUrl"),
|
|
478
494
|
appVersion = optionalStringValue(values, "appVersion"),
|
|
495
|
+
hotspotOtaVersion = optionalNumberValue(values, "hotspotOtaVersion"),
|
|
479
496
|
bluetoothName = optionalStringValue(values, "bluetoothName"),
|
|
480
497
|
serialNumber = optionalStringValue(values, "serialNumber"),
|
|
481
498
|
style = optionalStringValue(values, "style"),
|
|
@@ -43,6 +43,9 @@ data class Device(
|
|
|
43
43
|
val rssi: Int? = null,
|
|
44
44
|
/** Stable app-facing scan-result key. Do not parse; use typed fields instead. */
|
|
45
45
|
val id: String = address?.takeIf { it.isNotBlank() } ?: "${model.deviceType}:$name",
|
|
46
|
+
val pairingMode: Boolean? = null,
|
|
47
|
+
val pairingCode: String? = null,
|
|
48
|
+
val securePairingCapable: Boolean? = null,
|
|
46
49
|
) {
|
|
47
50
|
internal fun toMap(): Map<String, Any> =
|
|
48
51
|
buildMap {
|
|
@@ -56,6 +59,9 @@ data class Device(
|
|
|
56
59
|
put("projectName", it)
|
|
57
60
|
}
|
|
58
61
|
rssi?.let { put("rssi", it) }
|
|
62
|
+
pairingMode?.let { put("pairingMode", it) }
|
|
63
|
+
pairingCode?.takeIf { it.isNotBlank() }?.let { put("pairingCode", it) }
|
|
64
|
+
securePairingCapable?.let { put("securePairingCapable", it) }
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
companion object {
|
|
@@ -66,6 +72,9 @@ data class Device(
|
|
|
66
72
|
val projectName = stringValue(values, "projectName")?.takeIf { it.isNotBlank() }
|
|
67
73
|
val rssi = numberValue(values, "rssi")
|
|
68
74
|
val id = stringValue(values, "id")?.takeIf { it.isNotBlank() } ?: address ?: "${model}:$name"
|
|
75
|
+
val pairingMode = boolValue(values, "pairingMode")
|
|
76
|
+
val pairingCode = stringValue(values, "pairingCode")?.takeIf { it.isNotBlank() }
|
|
77
|
+
val securePairingCapable = boolValue(values, "securePairingCapable")
|
|
69
78
|
return Device(
|
|
70
79
|
model = DeviceModel.fromDeviceType(model),
|
|
71
80
|
name = name,
|
|
@@ -73,6 +82,9 @@ data class Device(
|
|
|
73
82
|
projectName = projectName,
|
|
74
83
|
rssi = rssi,
|
|
75
84
|
id = id,
|
|
85
|
+
pairingMode = pairingMode,
|
|
86
|
+
pairingCode = pairingCode,
|
|
87
|
+
securePairingCapable = securePairingCapable,
|
|
76
88
|
)
|
|
77
89
|
}
|
|
78
90
|
}
|
|
@@ -14,7 +14,7 @@ import org.json.JSONArray;
|
|
|
14
14
|
import org.json.JSONException;
|
|
15
15
|
import org.json.JSONObject;
|
|
16
16
|
|
|
17
|
-
/** BLE Wire Protocol v2 JSON compaction
|
|
17
|
+
/** BLE Wire Protocol v2 JSON compaction with lossless fallback for ambiguous payloads. */
|
|
18
18
|
public final class BleJsonCompact {
|
|
19
19
|
|
|
20
20
|
private static final Map<String, String> LONG_TO_SHORT = new HashMap<>();
|
|
@@ -47,8 +47,6 @@ public final class BleJsonCompact {
|
|
|
47
47
|
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("ck", "chunked_msg")));
|
|
48
48
|
|
|
49
49
|
private static long sessionConnectEpochMs;
|
|
50
|
-
private static boolean resolvedConfigSent;
|
|
51
|
-
private static String currentSessionResolvedConfigHash;
|
|
52
50
|
private static final Map<String, JSONObject> resolvedConfigByHash = new HashMap<>();
|
|
53
51
|
|
|
54
52
|
static {
|
|
@@ -102,8 +100,6 @@ public final class BleJsonCompact {
|
|
|
102
100
|
|
|
103
101
|
public static void resetSession() {
|
|
104
102
|
sessionConnectEpochMs = 0L;
|
|
105
|
-
resolvedConfigSent = false;
|
|
106
|
-
currentSessionResolvedConfigHash = null;
|
|
107
103
|
resolvedConfigByHash.clear();
|
|
108
104
|
}
|
|
109
105
|
|
|
@@ -160,7 +156,7 @@ public final class BleJsonCompact {
|
|
|
160
156
|
return json;
|
|
161
157
|
}
|
|
162
158
|
String messageType = extractMessageType(json);
|
|
163
|
-
if (!shouldCompactOutbound(messageType)) {
|
|
159
|
+
if (!shouldCompactOutbound(messageType) || !canCompactLosslessly(json, true)) {
|
|
164
160
|
return json;
|
|
165
161
|
}
|
|
166
162
|
return compactObject(json, true);
|
|
@@ -223,53 +219,85 @@ public final class BleJsonCompact {
|
|
|
223
219
|
fromInt.put(code, value);
|
|
224
220
|
}
|
|
225
221
|
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Compact JSON has reserved short keys and enum integers. If an otherwise valid future payload
|
|
224
|
+
* already uses one of those wire representations, sending verbose JSON is the only way an older
|
|
225
|
+
* decoder can distinguish the original value.
|
|
226
|
+
*/
|
|
227
|
+
private static boolean canCompactLosslessly(JSONObject input, boolean topLevel)
|
|
228
|
+
throws JSONException {
|
|
228
229
|
String messageType = input.optString("type", input.optString("t", ""));
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
Iterator<String> keys = input.keys();
|
|
231
|
+
while (keys.hasNext()) {
|
|
232
|
+
String key = keys.next();
|
|
233
|
+
Object value = input.get(key);
|
|
234
|
+
if (SHORT_TO_LONG.containsKey(key)
|
|
235
|
+
|| (topLevel && KEY_RESOLVED_CONFIG_HASH.equals(key))
|
|
236
|
+
|| isAmbiguousEnumValue(key, value, messageType)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (value instanceof JSONObject
|
|
240
|
+
&& !canCompactLosslessly((JSONObject) value, false)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (value instanceof JSONArray
|
|
244
|
+
&& !canCompactArrayLosslessly((JSONArray) value)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private static boolean canCompactArrayLosslessly(JSONArray array) throws JSONException {
|
|
252
|
+
for (int i = 0; i < array.length(); i++) {
|
|
253
|
+
Object value = array.get(i);
|
|
254
|
+
if (value instanceof JSONObject
|
|
255
|
+
&& !canCompactLosslessly((JSONObject) value, false)) {
|
|
256
|
+
return false;
|
|
241
257
|
}
|
|
258
|
+
if (value instanceof JSONArray
|
|
259
|
+
&& !canCompactArrayLosslessly((JSONArray) value)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static boolean isAmbiguousEnumValue(
|
|
267
|
+
String key, Object value, String messageType) {
|
|
268
|
+
if (!(value instanceof Number)) {
|
|
269
|
+
return false;
|
|
242
270
|
}
|
|
271
|
+
return "kind".equals(key)
|
|
272
|
+
|| "source".equals(key)
|
|
273
|
+
|| "aeStateName".equals(key)
|
|
274
|
+
|| ("status".equals(key) && "photo_status".equals(messageType));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static JSONObject compactObject(JSONObject input, boolean topLevel) throws JSONException {
|
|
278
|
+
JSONObject output = new JSONObject();
|
|
279
|
+
String messageType = input.optString("type", input.optString("t", ""));
|
|
243
280
|
|
|
244
281
|
Iterator<String> keys = input.keys();
|
|
245
282
|
while (keys.hasNext()) {
|
|
246
283
|
String key = keys.next();
|
|
247
|
-
if (skipResolvedConfig && "resolvedConfig".equals(key)) {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
284
|
Object value = input.get(key);
|
|
251
|
-
|
|
285
|
+
// Keep the absolute timestamp under its verbose key. Legacy peers recognize it and
|
|
286
|
+
// therefore do not mistake it for the old session-relative `ts` representation.
|
|
287
|
+
String shortKey =
|
|
288
|
+
topLevel && "timestamp".equals(key)
|
|
289
|
+
? key
|
|
290
|
+
: LONG_TO_SHORT.getOrDefault(key, key);
|
|
252
291
|
Object compacted = compactValue(key, value, messageType);
|
|
253
|
-
if (shouldOmit(compacted)) {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
292
|
output.put(shortKey, compacted);
|
|
257
293
|
}
|
|
258
|
-
|
|
259
|
-
if (topLevel) {
|
|
260
|
-
compactTimestamp(output);
|
|
261
|
-
}
|
|
262
294
|
return output;
|
|
263
295
|
}
|
|
264
296
|
|
|
265
|
-
private static boolean shouldDiffResolvedConfig(String messageType) {
|
|
266
|
-
return "photo_status".equals(messageType) || "stream_status".equals(messageType);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
297
|
private static Object compactValue(String longKey, Object value, String messageType)
|
|
270
298
|
throws JSONException {
|
|
271
299
|
if (value == JSONObject.NULL || value == null) {
|
|
272
|
-
return
|
|
300
|
+
return JSONObject.NULL;
|
|
273
301
|
}
|
|
274
302
|
if (value instanceof JSONObject) {
|
|
275
303
|
return compactObject((JSONObject) value, false);
|
|
@@ -321,40 +349,6 @@ public final class BleJsonCompact {
|
|
|
321
349
|
return out;
|
|
322
350
|
}
|
|
323
351
|
|
|
324
|
-
private static boolean shouldOmit(Object value) {
|
|
325
|
-
if (value == null || value == JSONObject.NULL) {
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
if (value instanceof Boolean && !((Boolean) value)) {
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
if (value instanceof JSONObject && ((JSONObject) value).length() == 0) {
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
if (value instanceof JSONArray && ((JSONArray) value).length() == 0) {
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private static void compactTimestamp(JSONObject output) throws JSONException {
|
|
341
|
-
String tsKey = output.has("timestamp") ? "timestamp" : (output.has("ts") ? "ts" : null);
|
|
342
|
-
if (tsKey == null) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
long absolute = output.optLong(tsKey);
|
|
346
|
-
if (absolute <= 0) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
if (sessionConnectEpochMs <= 0) {
|
|
350
|
-
sessionConnectEpochMs = absolute;
|
|
351
|
-
}
|
|
352
|
-
output.put("ts", absolute - sessionConnectEpochMs);
|
|
353
|
-
if (!"ts".equals(tsKey)) {
|
|
354
|
-
output.remove(tsKey);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
352
|
private static JSONObject expandObject(JSONObject input, boolean topLevel) throws JSONException {
|
|
359
353
|
JSONObject output = new JSONObject();
|
|
360
354
|
String messageType = input.optString("type", input.optString("t", ""));
|
|
@@ -369,9 +363,6 @@ public final class BleJsonCompact {
|
|
|
369
363
|
}
|
|
370
364
|
String longKey = SHORT_TO_LONG.getOrDefault(key, key);
|
|
371
365
|
Object expanded = expandValue(longKey, value, messageType);
|
|
372
|
-
if (expanded == null || expanded == JSONObject.NULL) {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
366
|
output.put(longKey, expanded);
|
|
376
367
|
}
|
|
377
368
|
|
|
@@ -384,14 +375,12 @@ public final class BleJsonCompact {
|
|
|
384
375
|
}
|
|
385
376
|
|
|
386
377
|
private static void cacheResolvedConfigIfPresent(JSONObject output) throws JSONException {
|
|
387
|
-
if (!output.
|
|
378
|
+
if (!(output.opt("resolvedConfig") instanceof JSONObject)) {
|
|
388
379
|
return;
|
|
389
380
|
}
|
|
390
381
|
JSONObject resolvedConfig = output.getJSONObject("resolvedConfig");
|
|
391
382
|
String hash = hashConfig(resolvedConfig);
|
|
392
383
|
resolvedConfigByHash.put(hash, cloneObject(resolvedConfig));
|
|
393
|
-
currentSessionResolvedConfigHash = hash;
|
|
394
|
-
resolvedConfigSent = true;
|
|
395
384
|
}
|
|
396
385
|
|
|
397
386
|
private static void expandResolvedConfigHash(JSONObject output) throws JSONException {
|
|
@@ -402,14 +391,14 @@ public final class BleJsonCompact {
|
|
|
402
391
|
JSONObject cached = resolvedConfigByHash.get(hash);
|
|
403
392
|
if (cached != null) {
|
|
404
393
|
output.put("resolvedConfig", cloneObject(cached));
|
|
394
|
+
output.remove(KEY_RESOLVED_CONFIG_HASH);
|
|
405
395
|
}
|
|
406
|
-
output.remove(KEY_RESOLVED_CONFIG_HASH);
|
|
407
396
|
}
|
|
408
397
|
|
|
409
398
|
private static Object expandValue(String longKey, Object value, String messageType)
|
|
410
399
|
throws JSONException {
|
|
411
400
|
if (value == JSONObject.NULL || value == null) {
|
|
412
|
-
return
|
|
401
|
+
return JSONObject.NULL;
|
|
413
402
|
}
|
|
414
403
|
if (value instanceof JSONObject) {
|
|
415
404
|
return expandObject((JSONObject) value, false);
|
|
@@ -448,7 +437,13 @@ public final class BleJsonCompact {
|
|
|
448
437
|
|
|
449
438
|
private static void expandTimestamp(JSONObject output) throws JSONException {
|
|
450
439
|
if (!output.has("timestamp") && output.has("ts")) {
|
|
451
|
-
|
|
440
|
+
Object timestamp = output.opt("ts");
|
|
441
|
+
if (!(timestamp instanceof Number)) {
|
|
442
|
+
output.put("timestamp", timestamp);
|
|
443
|
+
output.remove("ts");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
long delta = ((Number) timestamp).longValue();
|
|
452
447
|
long absolute = sessionConnectEpochMs > 0 ? sessionConnectEpochMs + delta : delta;
|
|
453
448
|
output.put("timestamp", absolute);
|
|
454
449
|
output.remove("ts");
|
|
@@ -429,7 +429,7 @@ public class K900ProtocolUtils {
|
|
|
429
429
|
String payloadStr;
|
|
430
430
|
try {
|
|
431
431
|
payloadStr = new String(payload, StandardCharsets.UTF_8);
|
|
432
|
-
android.util.Log.d("K900ProtocolUtils", "Extracted payload: " +
|
|
432
|
+
android.util.Log.d("K900ProtocolUtils", "Extracted JSON payload bytes: " + payloadLength);
|
|
433
433
|
} catch (Exception e) {
|
|
434
434
|
android.util.Log.e("K900ProtocolUtils", "Error converting payload to string", e);
|
|
435
435
|
return null;
|
|
@@ -437,7 +437,7 @@ public class K900ProtocolUtils {
|
|
|
437
437
|
|
|
438
438
|
// Check if it's valid JSON
|
|
439
439
|
if (!payloadStr.startsWith("{") || !payloadStr.endsWith("}")) {
|
|
440
|
-
android.util.Log.d("K900ProtocolUtils", "Payload is not valid JSON
|
|
440
|
+
android.util.Log.d("K900ProtocolUtils", "Payload is not valid JSON");
|
|
441
441
|
return null;
|
|
442
442
|
}
|
|
443
443
|
|
|
@@ -448,7 +448,7 @@ public class K900ProtocolUtils {
|
|
|
448
448
|
// Check if this is C-wrapped format {"C": "..."}
|
|
449
449
|
if (json.has(FIELD_C)) {
|
|
450
450
|
String innerContent = json.optString(FIELD_C, "");
|
|
451
|
-
android.util.Log.d("K900ProtocolUtils", "Detected C-wrapped format
|
|
451
|
+
android.util.Log.d("K900ProtocolUtils", "Detected C-wrapped format");
|
|
452
452
|
|
|
453
453
|
// Try to parse the inner content as JSON
|
|
454
454
|
try {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class OtaStartResponsePolicyTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun batteryLowFailureRejectsPendingStart() {
|
|
9
|
+
assertThat(otaStartRejectionErrorCode(status("failed", "battery_low")))
|
|
10
|
+
.isEqualTo("battery_low")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun unrelatedFailureDoesNotRejectPendingStart() {
|
|
15
|
+
assertThat(otaStartRejectionErrorCode(status("failed", "download_failed"))).isNull()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun nonFailureDoesNotRejectPendingStart() {
|
|
20
|
+
assertThat(otaStartRejectionErrorCode(status("in_progress", "battery_low"))).isNull()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private fun status(status: String, errorMessage: String): OtaStatusEvent =
|
|
24
|
+
OtaStatusEvent.fromMap(
|
|
25
|
+
mapOf(
|
|
26
|
+
"status" to status,
|
|
27
|
+
"error_message" to errorMessage,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class ScopedNetworkReadinessTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun `waits for the available network to become foreground`() {
|
|
10
|
+
val readiness = ScopedNetworkReadiness<String>()
|
|
11
|
+
|
|
12
|
+
readiness.onAvailable("glasses")
|
|
13
|
+
|
|
14
|
+
assertFalse(readiness.isReady("glasses", isForeground = false))
|
|
15
|
+
assertTrue(readiness.isReady("glasses", isForeground = true))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun `rejects capabilities from a stale network`() {
|
|
20
|
+
val readiness = ScopedNetworkReadiness<String>()
|
|
21
|
+
|
|
22
|
+
readiness.onAvailable("old")
|
|
23
|
+
readiness.onAvailable("current")
|
|
24
|
+
|
|
25
|
+
assertFalse(readiness.isReady("old", isForeground = true))
|
|
26
|
+
assertTrue(readiness.isReady("current", isForeground = true))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `clears the candidate when the request ends`() {
|
|
31
|
+
val readiness = ScopedNetworkReadiness<String>()
|
|
32
|
+
readiness.onAvailable("glasses")
|
|
33
|
+
|
|
34
|
+
readiness.clear()
|
|
35
|
+
|
|
36
|
+
assertFalse(readiness.contains("glasses"))
|
|
37
|
+
assertFalse(readiness.isReady("glasses", isForeground = true))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.otaserver
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.net.HttpURLConnection
|
|
5
|
+
import java.net.URL
|
|
6
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
7
|
+
import org.junit.After
|
|
8
|
+
import org.junit.Before
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
class LocalOtaServerTest {
|
|
12
|
+
private lateinit var server: LocalOtaServer
|
|
13
|
+
private lateinit var artifactFile: File
|
|
14
|
+
private var port = 0
|
|
15
|
+
|
|
16
|
+
private val manifest = """{"apps":{"com.mentra.asg_client":{"versionCode":1}}}"""
|
|
17
|
+
private val artifactBytes = ByteArray(1024) { (it % 251).toByte() }
|
|
18
|
+
private val sha = "a".repeat(64)
|
|
19
|
+
|
|
20
|
+
@Before
|
|
21
|
+
fun startServer() {
|
|
22
|
+
artifactFile = File.createTempFile("ota-artifact", ".bin").apply {
|
|
23
|
+
writeBytes(artifactBytes)
|
|
24
|
+
}
|
|
25
|
+
server = LocalOtaServer(onLog = {})
|
|
26
|
+
server.configure(manifest, mapOf(sha to artifactFile))
|
|
27
|
+
port = server.start("127.0.0.1", 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@After
|
|
31
|
+
fun stopServer() {
|
|
32
|
+
server.close()
|
|
33
|
+
artifactFile.delete()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun `serves the configured manifest`() {
|
|
38
|
+
val connection = open("/version.json")
|
|
39
|
+
assertThat(connection.responseCode).isEqualTo(200)
|
|
40
|
+
assertThat(connection.contentType).isEqualTo("application/json")
|
|
41
|
+
assertThat(connection.inputStream.readBytes().decodeToString()).isEqualTo(manifest)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `streams a full artifact`() {
|
|
46
|
+
val connection = open("/artifacts/$sha")
|
|
47
|
+
assertThat(connection.responseCode).isEqualTo(200)
|
|
48
|
+
assertThat(connection.inputStream.readBytes()).isEqualTo(artifactBytes)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun `returns 404 for unknown artifacts`() {
|
|
53
|
+
val connection = open("/artifacts/${"b".repeat(64)}")
|
|
54
|
+
assertThat(connection.responseCode).isEqualTo(404)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun `rejects non-GET methods`() {
|
|
59
|
+
val connection = open("/version.json")
|
|
60
|
+
connection.requestMethod = "POST"
|
|
61
|
+
connection.doOutput = true
|
|
62
|
+
connection.outputStream.use { it.write("{}".toByteArray()) }
|
|
63
|
+
assertThat(connection.responseCode).isEqualTo(405)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `reconfigures while running`() {
|
|
68
|
+
server.configure("""{"updated":true}""", emptyMap())
|
|
69
|
+
val manifestConnection = open("/version.json")
|
|
70
|
+
assertThat(manifestConnection.inputStream.readBytes().decodeToString())
|
|
71
|
+
.isEqualTo("""{"updated":true}""")
|
|
72
|
+
val artifactConnection = open("/artifacts/$sha")
|
|
73
|
+
assertThat(artifactConnection.responseCode).isEqualTo(404)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun open(path: String): HttpURLConnection {
|
|
77
|
+
val connection = URL("http://127.0.0.1:$port$path").openConnection() as HttpURLConnection
|
|
78
|
+
connection.connectTimeout = 5_000
|
|
79
|
+
connection.readTimeout = 5_000
|
|
80
|
+
return connection
|
|
81
|
+
}
|
|
82
|
+
}
|