@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +46 -45
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
  5. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +298 -181
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
  17. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
  18. package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
  19. package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
  20. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
  21. package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
  22. package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
  23. package/build/BluetoothSdk.types.d.ts +68 -2
  24. package/build/BluetoothSdk.types.d.ts.map +1 -1
  25. package/build/BluetoothSdk.types.js.map +1 -1
  26. package/build/_private/BluetoothSdkModule.d.ts +3 -4
  27. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  28. package/build/_private/BluetoothSdkModule.js.map +1 -1
  29. package/build/generated/changelogCatalog.d.ts +1 -1
  30. package/build/generated/changelogCatalog.js +1 -1
  31. package/build/generated/changelogCatalog.js.map +1 -1
  32. package/build/generated/releaseMetadata.js +5 -5
  33. package/build/generated/releaseMetadata.js.map +1 -1
  34. package/build/index.d.ts +1 -1
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +3 -0
  37. package/build/index.js.map +1 -1
  38. package/ios/BluetoothSdkModule.swift +8 -13
  39. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  40. package/ios/Source/Bridge.swift +63 -0
  41. package/ios/Source/DeviceManager.swift +6 -3
  42. package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
  43. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  44. package/ios/Source/MentraBluetoothSDK.swift +372 -220
  45. package/ios/Source/WifiRequestResolution.swift +251 -0
  46. package/ios/Source/sgcs/MentraLive.swift +216 -70
  47. package/ios/Source/sgcs/SGCManager.swift +13 -1
  48. package/ios/Source/status/DeviceStatus.swift +28 -0
  49. package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
  50. package/ios/Source/streaming/StreamModels.swift +12 -0
  51. package/ios/Source/streaming/StreamSessionState.swift +29 -0
  52. package/ios/Tests/StreamControllerProbeTests.swift +27 -0
  53. package/ios/Tests/StreamSessionStateTests.swift +38 -0
  54. package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
  55. package/package.json +1 -1
  56. package/src/BluetoothSdk.types.ts +82 -2
  57. package/src/_private/BluetoothSdkModule.ts +4 -4
  58. package/src/generated/changelogCatalog.ts +1 -1
  59. package/src/generated/releaseMetadata.ts +5 -5
  60. package/src/index.ts +11 -0
package/README.md CHANGED
@@ -152,21 +152,19 @@ iOS apps should include usage descriptions:
152
152
  Use `scan()` when your app needs to show a picker. It calls `onResults` every time the discovered list changes, then resolves with the final list after the timeout.
153
153
 
154
154
  ```ts
155
- import BluetoothSdk, {
156
- DeviceModels,
157
- } from '@mentra/bluetooth-sdk'
155
+ import BluetoothSdk, {DeviceModels} from "@mentra/bluetooth-sdk"
158
156
 
159
157
  const devices = await BluetoothSdk.scan(DeviceModels.MentraLive, {
160
158
  timeoutMs: 10_000,
161
159
  onResults: (nextDevices) => {
162
- console.log('Nearby glasses:', nextDevices)
160
+ console.log("Nearby glasses:", nextDevices)
163
161
  },
164
162
  onDiagnostic: (hint) => showScanHint(hint.message),
165
163
  })
166
164
 
167
165
  const device = await chooseDevice(devices)
168
166
  if (!device) {
169
- throw new Error('No Mentra Live glasses selected')
167
+ throw new Error("No Mentra Live glasses selected")
170
168
  }
171
169
 
172
170
  await BluetoothSdk.connect(device)
@@ -275,9 +273,9 @@ subpath for common lifecycle plumbing. Use the root `BluetoothSdk` object for
275
273
  commands such as `requestPhoto()`, `startStream()`, and `setMicState()`.
276
274
 
277
275
  ```tsx
278
- import {Button, Text, View} from 'react-native'
279
- import {DeviceModels} from '@mentra/bluetooth-sdk'
280
- import {useBluetoothEvent, useMentraBluetooth} from '@mentra/bluetooth-sdk/react'
276
+ import {Button, Text, View} from "react-native"
277
+ import {DeviceModels} from "@mentra/bluetooth-sdk"
278
+ import {useBluetoothEvent, useMentraBluetooth} from "@mentra/bluetooth-sdk/react"
281
279
 
