@glassly/bluetooth-sdk 0.1.0-dev.98 → 0.2.8-dev.444

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 (155) hide show
  1. package/README.md +47 -6
  2. package/android/build.gradle +7 -7
  3. package/android/lc3Lib/build.gradle +2 -2
  4. package/android/src/main/assets/G2Firmware/README.md +27 -2
  5. package/android/src/main/assets/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  6. package/android/src/main/assets/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  7. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
  8. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
  9. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
  10. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
  11. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
  12. package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
  13. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
  14. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
  15. package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
  16. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1075 -257
  17. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
  18. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
  19. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
  20. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
  21. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
  22. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
  23. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
  24. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
  25. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
  26. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
  27. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
  28. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
  29. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
  30. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
  31. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
  32. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
  33. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
  34. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
  35. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
  36. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
  37. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
  38. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
  39. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
  40. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
  41. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
  42. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
  43. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
  44. package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
  45. package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
  46. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
  47. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
  48. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
  49. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
  50. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
  51. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
  52. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
  53. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
  54. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
  55. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
  56. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
  57. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
  58. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
  59. package/build/BluetoothSdk.types.d.ts +209 -20
  60. package/build/BluetoothSdk.types.d.ts.map +1 -1
  61. package/build/BluetoothSdk.types.js.map +1 -1
  62. package/build/_internal.d.ts +1 -1
  63. package/build/_internal.d.ts.map +1 -1
  64. package/build/_internal.js.map +1 -1
  65. package/build/_private/BluetoothSdkModule.d.ts +30 -1
  66. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  67. package/build/_private/BluetoothSdkModule.js.map +1 -1
  68. package/build/generated/changelogCatalog.d.ts +28 -1
  69. package/build/generated/changelogCatalog.d.ts.map +1 -1
  70. package/build/generated/changelogCatalog.js +37 -1
  71. package/build/generated/changelogCatalog.js.map +1 -1
  72. package/build/generated/releaseMetadata.js +4 -4
  73. package/build/generated/releaseMetadata.js.map +1 -1
  74. package/build/index.d.ts +1 -1
  75. package/build/index.d.ts.map +1 -1
  76. package/build/index.js +14 -0
  77. package/build/index.js.map +1 -1
  78. package/ios/BluetoothSdkModule.swift +102 -13
  79. package/ios/GlasslyBluetoothSDK.podspec +1 -1
  80. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
  81. package/ios/Source/BluetoothSDK.swift +53 -0
  82. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  83. package/ios/Source/Bridge.swift +13 -0
  84. package/ios/Source/DeviceManager.swift +151 -33
  85. package/ios/Source/DeviceStore.swift +28 -14
  86. package/ios/Source/DisplayPacer.swift +180 -0
  87. package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
  88. package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
  89. package/ios/Source/GlassesNotifications.swift +181 -0
  90. package/ios/Source/ObservableStore.swift +14 -2
  91. package/ios/Source/Resources/G2Firmware/README.md +27 -2
  92. package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  93. package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  94. package/ios/Source/controllers/R1.swift +0 -1
  95. package/ios/Source/internal/BluetoothAvailability.swift +31 -0
  96. package/ios/Source/sgcs/G2.swift +1434 -504
  97. package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
  98. package/ios/Source/sgcs/G2BleLink.swift +102 -0
  99. package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
  100. package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
  101. package/ios/Source/sgcs/G2CfwScene.swift +779 -0
  102. package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
  103. package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
  104. package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
  105. package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
  106. package/ios/Source/sgcs/G2Flasher.swift +9 -9
  107. package/ios/Source/sgcs/G2Keyboard.swift +780 -0
  108. package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
  109. package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
  110. package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
  111. package/ios/Source/sgcs/G2RingBattery.swift +22 -0
  112. package/ios/Source/sgcs/G2Sensors.swift +75 -0
  113. package/ios/Source/sgcs/G2TouchInput.swift +51 -0
  114. package/ios/Source/sgcs/G2WritePacing.swift +18 -0
  115. package/ios/Source/sgcs/MentraNex.swift +44 -44
  116. package/ios/Source/sgcs/SGCManager.swift +120 -13
  117. package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
  118. package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
  119. package/ios/Source/status/DeviceStatus.swift +27 -0
  120. package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
  121. package/ios/Source/utils/Enums.swift +0 -1
  122. package/ios/Source/utils/G1Text.swift +0 -1
  123. package/ios/Source/utils/G2NotifyFile.swift +20 -10
  124. package/ios/Source/utils/JSCExperiment.swift +6 -6
  125. package/ios/Source/utils/Models.swift +0 -1
  126. package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
  127. package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
  128. package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
  129. package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
  130. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
  131. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
  132. package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
  133. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
  134. package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
  135. package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
  136. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
  137. package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
  138. package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
  139. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
  140. package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
  141. package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
  142. package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
  143. package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
  144. package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
  145. package/package.json +1 -1
  146. package/plugin/build/withAndroid.js +2 -2
  147. package/plugin/build/withIos.js +1 -1
  148. package/scripts/gen-nex-proto.sh +12 -0
  149. package/scripts/inject-ios-sdk-version.mjs +1 -1
  150. package/src/BluetoothSdk.types.ts +214 -19
  151. package/src/_internal.ts +1 -1
  152. package/src/_private/BluetoothSdkModule.ts +40 -0
  153. package/src/generated/changelogCatalog.ts +37 -1
  154. package/src/generated/releaseMetadata.ts +4 -4
  155. package/src/index.ts +24 -0
