@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942

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.
Files changed (191) hide show
  1. package/README.md +72 -10
  2. package/android/build.gradle +15 -0
  3. package/android/lc3Lib/build.gradle +6 -1
  4. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  5. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
  15. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  16. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  18. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
  19. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  31. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
  32. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
  33. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  34. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  35. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  37. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  42. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  43. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  45. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  48. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  50. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  51. package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
  52. package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
  53. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  54. package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
  55. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  56. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
  57. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  58. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
  59. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
  62. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  63. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
  65. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  66. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
  67. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  68. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  69. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
  70. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  71. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  72. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  73. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  74. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  75. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  76. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  77. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  78. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  79. package/build/BluetoothSdk.types.d.ts +16 -0
  80. package/build/BluetoothSdk.types.d.ts.map +1 -1
  81. package/build/BluetoothSdk.types.js +1 -0
  82. package/build/BluetoothSdk.types.js.map +1 -1
  83. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  84. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  85. package/build/_private/BluetoothSdkModule.js +5 -1
  86. package/build/_private/BluetoothSdkModule.js.map +1 -1
  87. package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
  88. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  89. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  90. package/build/_private/nativeLogConsole.d.ts +19 -0
  91. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  92. package/build/_private/nativeLogConsole.js +12 -0
  93. package/build/_private/nativeLogConsole.js.map +1 -1
  94. package/build/generated/changelogCatalog.d.ts +5 -2
  95. package/build/generated/changelogCatalog.d.ts.map +1 -1
  96. package/build/generated/changelogCatalog.js +6 -2
  97. package/build/generated/changelogCatalog.js.map +1 -1
  98. package/build/generated/releaseMetadata.js +6 -6
  99. package/build/generated/releaseMetadata.js.map +1 -1
  100. package/build/index.d.ts.map +1 -1
  101. package/build/index.js +1 -0
  102. package/build/index.js.map +1 -1
  103. package/build/ota-transport/index.d.ts +4 -0
  104. package/build/ota-transport/index.d.ts.map +1 -1
  105. package/build/ota-transport/index.js +2 -0
  106. package/build/ota-transport/index.js.map +1 -1
  107. package/ios/BluetoothSdkModule.swift +17 -4
  108. package/ios/MentraBluetoothSDK.podspec +3 -2
  109. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  110. package/ios/Source/BluetoothSdkDefaults.swift +7 -2
  111. package/ios/Source/Bridge.swift +15 -2
  112. package/ios/Source/DeviceManager.swift +102 -66
  113. package/ios/Source/DeviceStore.swift +26 -7
  114. package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
  115. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  116. package/ios/Source/MentraBluetoothSDK.swift +313 -54
  117. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  118. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  119. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  120. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  121. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  122. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  123. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  124. package/ios/Source/requests/DisplayRequests.swift +1 -1
  125. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  126. package/ios/Source/sgcs/Ar99.swift +1 -2
  127. package/ios/Source/sgcs/G1.swift +68 -38
  128. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  129. package/ios/Source/sgcs/G2.swift +261 -247
  130. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  131. package/ios/Source/sgcs/Mach1.swift +19 -7
  132. package/ios/Source/sgcs/MentraLive.swift +272 -197
  133. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  134. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  135. package/ios/Source/sgcs/MentraNex.swift +8 -31
  136. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  137. package/ios/Source/sgcs/Nimo.swift +280 -408
  138. package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
  139. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  140. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  141. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  142. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  143. package/ios/Source/sgcs/SGCManager.swift +25 -1
  144. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  145. package/ios/Source/status/DeviceStatus.swift +1 -0
  146. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  147. package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
  148. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  149. package/ios/Source/utils/G1Text.swift +2 -24
  150. package/ios/Source/utils/G1TextSanitizer.swift +27 -0
  151. package/ios/Source/utils/G2Text.swift +11 -0
  152. package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
  153. package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
  154. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  155. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  156. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  157. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  158. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  159. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  160. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  161. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  162. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  163. package/ios/Tests/G1PairingTests.swift +162 -0
  164. package/ios/Tests/G1TextSanitizerTests.swift +32 -0
  165. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  166. package/ios/Tests/G2TextTests.swift +10 -0
  167. package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
  168. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  169. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  170. package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
  171. package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
  172. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  173. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  174. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  175. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  176. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  177. package/ios/Tests/VersionInfoCommandTests.swift +194 -0
  178. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  179. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  180. package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
  181. package/package.json +2 -2
  182. package/plugin/build/withIos.d.ts +2 -0
  183. package/plugin/build/withIos.js +12 -1
  184. package/src/BluetoothSdk.types.ts +15 -0
  185. package/src/_private/BluetoothSdkModule.ts +10 -1
  186. package/src/_private/MentraLocalNetworkModule.ts +28 -0
  187. package/src/_private/nativeLogConsole.ts +19 -0
  188. package/src/generated/changelogCatalog.ts +6 -2
  189. package/src/generated/releaseMetadata.ts +6 -6
  190. package/src/index.ts +1 -0
  191. package/src/ota-transport/index.ts +10 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ota-transport/index.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,wBAAwB,GAAG;IAAC,MAAM,IAAI,IAAI,CAAA;CAAC,CAAA;AAEvD,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AAErD,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe;uBACT,OAAO;oBACF,MAAM,YAAY,MAAM,KAAG,OAAO,CAAC,yBAAyB,CAAC;yBAKtE,MAAM,OACZ,MAAM,UACH,MAAM,WACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QACzB,MAAM,GAAG,IAAI,aACR,MAAM,KAChB,OAAO,CAAC,uBAAuB,CAAC;0BAKtB,MAAM,OACZ,MAAM,eACE,MAAM,WACV,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,uBACV,MAAM,iBACZ,MAAM,KACpB,OAAO,CAAC,6BAA6B,CAAC;wBAIf,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;sBAI1B,OAAO,CAAC,IAAI,CAAC;mCAIJ,CAAC,KAAK,EAAE,+BAA+B,KAAK,IAAI,KAAG,wBAAwB;8BAIhF,CAAC,KAAK,EAAE,wBAAwB,KAAK,IAAI,KAAG,wBAAwB;EAI9F,CAAA;AAEF,qFAAqF;AACrF,eAAO,MAAM,SAAS;0BAEJ,MAAM,iBACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAC9B,MAAM,GAAG,IAAI,KACnB,OAAO,CAAC,eAAe,CAAC;gBACjB,OAAO,CAAC,IAAI,CAAC;kCACO,MAAM,aAAa,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;+BAE9C,MAAM,eAAe,MAAM,KAAG,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,CAAC;2CAErE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,KAAG,wBAAwB;EAE9G,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ota-transport/index.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ import MentraOtaServer from "../ota-server";
