@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 +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/sgcs/G1.swift +47 -32
- package/ios/Source/sgcs/MentraLive.swift +3 -3
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/package.json +2 -2
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
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
|
-
|
|
451
|
-
|
|
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 = "
|
|
8
|
-
ReleaseChangelog(version = "3.2.0", markdown = "
|
|
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.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.3.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
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.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
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$
|
|
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$
|
|
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
|
|
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: "
|
|
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: "
|
|
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": "
|
|
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": "
|
|
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,
|
|
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.
|
|
5
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
8
|
-
"otaManifestSha256": "
|
|
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.
|
|
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.
|
|
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: "
|
|
7
|
-
ReleaseChangelog(version: "3.2.0", markdown: "
|
|
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.
|
|
7
|
-
static let releaseSetId = "mentra-3.3.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
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$
|
|
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
|
package/ios/Source/sgcs/G1.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2529
|
+
self?.handleBluetoothState(central.state)
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2521
2532
|
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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$
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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.
|
|
16
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
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
|
})
|