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

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 (144) hide show
  1. package/README.md +67 -25
  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/GeneratedChangelogCatalog.kt +7 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -8
  12. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  13. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  14. package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  22. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1393 -284
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  29. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  30. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  31. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  32. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  38. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  39. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  40. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  41. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  42. package/build/BluetoothSdk.types.d.ts +84 -2
  43. package/build/BluetoothSdk.types.d.ts.map +1 -1
  44. package/build/BluetoothSdk.types.js.map +1 -1
  45. package/build/_internal.d.ts +2 -0
  46. package/build/_internal.d.ts.map +1 -1
  47. package/build/_internal.js +2 -0
  48. package/build/_internal.js.map +1 -1
  49. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  50. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  51. package/build/_private/BluetoothSdkModule.js +1 -1
  52. package/build/_private/BluetoothSdkModule.js.map +1 -1
  53. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  54. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  55. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  56. package/build/_private/sdkOtaManifest.d.ts +3 -12
  57. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  58. package/build/_private/sdkOtaManifest.js +5 -17
  59. package/build/_private/sdkOtaManifest.js.map +1 -1
  60. package/build/changelogs.d.ts +7 -0
  61. package/build/changelogs.d.ts.map +1 -0
  62. package/build/changelogs.js +44 -0
  63. package/build/changelogs.js.map +1 -0
  64. package/build/generated/changelogCatalog.d.ts +6 -0
  65. package/build/generated/changelogCatalog.d.ts.map +1 -0
  66. package/build/generated/changelogCatalog.js +8 -0
  67. package/build/generated/changelogCatalog.js.map +1 -0
  68. package/build/generated/releaseMetadata.d.ts +12 -0
  69. package/build/generated/releaseMetadata.d.ts.map +1 -0
  70. package/build/generated/releaseMetadata.js +10 -0
  71. package/build/generated/releaseMetadata.js.map +1 -0
  72. package/build/index.d.ts +1 -1
  73. package/build/index.d.ts.map +1 -1
  74. package/build/index.js +24 -2
  75. package/build/index.js.map +1 -1
  76. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  77. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  78. package/build/ota-server/MentraOtaServer.types.js +2 -0
  79. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  80. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  81. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  82. package/build/ota-server/MentraOtaServerModule.js +3 -0
  83. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  84. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  85. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  86. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  87. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  88. package/build/ota-server/index.d.ts +3 -0
  89. package/build/ota-server/index.d.ts.map +1 -0
  90. package/build/ota-server/index.js +3 -0
  91. package/build/ota-server/index.js.map +1 -0
  92. package/build/ota-transport/index.d.ts +67 -0
  93. package/build/ota-transport/index.d.ts.map +1 -0
  94. package/build/ota-transport/index.js +58 -0
  95. package/build/ota-transport/index.js.map +1 -0
  96. package/expo-module.config.json +3 -2
  97. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  98. package/ios/BluetoothSdkModule.swift +16 -2
  99. package/ios/LocalIPv4.swift +150 -0
  100. package/ios/LocalOtaServer.swift +309 -0
  101. package/ios/MentraBluetoothSDK.podspec +5 -1
  102. package/ios/MentraOtaServerModule.swift +147 -0
  103. package/ios/MentraPhotoReceiverModule.swift +1 -81
  104. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  105. package/ios/Source/Bridge.swift +43 -4
  106. package/ios/Source/DeviceManager.swift +154 -19
  107. package/ios/Source/DeviceStore.swift +2 -0
  108. package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
  109. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  110. package/ios/Source/MentraBluetoothSDK.swift +91 -6
  111. package/ios/Source/OtaManifest.swift +5 -7
  112. package/ios/Source/ReleaseChangelog.swift +65 -0
  113. package/ios/Source/audio/AudioModels.swift +17 -0
  114. package/ios/Source/camera/CameraModels.swift +1 -0
  115. package/ios/Source/events/BluetoothEvents.swift +27 -0
  116. package/ios/Source/sgcs/MentraLive.swift +512 -78
  117. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  118. package/ios/Source/sgcs/SGCManager.swift +11 -0
  119. package/ios/Source/status/DeviceStatus.swift +23 -0
  120. package/ios/Source/types/DeviceModels.swift +23 -2
  121. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  122. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  123. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  124. package/package.json +10 -1
  125. package/plugin/build/withIos.d.ts +3 -1
  126. package/plugin/build/withIos.js +22 -10
  127. package/scripts/public-ota-api.test.mjs +37 -0
  128. package/scripts/verify-release-package.mjs +34 -0
  129. package/scripts/write-release-metadata.mjs +154 -0
  130. package/scripts/write-release-metadata.test.mjs +29 -0
  131. package/src/BluetoothSdk.types.ts +97 -3
  132. package/src/_internal.ts +2 -0
  133. package/src/_private/BluetoothSdkModule.ts +5 -5
  134. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  135. package/src/_private/sdkOtaManifest.ts +5 -17
  136. package/src/changelogs.ts +44 -0
  137. package/src/generated/changelogCatalog.ts +9 -0
  138. package/src/generated/releaseMetadata.ts +20 -0
  139. package/src/index.ts +43 -2
  140. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  141. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  142. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  143. package/src/ota-server/index.ts +2 -0
  144. package/src/ota-transport/index.ts +116 -0