11
11
  */
12
12
  export const otaLocalNetwork = Object.freeze({
13
13
  isAvailable: () => MentraLocalNetwork != null,
14
+ /** Synchronous local observation; no network I/O, route changes or network identities. */
15
+ getNetworkDiagnostics: () => MentraLocalNetwork?.getNetworkDiagnostics() ?? null,
14
16
  connect: async (ssid, password) => {
15
17
  if (!MentraLocalNetwork)
16
18
  throw new Error("Scoped local networking is not available in this native build");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ota-transport/index.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,iDAAiD;AACjD,OAAO,kBAAkB,MAAM,sCAAsC,CAAA;AACrE,iDAAiD;AACjD,OAAO,eAAe,MAAM,eAAe,CAAA;AA6C3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,WAAW,EAAE,GAAY,EAAE,CAAC,kBAAkB,IAAI,IAAI;IACtD,OAAO,EAAE,KAAK,EAAE,IAAY,EAAE,QAAgB,EAAsC,EAAE;QACpF,IAAI,CAAC,kBAAkB;YAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAA;QACzG,OAAO,kBAAkB,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACnD,CAAC;IACD,OAAO,EAAE,KAAK,EACZ,SAAiB,EACjB,GAAW,EACX,MAAc,EACd,OAA+B,EAC/B,IAAmB,EACnB,SAAiB,EACiB,EAAE;QACpC,IAAI,CAAC,kBAAkB;YAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAA;QACzG,OAAO,kBAAkB,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAA;IACrF,CAAC;IACD,QAAQ,EAAE,KAAK,EACb,SAAiB,EACjB,GAAW,EACX,WAAmB,EACnB,OAA+B,EAC/B,mBAA2B,EAC3B,aAAqB,EACmB,EAAE;QAC1C,IAAI,CAAC,kBAAkB;YAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAA;QACzG,OAAO,kBAAkB,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB,EAAE,aAAa,CAAC,CAAA;IAC9G,CAAC;IACD,MAAM,EAAE,KAAK,EAAE,SAAiB,EAAiB,EAAE;QACjD,IAAI,CAAC,kBAAkB;YAAE,OAAM;QAC/B,MAAM,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAC5C,CAAC;IACD,UAAU,EAAE,KAAK,IAAmB,EAAE;QACpC,IAAI,CAAC,kBAAkB;YAAE,OAAM;QAC/B,MAAM,kBAAkB,CAAC,UAAU,EAAE,CAAA;IACvC,CAAC;IACD,kBAAkB,EAAE,CAAC,QAA0D,EAA4B,EAAE;QAC3G,IAAI,CAAC,kBAAkB;YAAE,OAAO,EAAC,MAAM,KAAI,CAAC,EAAC,CAAA;QAC7C,OAAO,kBAAkB,CAAC,WAAW,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;IACrE,CAAC;IACD,aAAa,EAAE,CAAC,QAAmD,EAA4B,EAAE;QAC/F,IAAI,CAAC,kBAAkB;YAAE,OAAO,EAAC,MAAM,KAAI,CAAC,EAAC,CAAA;QAC7C,OAAO,kBAAkB,CAAC,WAAW,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAA;IAChE,CAAC;CACF,CAAC,CAAA;AAEF,qFAAqF;AACrF,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,CACL,YAAoB,EACpB,aAAqC,EACrC,IAAoB,EACM,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC;IAChG,IAAI,EAAE,GAAkB,EAAE,CAAC,eAAe,CAAC,aAAa,EAAE;IAC1D,kBAAkB,EAAE,CAAC,OAAe,EAAE,SAAiB,EAAmB,EAAE,CAC1E,eAAe,CAAC,kBAAkB,CAAC,OAAO,EAAE,SAAS,CAAC;IACxD,gBAAgB,EAAE,CAAC,MAAc,EAAE,WAAmB,EAAuD,EAAE,CAC7G,eAAe,CAAC,gBAAgB,CAAC,MAAM,EAAE,WAAW,CAAC;IACvD,0BAA0B,EAAE,CAAC,QAAsD,EAA4B,EAAE,CAC/G,eAAe,CAAC,WAAW,CAAC,0BAA0B,EAAE,QAAQ,CAAC;CACpE,CAAC,CAAA","sourcesContent":["// This public adapter is the package-owned boundary over its native implementation.\n// eslint-disable-next-line no-restricted-imports\nimport MentraLocalNetwork from \"../_private/MentraLocalNetworkModule\"\n// eslint-disable-next-line no-restricted-imports\nimport MentraOtaServer from \"../ota-server\"\n\nexport type OtaTransportSubscription = {remove(): void}\n\nexport type OtaLocalNetworkConnection = {\n connected: boolean\n ssid: string\n localAddress?: string\n}\n\nexport type OtaLocalNetworkLostEvent = {ssid: string}\n\nexport type OtaLocalNetworkResponse = {\n status: number\n headers: Record<string, string>\n bodyBase64: string\n}\n\nexport type OtaLocalNetworkDownloadResult = {\n statusCode: number\n bytesWritten: number\n headers: Record<string, string>\n}\n\nexport type OtaLocalNetworkDownloadProgress = {\n requestId: string\n bytesWritten: number\n contentLength: number\n statusCode?: number\n headers?: Record<string, string>\n}\n\nexport type OtaArtifactDownloadProgress = {\n destination: string\n bytesWritten: number\n contentLength: number\n}\n\nexport type OtaServerResult = {\n baseUrl: string\n host: string\n manifestUrl: string\n port: number\n}\n\n/**\n * Scoped networking for traffic that must stay on a Mentra Live hotspot while\n * the phone's default route remains on cellular or another Wi-Fi network.\n *\n * The module is optional because iOS uses its system Wi-Fi join flow instead.\n */\nexport const otaLocalNetwork = Object.freeze({\n isAvailable: (): boolean => MentraLocalNetwork != null,\n connect: async (ssid: string, password: string): Promise<OtaLocalNetworkConnection> => {\n if (!MentraLocalNetwork) throw new Error(\"Scoped local networking is not available in this native build\")\n return MentraLocalNetwork.connect(ssid, password)\n },\n request: async (\n requestId: string,\n url: string,\n method: string,\n headers: Record<string, string>,\n body: string | null,\n timeoutMs: number,\n ): Promise<OtaLocalNetworkResponse> => {\n if (!MentraLocalNetwork) throw new Error(\"Scoped local networking is not available in this native build\")\n return MentraLocalNetwork.request(requestId, url, method, headers, body, timeoutMs)\n },\n download: async (\n requestId: string,\n url: string,\n destination: string,\n headers: Record<string, string>,\n connectionTimeoutMs: number,\n readTimeoutMs: number,\n ): Promise<OtaLocalNetworkDownloadResult> => {\n if (!MentraLocalNetwork) throw new Error(\"Scoped local networking is not available in this native build\")\n return MentraLocalNetwork.download(requestId, url, destination, headers, connectionTimeoutMs, readTimeoutMs)\n },\n cancel: async (requestId: string): Promise<void> => {\n if (!MentraLocalNetwork) return\n await MentraLocalNetwork.cancel(requestId)\n },\n disconnect: async (): Promise<void> => {\n if (!MentraLocalNetwork) return\n await MentraLocalNetwork.disconnect()\n },\n onDownloadProgress: (listener: (event: OtaLocalNetworkDownloadProgress) => void): OtaTransportSubscription => {\n if (!MentraLocalNetwork) return {remove() {}}\n return MentraLocalNetwork.addListener(\"downloadProgress\", listener)\n },\n onNetworkLost: (listener: (event: OtaLocalNetworkLostEvent) => void): OtaTransportSubscription => {\n if (!MentraLocalNetwork) return {remove() {}}\n return MentraLocalNetwork.addListener(\"networkLost\", listener)\n },\n})\n\n/** Native HTTP server and artifact downloader used by the Engine OTA coordinator. */\nexport const otaServer = Object.freeze({\n start: (\n manifestJson: string,\n artifactPaths: Record<string, string>,\n host?: string | null,\n ): Promise<OtaServerResult> => MentraOtaServer.startOtaServer(manifestJson, artifactPaths, host),\n stop: (): Promise<void> => MentraOtaServer.stopOtaServer(),\n waitForWifiAddress: (gateway: string, timeoutMs: number): Promise<string> =>\n MentraOtaServer.waitForWifiAddress(gateway, timeoutMs),\n downloadArtifact: (source: string, destination: string): Promise<{statusCode: number; bytesWritten: number}> =>\n MentraOtaServer.downloadArtifact(source, destination),\n onArtifactDownloadProgress: (listener: (event: OtaArtifactDownloadProgress) => void): OtaTransportSubscription =>\n MentraOtaServer.addListener(\"artifactDownloadProgress\", listener),\n})\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ota-transport/index.ts"],"names":[],"mappings":"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","sourcesContent":["// This public adapter is the package-owned boundary over its native implementation.\n// eslint-disable-next-line no-restricted-imports\nimport MentraLocalNetwork, {type NetworkDiagnostics} from \"../_private/MentraLocalNetworkModule\"\n// eslint-disable-next-line no-restricted-imports\nimport MentraOtaServer from \"../ota-server\"\n\n// eslint-disable-next-line no-restricted-imports\nexport type {\n NetworkCapabilitiesDiagnostic,\n NetworkDiagnostics,\n NetworkObservation,\n} from \"../_private/MentraLocalNetworkModule\"\n\nexport type OtaTransportSubscription = {remove(): void}\n\nexport type OtaLocalNetworkConnection = {\n connected: boolean\n ssid: string\n localAddress?: string\n}\n\nexport type OtaLocalNetworkLostEvent = {ssid: string}\n\nexport type OtaLocalNetworkResponse = {\n status: number\n headers: Record<string, string>\n bodyBase64: string\n}\n\nexport type OtaLocalNetworkDownloadResult = {\n statusCode: number\n bytesWritten: number\n headers: Record<string, string>\n}\n\nexport type OtaLocalNetworkDownloadProgress = {\n requestId: string\n bytesWritten: number\n contentLength: number\n statusCode?: number\n headers?: Record<string, string>\n}\n\nexport type OtaArtifactDownloadProgress = {\n destination: string\n bytesWritten: number\n contentLength: number\n}\n\nexport type OtaServerResult = {\n baseUrl: string\n host: string\n manifestUrl: string\n port: number\n}\n\n/**\n * Scoped networking for traffic that must stay on a Mentra Live hotspot while\n * the phone's default route remains on cellular or another Wi-Fi network.\n *\n * The module is optional because iOS uses its system Wi-Fi join flow instead.\n */\nexport const otaLocalNetwork = Object.freeze({\n isAvailable: (): boolean => MentraLocalNetwork != null,\n /** Synchronous local observation; no network I/O, route changes or network identities. */\n getNetworkDiagnostics: (): NetworkDiagnostics | null => MentraLocalNetwork?.getNetworkDiagnostics() ?? null,\n connect: async (ssid: string, password: string): Promise<OtaLocalNetworkConnection> => {\n if (!MentraLocalNetwork) throw new Error(\"Scoped local networking is not available in this native build\")\n return MentraLocalNetwork.connect(ssid, password)\n },\n request: async (\n requestId: string,\n url: string,\n method: string,\n headers: Record<string, string>,\n body: string | null,\n timeoutMs: number,\n ): Promise<OtaLocalNetworkResponse> => {\n if (!MentraLocalNetwork) throw new Error(\"Scoped local networking is not available in this native build\")\n return MentraLocalNetwork.request(requestId, url, method, headers, body, timeoutMs)\n },\n download: async (\n requestId: string,\n url: string,\n destination: string,\n headers: Record<string, string>,\n connectionTimeoutMs: number,\n readTimeoutMs: number,\n ): Promise<OtaLocalNetworkDownloadResult> => {\n if (!MentraLocalNetwork) throw new Error(\"Scoped local networking is not available in this native build\")\n return MentraLocalNetwork.download(requestId, url, destination, headers, connectionTimeoutMs, readTimeoutMs)\n },\n cancel: async (requestId: string): Promise<void> => {\n if (!MentraLocalNetwork) return\n await MentraLocalNetwork.cancel(requestId)\n },\n disconnect: async (): Promise<void> => {\n if (!MentraLocalNetwork) return\n await MentraLocalNetwork.disconnect()\n },\n onDownloadProgress: (listener: (event: OtaLocalNetworkDownloadProgress) => void): OtaTransportSubscription => {\n if (!MentraLocalNetwork) return {remove() {}}\n return MentraLocalNetwork.addListener(\"downloadProgress\", listener)\n },\n onNetworkLost: (listener: (event: OtaLocalNetworkLostEvent) => void): OtaTransportSubscription => {\n if (!MentraLocalNetwork) return {remove() {}}\n return MentraLocalNetwork.addListener(\"networkLost\", listener)\n },\n})\n\n/** Native HTTP server and artifact downloader used by the Engine OTA coordinator. */\nexport const otaServer = Object.freeze({\n start: (\n manifestJson: string,\n artifactPaths: Record<string, string>,\n host?: string | null,\n ): Promise<OtaServerResult> => MentraOtaServer.startOtaServer(manifestJson, artifactPaths, host),\n stop: (): Promise<void> => MentraOtaServer.stopOtaServer(),\n waitForWifiAddress: (gateway: string, timeoutMs: number): Promise<string> =>\n MentraOtaServer.waitForWifiAddress(gateway, timeoutMs),\n downloadArtifact: (source: string, destination: string): Promise<{statusCode: number; bytesWritten: number}> =>\n MentraOtaServer.downloadArtifact(source, destination),\n onArtifactDownloadProgress: (listener: (event: OtaArtifactDownloadProgress) => void): OtaTransportSubscription =>\n MentraOtaServer.addListener(\"artifactDownloadProgress\", listener),\n})\n"]}
@@ -12,6 +12,10 @@ private func codedBridgeError(_ error: BluetoothSdkError) -> Exception {
12
12
  Exception(name: error.code, description: error.message, code: error.code)
13
13
  }
14
14
 
15
+ /// Offline model archives take tens of seconds to extract. Android runs this on Dispatchers.IO;
16
+ /// on Expo's shared default queue it would hold every other module's AsyncFunction behind it.
17
+ private let modelExtractionQueue = DispatchQueue(label: "com.mentra.model-extraction", qos: .utility)
18
+
15
19
  public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
16
20
  private var sdk: MentraBluetoothSDK?
17
21
 
@@ -23,6 +27,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
23
27
  "glasses_status",
24
28
  "bluetooth_status",
25
29
  "log",
30
+ "sdk_update_notice",
26
31
  "device_discovered",
27
32
  "default_device_changed",
28
33
  // Individual event handlers
@@ -90,7 +95,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
90
95
  "send_command_to_ble",
91
96
  "receive_command_from_ble",
92
97
  "miniapp_selected",
93
- "captions_tester_incident",
94
98
  "extraction_progress"
95
99
  )
96
100
 
@@ -226,6 +230,10 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
226
230
  }
227
231
  }
