@mentra/bluetooth-sdk 3.4.0-dev.965 → 3.4.0-dev.971
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/debug/java/com/mentra/bluetoothsdk/sgcs/NimoResourceDiagnostics.kt +23 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +81 -19
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +89 -3
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -8,6 +8,7 @@ import android.os.Build
|
|
|
8
8
|
import android.os.Handler
|
|
9
9
|
import android.os.Looper
|
|
10
10
|
import android.os.Process
|
|
11
|
+
import android.os.SystemClock
|
|
11
12
|
import com.mentra.bluetoothsdk.BuildConfig
|
|
12
13
|
import com.mentra.bluetoothsdk.DeviceManager
|
|
13
14
|
import org.json.JSONObject
|
|
@@ -18,6 +19,7 @@ import java.util.UUID
|
|
|
18
19
|
/** Fixed, DUMP-protected debug commands. Expected address is comparison-only, never a connection target.
|
|
19
20
|
* No packet, device-command, caller path, reset, bond or radio inputs are accepted. */
|
|
20
21
|
internal object NimoResourceDiagnostics {
|
|
22
|
+
const val CLOCK_PROTOCOL = "nimo-monotonic-receipt-v1"
|
|
21
23
|
const val ACTION = "com.mentra.bluetoothsdk.NIMO_RESOURCE_LIFECYCLE"
|
|
22
24
|
private val main = Handler(Looper.getMainLooper())
|
|
23
25
|
private val processInstanceId = UUID.randomUUID().toString()
|
|
@@ -28,7 +30,8 @@ internal object NimoResourceDiagnostics {
|
|
|
28
30
|
private val requests = linkedSetOf<String>()
|
|
29
31
|
private data class Capture(val nonce: String, val name: String, val address: String,
|
|
30
32
|
var instance: Nimo? = null, var cancelled: Boolean = false, var connectStarted: Boolean = false,
|
|
31
|
-
var transportStarted: Boolean = false
|
|
33
|
+
var transportStarted: Boolean = false, val armedElapsedMs: Long,
|
|
34
|
+
var admittedElapsedMs: Long? = null)
|
|
32
35
|
// One diagnostic attempt per app process. Cancellation is a process-lifetime fence.
|
|
33
36
|
@Volatile private var capture: Capture? = null
|
|
34
37
|
internal fun preArmCaptureRequired(buildType: String = BuildConfig.BUILD_TYPE): Boolean = buildType == "nimoDiagnostic"
|
|
@@ -87,16 +90,21 @@ internal object NimoResourceDiagnostics {
|
|
|
87
90
|
owned.instance != null && owned.instance !== instance || owned.connectStarted) return false
|
|
88
91
|
if (owned.instance == null) {
|
|
89
92
|
if (!instance.resourceCaptureEligible()) return false
|
|
93
|
+
val candidateAdmittedElapsedMs = SystemClock.elapsedRealtime()
|
|
90
94
|
owned.instance = instance
|
|
91
95
|
// Immutable admission survives a setup timeout before any GATT session exists.
|
|
92
|
-
val app = application ?: return false
|
|
96
|
+
val app = application ?: run { owned.cancelled = true; return false }
|
|
93
97
|
val root = File(app.filesDir, "nimo-lifecycle")
|
|
94
98
|
val target = File(root, "${owned.nonce}.admission.json")
|
|
95
99
|
if (target.exists()) { owned.cancelled = true; return false }
|
|
96
100
|
val pending = File(root, "${owned.nonce}.admission.tmp")
|
|
97
101
|
try {
|
|
98
|
-
pending.outputStream().use { it.write(captureSnapshot(owned).toString().toByteArray(Charsets.UTF_8)); it.flush() }
|
|
102
|
+
pending.outputStream().use { it.write(captureSnapshot(owned, candidateAdmittedElapsedMs).toString().toByteArray(Charsets.UTF_8)); it.flush() }
|
|
99
103
|
check(pending.renameTo(target))
|
|
104
|
+
// Durable admission is authoritative; no live clock may claim it
|
|
105
|
+
// before the original immutable receipt is safely committed.
|
|
106
|
+
instance.resourceCaptureAdmission(owned.nonce, owned.armedElapsedMs, candidateAdmittedElapsedMs)
|
|
107
|
+
owned.admittedElapsedMs = candidateAdmittedElapsedMs
|
|
100
108
|
} catch (_: Exception) {
|
|
101
109
|
owned.cancelled = true
|
|
102
110
|
return false
|
|
@@ -124,7 +132,10 @@ internal object NimoResourceDiagnostics {
|
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
@Synchronized
|
|
127
|
-
private fun captureSnapshot(owned: Capture): JSONObject = JSONObject()
|
|
135
|
+
private fun captureSnapshot(owned: Capture, admittedElapsedMs: Long? = owned.admittedElapsedMs): JSONObject = JSONObject()
|
|
136
|
+
.put("clockProtocol", CLOCK_PROTOCOL)
|
|
137
|
+
.put("clock", JSONObject().put("kind", "android-elapsed-realtime-v1").put("armedElapsedMs", owned.armedElapsedMs)
|
|
138
|
+
.put("admittedElapsedMs", admittedElapsedMs ?: JSONObject.NULL).put("observedElapsedMs", SystemClock.elapsedRealtime()))
|
|
128
139
|
.put("version", "nimo-preconnect-capture-v1").put("nonce", owned.nonce)
|
|
129
140
|
.put("captureRequiredBeforeArm", preArmCaptureRequired())
|
|
130
141
|
.put("expectedPair", JSONObject().put("name", owned.name).put("bluetoothAddress", owned.address))
|
|
@@ -166,6 +177,7 @@ internal object NimoResourceDiagnostics {
|
|
|
166
177
|
@Synchronized
|
|
167
178
|
internal fun execute(intent: Intent) {
|
|
168
179
|
check(Looper.myLooper() == main.looper)
|
|
180
|
+
val requestReceivedElapsedMs = SystemClock.elapsedRealtime()
|
|
169
181
|
val nonce = intent.getStringExtra("nonce") ?: return
|
|
170
182
|
if (!uuid(nonce) || nonce in requests) return
|
|
171
183
|
val app = application ?: return
|
|
@@ -181,7 +193,9 @@ internal object NimoResourceDiagnostics {
|
|
|
181
193
|
.put("instanceId", instanceId ?: JSONObject.NULL).put("sessionGeneration", if (generation >= 0) generation else JSONObject.NULL)
|
|
182
194
|
fun save(session: JSONObject?, error: String? = null): Boolean {
|
|
183
195
|
val current = DeviceManager.getInstance().sgc
|
|
184
|
-
val receipt = JSONObject().put("
|
|
196
|
+
val receipt = JSONObject().put("clockProtocol", CLOCK_PROTOCOL)
|
|
197
|
+
.put("clock", JSONObject().put("kind", "android-elapsed-realtime-v1").put("requestReceivedElapsedMs", requestReceivedElapsedMs).put("observedElapsedMs", SystemClock.elapsedRealtime()))
|
|
198
|
+
.put("schemaVersion", 1).put("request", request).put("observedAtEpochMs", System.currentTimeMillis())
|
|
185
199
|
.put("appPid", Process.myPid()).put("processInstanceId", processInstanceId)
|
|
186
200
|
.put("currentManager", JSONObject().put("type", current?.type ?: JSONObject.NULL)
|
|
187
201
|
.put("instanceId", (current as? Nimo)?.resourceInstanceId ?: JSONObject.NULL))
|
|
@@ -207,7 +221,7 @@ internal object NimoResourceDiagnostics {
|
|
|
207
221
|
if (capture != null || captureNonce != nonce || manager != null) {
|
|
208
222
|
save(null, "capture-arm-refused-existing-owner"); return
|
|
209
223
|
}
|
|
210
|
-
capture = Capture(captureNonce, expectedName, address)
|
|
224
|
+
capture = Capture(captureNonce, expectedName, address, armedElapsedMs = requestReceivedElapsedMs)
|
|
211
225
|
}
|
|
212
226
|
val owned = capture
|
|
213
227
|
if (owned == null || owned.nonce != captureNonce || owned.name != expectedName || owned.address != address) {
|
|
@@ -224,7 +238,7 @@ internal object NimoResourceDiagnostics {
|
|
|
224
238
|
val original = owned.instance
|
|
225
239
|
if (original == null) { captureReceipt(); return }
|
|
226
240
|
if (DeviceManager.getInstance().sgc !== original) { captureReceipt(original.resourceSnapshot(), "capture-cancel-refused-replacement"); return }
|
|
227
|
-
original.releaseCapturedResource({ DeviceManager.getInstance().sgc === original }
|
|
241
|
+
original.releaseCapturedResource({ DeviceManager.getInstance().sgc === original }, { result -> captureReceipt(result) }, nonce, requestReceivedElapsedMs)
|
|
228
242
|
return
|
|
229
243
|
}
|
|
230
244
|
if (mode !in listOf("status", "release") || !Regex("^[0-9A-F]{2}(:[0-9A-F]{2}){5}$").matches(address) ||
|
|
@@ -250,10 +264,10 @@ internal object NimoResourceDiagnostics {
|
|
|
250
264
|
if (instanceId == null || generation < 1 || instance == null || instance !== current) {
|
|
251
265
|
save(snapshot, "release-refused-replaced-or-unowned-target"); return
|
|
252
266
|
}
|
|
253
|
-
instance.releaseResource(generation, { DeviceManager.getInstance().sgc === instance }
|
|
267
|
+
instance.releaseResource(generation, { DeviceManager.getInstance().sgc === instance }, { result ->
|
|
254
268
|
track(instance)
|
|
255
269
|
if (result.has("error")) save(instance.resourceSnapshot(), result.getString("error")) else save(result)
|
|
256
|
-
}
|
|
270
|
+
}, nonce, requestReceivedElapsedMs)
|
|
257
271
|
}
|
|
258
272
|
|
|
259
273
|
private fun uuid(value: String): Boolean = try { UUID.fromString(value).toString() == value } catch (_: IllegalArgumentException) { false }
|
|
@@ -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.4.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.4.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.4.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.4.0/mentra-live-ota-3.4.0-dev.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.4.0-dev.971"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.4.0-dev.971"
|
|
8
|
+
const val SOURCE_COMMIT: String = "c72b7ecf63112a98579639fc4fdb126aedbf425d"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.971.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "5be88fa1406efdd5024b94ae18c9124f33ed1cd8e1661f5bcfbabc522a120037"
|
|
11
11
|
}
|
|
@@ -969,6 +969,40 @@ class Nimo : SGCManager() {
|
|
|
969
969
|
private var resourceClosingGatt: BluetoothGatt? = null
|
|
970
970
|
private var resourceReleaseResult = "not-requested"
|
|
971
971
|
|
|
972
|
+
private var resourceCaptureNonce: String? = null
|
|
973
|
+
private var resourceArmedElapsedMs: Long? = null
|
|
974
|
+
private var resourceAdmittedElapsedMs: Long? = null
|
|
975
|
+
private var resourceSessionStartedElapsedMs: Long? = null
|
|
976
|
+
private var resourceReleaseNonce: String? = null
|
|
977
|
+
private var resourceReleaseStartedElapsedMs: Long? = null
|
|
978
|
+
private var firmwareElapsedMs: Long? = null
|
|
979
|
+
private var twsElapsedMs: Long? = null
|
|
980
|
+
private var handshakeTwsElapsedMs: Long? = null
|
|
981
|
+
private var timeWriteStartedElapsedMs: Long? = null
|
|
982
|
+
private var timeAckElapsedMs: Long? = null
|
|
983
|
+
private var micStopStartedElapsedMs: Long? = null
|
|
984
|
+
private var micStopMarkerElapsedMs: Long? = null
|
|
985
|
+
private var micStopWrittenElapsedMs: Long? = null
|
|
986
|
+
private var displayExitElapsedMs: Long? = null
|
|
987
|
+
|
|
988
|
+
// Keep paired clock sampling in this class even when the triggering callback
|
|
989
|
+
// is an escaping Kotlin lambda. This preserves event timing and permits APK
|
|
990
|
+
// bytecode inspection without treating an unrelated elapsed clock call as proof.
|
|
991
|
+
private fun recordResourceFirmwareClock() { resourceFirmwareAt = System.currentTimeMillis(); firmwareElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
992
|
+
private fun recordResourceTwsClock() { resourceTwsAt = System.currentTimeMillis(); twsElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
993
|
+
private fun recordResourceTimeWriteClock() { resourceTimeWriteStartedAt = System.currentTimeMillis(); timeWriteStartedElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
994
|
+
private fun recordResourceTimeAckClock() { resourceTimeAckAt = System.currentTimeMillis(); timeAckElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
995
|
+
private fun recordResourceMicStopStartedClock() { resourceMicStopStartedAt = System.currentTimeMillis(); micStopStartedElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
996
|
+
private fun recordResourceMicStopMarkerClock() { resourceMicStopMarkerAt = System.currentTimeMillis(); micStopMarkerElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
997
|
+
private fun recordResourceMicStopWrittenClock() { resourceMicStopWrittenAt = System.currentTimeMillis(); micStopWrittenElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
998
|
+
private fun recordResourceExitClock() { resourceExitAt = System.currentTimeMillis(); displayExitElapsedMs = android.os.SystemClock.elapsedRealtime() }
|
|
999
|
+
|
|
1000
|
+
internal fun resourceCaptureAdmission(nonce: String, armed: Long, admitted: Long) {
|
|
1001
|
+
check(Looper.myLooper() == mainHandler.looper)
|
|
1002
|
+
check(resourceCaptureNonce == null && admitted >= armed)
|
|
1003
|
+
resourceCaptureNonce = nonce; resourceArmedElapsedMs = armed; resourceAdmittedElapsedMs = admitted
|
|
1004
|
+
}
|
|
1005
|
+
|
|
972
1006
|
internal fun resourceCaptureEligible(): Boolean {
|
|
973
1007
|
val snapshot = resourceSnapshot()
|
|
974
1008
|
return resourceSessionGeneration == 0L && discoveryReceiver == null &&
|
|
@@ -982,6 +1016,20 @@ class Nimo : SGCManager() {
|
|
|
982
1016
|
check(Looper.myLooper() == mainHandler.looper)
|
|
983
1017
|
val pending = writes.pendingCount() + canvasEncoder.pendingCount() + audioClient.lifecyclePending() + resourcePostedWrites.get() + pendingAcks.size + canvas.pendingCount()
|
|
984
1018
|
return JSONObject().put("observedAtEpochMs", System.currentTimeMillis()).put("sessionObservationCurrent", true).put("instanceId", resourceInstanceId).put("sessionGeneration", resourceSessionGeneration)
|
|
1019
|
+
.put("clock", JSONObject().put("kind", "android-elapsed-realtime-v1").put("observedElapsedMs", android.os.SystemClock.elapsedRealtime())
|
|
1020
|
+
.put("captureNonce", resourceCaptureNonce ?: JSONObject.NULL).put("armedElapsedMs", resourceArmedElapsedMs ?: JSONObject.NULL)
|
|
1021
|
+
.put("admittedElapsedMs", resourceAdmittedElapsedMs ?: JSONObject.NULL).put("sessionStartedElapsedMs", resourceSessionStartedElapsedMs ?: JSONObject.NULL)
|
|
1022
|
+
.put("releaseNonce", resourceReleaseNonce ?: JSONObject.NULL).put("releaseStartedElapsedMs", resourceReleaseStartedElapsedMs ?: JSONObject.NULL)
|
|
1023
|
+
.put("firmwareElapsedMs", firmwareElapsedMs ?: JSONObject.NULL)
|
|
1024
|
+
.put("twsElapsedMs", twsElapsedMs ?: JSONObject.NULL)
|
|
1025
|
+
.put("handshakeTwsElapsedMs", handshakeTwsElapsedMs ?: JSONObject.NULL)
|
|
1026
|
+
.put("timeWriteStartedElapsedMs", timeWriteStartedElapsedMs ?: JSONObject.NULL)
|
|
1027
|
+
.put("timeAckElapsedMs", timeAckElapsedMs ?: JSONObject.NULL)
|
|
1028
|
+
.put("micStopStartedElapsedMs", micStopStartedElapsedMs ?: JSONObject.NULL)
|
|
1029
|
+
.put("micStopMarkerElapsedMs", micStopMarkerElapsedMs ?: JSONObject.NULL)
|
|
1030
|
+
.put("micStopWrittenElapsedMs", micStopWrittenElapsedMs ?: JSONObject.NULL)
|
|
1031
|
+
.put("displayExitElapsedMs", displayExitElapsedMs ?: JSONObject.NULL)
|
|
1032
|
+
)
|
|
985
1033
|
.put("pair", JSONObject().put("name", resourcePairName ?: lastDeviceName ?: JSONObject.NULL)
|
|
986
1034
|
.put("bluetoothAddress", resourcePairAddress ?: lastDeviceAddress ?: JSONObject.NULL))
|
|
987
1035
|
.put("handshake", handshakeState.name).put("managedSessionPresent", gatt != null)
|
|
@@ -1003,21 +1051,32 @@ class Nimo : SGCManager() {
|
|
|
1003
1051
|
|
|
1004
1052
|
/** Exact original instance only. Never delegates to the manager's current device. */
|
|
1005
1053
|
internal fun releaseResource(expectedGeneration: Long, isCurrent: () -> Boolean, completed: (JSONObject) -> Unit) =
|
|
1006
|
-
releaseOriginalResource(expectedGeneration, false, isCurrent, completed)
|
|
1054
|
+
releaseOriginalResource(expectedGeneration, false, isCurrent, completed, null, null)
|
|
1055
|
+
|
|
1056
|
+
internal fun releaseResource(expectedGeneration: Long, isCurrent: () -> Boolean, completed: (JSONObject) -> Unit, requestNonce: String, requestReceivedElapsedMs: Long) =
|
|
1057
|
+
releaseOriginalResource(expectedGeneration, false, isCurrent, completed, requestNonce, requestReceivedElapsedMs)
|
|
1007
1058
|
|
|
1008
1059
|
/** Only the private admitted capture may cancel an original pre-GATT generation-zero attempt. */
|
|
1009
1060
|
internal fun releaseCapturedResource(isCurrent: () -> Boolean, completed: (JSONObject) -> Unit) =
|
|
1010
|
-
releaseOriginalResource(resourceSessionGeneration, true, isCurrent, completed)
|
|
1061
|
+
releaseOriginalResource(resourceSessionGeneration, true, isCurrent, completed, null, null)
|
|
1062
|
+
|
|
1063
|
+
internal fun releaseCapturedResource(isCurrent: () -> Boolean, completed: (JSONObject) -> Unit, requestNonce: String, requestReceivedElapsedMs: Long) =
|
|
1064
|
+
releaseOriginalResource(resourceSessionGeneration, true, isCurrent, completed, requestNonce, requestReceivedElapsedMs)
|
|
1011
1065
|
|
|
1012
|
-
private fun releaseOriginalResource(expectedGeneration: Long, captured: Boolean, isCurrent: () -> Boolean, completed: (JSONObject) -> Unit) {
|
|
1066
|
+
private fun releaseOriginalResource(expectedGeneration: Long, captured: Boolean, isCurrent: () -> Boolean, completed: (JSONObject) -> Unit, requestNonce: String?, requestReceivedElapsedMs: Long?) {
|
|
1013
1067
|
check(Looper.myLooper() == mainHandler.looper)
|
|
1014
1068
|
if (!isCurrent() || expectedGeneration != resourceSessionGeneration || (!captured && expectedGeneration < 1) || resourceReleasePending || resourceReceiptPending) {
|
|
1015
1069
|
completed(JSONObject().put("error", "target-mismatch-or-release-pending")); return
|
|
1016
1070
|
}
|
|
1017
1071
|
if (resourceReleased) { completed(resourceSnapshot()); return }
|
|
1072
|
+
if (requestReceivedElapsedMs != null && android.os.SystemClock.elapsedRealtime() < requestReceivedElapsedMs) {
|
|
1073
|
+
completed(JSONObject().put("error", "invalid-request-clock")); return
|
|
1074
|
+
}
|
|
1075
|
+
resourceReleaseNonce = requestNonce
|
|
1076
|
+
resourceReleaseStartedElapsedMs = android.os.SystemClock.elapsedRealtime()
|
|
1018
1077
|
resourceReleasePending = true
|
|
1019
1078
|
resourceReleaseResult = "draining"
|
|
1020
|
-
resourceExitStatus = null; resourceExitAt = null; resourceMicStopStartedAt = null; resourceMicStopMarkerAt = null; resourceMicStopWrittenAt = null
|
|
1079
|
+
resourceExitStatus = null; resourceExitAt = null; displayExitElapsedMs = null; resourceMicStopStartedAt = null; micStopStartedElapsedMs = null; resourceMicStopMarkerAt = null; micStopMarkerElapsedMs = null; resourceMicStopWrittenAt = null; micStopWrittenElapsedMs = null
|
|
1021
1080
|
resourceExitRequired = canvas.active() || canvas.pendingCount() > 0
|
|
1022
1081
|
resourceInitialLaunchPendingAtRelease = !canvas.active() && canvas.launchPending()
|
|
1023
1082
|
isDisconnecting = true
|
|
@@ -1035,7 +1094,7 @@ class Nimo : SGCManager() {
|
|
|
1035
1094
|
val mic = micChar
|
|
1036
1095
|
resourceMicStopRequired = original != null && mic != null
|
|
1037
1096
|
if (original != null && mic != null) {
|
|
1038
|
-
writes.enqueue(original, mic, listOf(byteArrayOf(0x52, 0, 0, 0)), finalStarted = {
|
|
1097
|
+
writes.enqueue(original, mic, listOf(byteArrayOf(0x52, 0, 0, 0)), finalStarted = { recordResourceMicStopStartedClock() }, completed = { recordResourceMicStopWrittenClock() })
|
|
1039
1098
|
}
|
|
1040
1099
|
canvas.exit()
|
|
1041
1100
|
val deadline = android.os.SystemClock.elapsedRealtime() + 15_000
|
|
@@ -1099,7 +1158,7 @@ class Nimo : SGCManager() {
|
|
|
1099
1158
|
{ Bridge.log("NIMO: canvas rejected status=$it; not a render ACK") },
|
|
1100
1159
|
{ diagnostics?.cancelHeldCapture("Canvas scope takeover preempted held capture", resumeCanvas = false) },
|
|
1101
1160
|
{ key, status ->
|
|
1102
|
-
if (key == 3) { resourceExitStatus = status;
|
|
1161
|
+
if (key == 3) { resourceExitStatus = status; recordResourceExitClock() }
|
|
1103
1162
|
if (resourceReleasePending && resourceInitialLaunchPendingAtRelease && key == 1) {
|
|
1104
1163
|
// An explicit rejected initial Launch never acquired canvas ownership.
|
|
1105
1164
|
// Status 2 is transport-ambiguous and must retain the unknown fence.
|
|
@@ -1869,15 +1928,17 @@ class Nimo : SGCManager() {
|
|
|
1869
1928
|
// for system-paired dual-mode accessories).
|
|
1870
1929
|
Bridge.log("NIMO: connecting GATT over BR/EDR to ${device.address}")
|
|
1871
1930
|
resourceSessionGeneration++
|
|
1872
|
-
|
|
1873
|
-
|
|
1931
|
+
resourceSessionStartedElapsedMs = android.os.SystemClock.elapsedRealtime()
|
|
1932
|
+
resourceReleaseNonce = null; resourceReleaseStartedElapsedMs = null
|
|
1933
|
+
resourceExitStatus = null; resourceExitAt = null; displayExitElapsedMs = null; resourceMicStopWrittenAt = null; micStopWrittenElapsedMs = null
|
|
1934
|
+
resourceMicStopStartedAt = null; micStopStartedElapsedMs = null; resourceMicStopMarkerAt = null; micStopMarkerElapsedMs = null; resourceTransportFailure = false; resourceClosingGatt = null
|
|
1874
1935
|
resourceClosedWithPending = false; resourceReleaseResult = "not-requested"
|
|
1875
1936
|
resourceMicStopRequired = false; resourceExitRequired = false; resourceInitialLaunchPendingAtRelease = false
|
|
1876
1937
|
resourceReleased = false
|
|
1877
1938
|
resourcePairAddress = peer.address
|
|
1878
1939
|
resourcePairName = try { device.name ?: "" } catch (_: SecurityException) { "" }
|
|
1879
|
-
resourceFirmwareAt = null; resourceFirmwareVersion = null
|
|
1880
|
-
resourceTwsAt = null; resourceHandshakeTwsAt = null; resourceTimeAckAt = null; resourceTimeCommandId = null; resourceTimeWriteStartedAt = null
|
|
1940
|
+
resourceFirmwareAt = null; firmwareElapsedMs = null; resourceFirmwareVersion = null
|
|
1941
|
+
resourceTwsAt = null; twsElapsedMs = null; resourceHandshakeTwsAt = null; handshakeTwsElapsedMs = null; resourceTimeAckAt = null; timeAckElapsedMs = null; resourceTimeCommandId = null; resourceTimeWriteStartedAt = null; timeWriteStartedElapsedMs = null
|
|
1881
1942
|
resourceCloseCalled = false; resourceCloseObserved = false; resourceDisconnectCallbackStatus = null; resourceCloseError = null
|
|
1882
1943
|
NimoResourceDiagnostics.track(this)
|
|
1883
1944
|
DeviceStore.apply("glasses", "connectionState", ConnTypes.CONNECTING)
|
|
@@ -2121,8 +2182,9 @@ class Nimo : SGCManager() {
|
|
|
2121
2182
|
handleRxPacket(data)
|
|
2122
2183
|
}
|
|
2123
2184
|
NimoBLE.CHAR_MIC -> {
|
|
2124
|
-
if (resourceReleasePending && resourceMicStopStartedAt != null && data.contentEquals(byteArrayOf(0x52, 0, 0, 0)))
|
|
2125
|
-
|
|
2185
|
+
if (resourceReleasePending && resourceMicStopStartedAt != null && data.contentEquals(byteArrayOf(0x52, 0, 0, 0))) {
|
|
2186
|
+
recordResourceMicStopMarkerClock()
|
|
2187
|
+
}
|
|
2126
2188
|
audioClient.enqueue(data)
|
|
2127
2189
|
}
|
|
2128
2190
|
}
|
|
@@ -2180,7 +2242,7 @@ class Nimo : SGCManager() {
|
|
|
2180
2242
|
resourcePostedWrites.decrementAndGet()
|
|
2181
2243
|
if (gatt !== expectedGatt || resourceReleasePending || resourceReleased) return@post
|
|
2182
2244
|
writes.enqueue(expectedGatt, char, listOf(bytes), finalStarted = {
|
|
2183
|
-
if (timeCommand != null && timeCommand == resourceTimeCommandId)
|
|
2245
|
+
if (timeCommand != null && timeCommand == resourceTimeCommandId) { recordResourceTimeWriteClock() }
|
|
2184
2246
|
})
|
|
2185
2247
|
}
|
|
2186
2248
|
}
|
|
@@ -2294,7 +2356,7 @@ class Nimo : SGCManager() {
|
|
|
2294
2356
|
private fun proceedToTimeSync() {
|
|
2295
2357
|
if (handshakeState != HandshakeState.AWAITING_TWS) return
|
|
2296
2358
|
cancelTwsTimeout()
|
|
2297
|
-
resourceHandshakeTwsAt = resourceTwsAt
|
|
2359
|
+
resourceHandshakeTwsAt = resourceTwsAt; handshakeTwsElapsedMs = twsElapsedMs
|
|
2298
2360
|
handshakeState = HandshakeState.AWAITING_TIME_ACK
|
|
2299
2361
|
Bridge.log("NIMO: TWS OK — sending setTime (awaiting ACK)")
|
|
2300
2362
|
setTimeAttempts = 0
|
|
@@ -2306,7 +2368,7 @@ class Nimo : SGCManager() {
|
|
|
2306
2368
|
val expectedGatt = gatt
|
|
2307
2369
|
setTimeAttempts++
|
|
2308
2370
|
resourceTimeCommandId = UUID.randomUUID().toString()
|
|
2309
|
-
resourceTimeWriteStartedAt = null
|
|
2371
|
+
resourceTimeWriteStartedAt = null; timeWriteStartedElapsedMs = null
|
|
2310
2372
|
sendAwaitingAck(
|
|
2311
2373
|
NimoProtocol.CMD_SET_PARAMETER,
|
|
2312
2374
|
NimoProtocol.SET_TIME,
|
|
@@ -2319,7 +2381,7 @@ class Nimo : SGCManager() {
|
|
|
2319
2381
|
return@sendAwaitingAck
|
|
2320
2382
|
}
|
|
2321
2383
|
if (ok) {
|
|
2322
|
-
|
|
2384
|
+
recordResourceTimeAckClock()
|
|
2323
2385
|
finishHandshake()
|
|
2324
2386
|
} else if (setTimeAttempts < SET_TIME_MAX_ATTEMPTS &&
|
|
2325
2387
|
handshakeState == HandshakeState.AWAITING_TIME_ACK &&
|
|
@@ -2544,7 +2606,7 @@ class Nimo : SGCManager() {
|
|
|
2544
2606
|
}
|
|
2545
2607
|
|
|
2546
2608
|
private fun onTwsState(connected: Boolean) {
|
|
2547
|
-
|
|
2609
|
+
recordResourceTwsClock()
|
|
2548
2610
|
twsConnected = connected
|
|
2549
2611
|
if (connected && handshakeState == HandshakeState.AWAITING_TWS) {
|
|
2550
2612
|
proceedToTimeSync()
|
|
@@ -2617,7 +2679,7 @@ class Nimo : SGCManager() {
|
|
|
2617
2679
|
val micro = (v ushr 12) and 0x1FF
|
|
2618
2680
|
val build = v and 0xFFF
|
|
2619
2681
|
firmwareVersionPacked = "$major.$minor.$micro.$build"
|
|
2620
|
-
resourceFirmwareVersion = firmwareVersionPacked;
|
|
2682
|
+
resourceFirmwareVersion = firmwareVersionPacked; recordResourceFirmwareClock()
|
|
2621
2683
|
emitVersionInfo()
|
|
2622
2684
|
}
|
|
2623
2685
|
}
|
|
@@ -2626,7 +2688,7 @@ class Nimo : SGCManager() {
|
|
|
2626
2688
|
while (end > 0 && data[end - 1].toInt() == 0) end--
|
|
2627
2689
|
firmwareVersionDetail = String(data, 0, end, Charsets.UTF_8).trim()
|
|
2628
2690
|
if (firmwareVersionDetail.isNotEmpty() && firmwareVersionDetail.length <= 240) {
|
|
2629
|
-
resourceFirmwareVersion = firmwareVersionDetail;
|
|
2691
|
+
resourceFirmwareVersion = firmwareVersionDetail; recordResourceFirmwareClock()
|
|
2630
2692
|
}
|
|
2631
2693
|
emitVersionInfo()
|
|
2632
2694
|
}
|
|
@@ -120,6 +120,63 @@ class NimoLifecycleTest {
|
|
|
120
120
|
.set(null, previousManager)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
@Test fun monotonicSnapshotRetainsOriginalAdmissionWhenWallMarkersJumpBothDirections() {
|
|
124
|
+
val armed = android.os.SystemClock.elapsedRealtime()
|
|
125
|
+
val nonce = java.util.UUID.randomUUID().toString()
|
|
126
|
+
org.robolectric.shadows.ShadowSystemClock.advanceBy(25, TimeUnit.MILLISECONDS)
|
|
127
|
+
val admitted = android.os.SystemClock.elapsedRealtime()
|
|
128
|
+
nimo.resourceCaptureAdmission(nonce, armed, admitted)
|
|
129
|
+
setField("resourceSessionStartedElapsedMs", admitted)
|
|
130
|
+
setField("resourceSessionGeneration", 1L)
|
|
131
|
+
setField("twsElapsedMs", admitted)
|
|
132
|
+
setField("handshakeTwsElapsedMs", admitted)
|
|
133
|
+
setField("timeWriteStartedElapsedMs", admitted)
|
|
134
|
+
org.robolectric.shadows.ShadowSystemClock.advanceBy(10, TimeUnit.MILLISECONDS)
|
|
135
|
+
val ack = android.os.SystemClock.elapsedRealtime()
|
|
136
|
+
setField("timeAckElapsedMs", ack)
|
|
137
|
+
// Model independent phone epoch jumps: neither direction can rewrite causal time.
|
|
138
|
+
for (epoch in listOf(1L, 4_000_000_000_000L)) {
|
|
139
|
+
setField("resourceTwsAt", epoch)
|
|
140
|
+
setField("resourceHandshakeTwsAt", epoch)
|
|
141
|
+
setField("resourceTimeWriteStartedAt", epoch)
|
|
142
|
+
setField("resourceTimeAckAt", epoch)
|
|
143
|
+
val clock = nimo.resourceSnapshot().getJSONObject("clock")
|
|
144
|
+
assertEquals("android-elapsed-realtime-v1", clock.getString("kind"))
|
|
145
|
+
assertEquals(nonce, clock.getString("captureNonce"))
|
|
146
|
+
assertEquals(armed, clock.getLong("armedElapsedMs"))
|
|
147
|
+
assertEquals(admitted, clock.getLong("admittedElapsedMs"))
|
|
148
|
+
assertEquals(ack, clock.getLong("timeAckElapsedMs"))
|
|
149
|
+
assertTrue(clock.getLong("observedElapsedMs") >= ack)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test fun originalReleaseClockAndNonceSurviveLaterStatusAndRepeatedRelease() {
|
|
154
|
+
manager.sgc = nimo
|
|
155
|
+
NimoResourceDiagnostics.start(context)
|
|
156
|
+
setField("resourceSessionGeneration", 1L)
|
|
157
|
+
setField("resourcePairAddress", "00:11:22:33:44:55")
|
|
158
|
+
val nonce = java.util.UUID.randomUUID().toString()
|
|
159
|
+
val received = android.os.SystemClock.elapsedRealtime()
|
|
160
|
+
var first: JSONObject? = null
|
|
161
|
+
nimo.releaseResource(1L, { true }, { first = it }, nonce, received)
|
|
162
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(300, TimeUnit.MILLISECONDS)
|
|
163
|
+
val original = requireNotNull(first).getJSONObject("clock")
|
|
164
|
+
assertEquals(nonce, original.getString("releaseNonce"))
|
|
165
|
+
assertTrue(original.getLong("releaseStartedElapsedMs") >= received)
|
|
166
|
+
org.robolectric.shadows.ShadowSystemClock.advanceBy(10, TimeUnit.SECONDS)
|
|
167
|
+
val status = resourceCommand("status")
|
|
168
|
+
assertEquals("nimo-monotonic-receipt-v1", status.getString("clockProtocol"))
|
|
169
|
+
val statusClock = status.getJSONObject("clock")
|
|
170
|
+
assertTrue(statusClock.getLong("observedElapsedMs") >= statusClock.getLong("requestReceivedElapsedMs"))
|
|
171
|
+
var repeated: JSONObject? = null
|
|
172
|
+
nimo.releaseResource(1L, { true }, { repeated = it }, java.util.UUID.randomUUID().toString(), android.os.SystemClock.elapsedRealtime())
|
|
173
|
+
for (snapshot in listOf(status.getJSONObject("session"), requireNotNull(repeated))) {
|
|
174
|
+
val clock = snapshot.getJSONObject("clock")
|
|
175
|
+
assertEquals(nonce, clock.getString("releaseNonce"))
|
|
176
|
+
assertEquals(original.getLong("releaseStartedElapsedMs"), clock.getLong("releaseStartedElapsedMs"))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
123
180
|
@Test fun offMainDisconnectCompletesOnMainAndInterruptsActiveCapture() {
|
|
124
181
|
runOffMain { nimo.disconnect() }
|
|
125
182
|
assertDisconnectedOnMain()
|
|
@@ -303,12 +360,16 @@ class NimoLifecycleTest {
|
|
|
303
360
|
assertEquals(0, snapshot.getJSONObject("timeAck").getInt("statusCode"))
|
|
304
361
|
assertFalse(snapshot.getJSONObject("timeAck").isNull("writeStartedAtEpochMs"))
|
|
305
362
|
assertEquals("READY", snapshot.getString("handshake"))
|
|
306
|
-
val causalTws = snapshot.getJSONObject("
|
|
363
|
+
val causalTws = snapshot.getJSONObject("clock").getLong("handshakeTwsElapsedMs")
|
|
307
364
|
Shadows.shadowOf(Looper.getMainLooper()).idleFor(100, TimeUnit.MILLISECONDS)
|
|
308
365
|
Nimo::class.java.getDeclaredMethod("onTwsState", Boolean::class.javaPrimitiveType).apply { isAccessible = true }.invoke(nimo, true)
|
|
309
366
|
val heartbeat = nimo.resourceSnapshot()
|
|
310
|
-
|
|
311
|
-
|
|
367
|
+
// Raw phone epochs are diagnostics; event order belongs to phone elapsed time.
|
|
368
|
+
assertFalse(heartbeat.getJSONObject("tws").isNull("atEpochMs"))
|
|
369
|
+
assertFalse(heartbeat.getJSONObject("timeAck").isNull("atEpochMs"))
|
|
370
|
+
val clock = heartbeat.getJSONObject("clock")
|
|
371
|
+
assertEquals(causalTws, clock.getLong("handshakeTwsElapsedMs"))
|
|
372
|
+
assertTrue(clock.getLong("twsElapsedMs") > clock.getLong("timeAckElapsedMs"))
|
|
312
373
|
assertEquals("READY", heartbeat.getString("handshake"))
|
|
313
374
|
}
|
|
314
375
|
|
|
@@ -393,9 +454,34 @@ class NimoLifecycleTest {
|
|
|
393
454
|
File(context.filesDir, "nimo-lifecycle/$nonce.admission.tmp").mkdir()
|
|
394
455
|
assertFalse(NimoResourceDiagnostics.admit(nimo, "Nimo-test", true))
|
|
395
456
|
assertFalse(NimoResourceDiagnostics.allowManagerConnect("NIMO", "Nimo-test"))
|
|
457
|
+
assertFalse(NimoResourceDiagnostics.beginTransport(nimo, "00:11:22:33:44:55", true))
|
|
458
|
+
assertFalse(NimoResourceDiagnostics.admit(nimo, "Nimo-test", true))
|
|
459
|
+
assertFalse(File(context.filesDir, "nimo-lifecycle/$nonce.admission.json").exists())
|
|
460
|
+
val clock = nimo.resourceSnapshot().getJSONObject("clock")
|
|
461
|
+
assertTrue(clock.isNull("captureNonce")); assertTrue(clock.isNull("admittedElapsedMs"))
|
|
462
|
+
val capture = captureCommand("capture-status", nonce).getJSONObject("session").getJSONObject("capture")
|
|
463
|
+
assertTrue(capture.getBoolean("cancelled"))
|
|
464
|
+
assertTrue(capture.getJSONObject("clock").isNull("admittedElapsedMs"))
|
|
396
465
|
assertEquals(0L, nimo.resourceSnapshot().getLong("sessionGeneration"))
|
|
397
466
|
}
|
|
398
467
|
|
|
468
|
+
@Test fun preexistingAdmissionIsNeverOverwrittenOrStampedOntoLiveInstance() {
|
|
469
|
+
manager.sgc = null
|
|
470
|
+
NimoResourceDiagnostics.start(context)
|
|
471
|
+
val nonce = java.util.UUID.randomUUID().toString()
|
|
472
|
+
captureCommand("arm-capture", nonce, nonce)
|
|
473
|
+
manager.sgc = nimo
|
|
474
|
+
val original = File(context.filesDir, "nimo-lifecycle/$nonce.admission.json")
|
|
475
|
+
original.writeText("preexisting-immutable-evidence")
|
|
476
|
+
assertFalse(NimoResourceDiagnostics.admit(nimo, "Nimo-test", true))
|
|
477
|
+
assertEquals("preexisting-immutable-evidence", original.readText())
|
|
478
|
+
assertFalse(NimoResourceDiagnostics.allowManagerConnect("NIMO", "Nimo-test"))
|
|
479
|
+
assertFalse(NimoResourceDiagnostics.beginTransport(nimo, "00:11:22:33:44:55", true))
|
|
480
|
+
val clock = nimo.resourceSnapshot().getJSONObject("clock")
|
|
481
|
+
assertTrue(clock.isNull("captureNonce")); assertTrue(clock.isNull("admittedElapsedMs"))
|
|
482
|
+
assertTrue(captureCommand("capture-status", nonce).getJSONObject("session").getJSONObject("capture").getJSONObject("clock").isNull("admittedElapsedMs"))
|
|
483
|
+
}
|
|
484
|
+
|
|
399
485
|
@Test fun capturedGenerationZeroCancellationDrainsOriginalAndFencesQueuedAdmission() {
|
|
400
486
|
manager.sgc = null
|
|
401
487
|
NimoResourceDiagnostics.start(context)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "3.4.0",
|
|
4
|
-
"releaseIdentity": "3.4.0-dev.
|
|
5
|
-
"releaseSetId": "mentra-3.4.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.
|
|
8
|
-
"otaManifestSha256": "
|
|
4
|
+
"releaseIdentity": "3.4.0-dev.971",
|
|
5
|
+
"releaseSetId": "mentra-3.4.0-dev.971",
|
|
6
|
+
"sourceCommit": "c72b7ecf63112a98579639fc4fdb126aedbf425d",
|
|
7
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.971.json",
|
|
8
|
+
"otaManifestSha256": "5be88fa1406efdd5024b94ae18c9124f33ed1cd8e1661f5bcfbabc522a120037"
|
|
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.4.0\",\n \"releaseIdentity\": \"3.4.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.4.0\",\n \"releaseIdentity\": \"3.4.0-dev.971\",\n \"releaseSetId\": \"mentra-3.4.0-dev.971\",\n \"sourceCommit\": \"c72b7ecf63112a98579639fc4fdb126aedbf425d\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.971.json\",\n \"otaManifestSha256\": \"5be88fa1406efdd5024b94ae18c9124f33ed1cd8e1661f5bcfbabc522a120037\"\n})\n"]}
|
|
@@ -16,7 +16,7 @@ enum BluetoothSdkDefaults {
|
|
|
16
16
|
/// silently disable it on every pair the app touches.
|
|
17
17
|
static let autoPowerOffEnabled = false
|
|
18
18
|
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
19
|
-
private static let swiftPackageSdkVersion = "3.4.0-dev.
|
|
19
|
+
private static let swiftPackageSdkVersion = "3.4.0-dev.971"
|
|
20
20
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
21
21
|
|
|
22
22
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.4.0"
|
|
6
|
-
static let releaseIdentity = "3.4.0-dev.
|
|
7
|
-
static let releaseSetId = "mentra-3.4.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.4.0-dev.971"
|
|
7
|
+
static let releaseSetId = "mentra-3.4.0-dev.971"
|
|
8
|
+
static let sourceCommit = "c72b7ecf63112a98579639fc4fdb126aedbf425d"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.971.json"
|
|
10
|
+
static let otaManifestSha256 = "5be88fa1406efdd5024b94ae18c9124f33ed1cd8e1661f5bcfbabc522a120037"
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.4.0-dev.
|
|
3
|
+
"version": "3.4.0-dev.971",
|
|
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.4.0-dev.
|
|
117
|
+
"@mentra/cloud-protocol": "3.4.0-dev.971"
|
|
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.4.0",
|
|
15
|
-
"releaseIdentity": "3.4.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.4.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.4.0-dev.971",
|
|
16
|
+
"releaseSetId": "mentra-3.4.0-dev.971",
|
|
17
|
+
"sourceCommit": "c72b7ecf63112a98579639fc4fdb126aedbf425d",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.971.json",
|
|
19
|
+
"otaManifestSha256": "5be88fa1406efdd5024b94ae18c9124f33ed1cd8e1661f5bcfbabc522a120037"
|
|
20
20
|
})
|