@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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import Foundation
|
|
18
18
|
|
|
19
|
-
#if !SWIFT_PACKAGE ||
|
|
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
|
|
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
|
-
*
|
|
89
|
+
* Model assets come from the saved path; no UI context is needed.
|
|
94
90
|
*/
|
|
95
|
-
init(
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
}
|
|
@@ -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
|
|
45
|
-
///
|
|
46
|
-
///
|
|
47
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
header.appendLittleEndianUInt32(
|
|
53
|
-
header.appendLittleEndianUInt32(
|
|
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)
|
|
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 "
|
|
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
|
|
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["
|
|
65
|
-
os_log("🧪
|
|
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 = "
|
|
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, *) {
|
|
@@ -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
|
+
}
|