@mentra/bluetooth-sdk 3.2.0-dev.216 → 3.2.0-dev.221

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 CHANGED
@@ -29,6 +29,15 @@ to its existing retention limits. Native diagnostics are also kept in the native
29
29
  console. Forwarding covers SDK-owned diagnostics while the JS runtime is active,
30
30
  not arbitrary OS or third-party native logs or logs from before subscription.
31
31
 
32
+ On Android, forwarding to JavaScript is capped at 100 messages per second. Each
33
+ forwarded message holds a JNI global reference until the JavaScript thread drains
34
+ it, so an unbounded log source can exhaust the process-wide reference table and
35
+ abort the app rather than merely slow it down. Messages above the cap are withheld
36
+ from the JavaScript stream only — they are still written to the native console —
37
+ and the next forwarded message is preceded by a notice saying how many were
38
+ withheld. Per-frame microphone payload events are not traced at all; audio faults
39
+ are still reported through the `mic_health` event.
40
+
32
41
  Rebuild the native app after updating the SDK to pick up both platforms' logging
33
42
  changes. Adding this JS package alone cannot reroute logs in an older binary.
34
43
 
@@ -133,6 +133,7 @@ private inline fun <
133
133
  class BluetoothSdkModule : Module() {
134
134
  private var sdk: MentraBluetoothSdk? = null
135
135
  private var deviceManager: DeviceManager? = null
136
+ private val logForwarding = LogForwardingBudget()
136
137
  private val sdkListener =
137
138
  object : MentraBluetoothSdkListener {
138
139
  override fun onGlassesChanged(glasses: GlassesRuntimeState) {
@@ -295,6 +296,16 @@ class BluetoothSdkModule : Module() {
295
296
  }
296
297
 
297
298
  override fun onLog(message: String) {
299
+ // Each event pins a JNI global reference until JavaScript drains it. The
300
+ // log stream is the one source whose rate is unbounded, so it is budgeted.
301
+ val withheld = logForwarding.admit() ?: return
302
+ if (withheld > 0) {
303
+ val notice =
304
+ "[W/BluetoothSdkModule] withheld $withheld native log line(s) from " +
305
+ "JavaScript to stay under ${logForwarding.maxPerWindow}/s; " +
306
+ "logcat has them all"
307
+ sendEvent("log", mapOf("message" to notice))
308
+ }
298
309
  sendEvent("log", mapOf("message" to message))
299
310
  }
300
311
 
@@ -30,18 +30,6 @@ public class Bridge private constructor() {
30
30
  private const val MIC_CHANNELS = 1
31
31
  private const val LC3_FRAME_DURATION_MS = 10
32
32
  private const val DEFAULT_LC3_FRAME_SIZE_BYTES = 60
33
- private val AUDIO_TRACE_METADATA_KEYS =
34
- listOf(
35
- "sampleRate",
36
- "bitsPerSample",
37
- "channels",
38
- "encoding",
39
- "frameDurationMs",
40
- "frameSizeBytes",
41
- "bitrate",
42
- "packetizedFromGlasses",
43
- "voiceActivityDetectionEnabled",
44
- )
45
33
 
46
34
  @Volatile private var instance: Bridge? = null
47
35
 
@@ -943,43 +931,29 @@ public class Bridge private constructor() {
943
931
  }
944
932
  }
945
933
 
934
+ /**
935
+ * Returns null for events that must not be traced.
936
+ *
937
+ * "log" is excluded so tracing never recurses back through NativeLog. Audio payload
938
+ * events are excluded because they arrive at frame rate: tracing them turned every
939
+ * microphone frame into a second bridge event, and each of those pins a JNI global
940
+ * reference until JavaScript drains it. A JavaScript thread busy with call audio
941
+ * cannot keep up, so the references accumulated until the process-wide table
942
+ * overflowed and the runtime aborted. Audio faults (sequence gaps, decode failures)
943
+ * are still reported through "mic_health", and healthy frames need no trace.
944
+ */
946
945
  private fun tracePayloadForTypedMessage(
947
946
  type: String,
948
947
  body: Map<String, Any>
949
948
  ): Map<String, Any>? =
950
949
  when {
951
950
  type == "log" -> null
952
- isAudioPayloadEvent(type) -> audioTracePayload(type, body)
951
+ isAudioPayloadEvent(type) -> null
953
952
  else -> body
954
953
  }
955
954
 
956
955
  private fun isAudioPayloadEvent(type: String): Boolean =
957
956
  type == "mic_pcm" || type == "mic_lc3"
958
-
959
- private fun audioTracePayload(type: String, body: Map<String, Any>): Map<String, Any> {
960
- val payload = HashMap<String, Any>()
961
- payload["type"] = type
962
- payload["timestamp"] = System.currentTimeMillis()
963
- payload["payloadOmitted"] = true
964
- payload["payloadOmittedReason"] = "audio"
965
-
966
- val audioBytes =
967
- when (type) {
968
- "mic_pcm" -> (body["pcm"] as? ByteArray)?.size
969
- "mic_lc3" -> (body["lc3"] as? ByteArray)?.size
970
- else -> null
971
- }
972
- audioBytes?.let { payload["audioBytes"] = it }
973
-
974
- AUDIO_TRACE_METADATA_KEYS.forEach { key ->
975
- val value = body[key]
976
- if (value != null) {
977
- payload[key] = value
978
- }
979
- }
980
-
981
- return payload
982
- }
983
957
  }
984
958
 
985
959
  init {
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
3
3
  /** Generated by release CI. Do not edit in a release checkout. */
4
4
  internal object GeneratedReleaseMetadata {
5
5
  const val FAMILY_BASE_VERSION: String = "3.2.0"
6
- const val RELEASE_IDENTITY: String = "3.2.0-dev.216"
7
- const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.216"
8
- const val SOURCE_COMMIT: String = "4dc8e8b75c7a782abb2a9dead146d023d2fe45d6"
9
- const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.216.json"
10
- const val OTA_MANIFEST_SHA256: String = "e0b87036ab2c104b97dcf1a6910d3b0da114c59047ee29dbecf747647cbdcd9a"
6
+ const val RELEASE_IDENTITY: String = "3.2.0-dev.221"
7
+ const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.221"
8
+ const val SOURCE_COMMIT: String = "3837a3055e7a69e18553ce51c3a97579d2ad3b83"
9
+ const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.221.json"
10
+ const val OTA_MANIFEST_SHA256: String = "657e9616ad0c464ee57d827c05b53393e45559992c13b15adbb06e0c903eb870"
11
11
  }
@@ -0,0 +1,50 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import android.os.SystemClock
4
+
5
+ /**
6
+ * Bounds how many native log lines per second the module hands to the JavaScript runtime.
7
+ *
8
+ * Every event the module emits pins a JNI global reference until the JavaScript thread
9
+ * drains it, and the process-wide table holds about 51,200 references. Native code can log
10
+ * faster than a busy JavaScript thread drains, so an unbounded log stream aborts the process
11
+ * instead of merely lagging. Lines over the budget are withheld from JavaScript only; the
12
+ * native console still has every one of them. The first line forwarded after a shortfall
13
+ * reports how many were withheld, so the gap is visible in the JavaScript stream.
14
+ */
15
+ internal class LogForwardingBudget(
16
+ val maxPerWindow: Int = DEFAULT_MAX_PER_SECOND,
17
+ private val windowMs: Long = 1_000L,
18
+ private val clock: () -> Long = SystemClock::elapsedRealtime,
19
+ ) {
20
+ private var windowEndsAtMs = Long.MIN_VALUE
21
+ private var forwardedInWindow = 0
22
+ private var withheld = 0
23
+
24
+ /**
25
+ * Returns null when the current line must be withheld, otherwise the number of lines
26
+ * withheld since the previous forwarded one (zero in steady state).
27
+ */
28
+ @Synchronized
29
+ fun admit(): Int? {
30
+ val now = clock()
31
+ if (now >= windowEndsAtMs) {
32
+ windowEndsAtMs = now + windowMs
33
+ forwardedInWindow = 0
34
+ }
35
+ if (forwardedInWindow >= maxPerWindow) {
36
+ withheld++
37
+ return null
38
+ }
39
+ forwardedInWindow++
40
+ return withheld.also { withheld = 0 }
41
+ }
42
+
43
+ companion object {
44
+ /**
45
+ * Far above the SDK's steady-state diagnostic rate, and low enough that a
46
+ * JavaScript thread has to stay stalled for minutes before the table could fill.
47
+ */
48
+ const val DEFAULT_MAX_PER_SECOND = 100
49
+ }
50
+ }
@@ -0,0 +1,49 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
+ import org.junit.Test
6
+
7
+ class LogForwardingBudgetTest {
8
+ private var nowMs = 0L
9
+ private val budget = LogForwardingBudget(maxPerWindow = 3, windowMs = 1_000L, clock = { nowMs })
10
+
11
+ @Test
12
+ fun `forwards up to the budget within a window and withholds the rest`() {
13
+ assertEquals(0, budget.admit())
14
+ assertEquals(0, budget.admit())
15
+ assertEquals(0, budget.admit())
16
+ assertNull(budget.admit())
17
+ assertNull(budget.admit())
18
+ }
19
+
20
+ @Test
21
+ fun `the first line forwarded after a shortfall reports how many were withheld`() {
22
+ repeat(5) { budget.admit() }
23
+
24
+ nowMs = 1_000L
25
+ assertEquals(2, budget.admit())
26
+ assertEquals(0, budget.admit())
27
+ }
28
+
29
+ @Test
30
+ fun `the budget renews every window`() {
31
+ repeat(5) { budget.admit() }
32
+ nowMs = 999L
33
+ assertNull(budget.admit())
34
+
35
+ nowMs = 1_000L
36
+ assertEquals(3, budget.admit())
37
+ assertEquals(0, budget.admit())
38
+ assertEquals(0, budget.admit())
39
+ assertNull(budget.admit())
40
+ }
41
+
42
+ @Test
43
+ fun `withheld lines are reported even after a long silence`() {
44
+ repeat(4) { budget.admit() }
45
+
46
+ nowMs = 60_000L
47
+ assertEquals(1, budget.admit())
48
+ }
49
+ }
@@ -58,4 +58,19 @@ class NativeLogTest {
58
58
  Bridge.removeEventSink(healthy)
59
59
  }
60
60
  }
61
+
62
+ @Test
63
+ fun audioPayloadEventsAreNotTracedIntoTheLogStream() {
64
+ val events = mutableListOf<String>()
65
+ val sink = Bridge.addEventSink { type, _ -> events.add(type) }
66
+ try {
67
+ Bridge.sendTypedMessage("mic_pcm", mapOf("pcm" to ByteArray(320)))
68
+ Bridge.sendTypedMessage("mic_lc3", mapOf("lc3" to ByteArray(60)))
69
+ // Tracing these would emit a second bridge event per audio frame, and each one
70
+ // pins a JNI global reference until JavaScript drains it.
71
+ assertEquals(listOf("mic_pcm", "mic_lc3"), events)
72
+ } finally {
73
+ Bridge.removeEventSink(sink)
74
+ }
75
+ }
61
76
  }
@@ -1,10 +1,10 @@
1
1
  export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
2
2
  "schemaVersion": 1,
3
3
  "familyBaseVersion": "3.2.0",
4
- "releaseIdentity": "3.2.0-dev.216",
5
- "releaseSetId": "mentra-3.2.0-dev.216",
6
- "sourceCommit": "4dc8e8b75c7a782abb2a9dead146d023d2fe45d6",
7
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.216.json",
8
- "otaManifestSha256": "e0b87036ab2c104b97dcf1a6910d3b0da114c59047ee29dbecf747647cbdcd9a"
4
+ "releaseIdentity": "3.2.0-dev.221",
5
+ "releaseSetId": "mentra-3.2.0-dev.221",
6
+ "sourceCommit": "3837a3055e7a69e18553ce51c3a97579d2ad3b83",
7
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.221.json",
8
+ "otaManifestSha256": "657e9616ad0c464ee57d827c05b53393e45559992c13b15adbb06e0c903eb870"
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,wHAAwH;IAC1I,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.0\",\n \"releaseIdentity\": \"3.2.0-dev.216\",\n \"releaseSetId\": \"mentra-3.2.0-dev.216\",\n \"sourceCommit\": \"4dc8e8b75c7a782abb2a9dead146d023d2fe45d6\",\n \"otaManifestUrl\": \"https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.216.json\",\n \"otaManifestSha256\": \"e0b87036ab2c104b97dcf1a6910d3b0da114c59047ee29dbecf747647cbdcd9a\"\n})\n"]}
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,wHAAwH;IAC1I,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.0\",\n \"releaseIdentity\": \"3.2.0-dev.221\",\n \"releaseSetId\": \"mentra-3.2.0-dev.221\",\n \"sourceCommit\": \"3837a3055e7a69e18553ce51c3a97579d2ad3b83\",\n \"otaManifestUrl\": \"https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.221.json\",\n \"otaManifestSha256\": \"657e9616ad0c464ee57d827c05b53393e45559992c13b15adbb06e0c903eb870\"\n})\n"]}
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
11
11
  static let voiceActivityDetectionEnabled = false
