@mentra/bluetooth-sdk 3.3.0-dev.452 → 3.3.0-dev.458

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.
package/README.md CHANGED
@@ -446,9 +446,9 @@ console.log(ledAck.state)
446
446
 
447
447
  ## Dashboard Content
448
448
 
449
- `setDashboardContent(content)` keeps the standard `$TIME12$ $DATE$ $GBATT$
450
- $CONNECTION_STATUS$` status header and places non-empty content below it after a
451
- blank line. Pass the exact empty string to reset the dashboard to the status
449
+ `setDashboardContent(content)` keeps the standard `$TIME12$ $DATE$ $GBATT$`
450
+ status header and places non-empty content below it after a blank line. Pass the
451
+ exact empty string to reset the dashboard to the status
452
452
  header only. The template is held in memory for the SDK process/session, and its
453
453
  status placeholders are refreshed each time the dashboard renders. Calling this
454
454
  method does not open the dashboard; it updates an active contextual dashboard
@@ -4,8 +4,8 @@ package com.mentra.bluetoothsdk
4
4
  internal val GENERATED_RELEASE_CHANGELOGS: List<ReleaseChangelog> =
5
5
  listOf(
6
6
  ReleaseChangelog(version = "3.3.0", markdown = "This release is under active development. User-facing changes will be documented as they land."),
7
- ReleaseChangelog(version = "3.2.1", markdown = "This release is under active development. User-facing changes will be documented as they land.\n\n- Mentra Miniapp SDK: `session.stream.preview({source: \"call\"})` and the `<StreamPreview>` component (from `@mentra/miniapp/react`) show the live video of a call your miniapp owns inside its UI. Requires the `CAMERA` permission; the lease survives the UI closing and ends with `handle.stop()` or the meeting. See [Stream preview](https://docs.mentraglass.com/app-devs/core-concepts/stream-preview)."),
8
- ReleaseChangelog(version = "3.2.0", markdown = "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."),
7
+ ReleaseChangelog(version = "3.2.1", markdown = "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."),
8
+ ReleaseChangelog(version = "3.2.0", markdown = "- Improved glasses connectivity and Wi-Fi setup.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."),
9
9
  ReleaseChangelog(version = "3.1.1", markdown = "- Improved Google login on iOS.\n- Pairing no longer requires phone microphone access."),
10
10
  ReleaseChangelog(version = "3.1.0", markdown = "**Mentra App and glasses**\n\n- Improved glasses pairing and reconnection, including on iPad.\n- Improved Android background reliability for captions and miniapps.\n- Added Xingyi AR99 support.\n- More reliable photo and video capture, with improved shutter sounds and privacy-light timing. Taking photos no longer interrupts your running miniapp.\n- Added video thumbnails and smoother gallery browsing.\n- Easier Mentra Live updates through your phone, with clearer progress and better recovery from interruptions.\n\n**Miniapp updates**\n\n- **Mentra AI:** Control whether photos are included with questions. Improved visual answers and connected-app follow-ups.\n- **Captions:** Added offline transcription settings and top/bottom positioning on supported displays.\n- **Translation:** Restored all 60 languages.\n- **Mentra Maps:** Clearer directions on G1/G2, less display flickering, and improved Android offline routing.\n- **Livestreamer:** More video-quality controls and custom streaming options, plus improved startup and sharing.\n- **Merge:** Tap to expand an insight; swipe down or back to dismiss it.\n- **Mentra Notes:** Smoother back navigation.\n- **Recorder:** Fixed audio sharing."),
11
11
  )
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
3
3
  /** Generated by release CI. Do not edit in a release checkout. */
4
4
  internal object GeneratedReleaseMetadata {
5
5
  const val FAMILY_BASE_VERSION: String = "3.3.0"
6
- const val RELEASE_IDENTITY: String = "3.3.0-dev.452"
7
- const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.452"
8
- const val SOURCE_COMMIT: String = "21a3c247b1abb31b7ca288fd0452ecab2704f4df"
9
- const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.452.json"
10
- const val OTA_MANIFEST_SHA256: String = "2541d8ed2b8044e0ac7c64af2048423291e34da9ac3f960f678662746c5e9134"
6
+ const val RELEASE_IDENTITY: String = "3.3.0-dev.458"
7
+ const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.458"
8
+ const val SOURCE_COMMIT: String = "2effd6ae1190e29f350df83c19e5ab8db50df6e7"
9
+ const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.458.json"
10
+ const val OTA_MANIFEST_SHA256: String = "21d95e65e02f555d0c3f75c8852c8005d099ae5ba134e5c726c4d93f9895b642"
11
11
  }
@@ -1,7 +1,7 @@
1
1
  package com.mentra.bluetoothsdk
2
2
 
