@mentra/bluetooth-sdk 3.3.0-dev.774 → 3.3.0-dev.780
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +34 -8
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +2 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +5 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +44 -35
- package/ios/Source/sgcs/SGCManager.swift +1 -1
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -617,7 +617,7 @@ class BluetoothSdkModule : Module() {
|
|
|
617
617
|
// MARK: - Incident Reporting
|
|
618
618
|
|
|
619
619
|
SdkAsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
|
|
620
|
-
|
|
620
|
+
requireSdk().sendIncidentId(incidentId, apiBaseUrl)
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
// MARK: - Native Notification Centre
|
|
@@ -1989,8 +1989,11 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
|
|
|
1989
1989
|
sgc?.requestSavedWifiNetworks(requestId, sid) ?: false
|
|
1990
1990
|
|
|
1991
1991
|
fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
|
|
1992
|
+
val driver = sgc ?: throw BluetoothSdkException(
|
|
1993
|
+
"glasses_not_connected", "Cannot request incident logs because glasses are not connected.",
|
|
1994
|
+
)
|
|
1992
1995
|
Bridge.log("MAN: Sending incidentId to glasses for log upload: $incidentId")
|
|
1993
|
-
|
|
1996
|
+
driver.sendIncidentId(incidentId, apiBaseUrl)
|
|
1994
1997
|
}
|
|
1995
1998
|
|
|
1996
1999
|
/** Push a notification into the glasses' own notification centre; rejects unsupported or disconnected devices. */
|
|
@@ -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.3.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.3.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.3.0-dev.780"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.780"
|
|
8
|
+
const val SOURCE_COMMIT: String = "54e297970f1fe406469ee5f4548152b5a3fc4d9a"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.780.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "2207f01bb345e8988eccec986d4cadd52758dbf129c51b230050a95a8bc2f36b"
|
|
11
11
|
}
|
|
@@ -34,6 +34,7 @@ import android.os.SystemClock
|
|
|
34
34
|
import com.mentra.bluetoothsdk.utils.NativeLog as Log
|
|
35
35
|
import androidx.core.app.ActivityCompat
|
|
36
36
|
import com.mentra.bluetoothsdk.BluetoothSdkDefaults
|
|
37
|
+
import com.mentra.bluetoothsdk.BluetoothSdkException
|
|
37
38
|
import com.mentra.bluetoothsdk.Bridge
|
|
38
39
|
import com.mentra.bluetoothsdk.DeviceManager
|
|
39
40
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
@@ -3371,10 +3372,12 @@ class MentraLive : SGCManager() {
|
|
|
3371
3372
|
sendJson(json, wakeup, true)
|
|
3372
3373
|
}
|
|
3373
3374
|
|
|
3374
|
-
private fun sendJson(
|
|
3375
|
+
private fun sendJson(
|
|
3376
|
+
json: JSONObject?, wakeup: Boolean, bridgeLogging: Boolean,
|
|
3377
|
+
sessionGeneration: Long = bleSessionGeneration.get(),
|
|
3378
|
+
): Boolean {
|
|
3375
3379
|
if (json != null) {
|
|
3376
3380
|
try {
|
|
3377
|
-
val sessionGeneration = bleSessionGeneration.get()
|
|
3378
3381
|
if (buildNumberInt < 5) {
|
|
3379
3382
|
val jsonStr = json.toString()
|
|
3380
3383
|
// Bridge.log("LIVE: 📤 Sending JSON with esoteric message ID: " + jsonStr);
|
|
@@ -6075,6 +6078,17 @@ class MentraLive : SGCManager() {
|
|
|
6075
6078
|
}
|
|
6076
6079
|
|
|
6077
6080
|
override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {
|
|
6081
|
+
if (!isConnected || bluetoothGatt == null || txCharacteristic == null) {
|
|
6082
|
+
throw BluetoothSdkException(
|
|
6083
|
+
"glasses_not_connected", "Cannot request incident logs because the glasses BLE link is not ready.",
|
|
6084
|
+
)
|
|
6085
|
+
}
|
|
6086
|
+
// The JS token sync may happen after glasses_ready. Use only its current native store value.
|
|
6087
|
+
val coreToken = DeviceStore.get("bluetooth", "core_token") as? String
|
|
6088
|
+
if (coreToken.isNullOrBlank()) {
|
|
6089
|
+
throw BluetoothSdkException("core_token_unavailable", "Cannot request incident logs without the current Core access token.")
|
|
6090
|
+
}
|
|
6091
|
+
val sessionGeneration = bleSessionGeneration.get()
|
|
6078
6092
|
try {
|
|
6079
6093
|
var base = if (apiBaseUrl != null) apiBaseUrl.trim() else ""
|
|
6080
6094
|
if (base.isEmpty()) {
|
|
@@ -6087,13 +6101,25 @@ class MentraLive : SGCManager() {
|
|
|
6087
6101
|
bleIncidentLogRelays[lKey] =
|
|
6088
6102
|
BleIncidentLogRelay(lKey, incidentId, base, BleIncidentLogKind.LOGCAT)
|
|
6089
6103
|
|
|
6104
|
+
val auth = JSONObject()
|
|
6105
|
+
auth.put("type", "auth_token")
|
|
6106
|
+
auth.put("coreToken", coreToken)
|
|
6107
|
+
auth.put("timestamp", System.currentTimeMillis())
|
|
6090
6108
|
val json = JSONObject()
|
|
6091
6109
|
json.put("type", "upload_incident_logs")
|
|
6092
6110
|
json.put("incidentId", incidentId)
|
|
6093
6111
|
json.put("apiBaseUrl", base)
|
|
6094
|
-
|
|
6112
|
+
// Wake ASG with its current token before the upload command enters the same FIFO.
|
|
6113
|
+
if (!sendJson(auth, true, true, sessionGeneration) ||
|
|
6114
|
+
sessionGeneration != bleSessionGeneration.get() ||
|
|
6115
|
+
!isConnected || bluetoothGatt == null || txCharacteristic == null ||
|
|
6116
|
+
!sendJson(json, true, true, sessionGeneration) || sessionGeneration != bleSessionGeneration.get()) {
|
|
6117
|
+
bleIncidentLogRelays.remove(bKey)
|
|
6118
|
+
bleIncidentLogRelays.remove(lKey)
|
|
6119
|
+
throw BluetoothSdkException("incident_dispatch_failed", "Could not queue the glasses incident-log request.")
|
|
6120
|
+
}
|
|
6095
6121
|
Bridge.log(
|
|
6096
|
-
"LIVE:
|
|
6122
|
+
"LIVE: Queued incidentId to glasses for log upload: " +
|
|
6097
6123
|
incidentId +
|
|
6098
6124
|
" (BLE relay keys " +
|
|
6099
6125
|
bKey +
|
|
@@ -6102,7 +6128,7 @@ class MentraLive : SGCManager() {
|
|
|
6102
6128
|
")"
|
|
6103
6129
|
)
|
|
6104
6130
|
} catch (e: JSONException) {
|
|
6105
|
-
|
|
6131
|
+
throw BluetoothSdkException("incident_dispatch_failed", "Could not create the glasses incident-log request.", e)
|
|
6106
6132
|
}
|
|
6107
6133
|
}
|
|
6108
6134
|
|
|
@@ -9804,12 +9830,12 @@ class MentraLive : SGCManager() {
|
|
|
9804
9830
|
}
|
|
9805
9831
|
|
|
9806
9832
|
/**
|
|
9807
|
-
*
|
|
9808
|
-
* with the password redacted.
|
|
9833
|
+
* Credentials are sent unchanged but never logged; the BLE trace redacts their fields.
|
|
9809
9834
|
*/
|
|
9810
9835
|
private fun loggableOutgoingPayload(payload: String, commandType: String): String =
|
|
9811
9836
|
try {
|
|
9812
|
-
|
|
9837
|
+
val json = JSONObject(payload)
|
|
9838
|
+
if (json.has("password") || json.has("coreToken")) "<$commandType with credentials omitted>"
|
|
9813
9839
|
else payload
|
|
9814
9840
|
} catch (_: JSONException) {
|
|
9815
9841
|
payload
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothGattCallback
|
|
5
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import androidx.test.core.app.ApplicationProvider
|
|
8
|
+
import com.mentra.bluetoothsdk.BluetoothSdkException
|
|
9
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
10
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
11
|
+
import java.nio.charset.StandardCharsets
|
|
12
|
+
import java.time.Duration
|
|
13
|
+
import java.util.UUID
|
|
14
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
15
|
+
import org.junit.Assert.*
|
|
16
|
+
import org.junit.Test
|
|
17
|
+
import org.junit.runner.RunWith
|
|
18
|
+
import org.robolectric.RobolectricTestRunner
|
|
19
|
+
import org.robolectric.Shadows.shadowOf
|
|
20
|
+
import org.robolectric.annotation.Config
|
|
21
|
+
import org.robolectric.annotation.LooperMode
|
|
22
|
+
import org.robolectric.shadows.ShadowLog
|
|
23
|
+
|
|
24
|
+
@RunWith(RobolectricTestRunner::class)
|
|
25
|
+
@Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class], instrumentedPackages = ["com.mentra.lc3Lib"])
|
|
26
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
27
|
+
class MentraLiveIncidentDispatchTest {
|
|
28
|
+
private val incident = "rep_01M4HBVT88DFSN8TYNZ1WTV4PB"
|
|
29
|
+
private val apiBase = "https://core.dev.us-west-2.mentraglass.com"
|
|
30
|
+
|
|
31
|
+
@Test fun `missing native BLE link rejects even when cached status is connected`() = withLive { live ->
|
|
32
|
+
DeviceStore.set("glasses", "connected", true)
|
|
33
|
+
expectCode("glasses_not_connected") { live.sendIncidentId(incident, apiBase) }
|
|
34
|
+
assertTrue(queuedWrites(live).isEmpty())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test fun `missing current token does not fall back to old stored credentials`() = withLive { live ->
|
|
38
|
+
ready(live)
|
|
39
|
+
DeviceStore.set("bluetooth", "core_token", "")
|
|
40
|
+
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
|
|
41
|
+
val prefs = context.getSharedPreferences("augmentos_auth_prefs", android.content.Context.MODE_PRIVATE)
|
|
42
|
+
prefs.edit().putString("core_token", "stale-synthetic-token").commit()
|
|
43
|
+
expectCode("core_token_unavailable") { live.sendIncidentId(incident, apiBase) }
|
|
44
|
+
assertTrue(queuedWrites(live).isEmpty())
|
|
45
|
+
prefs.edit().remove("core_token").commit()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test fun `current token is queued before upload and never logged`() = withLive { live ->
|
|
49
|
+
ready(live)
|
|
50
|
+
val token = "synthetic-${UUID.randomUUID()}"
|
|
51
|
+
DeviceStore.set("bluetooth", "core_token", token)
|
|
52
|
+
ShadowLog.clear()
|
|
53
|
+
live.sendIncidentId(incident, apiBase)
|
|
54
|
+
val writes = queuedWrites(live)
|
|
55
|
+
val wire = writes.joinToString("") { String(it, StandardCharsets.UTF_8) }
|
|
56
|
+
assertEquals(2, writes.size)
|
|
57
|
+
assertTrue(wire.indexOf("auth_token") < wire.indexOf("upload_incident_logs"))
|
|
58
|
+
assertTrue(wire.contains(token))
|
|
59
|
+
assertTrue(wire.contains(incident))
|
|
60
|
+
assertFalse("token logged", ShadowLog.getLogs().any { it.msg.contains(token) })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test fun `link losing TX characteristic rejects instead of retaining incident relay`() = withLive { live ->
|
|
64
|
+
ready(live)
|
|
65
|
+
DeviceStore.set("bluetooth", "core_token", "synthetic")
|
|
66
|
+
set(live, "txCharacteristic", null)
|
|
67
|
+
expectCode("glasses_not_connected") { live.sendIncidentId(incident, apiBase) }
|
|
68
|
+
assertTrue(queuedWrites(live).isEmpty())
|
|
69
|
+
val relays = get(live, "bleIncidentLogRelays") as Map<*, *>
|
|
70
|
+
assertTrue(relays.isEmpty())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test fun `session loss during token serialization cannot queue an upload into the next session`() = withLive { live ->
|
|
74
|
+
ready(live)
|
|
75
|
+
DeviceStore.set("bluetooth", "core_token", "synthetic-token")
|
|
76
|
+
val generation = get(live, "bleSessionGeneration") as AtomicLong
|
|
77
|
+
val sink = Bridge.addEventSink { type, body ->
|
|
78
|
+
if (type == "log" && body["message"].toString().contains("<auth_token with credentials omitted>")) {
|
|
79
|
+
generation.incrementAndGet()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
expectCode("incident_dispatch_failed") { live.sendIncidentId(incident, apiBase) }
|
|
84
|
+
val wire = queuedWrites(live).joinToString("") { String(it, StandardCharsets.UTF_8) }
|
|
85
|
+
assertFalse(wire.contains("upload_incident_logs"))
|
|
86
|
+
assertTrue((get(live, "bleIncidentLogRelays") as Map<*, *>).isEmpty())
|
|
87
|
+
} finally { Bridge.removeEventSink(sink) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun ready(live: MentraLive) {
|
|
91
|
+
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
|
|
92
|
+
val gatt = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF")
|
|
93
|
+
.connectGatt(context, false, object : BluetoothGattCallback() {})
|
|
94
|
+
set(live, "isConnected", true)
|
|
95
|
+
set(live, "bluetoothGatt", gatt)
|
|
96
|
+
set(live, "txCharacteristic", BluetoothGattCharacteristic(UUID.randomUUID(), 0, 0))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun withLive(body: (MentraLive) -> Unit) {
|
|
100
|
+
Bridge.initialize(ApplicationProvider.getApplicationContext())
|
|
101
|
+
val live = MentraLive()
|
|
102
|
+
val token = DeviceStore.get("bluetooth", "core_token")
|
|
103
|
+
val connected = DeviceStore.get("glasses", "connected")
|
|
104
|
+
try { body(live) } finally {
|
|
105
|
+
DeviceStore.set("bluetooth", "core_token", token ?: "")
|
|
106
|
+
DeviceStore.set("glasses", "connected", connected ?: false)
|
|
107
|
+
live.destroy()
|
|
108
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(2))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun expectCode(code: String, body: () -> Unit) {
|
|
113
|
+
try { body(); fail("Expected $code") } catch (error: BluetoothSdkException) { assertEquals(code, error.code) }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun get(live: MentraLive, name: String): Any? =
|
|
117
|
+
MentraLive::class.java.getDeclaredField(name).apply { isAccessible = true }.get(live)
|
|
118
|
+
|
|
119
|
+
private fun set(live: MentraLive, name: String, value: Any?) {
|
|
120
|
+
MentraLive::class.java.getDeclaredField(name).apply { isAccessible = true }.set(live, value)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun queuedWrites(live: MentraLive): List<ByteArray> = (get(live, "sendQueue") as Iterable<*>).map { write ->
|
|
124
|
+
write!!.javaClass.getDeclaredField("data").apply { isAccessible = true }.get(write) as ByteArray
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "3.3.0",
|
|
4
|
-
"releaseIdentity": "3.3.0-dev.
|
|
5
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
8
|
-
"otaManifestSha256": "
|
|
4
|
+
"releaseIdentity": "3.3.0-dev.780",
|
|
5
|
+
"releaseSetId": "mentra-3.3.0-dev.780",
|
|
6
|
+
"sourceCommit": "54e297970f1fe406469ee5f4548152b5a3fc4d9a",
|
|
7
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.780.json",
|
|
8
|
+
"otaManifestSha256": "2207f01bb345e8988eccec986d4cadd52758dbf129c51b230050a95a8bc2f36b"
|
|
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,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,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.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.
|
|
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,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,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.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.780\",\n \"releaseSetId\": \"mentra-3.3.0-dev.780\",\n \"sourceCommit\": \"54e297970f1fe406469ee5f4548152b5a3fc4d9a\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.780.json\",\n \"otaManifestSha256\": \"2207f01bb345e8988eccec986d4cadd52758dbf129c51b230050a95a8bc2f36b\"\n})\n"]}
|
|
@@ -368,8 +368,8 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
|
|
|
368
368
|
// MARK: - Incident Reporting
|
|
369
369
|
|
|
370
370
|
AsyncFunction("sendIncidentId") { (incidentId: String, apiBaseUrl: String?) in
|
|
371
|
-
await MainActor.run {
|
|
372
|
-
self.bluetoothSdk().sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
371
|
+
try await MainActor.run {
|
|
372
|
+
try self.bluetoothSdk().sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -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.3.0-dev.
|
|
19
|
+
private static let swiftPackageSdkVersion = "3.3.0-dev.780"
|
|
20
20
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
21
21
|
|
|
22
22
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -1518,9 +1518,12 @@ struct ViewState {
|
|
|
1518
1518
|
sgc?.requestSavedWifiNetworks(requestId: requestId, sid: sid) ?? false
|
|
1519
1519
|
}
|
|
1520
1520
|
|
|
1521
|
-
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1521
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) throws {
|
|
1522
|
+
guard let driver = sgc else {
|
|
1523
|
+
throw BluetoothSdkError(code: "glasses_not_connected", message: "Cannot request incident logs because glasses are not connected.")
|
|
1524
|
+
}
|
|
1522
1525
|
Bridge.log("MAN: Sending incidentId to glasses for log upload: \(incidentId)")
|
|
1523
|
-
|
|
1526
|
+
try driver.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1524
1527
|
}
|
|
1525
1528
|
|
|
1526
1529
|
func sendWifiCredentials(_ ssid: String, _ password: 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.3.0"
|
|
6
|
-
static let releaseIdentity = "3.3.0-dev.
|
|
7
|
-
static let releaseSetId = "mentra-3.3.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.3.0-dev.780"
|
|
7
|
+
static let releaseSetId = "mentra-3.3.0-dev.780"
|
|
8
|
+
static let sourceCommit = "54e297970f1fe406469ee5f4548152b5a3fc4d9a"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.780.json"
|
|
10
|
+
static let otaManifestSha256 = "2207f01bb345e8988eccec986d4cadd52758dbf129c51b230050a95a8bc2f36b"
|
|
11
11
|
}
|
|
@@ -1667,8 +1667,8 @@ public final class MentraBluetoothSDK {
|
|
|
1667
1667
|
DeviceManager.shared.sendReboot()
|
|
1668
1668
|
}
|
|
1669
1669
|
|
|
1670
|
-
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1671
|
-
DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1670
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) throws {
|
|
1671
|
+
try DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1672
1672
|
}
|
|
1673
1673
|
|
|
1674
1674
|
public func invalidate() {
|
|
@@ -1349,7 +1349,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1349
1349
|
// Queued writes are session-bound: transmitting them into the NEXT session
|
|
1350
1350
|
// (e.g. a stale handshake whose reply activates v2 before the new session
|
|
1351
1351
|
// negotiated) is the bug class this clear removes.
|
|
1352
|
-
|
|
1352
|
+
commandQueue.removeAll()
|
|
1353
1353
|
stopSignalStrengthPolling()
|
|
1354
1354
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
1355
1355
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -2123,8 +2123,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2123
2123
|
/// Wire-session generation at enqueue time; the drain loop drops entries from a
|
|
2124
2124
|
/// previous session so a stale write (worst case: an old handshake whose reply
|
|
2125
2125
|
/// would activate v2 pre-negotiation) can never cross a session boundary. This is
|
|
2126
|
-
/// the deterministic guard
|
|
2127
|
-
/// cleanup that may land after a reconnect has already resumed the drain loop.
|
|
2126
|
+
/// the deterministic guard if a consumer already dequeued a previous-session write.
|
|
2128
2127
|
let generation: Int
|
|
2129
2128
|
}
|
|
2130
2129
|
|
|
@@ -2166,7 +2165,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2166
2165
|
Bridge.log("LIVE: Cleared pending ACK mId \(pendingMessage.id) because \(reason)")
|
|
2167
2166
|
}
|
|
2168
2167
|
|
|
2169
|
-
|
|
2168
|
+
@MainActor final class CommandQueue {
|
|
2170
2169
|
private var commands: [PendingMessage] = []
|
|
2171
2170
|
|
|
2172
2171
|
func enqueue(_ command: PendingMessage) -> Int {
|
|
@@ -2282,15 +2281,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2282
2281
|
)
|
|
2283
2282
|
|
|
2284
2283
|
// Push to front of queue for immediate retry
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
"queueSizeAfterAdd": queueSize,
|
|
2291
|
-
])
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2284
|
+
let queueSize = commandQueue.pushToFront(retryMessage)
|
|
2285
|
+
logBleWriteTrace("retry_queued", retryMessage.trace, extra: [
|
|
2286
|
+
"retryDelayMs": 0,
|
|
2287
|
+
"queueSizeAfterAdd": queueSize,
|
|
2288
|
+
])
|
|
2294
2289
|
|
|
2295
2290
|
Bridge.log(
|
|
2296
2291
|
"🔄 Retrying message mId: \(pendingMessage.id) (attempt \(retryMessage.retries)/3)"
|
|
@@ -3714,7 +3709,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3714
3709
|
sendJson(json, wakeUp: true)
|
|
3715
3710
|
}
|
|
3716
3711
|
|
|
3717
|
-
|
|
3712
|
+
var incidentLogTransportReady: Bool {
|
|
3713
|
+
connectedPeripheral?.state == .connected && txCharacteristic != nil
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String?) throws {
|
|
3717
|
+
guard incidentLogTransportReady else {
|
|
3718
|
+
throw BluetoothSdkError(code: "glasses_not_connected", message: "Cannot request incident logs because the glasses BLE link is not ready.")
|
|
3719
|
+
}
|
|
3720
|
+
// Token sync may happen after glasses_ready; never rely on the last pairing's token.
|
|
3721
|
+
let coreToken = DeviceStore.shared.get("bluetooth", "core_token") as? String ?? ""
|
|
3722
|
+
guard !coreToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
3723
|
+
throw BluetoothSdkError(code: "core_token_unavailable", message: "Cannot request incident logs without the current Core access token.")
|
|
3724
|
+
}
|
|
3718
3725
|
var base = (apiBaseUrl ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3719
3726
|
if base.isEmpty {
|
|
3720
3727
|
base = "https://api.mentra.glass"
|
|
@@ -3731,10 +3738,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3731
3738
|
fileBaseKey: lKey, incidentId: incidentId, apiBaseUrl: base, kind: .logcat
|
|
3732
3739
|
)
|
|
3733
3740
|
|
|
3734
|
-
|
|
3735
|
-
"
|
|
3736
|
-
|
|
3737
|
-
|
|
3741
|
+
let auth: [String: Any] = [
|
|
3742
|
+
"type": "auth_token", "coreToken": coreToken,
|
|
3743
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
3744
|
+
]
|
|
3745
|
+
// MainActor queue insertion preserves auth-before-upload order, including all fragments.
|
|
3746
|
+
guard sendJson(auth, wakeUp: true, requireAck: true, bridgeLogging: true),
|
|
3747
|
+
sendJson(["type": "upload_incident_logs", "incidentId": incidentId, "apiBaseUrl": base],
|
|
3748
|
+
wakeUp: true, requireAck: true, bridgeLogging: true)
|
|
3749
|
+
else {
|
|
3750
|
+
bleIncidentLogRelays.removeValue(forKey: bKey)
|
|
3751
|
+
bleIncidentLogRelays.removeValue(forKey: lKey)
|
|
3752
|
+
throw BluetoothSdkError(code: "incident_dispatch_failed", message: "Could not queue the glasses incident-log request.")
|
|
3753
|
+
}
|
|
3754
|
+
Bridge.log("LIVE: Queued incidentId to glasses for log upload: \(incidentId) (BLE relay \(bKey), \(lKey))")
|
|
3738
3755
|
}
|
|
3739
3756
|
|
|
3740
3757
|
private static func incidentBleFileBase(incidentId: String, prefix: Character) -> String {
|
|
@@ -4671,20 +4688,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4671
4688
|
|
|
4672
4689
|
func queueSend(_ data: Data, id: String, trace: BleWriteTrace?) {
|
|
4673
4690
|
let significantQueueSize = SIGNIFICANT_BLE_TRACE_QUEUE_SIZE
|
|
4674
|
-
//
|
|
4675
|
-
// session reset, and a pre-reset payload stamped with the new generation would
|
|
4676
|
-
// defeat the stale-write guard in the drain loop.
|
|
4691
|
+
// Insert synchronously on MainActor so fragments and dependent commands retain call order.
|
|
4677
4692
|
let generation = wireSessionGeneration
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
await MainActor.run {
|
|
4684
|
-
self.logBleChunkTrace("queued", trace, extra: [
|
|
4685
|
-
"queueSizeAfterAdd": queueSize,
|
|
4686
|
-
])
|
|
4687
|
-
}
|
|
4693
|
+
let queueSize = commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0, trace: trace, generation: generation))
|
|
4694
|
+
if trace != nil, queueSize >= significantQueueSize {
|
|
4695
|
+
logBleChunkTrace("queued", trace, extra: [
|
|
4696
|
+
"queueSizeAfterAdd": queueSize,
|
|
4697
|
+
])
|
|
4688
4698
|
}
|
|
4689
4699
|
}
|
|
4690
4700
|
|
|
@@ -5201,9 +5211,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5201
5211
|
bridgeLogging: bridgeLogging
|
|
5202
5212
|
)
|
|
5203
5213
|
}
|
|
5204
|
-
//
|
|
5205
|
-
|
|
5206
|
-
let loggedPayload = json["password"] == nil
|
|
5214
|
+
// Credentials reach the wire unchanged; diagnostic logs omit their values.
|
|
5215
|
+
let loggedPayload = json["password"] == nil && json["coreToken"] == nil
|
|
5207
5216
|
? jsonString : "<\(commandInfo.commandType) with credentials omitted>"
|
|
5208
5217
|
transportLog("LIVE: Sending data to glasses: \(loggedPayload)", bridgeLogging: bridgeLogging)
|
|
5209
5218
|
let packedData =
|
|
@@ -6000,7 +6009,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
6000
6009
|
// Stop all timers
|
|
6001
6010
|
stopAllTimers()
|
|
6002
6011
|
resetWireNegotiationState()
|
|
6003
|
-
|
|
6012
|
+
commandQueue.removeAll() // stale writes die with the session
|
|
6004
6013
|
closeL2capFileChannel()
|
|
6005
6014
|
|
|
6006
6015
|
// Disconnect BLE
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
/// Keep native admission, serialization and fragment packing; replace only the radio writes.
|
|
5
|
+
@MainActor
|
|
6
|
+
private final class IncidentLogTransport: MentraLive {
|
|
7
|
+
var ready = true
|
|
8
|
+
var writes: [(Data, BleWriteTrace?)] = []
|
|
9
|
+
|
|
10
|
+
override var incidentLogTransportReady: Bool {
|
|
11
|
+
ready
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override func queueSend(_ data: Data, id _: String, trace: BleWriteTrace?) {
|
|
15
|
+
writes.append((data, trace))
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@MainActor
|
|
20
|
+
final class IncidentLogDispatchTests: XCTestCase {
|
|
21
|
+
private let incident = "rep_01M4HBVT88DFSN8TYNZ1WTV4PB"
|
|
22
|
+
private let apiBase = "https://core.dev.us-west-2.mentraglass.com"
|
|
23
|
+
|
|
24
|
+
func testMissingNativeLinkRejectsDespiteCachedConnectedStatus() {
|
|
25
|
+
let live = MentraLive()
|
|
26
|
+
let store = DeviceStore.shared.store
|
|
27
|
+
let saved = store.getCategory("glasses")["connected"]
|
|
28
|
+
store.set("glasses", "connected", true)
|
|
29
|
+
defer {
|
|
30
|
+
if let saved { store.set("glasses", "connected", saved) }
|
|
31
|
+
else { store.remove("glasses", "connected") }
|
|
32
|
+
}
|
|
33
|
+
XCTAssertThrowsError(try live.sendIncidentId(incident, apiBaseUrl: apiBase)) { error in
|
|
34
|
+
XCTAssertEqual((error as? BluetoothSdkError)?.code, "glasses_not_connected")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func testNoDriverRejectsThroughDeviceManager() {
|
|
39
|
+
let manager = DeviceManager()
|
|
40
|
+
manager.sgc = nil
|
|
41
|
+
XCTAssertThrowsError(try manager.sendIncidentId(incident, apiBaseUrl: apiBase)) { error in
|
|
42
|
+
XCTAssertEqual((error as? BluetoothSdkError)?.code, "glasses_not_connected")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func testMissingCurrentTokenDoesNotQueueAnUpload() throws {
|
|
47
|
+
try withToken("") {
|
|
48
|
+
let live = IncidentLogTransport()
|
|
49
|
+
XCTAssertThrowsError(try live.sendIncidentId(incident, apiBaseUrl: apiBase)) { error in
|
|
50
|
+
XCTAssertEqual((error as? BluetoothSdkError)?.code, "core_token_unavailable")
|
|
51
|
+
}
|
|
52
|
+
XCTAssertTrue(live.writes.isEmpty)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testCurrentTokenPrecedesUploadAndNeverAppearsInLogs() throws {
|
|
57
|
+
let token = "synthetic-\(UUID().uuidString)"
|
|
58
|
+
try withToken(token) {
|
|
59
|
+
let live = IncidentLogTransport()
|
|
60
|
+
let manager = DeviceManager()
|
|
61
|
+
manager.sgc = live
|
|
62
|
+
var logs: [String] = []
|
|
63
|
+
let sink = Bridge.addEventSink { event, body in
|
|
64
|
+
if event == "log", let message = body["message"] as? String { logs.append(message) }
|
|
65
|
+
}
|
|
66
|
+
defer { Bridge.removeEventSink(sink) }
|
|
67
|
+
|
|
68
|
+
try manager.sendIncidentId(incident, apiBaseUrl: apiBase)
|
|
69
|
+
|
|
70
|
+
XCTAssertEqual(live.writes.compactMap { $0.1?.commandType }, ["auth_token", "upload_incident_logs"])
|
|
71
|
+
XCTAssertTrue(live.writes.allSatisfy { $0.1?.wakeup == true })
|
|
72
|
+
let wire = live.writes.map { String(decoding: $0.0, as: UTF8.self) }.joined()
|
|
73
|
+
XCTAssertTrue(wire.contains(token), "The current token must reach the glasses")
|
|
74
|
+
XCTAssertTrue(wire.contains(incident))
|
|
75
|
+
XCTAssertFalse(logs.contains { $0.contains(token) }, "Tokens must stay out of native and incident logs")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func testAllTokenFragmentsPrecedeUpload() throws {
|
|
80
|
+
try withToken(String(repeating: "synthetic", count: 120)) {
|
|
81
|
+
let live = IncidentLogTransport()
|
|
82
|
+
try live.sendIncidentId(incident, apiBaseUrl: apiBase)
|
|
83
|
+
let types = live.writes.compactMap { $0.1?.commandType }
|
|
84
|
+
XCTAssertGreaterThan(types.filter { $0 == "auth_token" }.count, 1)
|
|
85
|
+
XCTAssertEqual(types.last, "upload_incident_logs")
|
|
86
|
+
XCTAssertTrue(types.dropLast().allSatisfy { $0 == "auth_token" })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func testCommandQueueInsertionAndSessionClearAreSynchronous() {
|
|
91
|
+
let queue = MentraLive.CommandQueue()
|
|
92
|
+
let first = MentraLive.PendingMessage(data: Data([1]), id: "auth", retries: 0)
|
|
93
|
+
let second = MentraLive.PendingMessage(data: Data([2]), id: "upload", retries: 0)
|
|
94
|
+
XCTAssertEqual(queue.enqueue(first), 1)
|
|
95
|
+
XCTAssertEqual(queue.enqueue(second), 2)
|
|
96
|
+
XCTAssertEqual(queue.dequeue()?.id, "auth")
|
|
97
|
+
queue.removeAll()
|
|
98
|
+
XCTAssertNil(queue.dequeue())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func withToken<T>(_ value: String, body: () throws -> T) rethrows -> T {
|
|
102
|
+
let store = DeviceStore.shared.store
|
|
103
|
+
let saved = store.getCategory("bluetooth")["core_token"]
|
|
104
|
+
store.set("bluetooth", "core_token", value)
|
|
105
|
+
defer {
|
|
106
|
+
if let saved { store.set("bluetooth", "core_token", saved) }
|
|
107
|
+
else { store.remove("bluetooth", "core_token") }
|
|
108
|
+
}
|
|
109
|
+
return try body()
|
|
110
|
+
}
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.3.0-dev.
|
|
3
|
+
"version": "3.3.0-dev.780",
|
|
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.3.0-dev.
|
|
117
|
+
"@mentra/cloud-protocol": "3.3.0-dev.780"
|
|
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.3.0",
|
|
15
|
-
"releaseIdentity": "3.3.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.3.0-dev.780",
|
|
16
|
+
"releaseSetId": "mentra-3.3.0-dev.780",
|
|
17
|
+
"sourceCommit": "54e297970f1fe406469ee5f4548152b5a3fc4d9a",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.780.json",
|
|
19
|
+
"otaManifestSha256": "2207f01bb345e8988eccec986d4cadd52758dbf129c51b230050a95a8bc2f36b"
|
|
20
20
|
})
|