12
12
  static let loudnessGateEnabled = false
13
13
  private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
14
- private static let swiftPackageSdkVersion = "3.2.0-dev.216"
14
+ private static let swiftPackageSdkVersion = "3.2.0-dev.221"
15
15
  private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
16
16
 
17
17
  private static func normalizedSdkVersion(_ value: String?) -> String? {
@@ -15,17 +15,6 @@ class Bridge {
15
15
  private static let micChannels = 1
16
16
  private static let lc3FrameDurationMs = 10
17
17
  private static let defaultLc3FrameSizeBytes = 60
18
- private static let audioTraceMetadataKeys = [
19
- "sampleRate",
20
- "bitsPerSample",
21
- "channels",
22
- "encoding",
23
- "frameDurationMs",
24
- "frameSizeBytes",
25
- "bitrate",
26
- "packetizedFromGlasses",
27
- "voiceActivityDetectionEnabled",
28
- ]
29
18
  private static let eventSinkLock = NSLock()
30
19
  private static let defaultEventSinkId = "default"
31
20
  private static var eventSinks: [String: (String, [String: Any]) -> Void] = [:]
@@ -700,47 +689,22 @@ class Bridge {
700
689
  dispatchEvent(type, body)
701
690
  }
702
691
 
692
+ /// Returns nil for events that must not be traced.
693
+ ///
694
+ /// "log" is excluded so tracing never recurses back through the log event. Audio payload
695
+ /// events are excluded because they arrive at frame rate, which made every microphone
696
+ /// frame emit a second bridge event. On Android that overflowed the JNI global reference
697
+ /// table and aborted the process; here it is wasted work on the audio path. Audio faults
698
+ /// (sequence gaps, decode failures) are still reported through "mic_health", and healthy
699
+ /// frames need no trace.
703
700
  private static func tracePayloadForTypedMessage(_ type: String, body: [String: Any]) -> [String: Any]? {
704
- if type == "log" {
701
+ if type == "log" || isAudioPayloadEvent(type) {
705
702
  return nil
706
703
  }
707
- if isAudioPayloadEvent(type) {
708
- return audioTracePayload(type, body: body)
709
- }
710
704
  return body
711
705
  }
712
706
 
713
707
  private static func isAudioPayloadEvent(_ type: String) -> Bool {
714
708
  type == "mic_pcm" || type == "mic_lc3"
715
709
  }
716
-
717
- private static func audioTracePayload(_ type: String, body: [String: Any]) -> [String: Any] {
718
- var payload: [String: Any] = [
719
- "type": type,
720
- "timestamp": Int(Date().timeIntervalSince1970 * 1000),
721
- "payloadOmitted": true,
722
- "payloadOmittedReason": "audio",
723
- ]
724
-
725
- switch type {
726
- case "mic_pcm":
727
- if let data = body["pcm"] as? Data {
728
- payload["audioBytes"] = data.count
729
- }
730
- case "mic_lc3":
731
- if let data = body["lc3"] as? Data {
732
- payload["audioBytes"] = data.count
733
- }
734
- default:
735
- break
736
- }
737
-
738
- for key in audioTraceMetadataKeys {
739
- if let value = body[key] {
740
- payload[key] = value
741
- }
742
- }
743
-
744
- return payload
745
- }
746
710
  }
@@ -3,9 +3,9 @@ import Foundation
3
3
  /// Generated by release CI. Do not edit in a release checkout.
4
4
  enum GeneratedReleaseMetadata {
5
5
  static let familyBaseVersion = "3.2.0"
6
- static let releaseIdentity = "3.2.0-dev.216"
7
- static let releaseSetId = "mentra-3.2.0-dev.216"
8
- static let sourceCommit = "4dc8e8b75c7a782abb2a9dead146d023d2fe45d6"
9
- static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.216.json"
10
- static let otaManifestSha256 = "e0b87036ab2c104b97dcf1a6910d3b0da114c59047ee29dbecf747647cbdcd9a"
6
+ static let releaseIdentity = "3.2.0-dev.221"
7
+ static let releaseSetId = "mentra-3.2.0-dev.221"
8
+ static let sourceCommit = "3837a3055e7a69e18553ce51c3a97579d2ad3b83"
9
+ static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.221.json"
10
+ static let otaManifestSha256 = "657e9616ad0c464ee57d827c05b53393e45559992c13b15adbb06e0c903eb870"
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.2.0-dev.216",
3
+ "version": "3.2.0-dev.221",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.0",
15
- "releaseIdentity": "3.2.0-dev.216",
16
- "releaseSetId": "mentra-3.2.0-dev.216",
17
- "sourceCommit": "4dc8e8b75c7a782abb2a9dead146d023d2fe45d6",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.216.json",
19
- "otaManifestSha256": "e0b87036ab2c104b97dcf1a6910d3b0da114c59047ee29dbecf747647cbdcd9a"
15
+ "releaseIdentity": "3.2.0-dev.221",
16
+ "releaseSetId": "mentra-3.2.0-dev.221",
17
+ "sourceCommit": "3837a3055e7a69e18553ce51c3a97579d2ad3b83",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.221.json",
19
+ "otaManifestSha256": "657e9616ad0c464ee57d827c05b53393e45559992c13b15adbb06e0c903eb870"
20
20
  })