3
3
  internal object DashboardContentFormatter {
4
- const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$"
4
+ const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$"
5
5
 
6
6
  fun template(content: String): String =
7
7
  if (content.isEmpty()) STATUS_HEADER else "$STATUS_HEADER\n\n$content"
@@ -7,7 +7,7 @@ class DashboardContentFormatterTest {
7
7
  @Test
8
8
  fun emptyContentReturnsStatusHeaderOnly() {
9
9
  assertEquals(
10
- "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
10
+ "\$TIME12$ \$DATE$ \$GBATT$",
11
11
  DashboardContentFormatter.template(""),
12
12
  )
13
13
  }
@@ -15,7 +15,7 @@ class DashboardContentFormatterTest {
15
15
  @Test
16
16
  fun nonEmptyContentIsAppendedExactlyAfterBlankLine() {
17
17
  assertEquals(
18
- "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$\n\n Next meeting\nRoom 2 ",
18
+ "\$TIME12$ \$DATE$ \$GBATT$\n\n Next meeting\nRoom 2 ",
19
19
  DashboardContentFormatter.template(" Next meeting\nRoom 2 "),
20
20
  )
21
21
  }
@@ -4,10 +4,10 @@ export declare const GENERATED_RELEASE_CHANGELOGS: readonly [{
4
4
  readonly markdown: "This release is under active development. User-facing changes will be documented as they land.";
5
5
  }, {
6
6
  readonly version: "3.2.1";
7
- readonly markdown: "This release is under active development. User-facing changes will be documented as they land.\n\n- Mentra Miniapp SDK: `session.stream.preview({source: \"call\"})` and the `<StreamPreview>` component (from `@mentra/miniapp/react`) show the live video of a call your miniapp owns inside its UI. Requires the `CAMERA` permission; the lease survives the UI closing and ends with `handle.stop()` or the meeting. See [Stream preview](https://docs.mentraglass.com/app-devs/core-concepts/stream-preview).";
7
+ readonly markdown: "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend.";
8
8
  }, {
9
9
  readonly version: "3.2.0";
10
- readonly markdown: "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`.";
10
+ readonly markdown: "- Improved glasses connectivity and Wi-Fi setup.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`.";
11
11
  }, {
12
12
  readonly version: "3.1.1";
13
13
  readonly markdown: "- Improved Google login on iOS.\n- Pairing no longer requires phone microphone access.";
@@ -6,11 +6,11 @@ export const GENERATED_RELEASE_CHANGELOGS = Object.freeze([
6
6
  },
7
7
  {
8
8
  "version": "3.2.1",
9
- "markdown": "This release is under active development. User-facing changes will be documented as they land.\n\n- Mentra Miniapp SDK: `session.stream.preview({source: \"call\"})` and the `<StreamPreview>` component (from `@mentra/miniapp/react`) show the live video of a call your miniapp owns inside its UI. Requires the `CAMERA` permission; the lease survives the UI closing and ends with `handle.stop()` or the meeting. See [Stream preview](https://docs.mentraglass.com/app-devs/core-concepts/stream-preview)."
9
+ "markdown": "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."
10
10
  },
11
11
  {
12
12
  "version": "3.2.0",
13
- "markdown": "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
13
+ "markdown": "- Improved glasses connectivity and Wi-Fi setup.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
14
14
  },
15
15
  {
16
16
  "version": "3.1.1",
@@ -1 +1 @@
1
- {"version":3,"file":"changelogCatalog.js","sourceRoot":"","sources":["../../src/generated/changelogCatalog.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,4BAA4B,GAAG,MAAM,CAAC,MAAM,CACzD;IACE;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,gGAAgG;KAC7G;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,ofAAof;KACjgB;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,maAAma;KAChb;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,wFAAwF;KACrG;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,mrCAAmrC;KAChsC;CACO,CACT,CAAA","sourcesContent":["/** Generated from /changelogs. Do not edit directly. */\nexport const GENERATED_RELEASE_CHANGELOGS = Object.freeze(\n[\n {\n \"version\": \"3.3.0\",\n \"markdown\": \"This release is under active development. User-facing changes will be documented as they land.\"\n },\n {\n \"version\": \"3.2.1\",\n \"markdown\": \"This release is under active development. User-facing changes will be documented as they land.\\n\\n- Mentra Miniapp SDK: `session.stream.preview({source: \\\"call\\\"})` and the `<StreamPreview>` component (from `@mentra/miniapp/react`) show the live video of a call your miniapp owns inside its UI. Requires the `CAMERA` permission; the lease survives the UI closing and ends with `handle.stop()` or the meeting. See [Stream preview](https://docs.mentraglass.com/app-devs/core-concepts/stream-preview).\"\n },\n {\n \"version\": \"3.2.0\",\n \"markdown\": \"This release is under active development. User-facing changes will be documented as they land.\\n\\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`.\"\n },\n {\n \"version\": \"3.1.1\",\n \"markdown\": \"- Improved Google login on iOS.\\n- Pairing no longer requires phone microphone access.\"\n },\n {\n \"version\": \"3.1.0\",\n \"markdown\": \"**Mentra App and glasses**\\n\\n- Improved glasses pairing and reconnection, including on iPad.\\n- Improved Android background reliability for captions and miniapps.\\n- Added Xingyi AR99 support.\\n- More reliable photo and video capture, with improved shutter sounds and privacy-light timing. Taking photos no longer interrupts your running miniapp.\\n- Added video thumbnails and smoother gallery browsing.\\n- Easier Mentra Live updates through your phone, with clearer progress and better recovery from interruptions.\\n\\n**Miniapp updates**\\n\\n- **Mentra AI:** Control whether photos are included with questions. Improved visual answers and connected-app follow-ups.\\n- **Captions:** Added offline transcription settings and top/bottom positioning on supported displays.\\n- **Translation:** Restored all 60 languages.\\n- **Mentra Maps:** Clearer directions on G1/G2, less display flickering, and improved Android offline routing.\\n- **Livestreamer:** More video-quality controls and custom streaming options, plus improved startup and sharing.\\n- **Merge:** Tap to expand an insight; swipe down or back to dismiss it.\\n- **Mentra Notes:** Smoother back navigation.\\n- **Recorder:** Fixed audio sharing.\"\n }\n] as const,\n)\n"]}
1
+ {"version":3,"file":"changelogCatalog.js","sourceRoot":"","sources":["../../src/generated/changelogCatalog.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,4BAA4B,GAAG,MAAM,CAAC,MAAM,CACzD;IACE;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,gGAAgG;KAC7G;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,wtBAAwtB;KACruB;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,qXAAqX;KAClY;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,wFAAwF;KACrG;IACD;QACE,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,mrCAAmrC;KAChsC;CACO,CACT,CAAA","sourcesContent":["/** Generated from /changelogs. Do not edit directly. */\nexport const GENERATED_RELEASE_CHANGELOGS = Object.freeze(\n[\n {\n \"version\": \"3.3.0\",\n \"markdown\": \"This release is under active development. User-facing changes will be documented as they land.\"\n },\n {\n \"version\": \"3.2.1\",\n \"markdown\": \"- **Livestreamer:** \\\"Stream Here\\\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\\n- **Mentra AI:** Reliability improvements and bug fixes.\\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\\n- **Mentra Maps:** Bug fixes.\\n- **NIMO:** Improved text and map layouts.\\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend.\"\n },\n {\n \"version\": \"3.2.0\",\n \"markdown\": \"- Improved glasses connectivity and Wi-Fi setup.\\n\\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`.\"\n },\n {\n \"version\": \"3.1.1\",\n \"markdown\": \"- Improved Google login on iOS.\\n- Pairing no longer requires phone microphone access.\"\n },\n {\n \"version\": \"3.1.0\",\n \"markdown\": \"**Mentra App and glasses**\\n\\n- Improved glasses pairing and reconnection, including on iPad.\\n- Improved Android background reliability for captions and miniapps.\\n- Added Xingyi AR99 support.\\n- More reliable photo and video capture, with improved shutter sounds and privacy-light timing. Taking photos no longer interrupts your running miniapp.\\n- Added video thumbnails and smoother gallery browsing.\\n- Easier Mentra Live updates through your phone, with clearer progress and better recovery from interruptions.\\n\\n**Miniapp updates**\\n\\n- **Mentra AI:** Control whether photos are included with questions. Improved visual answers and connected-app follow-ups.\\n- **Captions:** Added offline transcription settings and top/bottom positioning on supported displays.\\n- **Translation:** Restored all 60 languages.\\n- **Mentra Maps:** Clearer directions on G1/G2, less display flickering, and improved Android offline routing.\\n- **Livestreamer:** More video-quality controls and custom streaming options, plus improved startup and sharing.\\n- **Merge:** Tap to expand an insight; swipe down or back to dismiss it.\\n- **Mentra Notes:** Smoother back navigation.\\n- **Recorder:** Fixed audio sharing.\"\n }\n] as const,\n)\n"]}
@@ -1,10 +1,10 @@
1
1
  export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
2
2
  "schemaVersion": 1,
3
3
  "familyBaseVersion": "3.3.0",
4
- "releaseIdentity": "3.3.0-dev.452",
5
- "releaseSetId": "mentra-3.3.0-dev.452",
6
- "sourceCommit": "21a3c247b1abb31b7ca288fd0452ecab2704f4df",
7
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.452.json",
8
- "otaManifestSha256": "2541d8ed2b8044e0ac7c64af2048423291e34da9ac3f960f678662746c5e9134"
4
+ "releaseIdentity": "3.3.0-dev.458",
5
+ "releaseSetId": "mentra-3.3.0-dev.458",
6
+ "sourceCommit": "2effd6ae1190e29f350df83c19e5ab8db50df6e7",
7
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.458.json",
8
+ "otaManifestSha256": "21d95e65e02f555d0c3f75c8852c8005d099ae5ba134e5c726c4d93f9895b642"
9
9
  });
10
10
  //# sourceMappingURL=releaseMetadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.452\",\n \"releaseSetId\": \"mentra-3.3.0-dev.452\",\n \"sourceCommit\": \"21a3c247b1abb31b7ca288fd0452ecab2704f4df\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.452.json\",\n \"otaManifestSha256\": \"2541d8ed2b8044e0ac7c64af2048423291e34da9ac3f960f678662746c5e9134\"\n})\n"]}
1
+ {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.458\",\n \"releaseSetId\": \"mentra-3.3.0-dev.458\",\n \"sourceCommit\": \"2effd6ae1190e29f350df83c19e5ab8db50df6e7\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.458.json\",\n \"otaManifestSha256\": \"21d95e65e02f555d0c3f75c8852c8005d099ae5ba134e5c726c4d93f9895b642\"\n})\n"]}
@@ -16,7 +16,7 @@ enum BluetoothSdkDefaults {
16
16
  /// silently disable it on every pair the app touches.
17
17
  static let autoPowerOffEnabled = false
18
18
  private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
19
- private static let swiftPackageSdkVersion = "3.3.0-dev.452"
19
+ private static let swiftPackageSdkVersion = "3.3.0-dev.458"
20
20
  private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
21
21
 
22
22
  private static func normalizedSdkVersion(_ value: String?) -> String? {
@@ -3,8 +3,8 @@ import Foundation
3
3
  /// Generated from /changelogs. Do not edit directly.
4
4
  let generatedReleaseChangelogs: [ReleaseChangelog] = [
5
5
  ReleaseChangelog(version: "3.3.0", markdown: "This release is under active development. User-facing changes will be documented as they land."),
6
- ReleaseChangelog(version: "3.2.1", markdown: "This release is under active development. User-facing changes will be documented as they land.\n\n- Mentra Miniapp SDK: `session.stream.preview({source: \"call\"})` and the `<StreamPreview>` component (from `@mentra/miniapp/react`) show the live video of a call your miniapp owns inside its UI. Requires the `CAMERA` permission; the lease survives the UI closing and ends with `handle.stop()` or the meeting. See [Stream preview](https://docs.mentraglass.com/app-devs/core-concepts/stream-preview)."),
7
- ReleaseChangelog(version: "3.2.0", markdown: "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."),
6
+ ReleaseChangelog(version: "3.2.1", markdown: "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."),
7
+ ReleaseChangelog(version: "3.2.0", markdown: "- Improved glasses connectivity and Wi-Fi setup.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."),
8
8
  ReleaseChangelog(version: "3.1.1", markdown: "- Improved Google login on iOS.\n- Pairing no longer requires phone microphone access."),
9
9
  ReleaseChangelog(version: "3.1.0", markdown: "**Mentra App and glasses**\n\n- Improved glasses pairing and reconnection, including on iPad.\n- Improved Android background reliability for captions and miniapps.\n- Added Xingyi AR99 support.\n- More reliable photo and video capture, with improved shutter sounds and privacy-light timing. Taking photos no longer interrupts your running miniapp.\n- Added video thumbnails and smoother gallery browsing.\n- Easier Mentra Live updates through your phone, with clearer progress and better recovery from interruptions.\n\n**Miniapp updates**\n\n- **Mentra AI:** Control whether photos are included with questions. Improved visual answers and connected-app follow-ups.\n- **Captions:** Added offline transcription settings and top/bottom positioning on supported displays.\n- **Translation:** Restored all 60 languages.\n- **Mentra Maps:** Clearer directions on G1/G2, less display flickering, and improved Android offline routing.\n- **Livestreamer:** More video-quality controls and custom streaming options, plus improved startup and sharing.\n- **Merge:** Tap to expand an insight; swipe down or back to dismiss it.\n- **Mentra Notes:** Smoother back navigation.\n- **Recorder:** Fixed audio sharing."),
10
10
  ]
@@ -3,9 +3,9 @@ import Foundation
3
3
  /// Generated by release CI. Do not edit in a release checkout.
4
4
  enum GeneratedReleaseMetadata {
5
5
  static let familyBaseVersion = "3.3.0"
6
- static let releaseIdentity = "3.3.0-dev.452"
7
- static let releaseSetId = "mentra-3.3.0-dev.452"
8
- static let sourceCommit = "21a3c247b1abb31b7ca288fd0452ecab2704f4df"
9
- static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.452.json"
10
- static let otaManifestSha256 = "2541d8ed2b8044e0ac7c64af2048423291e34da9ac3f960f678662746c5e9134"
6
+ static let releaseIdentity = "3.3.0-dev.458"
7
+ static let releaseSetId = "mentra-3.3.0-dev.458"
8
+ static let sourceCommit = "2effd6ae1190e29f350df83c19e5ab8db50df6e7"
9
+ static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.458.json"
10
+ static let otaManifestSha256 = "21d95e65e02f555d0c3f75c8852c8005d099ae5ba134e5c726c4d93f9895b642"
11
11
  }
@@ -1,7 +1,7 @@
1
1
  import Foundation
2
2
 
3
3
  enum DashboardContentFormatter {
4
- static let statusHeader = "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$"
4
+ static let statusHeader = "$TIME12$ $DATE$ $GBATT$"
5
5
 
6
6
  static func template(for content: String) -> String {
7
7
  content.isEmpty ? statusHeader : statusHeader + "\n\n" + content
@@ -7,8 +7,8 @@
7
7
 
8
8
  import Combine
9
9
  import CoreBluetooth
10
- import Foundation
11
10
  import CoreGraphics
11
+ import Foundation
12
12
 
13
13
  extension Data {
14
14
  func chunked(into size: Int) -> [Data] {
@@ -232,7 +232,8 @@ actor ReconnectionManager {
232
232
  }
233
233
 
234
234
  func start(onAttempt: @escaping @Sendable () async -> Bool) {
235
- stop()
235
+ // Multiple command failures must not restart the delay between attempts.
236
+ guard !isRunning else { return }
236
237
  attempts = 0
237
238
 
238
239
  task = Task {
@@ -258,6 +259,9 @@ actor ReconnectionManager {
258
259
  break
259
260
  }
260
261
  }
262
+ if !Task.isCancelled {
263
+ task = nil
264
+ }
261
265
  }
262
266
  }
263
267
 
@@ -312,7 +316,7 @@ class G1: NSObject, SGCManager {
312
316
 
313
317
  func queryGalleryStatus() {}
314
318
 
315
- func sendOtaStart(otaVersionUrl: String?) {}
319
+ func sendOtaStart(otaVersionUrl _: String?) {}
316
320
  func sendOtaQueryStatus() {}
317
321
 
318
322
  func ping() {}
@@ -450,13 +454,12 @@ class G1: NSObject, SGCManager {
450
454
  }
451
455
 
452
456
  private var centralManager: CBCentralManager?
457
+ private var scanRequested = false
453
458
 
454
459
  private var leftPeripheral: CBPeripheral?
455
460
  private var rightPeripheral: CBPeripheral?
456
461
  private var connectedDevices: [String: (CBPeripheral?, CBPeripheral?)] = [:]
457
462
  var lastConnectionTimestamp: Date = .distantPast
458
- private var leftInitialized: Bool = false
459
- private var rightInitialized: Bool = false
460
463
 
461
464
  private var leftGlassUUID: UUID? {
462
465
  get {
@@ -501,6 +504,8 @@ class G1: NSObject, SGCManager {
501
504
  }
502
505
 
503
506
  func forget() {
507
+ isDisconnecting = true
508
+ stopScan()
504
509
  Task {
505
510
  await heartbeatManager.stop()
506
511
  await reconnectionManager.stop()
@@ -704,11 +709,13 @@ class G1: NSObject, SGCManager {
704
709
 
705
710
  func connectById(_ id: String) {
706
711
  DEVICE_SEARCH_ID = "_" + id + "_"
712
+ scanRequested = true
707
713
  startScan()
708
714
  }
709
715
 
710
716
  func findCompatibleDevices() {
711
717
  DEVICE_SEARCH_ID = "NOT_SET"
718
+ scanRequested = true
712
719
  startScan()
713
720
  }
714
721
 
@@ -736,8 +743,7 @@ class G1: NSObject, SGCManager {
736
743
  )
737
744
  stopScan()
738
745
 
739
- // get battery status:
740
- getBatteryStatus()
746
+ // Battery requests start after both arms acknowledge initialization.
741
747
  return true
742
748
  }
743
749
 
@@ -985,11 +991,15 @@ class G1: NSObject, SGCManager {
985
991
  connected = leftReady && rightReady
986
992
  if fullyBooted {
987
993
  stopReconnectionTimer()
994
+ if !prevLeftReady || !prevRightReady {
995
+ getBatteryStatus()
996
+ }
988
997
  }
989
998
  }
990
999
 
991
1000
  func stopScan() {
992
- centralManager!.stopScan()
1001
+ scanRequested = false
1002
+ centralManager?.stopScan()
993
1003
  Bridge.log("G1: Stopped scanning for devices")
994
1004
  }
995
1005
 
@@ -1011,6 +1021,7 @@ class G1: NSObject, SGCManager {
1011
1021
 
1012
1022
  func disconnect() {
1013
1023
  isDisconnecting = true
1024
+ stopScan()
1014
1025
  leftGlassUUID = nil
1015
1026
  rightGlassUUID = nil
1016
1027
  stopReconnectionTimer()
@@ -1042,10 +1053,13 @@ class G1: NSObject, SGCManager {
1042
1053
  let data = Data(chunks[0])
1043
1054
  // CoreCommsService.log("SEND (\(side)) \(data.hexEncodedString())")
1044
1055
 
1045
- if isDisconnecting {
1046
- // forget whatever we were doing since we're disconnecting:
1047
- break
1048
- }
1056
+ // A queued command can outlive the link that enqueued it. Missing
1057
+ // transport during discovery/pairing is not an ACK timeout and must
1058
+ // not start a reconnect loop before CoreBluetooth finishes connecting.
1059
+ let peripheral = side == "L" ? leftPeripheral : rightPeripheral
1060
+ guard !isDisconnecting, peripheral?.state == .connected,
1061
+ getWriteCharacteristic(for: peripheral) != nil
1062
+ else { return }
1049
1063
 
1050
1064
  for i in 0 ..< chunks.count - 1 {
1051
1065
  let chunk = chunks[i]
@@ -1506,18 +1520,9 @@ extension G1 {
1506
1520
  /// don't call semaphore signals here as it's handled elswhere:
1507
1521
  private func handleInitResponse(from peripheral: CBPeripheral, success: Bool) {
1508
1522
  if peripheral == leftPeripheral {
1509
- leftInitialized = success
1510
- // CoreCommsService.log("G1: Left arm initialized: \(success)")
1511
- setReadiness(left: true, right: nil)
1523
+ setReadiness(left: success, right: nil)
1512
1524
  } else if peripheral == rightPeripheral {
1513
- rightInitialized = success
1514
- // CoreCommsService.log("G1: Right arm initialized: \(success)")
1515
- setReadiness(left: nil, right: true)
1516
- }
1517
-
1518
- // Only proceed if both glasses are initialized
1519
- if leftInitialized, rightInitialized {
1520
- setReadiness(left: true, right: true)
1525
+ setReadiness(left: nil, right: success)
1521
1526
  }
1522
1527
  }
1523
1528
 
@@ -1737,6 +1742,7 @@ extension G1 {
1737
1742
  }
1738
1743
 
1739
1744
  func getBatteryStatus() {
1745
+ guard !isDisconnecting, leftReady, rightReady else { return }
1740
1746
  Bridge.log("G1: getBatteryStatus()")
1741
1747
  let command: [UInt8] = [Commands.BLE_REQ_BATTERY.rawValue, 0x01]
1742
1748
  queueChunks([command])
@@ -2368,6 +2374,7 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2368
2374
 
2369
2375
  private func startReconnectionTimer() {
2370
2376
  Task {
2377
+ guard !isDisconnecting else { return }
2371
2378
  await reconnectionManager.start { [weak self] in
2372
2379
  guard let self else { return false }
2373
2380
 
@@ -2381,6 +2388,8 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2381
2388
 
2382
2389
  // Attempt to reconnect
2383
2390
  await MainActor.run {
2391
+ guard !self.isDisconnecting else { return }
2392
+ self.scanRequested = true
2384
2393
  self.startScan()
2385
2394
  }
2386
2395
 
@@ -2517,18 +2526,24 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2517
2526
  /// called whenever bluetooth is initialized / turned on or off:
2518
2527
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
2519
2528
  DispatchQueue.main.async { [weak self] in
2520
- guard let self = self else { return }
2529
+ self?.handleBluetoothState(central.state)
2530
+ }
2531
+ }
2521
2532
 
2522
- if central.state == .poweredOn {
2523
- Bridge.log("G1: Bluetooth was powered on")
2524
- self.setReadiness(left: false, right: false)
2533
+ func handleBluetoothState(_ state: CBManagerState) {
2534
+ if state == .poweredOn {
2535
+ Bridge.log("G1: Bluetooth was powered on")
2536
+ setReadiness(left: false, right: false)
2525
2537
 
2526
- if self.DEVICE_SEARCH_ID != "NOT_SET", !self.DEVICE_SEARCH_ID.isEmpty {
2527
- self.startScan()
2528
- }
2529
- } else {
2530
- Bridge.log("G1: Bluetooth was turned off.")
2538
+ // Discovery has no search ID, but still needs to resume after the
2539
+ // central manager's initial .unknown state. Preserve selected-pair
2540
+ // reconnects after a power cycle, unless intentionally disconnected.
2541
+ let hasConnectionTarget = DEVICE_SEARCH_ID != "NOT_SET" && !DEVICE_SEARCH_ID.isEmpty
2542
+ if !isDisconnecting, scanRequested || hasConnectionTarget {
2543
+ startScan()
2531
2544
  }
2545
+ } else {
2546
+ Bridge.log("G1: Bluetooth was turned off.")
2532
2547
  }
2533
2548
  }
2534
2549
 
@@ -7488,7 +7488,7 @@ extension MentraLive {
7488
7488
  if fps > 0 { settings["fps"] = fps }
7489
7489
  json["settings"] = settings
7490
7490
  }
7491
- sendJson(json)
7491
+ sendJson(json, wakeUp: true)
7492
7492
  }
7493
7493
 
7494
7494
  func stopVideoRecording(requestId: String) {
@@ -7503,7 +7503,7 @@ extension MentraLive {
7503
7503
  sendJson([
7504
7504
  "type": "get_video_recording_status",
7505
7505
  "requestId": requestId,
7506
- ])
7506
+ ], wakeUp: true)
7507
7507
  }
7508
7508
 
7509
7509
  func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
@@ -7528,7 +7528,7 @@ extension MentraLive {
7528
7528
  if let authToken, !authToken.isEmpty {
7529
7529
  json["authToken"] = authToken
7530
7530
  }
7531
- sendJson(json)
7531
+ sendJson(json, wakeUp: true)
7532
7532
  }
7533
7533
  }
7534
7534
 
@@ -5,14 +5,14 @@ final class DashboardContentFormatterTests: XCTestCase {
5
5
  func testEmptyContentReturnsStatusHeaderOnly() {
6
6
  XCTAssertEqual(
7
7
  DashboardContentFormatter.template(for: ""),
8
- "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$"
8
+ "$TIME12$ $DATE$ $GBATT$"
9
9
  )
10
10
  }
11
11
 
12
12
  func testNonEmptyContentIsAppendedExactlyAfterBlankLine() {
13
13
  XCTAssertEqual(
14
14
  DashboardContentFormatter.template(for: " Next meeting\nRoom 2 "),
15
- "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$\n\n Next meeting\nRoom 2 "
15
+ "$TIME12$ $DATE$ $GBATT$\n\n Next meeting\nRoom 2 "
16
16
  )
17
17
  }
18
18
  }
@@ -0,0 +1,162 @@
1
+ import CoreBluetooth
2
+ @testable import MentraBluetoothSDK
3
+ import XCTest
4
+
5
+ @MainActor
6
+ final class G1PairingTests: XCTestCase {
7
+ /// Exercise the real discovery/readiness entry points without accessing a
8
+ /// radio. Battery requests are observed through the SDK diagnostics.
9
+ private class RecordingG1: G1 {
10
+ var scans = 0
11
+ override func startScan() -> Bool {
12
+ scans += 1
13
+ return false // Central manager is still initializing.
14
+ }
15
+ }
16
+
17
+ private func withGlasses(_ test: (RecordingG1) async throws -> Void) async rethrows {
18
+ let store = DeviceStore.shared.store
19
+ let saved = store.getCategory("glasses")
20
+ let keys = ["leftGlassUUID", "rightGlassUUID", "leftGlassSearchID", "rightGlassSearchID"]
21
+ let savedDefaults = keys.map { ($0, UserDefaults.standard.object(forKey: $0)) }
22
+ let glasses = RecordingG1()
23
+ defer {
24
+ glasses.forget()
25
+ for (key, value) in savedDefaults {
26
+ UserDefaults.standard.set(value, forKey: key)
27
+ }
28
+ for key in store.getCategory("glasses").keys where saved[key] == nil {
29
+ store.remove("glasses", key)
30
+ }
31
+ for (key, value) in saved {
32
+ store.set("glasses", key, value)
33
+ }
34
+ }
35
+ try await test(glasses)
36
+ }
37
+
38
+ func testFirstDiscoveryResumesWhenBluetoothPowersOn() async {
39
+ await withGlasses { glasses in
40
+ glasses.findCompatibleDevices()
41
+ XCTAssertEqual(glasses.DEVICE_SEARCH_ID, "NOT_SET")
42
+ XCTAssertEqual(glasses.scans, 1)
43
+ glasses.handleBluetoothState(.poweredOn)
44
+ XCTAssertEqual(glasses.scans, 2)
45
+ }
46
+ }
47
+
48
+ func testStoppedDiscoveryDoesNotResumeOnLatePowerCallback() async {
49
+ await withGlasses { glasses in
50
+ glasses.findCompatibleDevices()
51
+ glasses.stopScan()
52
+ glasses.handleBluetoothState(.poweredOn)
53
+ XCTAssertEqual(glasses.scans, 1)
54
+ }
55
+ }
56
+
57
+ func testSelectedPairResumesButCancelledPairDoesNot() async {
58
+ await withGlasses { glasses in
59
+ glasses.connectById("59")
60
+ glasses.handleBluetoothState(.poweredOn)
61
+ XCTAssertEqual(glasses.scans, 2)
62
+ XCTAssertEqual(glasses.DEVICE_SEARCH_ID, "_59_")
63
+ // Finding both arms stops scanning, but should not prevent a
64
+ // selected pair from reconnecting after Bluetooth is toggled.
65
+ glasses.stopScan()
66
+ glasses.handleBluetoothState(.poweredOff)
67
+ glasses.handleBluetoothState(.poweredOn)
68
+ XCTAssertEqual(glasses.scans, 3)
69
+ glasses.disconnect()
70
+ glasses.handleBluetoothState(.poweredOn)
71
+ XCTAssertEqual(glasses.scans, 3)
72
+ }
73
+ }
74
+
75
+ func testBatteryWaitsForBothArmsAndRefreshesAfterReconnect() async {
76
+ await withGlasses { glasses in
77
+ var batteryRequests = 0
78
+ let sink = Bridge.addEventSink { event, body in
79
+ if event == "log", body["message"] as? String == "G1: getBatteryStatus()" {
80
+ batteryRequests += 1
81
+ }
82
+ }
83
+ defer { Bridge.removeEventSink(sink) }
84
+ glasses.getBatteryStatus()
85
+ XCTAssertEqual(batteryRequests, 0)
86
+ glasses.setReadiness(left: true, right: nil)
87
+ glasses.getBatteryStatus()
88
+ XCTAssertEqual(batteryRequests, 0)
89
+ glasses.setReadiness(left: nil, right: true)
90
+ XCTAssertEqual(batteryRequests, 1)
91
+ glasses.setReadiness(left: true, right: true)
92
+ XCTAssertEqual(batteryRequests, 1, "Duplicate init ACKs must not poll again")
93
+ glasses.setReadiness(left: false, right: false)
94
+ glasses.getBatteryStatus()
95
+ XCTAssertEqual(batteryRequests, 1)
96
+ glasses.setReadiness(left: nil, right: true)
97
+ glasses.setReadiness(left: true, right: nil)
98
+ XCTAssertEqual(batteryRequests, 2)
99
+ }
100
+ }
101
+
102
+ func testQueuedCommandWithoutTransportDoesNotReconnect() async throws {
103
+ try await withGlasses { glasses in
104
+ glasses.queueChunks([[Commands.BLE_REQ_BATTERY.rawValue, 0x01]])
105
+ // The original missing-characteristic path restarted scanning after
106
+ // five 100ms command attempts, before either arm could connect.
107
+ try await Task.sleep(nanoseconds: 1_000_000_000)
108
+ XCTAssertEqual(glasses.scans, 0)
109
+ }
110
+ }
111
+ }
112
+
113
+ final class G1ReconnectionTests: XCTestCase {
114
+ func testSuccessfulReconnectAllowsALaterRetryLoop() async {
115
+ let manager = ReconnectionManager()
116
+ let connected = expectation(description: "Reconnect completes")
117
+ await manager.start {
118
+ connected.fulfill()
119
+ return true
120
+ }
121
+ await fulfillment(of: [connected], timeout: 1)
122
+ let running = await manager.isRunning
123
+ XCTAssertFalse(running)
124
+
125
+ let retry = expectation(description: "A later disconnection can reconnect")
126
+ await manager.start {
127
+ retry.fulfill()
128
+ return true
129
+ }
130
+ await fulfillment(of: [retry], timeout: 1)
131
+ await manager.stop()
132
+ }
133
+
134
+ func testRepeatedFailuresDoNotRestartTheRetryDelay() async {
135
+ let manager = ReconnectionManager(intervalSeconds: 60)
136
+ let firstAttempt = expectation(description: "First reconnect attempt")
137
+ await manager.start {
138
+ firstAttempt.fulfill()
139
+ return false
140
+ }
141
+ await fulfillment(of: [firstAttempt], timeout: 1)
142
+
143
+ let restarted = expectation(description: "No immediate retry for another failed command")
144
+ restarted.isInverted = true
145
+ await manager.start {
146
+ restarted.fulfill()
147
+ return false
148
+ }
149
+ await fulfillment(of: [restarted], timeout: 0.1)
150
+ let attempts = await manager.attemptCount
151
+ XCTAssertEqual(attempts, 1)
152
+ await manager.stop()
153
+
154
+ let nextAttempt = expectation(description: "A later connection can retry")
155
+ await manager.start {
156
+ nextAttempt.fulfill()
157
+ return false
158
+ }
159
+ await fulfillment(of: [nextAttempt], timeout: 1)
160
+ await manager.stop()
161
+ }
162
+ }
@@ -0,0 +1,81 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ /// Replace the radio handoff while retaining public SDK dispatch and response handling.
5
+ @MainActor
6
+ private final class VideoRecordingCommandTransport: MentraLive {
7
+ var commands: [[String: Any]] = []
8
+
9
+ override func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool, requireAck: Bool) {
10
+ do {
11
+ let bytes = try JSONSerialization.data(withJSONObject: jsonOriginal)
12
+ let command = try XCTUnwrap(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
13
+ let type = try XCTUnwrap(command["type"] as? String)
14
+ let requestId = try XCTUnwrap(command["requestId"] as? String)
15
+ commands.append(command)
16
+ XCTAssertTrue(wakeUp, "\(type) must wake the glasses before ASG can handle it")
17
+ XCTAssertTrue(requireAck)
18
+
19
+ let statuses = [
20
+ "start_video_recording": "recording_started",
21
+ "get_video_recording_status": "recording_status",
22
+ "stop_video_recording": "recording_stopped",
23
+ ]
24
+ let status = try XCTUnwrap(statuses[type])
25
+ let response: [String: Any] = [
26
+ "type": "video_recording_status", "requestId": requestId,
27
+ "status": status, "success": true,
28
+ ]
29
+ try processReceivedData(JSONSerialization.data(withJSONObject: response))
30
+ } catch {
31
+ XCTFail("Could not replay video recording command: \(error)")
32
+ }
33
+ }
34
+ }
35
+
36
+ @MainActor
37
+ final class VideoRecordingCommandTests: XCTestCase {
38
+ func testStartStatusAndStopWakeGlassesThroughPublicSDK() async throws {
39
+ let previous = DeviceManager.shared.sgc
40
+ let store = DeviceStore.shared.store
41
+ let savedConnected = store.getCategory("glasses")["connected"]
42
+ let transport = VideoRecordingCommandTransport()
43
+ transport.connectionState = ConnTypes.CONNECTED
44
+ DeviceManager.shared.sgc = transport
45
+ DeviceStore.shared.apply("glasses", "connected", true)
46
+ let sdk = MentraBluetoothSDK()
47
+ defer {
48
+ sdk.invalidate()
49
+ DeviceManager.shared.sgc = previous
50
+ if let savedConnected {
51
+ store.set("glasses", "connected", savedConnected)
52
+ } else {
53
+ store.remove("glasses", "connected")
54
+ }
55
+ }
56
+
57
+ let requestId = "idle-recording"
58
+ let started = try await sdk.startVideoRecording(VideoRecordingRequest(
59
+ requestId: requestId, save: true, sound: false,
60
+ width: 1280, height: 720, fps: 30, maxRecordingTimeMinutes: 2
61
+ ))
62
+ XCTAssertEqual(started.status, "recording_started")
63
+ XCTAssertEqual(started.requestId, requestId)
64
+ let start = try XCTUnwrap(transport.commands.last)
65
+ XCTAssertEqual(start["save"] as? Bool, true)
66
+ XCTAssertEqual(start["sound"] as? Bool, false)
67
+ XCTAssertEqual(start["settings"] as? [String: Int], ["width": 1280, "height": 720, "fps": 30])
68
+ XCTAssertEqual(start["maxRecordingTimeMinutes"] as? Int, 2)
69
+
70
+ let status = try await sdk.queryVideoRecordingStatus(requestId: requestId)
71
+ XCTAssertEqual(status.status, "recording_status")
72
+ XCTAssertEqual(status.requestId, requestId)
73
+
74
+ let stopped = try await sdk.stopVideoRecording(requestId: requestId)
75
+ XCTAssertEqual(stopped.status, "recording_stopped")
76
+ XCTAssertEqual(stopped.requestId, requestId)
77
+ XCTAssertEqual(transport.commands.compactMap { $0["type"] as? String }, [
78
+ "start_video_recording", "get_video_recording_status", "stop_video_recording",
79
+ ])
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.3.0-dev.452",
3
+ "version": "3.3.0-dev.458",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -114,7 +114,7 @@
114
114
  "registry": "https://registry.npmjs.org/"
115
115
  },
116
116
  "dependencies": {
117
- "@mentra/cloud-protocol": "3.3.0-dev.452"
117
+ "@mentra/cloud-protocol": "3.3.0-dev.458"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@types/node": "^25.9.3",
@@ -7,11 +7,11 @@ export const GENERATED_RELEASE_CHANGELOGS = Object.freeze(
7
7
  },
8
8
  {
9
9
  "version": "3.2.1",
10
- "markdown": "This release is under active development. User-facing changes will be documented as they land.\n\n- Mentra Miniapp SDK: `session.stream.preview({source: \"call\"})` and the `<StreamPreview>` component (from `@mentra/miniapp/react`) show the live video of a call your miniapp owns inside its UI. Requires the `CAMERA` permission; the lease survives the UI closing and ends with `handle.stop()` or the meeting. See [Stream preview](https://docs.mentraglass.com/app-devs/core-concepts/stream-preview)."
10
+ "markdown": "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."
11
11
  },
12
12
  {
13
13
  "version": "3.2.0",
14
- "markdown": "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
14
+ "markdown": "- Improved glasses connectivity and Wi-Fi setup.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
15
15
  },
16
16
  {
17
17
  "version": "3.1.1",
@@ -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.3.0",
15
- "releaseIdentity": "3.3.0-dev.452",
16
- "releaseSetId": "mentra-3.3.0-dev.452",
17
- "sourceCommit": "21a3c247b1abb31b7ca288fd0452ecab2704f4df",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.452.json",
19
- "otaManifestSha256": "2541d8ed2b8044e0ac7c64af2048423291e34da9ac3f960f678662746c5e9134"
15
+ "releaseIdentity": "3.3.0-dev.458",
16
+ "releaseSetId": "mentra-3.3.0-dev.458",
17
+ "sourceCommit": "2effd6ae1190e29f350df83c19e5ab8db50df6e7",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.458.json",
19
+ "otaManifestSha256": "21d95e65e02f555d0c3f75c8852c8005d099ae5ba134e5c726c4d93f9895b642"
20
20
  })