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

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 (122) hide show
  1. package/README.md +56 -9
  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 +35 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +216 -23
  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 +1344 -284
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  29. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  30. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +75 -0
  38. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  39. package/build/BluetoothSdk.types.d.ts +58 -2
  40. package/build/BluetoothSdk.types.d.ts.map +1 -1
  41. package/build/BluetoothSdk.types.js.map +1 -1
  42. package/build/_internal.d.ts +2 -0
  43. package/build/_internal.d.ts.map +1 -1
  44. package/build/_internal.js +2 -0
  45. package/build/_internal.js.map +1 -1
  46. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  47. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  48. package/build/_private/BluetoothSdkModule.js +1 -1
  49. package/build/_private/BluetoothSdkModule.js.map +1 -1
  50. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  51. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  52. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  53. package/build/_private/sdkOtaManifest.d.ts +3 -12
  54. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  55. package/build/_private/sdkOtaManifest.js +5 -17
  56. package/build/_private/sdkOtaManifest.js.map +1 -1
  57. package/build/generated/releaseMetadata.d.ts +12 -0
  58. package/build/generated/releaseMetadata.d.ts.map +1 -0
  59. package/build/generated/releaseMetadata.js +10 -0
  60. package/build/generated/releaseMetadata.js.map +1 -0
  61. package/build/index.d.ts +1 -1
  62. package/build/index.d.ts.map +1 -1
  63. package/build/index.js +10 -2
  64. package/build/index.js.map +1 -1
  65. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  66. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  67. package/build/ota-server/MentraOtaServer.types.js +2 -0
  68. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  69. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  70. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  71. package/build/ota-server/MentraOtaServerModule.js +3 -0
  72. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  73. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  74. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  75. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  76. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  77. package/build/ota-server/index.d.ts +3 -0
  78. package/build/ota-server/index.d.ts.map +1 -0
  79. package/build/ota-server/index.js +3 -0
  80. package/build/ota-server/index.js.map +1 -0
  81. package/expo-module.config.json +3 -2
  82. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  83. package/ios/BluetoothSdkModule.swift +16 -2
  84. package/ios/LocalIPv4.swift +150 -0
  85. package/ios/LocalOtaServer.swift +309 -0
  86. package/ios/MentraBluetoothSDK.podspec +5 -1
  87. package/ios/MentraOtaServerModule.swift +147 -0
  88. package/ios/MentraPhotoReceiverModule.swift +1 -81
  89. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  90. package/ios/Source/Bridge.swift +43 -4
  91. package/ios/Source/DeviceManager.swift +154 -19
  92. package/ios/Source/DeviceStore.swift +2 -0
  93. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  94. package/ios/Source/MentraBluetoothSDK.swift +74 -6
  95. package/ios/Source/OtaManifest.swift +5 -7
  96. package/ios/Source/audio/AudioModels.swift +17 -0
  97. package/ios/Source/camera/CameraModels.swift +1 -0
  98. package/ios/Source/events/BluetoothEvents.swift +27 -0
  99. package/ios/Source/sgcs/MentraLive.swift +469 -77
  100. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  101. package/ios/Source/sgcs/SGCManager.swift +11 -0
  102. package/ios/Source/status/DeviceStatus.swift +23 -0
  103. package/ios/Source/types/DeviceModels.swift +23 -2
  104. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  105. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +104 -0
  106. package/package.json +5 -1
  107. package/plugin/build/withIos.d.ts +3 -1
  108. package/plugin/build/withIos.js +22 -10
  109. package/scripts/verify-release-package.mjs +34 -0
  110. package/scripts/write-release-metadata.mjs +154 -0
  111. package/scripts/write-release-metadata.test.mjs +29 -0
  112. package/src/BluetoothSdk.types.ts +69 -3
  113. package/src/_internal.ts +2 -0
  114. package/src/_private/BluetoothSdkModule.ts +5 -5
  115. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  116. package/src/_private/sdkOtaManifest.ts +5 -17
  117. package/src/generated/releaseMetadata.ts +20 -0
  118. package/src/index.ts +19 -2
  119. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  120. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  121. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  122. package/src/ota-server/index.ts +2 -0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import {readFileSync} from "node:fs"
