@mentra/bluetooth-sdk 3.1.0-dev.10 → 3.1.0-dev.101

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 (45) hide show
  1. package/README.md +16 -21
  2. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  4. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +15 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +74 -25
  7. package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  8. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +58 -0
  9. package/build/BluetoothSdk.types.d.ts +26 -0
  10. package/build/BluetoothSdk.types.d.ts.map +1 -1
  11. package/build/BluetoothSdk.types.js.map +1 -1
  12. package/build/changelogs.d.ts +7 -0
  13. package/build/changelogs.d.ts.map +1 -0
  14. package/build/changelogs.js +44 -0
  15. package/build/changelogs.js.map +1 -0
  16. package/build/generated/changelogCatalog.d.ts +6 -0
  17. package/build/generated/changelogCatalog.d.ts.map +1 -0
  18. package/build/generated/changelogCatalog.js +8 -0
  19. package/build/generated/changelogCatalog.js.map +1 -0
  20. package/build/generated/releaseMetadata.js +5 -5
  21. package/build/generated/releaseMetadata.js.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +14 -0
  25. package/build/index.js.map +1 -1
  26. package/build/ota-transport/index.d.ts +67 -0
  27. package/build/ota-transport/index.d.ts.map +1 -0
  28. package/build/ota-transport/index.js +58 -0
  29. package/build/ota-transport/index.js.map +1 -0
  30. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  31. package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
  32. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  33. package/ios/Source/MentraBluetoothSDK.swift +17 -0
  34. package/ios/Source/ReleaseChangelog.swift +65 -0
  35. package/ios/Source/sgcs/MentraLive.swift +56 -14
  36. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +56 -0
  37. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  38. package/package.json +6 -1
  39. package/scripts/public-ota-api.test.mjs +37 -0
  40. package/src/BluetoothSdk.types.ts +28 -0
  41. package/src/changelogs.ts +44 -0
  42. package/src/generated/changelogCatalog.ts +9 -0
  43. package/src/generated/releaseMetadata.ts +5 -5
  44. package/src/index.ts +24 -0
  45. package/src/ota-transport/index.ts +116 -0