282
280
  export function DeviceScreen() {
283
281
  const mentra = useMentraBluetooth({
@@ -285,13 +283,13 @@ export function DeviceScreen() {
285
283
  scanTimeoutMs: 10_000,
286
284
  })
287
285
 
288
- useBluetoothEvent('button_press', (event) => {
289
- console.log('Glasses button:', event.buttonId, event.pressType)
286
+ useBluetoothEvent("button_press", (event) => {
287
+ console.log("Glasses button:", event.buttonId, event.pressType)
290
288
  })
291
289
 
292
290
  return (
293
291
  <View>
294
- <Text>{mentra.glasses.connected ? 'Connected' : 'Disconnected'}</Text>
292
+ <Text>{mentra.glasses.connected ? "Connected" : "Disconnected"}</Text>
295
293
  <Button disabled={mentra.busy} title="Scan" onPress={() => mentra.scan.start()} />
296
294
  {mentra.scan.devices.map((device) => (
297
295
  <Button key={device.id} title={device.name} onPress={() => mentra.connect(device)} />
@@ -314,11 +312,11 @@ The React hook exposes `glasses.connection` as a discriminated union:
314
312
 
315
313
  ```ts
316
314
  type GlassesConnectionStatus =
317
- | {state: 'disconnected'}
318
- | {state: 'scanning'}
319
- | {state: 'connecting'}
320
- | {state: 'bonding'}
321
- | {state: 'connected'; fullyBooted: boolean}
315
+ | {state: "disconnected"}
316
+ | {state: "scanning"}
317
+ | {state: "connecting"}
318
+ | {state: "bonding"}
319
+ | {state: "connected"; fullyBooted: boolean}
322
320
  ```
323
321
 
324
322
  Use `connection.state` for link progress. `fullyBooted` only exists when `state === 'connected'`. Android and iOS native APIs also keep `connectionState`, `connected`, and `fullyBooted` as native status properties for Kotlin and Swift callers.
@@ -374,11 +372,14 @@ await BluetoothSdk.setDashboardPosition(4, 2)
374
372
  const networks = await BluetoothSdk.requestWifiScan()
375
373
  console.log(networks.map((network) => network.ssid))
376
374
 
377
- const wifiStatus = await BluetoothSdk.sendWifiCredentials('Office WiFi', 'secret')
375
+ const savedResult = await BluetoothSdk.getSavedWifiNetworks()
376
+ if (savedResult.outcome === "confirmed") console.log(savedResult.networks)
377
+
378
+ const wifiStatus = await BluetoothSdk.sendWifiCredentials("Office WiFi", "secret")
378
379
  console.log(wifiStatus.state)
379
380
 
380
- const forgetStatus = await BluetoothSdk.forgetWifiNetwork('Office WiFi')
381
- console.log(forgetStatus.state)
381
+ const forgetResult = await BluetoothSdk.forgetWifiNetwork("Office WiFi")
382
+ console.log(forgetResult.outcome)
382
383
 
383
384
  const hotspotStatus = await BluetoothSdk.setHotspotState(true)
384
385
  console.log(hotspotStatus.state)
@@ -389,19 +390,11 @@ const galleryAck = await BluetoothSdk.setGalleryModeEnabled(true)
389
390
  console.log(galleryAck.status)
390
391
  await BluetoothSdk.setGalleryModeEnabled(false)
391
392
 
392
- await BluetoothSdk.setPreferredMic('auto')
393
+ await BluetoothSdk.setPreferredMic("auto")
393
394
  await BluetoothSdk.setMicState(true)
394
395
  await BluetoothSdk.setOwnAppAudioPlaying(false)
395
396
 
396
- const ledAck = await BluetoothSdk.rgbLedControl(
397
- `led-${Date.now()}`,
398
- 'com.example.app',
399
- 'on',
400
- 'green',
401
- 500,
402
- 500,
403
- 3,
404
- )
397
+ const ledAck = await BluetoothSdk.rgbLedControl(`led-${Date.now()}`, "com.example.app", "on", "green", 500, 500, 3)
405
398
  console.log(ledAck.state)
406
399
  ```
407
400
 
@@ -440,7 +433,15 @@ await sdk.setDashboardContent("")
440
433
  Settings commands that return `SettingsAckSuccessEvent` reject when the ASG reports an error ack. The SDK updates its local settings store only after that ASG ack resolves successfully, so observed SDK state reflects the acknowledged glasses state rather than a queued request. Raw `settings_ack` listener events still use `SettingsAckEvent` because they can include both success and failure statuses. `rgbLedControl(...)` resolves from a successful ASG `rgb_led_control_response` and rejects when the ASG reports `state: "error"`; raw `settings_ack` and `rgb_led_control_response` events remain available through listeners.
441
434
 
442
435
  WiFi, hotspot, and version-info commands resolve from the ASG response path, not local dispatch:
443
- `requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. If older glasses stream non-empty scan results but never send the completion event, the request resolves with the accumulated scan list when the request times out. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` resolves when that SSID is no longer connected, and `setHotspotState()` resolves when the requested hotspot state is reported. `requestVersionInfo()` waits for all chunks declared by the correlated response's `chunkCount`, `chunkIndex`, and `final` metadata. Legacy single-message responses complete immediately; legacy chunked responses complete on `version_info_3` after `version_info_1`. Missing final chunks time out rather than returning partial data after a quiet period.
436
+ `requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. If older glasses stream non-empty scan results but never send the completion event, the request resolves with the accumulated scan list when the request times out. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` returns a semantic `WifiForgetResult`, `getSavedWifiNetworks()` returns a semantic `SavedWifiNetworksResult`, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` waits for all chunks declared by the correlated response's `chunkCount`, `chunkIndex`, and `final` metadata. Legacy single-message responses complete immediately; legacy chunked responses complete on `version_info_3` after `version_info_1`. Missing final chunks time out rather than returning partial data after a quiet period.
437
+
438
+ Current Mentra Live builds advertise `wifiForgetResultVersion` and `savedWifiNetworksVersion` in `version_info_1`. Native queues calls while those capabilities are unknown, sharing one 15-second deadline for negotiation and response. A discovery timeout rejects and never guesses that the glasses are legacy. Only version 1 is supported: modern commands carry `protocolVersion: 1`, a nonempty request ID, and the glasses process session ID; legacy forget commands omit all three fields. Unknown or malformed versions fail closed. Correlated results must match the complete tuple and (for forget) exact SSID; unrelated `wifi_status` events never settle a modern request. Session changes and disconnects reject pending WiFi work instead of allowing stale responses to cross sessions.
439
+
440
+ `WifiForgetResult.outcome` is `confirmed`, `dispatched`, `not_found`, `unsupported`, `failed`, or `legacy_unverified`. K900 returns `dispatched`: ASG queued its asynchronous SystemUI broadcast, but the vendor API provides no completion callback and credential removal is not verified. `connected`, `currentSsid`, and `localIp` are best-effort diagnostic snapshot fields; `connected` is omitted when ASG cannot read the link state and must not be interpreted as `false`. Older firmware without the capability advertisement uses the isolated legacy path; accepted native dispatch immediately returns `legacy_unverified`, with no invented link-state snapshot. `getSavedWifiNetworks()` preserves exact SSID identity. It returns `confirmed` only for a backend with reliable enumeration, while K900 and legacy firmware return a typed `unsupported` result because the vendor credential store has no reliable list response.
441
+
442
+ Promise results expose semantic outcomes and optional link snapshots, not `mode`, `capabilityVersion`, `requestId`, or `sid`. A legacy result returns `legacy_unverified` immediately after actual native transport acceptance; no active or compatible glasses transport rejects with `dispatch_failed`.
443
+
444
+ Raw `wifi_forget_result` listeners receive a discriminated event union. `mode: "modern"` carries `requestId`, `sid`, `protocolVersion`, and semantic `outcome`; `mode: "legacy"` preserves the older uncorrelated wire frame's `dispatched` boolean. Partial tuples are rejected. Terminal Wi-Fi events use at-least-once BLE delivery, so raw listeners can observe retries; deduplicate modern events by `(requestId, sid)`. Promise coordinators accept only the first matching terminal frame.
444
445
 
445
446
  The SDK automatically sends the phone wall clock once shortly after a glasses connection becomes ready. It waits for the initial command burst to drain before timestamping the command so startup queue delay does not become clock skew. The once-per-connection guard resets after disconnect, so every successful reconnect synchronizes again. The SDK does not periodically verify or correct clock skew during a long-lived connection; apps that require periodic reconciliation can compare `requestVersionInfo().systemTimeMs` with the phone clock.
446
447
 
@@ -525,15 +526,15 @@ Mentra Live also rejects `ota_start` before acknowledgement when its known batte
525
526
 
526
527
  ```ts
527
528
  const photo = await BluetoothSdk.requestPhoto({
528
- size: 'medium',
529
- webhookUrl: 'https://api.example.com/mentra/photo',
530
- authToken: 'optional-token',
531
- compress: 'medium',
529
+ size: "medium",
530
+ webhookUrl: "https://api.example.com/mentra/photo",
531
+ authToken: "optional-token",
532
+ compress: "medium",
532
533
  sound: true,
533
534
  exposureTimeNs: null, // auto exposure; pass a positive nanosecond value for manual exposure
534
535
  iso: null, // auto ISO; pass a positive ISO only with manual exposureTimeNs
535
536
  })
536
- console.log('photo delivered', photo.photoUrl ?? photo.uploadUrl, photo.fileSizeBytes)
537
+ console.log("photo delivered", photo.photoUrl ?? photo.uploadUrl, photo.fileSizeBytes)
537
538
  ```
538
539
 
539
540
  `requestPhoto(...)` resolves only after the full photo action reaches terminal success: capture completed and the photo was delivered to the webhook, either directly from the glasses over Wi-Fi or through the phone's Bluetooth fallback relay. If you omit `requestId`, the SDK generates one and the terminal response includes it. It rejects if the ASG reports `state: "error"`, if phone-side fallback upload fails, if the SDK cannot send the command, or if no terminal `photo_response` arrives within 30 seconds. Photo requests use this longer operation-specific deadline because max-quality BLE fallback can legitimately exceed the 15-second deadline used by ordinary commands. Use `photo_status` for intermediate stages such as `accepted`, `configuring`, `capturing`, `captured`, `uploading`, `ble_fallback_compression`, `ready_for_transfer`, and `transferring`; `photo_status` is progress, while `photo_response` is terminal success/error. The raw `photo_response` event stream still includes both success and error events for subscribers. The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
@@ -554,8 +555,8 @@ The missing/factory persistent FOV base is 102 degrees with centered ROI; existi
554
555
  const streamId = `stream-${Date.now()}`
555
556
 
556
557
  await BluetoothSdk.startStream({
557
- type: 'start_stream',
558
- streamUrl: 'http://192.168.1.42:8889/mentra-live/whip',
558
+ type: "start_stream",
559
+ streamUrl: "http://192.168.1.42:8889/mentra-live/whip",
559
560
  streamId,
560
561
  video: {fps: 15},
561
562
  })
@@ -571,19 +572,19 @@ Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https:
571
572
  React Native components should use `useBluetoothEvent()` for hardware events:
572
573
 
573
574
  ```tsx
574
- import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
575
+ import {useBluetoothEvent} from "@mentra/bluetooth-sdk/react"
575
576
 
576
577
  export function HardwareEventLogger() {
577
- useBluetoothEvent('button_press', (event) => console.log(event))
578
- useBluetoothEvent('touch_event', (event) => console.log(event))
579
- useBluetoothEvent('photo_status', (event) => console.log(event.status, event.resolvedConfig, event.captureMetadata))
580
- useBluetoothEvent('stream_status', (event) => console.log(event))
581
- useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
582
- useBluetoothEvent('mic_pcm', (event) => {
578
+ useBluetoothEvent("button_press", (event) => console.log(event))
579
+ useBluetoothEvent("touch_event", (event) => console.log(event))
580
+ useBluetoothEvent("photo_status", (event) => console.log(event.status, event.resolvedConfig, event.captureMetadata))
581
+ useBluetoothEvent("stream_status", (event) => console.log(event))
582
+ useBluetoothEvent("speaking_status", (event) => console.log(event.speaking))
583
+ useBluetoothEvent("mic_pcm", (event) => {
583
584
  console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
584
585
  console.log(event.pcm)
585
586
  })
586
- useBluetoothEvent('mic_health', (event) => {
587
+ useBluetoothEvent("mic_health", (event) => {
587
588
  console.log(event.reason, event.sequenceGapEvents, event.decodeFailures)
588
589
  })
589
590
 
@@ -339,6 +339,8 @@ class BluetoothSdkModule : Module() {
339
339
  "local_transcription",
340
340
  "wifi_status_change",
341
341
  "wifi_scan_result",
342
+ "wifi_forget_result",
343
+ "saved_wifi_networks",
342
344
  "hotspot_status_change",
343
345
  "hotspot_error",
344
346
  "photo_response",
@@ -592,12 +594,14 @@ class BluetoothSdkModule : Module() {
592
594
 
593
595
  SdkCoroutineFunction("requestWifiScan") { -> requireSdk().requestWifiScan().map { it.toMap() } }
594
596
 
597
+ SdkCoroutineFunction("getSavedWifiNetworks") { -> requireSdk().getSavedWifiNetworks().toMap() }
598
+
595
599
  SdkCoroutineFunction("sendWifiCredentials") { ssid: String, password: String ->
596
600
  requireSdk().sendWifiCredentials(ssid, password).values
597
601
  }
598
602
 
599
603
  SdkCoroutineFunction("forgetWifiNetwork") { ssid: String ->
600
- requireSdk().forgetWifiNetwork(ssid).values
604
+ requireSdk().forgetWifiNetwork(ssid).toMap()
601
605
  }
602
606
 
603
607
  SdkCoroutineFunction("setHotspotState") { enabled: Boolean ->
@@ -803,16 +807,8 @@ class BluetoothSdkModule : Module() {
803
807
  requireSdk().startStream(StreamRequest.fromMap(params)).values
804
808
  }
805
809
 
806
- SdkCoroutineFunction("startExternallyManagedStream") { params: Map<String, Any> ->
807
- requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
808
- }
809
-
810
810
  SdkCoroutineFunction("stopStream") { -> requireSdk().stopStream().values }
811
811
 
812
- SdkAsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
813
- sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
814
- }
815
-
816
812
  // MARK: - Microphone Commands
817
813
 
818
814
  SdkAsyncFunction("setMicState") {
@@ -494,6 +494,8 @@ public class Bridge private constructor() {
494
494
  ?.let {
495
495
  body[VersionInfoResponseAccumulator.RESPONSE_REQUEST_ID_KEY] = it
496
496
  }
497
+ body["versionInfoType"] = stringField("versionInfoType", "version_info_type")
498
+ body["sid"] = stringField("sid")
497
499
  body["androidVersion"] = stringField("androidVersion", "android_version")
498
500
  body["firmwareVersion"] = stringField("firmwareVersion", "firmware_version")
499
501
  body["besFirmwareVersion"] = stringField("besFirmwareVersion", "bes_fw_version")
@@ -512,6 +514,15 @@ public class Bridge private constructor() {
512
514
  (values["hotspotOtaVersion"] as? Number
513
515
  ?: values["hotspot_ota_version"] as? Number)
514
516
  ?.let { body["hotspotOtaVersion"] = it.toInt() }
517
+ for ((key, wireKey) in listOf(
518
+ "wifiForgetResultVersion" to "wifi_forget_result_version",
519
+ "savedWifiNetworksVersion" to "saved_wifi_networks_version",
520
+ )) {
521
+ if (values.containsKey(key) || values.containsKey(wireKey)) {
522
+ // Preserve malformed presence: it must not become legacy or be rounded to v1.
523
+ body[key] = values[key] ?: values[wireKey] ?: -1
524
+ }
525
+ }
515
526
  sendTypedMessage("version_info", body)
516
527
  }
517
528
 
@@ -592,6 +603,58 @@ public class Bridge private constructor() {
592
603
  sendTypedMessage("wifi_status_change", payload)
593
604
  }
594
605
 
606
+ @JvmStatic
607
+ fun sendWifiForgetResult(
608
+ requestId: String,
609
+ sid: String,
610
+ ssid: String,
611
+ protocolVersion: Int,
612
+ outcome: String,
613
+ legacyDispatched: Boolean?,
614
+ connected: Boolean?,
615
+ currentSsid: String,
616
+ localIp: String,
617
+ error: String?,
618
+ ) {
619
+ val body =
620
+ normalizeWifiForgetResultEvent(
621
+ requestId,
622
+ sid,
623
+ ssid,
624
+ protocolVersion,
625
+ outcome,
626
+ legacyDispatched,
627
+ connected,
628
+ currentSsid,
629
+ localIp,
630
+ error,
631
+ )
632
+ if (body == null) {
633
+ log("Dropping malformed wifi_forget_result without modern or legacy fields")
634
+ return
635
+ }
636
+ sendTypedMessage("wifi_forget_result", body)
637
+ }
638
+
639
+ @JvmStatic
640
+ fun sendSavedWifiNetworks(
641
+ requestId: String,
642
+ sid: String,
643
+ protocolVersion: Int,
644
+ outcome: String,
645
+ networks: List<String>,
646
+ error: String?,
647
+ ) {
648
+ val body = HashMap<String, Any>()
649
+ body["requestId"] = requestId
650
+ body["sid"] = sid
651
+ body["protocolVersion"] = protocolVersion
652
+ body["outcome"] = outcome
653
+ body["networks"] = networks
654
+ if (error != null) body["error"] = error
655
+ sendTypedMessage("saved_wifi_networks", body)
656
+ }
657
+
595
658
  /**
596
659
  * Claim the WiFi scan-results store for a newly requested scan. Called by the
597
660
  * SDK when it generates the scanId, BEFORE the scan command goes out: store
@@ -1784,6 +1784,9 @@ class DeviceManager {
1784
1784
  sgc?.requestWifiScan(scanId)
1785
1785
  }
1786
1786
 
1787
+ fun requestSavedWifiNetworks(requestId: String, sid: String): Boolean =
1788
+ sgc?.requestSavedWifiNetworks(requestId, sid) ?: false
1789
+
1787
1790
  fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
1788
1791
  Bridge.log("MAN: Sending incidentId to glasses for log upload: $incidentId")
1789
1792
  sgc?.sendIncidentId(incidentId, apiBaseUrl)
@@ -1802,10 +1805,8 @@ class DeviceManager {
1802
1805
  sgc?.sendWifiCredentials(ssid, password)
1803
1806
  }
1804
1807
 
1805
- fun forgetWifiNetwork(ssid: String) {
1806
- Bridge.log("MAN: Forgetting wifi network: $ssid")
1807
- sgc?.forgetWifiNetwork(ssid)
1808
- }
1808
+ fun forgetWifiNetwork(ssid: String, requestId: String? = null, sid: String? = null): Boolean =
1809
+ sgc?.forgetWifiNetwork(ssid, requestId, sid) ?: false
1809
1810
 
1810
1811
  fun setHotspotState(enabled: Boolean) {
1811
1812
  Bridge.log("MAN: Setting glasses hotspot state: $enabled")
@@ -3,6 +3,6 @@ package com.mentra.bluetoothsdk
3
3
  /** Generated from /changelogs. Do not edit directly. */
4
4
  internal val GENERATED_RELEASE_CHANGELOGS: List<ReleaseChangelog> =
5
5
  listOf(
6
- ReleaseChangelog(version = "3.2.0", markdown = "This release is under active development. User-facing changes will be documented as they land."),
6
+ 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
7
  ReleaseChangelog(version = "3.1.0", markdown = "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- MentraOS, Mentra Engine, the Bluetooth SDK, and the glasses client now share one coordinated release version.\n- Mentra Live updates can continue across APK, system, and firmware restarts without asking the user to start the same update again.\n- Bluetooth photo capture and transfer diagnostics are more precise and less disruptive to normal glasses traffic."),
8
8
  )
@@ -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.2.0"
6
- const val RELEASE_IDENTITY: String = "3.2.0-dev.200"
7
- const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.200"
8
- const val SOURCE_COMMIT: String = "1cdd911f596eba7b2b2f0e9ba5655c7ff16b5362"
9
- const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.200.json"
10
- const val OTA_MANIFEST_SHA256: String = "f0b5cca2fa4d1d43acf5933fb4de629819d36a145a162c763f4faa27a7c8ff79"
6
+ const val RELEASE_IDENTITY: String = "3.2.0-dev.206"
7
+ const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.206"
8
+ const val SOURCE_COMMIT: String = "3c22b92a5a53ce608c087503417ca5b3d16cc926"
9
+ const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.206.json"
10
+ const val OTA_MANIFEST_SHA256: String = "7bc5e09f2757b2c11da58c93459bb0a554f406dd0ae5b50b58db28b64b92c838"
11
11
  }