@glassly/bluetooth-sdk 0.1.0-dev.99 → 0.2.8-dev.445

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 +1065 -255
  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 +1420 -495
  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
@@ -1,6 +1,5 @@
1
1
  //
2
2
  // DeviceManager.swift
3
- // Glassly_Manager
4
3
  //
5
4
  // Created by Matthew Fosse on 3/5/25.
6
5
  //
@@ -254,7 +253,6 @@ struct ViewState {
254
253
  set { DeviceStore.shared.apply("bluetooth", "systemMicUnavailable", newValue) }
255
254
  }
256
255
 
257
-
258
256
  private var micEnabled: Bool {
259
257
  get { DeviceStore.shared.get("bluetooth", "micEnabled") as? Bool ?? false }
260
258
  set { DeviceStore.shared.apply("bluetooth", "micEnabled", newValue) }
@@ -305,7 +303,7 @@ struct ViewState {
305
303
  private var micReinitTimer: Timer?
306
304
 
307
305
  // STT:
308
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
306
+ #if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
309
307
  private var transcriber: SherpaOnnxTranscriber?
310
308
  #endif
311
309
 
@@ -320,6 +318,28 @@ struct ViewState {
320
318
  /// native re-dispatch coherent (nav exit re-applies a complete scene).
321
319
  var sceneState: SceneFrame?
322
320
 
321
+ /// Paces scene frames and legacy layouts to display_max_fps; clears bypass it.
322
+ /// CFW scene drivers coalesce off the link themselves, so they bypass it too.
323
+ private lazy var pacer = DisplayPacer(
324
+ dispatchScene: { [weak self] frame in self?.dispatchSceneFrame(frame) },
325
+ sendLegacy: { [weak self] in self?.sendCurrentState() }
326
+ )
327
+
328
+ func setDisplayMaxFps(_ fps: Int) {
329
+ pacer.setMaxFps(fps)
330
+ }
331
+
332
+ /// CFW identity can land after device-ready, so this is re-read per update;
333
+ /// the flush variants fold any frame already held into the immediate send.
334
+ private var pacerBypassed: Bool {
335
+ sgc?.supportsCfwSceneRendering == true
336
+ }
337
+
338
+ /// Drop paced display work (screen off): the retained state repaints on re-enable.
339
+ func cancelPendingDisplay() {
340
+ pacer.cancel()
341
+ }
342
+
323
343
  override init() {
324
344
  Bridge.log("MAN: init()")
325
345
  super.init()
@@ -327,23 +347,7 @@ struct ViewState {
327
347
  // Start memory monitoring (logs every 30s to help detect leaks)
328
348
  // MemoryMonitor.start()
329
349
 
330
- // Initialize SherpaOnnx Transcriber
331
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
332
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
333
- let window = windowScene.windows.first,
334
- let rootViewController = window.rootViewController
335
- {
336
- transcriber = SherpaOnnxTranscriber(context: rootViewController)
337
- } else {
338
- Bridge.log("Failed to create SherpaOnnxTranscriber - no root view controller found")
339
- }
340
-
341
- // Initialize the transcriber
342
- if let transcriber = transcriber {
343
- transcriber.initialize()
344
- Bridge.log("SherpaOnnxTranscriber fully initialized")
345
- }
346
- #endif
350
+ // Local STT is created on demand after the model is available.
347
351
 
348
352
  // Initialize persistent LC3 converter for unified audio encoding
349
353
  lc3Converter = PcmConverter()
@@ -483,7 +487,7 @@ struct ViewState {
483
487
  SpeakerIdEngine.shared.acceptAudio(pcm16le: pcmData)
484
488
 
485
489
  // Send PCM to local transcriber.
486
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
490
+ #if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
487
491
  if shouldSendTranscript || localSttFallbackActive {
488
492
  transcriber?.acceptAudio(pcm16le: pcmData)
489
493
  }
@@ -669,6 +673,7 @@ struct ViewState {
669
673
  Bridge.log("Initializing manager for wearable: \(wearable)")
670
674
  if sgc != nil && sgc?.type != wearable {
671
675
  Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
676
+ pacer.cancel()
672
677
  sgc?.cleanup()
673
678
  sgc = nil
674
679
  resetSystemTimeSync()
@@ -693,12 +698,12 @@ struct ViewState {
693
698
  } else if wearable.contains(DeviceTypes.FRAME) {
694
699
  // sgc = FrameManager()
695
700
  }
696
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_NEX
701
+ #if !SWIFT_PACKAGE || FEATURE_NEX
697
702
  if sgc == nil && wearable.contains(DeviceTypes.NEX) {
698
703
  sgc = MentraNexSGC.getInstance()
699
704
  }
700
705
  #endif
701
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_VUZIX
706
+ #if !SWIFT_PACKAGE || FEATURE_VUZIX
702
707
  if sgc == nil {
703
708
  if wearable.contains(DeviceTypes.MACH1) {
704
709
  sgc = Mach1()
@@ -811,6 +816,11 @@ struct ViewState {
811
816
 
812
817
  // MARK: - Native navigation (glasses-rendered nav UI, e.g. G2 service 0x08)
813
818
 
819
+ func glassesSensorControl(sensor: String, operation: String, options: [String: Double]) async -> Bool {
820
+ guard let g2 = sgc as? G2 else { return false }
821
+ return await g2.glassesSensorControl(sensor: sensor, operation: operation, options: options)
822
+ }
823
+
814
824
  func navigationModeStart() {
815
825
  sgc?.enterNavigationMode()
816
826
  }
@@ -1027,12 +1037,16 @@ struct ViewState {
1027
1037
  updateMicState()
1028
1038
  }
1029
1039
 
1030
- func restartTranscriber() {
1031
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
1040
+ func restartTranscriber() throws {
1041
+ #if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
1032
1042
  Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
1033
- transcriber?.restart()
1043
+ let nextTranscriber = transcriber ?? SherpaOnnxTranscriber()
1044
+ try nextTranscriber.restart()
1045
+ transcriber = nextTranscriber
1034
1046
  #else
1035
- Bridge.log("MAN: Local STT is not included in this SwiftPM build")
1047
+ throw NSError(domain: "LocalSTT", code: 1, userInfo: [
1048
+ NSLocalizedDescriptionKey: "Local STT is not included in this build",
1049
+ ])
1036
1050
  #endif
1037
1051
  }
1038
1052
 
@@ -1310,7 +1324,11 @@ struct ViewState {
1310
1324
  }
1311
1325
 
1312
1326
  viewState = newViewState
1313
- sendCurrentState()
1327
+ if layoutType == "clear_view" || pacerBypassed {
1328
+ pacer.flushLegacy()
1329
+ } else {
1330
+ pacer.submitLegacy()
1331
+ }
1314
1332
  }
1315
1333
 
1316
1334
  /// Parse + store a scene frame, then dispatch it.
@@ -1349,24 +1367,34 @@ struct ViewState {
1349
1367
  text: " ", data: nil, animationData: nil
1350
1368
  )
1351
1369
 
1352
- dispatchSceneFrame(frame)
1370
+ if frame.elements.isEmpty || pacerBypassed {
1371
+ pacer.flushScene(frame)
1372
+ } else {
1373
+ pacer.submitScene(frame)
1374
+ }
1353
1375
  }
1354
1376
 
1355
1377
  /// Guarded scene dispatch - mirrors sendCurrentState's send conditions.
1356
1378
  private func dispatchSceneFrame(_ frame: SceneFrame) {
1379
+ // Each early-out tells the host the frame won't show now; the SGC
1380
+ // reports the rest once the device has it.
1357
1381
  if screenDisabled {
1382
+ frame.reportPresented(false, reason: "screen off")
1358
1383
  return
1359
1384
  }
1360
1385
  // Native nav UI owns the display; the retained scene state (stored as
1361
1386
  // a replay frame above) repaints when navigation mode exits.
1362
1387
  if sgc?.navigationModeActive == true {
1388
+ frame.reportPresented(false, reason: "navigation UI active")
1363
1389
  return
1364
1390
  }
1365
1391
  if sgc?.type.contains(DeviceTypes.SIMULATED) ?? true {
1392
+ frame.reportPresented(false, reason: sgc == nil ? "no glasses" : "simulated glasses")
1366
1393
  return
1367
1394
  }
1368
1395
  guard sgc?.fullyBooted == true else {
1369
1396
  Bridge.log("MAN: dispatchSceneFrame(): sgc not ready")
1397
+ frame.reportPresented(false, reason: "glasses not ready")
1370
1398
  return
1371
1399
  }
1372
1400
  Task { [weak self] in
@@ -1382,6 +1410,13 @@ struct ViewState {
1382
1410
  let box = el["box"] as? [String: Any]
1383
1411
  else { return nil }
1384
1412
  let style = el["style"] as? [String: Any]
1413
+ let transition = el["transition"] as? [String: Any]
1414
+ let points = ((el["points"] as? [[String: Any]]) ?? []).flatMap { p -> [Int32] in
1415
+ [(p["x"] as? NSNumber)?.int32Value ?? 0, (p["y"] as? NSNumber)?.int32Value ?? 0]
1416
+ }
1417
+ // Open strokes carry their width as `width`; closed shapes and boxes as `border`.
1418
+ let border = ((style?["border"]) as? NSNumber)?.int32Value
1419
+ ?? ((style?["width"]) as? NSNumber)?.int32Value ?? 0
1385
1420
  return SceneElement(
1386
1421
  id: id,
1387
1422
  type: type,
@@ -1391,13 +1426,28 @@ struct ViewState {
1391
1426
  h: (box["h"] as? NSNumber)?.int32Value ?? 0,
1392
1427
  text: (el["text"] as? String).map { parsePlaceholders($0) },
1393
1428
  data: el["data"] as? String,
1394
- border: ((style?["border"]) as? NSNumber)?.int32Value ?? 0,
1429
+ border: border,
1395
1430
  radius: ((style?["radius"]) as? NSNumber)?.int32Value ?? 0,
1396
1431
  change: el["change"] as? String ?? "created",
1397
1432
  contentHash: el["contentHash"] as? String ?? "",
1398
1433
  font: (style?["font"] as? String) ?? "default",
1399
- fontSize: ((style?["fontSize"]) as? NSNumber)?.int32Value ?? 0
1434
+ fontSize: ((style?["fontSize"]) as? NSNumber)?.int32Value ?? 0,
1435
+ fill: (style?["fill"] as? Bool) ?? false,
1436
+ color: ((style?["color"]) as? NSNumber)?.int32Value ?? -1,
1437
+ points: points,
1438
+ startDeg: (el["startDeg"] as? NSNumber)?.int32Value ?? 0,
1439
+ endDeg: (el["endDeg"] as? NSNumber)?.int32Value ?? 0,
1440
+ transitionMs: (transition?["durationMs"] as? NSNumber)?.int32Value ?? 0,
1441
+ easing: (transition?["easing"] as? String) ?? "ease",
1442
+ path: ((el["path"] as? [NSNumber]) ?? []).map { UInt8(clamping: $0.intValue) },
1443
+ pathScale: (el["pathScale"] as? NSNumber)?.int32Value ?? 256,
1444
+ fillRule: (el["fillRule"] as? String) == "evenodd" ? 1 : 0,
1445
+ rotationQ8: Int32(max(-9_216_000, min(9_216_000, ((el["rotation"] as? NSNumber)?.doubleValue ?? 0) * 256))),
1446
+ pivotX: ((el["pivot"] as? [String: Any])?["x"] as? NSNumber)?.int32Value,
1447
+ pivotY: ((el["pivot"] as? [String: Any])?["y"] as? NSNumber)?.int32Value,
1448
+ depth: (el["depth"] as? NSNumber)?.int32Value ?? 0
1400
1449
  )
1450
+
1401
1451
  }
1402
1452
  let canvas = sceneMap["canvas"] as? [String: Any]
1403
1453
  return SceneFrame(
@@ -1407,7 +1457,8 @@ struct ViewState {
1407
1457
  elements: elements,
1408
1458
  removed: (sceneMap["removed"] as? [String]) ?? [],
1409
1459
  canvasWidth: (canvas?["width"] as? NSNumber)?.int32Value ?? 0,
1410
- canvasHeight: (canvas?["height"] as? NSNumber)?.int32Value ?? 0
1460
+ canvasHeight: (canvas?["height"] as? NSNumber)?.int32Value ?? 0,
1461
+ seq: (sceneMap["seq"] as? NSNumber)?.intValue ?? 0
1411
1462
  )
1412
1463
  }
1413
1464
 
@@ -1644,6 +1695,58 @@ struct ViewState {
1644
1695
  return try await g2.startFirmwareFlash(target: target)
1645
1696
  }
1646
1697
 
1698
+ // MARK: - BLE keyboard bridge (G2 Glassly CFW revision 39+)
1699
+
1700
+ private func requireG2ForKeyboard() throws -> G2 {
1701
+ guard let g2 = sgc as? G2 else {
1702
+ throw G2KeyboardBridge.BridgeError(description: "The keyboard bridge needs connected G2 glasses.")
1703
+ }
1704
+ return g2
1705
+ }
1706
+
1707
+ func g2KeyboardScan() async throws -> [[String: Any]] {
1708
+ try await requireG2ForKeyboard().keyboardScan()
1709
+ }
1710
+
1711
+ func g2KeyboardConnect(address: String, addressType: Int, name: String) async throws {
1712
+ try await requireG2ForKeyboard().keyboardConnect(address: address, addressType: addressType, name: name)
1713
+ }
1714
+
1715
+ func g2KeyboardReconnect() async throws {
1716
+ try await requireG2ForKeyboard().keyboardReconnect()
1717
+ }
1718
+
1719
+ func g2KeyboardDisconnect() async throws {
1720
+ guard let g2 = sgc as? G2 else { return }
1721
+ try await g2.keyboardDisconnect()
1722
+ }
1723
+
1724
+ func g2KeyboardForget() async {
1725
+ if let g2 = sgc as? G2 {
1726
+ await g2.keyboardForget()
1727
+ } else {
1728
+ // No live G2 driver: drop the remembered identity directly.
1729
+ UserDefaults.standard.removeObject(forKey: "g2_keyboard")
1730
+ DeviceStore.shared.apply("glasses", "keyboardSaved", false)
1731
+ DeviceStore.shared.apply("glasses", "keyboardName", "")
1732
+ }
1733
+ }
1734
+
1735
+ func g2KeyboardPasskey(_ pin: Int) async throws {
1736
+ try await requireG2ForKeyboard().keyboardPasskey(pin)
1737
+ }
1738
+
1739
+ func g2KeyboardCompare(_ accept: Bool) async throws {
1740
+ try await requireG2ForKeyboard().keyboardCompare(accept)
1741
+ }
1742
+
1743
+ func g2KeyboardStatus() async -> [String: Any] {
1744
+ guard let g2 = sgc as? G2 else {
1745
+ return ["keyboardBridge": false, "keyboardConnected": false, "keyboardState": "idle"]
1746
+ }
1747
+ return await g2.keyboardStatus()
1748
+ }
1749
+
1647
1750
  /// Send shutdown command to glasses.
1648
1751
  /// This will initiate a graceful shutdown of the device.
1649
1752
  func sendShutdown() {
@@ -1686,6 +1789,16 @@ struct ViewState {
1686
1789
  func setMicState() {
1687
1790
  let willSendPcm = shouldSendPcm || shouldSendLc3
1688
1791
  let willSendTranscript = shouldSendTranscript || localSttFallbackActive
1792
+ #if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
1793
+ // Direct SDK users can request transcripts without the JS coordinator.
1794
+ if willSendTranscript, transcriber == nil {
1795
+ do {
1796
+ try restartTranscriber()
1797
+ } catch {
1798
+ Bridge.log("MAN: Local STT startup failed: \(error.localizedDescription)")
1799
+ }
1800
+ }
1801
+ #endif
1689
1802
  // Speaker enrollment records through whichever mic is currently
1690
1803
  // ranked active, so it holds the mic open like any other consumer.
1691
1804
  micEnabled = willSendPcm || willSendTranscript || speakerEnrollmentActive
@@ -1933,6 +2046,7 @@ struct ViewState {
1933
2046
  }
1934
2047
 
1935
2048
  func disconnect(clearPendingConnection: Bool = true) {
2049
+ pacer.cancel()
1936
2050
  sgc?.clearDisplay() // clear the screen
1937
2051
  sgc?.disconnect()
1938
2052
  sgc = nil // Clear the SGC reference after disconnect
@@ -1952,6 +2066,10 @@ struct ViewState {
1952
2066
  DeviceStore.shared.apply("glasses", "leftMacAddress", "")
1953
2067
  DeviceStore.shared.apply("glasses", "rightMacAddress", "")
1954
2068
  DeviceStore.shared.apply("glasses", "macAddress", "")
2069
+ // The CFW scene canvas is published per classification; a G1/Nex paired
2070
+ // next must not inherit the G2's 576x288 canvas and lifted element caps.
2071
+ DeviceStore.shared.apply("glasses", "sceneCanvasWidth", 0)
2072
+ DeviceStore.shared.apply("glasses", "sceneCanvasHeight", 0)
1955
2073
  DeviceStore.shared.apply("glasses", "fullyBooted", false)
1956
2074
  DeviceStore.shared.apply("glasses", "connected", false)
1957
2075
  DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
@@ -2041,7 +2159,7 @@ struct ViewState {
2041
2159
 
2042
2160
  func cleanup() {
2043
2161
  // Clean up transcriber resources
2044
- #if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
2162
+ #if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
2045
2163
  transcriber?.shutdown()
2046
2164
  transcriber = nil
2047
2165
  #endif
@@ -34,6 +34,9 @@ class DeviceStore {
34
34
  store.set("glasses", "caseRemoved", true)
35
35
  store.set("glasses", "caseOpen", true)
36
36
  store.set("glasses", "caseCharging", false)
37
+ store.set("glasses", "ringConnected", false)
38
+ store.set("glasses", "ringBatteryLevel", -1)
39
+ store.set("glasses", "ringCharging", false)
37
40
  store.set("glasses", "caseBatteryLevel", -1)
38
41
  store.set("glasses", "headUp", false)
39
42
  store.set("glasses", "bluetoothMacAddress", "")
@@ -62,6 +65,14 @@ class DeviceStore {
62
65
  // iOS: whether a connected lens is authorized to read the phone's ANCS
63
66
  // notification stream (G2 consumes notifications on-firmware).
64
67
  store.set("glasses", "ancsAuthorized", false)
68
+ // G2 Glassly CFW keyboard bridge (revision 39+): availability on the
69
+ // connected firmware, the live keyboard link, and the remembered keyboard.
70
+ store.set("glasses", "keyboardBridge", false)
71
+ store.set("glasses", "keyboardConnected", false)
72
+ store.set("glasses", "keyboardName", "")
73
+ store.set("glasses", "keyboardSaved", false)
74
+ store.set("glasses", "keyboardState", "idle")
75
+ store.set("glasses", "keyboardError", "")
65
76
 
66
77
  // CORE STATE:
67
78
  store.set("bluetooth", "systemMicUnavailable", false)
@@ -112,15 +123,11 @@ class DeviceStore {
112
123
  store.set("bluetooth", "should_send_pcm", false)
113
124
  store.set("bluetooth", "should_send_lc3", false)
114
125
  store.set("bluetooth", "should_send_transcript", false)
115
- store.set("bluetooth", "use_native_dashboard", false)
116
126
  // Debug escape hatch: G2 CFW direct-framebuffer rendering (off = stock containers).
117
127
  store.set("bluetooth", "cfw_scene_renderer", true)
118
- // G2 on-glass ANCS notification behavior + app filter (iOS).
119
- store.set(
120
- "bluetooth", "notification_ctrl",
121
- ["enabled": true, "auto_display": true, "disp_time": 5, "avoid_disturb": true]
122
- )
123
- store.set("bluetooth", "notification_filter", ["enabled": false, "apps": [[String: Any]]()])
128
+ // Debug: G2 CFW fast BLE link profile (off = stock connection behaviour).
129
+ store.set("bluetooth", "cfw_fast_ble", false)
130
+ store.set("bluetooth", "display_max_fps", DisplayPacer.defaultFps)
124
131
  }
125
132
 
126
133
  func get(_ category: String, _ key: String) -> Any? {
@@ -202,7 +209,9 @@ class DeviceStore {
202
209
  // BLUETOOTH:
203
210
 
204
211
  case ("bluetooth", "brightness"):
205
- let b = value as? Int ?? 50
212
+ // Read the stored (number-normalized) value: the bridge hands a
213
+ // Double, which `as? Int` rejects, and every level came out as 50.
214
+ let b = store.get("bluetooth", "brightness") as? Int ?? 50
206
215
  let auto = store.get("bluetooth", "auto_brightness") as? Bool ?? true
207
216
  Task {
208
217
  DeviceManager.shared.sgc?.setBrightness(b, autoMode: auto)
@@ -231,6 +240,16 @@ class DeviceStore {
231
240
  Task { await DeviceManager.shared.sgc?.cfwSceneRendererToggled(enabled) }
232
241
  }
233
242
 
243
+ case ("bluetooth", "cfw_fast_ble"):
244
+ if let enabled = value as? Bool, (oldValue as? Bool) != enabled {
245
+ Task { await DeviceManager.shared.sgc?.cfwFastBleToggled(enabled) }
246
+ }
247
+
248
+ case ("bluetooth", "display_max_fps"):
249
+ if let fps = value as? NSNumber {
250
+ DeviceManager.shared.setDisplayMaxFps(fps.intValue)
251
+ }
252
+
234
253
  case ("bluetooth", "dashboard_height"):
235
254
  scheduleDashboardHeightToGlasses()
236
255
 
@@ -257,12 +276,6 @@ class DeviceStore {
257
276
  DeviceManager.shared.sgc?.sendCalendarEvents(items)
258
277
  }
259
278
 
260
- case ("bluetooth", "notification_ctrl"):
261
- DeviceManager.shared.sgc?.sendNotificationCtrl()
262
-
263
- case ("bluetooth", "notification_filter"):
264
- DeviceManager.shared.sgc?.sendNotificationFilter()
265
-
266
279
  case ("bluetooth", "metric_system"), ("bluetooth", "twelve_hour_time"):
267
280
  DeviceManager.shared.sgc?.sendDashboardDisplaySettings()
268
281
 
@@ -278,6 +291,7 @@ class DeviceStore {
278
291
  case ("bluetooth", "screen_disabled"):
279
292
  if let disabled = value as? Bool {
280
293
  if disabled {
294
+ DeviceManager.shared.cancelPendingDisplay()
281
295
  DeviceManager.shared.sgc?.exit()
282
296
  } else {
283
297
  DeviceManager.shared.sgc?.clearDisplay()
@@ -0,0 +1,180 @@
1
+ import Foundation
2
+
3
+ /// Paces display updates leaving the phone to at most `setMaxFps` per second.
4
+ ///
5
+ /// Updates that arrive inside the current slot are held and sent at the next
6
+ /// one, last-wins: a newer scene frame replaces a pending one (with the pending
7
+ /// frame's per-element annotations folded in so per-element devices never miss
8
+ /// a change), a legacy layout replaces a pending scene and vice versa. Clears
9
+ /// (`flushScene` with an empty frame, `flushLegacy`) bypass the pacer and drop
10
+ /// pending work.
11
+ @MainActor
12
+ final class DisplayPacer {
13
+ /// Runs `block` after `delayMs`; the returned closure cancels it.
14
+ typealias Scheduler = (_ delayMs: Int64, _ block: @escaping @MainActor () -> Void) -> () -> Void
15
+
16
+ static let minFps = 2
17
+ static let maxFps = 20
18
+ static let defaultFps = 8
19
+
20
+ static func interval(forFps fps: Int) -> Int64 {
21
+ Int64(1000 / min(max(fps, minFps), maxFps))
22
+ }
23
+
24
+ static func monotonicMs() -> Int64 {
25
+ Int64(DispatchTime.now().uptimeNanoseconds / 1_000_000)
26
+ }
27
+
28
+ static func taskScheduler(_ delayMs: Int64, _ block: @escaping @MainActor () -> Void) -> () -> Void {
29
+ let task = Task { @MainActor in
30
+ try? await Task.sleep(nanoseconds: UInt64(max(0, delayMs)) * 1_000_000)
31
+ if Task.isCancelled { return }
32
+ block()
33
+ }
34
+ return { task.cancel() }
35
+ }
36
+
37
+ /// Fold pending frame `previous` into `next` (both unsent). The device only
38
+ /// ever sees the merged frame, so an element the pending frame changed must
39
+ /// not read as "unchanged" in the merged one, and ids the pending frame
40
+ /// removed must still be removed unless `next` re-creates them. A different
41
+ /// app is already a from-scratch replay.
42
+ static func merge(_ previous: SceneFrame?, into next: SceneFrame) -> SceneFrame {
43
+ guard let previous, previous.appId == next.appId else { return next }
44
+ var pending: [String: SceneElement] = [:]
45
+ for el in previous.elements { pending[el.id] = el }
46
+ let elements = next.elements.map { el -> SceneElement in
47
+ guard let prior = pending[el.id] else { return el }
48
+ if prior.change == "created" { return el.with(change: "created") }
49
+ if prior.change != "unchanged", el.change == "unchanged" { return el.with(change: "updated") }
50
+ return el
51
+ }
52
+ let painted = Set(next.elements.map(\.id))
53
+ var seen = Set<String>()
54
+ var removed: [String] = []
55
+ for id in previous.removed + next.removed where !painted.contains(id) && seen.insert(id).inserted {
56
+ removed.append(id)
57
+ }
58
+ return SceneFrame(
59
+ appId: next.appId, epoch: next.epoch, replay: previous.replay || next.replay,
60
+ elements: elements, removed: removed,
61
+ canvasWidth: next.canvasWidth, canvasHeight: next.canvasHeight, seq: next.seq
62
+ )
63
+ }
64
+
65
+ private let dispatchScene: (SceneFrame) -> Void
66
+ private let sendLegacy: () -> Void
67
+ private let clock: () -> Int64
68
+ private let scheduler: Scheduler
69
+
70
+ private var intervalMs: Int64 = DisplayPacer.interval(forFps: DisplayPacer.defaultFps)
71
+ private var lastSendAt: Int64 = Int64.min / 2
72
+ private var pendingScene: SceneFrame?
73
+ private var pendingLegacy = false
74
+ private var cancelTimer: (() -> Void)?
75
+
76
+ init(
77
+ dispatchScene: @escaping (SceneFrame) -> Void,
78
+ sendLegacy: @escaping () -> Void,
79
+ clock: @escaping () -> Int64 = DisplayPacer.monotonicMs,
80
+ scheduler: @escaping Scheduler = DisplayPacer.taskScheduler
81
+ ) {
82
+ self.dispatchScene = dispatchScene
83
+ self.sendLegacy = sendLegacy
84
+ self.clock = clock
85
+ self.scheduler = scheduler
86
+ }
87
+
88
+ func setMaxFps(_ fps: Int) {
89
+ intervalMs = Self.interval(forFps: fps)
90
+ guard hasPending else { return }
91
+ stopTimer()
92
+ let now = clock()
93
+ if now - lastSendAt >= intervalMs {
94
+ take(now)?()
95
+ } else {
96
+ armTimer(now)
97
+ }
98
+ }
99
+
100
+ /// Send `frame` at the next slot; replaces any pending update.
101
+ func submitScene(_ frame: SceneFrame) {
102
+ let now = clock()
103
+ pendingScene = Self.merge(pendingScene, into: frame)
104
+ pendingLegacy = false
105
+ submitPending(now)
106
+ }
107
+
108
+ /// Send the current legacy view state at the next slot; replaces any pending update.
109
+ func submitLegacy() {
110
+ let now = clock()
111
+ pendingScene = nil
112
+ pendingLegacy = true
113
+ submitPending(now)
114
+ }
115
+
116
+ /// Immediate send for an empty (clearing) frame; pending removes ride along.
117
+ func flushScene(_ frame: SceneFrame) {
118
+ stopTimer()
119
+ let merged = Self.merge(pendingScene, into: frame)
120
+ pendingScene = nil
121
+ pendingLegacy = false
122
+ lastSendAt = clock()
123
+ dispatchScene(merged)
124
+ }
125
+
126
+ /// Immediate legacy send (clear_view); drops any pending scene.
127
+ func flushLegacy() {
128
+ stopTimer()
129
+ pendingScene = nil
130
+ pendingLegacy = false
131
+ lastSendAt = clock()
132
+ sendLegacy()
133
+ }
134
+
135
+ /// Drop pending work without sending (disconnect, screen off, clearDisplay).
136
+ func cancel(reason: String = "cancelled") {
137
+ stopTimer()
138
+ pendingScene?.reportPresented(false, reason: reason)
139
+ pendingScene = nil
140
+ pendingLegacy = false
141
+ }
142
+
143
+ private var hasPending: Bool { pendingScene != nil || pendingLegacy }
144
+
145
+ private func submitPending(_ now: Int64) {
146
+ if now - lastSendAt >= intervalMs {
147
+ stopTimer()
148
+ take(now)?()
149
+ } else if cancelTimer == nil {
150
+ armTimer(now)
151
+ }
152
+ }
153
+
154
+ private func stopTimer() {
155
+ cancelTimer?()
156
+ cancelTimer = nil
157
+ }
158
+
159
+ /// Consume pending work into a send closure and stamp the slot.
160
+ private func take(_ now: Int64) -> (() -> Void)? {
161
+ let scene = pendingScene
162
+ let legacy = pendingLegacy
163
+ pendingScene = nil
164
+ pendingLegacy = false
165
+ if scene == nil, !legacy { return nil }
166
+ lastSendAt = now
167
+ if let scene { return { self.dispatchScene(scene) } }
168
+ return { self.sendLegacy() }
169
+ }
170
+
171
+ /// Arm the single trailing timer for the next slot.
172
+ private func armTimer(_ now: Int64) {
173
+ let delay = max(0, lastSendAt + intervalMs - now)
174
+ cancelTimer = scheduler(delay) { [weak self] in
175
+ guard let self else { return }
176
+ self.cancelTimer = nil
177
+ self.take(self.clock())?()
178
+ }
179
+ }
180
+ }
@@ -3,6 +3,15 @@ import Foundation
3
3
  /// Generated from /changelogs. Do not edit directly.
4
4
  let generatedReleaseChangelogs: [ReleaseChangelog] = [
5
5
  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.1.0", markdown: "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- GlasslyOS, Glassly 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."),
6
+ ReleaseChangelog(version: "3.1.0", markdown: "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- Glassly, Glassly 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."),
7
+ ReleaseChangelog(version: "0.2.8", markdown: "This release is under active development. User-facing changes will be documented as they land."),
8
+ ReleaseChangelog(version: "0.2.7", markdown: "This release is under active development. User-facing changes will be documented as they land."),
9
+ ReleaseChangelog(version: "0.2.6", markdown: "- Terminal follows light and dark mode, removes the on-glasses status badge, and uses font-scaled line spacing in the glasses session picker.\n- Notifications uses the explicit 5-second popup default and prevents text selection on iOS.\n- The session.display example removes the SVG and rotation demo."),
10
+ ReleaseChangelog(version: "0.2.5", markdown: "This release is under active development. User-facing changes will be documented as they land."),
11
+ ReleaseChangelog(version: "0.2.4", markdown: "This release is under active development. User-facing changes will be documented as they land."),
12
+ ReleaseChangelog(version: "0.2.3", markdown: "This release is under active development. User-facing changes will be documented as they land."),
13
+ ReleaseChangelog(version: "0.2.2", markdown: "This release is under active development. User-facing changes will be documented as they land."),
14
+ ReleaseChangelog(version: "0.2.1", markdown: "This release is under active development. User-facing changes will be documented as they land."),
15
+ ReleaseChangelog(version: "0.2.0", markdown: "The glasses UI on Glassly CFW is now a set of pages over one overlay: the dashboard, side menu, calendar, notification viewer and debug menu each stand on their own.\n\n- A phone notification shows the viewer on its own and closes back to whatever was on the glasses; it no longer opens the dashboard first, and the ignore-while-active hold is honoured everywhere on custom firmware.\n- Double tap on any page returns to the main content; head-down closes only the dashboard.\n- A debug menu on the dashboard (Debug Settings toggle) queries phone-side state such as isPhoneActive().\n- The metric toggle moved to Appearance, and miniapps can read the host's preferences.\n- Messenger group threads show on the notification title row."),
7
16
  ReleaseChangelog(version: "0.1.0", markdown: "First coordinated Glassly release: the app, Glassly Engine, and the Bluetooth SDK now ship one shared release version.\n\n- The Glassly App, Glassly Engine, and the Bluetooth SDK are versioned and published together.\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
17
  ]
@@ -2,10 +2,10 @@ import Foundation
2
2
 
3
3
  /// Generated by release CI. Do not edit in a release checkout.
4
4
  enum GeneratedReleaseMetadata {
5
- static let familyBaseVersion = "0.1.0"
6
- static let releaseIdentity = "0.1.0-dev.99"
7
- static let releaseSetId = "glassly-0.1.0-dev.99"
8
- static let sourceCommit = "2aeea5db8ad4e84303faf84a92d025f0d1930ebf"
5
+ static let familyBaseVersion = "0.2.8"
6
+ static let releaseIdentity = "0.2.8-dev.445"
7
+ static let releaseSetId = "glassly-0.2.8-dev.445"
8
+ static let sourceCommit = "7e82d274d259dc795d05a3aef988d261d871106a"
9
9
  static let otaManifestUrl = ""
10
10
  static let otaManifestSha256 = ""
11
11
  }