3
+ import path from "node:path"
4
+
5
+ function option(name) {
6
+ const index = process.argv.indexOf(`--${name}`)
7
+ if (index < 0 || !process.argv[index + 1]) throw new Error(`Missing --${name}`)
8
+ return process.argv[index + 1]
9
+ }
10
+
11
+ const packageRoot = path.resolve(option("package-root"))
12
+ const releaseIdentity = option("release-identity")
13
+ const otaManifestUrl = option("ota-manifest-url")
14
+ const otaManifestSha256 = option("ota-manifest-sha256")
15
+ const packageJson = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"))
16
+ if (packageJson.version !== releaseIdentity) {
17
+ throw new Error(`Packed SDK version ${JSON.stringify(packageJson.version)} does not match ${releaseIdentity}`)
18
+ }
19
+
20
+ const files = [
21
+ "src/generated/releaseMetadata.ts",
22
+ "build/generated/releaseMetadata.js",
23
+ "android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt",
24
+ "ios/Source/GeneratedReleaseMetadata.swift",
25
+ ]
26
+ for (const relativePath of files) {
27
+ const contents = readFileSync(path.join(packageRoot, relativePath), "utf8")
28
+ if (!contents.includes(otaManifestUrl)) throw new Error(`${relativePath} does not contain the OTA manifest URL`)
29
+ if (!contents.includes(otaManifestSha256))
30
+ throw new Error(`${relativePath} does not contain the OTA manifest SHA-256`)
31
+ if (!contents.includes(releaseIdentity)) throw new Error(`${relativePath} does not contain the release identity`)
32
+ }
33
+
34
+ console.log(`Verified packed Bluetooth SDK ${releaseIdentity} metadata across JavaScript, Android, and iOS`)
@@ -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,23 @@ export type PairFailureEvent = {
648
650
  error: string
649
651
  }
650
652
 