@@ -0,0 +1,13 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ final class ReleaseChangelogTests: XCTestCase {
5
+ func testIncludesTargetNotesForTransitionWithinOneReleaseTrain() throws {
6
+ let changelogs = try ReleaseChangelogCatalog.select(
7
+ fromVersion: "3.1.0-dev.2",
8
+ toVersion: "3.1.0-beta.8"
9
+ )
10
+
11
+ XCTAssertEqual(changelogs.map(\.version), ["3.1.0"])
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.1.0-dev.10",
3
+ "version": "3.1.0-dev.101",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -32,6 +32,11 @@
32
32
  "types": "./build/photo-receiver/index.d.ts",
33
33
  "default": "./build/photo-receiver/index.js"
34
34
  },
35
+ "./ota-transport": {
36
+ "react-native": "./src/ota-transport/index.ts",
37
+ "types": "./build/ota-transport/index.d.ts",
38
+ "default": "./build/ota-transport/index.js"
39
+ },
35
40
  "./debug": {
36
41
  "react-native": "./src/debug.ts",
37
42
  "types": "./build/debug.d.ts",
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict"
2
+ import {readFileSync} from "node:fs"
3
+ import path from "node:path"
4
+ import test from "node:test"
5
+ import {fileURLToPath} from "node:url"
6
+
7
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
8
+
9
+ test("publishes the supported OTA transport entrypoint", () => {
10
+ const manifest = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"))
11
+ assert.deepEqual(manifest.exports["./ota-transport"], {
12
+ "react-native": "./src/ota-transport/index.ts",
13
+ "types": "./build/ota-transport/index.d.ts",
14
+ "default": "./build/ota-transport/index.js",
15
+ })
16
+
17
+ const source = readFileSync(path.join(packageRoot, "src/ota-transport/index.ts"), "utf8")
18
+ assert.match(source, /export const otaLocalNetwork/)
19
+ assert.match(source, /export const otaServer/)
20
+ assert.doesNotMatch(source, /export \{default as Mentra(LocalNetwork|OtaServer)/)
21
+ })
22
+
23
+ test("keeps restart signals and correlated OTA status on the public root", () => {
24
+ const source = readFileSync(path.join(packageRoot, "src/index.ts"), "utf8")
25
+ for (const required of [
26
+ '"glasses_session_changed"',
27
+ '"mtk_update_complete"',
28
+ "queryOtaStatus",
29
+ "subscribeGlassesStatus",
30
+ "subscribeBluetoothStatus",
31
+ "setSystemTime",
32
+ "ping",
33
+ ]) {
34
+ assert.ok(source.includes(required), `missing public OTA primitive: ${required}`)
35
+ }
36
+ assert.match(source, /Unsupported BluetoothSdk event/)
37
+ })
@@ -1043,6 +1043,10 @@ export type BluetoothSdkEventMap = {
1043
1043
  mic_lc3: MicLc3Event
1044
1044
  mic_health: MicHealthEvent
1045
1045
  stream_status: StreamStatusEvent
1046
+ /** Mentra Live MTK updater completed and the glasses are about to restart. */
1047
+ mtk_update_complete: MtkUpdateCompleteEvent
1048
+ /** The ASG process restarted while the BES kept the BLE connection alive. */
1049
+ glasses_session_changed: GlassesSessionChangedEvent
1046
1050
  ota_start_ack: OtaStartAckEvent
1047
1051
  ota_status: OtaStatusEvent
1048
1052
  ar99_ota_status: Ar99OtaStatusEvent
@@ -1068,6 +1072,15 @@ export interface BluetoothSdkPublicModule {
1068
1072
  listener: BluetoothSdkEventListener<EventName>,
1069
1073
  ): BluetoothSdkSubscription
1070
1074
 
1075
+ /** Read an immutable snapshot of the current glasses state. */
1076
+ getGlassesStatus(): Promise<PublicGlassesStatus>
1077
+ /** Read an immutable snapshot of the phone Bluetooth adapter state. */
1078
+ getBluetoothStatus(): Promise<PublicBluetoothStatus>
1079
+ /** Observe immutable glasses-state patches. Returns an unsubscribe function. */
1080
+ subscribeGlassesStatus(listener: (changed: Partial<PublicGlassesStatus>) => void): () => void
1081
+ /** Observe immutable Bluetooth-state patches. Returns an unsubscribe function. */
1082
+ subscribeBluetoothStatus(listener: (changed: Partial<PublicBluetoothStatus>) => void): () => void
1083
+
1071
1084
  getDefaultDevice(): Promise<Device | null>
1072
1085
  setDefaultDevice(device: Device | null): Promise<void>
1073
1086
  clearDefaultDevice(): Promise<void>
@@ -1089,11 +1102,15 @@ export interface BluetoothSdkPublicModule {
1089
1102
  setHeadUpAngle(angleDegrees: number): Promise<void>
1090
1103
  setImuEnabled(enabled: boolean): Promise<void>
1091
1104
  setScreenDisabled(disabled: boolean): Promise<void>
1105
+ /** Keep legacy Mentra Live OTA sessions awake. Modern sessions normally do not require this. */
1106
+ ping(): Promise<void>
1092
1107
 
1093
1108
  requestWifiScan(): Promise<WifiSearchResult[]>
1094
1109
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
1095
1110
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
1096
1111
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
1112
+ /** Set the glasses clock from the phone after an OTA clock-skew failure. */
1113
+ setSystemTime(timestampMs: number): Promise<void>
1097
1114
  /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
1098
1115
  setWifiAdbState(enabled: boolean): Promise<void>
1099
1116
 
@@ -1182,8 +1199,12 @@ export interface BluetoothSdkPublicModule {
1182
1199
  getOtaVersionUrl(): string
1183
1200
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
1184
1201
  checkForOtaUpdate(): Promise<boolean>
1202
+ /** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
1203
+ getReleaseChangelogs(fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[]
1185
1204
  /** Start OTA from the configured or explicitly supplied manifest URL. */
1186
1205
  startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
1206
+ /** Query the active OTA session and return the correlated status response. */
1207
+ queryOtaStatus(): Promise<OtaQueryResult>
1187
1208
  startAr99OtaFromFile(path: string): Promise<boolean>
1188
1209
  cancelAr99Ota(): Promise<void>
1189
1210
  sendAr99FactoryReset(): Promise<void>
@@ -1262,6 +1283,13 @@ export interface OtaUpdateInfo {
1262
1283
  isDowngrade?: boolean
1263
1284
  }
1264
1285
 
1286
+ export interface ReleaseChangelog {
1287
+ /** Base production version, for example `3.1.0`. */
1288
+ version: string
1289
+ /** Markdown body authored in `/changelogs/<version>.md`. */
1290
+ markdown: string
1291
+ }
1292
+
1265
1293
  export interface OtaProgress {
1266
1294
  stage: OtaStage
1267
1295
  status: OtaProgressStatus
@@ -0,0 +1,44 @@
1
+ import type {ReleaseChangelog} from "./BluetoothSdk.types"
2
+ import {GENERATED_RELEASE_CHANGELOGS} from "./generated/changelogCatalog"
3
+ import {BLUETOOTH_SDK_RELEASE_METADATA} from "./generated/releaseMetadata"
4
+
5
+ const BASE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+].*)?$/
6
+
7
+ function baseVersion(version: string, label: string): string {
8
+ const match = BASE_VERSION_PATTERN.exec(version.trim())
9
+ if (!match) throw new Error(`${label} must be a semantic version such as 3.1.0, 3.1.0-dev.4, or 3.1.0-beta.2`)
10
+ return `${match[1]}.${match[2]}.${match[3]}`
11
+ }
12
+
13
+ function compareVersions(left: string, right: string): number {
14
+ const a = left.split(".").map(Number)
15
+ const b = right.split(".").map(Number)
16
+ for (let index = 0; index < 3; index += 1) {
17
+ if (a[index] !== b[index]) return a[index] - b[index]
18
+ }
19
+ return 0
20
+ }
21
+
22
+ /**
23
+ * Return authored release notes crossed between two product versions, newest first.
24
+ * The target release is always included, including transitions within one base train.
25
+ */
26
+ export function getReleaseChangelogs(fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[] {
27
+ const fallbackTarget = BLUETOOTH_SDK_RELEASE_METADATA.familyBaseVersion ?? GENERATED_RELEASE_CHANGELOGS[0]?.version
28
+ if (!fallbackTarget && !toVersion) return []
29
+ const target = baseVersion(toVersion ?? fallbackTarget, "toVersion")
30
+ if (!GENERATED_RELEASE_CHANGELOGS.some(({version}) => version === target)) {
31
+ throw new Error(`No changelog is bundled for target version ${target}`)
32
+ }
33
+ if (!fromVersion) {
34
+ return GENERATED_RELEASE_CHANGELOGS.filter(({version}) => version === target).map((entry) => ({...entry}))
35
+ }
36
+
37
+ const source = baseVersion(fromVersion, "fromVersion")
38
+ const direction = compareVersions(target, source)
39
+ return GENERATED_RELEASE_CHANGELOGS.filter(({version}) => {
40
+ if (direction === 0) return version === target
41
+ if (direction > 0) return compareVersions(version, source) > 0 && compareVersions(version, target) <= 0
42
+ return compareVersions(version, source) < 0 && compareVersions(version, target) >= 0
43
+ }).map((entry) => ({...entry}))
44
+ }
@@ -0,0 +1,9 @@
1
+ /** Generated from /changelogs. Do not edit directly. */
2
+ export const GENERATED_RELEASE_CHANGELOGS = Object.freeze(
3
+ [
4
+ {
5
+ "version": "3.1.0",
6
+ "markdown": "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- MentraOS, Mentra Engine, the Bluetooth SDK, and the glasses client now share one coordinated release version.\n- Mentra Live updates can continue across APK, system, and firmware restarts without asking the user to start the same update again.\n- Bluetooth photo capture and transfer diagnostics are more precise and less disruptive to normal glasses traffic."
7
+ }
8
+ ] as const,
9
+ )
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.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"
15
+ "releaseIdentity": "3.1.0-dev.101",
16
+ "releaseSetId": "mentra-3.1.0-dev.101",
17
+ "sourceCommit": "7cbd0579aa228fcfd4d36adda6de1150c3a43647",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.1.0/mentra-live-ota-3.1.0-dev.101.json",
19
+ "otaManifestSha256": "37c1d4399b7d7a9bf973138bca4035e930df80cf67da828a3e54049ed0034802"
20
20
  })
package/src/index.ts CHANGED
@@ -3,8 +3,11 @@ import type {
3
3
  BluetoothSdkEventListener,
4
4
  BluetoothSdkEventName,
5
5
  BluetoothSdkPublicModule,
6
+ PublicBluetoothStatus,
7
+ PublicGlassesStatus,
6
8
  VideoRecordingDefaults,
7
9
  } from "./BluetoothSdk.types"
10
+ import {getReleaseChangelogs} from "./changelogs"
8
11
 
9
12
  const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
10
13
  "log",
@@ -46,6 +49,8 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
46
49
  "mic_lc3",
47
50
  "mic_health",
48
51
  "stream_status",
52
+ "mtk_update_complete",
53
+ "glasses_session_changed",
49
54
  "ota_start_ack",
50
55
  "ota_status",
51
56
  "ar99_ota_status",
@@ -64,6 +69,10 @@ const startOtaUpdate: BluetoothSdkPublicModule["startOtaUpdate"] = (otaVersionUr
64
69
  return PrivateBluetoothSdkModule.startOtaUpdate(otaVersionUrl)
65
70
  }
66
71
 
72
+ const queryOtaStatus: BluetoothSdkPublicModule["queryOtaStatus"] = () => {
73
+ return PrivateBluetoothSdkModule.sendOtaQueryStatus()
74
+ }
75
+
67
76
  const bindPublicMethod = <K extends keyof BluetoothSdkPublicModule>(name: K): BluetoothSdkPublicModule[K] => {
68
77
  const method = (PrivateBluetoothSdkModule as unknown as Record<string, unknown>)[name]
69
78
  if (typeof method === "function") {
@@ -77,6 +86,12 @@ const bindPublicMethod = <K extends keyof BluetoothSdkPublicModule>(name: K): Bl
77
86
 
78
87
  export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
79
88
  addListener,
89
+ getGlassesStatus: bindPublicMethod("getGlassesStatus"),
90
+ getBluetoothStatus: bindPublicMethod("getBluetoothStatus"),
91
+ subscribeGlassesStatus: (listener: (changed: Partial<PublicGlassesStatus>) => void) =>
92
+ PrivateBluetoothSdkModule.onGlassesStatus(listener),
93
+ subscribeBluetoothStatus: (listener: (changed: Partial<PublicBluetoothStatus>) => void) =>
94
+ PrivateBluetoothSdkModule.onBluetoothStatus(listener),
80
95
  getDefaultDevice: bindPublicMethod("getDefaultDevice"),
81
96
  setDefaultDevice: bindPublicMethod("setDefaultDevice"),
82
97
  clearDefaultDevice: bindPublicMethod("clearDefaultDevice"),
@@ -95,10 +110,12 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
95
110
  setHeadUpAngle: bindPublicMethod("setHeadUpAngle"),
96
111
  setImuEnabled: bindPublicMethod("setImuEnabled"),
97
112
  setScreenDisabled: bindPublicMethod("setScreenDisabled"),
113
+ ping: bindPublicMethod("ping"),
98
114
  requestWifiScan: bindPublicMethod("requestWifiScan"),
99
115
  sendWifiCredentials: bindPublicMethod("sendWifiCredentials"),
100
116
  forgetWifiNetwork: bindPublicMethod("forgetWifiNetwork"),
101
117
  setHotspotState: bindPublicMethod("setHotspotState"),
118
+ setSystemTime: bindPublicMethod("setSystemTime"),
102
119
  setWifiAdbState: bindPublicMethod("setWifiAdbState"),
103
120
  setGalleryModeEnabled: bindPublicMethod("setGalleryModeEnabled"),
104
121
  setVoiceActivityDetectionEnabled: bindPublicMethod("setVoiceActivityDetectionEnabled"),
@@ -143,7 +160,9 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
143
160
  setOtaVersionUrl: bindPublicMethod("setOtaVersionUrl"),
144
161
  getOtaVersionUrl: bindPublicMethod("getOtaVersionUrl"),
145
162
  checkForOtaUpdate: bindPublicMethod("checkForOtaUpdate"),
163
+ getReleaseChangelogs,
146
164
  startOtaUpdate,
165
+ queryOtaStatus,
147
166
  startAr99OtaFromFile: bindPublicMethod("startAr99OtaFromFile"),
148
167
  cancelAr99Ota: bindPublicMethod("cancelAr99Ota"),
149
168
  sendAr99FactoryReset: bindPublicMethod("sendAr99FactoryReset"),
@@ -212,6 +231,7 @@ export type {
212
231
  HotspotErrorEvent,
213
232
  HotspotStatus,
214
233
  HotspotStatusChangeEvent,
234
+ GlassesSessionChangedEvent,
215
235
  KeepAliveAckEvent,
216
236
  LocalTranscriptionEvent,
217
237
  LogEvent,
@@ -228,6 +248,8 @@ export type {
228
248
  OtaStatusEvent,
229
249
  OtaQueryResult,
230
250
  OtaUpdateInfo,
251
+ ReleaseChangelog,
252
+ MtkUpdateCompleteEvent,
231
253
  PairFailureEvent,
232
254
  PairingInfoEvent,
233
255
  EnteringPairingModeEvent,
@@ -238,6 +260,8 @@ export type {
238
260
  PhotoFpsRange,
239
261
  PhotoMeteredPreview,
240
262
  PhotoMode,
263
+ PublicBluetoothStatus,
264
+ PublicGlassesStatus,
241
265
  PhotoTransferMethod,
242
266
  PhotoResponseEvent,
243
267
  PhotoRequestedCaptureConfig,
@@ -0,0 +1,116 @@
1
+ // This public adapter is the package-owned boundary over its native implementation.
2
+ // eslint-disable-next-line no-restricted-imports
3
+ import MentraLocalNetwork from "../_private/MentraLocalNetworkModule"
4
+ // eslint-disable-next-line no-restricted-imports
5
+ import MentraOtaServer from "../ota-server"
6
+
7
+ export type OtaTransportSubscription = {remove(): void}
8
+
9
+ export type OtaLocalNetworkConnection = {
10
+ connected: boolean
11
+ ssid: string
12
+ localAddress?: string
13
+ }
14
+
15
+ export type OtaLocalNetworkLostEvent = {ssid: string}
16
+
17
+ export type OtaLocalNetworkResponse = {
18
+ status: number
19
+ headers: Record<string, string>
20
+ bodyBase64: string
21
+ }
22
+
23
+ export type OtaLocalNetworkDownloadResult = {
24
+ statusCode: number
25
+ bytesWritten: number
26
+ headers: Record<string, string>
27
+ }
28
+
29
+ export type OtaLocalNetworkDownloadProgress = {
30
+ requestId: string
31
+ bytesWritten: number
32
+ contentLength: number
33
+ statusCode?: number
34
+ headers?: Record<string, string>
35
+ }
36
+
37
+ export type OtaArtifactDownloadProgress = {
38
+ destination: string
39
+ bytesWritten: number
40
+ contentLength: number
41
+ }
42
+
43
+ export type OtaServerResult = {
44
+ baseUrl: string
45
+ host: string
46
+ manifestUrl: string
47
+ port: number
48
+ }
49
+
50
+ /**
51
+ * Scoped networking for traffic that must stay on a Mentra Live hotspot while
52
+ * the phone's default route remains on cellular or another Wi-Fi network.
53
+ *
54
+ * The module is optional because iOS uses its system Wi-Fi join flow instead.
55
+ */
56
+ export const otaLocalNetwork = Object.freeze({
57
+ isAvailable: (): boolean => MentraLocalNetwork != null,
58
+ connect: async (ssid: string, password: string): Promise<OtaLocalNetworkConnection> => {
59
+ if (!MentraLocalNetwork) throw new Error("Scoped local networking is not available in this native build")
60
+ return MentraLocalNetwork.connect(ssid, password)
61
+ },
62
+ request: async (
63
+ requestId: string,
64
+ url: string,
65
+ method: string,
66
+ headers: Record<string, string>,
67
+ body: string | null,
68
+ timeoutMs: number,
69
+ ): Promise<OtaLocalNetworkResponse> => {
70
+ if (!MentraLocalNetwork) throw new Error("Scoped local networking is not available in this native build")
71
+ return MentraLocalNetwork.request(requestId, url, method, headers, body, timeoutMs)
72
+ },
73
+ download: async (
74
+ requestId: string,
75
+ url: string,
76
+ destination: string,
77
+ headers: Record<string, string>,
78
+ connectionTimeoutMs: number,
79
+ readTimeoutMs: number,
80
+ ): Promise<OtaLocalNetworkDownloadResult> => {
81
+ if (!MentraLocalNetwork) throw new Error("Scoped local networking is not available in this native build")
82
+ return MentraLocalNetwork.download(requestId, url, destination, headers, connectionTimeoutMs, readTimeoutMs)
83
+ },
84
+ cancel: async (requestId: string): Promise<void> => {
85
+ if (!MentraLocalNetwork) return
86
+ await MentraLocalNetwork.cancel(requestId)
87
+ },
88
+ disconnect: async (): Promise<void> => {
89
+ if (!MentraLocalNetwork) return
90
+ await MentraLocalNetwork.disconnect()
91
+ },
92
+ onDownloadProgress: (listener: (event: OtaLocalNetworkDownloadProgress) => void): OtaTransportSubscription => {
93
+ if (!MentraLocalNetwork) return {remove() {}}
94
+ return MentraLocalNetwork.addListener("downloadProgress", listener)
95
+ },
96
+ onNetworkLost: (listener: (event: OtaLocalNetworkLostEvent) => void): OtaTransportSubscription => {
97
+ if (!MentraLocalNetwork) return {remove() {}}
98
+ return MentraLocalNetwork.addListener("networkLost", listener)
99
+ },
100
+ })
101
+
102
+ /** Native HTTP server and artifact downloader used by the Engine OTA coordinator. */
103
+ export const otaServer = Object.freeze({
104
+ start: (
105
+ manifestJson: string,
106
+ artifactPaths: Record<string, string>,
107
+ host?: string | null,
108
+ ): Promise<OtaServerResult> => MentraOtaServer.startOtaServer(manifestJson, artifactPaths, host),
109
+ stop: (): Promise<void> => MentraOtaServer.stopOtaServer(),
110
+ waitForWifiAddress: (gateway: string, timeoutMs: number): Promise<string> =>
111
+ MentraOtaServer.waitForWifiAddress(gateway, timeoutMs),
112
+ downloadArtifact: (source: string, destination: string): Promise<{statusCode: number; bytesWritten: number}> =>
113
+ MentraOtaServer.downloadArtifact(source, destination),
114
+ onArtifactDownloadProgress: (listener: (event: OtaArtifactDownloadProgress) => void): OtaTransportSubscription =>
115
+ MentraOtaServer.addListener("artifactDownloadProgress", listener),
116
+ })