@glassly/bluetooth-sdk 0.1.0-dev.62 → 0.1.0-dev.90
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 +4 -4
- package/android/src/main/assets/G2Firmware/README.md +3 -3
- package/android/src/main/assets/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/android/src/main/assets/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +0 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +0 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +30 -86
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +1 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +171 -51
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +23 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +18 -6
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +8 -8
- package/build/BluetoothSdk.types.d.ts +37 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +0 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +0 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +0 -6
- package/ios/Source/BluetoothSDK.swift +0 -4
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +35 -88
- package/ios/Source/DeviceStore.swift +16 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
- package/ios/Source/Resources/G2Firmware/README.md +3 -3
- package/ios/Source/Resources/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/ios/Source/Resources/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
- package/ios/Source/controllers/R1.swift +145 -49
- package/ios/Source/sgcs/G2.swift +311 -54
- package/ios/Source/sgcs/G2CfwRenderer.swift +24 -8
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +8 -8
- package/ios/Source/sgcs/SGCManager.swift +13 -0
- package/ios/Source/utils/G2NotifyFile.swift +116 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +2 -2
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +91 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +37 -1
- package/src/_private/BluetoothSdkModule.ts +0 -1
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +0 -1
package/README.md
CHANGED
|
@@ -352,7 +352,7 @@ Android and iOS async APIs use `BluetoothSdkException` / `BluetoothSdkError` for
|
|
|
352
352
|
React Native apps should use `MentraLiveOtaFlow` or `useMentraLiveOta` from
|
|
353
353
|
`@glassly/engine/ota`. They provide the same tested Wi-Fi/hotspot,
|
|
354
354
|
APK/MTK/BES, restart, retry, and verification flow as the Glassly App. See
|
|
355
|
-
[Update Mentra Live](https://docs.glassly.com/
|
|
355
|
+
[Update Mentra Live](https://docs.glassly.com/bluetooth-sdk/software-update).
|
|
356
356
|
|
|
357
357
|
The Bluetooth SDK exposes the lower-level commands and transport capabilities
|
|
358
358
|
that Glassly Engine and native apps build on. They are not a replacement for the
|
|
@@ -371,7 +371,7 @@ Engine coordinator:
|
|
|
371
371
|
Release CI embeds an immutable manifest URL and checksum into every published
|
|
372
372
|
SDK distribution. The completed coordinated release record correlates the SDK
|
|
373
373
|
package with its exact manifest and OTA bundle. A completed release is available at
|
|
374
|
-
`https://github.com/
|
|
374
|
+
`https://github.com/tetramo-labs/glassly/releases/tag/glassly-v<releaseIdentity>`
|
|
375
375
|
and contains `glassly-release-<releaseIdentity>.json`, which records the exact
|
|
376
376
|
package coordinates, asset URLs, sizes, checksums, and build provenance.
|
|
377
377
|
|
|
@@ -409,7 +409,7 @@ OTA requires Mentra Live glasses firmware that supports the ASG OTA protocol and
|
|
|
409
409
|
|
|
410
410
|
For release selection, bundle verification, analytics opt-out, internal server
|
|
411
411
|
requirements, and disconnected validation, see the
|
|
412
|
-
[air-gapped deployment guide](https://docs.glassly.com/
|
|
412
|
+
[air-gapped deployment guide](https://docs.glassly.com/bluetooth-sdk/air-gapped-deployment).
|
|
413
413
|
|
|
414
414
|
Mentra Live also rejects `ota_start` before acknowledgement when its known battery level is below 5%, emitting a failed `ota_status` with `error_message: "battery_low"`. Unknown battery state remains fail-open, so apps should keep their own (typically stricter) user-facing battery policy.
|
|
415
415
|
|
|
@@ -560,4 +560,4 @@ secrets, not in the repository.
|
|
|
560
560
|
|
|
561
561
|
## Starter Example App
|
|
562
562
|
|
|
563
|
-
The [Glassly Bluetooth SDK Starter Kit](https://github.com/
|
|
563
|
+
The [Glassly Bluetooth SDK Starter Kit](https://github.com/tetramo-labs/glassly-starter-kit) includes starter example apps for Android, iOS, and React Native / Expo. The React Native starter demonstrates scan/connect, display, camera photo upload, RTMP/SRT/WebRTC streaming, Wi-Fi/hotspot, microphone PCM, RGB LED, gallery mode, and console event inspection.
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
The Android library bundles the stock and Glassly CFW EVENOTA images stored in
|
|
4
4
|
this directory. They are copied from the sibling `g2flash` checkout:
|
|
5
5
|
|
|
6
|
-
- `g2_2.2.
|
|
7
|
-
- `g2_2.2.
|
|
6
|
+
- `g2_2.2.9.22.bin` -> `g2-stock-2.2.9.22.evenota.bin`
|
|
7
|
+
- `g2_2.2.9.22_cfw.bin` -> `g2-glassly-cfw-2.2.9.22.evenota.bin`
|
|
8
8
|
|
|
9
9
|
Run `./build_cfw.sh` in `g2flash` before manually replacing the CFW image. The
|
|
10
|
-
firmware binaries are
|
|
10
|
+
firmware binaries are committed so CI builds bundle them, and verified by size,
|
|
11
11
|
SHA-256, per-component CRC32C, and the MRAM guard before flashing.
|
|
Binary file
|
|
@@ -501,10 +501,6 @@ class BluetoothSdk private constructor(
|
|
|
501
501
|
deviceManager.showDashboard()
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
fun toggleHeadUp() {
|
|
505
|
-
deviceManager.headUp = !deviceManager.headUp
|
|
506
|
-
}
|
|
507
|
-
|
|
508
504
|
fun showNotificationsPanel() {
|
|
509
505
|
deviceManager.showNotificationsPanel()
|
|
510
506
|
}
|
|
@@ -550,7 +550,6 @@ class BluetoothSdkModule : Module() {
|
|
|
550
550
|
|
|
551
551
|
SdkAsyncFunction("showDashboard") { -> sdk?.showDashboard() }
|
|
552
552
|
|
|
553
|
-
SdkAsyncFunction("toggleHeadUp") { -> sdk?.toggleHeadUp() }
|
|
554
553
|
|
|
555
554
|
SdkAsyncFunction("showNativeNotificationPanel") { -> sdk?.showNotificationsPanel() }
|
|
556
555
|
|
|
@@ -176,10 +176,6 @@ class DeviceManager {
|
|
|
176
176
|
get() = DeviceStore.store.get("bluetooth", "should_send_transcript") as? Boolean ?: false
|
|
177
177
|
set(value) = DeviceStore.apply("bluetooth", "should_send_transcript", value)
|
|
178
178
|
|
|
179
|
-
private var contextualDashboard: Boolean
|
|
180
|
-
get() = DeviceStore.store.get("bluetooth", "contextual_dashboard") as? Boolean ?: true
|
|
181
|
-
set(value) = DeviceStore.apply("bluetooth", "contextual_dashboard", value)
|
|
182
|
-
|
|
183
179
|
private var dashboardHeight: Int
|
|
184
180
|
get() = (DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)?.toInt() ?: 4
|
|
185
181
|
set(value) = DeviceStore.apply("bluetooth", "dashboard_height", value)
|
|
@@ -223,10 +219,6 @@ class DeviceManager {
|
|
|
223
219
|
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
|
|
224
220
|
set(value) = DeviceStore.apply("bluetooth", "systemMicUnavailable", value)
|
|
225
221
|
|
|
226
|
-
public var headUp: Boolean
|
|
227
|
-
get() = DeviceStore.store.get("glasses", "headUp") as? Boolean ?: false
|
|
228
|
-
set(value) = DeviceStore.apply("glasses", "headUp", value)
|
|
229
|
-
|
|
230
222
|
private var micEnabled: Boolean
|
|
231
223
|
get() = DeviceStore.store.get("bluetooth", "micEnabled") as? Boolean ?: false
|
|
232
224
|
set(value) = DeviceStore.apply("bluetooth", "micEnabled", value)
|
|
@@ -280,12 +272,12 @@ class DeviceManager {
|
|
|
280
272
|
// STT
|
|
281
273
|
private var transcriber: SherpaOnnxTranscriber? = null
|
|
282
274
|
|
|
283
|
-
//
|
|
284
|
-
|
|
275
|
+
// Single retained display state. The JS engine owns view switching
|
|
276
|
+
// (dashboard vs main); native just paints and repaints the last frame.
|
|
277
|
+
private var viewState = ViewState(" ", " ", " ", "text_wall", "", null, null)
|
|
285
278
|
|
|
286
279
|
init {
|
|
287
280
|
Bridge.log("DeviceManager: init()")
|
|
288
|
-
initializeViewStates()
|
|
289
281
|
startForegroundService()
|
|
290
282
|
// setupPermissionMonitoring()
|
|
291
283
|
setupBluetoothStateMonitoring()
|
|
@@ -673,36 +665,6 @@ class DeviceManager {
|
|
|
673
665
|
}
|
|
674
666
|
}
|
|
675
667
|
|
|
676
|
-
private fun initializeViewStates() {
|
|
677
|
-
viewStates.clear()
|
|
678
|
-
|
|
679
|
-
// Matching Swift's 4 view states exactly
|
|
680
|
-
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
681
|
-
viewStates.add(
|
|
682
|
-
ViewState(
|
|
683
|
-
" ",
|
|
684
|
-
" ",
|
|
685
|
-
" ",
|
|
686
|
-
"text_wall",
|
|
687
|
-
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
688
|
-
null,
|
|
689
|
-
null
|
|
690
|
-
)
|
|
691
|
-
)
|
|
692
|
-
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
693
|
-
viewStates.add(
|
|
694
|
-
ViewState(
|
|
695
|
-
" ",
|
|
696
|
-
" ",
|
|
697
|
-
" ",
|
|
698
|
-
"text_wall",
|
|
699
|
-
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
700
|
-
null,
|
|
701
|
-
null
|
|
702
|
-
)
|
|
703
|
-
)
|
|
704
|
-
}
|
|
705
|
-
|
|
706
668
|
private fun statesEqual(s1: ViewState, s2: ViewState): Boolean {
|
|
707
669
|
val state1 =
|
|
708
670
|
"${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
|
|
@@ -736,12 +698,11 @@ class DeviceManager {
|
|
|
736
698
|
var borderRadius: Int? = null
|
|
737
699
|
)
|
|
738
700
|
|
|
739
|
-
//
|
|
740
|
-
// viewStates. When a slot holds a scene, viewStates carries a "scene"
|
|
701
|
+
// Retained SceneFrame. When it holds a scene, viewState carries a "scene"
|
|
741
702
|
// sentinel so sendCurrentState routes here. Holding the WHOLE frame keeps
|
|
742
|
-
// native re-dispatch coherent (
|
|
743
|
-
//
|
|
744
|
-
private
|
|
703
|
+
// native re-dispatch coherent (nav exit re-applies a complete scene, not
|
|
704
|
+
// whatever element happened to arrive last).
|
|
705
|
+
private var sceneState: SceneFrame? = null
|
|
745
706
|
// MARK: - End Unique
|
|
746
707
|
|
|
747
708
|
// MARK: - Voice Data Handling
|
|
@@ -1021,15 +982,12 @@ class DeviceManager {
|
|
|
1021
982
|
}
|
|
1022
983
|
|
|
1023
984
|
fun sendCurrentState() {
|
|
1024
|
-
val hUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
|
|
1025
|
-
// Bridge.log("MAN: sendCurrentState(): $isHeadUp")
|
|
1026
985
|
if (screenDisabled) {
|
|
1027
986
|
return
|
|
1028
987
|
}
|
|
1029
988
|
|
|
1030
989
|
// executor.execute {
|
|
1031
|
-
val
|
|
1032
|
-
val currentViewState: ViewState = viewStates[currentStateIndex]
|
|
990
|
+
val currentViewState: ViewState = viewState
|
|
1033
991
|
|
|
1034
992
|
if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) {
|
|
1035
993
|
// dont send the event to glasses that aren't there:
|
|
@@ -1093,7 +1051,7 @@ class DeviceManager {
|
|
|
1093
1051
|
}
|
|
1094
1052
|
|
|
1095
1053
|
"scene" -> {
|
|
1096
|
-
|
|
1054
|
+
sceneState?.let { sgc?.applySceneFrame(it) }
|
|
1097
1055
|
}
|
|
1098
1056
|
|
|
1099
1057
|
"clear_view" -> sgc?.clearDisplay()
|
|
@@ -1529,6 +1487,12 @@ class DeviceManager {
|
|
|
1529
1487
|
val projectName = (DeviceStore.store.get("bluetooth", "project_name") as? String)?.trim().orEmpty()
|
|
1530
1488
|
Bridge.saveSetting("project_name", projectName)
|
|
1531
1489
|
}
|
|
1490
|
+
|
|
1491
|
+
// Restore the ring: connecting glasses tears the controller down
|
|
1492
|
+
// (disconnect() nulls it), so bring a paired default controller back.
|
|
1493
|
+
if (DeviceStore.get("glasses", "controllerConnected") as? Boolean != true) {
|
|
1494
|
+
connectDefaultController()
|
|
1495
|
+
}
|
|
1532
1496
|
}
|
|
1533
1497
|
|
|
1534
1498
|
private fun syncSystemTimeOnceForConnection(connectionKey: String) {
|
|
@@ -1620,22 +1584,13 @@ class DeviceManager {
|
|
|
1620
1584
|
}
|
|
1621
1585
|
|
|
1622
1586
|
fun displayEvent(event: Map<String, Any>) {
|
|
1623
|
-
val view = event["view"] as? String
|
|
1624
|
-
if (view == null) {
|
|
1625
|
-
Bridge.log("MAN: Invalid view")
|
|
1626
|
-
return
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
val isDashboard = view == "dashboard"
|
|
1630
|
-
val stateIndex = if (isDashboard) 1 else 0
|
|
1631
|
-
|
|
1632
1587
|
// Scene frames (display.render() pipeline) take their own path: the
|
|
1633
1588
|
// whole frame is the unit, not a layout, and the host's per-element
|
|
1634
1589
|
// annotations make redundant frames self-deduping (all-"unchanged"
|
|
1635
1590
|
// frames no-op in the SGC base handler).
|
|
1636
1591
|
@Suppress("UNCHECKED_CAST") val sceneMap = event["scene"] as? Map<String, Any>
|
|
1637
1592
|
if (sceneMap != null) {
|
|
1638
|
-
handleSceneEvent(
|
|
1593
|
+
handleSceneEvent(sceneMap)
|
|
1639
1594
|
return
|
|
1640
1595
|
}
|
|
1641
1596
|
|
|
@@ -1647,8 +1602,8 @@ class DeviceManager {
|
|
|
1647
1602
|
// (e.g. a cloud app taking the view from a miniapp). Sweep the scene's
|
|
1648
1603
|
// elements first so they don't linger under the new content; clear_view
|
|
1649
1604
|
// wipes everything anyway.
|
|
1650
|
-
|
|
1651
|
-
|
|
1605
|
+
sceneState?.let { prevFrame ->
|
|
1606
|
+
sceneState = null
|
|
1652
1607
|
if (layoutType != "clear_view") {
|
|
1653
1608
|
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1654
1609
|
}
|
|
@@ -1687,33 +1642,25 @@ class DeviceManager {
|
|
|
1687
1642
|
borderRadius
|
|
1688
1643
|
)
|
|
1689
1644
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
if (statesEqual(currentState, newViewState)) {
|
|
1645
|
+
if (statesEqual(viewState, newViewState)) {
|
|
1693
1646
|
return
|
|
1694
1647
|
}
|
|
1695
1648
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
// send the state we just received if the user is currently in that state:
|
|
1699
|
-
if (stateIndex == 0 && !hUp) {
|
|
1700
|
-
sendCurrentState()
|
|
1701
|
-
} else if (stateIndex == 1 && hUp) {
|
|
1702
|
-
sendCurrentState()
|
|
1703
|
-
}
|
|
1649
|
+
viewState = newViewState
|
|
1650
|
+
sendCurrentState()
|
|
1704
1651
|
}
|
|
1705
1652
|
|
|
1706
|
-
/** Parse + store a scene frame, then dispatch it
|
|
1707
|
-
private fun handleSceneEvent(
|
|
1653
|
+
/** Parse + store a scene frame, then dispatch it. */
|
|
1654
|
+
private fun handleSceneEvent(sceneMap: Map<String, Any>) {
|
|
1708
1655
|
var frame = parseSceneFrame(sceneMap) ?: return
|
|
1709
|
-
val prevFrame =
|
|
1656
|
+
val prevFrame = sceneState
|
|
1710
1657
|
|
|
1711
1658
|
if (prevFrame == null) {
|
|
1712
1659
|
// Legacy→scene handoff: stale legacy content (e.g. a cloud app's
|
|
1713
1660
|
// text wall) must not linger under the scene's elements.
|
|
1714
1661
|
// clearDisplay is the per-device "wipe what's there" (blank-in-place
|
|
1715
1662
|
// on G2 — no page rebuild).
|
|
1716
|
-
val prevLegacyType =
|
|
1663
|
+
val prevLegacyType = viewState.layoutType
|
|
1717
1664
|
if (prevLegacyType.isNotEmpty() && prevLegacyType != "clear_view" && prevLegacyType != "scene") {
|
|
1718
1665
|
sgc?.clearDisplay()
|
|
1719
1666
|
}
|
|
@@ -1728,17 +1675,14 @@ class DeviceManager {
|
|
|
1728
1675
|
frame = frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
1729
1676
|
}
|
|
1730
1677
|
|
|
1731
|
-
// Store the REDISPATCH form: any later sendCurrentState (
|
|
1732
|
-
//
|
|
1678
|
+
// Store the REDISPATCH form: any later sendCurrentState (nav exit,
|
|
1679
|
+
// screen re-enable) must repaint the whole frame — the original
|
|
1733
1680
|
// annotations are only valid for the first dispatch right now.
|
|
1734
|
-
|
|
1681
|
+
sceneState =
|
|
1735
1682
|
frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
1736
|
-
|
|
1683
|
+
viewState = ViewState(" ", " ", " ", "scene", " ", null, null)
|
|
1737
1684
|
|
|
1738
|
-
|
|
1739
|
-
if ((stateIndex == 0 && !hUp) || (stateIndex == 1 && hUp)) {
|
|
1740
|
-
dispatchSceneFrame(frame)
|
|
1741
|
-
}
|
|
1685
|
+
dispatchSceneFrame(frame)
|
|
1742
1686
|
}
|
|
1743
1687
|
|
|
1744
1688
|
/** Guarded scene dispatch — mirrors sendCurrentState's send conditions. */
|
|
@@ -128,7 +128,6 @@ object DeviceStore {
|
|
|
128
128
|
store.set("bluetooth", "dashboard_depth", 2)
|
|
129
129
|
store.set("bluetooth", "head_up_angle", 30)
|
|
130
130
|
store.set("bluetooth", "imu_enabled", false)
|
|
131
|
-
store.set("bluetooth", "contextual_dashboard", true)
|
|
132
131
|
store.set("bluetooth", "gallery_mode", true)
|
|
133
132
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
134
133
|
store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.LOUDNESS_GATE_ENABLED)
|
|
@@ -198,9 +197,9 @@ object DeviceStore {
|
|
|
198
197
|
}
|
|
199
198
|
}
|
|
200
199
|
}
|
|
200
|
+
// Head position only reaches JS now; the engine owns dashboard switching.
|
|
201
201
|
"glasses" to "headUp" -> {
|
|
202
202
|
if (value is Boolean) {
|
|
203
|
-
DeviceManager.getInstance().sendCurrentState()
|
|
204
203
|
Bridge.sendHeadUp(value)
|
|
205
204
|
}
|
|
206
205
|
}
|
|
@@ -3,9 +3,9 @@ package com.glassly.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 = "0.1.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "0.1.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "glassly-0.1.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "0.1.0-dev.90"
|
|
7
|
+
const val RELEASE_SET_ID: String = "glassly-0.1.0-dev.90"
|
|
8
|
+
const val SOURCE_COMMIT: String = "1140c7490313353cb1d8da0782bf74c87d9119eb"
|
|
9
9
|
const val OTA_MANIFEST_URL: String = ""
|
|
10
10
|
const val OTA_MANIFEST_SHA256: String = ""
|
|
11
11
|
}
|
|
@@ -44,16 +44,101 @@ private object R1BLE {
|
|
|
44
44
|
|
|
45
45
|
val NAME_FILTERS = arrayOf("EVEN R1", "BCL60")
|
|
46
46
|
|
|
47
|
-
val
|
|
48
|
-
|
|
47
|
+
const val GESTURE_MARKER: Byte = 0xFF.toByte()
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
// MARK: - EUS transport encoder (phone → ring, channel 2)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recovered firmware contract (openCFW r1/ research): every phone→ring packet
|
|
54
|
+
* is a 12-byte "model" header
|
|
55
|
+
* [version, module, moduleVersion, serial LE16, status, command, subcommand,
|
|
56
|
+
* totalLength LE16, crc16 LE16]
|
|
57
|
+
* followed by the payload, where the phone-direction CRC-16 (CCITT rounds,
|
|
58
|
+
* init 0xFFFF) runs over the compact bytes [0,1,2,3,5,6,7,8,0x00] and then
|
|
59
|
+
* continues over the payload bytes. The model is carried on channel 2 inside a
|
|
60
|
+
* fragment train [sequence, crc32 LE32, ≤239 bytes]; the CRC-32 (poly
|
|
61
|
+
* 0x1EDC6F41, MSB-first, zero seed, no final xor) covers the whole model and
|
|
62
|
+
* sequence 0 terminates the train — all our packets fit a single fragment.
|
|
63
|
+
* Mirrors iOS R1.swift's R1Eus.
|
|
64
|
+
*/
|
|
65
|
+
private object R1Eus {
|
|
66
|
+
const val VERSION = 100
|
|
67
|
+
const val MODULE_SYSTEM = 0x01
|
|
68
|
+
const val MODULE_VERSION = 100
|
|
69
|
+
/** status bit 0x02 marks a SET; advStart requires it */
|
|
70
|
+
const val STATUS_SET = 0x02
|
|
71
|
+
|
|
72
|
+
/** system subcommand 0x08: payload [0x01] claims the phone role; the ring
|
|
73
|
+
* requests link encryption/bonding in response */
|
|
74
|
+
const val SUBCMD_PAIR_AUTH = 0x08
|
|
75
|
+
/** system subcommand 0x0A: 12-byte payload = two 6-byte glasses arm MACs;
|
|
76
|
+
* the ring stores both targets and fast-advertises for them */
|
|
77
|
+
const val SUBCMD_ADV_START = 0x0A
|
|
78
|
+
|
|
79
|
+
private fun crc16Step(crc: Int, byte: Int): Int {
|
|
80
|
+
var c = ((crc shr 8) or (crc shl 8)) and 0xFFFF
|
|
81
|
+
c = c xor (byte and 0xFF)
|
|
82
|
+
c = c xor ((c and 0x00FF) shr 4)
|
|
83
|
+
c = (c xor (c shl 12)) and 0xFFFF
|
|
84
|
+
c = (c xor ((c and 0x00FF) shl 5)) and 0xFFFF
|
|
85
|
+
return c
|
|
86
|
+
}
|
|
55
87
|
|
|
56
|
-
|
|
88
|
+
private fun phoneCrc16(header: ByteArray, payload: ByteArray): Int {
|
|
89
|
+
var crc = 0xFFFF
|
|
90
|
+
// compact header: bytes 0-3 and 5-8, then a zero placeholder
|
|
91
|
+
for (b in intArrayOf(
|
|
92
|
+
header[0].toInt(), header[1].toInt(), header[2].toInt(), header[3].toInt(),
|
|
93
|
+
header[5].toInt(), header[6].toInt(), header[7].toInt(), header[8].toInt(), 0
|
|
94
|
+
)) {
|
|
95
|
+
crc = crc16Step(crc, b)
|
|
96
|
+
}
|
|
97
|
+
for (b in payload) {
|
|
98
|
+
crc = crc16Step(crc, b.toInt())
|
|
99
|
+
}
|
|
100
|
+
return crc
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun crc32(data: ByteArray): Long {
|
|
104
|
+
var crc = 0L
|
|
105
|
+
for (byte in data) {
|
|
106
|
+
crc = crc xor ((byte.toLong() and 0xFF) shl 24)
|
|
107
|
+
repeat(8) {
|
|
108
|
+
crc = if ((crc and 0x8000_0000L) != 0L) {
|
|
109
|
+
((crc shl 1) xor 0x1EDC6F41L) and 0xFFFF_FFFFL
|
|
110
|
+
} else {
|
|
111
|
+
(crc shl 1) and 0xFFFF_FFFFL
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return crc
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Build one single-fragment system-module command packet. */
|
|
119
|
+
fun frame(subcommand: Int, status: Int, serial: Int, payload: ByteArray): ByteArray {
|
|
120
|
+
val length = 12 + payload.size
|
|
121
|
+
val header = byteArrayOf(
|
|
122
|
+
VERSION.toByte(), MODULE_SYSTEM.toByte(), MODULE_VERSION.toByte(),
|
|
123
|
+
(serial and 0xFF).toByte(), ((serial shr 8) and 0xFF).toByte(),
|
|
124
|
+
status.toByte(), 0x00, subcommand.toByte(),
|
|
125
|
+
(length and 0xFF).toByte(), ((length shr 8) and 0xFF).toByte(),
|
|
126
|
+
0, 0,
|
|
127
|
+
)
|
|
128
|
+
val crc16 = phoneCrc16(header, payload)
|
|
129
|
+
header[10] = (crc16 and 0xFF).toByte()
|
|
130
|
+
header[11] = ((crc16 shr 8) and 0xFF).toByte()
|
|
131
|
+
val model = header + payload
|
|
132
|
+
|
|
133
|
+
val outer = crc32(model)
|
|
134
|
+
return byteArrayOf(
|
|
135
|
+
0x00, // sequence 0: single-fragment train
|
|
136
|
+
(outer and 0xFF).toByte(),
|
|
137
|
+
((outer shr 8) and 0xFF).toByte(),
|
|
138
|
+
((outer shr 16) and 0xFF).toByte(),
|
|
139
|
+
((outer shr 24) and 0xFF).toByte(),
|
|
140
|
+
) + model
|
|
141
|
+
}
|
|
57
142
|
}
|
|
58
143
|
|
|
59
144
|
// MARK: - R1 Gesture Types
|
|
@@ -166,9 +251,22 @@ class R1 : ControllerManager() {
|
|
|
166
251
|
|
|
167
252
|
private fun getRingMacFromMap(name: String): String? = loadRingMacAddressMap()[name]
|
|
168
253
|
|
|
169
|
-
// Reconnection
|
|
254
|
+
// Reconnection
|
|
170
255
|
private val reconnectionManager = R1ReconnectionManager()
|
|
171
256
|
|
|
257
|
+
private fun startReconnectionTimer() {
|
|
258
|
+
reconnectionManager.start {
|
|
259
|
+
val connected = DeviceStore.get("glasses", "controllerConnected") as? Boolean ?: false
|
|
260
|
+
if (connected) {
|
|
261
|
+
true // already connected — stop retrying
|
|
262
|
+
} else {
|
|
263
|
+
Bridge.log("R1: Attempting reconnection...")
|
|
264
|
+
mainHandler.post { startScan() }
|
|
265
|
+
false // keep trying
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
172
270
|
// Battery
|
|
173
271
|
private var _batteryLevel: Int = -1
|
|
174
272
|
set(value) {
|
|
@@ -378,27 +476,52 @@ class R1 : ControllerManager() {
|
|
|
378
476
|
}
|
|
379
477
|
}
|
|
380
478
|
|
|
479
|
+
// MARK: - EUS commands
|
|
480
|
+
|
|
481
|
+
private var eusSerial = 0
|
|
482
|
+
|
|
483
|
+
private fun nextEusSerial(): Int {
|
|
484
|
+
eusSerial = (eusSerial + 1) and 0xFFFF
|
|
485
|
+
if (eusSerial == 0) eusSerial = 1
|
|
486
|
+
return eusSerial
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* EUS system commands go to WRITE_CHAR_2 only — channel 2 is the framed
|
|
491
|
+
* transport; channel 1 speaks the separate legacy protocol.
|
|
492
|
+
*/
|
|
493
|
+
private fun sendEusCommand(subcommand: Int, payload: ByteArray, log: String) {
|
|
494
|
+
val wc = writeChar2
|
|
495
|
+
if (wc == null) {
|
|
496
|
+
Bridge.log("R1: $log: no channel-2 write characteristic")
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
val packet = R1Eus.frame(subcommand, R1Eus.STATUS_SET, nextEusSerial(), payload)
|
|
500
|
+
writeNoResponse(wc, packet)
|
|
501
|
+
Bridge.log("R1: $log sent (${packet.size} bytes)")
|
|
502
|
+
}
|
|
503
|
+
|
|
381
504
|
// MARK: - Init Sequence
|
|
382
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Runs after notify subscriptions are confirmed: claim the phone role,
|
|
508
|
+
* then mark the phone link ready.
|
|
509
|
+
*/
|
|
383
510
|
private fun runInitSequence() {
|
|
384
511
|
if (initSequenceRun) return
|
|
385
512
|
initSequenceRun = true
|
|
386
513
|
|
|
387
514
|
Bridge.log("R1: Running init sequence")
|
|
388
515
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
Bridge.log("R1: No write characteristics found, skipping init")
|
|
516
|
+
if (writeChar2 == null) {
|
|
517
|
+
Bridge.log("R1: No channel-2 write characteristic, skipping init")
|
|
392
518
|
markConnected()
|
|
393
519
|
return
|
|
394
520
|
}
|
|
395
521
|
|
|
396
|
-
|
|
522
|
+
sendEusCommand(R1Eus.SUBCMD_PAIR_AUTH, byteArrayOf(0x01), "pairAuth")
|
|
397
523
|
|
|
398
|
-
mainHandler.postDelayed({
|
|
399
|
-
writeChars.forEach { writeNoResponse(it, R1BLE.CONFIG_11) }
|
|
400
|
-
markConnected()
|
|
401
|
-
}, 200)
|
|
524
|
+
mainHandler.postDelayed({ markConnected() }, 300)
|
|
402
525
|
}
|
|
403
526
|
|
|
404
527
|
private fun writeNoResponse(char: BluetoothGattCharacteristic, data: ByteArray) {
|
|
@@ -430,7 +553,10 @@ class R1 : ControllerManager() {
|
|
|
430
553
|
}
|
|
431
554
|
DeviceStore.apply("glasses", "controllerMacAddress", mac)
|
|
432
555
|
DeviceStore.apply("glasses", "controllerConnected", true)
|
|
433
|
-
//
|
|
556
|
+
// The phone link owns these flags: a ready phone link IS pairing
|
|
557
|
+
// success (fires handleControllerReady). The glasses link is layered
|
|
558
|
+
// on afterwards via advStart + RING_CONNECT_INFO.
|
|
559
|
+
DeviceStore.apply("glasses", "controllerFullyBooted", true)
|
|
434
560
|
|
|
435
561
|
// tell the ring to connect to the glasses if we have its mac address:
|
|
436
562
|
connectToGlasses()
|
|
@@ -445,46 +571,41 @@ class R1 : ControllerManager() {
|
|
|
445
571
|
}
|
|
446
572
|
|
|
447
573
|
/**
|
|
448
|
-
* Tells the ring
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* TODO: BleRing1CmdPublicExt.sendCmd may add additional outer framing (length/seq/CRC) around
|
|
454
|
-
* the 3-byte header + MAC. If the ring rejects this raw payload, decode the wrapper.
|
|
574
|
+
* Tells the ring which glasses may connect to it: EUS advStart whose
|
|
575
|
+
* 12-byte payload is the two glasses arm MACs. The ring stores both
|
|
576
|
+
* targets, fast-advertises while a glasses role is open, and accepts a
|
|
577
|
+
* connection from either arm. With only one arm MAC known it is sent for
|
|
578
|
+
* both targets — the ring matches against either slot.
|
|
455
579
|
*/
|
|
456
580
|
private fun connectToGlasses() {
|
|
457
|
-
|
|
458
|
-
|
|
581
|
+
fun stored(key: String): String? =
|
|
582
|
+
(DeviceStore.get("glasses", key) as? String)?.takeIf { it.isNotEmpty() }
|
|
583
|
+
|
|
584
|
+
val left = stored("leftMacAddress")
|
|
585
|
+
?: stored("bluetoothMacAddress")
|
|
459
586
|
?: prefs.getString("glasses_btMacAddress", null)
|
|
460
|
-
|
|
587
|
+
val right = stored("rightMacAddress")
|
|
588
|
+
?: prefs.getString("glasses_btMacAddressRight", null)
|
|
589
|
+
|
|
590
|
+
if (left == null) {
|
|
461
591
|
Bridge.log("R1: connectToGlasses: no glasses MAC")
|
|
462
592
|
return
|
|
463
593
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
val macBytes = parseMac(glassesMac)
|
|
468
|
-
if (macBytes == null) {
|
|
594
|
+
val leftBytes = parseMac(left)
|
|
595
|
+
if (leftBytes == null) {
|
|
469
596
|
Bridge.log("R1: connectToGlasses: could not parse glasses MAC")
|
|
470
597
|
return
|
|
471
598
|
}
|
|
472
|
-
val
|
|
473
|
-
if (wc == null) {
|
|
474
|
-
Bridge.log("R1: connectToGlasses: no write characteristic")
|
|
475
|
-
return
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
val payload = byteArrayOf(R1BLE.CMD_SYSTEM, R1BLE.MODULE_SYSTEM, R1BLE.SUBCMD_ADV_START) + macBytes
|
|
479
|
-
Bridge.log("R1: advStart sent")
|
|
599
|
+
val rightBytes = right?.let { parseMac(it) } ?: leftBytes
|
|
480
600
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
Bridge.log("R1: connectToGlasses writeCharacteristic SecurityException: ${e.message}")
|
|
601
|
+
// Cache so we can re-send even before the glasses are next scanned.
|
|
602
|
+
prefs.edit().apply {
|
|
603
|
+
putString("glasses_btMacAddress", left)
|
|
604
|
+
if (right != null) putString("glasses_btMacAddressRight", right)
|
|
605
|
+
apply()
|
|
487
606
|
}
|
|
607
|
+
|
|
608
|
+
sendEusCommand(R1Eus.SUBCMD_ADV_START, leftBytes + rightBytes, "advStart")
|
|
488
609
|
}
|
|
489
610
|
|
|
490
611
|
/** Parse "AA:BB:CC:DD:EE:FF" or "AABBCCDDEEFF" into 6 raw bytes. */
|
|
@@ -598,8 +719,8 @@ class R1 : ControllerManager() {
|
|
|
598
719
|
pendingReads.clear()
|
|
599
720
|
descriptorWriteInFlight = false
|
|
600
721
|
readInFlight = false
|
|
601
|
-
ringMacAddress
|
|
602
|
-
|
|
722
|
+
// ringMacAddress/ringBleAddress persist across disconnects so
|
|
723
|
+
// reconnection can find the ring again; only forget() clears them.
|
|
603
724
|
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
604
725
|
DeviceStore.apply("glasses", "controllerFullyBooted", false)
|
|
605
726
|
}
|
|
@@ -645,7 +766,7 @@ class R1 : ControllerManager() {
|
|
|
645
766
|
try { gatt.close() } catch (e: SecurityException) {}
|
|
646
767
|
if (isDisconnecting) return@post
|
|
647
768
|
resetConnectionState()
|
|
648
|
-
|
|
769
|
+
startReconnectionTimer()
|
|
649
770
|
}
|
|
650
771
|
}
|
|
651
772
|
}
|
|
@@ -906,7 +1027,6 @@ class R1 : ControllerManager() {
|
|
|
906
1027
|
|
|
907
1028
|
// MARK: - R1 Reconnection Manager
|
|
908
1029
|
// Coroutine-based port of Swift's R1ReconnectionManager actor.
|
|
909
|
-
// Currently unwired — matches iOS which leaves the reconnection timer commented out.
|
|
910
1030
|
|
|
911
1031
|
private class R1ReconnectionManager(
|
|
912
1032
|
private val intervalSeconds: Long = 30L,
|