228
232
 
233
+ AsyncFunction("unpair") {
234
+ try await DeviceManager.shared.unpair()
235
+ }
236
+
229
237
  AsyncFunction("forgetController") {
230
238
  await MainActor.run {
231
239
  DeviceManager.shared.forgetController()
@@ -361,8 +369,8 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
361
369
  // MARK: - Incident Reporting
362
370
 
363
371
  AsyncFunction("sendIncidentId") { (incidentId: String, apiBaseUrl: String?) in
364
- await MainActor.run {
365
- self.bluetoothSdk().sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
372
+ try await MainActor.run {
373
+ try self.bluetoothSdk().sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
366
374
  }
367
375
  }
368
376
 
@@ -465,6 +473,11 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
465
473
  try await sdk.setLoudnessGateEnabled(enabled)
466
474
  }
467
475
 
476
+ AsyncFunction("setAutoPowerOffEnabled") { (enabled: Bool) in
477
+ let sdk = await MainActor.run { self.bluetoothSdk() }
478
+ try await sdk.setAutoPowerOffEnabled(enabled)
479
+ }
480
+
468
481
  AsyncFunction("setPhotoCaptureDefaults") { (params: [String: Any]) in
469
482
  let sdk = await MainActor.run { self.bluetoothSdk() }
470
483
  return try await sdk.setPhotoCaptureDefaults(PhotoCaptureDefaults.from(params: params)).values
@@ -836,7 +849,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
836
849
 
837
850
  AsyncFunction("extractTarBz2") { (sourcePath: String, destinationPath: String) -> Bool in
838
851
  return STTTools.extractTarBz2(sourcePath: sourcePath, destinationPath: destinationPath)
839
- }
852
+ }.runOnQueue(modelExtractionQueue)
840
853
 
841
854
  // MARK: - TTS Model Management
842
855
 
@@ -38,13 +38,14 @@ Pod::Spec.new do |s|
38
38
  }
39
39
 
40
40
  # iOS frameworks required by Bluetooth SDK
41
- ios_frameworks = ['AVFoundation', 'CoreBluetooth', 'UIKit', 'CoreGraphics']
41
+ ios_frameworks = ['AVFoundation', 'CoreBluetooth', 'UIKit', 'CoreGraphics', 'ImageIO']
42
42
  ios_frameworks << 'Network' if include_expo_adapter
43
43
  s.ios.frameworks = ios_frameworks
44
44
  s.osx.frameworks = ['AVFoundation', 'CoreBluetooth', 'CoreAudio', 'AudioToolbox', 'ImageIO', 'JavaScriptCore', 'Network']
45
45
 
46
46
  # System libraries required by MentraOS
47
- s.ios.library = 'bz2'
47
+ s.ios.libraries = 'bz2', 'z'
48
+ s.osx.library = 'z'
48
49
 
49
50
  # Vendored frameworks
50
51
  s.ios.vendored_frameworks = 'Packages/SherpaOnnx/sherpa-onnx.xcframework'
@@ -0,0 +1,32 @@
1
+ import Foundation
2
+
3
+ /// Whether the host app lets iOS keep the glasses link working while the app is suspended.
4
+ ///
5
+ /// The SDK keeps a pending reconnection open after the glasses drop the link (for example while
6
+ /// their Bluetooth chip restarts during a software update). iOS completes it, and delivers BLE
7
+ /// events, only to apps that declare the `bluetooth-central` background mode. A Swift package
8
+ /// cannot declare it for the app, so the SDK checks it and warns instead.
9
+ enum BluetoothBackgroundMode {
10
+ static let requiredMode = "bluetooth-central"
11
+
12
+ /// Platform-independent plist check, so tests exercise it on every host.
13
+ static func declaresBluetoothCentral(_ infoDictionary: [String: Any]?) -> Bool {
14
+ guard let modes = infoDictionary?["UIBackgroundModes"] as? [String] else { return false }
15
+ return modes.contains(requiredMode)
16
+ }
17
+
18
+ static func isDeclared(in infoDictionary: [String: Any]?) -> Bool {
19
+ #if os(iOS)
20
+ return declaresBluetoothCentral(infoDictionary)
21
+ #else
22
+ // macOS apps are not suspended the same way and have no background modes.
23
+ return true
24
+ #endif
25
+ }
26
+
27
+ static let missingWarning =
28
+ "MentraBluetoothSDK: WARNING Info.plist is missing UIBackgroundModes `bluetooth-central`. "
29
+ + "While the phone is locked or the app is in the background, iOS will not reconnect to "
30
+ + "the glasses after a link drop (for example during a software update) until the app "
31
+ + "is opened. See https://docs.mentraglass.com/bluetooth-sdk/ios#background-operation"
32
+ }
@@ -9,9 +9,14 @@ enum BluetoothSdkDefaults {
9
9
  }
10
10
 
11
11
  static let voiceActivityDetectionEnabled = false
12
- static let loudnessGateEnabled = false
12
+ static let loudnessGateEnabled = true
13
+
14
+ /// Must stay true. The firmware defaults auto power-off on, and we re-push
15
+ /// every Bluetooth setting on connect, so a false default here would
16
+ /// silently disable it on every pair the app touches.
17
+ static let autoPowerOffEnabled = false
13
18
  private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
14
- private static let swiftPackageSdkVersion = "3.2.1-dev.314"
19
+ private static let swiftPackageSdkVersion = "3.3.0-beta.942"
15
20
  private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
16
21
 
17
22
  private static func normalizedSdkVersion(_ value: String?) -> String? {
@@ -335,8 +335,21 @@ class Bridge {
335
335
  }
336
336
 
337
337
  /// Wear state from sr_wrst, or from a wear transition on the glasses.
338
- static func sendWearState(worn: Bool) {
339
- Bridge.sendTypedMessage("wear_state", body: ["worn": worn])
338
+ static func sendWearState(
339
+ worn: Bool,
340
+ elapsedMs: Int? = nil,
341
+ timeoutMs: Int? = nil,
342
+ enabled: Bool? = nil,
343
+ armed: Bool? = nil,
344
+ inhibited: Bool? = nil
345
+ ) {
346
+ var body: [String: Any] = ["worn": worn]
347
+ if let elapsedMs { body["elapsedMs"] = elapsedMs }
348
+ if let timeoutMs { body["timeoutMs"] = timeoutMs }
349
+ if let enabled { body["enabled"] = enabled }
350
+ if let armed { body["armed"] = armed }
351
+ if let inhibited { body["inhibited"] = inhibited }
352
+ Bridge.sendTypedMessage("wear_state", body: body)
340
353
  }
341
354
 
342
355
  /// Wear vote config the glasses report as in force, from sr_weartun.
@@ -318,9 +318,9 @@ struct ViewState {
318
318
  var lc3Converter: PcmConverter?
319
319
  /// Audio output format - defaults to LC3 for bandwidth savings
320
320
  private var audioOutputFormat: AudioOutputFormat = .lc3
321
- /// Last time we received an LC3 frame from the glasses (used by the mic
322
- /// inactivity watchdog).
323
- private var lastLc3Event: Date?
321
+ private var micWatchdog = GlassesMicWatchdog()
322
+ // Injectable monotonic clock so recovery deadlines can be tested without sleeping.
323
+ var micWatchdogNow: () -> TimeInterval = { ProcessInfo.processInfo.systemUptime }
324
324
  private var sequenceGapEvents: Int64 = 0
325
325
  private var decodeFailures: Int64 = 0
326
326
  private var lastLc3ReceivedAt: Int64?
@@ -330,7 +330,7 @@ struct ViewState {
330
330
 
331
331
  /// STT:
332
332
  #if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
333
- private var transcriber: SherpaOnnxTranscriber?
333
+ private let transcriber = SherpaOnnxTranscriber()
334
334
  #endif
335
335
 
336
336
  var viewStates: [ViewState] = [
@@ -383,24 +383,6 @@ struct ViewState {
383
383
  // Start memory monitoring (logs every 30s to help detect leaks)
384
384
  // MemoryMonitor.start()
385
385
 
386
- // Initialize SherpaOnnx Transcriber
387
- #if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
388
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
389
- let window = windowScene.windows.first,
390
- let rootViewController = window.rootViewController
391
- {
392
- transcriber = SherpaOnnxTranscriber(context: rootViewController)
393
- } else {
394
- Bridge.log("Failed to create SherpaOnnxTranscriber - no root view controller found")
395
- }
396
-
397
- // Initialize the transcriber
398
- if let transcriber = transcriber {
399
- transcriber.initialize()
400
- Bridge.log("SherpaOnnxTranscriber fully initialized")
401
- }
402
- #endif
403
-
404
386
  // Initialize persistent LC3 converter for unified audio encoding
405
387
  lc3Converter = PcmConverter()
406
388
  Bridge.log("LC3 converter initialized for unified audio encoding")
@@ -472,10 +454,17 @@ struct ViewState {
472
454
  }
473
455
 
474
456
  func reportGlassesAudioActivity() {
475
- lastLc3Event = Date()
457
+ micWatchdog.receivedAudio(at: micWatchdogNow())
476
458
  lastPcmProducedAt = nowMs()
477
459
  }
478
460
 
461
+ /// Decoded glasses audio must update recovery health; phone PCM must not.
462
+ func handleGlassesPcm(_ pcmData: Data) {
463
+ guard !pcmData.isEmpty else { return }
464
+ reportGlassesAudioActivity()
465
+ handlePcm(pcmData)
466
+ }
467
+
479
468
  private func micHealth() -> MicHealth {
480
469
  MicHealth(
481
470
  sequenceGapEvents: sequenceGapEvents,
@@ -499,13 +488,13 @@ struct ViewState {
499
488
  lastLc3ReceivedAt = nil
500
489
  lastPcmProducedAt = nil
501
490
  lastLc3Sequence = nil
502
- lastLc3Event = nil
491
+ micWatchdog = GlassesMicWatchdog()
503
492
  }
504
493
 
505
494
  private func recordLc3Packet(sequenceNumber: Int?) {
506
495
  let receivedAt = nowMs()
507
496
  lastLc3ReceivedAt = receivedAt
508
- lastLc3Event = Date()
497
+ micWatchdog.receivedAudio(at: micWatchdogNow())
509
498
  var hasGap = false
510
499
  if let sequenceNumber {
511
500
  let normalized = sequenceNumber & 0xFF
@@ -535,7 +524,7 @@ struct ViewState {
535
524
  // Send PCM to local transcriber.
536
525
  #if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
537
526
  if shouldSendTranscript || localSttFallbackActive {
538
- transcriber?.acceptAudio(pcm16le: pcmData)
527
+ transcriber.acceptAudio(pcm16le: pcmData)
539
528
  }
540
529
  #endif
541
530
  }
@@ -605,6 +594,7 @@ struct ViewState {
605
594
  }
606
595
 
607
596
  currentMic = micUsed
597
+ updateGlassesMicExpectation()
608
598
 
609
599
  // log if no mic was found:
610
600
  if micUsed == "" && micEnabled {
@@ -723,6 +713,7 @@ struct ViewState {
723
713
  Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
724
714
  sgc?.cleanup()
725
715
  sgc = nil
716
+ DeviceStore.shared.apply("glasses", "micEnabled", false)
726
717
  resetSystemTimeSync()
727
718
  }
728
719
 
@@ -902,27 +893,20 @@ struct ViewState {
902
893
  return result
903
894
  }
904
895
 
905
- private func checkAndReinitGlassesMic() {
906
- // if the glasses mic is marked as enabled (and the glasses are connected), but our last known lc3 event is from > 5 seconds ago, reinitialize the mic:
907
- let glassesMicEnabled = DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
908
- let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
909
- if !glassesMicEnabled || !glassesConnected {
910
- return
911
- }
912
-
913
- if sgc?.isMicSuspendedForAudio == true {
914
- Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
915
- return
916
- }
917
-
918
- if PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying() {
919
- Bridge.log("MAN: Mentra audio is playing; skipping glasses mic recovery")
920
- return
921
- }
896
+ private func updateGlassesMicExpectation() {
897
+ // Demand/route and device readiness decide whether audio is expected. The
898
+ // shared micEnabled flag alone is not evidence of a running hardware stream.
899
+ let expected = micEnabled && currentMic == MicTypes.GLASSES_CUSTOM
900
+ && sgc?.fullyBooted == true && sgc?.hasMic == true
901
+ && sgc?.isMicSuspendedForAudio != true
902
+ && !PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying()
903
+ micWatchdog.expectAudio(expected, at: micWatchdogNow())
904
+ }
922
905
 
923
- let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
924
- if timeSinceLastLc3Event > 5 {
925
- Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
906
+ func checkAndReinitGlassesMic() {
907
+ updateGlassesMicExpectation()
908
+ if micWatchdog.shouldRetry(at: micWatchdogNow()) {
909
+ Bridge.log("MAN: Expected glasses audio missing for 5 seconds; retrying mic start")
926
910
  sgc?.setMicEnabled(true)
927
911
  }
928
912
  }
@@ -982,7 +966,7 @@ struct ViewState {
982
966
  func restartTranscriber() {
983
967
  #if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
984
968
  Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
985
- transcriber?.restart()
969
+ transcriber.restart()
986
970
  #else
987
971
  Bridge.log("MAN: Local STT is not included in this SwiftPM build")
988
972
  #endif
@@ -998,12 +982,16 @@ struct ViewState {
998
982
  Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
999
983
  resetMicHealth()
1000
984
 
985
+ // A new identity must never inherit the previous glasses' address when
986
+ // pairing supplied only a name. Same-device reconnects retain their cache.
987
+ let identityChanged = !pendingDeviceName.isEmpty &&
988
+ (pendingDeviceName != deviceName || sgc.type != defaultWearable)
989
+ if identityChanged || !pendingDeviceAddress.isEmpty {
990
+ deviceAddress = pendingDeviceAddress
991
+ }
1001
992
  if !pendingDeviceName.isEmpty {
1002
993
  deviceName = pendingDeviceName
1003
994
  }
1004
- if !pendingDeviceAddress.isEmpty {
1005
- deviceAddress = pendingDeviceAddress
1006
- }
1007
995
  clearPendingConnection()
1008
996
  defaultWearable = sgc.type
1009
997
  searching = false
@@ -1020,18 +1008,22 @@ struct ViewState {
1020
1008
  let h = DeviceStore.shared.get("bluetooth", "dashboard_height") as? Int ?? 4
1021
1009
  // Fall back to the canonical default (2), matching DeviceStore, not 1.
1022
1010
  let rawDepth = DeviceStore.shared.get("bluetooth", "dashboard_depth") as? Int ?? 2
1023
- let d = min(max(rawDepth, 1), 4)
1011
+ let d = sgc.type == DeviceTypes.NIMO ? min(max(rawDepth, 0), 10) : min(max(rawDepth, 1), 4)
1024
1012
  sgc.setDashboardPosition(h, d)
1025
1013
  }
1026
1014
 
1027
- // Show welcome message on first connect for all display glasses
1015
+ // Preserve the connected-edge scene replay on full-frame adapters.
1028
1016
  if shouldSendBootingMessage {
1029
- Task {
1030
- await sgc.sendTextWall("// MentraOS Connected")
1031
- try? await Task.sleep(nanoseconds: 3_000_000_000) // 1 second
1032
- sgc.clearDisplay()
1033
- }
1034
1017
  shouldSendBootingMessage = false
1018
+ if sgc.showConnectionConfirmation {
1019
+ Task {
1020
+ guard (self.sgc as AnyObject?) === (sgc as AnyObject) else { return }
1021
+ await sgc.sendTextWall("// MentraOS Connected")
1022
+ try? await Task.sleep(nanoseconds: 3_000_000_000)
1023
+ guard (self.sgc as AnyObject?) === (sgc as AnyObject) else { return }
1024
+ sgc.clearDisplay()
1025
+ }
1026
+ }
1035
1027
  }
1036
1028
 
1037
1029
  // Call device-specific setup handlers
@@ -1058,6 +1050,11 @@ struct ViewState {
1058
1050
  #endif
1059
1051
  checkCurrentAudioDevice()
1060
1052
 
1053
+ // Disconnect clears micEnabled but preserves the consumers' audio requests.
1054
+ // Recompute demand before selecting a microphone; unchanged requests are
1055
+ // deduplicated by DeviceStore.apply().
1056
+ setMicState()
1057
+
1061
1058
  // save the default_wearable now that we're connected:
1062
1059
  Bridge.saveSetting("default_wearable", defaultWearable)
1063
1060
  Bridge.saveSetting("device_name", deviceName)
@@ -1214,7 +1211,9 @@ struct ViewState {
1214
1211
  self.dashboardSceneCleanupPending = false
1215
1212
  let elementIds = Array(self.pendingDashboardSceneElementIds)
1216
1213
  self.pendingDashboardSceneElementIds.removeAll()
1217
- await self.sgc?.clearSceneElements(elementIds)
1214
+ if self.sgc?.sceneHandoffRequiresClear == true {
1215
+ await self.sgc?.clearSceneElements(elementIds)
1216
+ }
1218
1217
  }
1219
1218
  self.dashboardSceneCleanupTask = nil
1220
1219
  }
@@ -1261,7 +1260,7 @@ struct ViewState {
1261
1260
  if stateIndex == 1, dashboardSceneCleanupDeferred {
1262
1261
  dashboardSceneCleanupPending = true
1263
1262
  pendingDashboardSceneElementIds.formUnion(prevFrame.elements.map(\.id))
1264
- } else if layoutType != "clear_view" {
1263
+ } else if layoutType != "clear_view", shouldClearSceneHandoff(stateIndex) {
1265
1264
  let ids = prevFrame.elements.map(\.id)
1266
1265
  Task { [weak self] in
1267
1266
  await self?.sgc?.clearSceneElements(ids)
@@ -1355,7 +1354,7 @@ struct ViewState {
1355
1354
  // on G2 - no page rebuild).
1356
1355
  let prevLegacyType = viewStates[stateIndex].layoutType
1357
1356
  let cleanupDeferred = stateIndex == 1 && dashboardSceneCleanupDeferred
1358
- if !cleanupDeferred,
1357
+ if !cleanupDeferred, shouldClearSceneHandoff(stateIndex),
1359
1358
  !prevLegacyType.isEmpty,
1360
1359
  prevLegacyType != "clear_view",
1361
1360
  prevLegacyType != "scene"
@@ -1371,7 +1370,7 @@ struct ViewState {
1371
1370
  if stateIndex == 1, dashboardSceneCleanupDeferred {
1372
1371
  dashboardSceneCleanupPending = true
1373
1372
  pendingDashboardSceneElementIds.formUnion(prevFrame.elements.map(\.id))
1374
- } else {
1373
+ } else if shouldClearSceneHandoff(stateIndex) {
1375
1374
  let ids = prevFrame.elements.map(\.id)
1376
1375
  Task { [weak self] in
1377
1376
  await self?.sgc?.clearSceneElements(ids)
@@ -1395,6 +1394,13 @@ struct ViewState {
1395
1394
  }
1396
1395
  }
1397
1396
 
1397
+ private func shouldClearSceneHandoff(_ stateIndex: Int) -> Bool {
1398
+ let visibleIndex = headUp && contextualDashboard ? 1 : 0
1399
+ guard let sgc else { return false }
1400
+ return stateIndex == visibleIndex && !screenDisabled && sgc.fullyBooted
1401
+ && !sgc.type.contains(DeviceTypes.SIMULATED) && sgc.sceneHandoffRequiresClear
1402
+ }
1403
+
1398
1404
  /// Guarded scene dispatch - mirrors sendCurrentState's send conditions.
1399
1405
  private func dispatchSceneFrame(_ frame: SceneFrame, stateIndex: Int) {
1400
1406
  if screenDisabled { return }
@@ -1512,13 +1518,16 @@ struct ViewState {
1512
1518
  sgc?.requestSavedWifiNetworks(requestId: requestId, sid: sid) ?? false
1513
1519
  }
1514
1520
 
1515
- 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
+ }
1516
1525
  Bridge.log("MAN: Sending incidentId to glasses for log upload: \(incidentId)")
1517
- sgc?.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
1526
+ try driver.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
1518
1527
  }
1519
1528
 
1520
1529
  func sendWifiCredentials(_ ssid: String, _ password: String) {
1521
- Bridge.log("MAN: Sending wifi credentials: \(ssid) \(password)")
1530
+ Bridge.log("MAN: Sending wifi credentials: \(ssid)")
1522
1531
  sgc?.sendWifiCredentials(ssid, password)
1523
1532
  }
1524
1533
 
@@ -1738,6 +1747,9 @@ struct ViewState {
1738
1747
  func setMicState() {
1739
1748
  let willSendPcm = shouldSendPcm || shouldSendLc3
1740
1749
  let willSendTranscript = shouldSendTranscript || localSttFallbackActive
1750
+ #if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
1751
+ transcriber.setActive(willSendTranscript)
1752
+ #endif
1741
1753
  micEnabled = willSendPcm || willSendTranscript
1742
1754
  updateMicState()
1743
1755
  }
@@ -1943,6 +1955,9 @@ struct ViewState {
1943
1955
  sgc?.clearDisplay() // clear the screen
1944
1956
  sgc?.disconnect()
1945
1957
  sgc = nil // Clear the SGC reference after disconnect
1958
+ // This cache belongs to the discarded connection. Keep consumer demand,
1959
+ // but require a new mic-enable command when replacement glasses are ready.
1960
+ DeviceStore.shared.apply("glasses", "micEnabled", false)
1946
1961
  resetSystemTimeSync()
1947
1962
  resetMicHealth()
1948
1963
  searching = false
@@ -1987,6 +2002,28 @@ struct ViewState {
1987
2002
  controller = nil // Clear the controller reference after disconnect
1988
2003
  }
1989
2004
 
2005
+ private var unpairInProgress = false
2006
+
2007
+ /// Explicit user Unpair, separate from passive pairing cleanup and logout.
2008
+ func unpair() async throws {
2009
+ guard !unpairInProgress else {
2010
+ throw NSError(domain: "NimoUnpair", code: 1,
2011
+ userInfo: [NSLocalizedDescriptionKey: "Unpair already in progress"])
2012
+ }
2013
+ unpairInProgress = true
2014
+ defer { unpairInProgress = false }
2015
+ if let nimo = sgc as? Nimo {
2016
+ _ = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
2017
+ nimo.resetForUnpair { continuation.resume(returning: $0) }
2018
+ }
2019
+ guard (sgc as? Nimo) === nimo else {
2020
+ throw NSError(domain: "NimoUnpair", code: 2,
2021
+ userInfo: [NSLocalizedDescriptionKey: "Glasses changed during Unpair"])
2022
+ }
2023
+ }
2024
+ forget()
2025
+ }
2026
+
1990
2027
  func forget() {
1991
2028
  Bridge.log("MAN: Forgetting smart glasses")
1992
2029
  // Call forget first to stop timers/handlers/reconnect logic
@@ -2053,8 +2090,7 @@ struct ViewState {
2053
2090
  #endif
2054
2091
  // Clean up transcriber resources
2055
2092
  #if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
2056
- transcriber?.shutdown()
2057
- transcriber = nil
2093
+ transcriber.shutdown()
2058
2094
  #endif
2059
2095
 
2060
2096
  // Clean up LC3 converter
@@ -94,6 +94,7 @@ class DeviceStore {
94
94
  store.set("bluetooth", "gallery_mode", true)
95
95
  store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
96
96
  store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.loudnessGateEnabled)
97
+ store.set("bluetooth", "auto_power_off_enabled", BluetoothSdkDefaults.autoPowerOffEnabled)
97
98
  // Mentra Nex feature flag (off by default; toggled from Nex Developer Settings):
98
99
  store.set("bluetooth", "nex_chinese_captions", false)
99
100
  store.set("bluetooth", "screen_disabled", false)
@@ -191,14 +192,25 @@ class DeviceStore {
191
192
 
192
193
  // BLUETOOTH:
193
194
 
195
+ case ("bluetooth", "contextual_dashboard"):
196
+ if value is Bool {
197
+ Task { @MainActor in
198
+ if self.store.get("glasses", "headUp") as? Bool == true {
199
+ DeviceManager.shared.sendCurrentState()
200
+ }
201
+ }
202
+ }
203
+
194
204
  case ("bluetooth", "brightness"):
195
205
  let b = value as? Int ?? 50
196
206
  let auto = store.get("bluetooth", "auto_brightness") as? Bool ?? true
197
207
  Task {
198
- DeviceManager.shared.sgc?.setBrightness(b, autoMode: auto)
199
- await DeviceManager.shared.sgc?.sendTextWall("Set brightness to \(b)%")
208
+ guard let device = DeviceManager.shared.sgc else { return }
209
+ device.setBrightness(b, autoMode: auto)
210
+ guard device.showBrightnessConfirmation else { return }
211
+ await device.sendTextWall("Set brightness to \(b)%")
200
212
  try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
201
- DeviceManager.shared.sgc?.clearDisplay()
213
+ if (DeviceManager.shared.sgc as AnyObject?) === (device as AnyObject) { device.clearDisplay() }
202
214
  }
203
215
 
204
216
  case ("bluetooth", "auto_brightness"):
@@ -206,13 +218,14 @@ class DeviceStore {
206
218
  let auto = value as? Bool ?? true
207
219
  let autoBrightnessChanged = (oldValue as? Bool) != auto
208
220
  Task {
209
- DeviceManager.shared.sgc?.setBrightness(b, autoMode: auto)
210
- if autoBrightnessChanged {
211
- await DeviceManager.shared.sgc?.sendTextWall(
221
+ guard let device = DeviceManager.shared.sgc else { return }
222
+ device.setBrightness(b, autoMode: auto)
223
+ if autoBrightnessChanged, device.showBrightnessConfirmation {
224
+ await device.sendTextWall(
212
225
  auto ? "Enabled auto brightness" : "Disabled auto brightness"
213
226
  )
214
227
  try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
215
- DeviceManager.shared.sgc?.clearDisplay()
228
+ if (DeviceManager.shared.sgc as AnyObject?) === (device as AnyObject) { device.clearDisplay() }
216
229
  }
217
230
  }
218
231
 
@@ -254,6 +267,12 @@ class DeviceStore {
254
267
  case ("bluetooth", "loudness_gate_enabled"):
255
268
  DeviceManager.shared.sgc?.sendLoudnessGateSetting()
256
269
 
270
+ case ("bluetooth", "auto_power_off_enabled"):
271
+ Bridge.log(
272
+ "DeviceStore: auto_power_off_enabled changed to \(String(describing: value)) — sending cs_swit type 11"
273
+ )
274
+ DeviceManager.shared.sgc?.sendAutoPowerOffSetting()
275
+
257
276
  // Deliberately has no seeded default: the key starts absent so that a
258
277
  // session where the engine has not authorized tuning can only ever
259
278
  // send a reset.