@mentra/bluetooth-sdk 0.1.21-beta.3 → 3.1.0-dev.4

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 (118) hide show
  1. package/README.md +38 -2
  2. package/android/build.gradle +7 -4
  3. package/android/lc3Lib/build.gradle +18 -4
  4. package/android/silero/build.gradle +9 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +209 -21
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  12. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  13. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  20. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1381 -284
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  29. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  37. package/build/BluetoothSdk.types.d.ts +63 -2
  38. package/build/BluetoothSdk.types.d.ts.map +1 -1
  39. package/build/BluetoothSdk.types.js.map +1 -1
  40. package/build/_internal.d.ts +2 -0
  41. package/build/_internal.d.ts.map +1 -1
  42. package/build/_internal.js +2 -0
  43. package/build/_internal.js.map +1 -1
  44. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  45. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  46. package/build/_private/BluetoothSdkModule.js +1 -1
  47. package/build/_private/BluetoothSdkModule.js.map +1 -1
  48. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  49. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  50. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  51. package/build/_private/sdkOtaManifest.d.ts +3 -12
  52. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  53. package/build/_private/sdkOtaManifest.js +5 -17
  54. package/build/_private/sdkOtaManifest.js.map +1 -1
  55. package/build/generated/releaseMetadata.d.ts +12 -0
  56. package/build/generated/releaseMetadata.d.ts.map +1 -0
  57. package/build/generated/releaseMetadata.js +10 -0
  58. package/build/generated/releaseMetadata.js.map +1 -0
  59. package/build/index.d.ts +1 -1
  60. package/build/index.d.ts.map +1 -1
  61. package/build/index.js +10 -2
  62. package/build/index.js.map +1 -1
  63. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  64. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  65. package/build/ota-server/MentraOtaServer.types.js +2 -0
  66. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  67. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  68. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  69. package/build/ota-server/MentraOtaServerModule.js +3 -0
  70. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  71. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  72. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  73. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  74. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  75. package/build/ota-server/index.d.ts +3 -0
  76. package/build/ota-server/index.d.ts.map +1 -0
  77. package/build/ota-server/index.js +3 -0
  78. package/build/ota-server/index.js.map +1 -0
  79. package/expo-module.config.json +3 -2
  80. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  81. package/ios/BluetoothSdkModule.swift +16 -2
  82. package/ios/LocalIPv4.swift +150 -0
  83. package/ios/LocalOtaServer.swift +309 -0
  84. package/ios/MentraBluetoothSDK.podspec +5 -1
  85. package/ios/MentraOtaServerModule.swift +147 -0
  86. package/ios/MentraPhotoReceiverModule.swift +1 -81
  87. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  88. package/ios/Source/Bridge.swift +47 -3
  89. package/ios/Source/DeviceManager.swift +146 -16
  90. package/ios/Source/DeviceStore.swift +2 -0
  91. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  92. package/ios/Source/MentraBluetoothSDK.swift +74 -6
  93. package/ios/Source/OtaManifest.swift +5 -7
  94. package/ios/Source/audio/AudioModels.swift +17 -0
  95. package/ios/Source/camera/CameraModels.swift +1 -0
  96. package/ios/Source/events/BluetoothEvents.swift +27 -0
  97. package/ios/Source/sgcs/MentraLive.swift +498 -77
  98. package/ios/Source/sgcs/SGCManager.swift +11 -0
  99. package/ios/Source/status/DeviceStatus.swift +23 -0
  100. package/ios/Source/types/DeviceModels.swift +23 -2
  101. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  102. package/package.json +5 -1
  103. package/plugin/build/withIos.d.ts +3 -1
  104. package/plugin/build/withIos.js +22 -10
  105. package/scripts/verify-release-package.mjs +34 -0
  106. package/scripts/write-release-metadata.mjs +154 -0
  107. package/scripts/write-release-metadata.test.mjs +29 -0
  108. package/src/BluetoothSdk.types.ts +74 -3
  109. package/src/_internal.ts +2 -0
  110. package/src/_private/BluetoothSdkModule.ts +5 -5
  111. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  112. package/src/_private/sdkOtaManifest.ts +5 -17
  113. package/src/generated/releaseMetadata.ts +20 -0
  114. package/src/index.ts +19 -2
  115. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  116. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  117. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  118. package/src/ota-server/index.ts +2 -0
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import {mkdirSync, writeFileSync} from "node:fs"
3
+ import path from "node:path"
4
+ import {fileURLToPath} from "node:url"
5
+
6
+ const STABLE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
7
+ const RELEASE_IDENTITY_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:dev|beta)\.[1-9]\d*)?$/
8
+ const COMMIT_PATTERN = /^[0-9a-f]{40}$/
9
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/
10
+
11
+ function validateHttpsUrl(value) {
12
+ let url
13
+ try {
14
+ url = new URL(value)
15
+ } catch {
16
+ throw new Error(`OTA manifest URL is invalid: ${JSON.stringify(value)}`)
17
+ }
18
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) {
19
+ throw new Error("OTA manifest URL must be credential-free HTTPS without a fragment")
20
+ }
21
+ return url.toString()
22
+ }
23
+
24
+ export function validateReleaseMetadata(input) {
25
+ if (!STABLE_VERSION_PATTERN.test(input.familyBaseVersion)) {
26
+ throw new Error("familyBaseVersion must be a plain X.Y.Z version")
27
+ }
28
+ if (!RELEASE_IDENTITY_PATTERN.test(input.releaseIdentity)) {
29
+ throw new Error("releaseIdentity must be X.Y.Z, X.Y.Z-dev.N, or X.Y.Z-beta.N")
30
+ }
31
+ if (
32
+ input.releaseIdentity !== input.familyBaseVersion &&
33
+ !input.releaseIdentity.startsWith(`${input.familyBaseVersion}-`)
34
+ ) {
35
+ throw new Error("releaseIdentity does not belong to familyBaseVersion")
36
+ }
37
+ if (input.releaseSetId !== `mentra-${input.releaseIdentity}`) {
38
+ throw new Error("releaseSetId must be mentra-<releaseIdentity>")
39
+ }
40
+ if (!COMMIT_PATTERN.test(input.sourceCommit)) throw new Error("sourceCommit must be a full lowercase Git SHA")
41
+ if (!SHA256_PATTERN.test(input.otaManifestSha256)) {
42
+ throw new Error("otaManifestSha256 must be a lowercase SHA-256 digest")
43
+ }
44
+ return {
45
+ schemaVersion: 1,
46
+ familyBaseVersion: input.familyBaseVersion,
47
+ releaseIdentity: input.releaseIdentity,
48
+ releaseSetId: input.releaseSetId,
49
+ sourceCommit: input.sourceCommit,
50
+ otaManifestUrl: validateHttpsUrl(input.otaManifestUrl),
51
+ otaManifestSha256: input.otaManifestSha256,
52
+ }
53
+ }
54
+
55
+ function swiftString(value) {
56
+ return JSON.stringify(value)
57
+ }
58
+
59
+ export function renderReleaseMetadata(input) {
60
+ const metadata = validateReleaseMetadata(input)
61
+ const typescript = [
62
+ "/** Generated by release CI. Do not edit in a release checkout. */",
63
+ "export interface BluetoothSdkReleaseMetadata {",
64
+ " schemaVersion: 1",
65
+ " familyBaseVersion: string | null",
66
+ " releaseIdentity: string | null",
67
+ " releaseSetId: string | null",
68
+ " sourceCommit: string | null",
69
+ " otaManifestUrl: string | null",
70
+ " otaManifestSha256: string | null",
71
+ "}",
72
+ "",
73
+ `export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze(${JSON.stringify(
74
+ metadata,
75
+ null,
76
+ 2,
77
+ )})`,
78
+ "",
79
+ ].join("\n")
80
+ const kotlin = [
81
+ "package com.mentra.bluetoothsdk",
82
+ "",
83
+ "/** Generated by release CI. Do not edit in a release checkout. */",
84
+ "internal object GeneratedReleaseMetadata {",
85
+ ` const val FAMILY_BASE_VERSION: String = ${JSON.stringify(metadata.familyBaseVersion)}`,
86
+ ` const val RELEASE_IDENTITY: String = ${JSON.stringify(metadata.releaseIdentity)}`,
87
+ ` const val RELEASE_SET_ID: String = ${JSON.stringify(metadata.releaseSetId)}`,
88
+ ` const val SOURCE_COMMIT: String = ${JSON.stringify(metadata.sourceCommit)}`,
89
+ ` const val OTA_MANIFEST_URL: String = ${JSON.stringify(metadata.otaManifestUrl)}`,
90
+ ` const val OTA_MANIFEST_SHA256: String = ${JSON.stringify(metadata.otaManifestSha256)}`,
91
+ "}",
92
+ "",
93
+ ].join("\n")
94
+ const swift = [
95
+ "import Foundation",
96
+ "",
97
+ "/// Generated by release CI. Do not edit in a release checkout.",
98
+ "enum GeneratedReleaseMetadata {",
99
+ ` static let familyBaseVersion = ${swiftString(metadata.familyBaseVersion)}`,
100
+ ` static let releaseIdentity = ${swiftString(metadata.releaseIdentity)}`,
101
+ ` static let releaseSetId = ${swiftString(metadata.releaseSetId)}`,
102
+ ` static let sourceCommit = ${swiftString(metadata.sourceCommit)}`,
103
+ ` static let otaManifestUrl = ${swiftString(metadata.otaManifestUrl)}`,
104
+ ` static let otaManifestSha256 = ${swiftString(metadata.otaManifestSha256)}`,
105
+ "}",
106
+ "",
107
+ ].join("\n")
108
+ return {metadata, typescript, kotlin, swift}
109
+ }
110
+
111
+ function parseArgs(args) {
112
+ const values = {}
113
+ for (let index = 0; index < args.length; index += 2) {
114
+ const option = args[index]
115
+ const value = args[index + 1]
116
+ if (!option?.startsWith("--") || value === undefined) throw new Error("Expected --name value pairs")
117
+ values[option.slice(2)] = value
118
+ }
119
+ return values
120
+ }
121
+
122
+ function main() {
123
+ const args = parseArgs(process.argv.slice(2))
124
+ const required = [
125
+ "family-base-version",
126
+ "release-identity",
127
+ "release-set-id",
128
+ "source-commit",
129
+ "ota-manifest-url",
130
+ "ota-manifest-sha256",
131
+ ]
132
+ for (const field of required) if (!args[field]) throw new Error(`Missing --${field}`)
133
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
134
+ const rendered = renderReleaseMetadata({
135
+ familyBaseVersion: args["family-base-version"],
136
+ releaseIdentity: args["release-identity"],
137
+ releaseSetId: args["release-set-id"],
138
+ sourceCommit: args["source-commit"],
139
+ otaManifestUrl: args["ota-manifest-url"],
140
+ otaManifestSha256: args["ota-manifest-sha256"],
141
+ })
142
+ const outputs = {
143
+ typescript: path.join(packageRoot, "src/generated/releaseMetadata.ts"),
144
+ kotlin: path.join(packageRoot, "android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt"),
145
+ swift: path.join(packageRoot, "ios/Source/GeneratedReleaseMetadata.swift"),
146
+ }
147
+ for (const [target, file] of Object.entries(outputs)) {
148
+ mkdirSync(path.dirname(file), {recursive: true})
149
+ writeFileSync(file, rendered[target])
150
+ }
151
+ console.log(`Wrote Bluetooth SDK release metadata for ${rendered.metadata.releaseSetId}`)
152
+ }
153
+
154
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main()
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict"
2
+ import test from "node:test"
3
+
4
+ import {renderReleaseMetadata, validateReleaseMetadata} from "./write-release-metadata.mjs"
5
+
6
+ const input = {
7
+ familyBaseVersion: "3.1.0",
8
+ releaseIdentity: "3.1.0-beta.57",
9
+ releaseSetId: "mentra-3.1.0-beta.57",
10
+ sourceCommit: "a".repeat(40),
11
+ otaManifestUrl:
12
+ "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-3.1.0-beta.57/mentra-live-ota-3.1.0-beta.57.json",
13
+ otaManifestSha256: "b".repeat(64),
14
+ }
15
+
16
+ test("renders identical immutable metadata for TypeScript, Kotlin, and Swift", () => {
17
+ const rendered = renderReleaseMetadata(input)
18
+ for (const contents of [rendered.typescript, rendered.kotlin, rendered.swift]) {
19
+ assert.match(contents, /3\.1\.0-beta\.57/)
20
+ assert.match(contents, /mentra-live-ota-3\.1\.0-beta\.57\.json/)
21
+ assert.match(contents, new RegExp("b".repeat(64)))
22
+ }
23
+ })
24
+
25
+ test("rejects mutable or cross-family release metadata", () => {
26
+ assert.throws(() => validateReleaseMetadata({...input, releaseIdentity: "3.2.0-beta.57"}), /does not belong/)
27
+ assert.throws(() => validateReleaseMetadata({...input, otaManifestUrl: "http://example.com/version.json"}), /HTTPS/)
28
+ assert.throws(() => validateReleaseMetadata({...input, otaManifestSha256: "short"}), /SHA-256/)
29
+ })
@@ -155,6 +155,8 @@ export type VersionInfoResult = {
155
155
  systemTimeMs?: number
156
156
  otaVersionUrl: string
157
157
  appVersion: string
158
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
159
+ hotspotOtaVersion: number
158
160
  }
