@glassly/bluetooth-sdk 0.1.0-dev.99 → 0.2.8-dev.444
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -19,40 +19,50 @@ final class G2CfwTextureCacheTests: XCTestCase {
|
|
|
19
19
|
)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
private func u32(_ v: UInt32) -> [UInt8] {
|
|
23
|
+
[UInt8(v & 0xFF), UInt8((v >> 8) & 0xFF), UInt8((v >> 16) & 0xFF), UInt8((v >> 24) & 0xFF)]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// MARK: - Asset mirror
|
|
23
27
|
|
|
24
|
-
func
|
|
28
|
+
func testFontsUploadWholeAsOneAssetWithRelativeGlyphOffsets() throws {
|
|
25
29
|
let cache = G2CfwTextureCache()
|
|
26
30
|
cache.reset(generation: 1)
|
|
27
|
-
let font = try XCTUnwrap(cache.font(fontSize: 12, cellWidth: 4, lineHeight: 2))
|
|
28
|
-
// Bytes 0..1 stay zero (invalid-image guard); the table lands at 2.
|
|
29
|
-
XCTAssertEqual(font.tableOffset, 2)
|
|
30
|
-
|
|
31
31
|
let raster = Data(repeating: 0xFF, count: 8) // 4×2 white cell
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
var rasterized = 0
|
|
33
|
+
let font = try XCTUnwrap(cache.font(fontSize: 12, cellWidth: 4, lineHeight: 2, codes: [65, 66]) { code in
|
|
34
|
+
rasterized += 1
|
|
35
|
+
return code == 65 ? raster : nil
|
|
36
|
+
})
|
|
37
|
+
XCTAssertEqual(rasterized, 2)
|
|
38
|
+
XCTAssertEqual(font.glyphs, [65])
|
|
39
|
+
XCTAssertEqual(font.id, G2CfwTextureCache.hash31("font:12:4x2:ascii"))
|
|
40
|
+
// asking again is a no-op: same entry, nothing rasterized, nothing uploaded twice
|
|
41
|
+
XCTAssertTrue(cache.font(fontSize: 12, cellWidth: 4, lineHeight: 2, codes: [65, 66]) { _ in rasterized += 1; return nil } === font)
|
|
42
|
+
XCTAssertEqual(rasterized, 2)
|
|
35
43
|
|
|
36
44
|
let payloads = cache.takeUploadPayloads()
|
|
37
45
|
XCTAssertEqual(payloads.count, 1)
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
XCTAssertEqual(Array(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
])
|
|
50
|
-
|
|
46
|
+
let p = [UInt8](payloads[0])
|
|
47
|
+
XCTAssertEqual(Array(p[0 ..< 5]), [37, 0, 0, 0, 0], "the link stamps epoch and request")
|
|
48
|
+
XCTAssertEqual(p[5], 0)
|
|
49
|
+
XCTAssertEqual(Array(p[6 ..< 10]), u32(font.id))
|
|
50
|
+
XCTAssertEqual(p[14], G2CfwTextureCache.kindFont)
|
|
51
|
+
let total = 96 * 4 + 2 + 1
|
|
52
|
+
XCTAssertEqual(Array(p[15 ..< 19]), u32(UInt32(total)))
|
|
53
|
+
XCTAssertEqual(Array(p[19 ..< 23]), u32(0))
|
|
54
|
+
XCTAssertEqual(Array(p[23 ..< 25]), [UInt8(total & 0xFF), UInt8(total >> 8)])
|
|
55
|
+
let table = Array(p[25 ..< 25 + 96 * 4])
|
|
56
|
+
XCTAssertEqual(Array(table[(65 - 32) * 4 ..< (65 - 32) * 4 + 4]), u32(UInt32(96 * 4)), "glyph A right after the table")
|
|
57
|
+
XCTAssertEqual(Array(table[(66 - 32) * 4 ..< (66 - 32) * 4 + 4]), [0, 0, 0, 0], "absent glyph")
|
|
58
|
+
XCTAssertEqual(Array(p[25 + 96 * 4 ..< 25 + total]), [4, 2, 0x8F])
|
|
51
59
|
XCTAssertTrue(cache.takeUploadPayloads().isEmpty)
|
|
60
|
+
// a forgotten font re-uploads
|
|
61
|
+
cache.forget(assetIds: [font.id])
|
|
62
|
+
_ = cache.font(fontSize: 12, cellWidth: 4, lineHeight: 2, codes: [65]) { _ in raster }
|
|
63
|
+
XCTAssertEqual(cache.takeUploadPayloads().count, 1)
|
|
52
64
|
cache.reset(generation: 2)
|
|
53
|
-
|
|
54
|
-
let font2 = try XCTUnwrap(cache.font(fontSize: 12, cellWidth: 4, lineHeight: 2))
|
|
55
|
-
XCTAssertTrue(cache.ensureGlyph(font: font2, char: UInt8(ascii: "A")) { raster })
|
|
65
|
+
_ = cache.font(fontSize: 12, cellWidth: 4, lineHeight: 2, codes: [65]) { _ in raster }
|
|
56
66
|
XCTAssertEqual(cache.takeUploadPayloads().count, 1)
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -62,34 +72,88 @@ final class G2CfwTextureCacheTests: XCTestCase {
|
|
|
62
72
|
XCTAssertFalse(cache.noteImageSighting("icon@10x10"))
|
|
63
73
|
XCTAssertTrue(cache.noteImageSighting("icon@10x10"))
|
|
64
74
|
let gray = Data(repeating: 0x80, count: 100)
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
XCTAssertEqual(cache.ensureImage(hash: "icon@10x10", gray: gray, width: 10, height: 10),
|
|
75
|
+
let id = cache.ensureImage(hash: "icon@10x10", gray: gray, width: 10, height: 10)
|
|
76
|
+
XCTAssertEqual(id, G2CfwTextureCache.hash31("icon@10x10"))
|
|
77
|
+
XCTAssertEqual(cache.ensureImage(hash: "icon@10x10", gray: gray, width: 10, height: 10), id)
|
|
68
78
|
XCTAssertNil(cache.ensureImage(hash: "big", gray: Data(count: 300 * 10), width: 300, height: 10))
|
|
79
|
+
XCTAssertEqual(cache.takeUploadPayloads().count, 1)
|
|
80
|
+
XCTAssertTrue(cache.holds(assetId: id!))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func testStringsAreBoundedAndLargeAssetsAreChunked() {
|
|
84
|
+
let cache = G2CfwTextureCache()
|
|
85
|
+
cache.reset(generation: 1)
|
|
86
|
+
XCTAssertNil(cache.ensureBytes(key: "empty", bytes: []))
|
|
87
|
+
XCTAssertNil(cache.ensureBytes(key: "nul", bytes: [65, 0]))
|
|
88
|
+
XCTAssertNil(cache.ensureBytes(key: "long", bytes: [UInt8](repeating: 65, count: 129)))
|
|
89
|
+
XCTAssertNotNil(cache.ensureBytes(key: "ok", bytes: [UInt8](repeating: 65, count: 128)))
|
|
90
|
+
let entries = G2CfwTextureCache.putAssetEntries(id: 7, version: 9, kind: 0, data: Data(repeating: 1, count: 20000))
|
|
91
|
+
XCTAssertEqual(entries.count, 3)
|
|
92
|
+
var offset = 0
|
|
93
|
+
for entry in entries {
|
|
94
|
+
let e = [UInt8](entry)
|
|
95
|
+
XCTAssertEqual(Array(e[10 ..< 14]), u32(20000))
|
|
96
|
+
XCTAssertEqual(Array(e[14 ..< 18]), u32(UInt32(offset)))
|
|
97
|
+
let len = Int(e[18]) | Int(e[19]) << 8
|
|
98
|
+
XCTAssertEqual(e.count, 20 + len)
|
|
99
|
+
offset += len
|
|
100
|
+
}
|
|
101
|
+
XCTAssertEqual(offset, 20000)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func testUploadPayloadsNameTheAssetsTheyPut() throws {
|
|
105
|
+
let cache = G2CfwTextureCache()
|
|
106
|
+
cache.reset(generation: 1)
|
|
107
|
+
var noise = Data(count: 255 * 255)
|
|
108
|
+
for i in 0 ..< noise.count { noise[i] = UInt8(truncatingIfNeeded: i * 37) }
|
|
109
|
+
let a = try XCTUnwrap(cache.ensureBytes(key: "a", bytes: [65, 66]))
|
|
110
|
+
let big = try XCTUnwrap(cache.ensureImage(hash: "noise", gray: noise, width: 255, height: 255))
|
|
111
|
+
let b = try XCTUnwrap(cache.ensureBytes(key: "b", bytes: [67]))
|
|
112
|
+
let payloads = cache.takeUploadPayloads()
|
|
113
|
+
XCTAssertGreaterThan(payloads.count, 1, "the image spans several payloads")
|
|
114
|
+
let ids = payloads.map(G2CfwTextureCache.assetIds(inUploadPayload:))
|
|
115
|
+
XCTAssertEqual(Set(ids.joined()), [a, big, b])
|
|
116
|
+
XCTAssertEqual(ids.first?.first, a)
|
|
117
|
+
XCTAssertEqual(ids.last?.last, b)
|
|
118
|
+
XCTAssertEqual(G2CfwTextureCache.assetIds(inUploadPayload: Data([38, 0, 0, 0, 0, 0])), [])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func testPredictedOverflowForgetsTheLeastRecentlyUsedAssets() {
|
|
122
|
+
let cache = G2CfwTextureCache()
|
|
123
|
+
cache.reset(generation: 1)
|
|
124
|
+
// 255x255 noisy tiles compress poorly: ~65 KiB each, four fill the store
|
|
125
|
+
var noise = Data(count: 255 * 255)
|
|
126
|
+
for i in 0 ..< noise.count { noise[i] = UInt8(truncatingIfNeeded: i * 37) }
|
|
127
|
+
var ids: [UInt32] = []
|
|
128
|
+
for i in 0 ..< 5 {
|
|
129
|
+
ids.append(cache.ensureImage(hash: "n\(i)", gray: noise, width: 255, height: 255)!)
|
|
130
|
+
}
|
|
131
|
+
XCTAssertTrue(cache.exhausted)
|
|
132
|
+
XCTAssertFalse(cache.holds(assetId: ids[0]), "the oldest prediction was dropped")
|
|
133
|
+
XCTAssertTrue(cache.holds(assetId: ids[4]))
|
|
134
|
+
XCTAssertLessThanOrEqual(cache.predictedBytes, G2CfwTextureCache.storeSize)
|
|
69
135
|
}
|
|
70
136
|
|
|
71
137
|
// MARK: - Draw payload wire formats
|
|
72
138
|
|
|
73
|
-
func
|
|
139
|
+
func testImageDrawPayloadMatchesMode19Wire() throws {
|
|
74
140
|
let payload = try XCTUnwrap(
|
|
75
|
-
G2CfwTextureCache.imageDrawPayload(
|
|
141
|
+
G2CfwTextureCache.imageDrawPayload(assetId: 0x1234, x: 300, y: -2, options: 0x0F)
|
|
76
142
|
)
|
|
77
|
-
XCTAssertEqual(Array(payload), [
|
|
143
|
+
XCTAssertEqual(Array(payload), [19, 0x34, 0x12, 0, 0, 0x2C, 0x01, 0xFE, 0xFF, 0x0F])
|
|
144
|
+
XCTAssertNil(G2CfwTextureCache.imageDrawPayload(assetId: 0, x: 0, y: 0, options: 0))
|
|
78
145
|
}
|
|
79
146
|
|
|
80
|
-
func
|
|
147
|
+
func testStringRunPayloadMatchesMode20Wire() throws {
|
|
81
148
|
let chars = Array("Hi".utf8)
|
|
82
149
|
let payload = try XCTUnwrap(G2CfwTextureCache.stringRunPayload(
|
|
83
|
-
|
|
150
|
+
fontId: 2, x: 40, y: 150, options: 0x0F, chars: chars
|
|
84
151
|
))
|
|
85
|
-
XCTAssertEqual(Array(payload), [
|
|
152
|
+
XCTAssertEqual(Array(payload), [20, 2, 0, 0, 0, 40, 0, 150, 0, 0x0F, 2] + chars)
|
|
86
153
|
// Control/non-ASCII bytes are x-adjust commands on the wire — rejected.
|
|
87
|
-
XCTAssertNil(G2CfwTextureCache.stringRunPayload(
|
|
88
|
-
|
|
89
|
-
))
|
|
90
|
-
XCTAssertNil(G2CfwTextureCache.stringRunPayload(
|
|
91
|
-
fontTableOffset: 2, x: 0, y: 0, options: 0, chars: [200]
|
|
92
|
-
))
|
|
154
|
+
XCTAssertNil(G2CfwTextureCache.stringRunPayload(fontId: 2, x: 0, y: 0, options: 0, chars: [10]))
|
|
155
|
+
XCTAssertNil(G2CfwTextureCache.stringRunPayload(fontId: 2, x: 0, y: 0, options: 0, chars: [200]))
|
|
156
|
+
XCTAssertNil(G2CfwTextureCache.stringRunPayload(fontId: 0, x: 0, y: 0, options: 0, chars: chars))
|
|
93
157
|
}
|
|
94
158
|
|
|
95
159
|
// MARK: - End-to-end mono fast path (actor)
|
|
@@ -126,10 +190,10 @@ final class G2CfwTextureCacheTests: XCTestCase {
|
|
|
126
190
|
)
|
|
127
191
|
let second = try XCTUnwrap(secondResult)
|
|
128
192
|
XCTAssertFalse(second.usedFullFrame)
|
|
129
|
-
// The changed cells travel as
|
|
193
|
+
// The changed cells travel as a font-asset PUT + mode-20 runs; no
|
|
130
194
|
// bitmap delta (0x83) should remain for a pure-ASCII grid change.
|
|
131
195
|
XCTAssertFalse(second.uploads.isEmpty)
|
|
132
|
-
XCTAssertTrue(second.uploads.allSatisfy { $0.first ==
|
|
196
|
+
XCTAssertTrue(second.uploads.allSatisfy { $0.first == 37 })
|
|
133
197
|
let payload = try XCTUnwrap(second.payload)
|
|
134
198
|
if payload.first == 8 {
|
|
135
199
|
var segments: [UInt8] = []
|
|
@@ -141,10 +205,10 @@ final class G2CfwTextureCacheTests: XCTestCase {
|
|
|
141
205
|
if pos < bytes.count { segments.append(bytes[pos]) }
|
|
142
206
|
pos += len
|
|
143
207
|
}
|
|
144
|
-
XCTAssertTrue(segments.contains(
|
|
208
|
+
XCTAssertTrue(segments.contains(20))
|
|
145
209
|
XCTAssertFalse(segments.contains(0x83))
|
|
146
210
|
} else {
|
|
147
|
-
XCTAssertEqual(payload.first,
|
|
211
|
+
XCTAssertEqual(payload.first, 20)
|
|
148
212
|
}
|
|
149
213
|
|
|
150
214
|
// A lens-unequal window must fall back to plain bitmap deltas.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
import zlib
|
|
5
|
+
|
|
6
|
+
/// Wire contract of the SID-0xf0 message stream (g2flash message_transport.c,
|
|
7
|
+
/// GLASSLYCFW/31). The probe in g2flash (`send_message_probe.py --dry-run`)
|
|
8
|
+
/// produces the same packets for the same input.
|
|
9
|
+
final class G2CfwTransportTests: XCTestCase {
|
|
10
|
+
private func inflateRecords(_ records: [Data]) throws -> [Data] {
|
|
11
|
+
var stream = z_stream()
|
|
12
|
+
XCTAssertEqual(inflateInit_(&stream, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)), Z_OK)
|
|
13
|
+
defer { inflateEnd(&stream) }
|
|
14
|
+
var out: [Data] = []
|
|
15
|
+
for record in records {
|
|
16
|
+
let flags = record[0]
|
|
17
|
+
let length = Int(record[1]) | Int(record[2]) << 8
|
|
18
|
+
XCTAssertEqual(length, record.count - 5)
|
|
19
|
+
let body = record.subdata(in: 5 ..< record.count)
|
|
20
|
+
guard flags & G2CfwTransport.flagCompressed != 0 else {
|
|
21
|
+
out.append(body)
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
// Every compressed record ends in a complete SYNC_FLUSH marker.
|
|
25
|
+
XCTAssertEqual(Array(body.suffix(4)), [0x00, 0x00, 0xFF, 0xFF])
|
|
26
|
+
// As the firmware does: a RESET_CONTEXT record starts a fresh inflate stream.
|
|
27
|
+
if flags & G2CfwTransport.flagResetContext != 0 { XCTAssertEqual(inflateReset(&stream), Z_OK) }
|
|
28
|
+
var decoded = Data(count: 0x10000)
|
|
29
|
+
|
|
30
|
+
var input = body
|
|
31
|
+
let produced: Int = input.withUnsafeMutableBytes { inRaw in
|
|
32
|
+
decoded.withUnsafeMutableBytes { outRaw in
|
|
33
|
+
stream.next_in = inRaw.bindMemory(to: Bytef.self).baseAddress
|
|
34
|
+
stream.avail_in = uInt(body.count)
|
|
35
|
+
stream.next_out = outRaw.bindMemory(to: Bytef.self).baseAddress
|
|
36
|
+
stream.avail_out = 0x10000
|
|
37
|
+
XCTAssertEqual(inflate(&stream, Z_NO_FLUSH), Z_OK)
|
|
38
|
+
XCTAssertEqual(stream.avail_in, 0)
|
|
39
|
+
return 0x10000 - Int(stream.avail_out)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
out.append(decoded.prefix(produced))
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testRecordsShareOneContextAndCarryThePlainCrc() throws {
|
|
48
|
+
let encoder = G2CfwTransport.Encoder()
|
|
49
|
+
let first = Data([6] + [UInt8](repeating: 0x00, count: 5000))
|
|
50
|
+
let second = Data([3, 1, 2, 3, 4, 5, 0] + [UInt8](repeating: 0x1F, count: 300))
|
|
51
|
+
let r1 = try XCTUnwrap(encoder.record(first, targets: 3))
|
|
52
|
+
let r2 = try XCTUnwrap(encoder.record(second, targets: 3))
|
|
53
|
+
// First record resets the context (flag 8); the second continues it.
|
|
54
|
+
XCTAssertEqual(r1[0], 3 | 4 | 8)
|
|
55
|
+
XCTAssertEqual(r2[0], 3 | 4)
|
|
56
|
+
XCTAssertEqual(UInt16(r1[3]) | UInt16(r1[4]) << 8, BinaryCodec.crc16CCITT(first))
|
|
57
|
+
XCTAssertEqual(UInt16(r2[3]) | UInt16(r2[4]) << 8, BinaryCodec.crc16CCITT(second))
|
|
58
|
+
XCTAssertLessThan(r1.count, 100) // 5000 zero bytes deflate to a few
|
|
59
|
+
XCTAssertEqual(try inflateRecords([r1, r2]), [first, second])
|
|
60
|
+
// After a NACK/timeout the context restarts and the next record says so.
|
|
61
|
+
encoder.resetContext()
|
|
62
|
+
let r3 = try XCTUnwrap(encoder.record(second, targets: 1))
|
|
63
|
+
XCTAssertEqual(r3[0], 1 | 4 | 8)
|
|
64
|
+
XCTAssertEqual(try inflateRecords([r3]), [second])
|
|
65
|
+
XCTAssertNil(encoder.record(Data(count: 0x10000), targets: 3))
|
|
66
|
+
XCTAssertNil(encoder.record(Data([7, 255]), targets: 4))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func testLensTargetedRecordsAreSelfContainedSoTheSkippingLensStaysInSync() throws {
|
|
70
|
+
let encoder = G2CfwTransport.Encoder()
|
|
71
|
+
let shared = Data([38, 0, 0, 0, 0, 1, 0] + [UInt8](repeating: 0x2A, count: 400))
|
|
72
|
+
let leftOnly = Data([38, 0, 0, 1, 0, 1, 0] + [UInt8](repeating: 0x2B, count: 400))
|
|
73
|
+
let rightOnly = Data([38, 0, 0, 2, 0, 1, 0] + [UInt8](repeating: 0x2C, count: 400))
|
|
74
|
+
let r1 = try XCTUnwrap(encoder.record(shared, targets: 3))
|
|
75
|
+
let r2 = try XCTUnwrap(encoder.record(leftOnly, targets: 1))
|
|
76
|
+
let r3 = try XCTUnwrap(encoder.record(rightOnly, targets: 2))
|
|
77
|
+
let r4 = try XCTUnwrap(encoder.record(shared, targets: 3))
|
|
78
|
+
let r5 = try XCTUnwrap(encoder.record(shared, targets: 3))
|
|
79
|
+
XCTAssertEqual(r2[0], 1 | 4 | 8, "a left-only record starts a fresh context")
|
|
80
|
+
XCTAssertEqual(r3[0], 2 | 4 | 8)
|
|
81
|
+
XCTAssertEqual(r4[0], 3 | 4 | 8, "and the record after it starts fresh again")
|
|
82
|
+
XCTAssertEqual(r5[0], 3 | 4, "then the shared context resumes")
|
|
83
|
+
// Each lens inflates exactly the records addressed to it.
|
|
84
|
+
XCTAssertEqual(try inflateRecords([r1, r2, r4, r5]), [shared, leftOnly, shared, shared])
|
|
85
|
+
XCTAssertEqual(try inflateRecords([r1, r3, r4, r5]), [shared, rightOnly, shared, shared])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func testPacketsMatchTheProbeFraming() {
|
|
89
|
+
|
|
90
|
+
let stream = Data((0 ..< 500).map { UInt8($0 & 0xFF) })
|
|
91
|
+
let packets = G2CfwTransport.packets(stream: stream, sequence: 254, targets: 3, chunkBytes: 200)
|
|
92
|
+
XCTAssertEqual(packets.count, 3)
|
|
93
|
+
// [aa 21 seq len+2 1 1 f0 0][options][chunk][crc lo][crc hi]
|
|
94
|
+
XCTAssertEqual(Array(packets[0].prefix(9)), [0xAA, 0x21, 254, 203, 1, 1, 0xF0, 0, 3 | 0x80])
|
|
95
|
+
XCTAssertEqual(Array(packets[1].prefix(9)), [0xAA, 0x21, 255, 203, 1, 1, 0xF0, 0, 3])
|
|
96
|
+
XCTAssertEqual(Array(packets[2].prefix(9)), [0xAA, 0x21, 0, 103, 1, 1, 0xF0, 0, 3 | 0x40])
|
|
97
|
+
XCTAssertEqual(packets[2].count, 8 + 1 + 100 + 2)
|
|
98
|
+
for packet in packets {
|
|
99
|
+
let body = packet.subdata(in: 8 ..< packet.count - 2)
|
|
100
|
+
XCTAssertEqual(Array(packet.suffix(2)), BinaryCodec.crc16CCITTLittleEndian(body))
|
|
101
|
+
XCTAssertEqual(Int(packet[3]), packet.count - 8)
|
|
102
|
+
}
|
|
103
|
+
XCTAssertEqual(Array(packets.flatMap { $0.subdata(in: 9 ..< $0.count - 2) }), Array(stream))
|
|
104
|
+
// An empty stream is one RESET|END packet; the probe rejects >252-byte chunks.
|
|
105
|
+
let empty = G2CfwTransport.packets(stream: Data(), sequence: 7, targets: 1)
|
|
106
|
+
XCTAssertEqual(Array(empty[0]), [0xAA, 0x21, 7, 3, 1, 1, 0xF0, 0, 1 | 0x80 | 0x40] + BinaryCodec.crc16CCITTLittleEndian([1 | 0x80 | 0x40]))
|
|
107
|
+
XCTAssertLessThanOrEqual(G2CfwTransport.maxChunkBytes, 252)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func testAckParsingIncludesHistoryAndRejectsMalformedReplies() throws {
|
|
111
|
+
let body = Data([1, 7, 0, 0, 2, 0x34, 0x12, 0xCD, 0xAB, 6, 1, 0, 0x10, 0, 0x22, 0x11])
|
|
112
|
+
let ack = try XCTUnwrap(G2CfwTransport.parseAck(body))
|
|
113
|
+
XCTAssertTrue(ack.success)
|
|
114
|
+
XCTAssertEqual(ack.lens, 2)
|
|
115
|
+
XCTAssertEqual(ack.current, .init(streamId: 7, ordinal: 0, size: 0x1234, crc: 0xABCD))
|
|
116
|
+
XCTAssertEqual(ack.entries.count, 2)
|
|
117
|
+
XCTAssertEqual(ack.entries[1], .init(streamId: 6, ordinal: 1, size: 0x10, crc: 0x1122))
|
|
118
|
+
let nack = try XCTUnwrap(G2CfwTransport.parseAck(Data([3, 7, 0, 0, 1, 0, 0, 0, 0])))
|
|
119
|
+
XCTAssertFalse(nack.success)
|
|
120
|
+
XCTAssertNil(G2CfwTransport.parseAck(Data([3, 7, 0, 0, 1, 0, 0, 0, 0, 6, 1, 0, 0, 0, 0, 0]))) // NACK has no history
|
|
121
|
+
XCTAssertNil(G2CfwTransport.parseAck(Data([2, 7, 0, 0, 1, 0, 0, 0, 0])))
|
|
122
|
+
XCTAssertNil(G2CfwTransport.parseAck(Data([1, 7, 0, 0, 3, 0, 0, 0, 0])))
|
|
123
|
+
XCTAssertNil(G2CfwTransport.parseAck(Data([1, 7, 0, 0])))
|
|
124
|
+
|
|
125
|
+
// Notify frame as the glasses send it: [aa 12 seq len 1 1 f0 0][body][crc].
|
|
126
|
+
var frame = Data([0xAA, 0x12, 9, UInt8(body.count + 2), 1, 1, 0xF0, 0])
|
|
127
|
+
frame.append(body)
|
|
128
|
+
frame.append(contentsOf: BinaryCodec.crc16CCITTLittleEndian(body))
|
|
129
|
+
XCTAssertEqual(G2CfwTransport.replyBody(fromNotifyFrame: frame), body)
|
|
130
|
+
var corrupt = frame
|
|
131
|
+
corrupt[10] ^= 1
|
|
132
|
+
XCTAssertNil(G2CfwTransport.replyBody(fromNotifyFrame: corrupt))
|
|
133
|
+
var otherService = frame
|
|
134
|
+
otherService[6] = 0xE0
|
|
135
|
+
XCTAssertNil(G2CfwTransport.replyBody(fromNotifyFrame: otherService))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@MainActor
|
|
139
|
+
func testAckBoxCompletesWhenBothLensesAckAndFailsOnNack() async throws {
|
|
140
|
+
let box = G2CfwAckBox()
|
|
141
|
+
let left = try XCTUnwrap(G2CfwTransport.parseAck(Data([1, 5, 0, 0, 1, 2, 0, 0, 0])))
|
|
142
|
+
let right = try XCTUnwrap(G2CfwTransport.parseAck(Data([1, 5, 0, 0, 2, 2, 0, 0, 0])))
|
|
143
|
+
let ok = await box.wait(streamId: 5, lenses: 3, timeoutNs: 1_000_000_000) { _ in
|
|
144
|
+
XCTAssertFalse(box.resolve(left)) // one lens is not enough
|
|
145
|
+
XCTAssertFalse(box.resolve(left)) // duplicate is harmless
|
|
146
|
+
XCTAssertTrue(box.resolve(right))
|
|
147
|
+
}
|
|
148
|
+
XCTAssertTrue(ok)
|
|
149
|
+
XCTAssertEqual(box.outcome()?.reason, "ack")
|
|
150
|
+
XCTAssertLessThan(box.msSinceLastSuccess(), 1000)
|
|
151
|
+
|
|
152
|
+
let nack = try XCTUnwrap(G2CfwTransport.parseAck(Data([3, 6, 0, 0, 2, 0, 0, 0, 0])))
|
|
153
|
+
let failed = await box.wait(streamId: 6, lenses: 3, timeoutNs: 1_000_000_000) { _ in
|
|
154
|
+
XCTAssertFalse(box.resolve(left)) // wrong stream
|
|
155
|
+
XCTAssertTrue(box.resolve(nack))
|
|
156
|
+
}
|
|
157
|
+
XCTAssertFalse(failed)
|
|
158
|
+
XCTAssertEqual(box.outcome()?.reason, "nack")
|
|
159
|
+
|
|
160
|
+
let timedOut = await box.wait(streamId: 7, lenses: 1, timeoutNs: 50_000_000) { _ in }
|
|
161
|
+
XCTAssertFalse(timedOut)
|
|
162
|
+
XCTAssertEqual(box.outcome()?.reason, "timeout")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@MainActor
|
|
166
|
+
func testQueuedWritesExpireBeforeCreditsReturnAndStayExpiredWhenStreamIdIsReused() async {
|
|
167
|
+
let box = G2CfwAckBox()
|
|
168
|
+
var staleWrite: (() -> Bool)?
|
|
169
|
+
let timedOut = await box.wait(streamId: 7, lenses: 1, timeoutNs: 20_000_000) { isPending in
|
|
170
|
+
staleWrite = isPending
|
|
171
|
+
XCTAssertTrue(isPending())
|
|
172
|
+
// Simulate no BLE credits: no packet is written and no ACK arrives.
|
|
173
|
+
}
|
|
174
|
+
XCTAssertFalse(timedOut)
|
|
175
|
+
XCTAssertEqual(box.outcome()?.reason, "timeout")
|
|
176
|
+
XCTAssertEqual(staleWrite?(), false)
|
|
177
|
+
|
|
178
|
+
// Recovery can reuse the wire ID after it wraps. An old queue entry
|
|
179
|
+
// must still be discarded, while the recovery's packets may be sent.
|
|
180
|
+
var recoveryWrite: (() -> Bool)?
|
|
181
|
+
let recovered = await box.wait(streamId: 7, lenses: 1, timeoutNs: 1_000_000_000) { isPending in
|
|
182
|
+
recoveryWrite = isPending
|
|
183
|
+
XCTAssertEqual(staleWrite?(), false)
|
|
184
|
+
XCTAssertTrue(isPending())
|
|
185
|
+
box.resolve(G2CfwTransport.Ack(success: true, lens: 1, entries: [.init(streamId: 7, ordinal: 0, size: 1, crc: 0)]))
|
|
186
|
+
}
|
|
187
|
+
XCTAssertTrue(recovered)
|
|
188
|
+
XCTAssertEqual(recoveryWrite?(), false)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@MainActor
|
|
192
|
+
func testAckBoxSingleLensAndCancellation() async throws {
|
|
193
|
+
let box = G2CfwAckBox()
|
|
194
|
+
let right = try XCTUnwrap(G2CfwTransport.parseAck(Data([1, 9, 0, 0, 2, 0, 0, 0, 0])))
|
|
195
|
+
let ok = await box.wait(streamId: 9, lenses: 2, timeoutNs: 1_000_000_000) { _ in
|
|
196
|
+
XCTAssertTrue(box.resolve(right))
|
|
197
|
+
}
|
|
198
|
+
XCTAssertTrue(ok)
|
|
199
|
+
var queuedWrite: (() -> Bool)?
|
|
200
|
+
let task = Task { @MainActor in
|
|
201
|
+
await box.wait(streamId: 10, lenses: 3, timeoutNs: 60_000_000_000) { isPending in
|
|
202
|
+
queuedWrite = isPending
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
try await Task.sleep(nanoseconds: 20_000_000)
|
|
206
|
+
task.cancel()
|
|
207
|
+
let cancelled = await task.value
|
|
208
|
+
XCTAssertFalse(cancelled)
|
|
209
|
+
XCTAssertEqual(box.outcome()?.reason, "cancelled")
|
|
210
|
+
XCTAssertEqual(queuedWrite?(), false)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func testCacheRepliesParseAndStampFillsEpochAndRequest() async throws {
|
|
214
|
+
let body = Data([5, 9, 0, 0, 2, 38, 7, 0, 2, 0x34, 0x12, 5, 0, 0, 0, 2, 1, 1, 0, 0, 0, 0, 1, 0, 0])
|
|
215
|
+
let reply = try XCTUnwrap(G2CfwTransport.parseCacheReply(body))
|
|
216
|
+
XCTAssertEqual(reply.streamId, 9)
|
|
217
|
+
XCTAssertEqual(reply.lens, 2)
|
|
218
|
+
XCTAssertEqual(reply.mode, 38)
|
|
219
|
+
XCTAssertEqual(reply.request, 7)
|
|
220
|
+
XCTAssertEqual(reply.status, G2CfwTransport.CacheReply.missing)
|
|
221
|
+
XCTAssertEqual(reply.epoch, 0x1234)
|
|
222
|
+
XCTAssertEqual(reply.revision, 5)
|
|
223
|
+
XCTAssertEqual(reply.missingIds, [1, 256])
|
|
224
|
+
XCTAssertTrue(reply.summary.hasPrefix("missing"))
|
|
225
|
+
XCTAssertNil(G2CfwTransport.parseCacheReply(Data([1, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])))
|
|
226
|
+
XCTAssertNil(G2CfwTransport.parseAck(body))
|
|
227
|
+
|
|
228
|
+
let show = Data([38, 0, 0, 0, 0, 1, 0, 0, 0, 0])
|
|
229
|
+
XCTAssertTrue(G2CfwTransport.isCacheMessage(show))
|
|
230
|
+
XCTAssertFalse(G2CfwTransport.isCacheMessage(Data([36, 0, 0, 0, 0])))
|
|
231
|
+
XCTAssertEqual(Array(G2CfwTransport.stamp(show, epoch: 0x1234, request: 0x0102).prefix(5)), [38, 0x34, 0x12, 2, 1])
|
|
232
|
+
let tagged = Data([38, 0, 0, 9, 0, 1, 0, 0, 0, 0])
|
|
233
|
+
XCTAssertEqual(Array(G2CfwTransport.stamp(tagged, epoch: 1, request: 7).prefix(5)), [38, 1, 0, 9, 0], "a tagged request is kept")
|
|
234
|
+
let state = Data([40, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
235
|
+
XCTAssertEqual(Array(G2CfwTransport.stamp(state, epoch: 1, request: 7).prefix(5)), [40, 1, 0, 0, 0], "STATE carries a revision, not a request")
|
|
236
|
+
|
|
237
|
+
let box = G2CfwAckBox()
|
|
238
|
+
let armed = expectation(description: "armed")
|
|
239
|
+
Task { @MainActor in
|
|
240
|
+
let ok = await box.wait(streamId: 9, lenses: G2CfwTransport.lensBoth, timeoutNs: 1_000_000_000) { _ in
|
|
241
|
+
box.record(reply)
|
|
242
|
+
box.resolve(G2CfwTransport.Ack(success: true, lens: 2, entries: [.init(streamId: 9, ordinal: 0, size: 1, crc: 0)]))
|
|
243
|
+
box.resolve(G2CfwTransport.Ack(success: true, lens: 1, entries: [.init(streamId: 9, ordinal: 0, size: 1, crc: 0)]))
|
|
244
|
+
}
|
|
245
|
+
XCTAssertTrue(ok)
|
|
246
|
+
XCTAssertEqual(box.cacheReplies()[2], reply)
|
|
247
|
+
XCTAssertNil(box.cacheReplies()[1])
|
|
248
|
+
armed.fulfill()
|
|
249
|
+
}
|
|
250
|
+
await fulfillment(of: [armed], timeout: 2)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func testAckTimeoutScalesWithStreamSize() {
|
|
254
|
+
XCTAssertEqual(G2CfwTransport.ackTimeoutNs(streamBytes: 0), 3_500_000_000)
|
|
255
|
+
XCTAssertEqual(G2CfwTransport.ackTimeoutNs(streamBytes: 65_535), 3_500_000_000 + 65_535 * 1_000_000)
|
|
256
|
+
// A 4.5 KB upload chunk acked in ~4.1 s on GLASSLYCFW/39; the deadline must clear it.
|
|
257
|
+
XCTAssertGreaterThan(G2CfwTransport.ackTimeoutNs(streamBytes: 4_500), 6_000_000_000)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class G2DisplayDiagnosticsTests: XCTestCase {
|
|
6
|
+
private func decode(_ line: String) throws -> [String: Any] {
|
|
7
|
+
try XCTUnwrap(JSONSerialization.jsonObject(with: Data(line.dropFirst(7).utf8)) as? [String: Any])
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@MainActor
|
|
11
|
+
func testSamplesAreThrottledResetAndLeaveOutEmptyValues() throws {
|
|
12
|
+
let diagnostics = G2DisplayDiagnostics(enabled: true, now: 0)
|
|
13
|
+
diagnostics.count("framesIn")
|
|
14
|
+
diagnostics.count("txBytes", 100)
|
|
15
|
+
diagnostics.timing("ackMs", 10)
|
|
16
|
+
diagnostics.timing("ackMs", 30.25)
|
|
17
|
+
diagnostics.timing("resumeMs", 0.2)
|
|
18
|
+
XCTAssertNil(diagnostics.sample(context: [:], now: 0.9))
|
|
19
|
+
let line = try XCTUnwrap(diagnostics.sample(
|
|
20
|
+
context: ["app": "com.glassly.dashboard", "qL": 0, "pendingFrame": false, "page": true, "blockedL": false],
|
|
21
|
+
now: 1
|
|
22
|
+
))
|
|
23
|
+
let first = try decode(line)
|
|
24
|
+
XCTAssertEqual(first["txBytes"] as? Int, 100)
|
|
25
|
+
XCTAssertEqual(first["ackMsAvg"] as? Double, 20.1)
|
|
26
|
+
XCTAssertEqual(first["ackMsMax"] as? Double, 30.3)
|
|
27
|
+
XCTAssertNil(first["ackMsTotal"])
|
|
28
|
+
XCTAssertNil(first["resumeMsAvg"], "a sub-millisecond timing is left out")
|
|
29
|
+
XCTAssertTrue(line.contains("\"ackMsAvg\":20.1,"), "one decimal, no binary float noise: \(line)")
|
|
30
|
+
XCTAssertEqual(first["windowMs"] as? Int, 1000)
|
|
31
|
+
XCTAssertEqual(first["page"] as? Bool, true)
|
|
32
|
+
for key in ["qL", "pendingFrame", "blockedL", "timeouts", "errors"] {
|
|
33
|
+
XCTAssertNil(first[key], key)
|
|
34
|
+
}
|
|
35
|
+
XCTAssertEqual((first["run"] as? String)?.count, 8)
|
|
36
|
+
// Counters and timings reset with the window.
|
|
37
|
+
diagnostics.count("framesIn")
|
|
38
|
+
let second = try decode(XCTUnwrap(diagnostics.sample(context: [:], now: 2)))
|
|
39
|
+
XCTAssertNil(second["txBytes"])
|
|
40
|
+
XCTAssertNil(second["ackMsAvg"])
|
|
41
|
+
XCTAssertEqual(first["run"] as? String, second["run"] as? String)
|
|
42
|
+
XCTAssertEqual(second["id"] as? Int, 2)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@MainActor
|
|
46
|
+
func testIdleWindowsAreSkippedUnlessBusyOrLate() throws {
|
|
47
|
+
let diagnostics = G2DisplayDiagnostics(enabled: true, now: 0)
|
|
48
|
+
// IMU reports and heartbeat writes alone are idle.
|
|
49
|
+
diagnostics.count("imuReceived")
|
|
50
|
+
diagnostics.count("txPackets", 2)
|
|
51
|
+
diagnostics.timing("notifyDispatchMs", 0.1)
|
|
52
|
+
XCTAssertNil(diagnostics.sample(context: [:], now: 1))
|
|
53
|
+
// Something queued for the glasses is never idle.
|
|
54
|
+
XCTAssertNotNil(diagnostics.sample(context: ["qL": 3], busy: true, now: 2))
|
|
55
|
+
// A tick held up past the stretched window is logged even when nothing happened.
|
|
56
|
+
XCTAssertNil(diagnostics.sample(context: [:], now: 3))
|
|
57
|
+
let late = try decode(XCTUnwrap(diagnostics.sample(context: [:], now: 26.3)))
|
|
58
|
+
XCTAssertEqual(late["windowMs"] as? Int, 23300)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@MainActor
|
|
62
|
+
func testSessionFactsAreLoggedOnChangeAndOnceAMinute() throws {
|
|
63
|
+
let diagnostics = G2DisplayDiagnostics(enabled: true, now: 0)
|
|
64
|
+
let facts: [String: Any] = ["cfw": "GLASSLYCFW/39", "mtuL": 244, "mtuR": 244, "fpsLimit": 8]
|
|
65
|
+
let first = try decode(XCTUnwrap(diagnostics.session(facts, now: 1)))
|
|
66
|
+
XCTAssertEqual(first["type"] as? String, "session")
|
|
67
|
+
XCTAssertEqual(first["cfw"] as? String, "GLASSLYCFW/39")
|
|
68
|
+
XCTAssertNil(diagnostics.session(facts, now: 30))
|
|
69
|
+
var changed = facts
|
|
70
|
+
changed["mtuR"] = 180
|
|
71
|
+
XCTAssertNotNil(diagnostics.session(changed, now: 31))
|
|
72
|
+
XCTAssertNil(diagnostics.session(changed, now: 90))
|
|
73
|
+
XCTAssertNotNil(diagnostics.session(changed, now: 91))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@MainActor
|
|
77
|
+
func testStallDetailsAreBoundedButFailureCountersAreNotLost() throws {
|
|
78
|
+
let diagnostics = G2DisplayDiagnostics(enabled: true, now: 0)
|
|
79
|
+
for i in 0 ..< 100 {
|
|
80
|
+
diagnostics.count("errors")
|
|
81
|
+
let line = diagnostics.stall(fields: ["result": "timeout"], now: Double(i) / 100)
|
|
82
|
+
XCTAssertEqual(line != nil, i == 0)
|
|
83
|
+
}
|
|
84
|
+
let sample = try decode(XCTUnwrap(diagnostics.sample(context: [:], now: 2)))
|
|
85
|
+
XCTAssertEqual(sample["errors"] as? Int, 100)
|
|
86
|
+
XCTAssertEqual(sample["windowMs"] as? Int, 2000)
|
|
87
|
+
XCTAssertNotNil(diagnostics.stall(fields: [:], now: 2))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -129,9 +129,10 @@ final class G2FlashProtocolTests: XCTestCase {
|
|
|
129
129
|
XCTAssertEqual(Array(longRun), [0, 0, 0, 1])
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
func
|
|
132
|
+
func testCfwBlackPayloadUsesModeSixRleEnvelope() throws {
|
|
133
|
+
// Plain RLE follows the mode byte: the SID-0xf0 transport compresses.
|
|
133
134
|
let payload = try XCTUnwrap(G2CfwRenderer.blackPayload())
|
|
134
|
-
XCTAssertEqual(Array(payload.prefix(
|
|
135
|
+
XCTAssertEqual(Array(payload.prefix(5)), [6, 0, 0, 0xFF, 0xFF])
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
private func makeFirmware() -> [UInt8] {
|