@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.
- package/README.md +47 -6
- package/android/build.gradle +7 -7
- package/android/lc3Lib/build.gradle +2 -2
- package/android/src/main/assets/G2Firmware/README.md +27 -2
- 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
- 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
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1065 -255
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +209 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +30 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +28 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +37 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +102 -13
- package/ios/GlasslyBluetoothSDK.podspec +1 -1
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
- package/ios/Source/BluetoothSDK.swift +53 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -0
- package/ios/Source/DeviceManager.swift +151 -33
- package/ios/Source/DeviceStore.swift +28 -14
- package/ios/Source/DisplayPacer.swift +180 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
- package/ios/Source/GlassesNotifications.swift +181 -0
- package/ios/Source/ObservableStore.swift +14 -2
- package/ios/Source/Resources/G2Firmware/README.md +27 -2
- package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/controllers/R1.swift +0 -1
- package/ios/Source/internal/BluetoothAvailability.swift +31 -0
- package/ios/Source/sgcs/G2.swift +1420 -495
- package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
- package/ios/Source/sgcs/G2BleLink.swift +102 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
- package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
- package/ios/Source/sgcs/G2CfwScene.swift +779 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
- package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
- package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +9 -9
- package/ios/Source/sgcs/G2Keyboard.swift +780 -0
- package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
- package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
- package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
- package/ios/Source/sgcs/G2RingBattery.swift +22 -0
- package/ios/Source/sgcs/G2Sensors.swift +75 -0
- package/ios/Source/sgcs/G2TouchInput.swift +51 -0
- package/ios/Source/sgcs/G2WritePacing.swift +18 -0
- package/ios/Source/sgcs/MentraNex.swift +44 -44
- package/ios/Source/sgcs/SGCManager.swift +120 -13
- package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
- package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
- package/ios/Source/status/DeviceStatus.swift +27 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
- package/ios/Source/utils/Enums.swift +0 -1
- package/ios/Source/utils/G1Text.swift +0 -1
- package/ios/Source/utils/G2NotifyFile.swift +20 -10
- package/ios/Source/utils/JSCExperiment.swift +6 -6
- package/ios/Source/utils/Models.swift +0 -1
- package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
- package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
- package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
- package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
- package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
- package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
- package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
- package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
- package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +2 -2
- package/plugin/build/withIos.js +1 -1
- package/scripts/gen-nex-proto.sh +12 -0
- package/scripts/inject-ios-sdk-version.mjs +1 -1
- package/src/BluetoothSdk.types.ts +214 -19
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +40 -0
- package/src/generated/changelogCatalog.ts +37 -1
- package/src/generated/releaseMetadata.ts +4 -4
- 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 ||
|
|
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
|
-
//
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
1040
|
+
func restartTranscriber() throws {
|
|
1041
|
+
#if !SWIFT_PACKAGE || FEATURE_LOCAL_STT
|
|
1032
1042
|
Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
|
|
1033
|
-
transcriber
|
|
1043
|
+
let nextTranscriber = transcriber ?? SherpaOnnxTranscriber()
|
|
1044
|
+
try nextTranscriber.restart()
|
|
1045
|
+
transcriber = nextTranscriber
|
|
1034
1046
|
#else
|
|
1035
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 ||
|
|
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
|
|
119
|
-
store.set(
|
|
120
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
6
|
-
static let releaseIdentity = "0.
|
|
7
|
-
static let releaseSetId = "glassly-0.
|
|
8
|
-
static let sourceCommit = "
|
|
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
|
}
|