@@ -1,3 +1,5 @@
1
- import { type ConfigPlugin } from "expo/config-plugins";
1
+ import { IOSConfig, type ConfigPlugin } from "expo/config-plugins";
2
2
  import { type BluetoothSdkPluginProps } from "./index";
3
+ export declare const INFO_SDK_VERSION = "MentraBluetoothSdkVersion";
4
+ export declare function applyBluetoothSdkInfoPlist(infoPlist: IOSConfig.InfoPlist, props: BluetoothSdkPluginProps | undefined): IOSConfig.InfoPlist;
3
5
  export declare const withIosConfiguration: ConfigPlugin<BluetoothSdkPluginProps>;
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.withIosConfiguration = void 0;
6
+ exports.withIosConfiguration = exports.INFO_SDK_VERSION = void 0;
7
+ exports.applyBluetoothSdkInfoPlist = applyBluetoothSdkInfoPlist;
7
8
  const child_process_1 = require("child_process");
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
@@ -11,6 +12,7 @@ const config_plugins_1 = require("expo/config-plugins");
11
12
  const BLUETOOTH_SDK_EXPO_ADAPTER_ENV = "MENTRA_BLUETOOTH_SDK_INCLUDE_EXPO_ADAPTER";
12
13
  const BLUETOOTH_SDK_EXPO_ADAPTER_LINE = `ENV['${BLUETOOTH_SDK_EXPO_ADAPTER_ENV}'] ||= '1'`;
13
14
  const INFO_ANALYTICS_DISABLED = "MentraBluetoothSdkAnalyticsDisabled";
15
+ exports.INFO_SDK_VERSION = "MentraBluetoothSdkVersion";
14
16
  const STALE_INFO_POSTHOG_API_KEY = "MentraBluetoothSdkPostHogApiKey";
15
17
  const STALE_INFO_POSTHOG_HOST = "MentraBluetoothSdkPostHogHost";
16
18
  const ensureBluetoothSdkExpoAdapterPodEnv = (podfile) => {
@@ -70,15 +72,25 @@ function resolveAnalyticsProps(props) {
70
72
  disabled,
71
73
  };
72
74
  }
73
- function withAnalyticsInfoPlist(config, props) {
75
+ function applyBluetoothSdkInfoPlist(infoPlist, props) {
76
+ const packageJson = require("../../package.json");
77
+ const sdkVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : "";
78
+ if (!sdkVersion) {
79
+ throw new Error("@mentra/bluetooth-sdk package.json is missing a version");
80
+ }
81
+ const analytics = resolveAnalyticsProps(props);
82
+ delete infoPlist[INFO_ANALYTICS_DISABLED];
83
+ delete infoPlist[STALE_INFO_POSTHOG_API_KEY];
84
+ delete infoPlist[STALE_INFO_POSTHOG_HOST];
85
+ infoPlist[exports.INFO_SDK_VERSION] = sdkVersion;
86
+ if (analytics.disabled !== undefined) {
87
+ infoPlist[INFO_ANALYTICS_DISABLED] = analytics.disabled;
88
+ }
89
+ return infoPlist;
90
+ }
91
+ function withBluetoothSdkInfoPlist(config, props) {
74
92
  return (0, config_plugins_1.withInfoPlist)(config, (config) => {
75
- const analytics = resolveAnalyticsProps(props);
76
- delete config.modResults[INFO_ANALYTICS_DISABLED];
77
- delete config.modResults[STALE_INFO_POSTHOG_API_KEY];
78
- delete config.modResults[STALE_INFO_POSTHOG_HOST];
79
- if (analytics.disabled !== undefined) {
80
- config.modResults[INFO_ANALYTICS_DISABLED] = analytics.disabled;
81
- }
93
+ config.modResults = applyBluetoothSdkInfoPlist(config.modResults, props);
82
94
  return config;
83
95
  });
84
96
  }
@@ -87,7 +99,7 @@ const withIosConfiguration = (config, props) => {
87
99
  config.modResults.contents = ensureBluetoothSdkExpoAdapterPodEnv(config.modResults.contents);
88
100
  return config;
89
101
  });
90
- config = withAnalyticsInfoPlist(config, props);
102
+ config = withBluetoothSdkInfoPlist(config, props);
91
103
  if (props?.node) {
92
104
  config = withXcodeEnvLocal(config);
93
105
  }
@@ -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
+ })
@@ -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,12 +1033,20 @@ 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
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
1008
1050
  ota_start_ack: OtaStartAckEvent
1009
1051
  ota_status: OtaStatusEvent
1010
1052
  ar99_ota_status: Ar99OtaStatusEvent
@@ -1030,6 +1072,15 @@ export interface BluetoothSdkPublicModule {
1030
1072
  listener: BluetoothSdkEventListener<EventName>,
1031
1073
  ): BluetoothSdkSubscription
1032
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
+
1033
1084
  getDefaultDevice(): Promise<Device | null>
1034
1085
  setDefaultDevice(device: Device | null): Promise<void>