653
+ export type PairingInfoEvent = {
654
+ had_previous_bond: boolean
655
+ pairing_code?: string
656
+ classic_bond_ready?: boolean
657
+ secure_pairing_capable?: boolean
658
+ protocol_version?: number
659
+ }
660
+
661
+ export type EnteringPairingModeEvent = {
662
+ window_ms: number
663
+ reason?: string
664
+ }
665
+
666
+ export type OwnerReplacedEvent = {
667
+ reason: string
668
+ }
669
+
651
670
  export type AudioPairingNeededEvent = {
652
671
  type: "audio_pairing_needed"
653
672
  deviceName: string
@@ -701,6 +720,17 @@ export type MicLc3Event = {
701
720
  voiceActivityDetectionEnabled: boolean
702
721
  }
703
722
 
723
+ /** Native glasses-microphone diagnostics emitted when the SDK detects a transport or decode issue. */
724
+ export type MicHealthEvent = {
725
+ type: "mic_health"
726
+ reason: "sequence_gap" | "decode_failure"
727
+ sequenceGapEvents: number
728
+ decodeFailures: number
729
+ lastLc3ReceivedAt?: number
730
+ lastPcmProducedAt?: number
731
+ timestamp: number
732
+ }
733
+
704
734
  export type StreamStatusLifecycleState = "initializing" | "streaming" | "stopping" | "stopped"
705
735
  export type StreamStatusReconnectState = "reconnecting" | "reconnected" | "reconnect_failed"
706
736
  export type StreamStatusState = StreamStatusLifecycleState | StreamStatusReconnectState | "error"
@@ -895,6 +925,9 @@ export type BluetoothSdkModuleEvents = {
895
925
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void
896
926
  settings_ack: (event: SettingsAckEvent) => void
897
927
  pair_failure: (event: PairFailureEvent) => void
928
+ pairing_info: (event: PairingInfoEvent) => void
929
+ entering_pairing_mode: (event: EnteringPairingModeEvent) => void
930
+ owner_replaced: (event: OwnerReplacedEvent) => void
898
931
  audio_pairing_needed: (event: AudioPairingNeededEvent) => void
899
932
  audio_connected: (event: AudioConnectedEvent) => void
900
933
  audio_disconnected: (event: AudioDisconnectedEvent) => void
@@ -903,6 +936,7 @@ export type BluetoothSdkModuleEvents = {
903
936
  ws_bin: (event: WsBinEvent) => void
904
937
  mic_pcm: (event: MicPcmEvent) => void
905
938
  mic_lc3: (event: MicLc3Event) => void
939
+ mic_health: (event: MicHealthEvent) => void
906
940
  stream_status: (event: StreamStatusEvent) => void
907
941
  keep_alive_ack: (event: KeepAliveAckEvent) => void
908
942
  mtk_update_complete: (event: MtkUpdateCompleteEvent) => void
@@ -999,11 +1033,15 @@ export type BluetoothSdkEventMap = {
999
1033
  rgb_led_control_response: RgbLedControlResponseEvent
1000
1034
  settings_ack: SettingsAckEvent
1001
1035
  pair_failure: PairFailureEvent
1036
+ pairing_info: PairingInfoEvent
1037
+ entering_pairing_mode: EnteringPairingModeEvent
1038
+ owner_replaced: OwnerReplacedEvent
1002
1039
  audio_pairing_needed: AudioPairingNeededEvent
1003
1040
  audio_connected: AudioConnectedEvent
1004
1041
  audio_disconnected: AudioDisconnectedEvent
1005
1042
  mic_pcm: MicPcmEvent
1006
1043
  mic_lc3: MicLc3Event
1044
+ mic_health: MicHealthEvent
1007
1045
  stream_status: StreamStatusEvent
1008
1046
  ota_start_ack: OtaStartAckEvent
1009
1047
  ota_status: OtaStatusEvent
@@ -1056,6 +1094,8 @@ export interface BluetoothSdkPublicModule {
1056
1094
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
1057
1095
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
1058
1096
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
1097
+ /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
1098
+ setWifiAdbState(enabled: boolean): Promise<void>
1059
1099
 
1060
1100
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
1061
1101
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
@@ -1110,6 +1150,8 @@ export interface BluetoothSdkPublicModule {
1110
1150
  webhookUrl?: string,
1111
1151
  authToken?: string,
1112
1152
  ): Promise<VideoRecordingStoppedStatusEvent>
1153
+ /** Query the glasses for the current recording state and elapsed duration. */
1154
+ queryVideoRecordingStatus(requestId: string): Promise<VideoRecordingStatusEvent>
1113
1155
 
1114
1156
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
1115
1157
  stopStream(): Promise<StreamStatusEvent>
@@ -1131,14 +1173,27 @@ export interface BluetoothSdkPublicModule {
1131
1173
  ): Promise<RgbLedControlSuccessResponseEvent>
1132
1174
 
1133
1175
  requestVersionInfo(): Promise<VersionInfoResult>
1176
+ /**
1177
+ * Select the OTA manifest used by subsequent update checks and installs.
1178
+ * The URL may point at Mentra's hosted manifest or any customer-controlled HTTP(S) server.
1179
+ */
1180
+ setOtaVersionUrl(otaVersionUrl: string): void
1181
+ /** Return the configured or release-embedded OTA manifest URL. Rejects when a source build is unconfigured. */
1182
+ getOtaVersionUrl(): string
1134
1183
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
1135
1184
  checkForOtaUpdate(): Promise<boolean>
1136
- /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
1137
- startOtaUpdate(): Promise<OtaStartAckEvent>
1185
+ /** Start OTA from the configured or explicitly supplied manifest URL. */
1186
+ startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
1138
1187
  startAr99OtaFromFile(path: string): Promise<boolean>
1139
1188
  cancelAr99Ota(): Promise<void>
1140
1189
  sendAr99FactoryReset(): Promise<void>
1141
- buildAr99OtaSignature(secret: string, appName: string, currentVersion: string, serialNumber: string, nonce: string): string
1190
+ buildAr99OtaSignature(
1191
+ secret: string,
1192
+ appName: string,
1193
+ currentVersion: string,
1194
+ serialNumber: string,
1195
+ nonce: string,
1196
+ ): string
1142
1197
 
1143
1198
  // // stt commands (MOVE TO CRUST)
1144
1199
  // setSttModelDetails(path: string, languageCode: string): Promise<void>
@@ -1201,6 +1256,8 @@ export interface OtaUpdateInfo {
1201
1256
  updates: string[] // ["apk", "mtk", "bes"]
1202
1257
  totalSize: number
1203
1258
  cacheReady?: boolean
1259
+ /** Exact BES target selected from the OTA manifest, when a BES step is pending. */
1260
+ besVersion?: string
1204
1261
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
1205
1262
  isDowngrade?: boolean
1206
1263
  }
@@ -1231,6 +1288,7 @@ export interface GlassesStatus {
1231
1288
  besFirmwareVersion: string
1232
1289
  mtkFirmwareVersion: string
1233
1290
  bluetoothMacAddress: string
1291
+ wifiMacAddress: string
1234
1292
  leftMacAddress: string
1235
1293
  rightMacAddress: string
1236
1294
  buildNumber: string
@@ -1238,6 +1296,8 @@ export interface GlassesStatus {
1238
1296
  systemTimeMs?: number
1239
1297
  otaVersionUrl: string
1240
1298
  appVersion: string
1299
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
1300
+ hotspotOtaVersion: number
1241
1301
  bluetoothName: string
1242
1302
  serialNumber: string
1243
1303
  style: string
@@ -1301,6 +1361,12 @@ export interface Device {
1301
1361
  * appear in a later scan update when the platform reports RSSI metadata.
1302
1362
  */
1303
1363
  rssi?: number
1364
+ /** Mentra Live: unit is currently in pairing mode (adv flag). */
1365
+ pairingMode?: boolean
1366
+ /** Mentra Live: four-character hex spoken pairing code when available. */
1367
+ pairingCode?: string
1368
+ /** Mentra Live: advertisement carries the secure-pairing capability trailer. */
1369
+ securePairingCapable?: boolean
1304
1370
  }
1305
1371
 
1306
1372
  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.10",
16
+ "releaseSetId": "mentra-3.1.0-dev.10",
17
+ "sourceCommit": "64cf83b23a71e70261571a5f4401d1a7538f1852",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.10/mentra-live-ota-3.1.0-dev.10.json",
19
+ "otaManifestSha256": "214d92c8894033eaab276e668cb553c79990005d3330d23aaf499a514dc08cff"
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"