@@ -0,0 +1,181 @@
1
+ import Foundation
2
+
3
+ public enum GlassesNotificationError: Error, LocalizedError {
4
+ case unsupported
5
+ case notConnected
6
+ case invalidConfig(String)
7
+
8
+ public var errorDescription: String? {
9
+ switch self {
10
+ case .unsupported:
11
+ "The connected glasses have no notification surface"
12
+ case .notConnected:
13
+ "No glasses connected"
14
+ case let .invalidConfig(reason):
15
+ reason
16
+ }
17
+ }
18
+ }
19
+
20
+ /// Desired on-glass notification behaviour in glasses-neutral terms. Drivers map
21
+ /// it onto their protocol and snap values their firmware cannot take.
22
+ public struct GlassesNotificationConfig: Equatable, Sendable {
23
+ public struct AllowedApp: Equatable, Sendable {
24
+ public let id: String
25
+ public let name: String
26
+
27
+ public init(id: String, name: String) {
28
+ self.id = id
29
+ self.name = name
30
+ }
31
+ }
32
+
33
+ /// Popup duration the firmware is allowed to be handed, in seconds.
34
+ static let popupSecondsRange = 0 ... 60
35
+
36
+ public let enabled: Bool
37
+ public let popups: Bool
38
+ /// 0 lets the firmware choose.
39
+ public let popupSeconds: Int
40
+ public let holdDuringApps: Bool
41
+ /// Android package names or iOS bundle ids.
42
+ public let blockedApps: [String]
43
+ /// Known ids minus blocked, for firmware whose filter is an allowlist.
44
+ public let allowedApps: [AllowedApp]
45
+
46
+ public init(
47
+ enabled: Bool,
48
+ popups: Bool,
49
+ popupSeconds: Int,
50
+ holdDuringApps: Bool,
51
+ blockedApps: [String],
52
+ allowedApps: [AllowedApp]
53
+ ) {
54
+ self.enabled = enabled
55
+ self.popups = popups
56
+ self.popupSeconds = popupSeconds
57
+ self.holdDuringApps = holdDuringApps
58
+ self.blockedApps = blockedApps
59
+ self.allowedApps = allowedApps
60
+ }
61
+
62
+ /// Numbers arrive as NSNumber across the Expo bridge; `as? Int` would drop a Double.
63
+ public init(dictionary: [String: Any]) throws {
64
+ enabled = dictionary["enabled"] as? Bool ?? true
65
+ popups = dictionary["popups"] as? Bool ?? true
66
+ holdDuringApps = dictionary["holdDuringApps"] as? Bool ?? true
67
+
68
+ let seconds = (dictionary["popupSeconds"] as? NSNumber)?.intValue ?? 0
69
+ guard Self.popupSecondsRange.contains(seconds) else {
70
+ throw GlassesNotificationError.invalidConfig(
71
+ "popupSeconds \(seconds) outside \(Self.popupSecondsRange.lowerBound)…\(Self.popupSecondsRange.upperBound)"
72
+ )
73
+ }
74
+ popupSeconds = seconds
75
+
76
+ blockedApps = (dictionary["blockedApps"] as? [String] ?? []).filter { !$0.isEmpty }
77
+ allowedApps = (dictionary["allowedApps"] as? [[String: Any]] ?? []).compactMap { entry in
78
+ guard let id = entry["id"] as? String, !id.isEmpty else { return nil }
79
+ return AllowedApp(id: id, name: entry["name"] as? String ?? id)
80
+ }
81
+ }
82
+
83
+ public var dictionary: [String: Any] {
84
+ [
85
+ "enabled": enabled,
86
+ "popups": popups,
87
+ "popupSeconds": popupSeconds,
88
+ "holdDuringApps": holdDuringApps,
89
+ "blockedApps": blockedApps,
90
+ "allowedApps": allowedApps.map { ["id": $0.id, "name": $0.name] },
91
+ ]
92
+ }
93
+ }
94
+
95
+ public struct GlassesNotificationStatus: Sendable {
96
+ /// "phone_upload" | "ancs" | "none"
97
+ public let feed: String
98
+ /// "granted" | "denied" | "unknown" | "not_needed"
99
+ public let access: String
100
+ /// "idle" | "applying" | "applied" | "stale" | "error"
101
+ public let phase: String
102
+
103
+ public let supported: Bool
104
+ public let config: GlassesNotificationConfig?
105
+ public let error: String?
106
+
107
+ public init(
108
+ supported: Bool,
109
+ feed: String,
110
+ access: String,
111
+ phase: String,
112
+ config: GlassesNotificationConfig?,
113
+ error: String?
114
+ ) {
115
+ self.supported = supported
116
+ self.feed = feed
117
+ self.access = access
118
+ self.phase = phase
119
+ self.config = config
120
+ self.error = error
121
+ }
122
+
123
+ public static let unsupported = GlassesNotificationStatus(
124
+ supported: false,
125
+ feed: "none",
126
+ access: "not_needed",
127
+ phase: "idle",
128
+ config: nil,
129
+ error: nil
130
+ )
131
+
132
+ public var dictionary: [String: Any] {
133
+ [
134
+ "supported": supported,
135
+ "feed": feed,
136
+ "access": access,
137
+ "phase": phase,
138
+ "config": config?.dictionary ?? NSNull(),
139
+ "error": error ?? NSNull(),
140
+ ]
141
+ }
142
+ }
143
+
144
+ /// One phone notification handed to glasses that present content themselves.
145
+ public struct PhoneNotificationUpload: Sendable {
146
+ public let notificationId: String
147
+ public let packageName: String
148
+ public let appName: String
149
+ public let title: String
150
+ public let subtitle: String
151
+ public let body: String
152
+ public let timestampMs: Int64
153
+
154
+ public init(
155
+ notificationId: String,
156
+ packageName: String,
157
+ appName: String,
158
+ title: String,
159
+ subtitle: String,
160
+ body: String,
161
+ timestampMs: Int64
162
+ ) {
163
+ self.notificationId = notificationId
164
+ self.packageName = packageName
165
+ self.appName = appName
166
+ self.title = title
167
+ self.subtitle = subtitle
168
+ self.body = body
169
+ self.timestampMs = timestampMs
170
+ }
171
+
172
+ public init(dictionary: [String: Any]) {
173
+ notificationId = dictionary["notificationId"] as? String ?? ""
174
+ packageName = dictionary["packageName"] as? String ?? ""
175
+ appName = dictionary["appName"] as? String ?? ""
176
+ title = dictionary["title"] as? String ?? ""
177
+ subtitle = dictionary["subtitle"] as? String ?? ""
178
+ body = dictionary["body"] as? String ?? ""
179
+ timestampMs = (dictionary["timestampMs"] as? NSNumber)?.int64Value ?? 0
180
+ }
181
+ }
@@ -106,13 +106,25 @@ class ObservableStore {
106
106
  if let l = lhs as? Double, let r = rhs as? Double { return l == r }
107
107
  if let l = lhs as? [String], let r = rhs as? [String] { return l == r }
108
108
  if let l = lhs as? [[String: Any]], let r = rhs as? [[String: Any]] {
109
- return toJson(l) == toJson(r)
109
+ return jsonEqual(l, r)
110
+ }
111
+ // Dict-valued settings — without this every apply re-fires the subscriber.
112
+ if let l = lhs as? [String: Any], let r = rhs as? [String: Any] {
113
+ return jsonEqual(l, r)
110
114
  }
111
115
  return false
112
116
  }
