@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
@@ -16,7 +16,7 @@
16
16
 
17
17
  import Foundation
18
18
 
19
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
19
+ #if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
20
20
 
21
21
  final class SpeakerIdEngine {
22
22
  static let shared = SpeakerIdEngine()
@@ -537,7 +537,7 @@ final class SpeakerIdEngine {
537
537
 
538
538
  #else
539
539
 
540
- /// Stub for SWIFT_PACKAGE builds without GLASSLY_FEATURE_LOCAL_STT — the
540
+ /// Stub for SWIFT_PACKAGE builds without FEATURE_LOCAL_STT — the
541
541
  /// sherpa-onnx runtime isn't linked, so speaker ID is unavailable.
542
542
  final class SpeakerIdEngine {
543
543
  static let shared = SpeakerIdEngine()
@@ -113,6 +113,9 @@ struct GlassesStatus: CustomStringConvertible {
113
113
  "connectionState": "DISCONNECTED",
114
114
  "fullyBooted": false,
115
115
  "batteryLevel": -1,
116
+ "ringConnected": false,
117
+ "ringBatteryLevel": -1,
118
+ "ringCharging": false,
116
119
  "charging": false,
117
120
  "hotspotEnabled": false,
118
121
  "hotspotGatewayIp": "",
@@ -250,6 +253,18 @@ struct GlassesStatus: CustomStringConvertible {
250
253
  boolValue(values, "charging") ?? false
251
254
  }
252
255
 
256
+ var ringConnected: Bool {
257
+ boolValue(values, "ringConnected") ?? false
258
+ }
259
+
260
+ var ringBatteryLevel: Int {
261
+ intValue(values["ringBatteryLevel"]) ?? -1
262
+ }
263
+
264
+ var ringCharging: Bool {
265
+ boolValue(values, "ringCharging") ?? false
266
+ }
267
+
253
268
  var caseBatteryLevel: Int {
254
269
  intValue(values["caseBatteryLevel"]) ?? -1
255
270
  }
@@ -756,6 +771,18 @@ struct GlassesStatusUpdate: CustomStringConvertible {
756
771
  optionalBoolValue(values, "charging")
757
772
  }
758
773
 
774
+ var ringConnected: Bool? {
775
+ optionalBoolValue(values, "ringConnected")
776
+ }
777
+
778
+ var ringBatteryLevel: Int? {
779
+ optionalIntValue(values, "ringBatteryLevel")
780
+ }
781
+
782
+ var ringCharging: Bool? {
783
+ optionalBoolValue(values, "ringCharging")
784
+ }
785
+
759
786
  var caseBatteryLevel: Int? {
760
787
  optionalIntValue(values, "caseBatteryLevel")
761
788
  }
@@ -1,5 +1,4 @@
1
1
  import Foundation
2
- import UIKit
3
2
 
4
3
  /**
5
4
  * SherpaOnnxTranscriber handles real-time audio transcription using Sherpa-ONNX.
@@ -24,9 +23,6 @@ class SherpaOnnxTranscriber {
24
23
 
25
24
  private var lastPartialResult = ""
26
25
 
27
- /// Parent context
28
- private weak var context: UIViewController?
29
-
30
26
  /// Session start time for relative timestamps
31
27
  private var transcriptionSessionStart: Date
32
28
 
@@ -90,10 +86,9 @@ class SherpaOnnxTranscriber {
90
86
  }
91
87
 
92
88
  /**
93
- * Constructor that accepts a UIViewController to load model assets.
89
+ * Model assets come from the saved path; no UI context is needed.
94
90
  */
95
- init(context: UIViewController) {
96
- self.context = context
91
+ init() {
97
92
  transcriptionSessionStart = Date()
98
93
  }
99
94
 
@@ -105,7 +100,7 @@ class SherpaOnnxTranscriber {
105
100
  * Initialize the Sherpa-ONNX recognizer.
106
101
  * Loads models and configuration, sets up processing thread.
107
102
  */
108
- func initialize() {
103
+ func initialize() throws {
109
104
  do {
110
105
  var tokensPath: String
111
106
  var modelType = "unknown"
@@ -245,22 +240,24 @@ class SherpaOnnxTranscriber {
245
240
  } else {
246
241
  Bridge.log("No Sherpa ONNX model available. Transcription will be disabled.")
247
242
  Bridge.log("Please download a model using the model downloader in settings.")
248
- recognizer = nil
249
- isRunning = false
250
- return
243
+ throw NSError(domain: "LocalSTT", code: 2, userInfo: [
244
+ NSLocalizedDescriptionKey: "No offline speech model has been selected",
245
+ ])
251
246
  }
252
247
 
253
248
  if recognizer == nil {
254
249
  throw NSError(domain: "SherpaOnnxTranscriber", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create recognizer"])
255
250
  }
256
251
 
257
- startProcessingTask()
258
252
  isRunning = true
253
+ startProcessingTask()
259
254
 
260
255
  Bridge.log("Sherpa-ONNX ASR initialized successfully with \(modelType) model")
261
256
 
262
257
  } catch {
263
258
  Bridge.log("Failed to initialize Sherpa-ONNX: \(error.localizedDescription)")
259
+ shutdown()
260
+ throw error
264
261
  }
265
262
  }
266
263
 
@@ -431,6 +428,10 @@ class SherpaOnnxTranscriber {
431
428
 
432
429
  isRunning = false
433
430
  processingTask?.cancel()
431
+ // Join the old loop before a restart sets isRunning=true again.
432
+ processingQueue?.sync {}
433
+ processingTask = nil
434
+ processingQueue = nil
434
435
 
435
436
  // Synchronize access to recognizer during shutdown
436
437
  objc_sync_enter(self)
@@ -458,9 +459,9 @@ class SherpaOnnxTranscriber {
458
459
  * Restarts the transcriber after a model change.
459
460
  * Shuts down existing resources, clears buffers, and reinitializes the recognizer.
460
461
  */
461
- func restart() {
462
+ func restart() throws {
462
463
  Bridge.log("♻️ Restarting SherpaOnnxTranscriber...")
463
464
  shutdown()
464
- initialize()
465
+ try initialize()
465
466
  }
466
467
  }
@@ -1,6 +1,5 @@
1
1
  //
2
2
  // Enums.swift
3
- // Glassly_Manager
4
3
  //
5
4
  // Created by Matthew Fosse on 3/3/25.
6
5
  //
@@ -1,6 +1,5 @@
1
1
  //
2
2
  // G1Text.swift
3
- // Glassly_Manager
4
3
  //
5
4
  // Created by Matthew Fosse on 3/5/25.
6
5
  //
@@ -12,6 +12,16 @@ import Foundation
12
12
  enum G2NotifyFile {
13
13
  static let whitelistFilename = "user/notify_whitelist.json"
14
14
 
15
+ /// eEvenFileServiceType from the stock app's efs_transmit.proto. The
16
+ /// firmware's `_efsFileCmdParse` dispatches on this word alone: the
17
+ /// filename is not consulted, and a notification-typed file whose JSON
18
+ /// has no `android_notification` node leaves a blank card in the stock
19
+ /// notification viewer.
20
+ enum FileType: UInt32 {
21
+ case whitelist = 0 // NOTIFICATION_JSON_WHITELIST
22
+ case androidNotification = 1 // ANDROID_MSG_JSON_NOTIFICATION
23
+ }
24
+
15
25
  /// Firmware caps: 100-app list, 80-byte records (64-byte identifier
16
26
  /// including NUL, 16-byte display name including NUL).
17
27
  static let maxApps = 100
@@ -41,18 +51,18 @@ enum G2NotifyFile {
41
51
  return crc
42
52
  }
43
53
 
44
- /// 93-byte FILE_CHECK payload: mode, size, split CRC-32C, 80-byte
45
- /// NUL-padded filename. The glasses reconstruct the checksum as
46
- /// `(extra << 24) | (checksum >> 8)`.
47
- static func fileCheckHeader(filename: String, content: Data) -> Data {
54
+ /// 93-byte FILE_CHECK payload as the firmware reads it: SEND_START command
55
+ /// byte, then u32 file type, u32 size, u32 CRC-32C and the 80-byte
56
+ /// NUL-padded filename. (The even-g2-protocol docs describe the same
57
+ /// bytes shifted by one — "mode 0x100, size * 256, crc << 8, extra" — which
58
+ /// hides that "mode 0x100" is file type 1, the Android notification.)
59
+ static func fileCheckHeader(filename: String, content: Data, fileType: FileType) -> Data {
48
60
  let crc = crc32c(content)
49
61
  var header = Data()
50
- // Mode 0x100: the even-g2-protocol docs' prose says 0x00010000, but the
51
- // working example (`notification.py`) packs `<I` 0x100 — trust the code.
52
- header.appendLittleEndianUInt32(0x100) // mode
53
- header.appendLittleEndianUInt32(UInt32(content.count) &* 256) // size
54
- header.appendLittleEndianUInt32(crc << 8) // checksum (low 24 bits, shifted)
55
- header.append(UInt8(crc >> 24)) // extra (high CRC byte)
62
+ header.append(0x00) // eEvenFileSendServiceCID SEND_START
63
+ header.appendLittleEndianUInt32(fileType.rawValue)
64
+ header.appendLittleEndianUInt32(UInt32(content.count))
65
+ header.appendLittleEndianUInt32(crc)
56
66
  var name = Array(filename.utf8)
57
67
  if name.count > 80 {
58
68
  name = Array(name.prefix(80))
@@ -3,7 +3,7 @@
3
3
  // Glassly
4
4
  //
5
5
  // Test/benchmark infrastructure. Self-contained file, never invoked in
6
- // normal user flow. Triggered only when (a) GLASSLY_RUN_JSC_BENCH env
6
+ // normal user flow. Triggered only when (a) RUN_JSC_BENCH env
7
7
  // var is set at app launch, or (b) the user taps a JSC button on the
8
8
  // (Super-Mode-gated) stress-test screen.
9
9
  //
@@ -17,7 +17,7 @@
17
17
  // - Has __dispatch as a single bridge function (no per-method bindings,
18
18
  // to avoid the production crash Pebble documented in CrashReproducer.kt)
19
19
  // - Runs a representative idle workload: setInterval ping every 5 s
20
- // - Names itself "GlasslyJS: <id>" for Safari Web Inspector
20
+ // - Names itself "JSRuntime: <id>" for Safari Web Inspector
21
21
  // - Is NOT inspectable in release builds
22
22
  //
23
23
  // Measure resident memory before and after spawning N contexts.
@@ -57,12 +57,12 @@ private func jlog(_ message: String) {
57
57
  }
58
58
 
59
59
  @objc class JSCExperiment: NSObject {
60
- /// Called from BluetoothSdkModule init. If GLASSLY_RUN_JSC_BENCH env var
60
+ /// Called from BluetoothSdkModule init. If RUN_JSC_BENCH env var
61
61
  /// is set, kick off the benchmark after a 5s settle window so the app
62
62
  /// is fully booted (RN bridge up, Metro pull done if dev, etc).
63
63
  @objc static func maybeAutoBenchmark() {
64
- guard ProcessInfo.processInfo.environment["GLASSLY_RUN_JSC_BENCH"] != nil else { return }
65
- os_log("🧪 GLASSLY_RUN_JSC_BENCH set — auto-running benchmark in 5s",
64
+ guard ProcessInfo.processInfo.environment["RUN_JSC_BENCH"] != nil else { return }
65
+ os_log("🧪 RUN_JSC_BENCH set — auto-running benchmark in 5s",
66
66
  log: jscLog, type: .info)
67
67
  DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 5.0) {
68
68
  runBenchmark()
@@ -99,7 +99,7 @@ private func jlog(_ message: String) {
99
99
  let vm = JSVirtualMachine()!
100
100
  let ctx = JSContext(virtualMachine: vm)!
101
101
 
102
- ctx.name = "GlasslyJS: \(id)"
102
+ ctx.name = "JSRuntime: \(id)"
103
103
  // Inspectable in dev builds only. iOS 16.4+ guarded.
104
104
  #if DEBUG
105
105
  if #available(iOS 16.4, *) {
@@ -1,6 +1,5 @@
1
1
  //
2
2
  // Models.swift
3
- // Glassly_Manager
4
3
  //
5
4
  // Created by Matthew Fosse on 3/3/25.
6
5
  //
@@ -0,0 +1,29 @@
1
+ @testable import BluetoothSDK
2
+ import XCTest
3
+
4
+ /// Building the SDK must not create a `CBCentralManager`: that is what shows
5
+ /// the iOS Bluetooth permission alert, and the Expo module builds the SDK at
6
+ /// app launch. A regression here prompts a fresh install before onboarding.
7
+ ///
8
+ /// One method on purpose: `BluetoothAvailability` is a process-wide
9
+ /// singleton, so the "not yet activated" assertion has to run before the
10
+ /// first command activates it.
11
+ @MainActor
12
+ final class BluetoothAvailabilityTests: XCTestCase {
13
+ func testCentralManagerIsCreatedByTheFirstCommandNotBySdkInit() throws {
14
+ XCTAssertFalse(BluetoothAvailability.shared.isActivated)
15
+
16
+ let sdk = BluetoothSDK()
17
+ defer { sdk.invalidate() }
18
+ XCTAssertFalse(
19
+ BluetoothAvailability.shared.isActivated,
20
+ "SDK init created a CBCentralManager, which prompts for Bluetooth at launch"
21
+ )
22
+
23
+ // The first command activates the manager. CoreBluetooth has not
24
+ // reported a state yet, so the gate lets the command through instead
25
+ // of failing with bluetooth_not_ready.
26
+ XCTAssertNoThrow(try BluetoothAvailability.shared.requirePoweredOn(operation: "test"))
27
+ XCTAssertTrue(BluetoothAvailability.shared.isActivated)
28
+ }
29
+ }
@@ -0,0 +1,255 @@
1
+ @testable import BluetoothSDK
2
+ import Foundation
3
+ import XCTest
4
+
5
+ @MainActor
6
+ final class DisplayPacerTests: XCTestCase {
7
+ @MainActor private final class Harness {
8
+ var now: Int64 = 1000
9
+ var delay: Int64?
10
+ var block: (@MainActor () -> Void)?
11
+ var scheduled = 0
12
+ var scenes: [SceneFrame] = []
13
+ var legacySends = 0
14
+ var pacer: DisplayPacer!
15
+
16
+ init(fps: Int = 8) {
17
+ pacer = DisplayPacer(
18
+ dispatchScene: { [unowned self] in self.scenes.append($0) },
19
+ sendLegacy: { [unowned self] in self.legacySends += 1 },
20
+ clock: { [unowned self] in self.now },
21
+ scheduler: { [unowned self] delayMs, block in
22
+ self.scheduled += 1
23
+ self.delay = delayMs
24
+ self.block = block
25
+ let token = self.scheduled
26
+ return {
27
+ if self.scheduled == token {
28
+ self.block = nil
29
+ self.delay = nil
30
+ }
31
+ }
32
+ }
33
+ )
34
+ pacer.setMaxFps(fps)
35
+ }
36
+
37
+ func fire() {
38
+ guard let b = block else { XCTFail("no timer armed"); return }
39
+ block = nil
40
+ delay = nil
41
+ b()
42
+ }
43
+ }
44
+
45
+ private func element(_ id: String, change: String = "created", x: Int32 = 0) -> SceneElement {
46
+ SceneElement(
47
+ id: id, type: "text", x: x, y: 0, w: 10, h: 10, text: id, data: nil, border: 0, radius: 0,
48
+ change: change, contentHash: "h-\(id)"
49
+ )
50
+ }
51
+
52
+ private func frame(
53
+ _ elements: [SceneElement], removed: [String] = [], appId: String = "app", replay: Bool = false,
54
+ epoch: Int = 1
55
+ ) -> SceneFrame {
56
+ SceneFrame(appId: appId, epoch: epoch, replay: replay, elements: elements, removed: removed)
57
+ }
58
+
59
+ private func change(_ frame: SceneFrame, _ id: String) -> String? {
60
+ frame.elements.first { $0.id == id }?.change
61
+ }
62
+
63
+ func testFirstSubmitSendsImmediately() {
64
+ let h = Harness()
65
+ h.pacer.submitScene(frame([element("a")]))
66
+ XCTAssertEqual(h.scenes.count, 1)
67
+ XCTAssertNil(h.block)
68
+ }
69
+
70
+ func testSecondSubmitInsideWindowIsHeldUntilTheSlot() {
71
+ let h = Harness(fps: 10)
72
+ h.pacer.submitScene(frame([element("a")], epoch: 1))
73
+ h.now += 30
74
+ h.pacer.submitScene(frame([element("a", change: "updated")], epoch: 2))
75
+ XCTAssertEqual(h.scenes.count, 1)
76
+ XCTAssertEqual(h.delay, 70)
77
+ h.now += 70
78
+ h.fire()
79
+ XCTAssertEqual(h.scenes.count, 2)
80
+ XCTAssertEqual(h.scenes[1].epoch, 2)
81
+ }
82
+
83
+ func testRapidFramesCoalesceIntoOneMergedSend() {
84
+ let h = Harness(fps: 10)
85
+ h.pacer.submitScene(frame([element("a")], epoch: 1))
86
+ h.now += 10
87
+ h.pacer.submitScene(frame([element("a", change: "updated"), element("b", change: "created")], epoch: 2))
88
+ h.now += 10
89
+ h.pacer.submitScene(frame([element("a", change: "unchanged"), element("b", change: "moved", x: 5)], epoch: 3))
90
+ h.now += 10
91
+ h.pacer.submitScene(
92
+ frame([element("a", change: "unchanged"), element("b", change: "unchanged", x: 5)], epoch: 4)
93
+ )
94
+ XCTAssertEqual(h.scheduled, 1)
95
+ h.now += 70
96
+ h.fire()
97
+ XCTAssertEqual(h.scenes.count, 2)
98
+ let sent = h.scenes[1]
99
+ XCTAssertEqual(sent.epoch, 4)
100
+ XCTAssertEqual(change(sent, "a"), "updated")
101
+ XCTAssertEqual(change(sent, "b"), "created")
102
+ XCTAssertEqual(sent.elements.first { $0.id == "b" }?.x, 5)
103
+ }
104
+
105
+ func testUpdatedThenUnchangedBecomesUpdated() {
106
+ let h = Harness()
107
+ h.pacer.submitScene(frame([element("a")]))
108
+ h.now += 1
109
+ h.pacer.submitScene(frame([element("a", change: "moved", x: 3)]))
110
+ h.now += 1
111
+ h.pacer.submitScene(frame([element("a", change: "unchanged", x: 3)]))
112
+ h.now += 200
113
+ h.fire()
114
+ XCTAssertEqual(change(h.scenes[1], "a"), "updated")
115
+ }
116
+
117
+ func testRemovedThenRecreatedIdIsNotRemoved() {
118
+ let h = Harness()
119
+ h.pacer.submitScene(frame([element("a"), element("b")]))
120
+ h.now += 1
121
+ h.pacer.submitScene(frame([element("a", change: "unchanged")], removed: ["b"]))
122
+ h.now += 1
123
+ h.pacer.submitScene(frame([element("a", change: "unchanged"), element("b", change: "created")]))
124
+ h.now += 200
125
+ h.fire()
126
+ XCTAssertTrue(h.scenes[1].removed.isEmpty)
127
+ XCTAssertEqual(change(h.scenes[1], "b"), "created")
128
+ }
129
+
130
+ func testPendingRemovesSurviveIntoTheMergedFrame() {
131
+ let h = Harness()
132
+ h.pacer.submitScene(frame([element("a"), element("b"), element("c")]))
133
+ h.now += 1
134
+ h.pacer.submitScene(frame([element("a", change: "unchanged")], removed: ["b", "c"]))
135
+ h.now += 1
136
+ h.pacer.submitScene(frame([element("a", change: "unchanged")], removed: ["c"]))
137
+ h.now += 200
138
+ h.fire()
139
+ XCTAssertEqual(h.scenes[1].removed, ["b", "c"])
140
+ }
141
+
142
+ func testAppSwitchDiscardsPending() {
143
+ let h = Harness()
144
+ h.pacer.submitScene(frame([element("a")], appId: "one"))
145
+ h.now += 1
146
+ h.pacer.submitScene(frame([element("a", change: "updated")], removed: ["z"], appId: "one"))
147
+ h.now += 1
148
+ h.pacer.submitScene(frame([element("b", change: "created")], appId: "two", replay: true))
149
+ h.now += 200
150
+ h.fire()
151
+ let sent = h.scenes[1]
152
+ XCTAssertEqual(sent.appId, "two")
153
+ XCTAssertTrue(sent.removed.isEmpty)
154
+ XCTAssertEqual(sent.elements.map(\.id), ["b"])
155
+ }
156
+
157
+ func testEmptyFrameFlushesImmediatelyWithPendingRemoves() {
158
+ let h = Harness()
159
+ h.pacer.submitScene(frame([element("a"), element("b")]))
160
+ h.now += 1
161
+ h.pacer.submitScene(frame([element("a", change: "unchanged")], removed: ["b"]))
162
+ h.now += 1
163
+ h.pacer.flushScene(frame([], removed: ["a"]))
164
+ XCTAssertEqual(h.scenes.count, 2)
165
+ XCTAssertEqual(Set(h.scenes[1].removed), ["a", "b"])
166
+ XCTAssertTrue(h.scenes[1].elements.isEmpty)
167
+ XCTAssertNil(h.block)
168
+ h.now += 200
169
+ h.pacer.setMaxFps(8)
170
+ XCTAssertEqual(h.scenes.count, 2)
171
+ }
172
+
173
+ func testClearViewFlushesImmediatelyAndDropsPendingScene() {
174
+ let h = Harness()
175
+ h.pacer.submitScene(frame([element("a")]))
176
+ h.now += 1
177
+ h.pacer.submitScene(frame([element("a", change: "updated")]))
178
+ XCTAssertNotNil(h.block)
179
+ h.pacer.flushLegacy()
180
+ XCTAssertEqual(h.legacySends, 1)
181
+ XCTAssertEqual(h.scenes.count, 1)
182
+ XCTAssertNil(h.block)
183
+ }
184
+
185
+ func testLegacyAfterSceneDropsThePendingScene() {
186
+ let h = Harness()
187
+ h.pacer.submitScene(frame([element("a")]))
188
+ h.now += 1
189
+ h.pacer.submitScene(frame([element("a", change: "updated")]))
190
+ h.now += 1
191
+ h.pacer.submitLegacy()
192
+ h.now += 200
193
+ h.fire()
194
+ XCTAssertEqual(h.scenes.count, 1)
195
+ XCTAssertEqual(h.legacySends, 1)
196
+ }
197
+
198
+ func testSceneAfterLegacyDropsThePendingLegacy() {
199
+ let h = Harness()
200
+ h.pacer.submitLegacy()
201
+ XCTAssertEqual(h.legacySends, 1)
202
+ h.now += 1
203
+ h.pacer.submitLegacy()
204
+ h.now += 1
205
+ h.pacer.submitScene(frame([element("a")]))
206
+ h.now += 200
207
+ h.fire()
208
+ XCTAssertEqual(h.legacySends, 1)
209
+ XCTAssertEqual(h.scenes.count, 1)
210
+ }
211
+
212
+ func testSetMaxFpsRearmsAPendingTimer() {
213
+ let h = Harness(fps: 2)
214
+ h.pacer.submitScene(frame([element("a")]))
215
+ h.now += 50
216
+ h.pacer.submitScene(frame([element("a", change: "updated")]))
217
+ XCTAssertEqual(h.delay, 450)
218
+ h.pacer.setMaxFps(20)
219
+ XCTAssertEqual(h.scenes.count, 2)
220
+ XCTAssertNil(h.block)
221
+
222
+ h.now += 10
223
+ h.pacer.submitScene(frame([element("a", change: "updated")]))
224
+ XCTAssertEqual(h.delay, 40)
225
+ h.pacer.setMaxFps(10)
226
+ XCTAssertEqual(h.delay, 90)
227
+ XCTAssertEqual(h.scenes.count, 2)
228
+ }
229
+
230
+ func testCancelDropsPendingWithoutSending() {
231
+ let h = Harness()
232
+ h.pacer.submitScene(frame([element("a")]))
233
+ h.now += 1
234
+ h.pacer.submitScene(frame([element("a", change: "updated")]))
235
+ h.pacer.cancel()
236
+ XCTAssertNil(h.block)
237
+ h.now += 200
238
+ h.pacer.setMaxFps(8)
239
+ XCTAssertEqual(h.scenes.count, 1)
240
+ XCTAssertNil(h.block)
241
+ }
242
+
243
+ func testLateSubmitAfterTheSlotSendsMergedImmediately() {
244
+ let h = Harness(fps: 10)
245
+ h.pacer.submitScene(frame([element("a")]))
246
+ h.now += 10
247
+ h.pacer.submitScene(frame([element("a", change: "updated"), element("b", change: "created")]))
248
+ h.now += 200
249
+ h.pacer.submitScene(frame([element("a", change: "unchanged"), element("b", change: "unchanged")]))
250
+ XCTAssertEqual(h.scenes.count, 2)
251
+ XCTAssertEqual(change(h.scenes[1], "a"), "updated")
252
+ XCTAssertEqual(change(h.scenes[1], "b"), "created")
253
+ XCTAssertNil(h.block)
254
+ }
255
+ }