1035
1086
  clearDefaultDevice(): Promise<void>
@@ -1051,11 +1102,17 @@ export interface BluetoothSdkPublicModule {
1051
1102
  setHeadUpAngle(angleDegrees: number): Promise<void>
1052
1103
  setImuEnabled(enabled: boolean): Promise<void>
1053
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>
1054
1107
 
1055
1108
  requestWifiScan(): Promise<WifiSearchResult[]>
1056
1109
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
1057
1110
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
1058
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>
1114
+ /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
1115
+ setWifiAdbState(enabled: boolean): Promise<void>
1059
1116
 
1060
1117
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
1061
1118
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
@@ -1110,6 +1167,8 @@ export interface BluetoothSdkPublicModule {
1110
1167
  webhookUrl?: string,
1111
1168
  authToken?: string,
1112
1169
  ): Promise<VideoRecordingStoppedStatusEvent>
1170
+ /** Query the glasses for the current recording state and elapsed duration. */
1171
+ queryVideoRecordingStatus(requestId: string): Promise<VideoRecordingStatusEvent>
1113
1172
 
1114
1173
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
1115
1174
  stopStream(): Promise<StreamStatusEvent>
@@ -1131,14 +1190,31 @@ export interface BluetoothSdkPublicModule {
1131
1190
  ): Promise<RgbLedControlSuccessResponseEvent>
1132
1191
 
1133
1192
  requestVersionInfo(): Promise<VersionInfoResult>
1193
+ /**
1194
+ * Select the OTA manifest used by subsequent update checks and installs.
1195
+ * The URL may point at Mentra's hosted manifest or any customer-controlled HTTP(S) server.
1196
+ */
1197
+ setOtaVersionUrl(otaVersionUrl: string): void
1198
+ /** Return the configured or release-embedded OTA manifest URL. Rejects when a source build is unconfigured. */
1199
+ getOtaVersionUrl(): string
1134
1200
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
1135
1201
  checkForOtaUpdate(): Promise<boolean>
1136
- /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
1137
- startOtaUpdate(): Promise<OtaStartAckEvent>
1202
+ /** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
1203
+ getReleaseChangelogs(fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[]
1204
+ /** Start OTA from the configured or explicitly supplied manifest URL. */
1205
+ startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
1206
+ /** Query the active OTA session and return the correlated status response. */
1207
+ queryOtaStatus(): Promise<OtaQueryResult>
1138
1208
  startAr99OtaFromFile(path: string): Promise<boolean>
1139
1209
  cancelAr99Ota(): Promise<void>
1140
1210
  sendAr99FactoryReset(): Promise<void>
1141
- buildAr99OtaSignature(secret: string, appName: string, currentVersion: string, serialNumber: string, nonce: string): string
1211
+ buildAr99OtaSignature(
1212
+ secret: string,
1213
+ appName: string,
1214
+ currentVersion: string,
1215
+ serialNumber: string,
1216
+ nonce: string,
1217
+ ): string
1142
1218
 
1143
1219
  // // stt commands (MOVE TO CRUST)
1144
1220
  // setSttModelDetails(path: string, languageCode: string): Promise<void>
@@ -1201,10 +1277,19 @@ export interface OtaUpdateInfo {
1201
1277
  updates: string[] // ["apk", "mtk", "bes"]
1202
1278
  totalSize: number
1203
1279
  cacheReady?: boolean
1280
+ /** Exact BES target selected from the OTA manifest, when a BES step is pending. */
1281
+ besVersion?: string
1204
1282
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
1205
1283
  isDowngrade?: boolean
1206
1284
  }
1207
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
+
1208
1293
  export interface OtaProgress {
1209
1294
  stage: OtaStage
1210
1295
  status: OtaProgressStatus
@@ -1231,6 +1316,7 @@ export interface GlassesStatus {
1231
1316
  besFirmwareVersion: string
1232
1317
  mtkFirmwareVersion: string
1233
1318
  bluetoothMacAddress: string
1319
+ wifiMacAddress: string
1234
1320
  leftMacAddress: string
1235
1321
  rightMacAddress: string
1236
1322
  buildNumber: string
@@ -1238,6 +1324,8 @@ export interface GlassesStatus {
1238
1324
  systemTimeMs?: number
1239
1325
  otaVersionUrl: string
1240
1326
  appVersion: string
1327
+ /** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
1328
+ hotspotOtaVersion: number
1241
1329
  bluetoothName: string
1242
1330
  serialNumber: string
1243
1331
  style: string
@@ -1301,6 +1389,12 @@ export interface Device {
1301
1389
  * appear in a later scan update when the platform reports RSSI metadata.
1302
1390
  */
1303
1391
  rssi?: number
1392
+ /** Mentra Live: unit is currently in pairing mode (adv flag). */
1393
+ pairingMode?: boolean
1394
+ /** Mentra Live: four-character hex spoken pairing code when available. */
1395
+ pairingCode?: string
1396
+ /** Mentra Live: advertisement carries the secure-pairing capability trailer. */
1397
+ securePairingCapable?: boolean
1304
1398
  }
1305
1399
 
1306
1400
  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
  }