113
117
 
118
+ /// Unserializable on either side is "changed" — nil == nil must not read as equal.
119
+ private func jsonEqual(_ lhs: Any, _ rhs: Any) -> Bool {
120
+ guard let l = toJson(lhs), let r = toJson(rhs) else { return false }
121
+ return l == r
122
+ }
123
+
114
124
  private func toJson(_ value: Any) -> String? {
115
- guard let data = try? JSONSerialization.data(withJSONObject: value) else { return nil }
125
+ guard JSONSerialization.isValidJSONObject(value),
126
+ let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
127
+ else { return nil }
116
128
  return String(data: data, encoding: .utf8)
117
129
  }
118
130
  }
@@ -3,8 +3,33 @@
3
3
  The iOS target bundles the stock and Glassly CFW EVENOTA images stored in this
4
4
  directory. They were copied from the sibling `g2flash` checkout:
5
5
 
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`
6
+ - `g2_2.3.0.24.bin` -> `g2-stock-2.3.0.24.evenota.bin`
7
+ - `g2_2.3.0.24_cfw.bin` -> `g2-glassly-cfw-2.3.0.24.evenota.bin`
8
8
 
9
9
  Run `./build_cfw.sh` in `g2flash` before manually replacing the CFW image. The
10
10
  firmware binaries are committed so CI builds bundle them.
11
+
12
+ The bundled CFW is **GLASSLYCFW/37**, built in `tetramo-labs/g2flash` at
13
+ the commit named in its README, on stock G2 **2.3.0.24** (upstream
14
+ jimrandomh/g2flash's new base with its audited stock ABI table). Every stock
15
+ address was re-derived for 2.3.0.24; the fork keeps its own ANCS relay,
16
+ phone-selectable fast BLE link and four-microphone relay, and gains upstream's
17
+ ring touch-down and ring-report forwarding. Verified on hardware with the
18
+ g2flash shapes, vector and Bad Apple suites and the fast-link toggle.
19
+ Revision 38 adds the retained object cache (modes 37-41): objects and assets
20
+ with 32-bit ids and versions survive a hidden view, so reopening the dashboard
21
+ is one SHOW of references; the raw texture writes (mode 18) are gone. Verified
22
+ on hardware with the g2flash vector and shapes suites (43/43).
23
+ Field 100 is the bare revision string; clients gate on the number (the app
24
+ requires 39).
25
+
26
+ - Size: **4,661,963 bytes**
27
+ - SHA-256: `4c7bf9a552df1a42f26b3378425c7c2121ca4058fed7283ec863c3f8d2cd13fe`
28
+
29
+ Keep both platform copies and their `G2FirmwareCatalog` size/hash pins in sync.
30
+
31
+ Revision 39 adds the BLE keyboard bridge (`patches/keyboard.c`): the right
32
+ lens hosts one HID-over-GATT keyboard, driven by the phone over settings
33
+ fields 130 (control) and 131 (events). Verified on hardware from the g2flash
34
+ reference client (pairing, HOGP discovery and live key events); the phone
35
+ path (G2KeyboardBridge) is not yet hardware-validated. The app requires 39.
@@ -1,6 +1,5 @@
1
1
  //
2
2
  // R1.swift
3
- // Glassly_Manager
4
3
  //
5
4
  // Even Realities R1 Smart Ring controller
6
5
  // Protocol reverse-engineered from BTSnoop captures and firmware analysis
@@ -1,15 +1,34 @@
1
1
  import CoreBluetooth
2
2
  import Foundation
3
3
 
4
+ /// Phone-Bluetooth state watcher shared by every SDK command that needs the
5
+ /// radio. Creating a `CBCentralManager` is what makes iOS show the Bluetooth
6
+ /// permission alert, so the manager is created lazily on the first command
7
+ /// that needs Bluetooth, never on app launch. `BluetoothSDK` is built when
8
+ /// the Expo module is created, so an eager manager here prompts a fresh
9
+ /// install before onboarding.
4
10
  final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
5
11
  static let shared = BluetoothAvailability()
6
12
 
7
13
  private var centralManager: CBCentralManager?
8
14
  private var state: CBManagerState = .unknown
15
+ /// CoreBluetooth reports state asynchronously after the manager exists.
16
+ /// Until the first report, `.unknown` means "not asked yet", not "broken".
17
+ private var hasReportedState = false
9
18
  private var listeners: [UUID: (CBManagerState) -> Void] = [:]
10
19
 
11
20
  override private init() {
12
21
  super.init()
22
+ }
23
+
24
+ /// True once a central manager exists (the permission alert has been
25
+ /// shown, or authorization was already decided).
26
+ var isActivated: Bool {
27
+ centralManager != nil
28
+ }
29
+
30
+ private func activate() {
31
+ guard centralManager == nil else { return }
13
32
  centralManager = CBCentralManager(
14
33
  delegate: self,
15
34
  queue: .main,
@@ -20,11 +39,15 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
20
39
 
21
40
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
22
41
  state = central.state
42
+ hasReportedState = true
23
43
  for listener in Array(listeners.values) {
24
44
  listener(state)
25
45
  }
26
46
  }
27
47
 
48
+ /// Listeners registered before activation receive `.unknown` now and the
49
+ /// real state once a command activates the manager and CoreBluetooth
50
+ /// reports.
28
51
  @discardableResult
29
52
  func addStateListener(_ listener: @escaping (CBManagerState) -> Void) -> UUID {
30
53
  // `listeners` is unsynchronized; it is only safe because the central
@@ -42,6 +65,7 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
42
65
  }
43
66
 
44
67
  func requirePoweredOn(operation: String) throws {
68
+ activate()
45
69
  if let current = centralManager?.state {
46
70
  state = current
47
71
  }
@@ -63,6 +87,13 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
63
87
  code: "bluetooth_unsupported",
64
88
  message: "This phone does not support Bluetooth."
65
89
  )
90
+ case .unknown where !hasReportedState:
91
+ // First command after lazy activation: CoreBluetooth has not
92
+ // reported yet (the permission alert may be on screen). Let the
93
+ // command proceed; the per-device controller owns its own
94
+ // central manager and handles the radio state itself, and this
95
+ // gate enforces from the first report onward.
96
+ return
66
97
  case .resetting, .unknown:
67
98
  throw BluetoothSdkError(
68
99
  code: "bluetooth_not_ready",