159
161
 
160
162
  export type VersionInfoEvent = VersionInfoResult & {
@@ -648,6 +650,28 @@ export type PairFailureEvent = {
648
650
  error: string
649
651
  }
650
652
 
653
+ export type PairingInfoEvent = {
654
+ had_previous_bond: boolean
655
+ /** 16-char uppercase hex transfer id when secure pairing is active. */
656
+ transfer_id?: string
657
+ pairing_code?: string
658
+ classic_bond_ready?: boolean
659
+ secure_pairing_capable?: boolean
660
+ protocol_version?: number
661
+ /** Credential binding mode negotiated for this transfer, when reported by the glasses. */
662
+ binding?: "ctkd" | "temporal" | "none" | string
663
+ }
664
+
665
+ export type EnteringPairingModeEvent = {
666
+ window_ms: number
667
+ reason?: string
668
+ txn?: number
669
+ }
670
+
671
+ export type OwnerReplacedEvent = {
672
+ reason: string
673
+ }
674
+
651
675
  export type AudioPairingNeededEvent = {
652
676
  type: "audio_pairing_needed"
653
677
  deviceName: string
@@ -701,6 +725,17 @@ export type MicLc3Event = {
701
725
  voiceActivityDetectionEnabled: boolean
702
726
  }
703
727
 
728
+ /** Native glasses-microphone diagnostics emitted when the SDK detects a transport or decode issue. */
729
+ export type MicHealthEvent = {
730
+ type: "mic_health"
731
+ reason: "sequence_gap" | "decode_failure"
732
+ sequenceGapEvents: number
733
+ decodeFailures: number
734
+ lastLc3ReceivedAt?: number
735
+ lastPcmProducedAt?: number
736
+ timestamp: number
737
+ }
738
+
704
739
  export type StreamStatusLifecycleState = "initializing" | "streaming" | "stopping" | "stopped"
705
740
  export type StreamStatusReconnectState = "reconnecting" | "reconnected" | "reconnect_failed"
706
741
  export type StreamStatusState = StreamStatusLifecycleState | StreamStatusReconnectState | "error"
@@ -895,6 +930,9 @@ export type BluetoothSdkModuleEvents = {
895
930
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void
896
931
  settings_ack: (event: SettingsAckEvent) => void
897
932
  pair_failure: (event: PairFailureEvent) => void
933
+ pairing_info: (event: PairingInfoEvent) => void
934
+ entering_pairing_mode: (event: EnteringPairingModeEvent) => void
935
+ owner_replaced: (event: OwnerReplacedEvent) => void
898
936
  audio_pairing_needed: (event: AudioPairingNeededEvent) => void
899
937
  audio_connected: (event: AudioConnectedEvent) => void
900
938
  audio_disconnected: (event: AudioDisconnectedEvent) => void
@@ -903,6 +941,7 @@ export type BluetoothSdkModuleEvents = {
903
941
  ws_bin: (event: WsBinEvent) => void
904
942
  mic_pcm: (event: MicPcmEvent) => void
905
943
  mic_lc3: (event: MicLc3Event) => void
944
+ mic_health: (event: MicHealthEvent) => void
906
945
  stream_status: (event: StreamStatusEvent) => void
907
946
  keep_alive_ack: (event: KeepAliveAckEvent) => void
908
947
  mtk_update_complete: (event: MtkUpdateCompleteEvent) => void
@@ -999,11 +1038,15 @@ export type BluetoothSdkEventMap = {
999
1038
  rgb_led_control_response: RgbLedControlResponseEvent
1000
1039
  settings_ack: SettingsAckEvent
1001
1040
  pair_failure: PairFailureEvent
1041
+ pairing_info: PairingInfoEvent
1042
+ entering_pairing_mode: EnteringPairingModeEvent
1043
+ owner_replaced: OwnerReplacedEvent
1002
1044
  audio_pairing_needed: AudioPairingNeededEvent
1003
1045
  audio_connected: AudioConnectedEvent
1004
1046
  audio_disconnected: AudioDisconnectedEvent
1005
1047
  mic_pcm: MicPcmEvent
1006
1048
  mic_lc3: MicLc3Event
1049
+ mic_health: MicHealthEvent
1007
1050
  stream_status: StreamStatusEvent
1008
1051
  ota_start_ack: OtaStartAckEvent
1009
1052
  ota_status: OtaStatusEvent
@@ -1056,6 +1099,8 @@ export interface BluetoothSdkPublicModule {
1056
1099
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
1057
1100
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
1058
1101
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
1102
+ /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
1103
+ setWifiAdbState(enabled: boolean): Promise<void>
1059
1104
 
1060
1105
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
1061
1106
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
@@ -1110,6 +1155,8 @@ export interface BluetoothSdkPublicModule {
1110
1155
  webhookUrl?: string,
1111
1156
  authToken?: string,
1112
1157
  ): Promise<VideoRecordingStoppedStatusEvent>
1158
+ /** Query the glasses for the current recording state and elapsed duration. */
1159
+ queryVideoRecordingStatus(requestId: string): Promise<VideoRecordingStatusEvent>
1113
1160
 
1114
1161
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
1115
1162
  stopStream(): Promise<StreamStatusEvent>
@@ -1131,14 +1178,27 @@ export interface BluetoothSdkPublicModule {
1131
1178
  ): Promise<RgbLedControlSuccessResponseEvent>
1132
1179
 
1133
1180
  requestVersionInfo(): Promise<VersionInfoResult>
1181
+ /**
1182
+ * Select the OTA manifest used by subsequent update checks and installs.
1183
+ * The URL may point at Mentra's hosted manifest or any customer-controlled HTTP(S) server.
1184
+ */
1185
+ setOtaVersionUrl(otaVersionUrl: string): void
1186
+ /** Return the configured or release-embedded OTA manifest URL. Rejects when a source build is unconfigured. */
1187
+ getOtaVersionUrl(): string
1134
1188
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
1135
1189
  checkForOtaUpdate(): Promise<boolean>
1136
- /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
1137
- startOtaUpdate(): Promise<OtaStartAckEvent>
1190
+ /** Start OTA from the configured or explicitly supplied manifest URL. */
1191
+ startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
1138
1192
  startAr99OtaFromFile(path: string): Promise<boolean>
1139
1193
  cancelAr99Ota(): Promise<void>
1140
1194
  sendAr99FactoryReset(): Promise<void>
1141
- buildAr99OtaSignature(secret: string, appName: string, currentVersion: string, serialNumber: string, nonce: string): string
1195
+ buildAr99OtaSignature(
1196
+ secret: string,
1197
+ appName: string,
1198
+ currentVersion: string,
1199
+ serialNumber: string,
1200
+ nonce: string,
1201
+ ): string
1142
1202
 
1143
1203
  // // stt commands (MOVE TO CRUST)
1144
1204
  // setSttModelDetails(path: string, languageCode: string): Promise<void>
@@ -1201,6 +1261,8 @@ export interface OtaUpdateInfo {
1201
1261
  updates: string[] // ["apk", "mtk", "bes"]
1202
1262
  totalSize: number
1203
1263
  cacheReady?: boolean
1264
+ /** Exact BES target selected from the OTA manifest, when a BES step is pending. */
1265
+ besVersion?: string
1204
1266
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
1205
1267
  isDowngrade?: boolean
1206
1268
  }
@@ -1231,6 +1293,7 @@ export interface GlassesStatus {
1231
1293
  besFirmwareVersion: string
1232
1294
  mtkFirmwareVersion: string
1233
1295
  bluetoothMacAddress: string
1296
+ wifiMacAddress: string
1234
1297
  leftMacAddress: string
1235
1298
  rightMacAddress: string
1236
1299
  buildNumber: string
@@ -1238,6 +1301,8 @@ export interface GlassesStatus {
1238
1301
  systemTimeMs?: number
1239
1302
  otaVersionUrl: string
1240
1303
  appVersion: string
1304
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
1305
+ hotspotOtaVersion: number
1241
1306
  bluetoothName: string
1242
1307
  serialNumber: string
1243
1308
  style: string
@@ -1301,6 +1366,12 @@ export interface Device {
1301
1366
  * appear in a later scan update when the platform reports RSSI metadata.
1302
1367
  */
1303
1368
  rssi?: number
1369
+ /** Mentra Live: unit is currently in pairing mode (adv flag). */
1370
+ pairingMode?: boolean
1371
+ /** Mentra Live: four-character hex spoken pairing code when available. */
1372
+ pairingCode?: string
1373
+ /** Mentra Live: advertisement carries the secure-pairing capability trailer. */
1374
+ securePairingCapable?: boolean
1304
1375
  }
1305
1376
 
1306
1377
  export interface ConnectOptions {
package/src/_internal.ts CHANGED
@@ -11,4 +11,6 @@ export type {BluetoothSdkInternalModule} from "./_private/BluetoothSdkModule"
11
11
  export * from "./BluetoothSdk.types"
12
12
  export {default as MentraLocalNetwork} from "./_private/MentraLocalNetworkModule"
13
13
  export * from "./_private/MentraLocalNetworkModule"
14
+ export {default as MentraOtaServer} from "./ota-server"
15
+ export * from "./ota-server/MentraOtaServer.types"
14
16
  export {BLUETOOTH_SDK_VERSION, sdkPinnedOtaManifestUrl} from "./_private/sdkOtaManifest"
@@ -44,6 +44,7 @@ import {
44
44
  VideoRecordingStartedStatusEvent,
45
45
  VideoRecordingSettings,
46
46
  VideoRecordingStoppedStatusEvent,
47
+ VideoRecordingStatusEvent,
47
48
  VersionInfoResult,
48
49
  WarmUpCameraParams,
49
50
  WifiSearchResult,
@@ -117,6 +118,8 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
117
118
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
118
119
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
119
120
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
121
+ /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
122
+ setWifiAdbState(enabled: boolean): Promise<void>
120
123
  /** Set the glasses system clock when phone detects clock skew (Mentra Live and G2). */
121
124
  setSystemTime(timestampMs: number): Promise<void>
122
125
  /** Logs current WiFi frequency (MHz) and 5 GHz band to Android logcat. */
@@ -179,6 +182,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
179
182
  webhookUrl?: string,
180
183
  authToken?: string,
181
184
  ): Promise<VideoRecordingStoppedStatusEvent>
185
+ queryVideoRecordingStatus(requestId: string): Promise<VideoRecordingStatusEvent>
182
186
 
183
187
  // Stream Commands
184
188
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
@@ -293,7 +297,7 @@ const CAMERA_ROI_POSITION_VALUES: Record<CameraRoiPosition, CameraFovSetting["ro
293
297
  }
294
298
 
295
299
  // Named presets are a convenience layer over the numeric {fov, roiPosition} API.
296
- // The default is the full sensor; "standard" preserves the historical 102 crop.
300
+ // The default is the full sensor; "standard" preserves the historical 102° crop.
297
301
  const CAMERA_FOV_PRESETS: Record<CameraFovPreset, CameraFovSetting> = {
298
302
  narrow: {fov: 82, roiPosition: 0},
299
303
  standard: {fov: 102, roiPosition: 0},
@@ -649,7 +653,3 @@ NativeBluetoothSdkModule.warmUpCamera = function (params: WarmUpCameraParams) {
649
653
 
650
654
  export default NativeBluetoothSdkModule
651
655
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkInternalModule
652
-
653
-
654
-
655
-
@@ -26,7 +26,7 @@ type LocalNetworkEvents = {
26
26
  }
27
27
 
28
28
  declare class MentraLocalNetworkNativeModule extends NativeModule<LocalNetworkEvents> {
29
- connect(ssid: string, password: string): Promise<{connected: boolean; ssid: string}>
29
+ connect(ssid: string, password: string): Promise<{connected: boolean; ssid: string; localAddress?: string}>
30
30
  request(
31
31
  requestId: string,
32
32
  url: string,
@@ -1,17 +1,9 @@
1
1
  /**
2
- * SDK-pinned OTA manifest URL derivation.
3
- *
4
- * Each Bluetooth SDK release publishes an immutable manifest
5
- * (`bluetooth-sdk-<version>-version.json`) pinning the exact ASG artifact paired with that SDK
6
- * version. The URL is derived from this package's own version — the version is frozen into the
7
- * published artifact, so this is equivalent to baking the URL at release time while keeping the
8
- * manifest itself replaceable as an operational escape hatch (asset moves, CDN migration).
9
- *
10
- * Mirrors the native derivation in `OtaManifestDefaults.defaultOtaVersionUrl()` (Kotlin) /
11
- * `BluetoothSdkDefaults` (Swift).
2
+ * Release CI embeds one immutable OTA manifest URL into every SDK distribution.
3
+ * Source builds remain unpinned and must opt in through the debug configuration API.
12
4
  */
13
5
 
14
- const SDK_OTA_RELEASE_BASE_URL = "https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
6
+ import {BLUETOOTH_SDK_RELEASE_METADATA} from "../generated/releaseMetadata"
15
7
 
16
8
  // eslint-disable-next-line @typescript-eslint/no-var-requires
17
9
  const packageJson = require("../../package.json") as {version?: string}
@@ -20,12 +12,8 @@ const packageJson = require("../../package.json") as {version?: string}
20
12
  export const BLUETOOTH_SDK_VERSION: string = (packageJson.version ?? "").trim()
21
13
 
22
14
  /**
23
- * The immutable manifest URL pinning the ASG artifact paired with this SDK version, or null when
24
- * the package version is unavailable (malformed workspace state).
15
+ * The immutable manifest URL embedded by release CI, or null for an unpinned source build.
25
16
  */
26
17
  export function sdkPinnedOtaManifestUrl(): string | null {
27
- if (!BLUETOOTH_SDK_VERSION) {
28
- return null
29
- }
30
- return `${SDK_OTA_RELEASE_BASE_URL}/bluetooth-sdk-${BLUETOOTH_SDK_VERSION}-version.json`
18
+ return BLUETOOTH_SDK_RELEASE_METADATA.otaManifestUrl
31
19
  }
@@ -0,0 +1,20 @@
1
+ /** Generated by release CI. Do not edit in a release checkout. */
2
+ export interface BluetoothSdkReleaseMetadata {
3
+ schemaVersion: 1
4
+ familyBaseVersion: string | null
5
+ releaseIdentity: string | null
6
+ releaseSetId: string | null
7
+ sourceCommit: string | null
8
+ otaManifestUrl: string | null
9
+ otaManifestSha256: string | null
10
+ }
11
+
12
+ export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
+ "schemaVersion": 1,
14
+ "familyBaseVersion": "3.1.0",
15
+ "releaseIdentity": "3.1.0-dev.4",
16
+ "releaseSetId": "mentra-3.1.0-dev.4",
17
+ "sourceCommit": "43b81aea1c84675fade024077063e45fddde4861",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.4/mentra-live-ota-3.1.0-dev.4.json",
19
+ "otaManifestSha256": "8fbea7589345658dc5dffcb394229ffc5c52fbe75352acb8f6827d83290ce750"
20
+ })
package/src/index.ts CHANGED
@@ -36,11 +36,15 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
36
36
  "rgb_led_control_response",
37
37
  "settings_ack",
38
38
  "pair_failure",
39
+ "pairing_info",
40
+ "entering_pairing_mode",
41
+ "owner_replaced",
39
42
  "audio_pairing_needed",
40
43
  "audio_connected",
41
44
  "audio_disconnected",
42
45
  "mic_pcm",
43
46
  "mic_lc3",
47
+ "mic_health",
44
48
  "stream_status",
45
49
  "ota_start_ack",
46
50
  "ota_status",
@@ -56,8 +60,8 @@ const addListener: BluetoothSdkPublicModule["addListener"] = (eventName, listene
56
60
  return PrivateBluetoothSdkModule.addListener(eventName, listener as BluetoothSdkEventListener<BluetoothSdkEventName>)
57
61
  }
58
62
 
59
- const startOtaUpdate: BluetoothSdkPublicModule["startOtaUpdate"] = () => {
60
- return PrivateBluetoothSdkModule.startOtaUpdate()
63
+ const startOtaUpdate: BluetoothSdkPublicModule["startOtaUpdate"] = (otaVersionUrl) => {
64
+ return PrivateBluetoothSdkModule.startOtaUpdate(otaVersionUrl)
61
65
  }
62
66
 
63
67
  const bindPublicMethod = <K extends keyof BluetoothSdkPublicModule>(name: K): BluetoothSdkPublicModule[K] => {
@@ -95,6 +99,7 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
95
99
  sendWifiCredentials: bindPublicMethod("sendWifiCredentials"),
96
100
  forgetWifiNetwork: bindPublicMethod("forgetWifiNetwork"),
97
101
  setHotspotState: bindPublicMethod("setHotspotState"),
102
+ setWifiAdbState: bindPublicMethod("setWifiAdbState"),
98
103
  setGalleryModeEnabled: bindPublicMethod("setGalleryModeEnabled"),
99
104
  setVoiceActivityDetectionEnabled: bindPublicMethod("setVoiceActivityDetectionEnabled"),
100
105
  setLoudnessGateEnabled: bindPublicMethod("setLoudnessGateEnabled"),
@@ -125,6 +130,7 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
125
130
  stopCameraWarmUp: bindPublicMethod("stopCameraWarmUp"),
126
131
  startVideoRecording: bindPublicMethod("startVideoRecording"),
127
132
  stopVideoRecording: bindPublicMethod("stopVideoRecording"),
133
+ queryVideoRecordingStatus: bindPublicMethod("queryVideoRecordingStatus"),
128
134
  startStream: bindPublicMethod("startStream"),
129
135
  stopStream: bindPublicMethod("stopStream"),
130
136
  setMicState: bindPublicMethod("setMicState"),
@@ -134,6 +140,8 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
134
140
  setGlassesMediaVolume: bindPublicMethod("setGlassesMediaVolume"),
135
141
  rgbLedControl: bindPublicMethod("rgbLedControl"),
136
142
  requestVersionInfo: bindPublicMethod("requestVersionInfo"),
143
+ setOtaVersionUrl: bindPublicMethod("setOtaVersionUrl"),
144
+ getOtaVersionUrl: bindPublicMethod("getOtaVersionUrl"),
137
145
  checkForOtaUpdate: bindPublicMethod("checkForOtaUpdate"),
138
146
  startOtaUpdate,
139
147
  startAr99OtaFromFile: bindPublicMethod("startAr99OtaFromFile"),
@@ -204,17 +212,26 @@ export type {
204
212
  HotspotErrorEvent,
205
213
  HotspotStatus,
206
214
  HotspotStatusChangeEvent,
215
+ KeepAliveAckEvent,
207
216
  LocalTranscriptionEvent,
208
217
  LogEvent,
209
218
  MicLc3Event,
219
+ MicHealthEvent,
210
220
  MicMode,
211
221
  MicPcmEvent,
212
222
  MicPreference,
213
223
  OtaStartAckEvent,
224
+ OtaProgress,
225
+ OtaProgressStatus,
226
+ OtaStage,
214
227
  OtaStatus,
215
228
  OtaStatusEvent,
216
229
  OtaQueryResult,
230
+ OtaUpdateInfo,
217
231
  PairFailureEvent,
232
+ PairingInfoEvent,
233
+ EnteringPairingModeEvent,
234
+ OwnerReplacedEvent,
218
235
  PhotoCaptureMetadata,
219
236
  PhotoResolvedConfig,
220
237
  PhotoCompression,
@@ -0,0 +1,10 @@
1
+ export type OtaServerResult = {
2
+ baseUrl: string
3
+ host: string
4
+ manifestUrl: string
5
+ port: number
6
+ }
7
+
8
+ export type MentraOtaServerModuleEvents = {
9
+ artifactDownloadProgress: (event: {destination: string; bytesWritten: number; contentLength: number}) => void
10
+ }
@@ -0,0 +1,23 @@
1
+ import {NativeModule, requireNativeModule} from "expo"
2
+
3
+ import type {MentraOtaServerModuleEvents, OtaServerResult} from "./MentraOtaServer.types"
4
+
5
+ declare class MentraOtaServerModule extends NativeModule<MentraOtaServerModuleEvents> {
6
+ /**
7
+ * Serve `manifestJson` at /version.json and `artifactPaths` (sha256 -> local file path)
8
+ * at /artifacts/<sha256>. Pass `host` when the caller knows the authoritative local
9
+ * address (e.g. from the scoped hotspot network); omit to fall back to interface
10
+ * scanning. Returns the URLs to hand to the glasses via ota_start.
11
+ */
12
+ startOtaServer(
13
+ manifestJson: string,
14
+ artifactPaths: Record<string, string>,
15
+ host?: string | null,
16
+ ): Promise<OtaServerResult>
17
+ stopOtaServer(): Promise<void>
18
+ /** Wait for iPhone Wi-Fi (`en0`) to acquire an address on the glasses gateway subnet. */
19
+ waitForWifiAddress(gateway: string, timeoutMs: number): Promise<string>
20
+ downloadArtifact(source: string, destination: string): Promise<{statusCode: number; bytesWritten: number}>
21
+ }
22
+
23
+ export default requireNativeModule<MentraOtaServerModule>("MentraOtaServer")
@@ -0,0 +1,17 @@
1
+ import {NativeModule, registerWebModule} from "expo"
2
+
3
+ import type {MentraOtaServerModuleEvents, OtaServerResult} from "./MentraOtaServer.types"
4
+
5
+ class MentraOtaServerModule extends NativeModule<MentraOtaServerModuleEvents> {
6
+ async startOtaServer(): Promise<OtaServerResult> {
7
+ throw new Error("The OTA server is only available in native apps.")
8
+ }
9
+
10
+ async stopOtaServer(): Promise<void> {}
11
+
12
+ async waitForWifiAddress(): Promise<string> {
13
+ throw new Error("The OTA server is only available in native apps.")
14
+ }
15
+ }
16
+
17
+ export default registerWebModule(MentraOtaServerModule, "MentraOtaServer")
@@ -0,0 +1,2 @@
1
+ export {default} from "./MentraOtaServerModule"
2
+ export * from "./MentraOtaServer.types"