@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
/// Pins the ANCS relay wire format shared with g2flash's patches/ancs_relay.c.
|
|
6
|
+
final class G2AncsRelayTests: XCTestCase {
|
|
7
|
+
private var relay = G2AncsRelay()
|
|
8
|
+
|
|
9
|
+
override func setUp() {
|
|
10
|
+
super.setUp()
|
|
11
|
+
relay = G2AncsRelay()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// MARK: - helpers
|
|
15
|
+
|
|
16
|
+
private func le16(_ v: Int) -> [UInt8] { [UInt8(v & 0xFF), UInt8(v >> 8)] }
|
|
17
|
+
private func le32(_ v: UInt32) -> [UInt8] {
|
|
18
|
+
[UInt8(v & 0xFF), UInt8((v >> 8) & 0xFF), UInt8((v >> 16) & 0xFF), UInt8(v >> 24)]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private func source(uid: UInt32, event: UInt8 = 0, flags: UInt8 = 0, category: UInt8 = 4, count: UInt8 = 1) -> Data {
|
|
22
|
+
Data([0x41, 0x4E, 1, 1, event, flags, category, count] + le32(uid))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func attr(uid: UInt32, id: UInt8, _ value: String, total: Int? = nil, offset: Int = 0) -> Data {
|
|
26
|
+
let bytes = [UInt8](value.utf8)
|
|
27
|
+
return Data([0x41, 0x4E, 1, 2] + le32(uid) + [id] + le16(total ?? bytes.count) + le16(offset) + bytes)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func app(_ id: String, name: String, total: Int? = nil, offset: Int = 0) -> Data {
|
|
31
|
+
let idBytes = [UInt8](id.utf8), nameBytes = [UInt8](name.utf8)
|
|
32
|
+
return Data([0x41, 0x4E, 1, 3] + le16(total ?? nameBytes.count) + le16(offset) + [UInt8(idBytes.count)] + idBytes + nameBytes)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Feed a whole notification; returns the event produced by the last attribute.
|
|
36
|
+
private func feedNotification(
|
|
37
|
+
uid: UInt32, flags: UInt8 = 0, app: String = "com.apple.MobileSMS",
|
|
38
|
+
title: String = "Alice", subtitle: String = "", message: String = "hi",
|
|
39
|
+
date: String = "20260902T183000"
|
|
40
|
+
) -> G2AncsRelay.Event? {
|
|
41
|
+
XCTAssertNil(relay.ingest(source(uid: uid, flags: flags)))
|
|
42
|
+
let values: [(UInt8, String)] = [
|
|
43
|
+
(0, app), (1, title), (2, subtitle), (3, message), (4, "\(message.utf8.count)"),
|
|
44
|
+
(5, date), (6, "Open"), (7, "Clear"),
|
|
45
|
+
]
|
|
46
|
+
var last: G2AncsRelay.Event?
|
|
47
|
+
for (id, value) in values {
|
|
48
|
+
last = relay.ingest(attr(uid: uid, id: id, value))
|
|
49
|
+
if id != 7 { XCTAssertNil(last, "attribute \(id) completed the notification early") }
|
|
50
|
+
}
|
|
51
|
+
return last
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - encoding
|
|
55
|
+
|
|
56
|
+
func testControlPacketFramesFieldOneHundredSix() {
|
|
57
|
+
XCTAssertEqual(
|
|
58
|
+
[UInt8](G2AncsRelay.controlPacket(op: .enable)),
|
|
59
|
+
[0x08, 0x01, 0x10, 0x00, 0xF2, 0x07, 0x04, 0x41, 0x4E, 0x01, 0x01]
|
|
60
|
+
)
|
|
61
|
+
XCTAssertEqual(
|
|
62
|
+
[UInt8](G2AncsRelay.actionPacket(uid: 0x1234_5678, positive: false)),
|
|
63
|
+
[0x08, 0x01, 0x10, 0x00, 0xF2, 0x07, 0x09, 0x41, 0x4E, 0x01, 0x04, 0x78, 0x56, 0x34, 0x12, 0x01]
|
|
64
|
+
)
|
|
65
|
+
XCTAssertEqual([UInt8](G2AncsRelay.actionPacket(uid: 1, positive: true)).last, 0)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - reassembly
|
|
69
|
+
|
|
70
|
+
func testNotificationCompletesAfterAllEightAttributes() throws {
|
|
71
|
+
let event = try XCTUnwrap(feedNotification(uid: 42, flags: 0x02, subtitle: "Re: lunch"))
|
|
72
|
+
guard case let .notification(n) = event else { return XCTFail("expected a notification, got \(event)") }
|
|
73
|
+
XCTAssertEqual(n.uid, 42)
|
|
74
|
+
XCTAssertEqual(n.notificationId, "ancs-42")
|
|
75
|
+
XCTAssertEqual(n.event, .added)
|
|
76
|
+
XCTAssertTrue(n.flags.contains(.important))
|
|
77
|
+
XCTAssertFalse(n.flags.contains(.preExisting))
|
|
78
|
+
XCTAssertEqual(n.category, 4)
|
|
79
|
+
XCTAssertEqual(n.appIdentifier, "com.apple.MobileSMS")
|
|
80
|
+
XCTAssertEqual(n.title, "Alice")
|
|
81
|
+
XCTAssertEqual(n.subtitle, "Re: lunch")
|
|
82
|
+
XCTAssertEqual(n.message, "hi")
|
|
83
|
+
XCTAssertEqual(n.date, "20260902T183000")
|
|
84
|
+
XCTAssertEqual(n.positiveActionLabel, "Open")
|
|
85
|
+
XCTAssertEqual(n.negativeActionLabel, "Clear")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func testChunkedMessageReassemblesInAnyOrder() throws {
|
|
89
|
+
let message = String(repeating: "abcdefghij", count: 30) // 300 bytes, three firmware chunks
|
|
90
|
+
let bytes = [UInt8](message.utf8)
|
|
91
|
+
XCTAssertNil(relay.ingest(source(uid: 7)))
|
|
92
|
+
for (id, value) in [(0, "com.example"), (1, "t"), (2, ""), (4, "300"), (5, ""), (6, ""), (7, "")] as [(UInt8, String)] {
|
|
93
|
+
XCTAssertNil(relay.ingest(attr(uid: 7, id: id, value)))
|
|
94
|
+
}
|
|
95
|
+
let chunks = [(274, bytes[274...]), (0, bytes[0 ..< 137]), (137, bytes[137 ..< 274])]
|
|
96
|
+
var event: G2AncsRelay.Event?
|
|
97
|
+
for (offset, chunk) in chunks {
|
|
98
|
+
let record = Data([0x41, 0x4E, 1, 2] + le32(7) + [3] + le16(300) + le16(offset) + chunk)
|
|
99
|
+
event = relay.ingest(record)
|
|
100
|
+
}
|
|
101
|
+
guard case let .notification(n) = try XCTUnwrap(event) else { return XCTFail("expected a notification") }
|
|
102
|
+
XCTAssertEqual(n.message, message)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func testAttributesWithoutASourceAreIgnored() {
|
|
106
|
+
XCTAssertNil(relay.ingest(attr(uid: 9, id: 1, "orphan")))
|
|
107
|
+
XCTAssertNotNil(feedNotification(uid: 9))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func testRemovedReportsTheAppLearnedFromTheNotification() throws {
|
|
111
|
+
_ = feedNotification(uid: 5, app: "com.apple.mobilemail")
|
|
112
|
+
XCTAssertEqual(
|
|
113
|
+
relay.ingest(source(uid: 5, event: 2)),
|
|
114
|
+
.removed(uid: 5, appIdentifier: "com.apple.mobilemail")
|
|
115
|
+
)
|
|
116
|
+
// A second removal (or one for an unknown uid) carries no app.
|
|
117
|
+
XCTAssertEqual(relay.ingest(source(uid: 5, event: 2)), .removed(uid: 5, appIdentifier: nil))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func testRemovalDiscardsAPartialReassembly() {
|
|
121
|
+
XCTAssertNil(relay.ingest(source(uid: 3)))
|
|
122
|
+
XCTAssertNil(relay.ingest(attr(uid: 3, id: 0, "com.example")))
|
|
123
|
+
XCTAssertEqual(relay.ingest(source(uid: 3, event: 2)), .removed(uid: 3, appIdentifier: nil))
|
|
124
|
+
XCTAssertNil(relay.ingest(attr(uid: 3, id: 1, "late")))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func testAppNameRecordUpdatesTheDirectory() {
|
|
128
|
+
XCTAssertEqual(
|
|
129
|
+
relay.ingest(app("com.apple.MobileSMS", name: "Messages")),
|
|
130
|
+
.appName(appIdentifier: "com.apple.MobileSMS", displayName: "Messages")
|
|
131
|
+
)
|
|
132
|
+
XCTAssertEqual(relay.appNames["com.apple.MobileSMS"], "Messages")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func testAppNameChunksReassemble() {
|
|
136
|
+
let name = String(repeating: "N", count: 200)
|
|
137
|
+
let bytes = [UInt8](name.utf8)
|
|
138
|
+
XCTAssertNil(relay.ingest(app("com.example", name: String(decoding: bytes[0 ..< 120], as: UTF8.self), total: 200)))
|
|
139
|
+
XCTAssertEqual(
|
|
140
|
+
relay.ingest(app("com.example", name: String(decoding: bytes[120...], as: UTF8.self), total: 200, offset: 120)),
|
|
141
|
+
.appName(appIdentifier: "com.example", displayName: name)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func testStatusDecodes() {
|
|
146
|
+
let record = Data([0x41, 0x4E, 1, 4, 1, 1] + le16(3) + le16(70) + [2])
|
|
147
|
+
XCTAssertEqual(
|
|
148
|
+
relay.ingest(record),
|
|
149
|
+
.status(G2AncsRelay.Status(enabled: true, side: 1, drops: 3, sent: 70, sendErrors: 2))
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func testUnknownVersionKindAndShortRecordsAreIgnored() {
|
|
154
|
+
XCTAssertNil(relay.ingest(Data([0x41, 0x4E, 2, 1, 0, 0, 0, 0, 1, 0, 0, 0])))
|
|
155
|
+
XCTAssertNil(relay.ingest(Data([0x41, 0x4E, 1, 9])))
|
|
156
|
+
XCTAssertNil(relay.ingest(Data([0x41, 0x4E, 1, 1, 0])))
|
|
157
|
+
XCTAssertNil(relay.ingest(Data([0x41, 0x4E, 1, 4, 1])))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func testPruneDropsStaleReassemblies() {
|
|
161
|
+
let start = Date(timeIntervalSince1970: 1000)
|
|
162
|
+
XCTAssertNil(relay.ingest(source(uid: 11), now: start))
|
|
163
|
+
relay.prune(olderThan: 30, now: start.addingTimeInterval(31))
|
|
164
|
+
XCTAssertNil(relay.ingest(attr(uid: 11, id: 0, "com.example")))
|
|
165
|
+
XCTAssertNil(relay.ingest(source(uid: 12), now: start))
|
|
166
|
+
relay.prune(olderThan: 30, now: start.addingTimeInterval(5))
|
|
167
|
+
// Still pending: attributes are accepted and the notification completes.
|
|
168
|
+
for id in UInt8(0) ... 6 { XCTAssertNil(relay.ingest(attr(uid: 12, id: id, "x"))) }
|
|
169
|
+
XCTAssertNotNil(relay.ingest(attr(uid: 12, id: 7, "x")))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - timestamps
|
|
173
|
+
|
|
174
|
+
func testTimestampParsesLocalAncsDateAndFallsBackToNow() {
|
|
175
|
+
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
|
176
|
+
XCTAssertEqual(G2AncsRelay.timestampMs(date: "", now: now), 1_700_000_000_000)
|
|
177
|
+
XCTAssertEqual(G2AncsRelay.timestampMs(date: "garbage", now: now), 1_700_000_000_000)
|
|
178
|
+
var components = DateComponents()
|
|
179
|
+
components.calendar = Calendar(identifier: .gregorian)
|
|
180
|
+
components.timeZone = .current
|
|
181
|
+
components.year = 2026; components.month = 9; components.day = 2
|
|
182
|
+
components.hour = 18; components.minute = 30; components.second = 0
|
|
183
|
+
let expected = Int64(components.date!.timeIntervalSince1970 * 1000)
|
|
184
|
+
XCTAssertEqual(G2AncsRelay.timestampMs(date: "20260902T183000", now: now), expected)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
/// Pins the BLE link control wire format shared with g2flash's patches/ble_link.c.
|
|
6
|
+
final class G2BleLinkTests: XCTestCase {
|
|
7
|
+
private func le16(_ v: Int) -> [UInt8] {
|
|
8
|
+
[UInt8(v & 0xFF), UInt8(v >> 8)]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func testControlPacketFramesField127OnDeviceReceiveInfo() {
|
|
12
|
+
XCTAssertEqual(
|
|
13
|
+
[UInt8](G2BleLink.controlPacket(op: .fast)),
|
|
14
|
+
[0x08, 0x01, 0x10, 0x00, 0xFA, 0x07, 4, 0x42, 0x4C, 1, 1]
|
|
15
|
+
)
|
|
16
|
+
XCTAssertEqual([UInt8](G2BleLink.controlPacket(op: .stock)).suffix(1), [0])
|
|
17
|
+
XCTAssertEqual([UInt8](G2BleLink.controlPacket(op: .query)).suffix(1), [2])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func testDecodesFullStatusWithLivePair() {
|
|
21
|
+
let body = Data([0x42, 0x4C, 1, 1, 1, 0xA3, 0xA3] + le16(6) + le16(6) + le16(0) + le16(12) + le16(0))
|
|
22
|
+
let status = G2BleLink.decodeStatus(body)
|
|
23
|
+
XCTAssertEqual(status, G2BleLink.Status(
|
|
24
|
+
fast: true, side: 1, wanted: .fast, applied: .fast,
|
|
25
|
+
requestedMinMs: 7.5, requestedMaxMs: 7.5, requestedLatency: 0,
|
|
26
|
+
liveIntervalMs: 15, liveLatency: 0
|
|
27
|
+
))
|
|
28
|
+
XCTAssertEqual(status?.summary, "FAST wanted=fast applied=fast requested 7.50-7.50 ms / latency 0 live 15.00 ms / latency 0")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func testDecodesThirteenByteStatusWithoutLivePair() {
|
|
32
|
+
let body = Data([0x42, 0x4C, 1, 0, 2, 0xA4, 0] + le16(36) + le16(72) + le16(4))
|
|
33
|
+
let status = G2BleLink.decodeStatus(body)
|
|
34
|
+
XCTAssertEqual(status?.fast, false)
|
|
35
|
+
XCTAssertEqual(status?.side, 2)
|
|
36
|
+
XCTAssertEqual(status?.wanted, .slow)
|
|
37
|
+
XCTAssertEqual(status?.applied, .pending)
|
|
38
|
+
XCTAssertEqual(status?.requestedMinMs, 45)
|
|
39
|
+
XCTAssertEqual(status?.requestedMaxMs, 90)
|
|
40
|
+
XCTAssertEqual(status?.requestedLatency, 4)
|
|
41
|
+
XCTAssertNil(status?.liveIntervalMs)
|
|
42
|
+
XCTAssertNil(status?.liveLatency)
|
|
43
|
+
XCTAssertEqual(status?.summary, "stock wanted=slow applied=pending requested 45.00-90.00 ms / latency 4")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func testRejectsForeignShortOrVersionedBodies() {
|
|
47
|
+
XCTAssertNil(G2BleLink.decodeStatus(Data([0x41, 0x4E, 1, 1, 1, 0xA3, 0xA3] + le16(6) + le16(6) + le16(0))))
|
|
48
|
+
XCTAssertNil(G2BleLink.decodeStatus(Data([0x42, 0x4C, 2, 1, 1, 0xA3, 0xA3] + le16(6) + le16(6) + le16(0))))
|
|
49
|
+
XCTAssertNil(G2BleLink.decodeStatus(Data([0x42, 0x4C, 1, 1, 1, 0xA3, 0xA3] + le16(6) + le16(6))))
|
|
50
|
+
XCTAssertEqual(G2BleLink.decodeStatus(Data([0x42, 0x4C, 1, 0, 1, 0x99, 0] + le16(0) + le16(0) + le16(0)))?.wanted, .other(0x99))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
import Foundation
|
|
3
3
|
import XCTest
|
|
4
4
|
|
|
5
|
-
/// Pins the stock-vs-CFW identity rules that gate the connect banner. A
|
|
6
|
-
/// regression here re-opens the stuck "// Glassly connected" bug: text drawn
|
|
7
|
-
/// down the stock path before classification, cleared down the CFW path
|
|
8
|
-
/// after it, never leaves the glass.
|
|
9
5
|
final class G2CfwClassificationTests: XCTestCase {
|
|
10
6
|
private func classify(
|
|
11
7
|
_ capability: String?, definitive: Bool = false, current: String? = nil
|
|
@@ -17,62 +13,61 @@ final class G2CfwClassificationTests: XCTestCase {
|
|
|
17
13
|
)
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
func testCfwDetectionDoesNotWaitForADefinitiveResponse() throws {
|
|
31
|
-
// The banner may ride any settings packet; requiring `definitive`
|
|
32
|
-
// would delay classification past the connect banner and re-split
|
|
33
|
-
// the draw/clear paths.
|
|
34
|
-
let result = try XCTUnwrap(classify("EVENCFW/12 img640", definitive: false))
|
|
35
|
-
XCTAssertTrue(result.isCfw)
|
|
16
|
+
func testGlasslyUsesOneContractWithoutRevisionOrTokenNegotiation() throws {
|
|
17
|
+
for banner in ["GLASSLYCFW/24", "GLASSLYCFW/24 shapes36 scene37 anim38", "GLASSLYCFW/25"] {
|
|
18
|
+
let result = try XCTUnwrap(classify(banner))
|
|
19
|
+
XCTAssertEqual(result.family, .glassly)
|
|
20
|
+
XCTAssertTrue(result.isCfw)
|
|
21
|
+
XCTAssertTrue(result.isGlassly)
|
|
22
|
+
XCTAssertEqual(result.displayName, banner == "GLASSLYCFW/25" ? "Glassly CFW v25" : "Glassly CFW v24")
|
|
23
|
+
XCTAssertEqual(result.capabilityString, banner)
|
|
24
|
+
}
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
func
|
|
39
|
-
|
|
40
|
-
XCTAssertEqual(
|
|
41
|
-
XCTAssertEqual(
|
|
27
|
+
func testRevisionIsRetainedIndependentlyOfTheFirmwareFamily() throws {
|
|
28
|
+
XCTAssertEqual(classify("GLASSLYCFW/24 shapes36 scene37 anim38")?.revision, 24)
|
|
29
|
+
XCTAssertEqual(classify("GLASSLYCFW/25")?.revision, 25)
|
|
30
|
+
XCTAssertEqual(classify("EVENCFW/22 img640")?.revision, 22)
|
|
31
|
+
XCTAssertEqual(classify("GLASSLYCFW/24\0\0")?.revision, 24)
|
|
32
|
+
XCTAssertNil(G2CfwClassification.stock.revision)
|
|
33
|
+
for banner in ["GLASSLYCFW/", "GLASSLYCFW/beta", "GLASSLYCFW/0", "GLASSLYCFW/-1"] {
|
|
34
|
+
let result = try XCTUnwrap(classify(banner))
|
|
35
|
+
XCTAssertNil(result.revision)
|
|
36
|
+
XCTAssertTrue(result.isGlassly)
|
|
37
|
+
}
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
func
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
func testEvenCfwIsUpstreamAndNeverGlassly() throws {
|
|
41
|
+
for banner in ["EVENCFW/22 img640 imgz rle font15 ringbat17", "EVENCFW/24 shapes36 scene37 anim38"] {
|
|
42
|
+
let result = try XCTUnwrap(classify(banner))
|
|
43
|
+
XCTAssertEqual(result.family, .upstream)
|
|
44
|
+
XCTAssertTrue(result.isCfw)
|
|
45
|
+
XCTAssertFalse(result.isGlassly)
|
|
46
|
+
XCTAssertEqual(result.displayName, "g2flash CFW")
|
|
47
|
+
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let result = try XCTUnwrap(classify(nil, definitive: true))
|
|
54
|
-
XCTAssertFalse(result.isCfw)
|
|
55
|
-
XCTAssertEqual(result, .stock)
|
|
50
|
+
func testBannerTrimsNulTermination() throws {
|
|
51
|
+
let result = try XCTUnwrap(classify("GLASSLYCFW/24\0\0"))
|
|
52
|
+
XCTAssertEqual(result.capabilityString, "GLASSLYCFW/24")
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
func
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Individual settings packets can legitimately omit field 100 or
|
|
67
|
-
// arrive truncated — only a deviceReceiveRequest response is proof.
|
|
68
|
-
XCTAssertNil(classify(nil, definitive: false))
|
|
69
|
-
XCTAssertNil(classify("garbage", definitive: false))
|
|
55
|
+
func testDefinitiveAbsenceOrUnrecognizedPrefixClassifiesStock() throws {
|
|
56
|
+
for banner: String? in [nil, "something-else", "GLASSLYCFW-invalid", "EVENCFW-invalid"] {
|
|
57
|
+
let result = try XCTUnwrap(classify(banner, definitive: true))
|
|
58
|
+
XCTAssertEqual(result, .stock)
|
|
59
|
+
XCTAssertFalse(result.isCfw)
|
|
60
|
+
XCTAssertFalse(result.isGlassly)
|
|
61
|
+
XCTAssertNil(result.displayName)
|
|
62
|
+
}
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
func
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
func testPartialSettingsPacketsPreserveTheKnownRenderPath() {
|
|
66
|
+
XCTAssertNil(classify(nil))
|
|
67
|
+
XCTAssertNil(classify("garbage"))
|
|
68
|
+
for banner in ["GLASSLYCFW/24", "EVENCFW/22"] {
|
|
69
|
+
XCTAssertNil(classify(nil, definitive: true, current: banner))
|
|
70
|
+
XCTAssertNil(classify("garbage", definitive: true, current: banner))
|
|
71
|
+
}
|
|
77
72
|
}
|
|
78
73
|
}
|
|
@@ -5,7 +5,161 @@ import XCTest
|
|
|
5
5
|
final class G2CfwRendererTests: XCTestCase {
|
|
6
6
|
private static let white: UInt8 = 200
|
|
7
7
|
|
|
8
|
+
func testGlobalDepthMovesFlatScenesWithoutMiniappDepth() async throws {
|
|
9
|
+
let renderer = G2CfwRenderer()
|
|
10
|
+
let content = frame([rect(x: 100, y: 40, w: 100, h: 50, border: 1)])
|
|
11
|
+
// Start flat, split the retained mirror, change sides, then converge again.
|
|
12
|
+
for (index, setting) in [2, 1, 3, 2].enumerated() {
|
|
13
|
+
let b = boxes(height: 4, depth: G2CfwRenderer.depthLevel(forSetting: setting))
|
|
14
|
+
let result = await renderer.encodeVector(
|
|
15
|
+
content, boxes: b, cacheGeneration: 1, invalidate: index == 0, inlineText: true
|
|
16
|
+
)
|
|
17
|
+
let encoded = try XCTUnwrap(result)
|
|
18
|
+
if setting == 2 {
|
|
19
|
+
XCTAssertEqual(encoded.shows.map(\.lenses), [G2CfwTransport.lensBoth])
|
|
20
|
+
XCTAssertEqual(firstRectX(encoded.shows[0].payload), 132)
|
|
21
|
+
} else {
|
|
22
|
+
XCTAssertEqual(encoded.shows.map(\.lenses), [G2CfwTransport.lensLeft, G2CfwTransport.lensRight])
|
|
23
|
+
XCTAssertEqual(firstRectX(encoded.shows[0].payload), setting == 1 ? 140 : 124)
|
|
24
|
+
XCTAssertEqual(firstRectX(encoded.shows[1].payload), setting == 1 ? 124 : 140)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func testElementDepthIsRelativeToEachGlobalLensOrigin() async throws {
|
|
30
|
+
let renderer = G2CfwRenderer()
|
|
31
|
+
let content = SceneElement(
|
|
32
|
+
id: "card", type: "rect", x: 100, y: 40, w: 100, h: 50,
|
|
33
|
+
text: nil, data: nil, border: 1, radius: 0, change: "created", contentHash: "card", depth: 5
|
|
34
|
+
)
|
|
35
|
+
let result = await renderer.encodeVector(
|
|
36
|
+
frame([content]), boxes: boxes(height: 4, depth: G2CfwRenderer.depthLevel(forSetting: 3)),
|
|
37
|
+
cacheGeneration: 1, invalidate: true, inlineText: true
|
|
38
|
+
)
|
|
39
|
+
let encoded = try XCTUnwrap(result)
|
|
40
|
+
XCTAssertEqual(encoded.shows.map(\.lenses), [G2CfwTransport.lensLeft, G2CfwTransport.lensRight])
|
|
41
|
+
XCTAssertEqual(firstRectX(encoded.shows[0].payload), 24 + 100 + 5)
|
|
42
|
+
XCTAssertEqual(firstRectX(encoded.shows[1].payload), 40 + 100 - 5)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func firstRectX(_ payload: Data) -> Int {
|
|
46
|
+
let bytes = [UInt8](payload)
|
|
47
|
+
XCTAssertEqual(bytes[0], 38) // SHOW
|
|
48
|
+
XCTAssertEqual(bytes[7], 1) // one object PUT
|
|
49
|
+
// SHOW header (8), PUT opcode/id/version (9), type/flags/color/width (4).
|
|
50
|
+
return Int(Int16(bitPattern: UInt16(bytes[21]) | UInt16(bytes[22]) << 8))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func testGlobalDepthSettingsMapToAllThreeFirmwareLevels() {
|
|
54
|
+
XCTAssertEqual([1, 2, 3].map { G2CfwRenderer.depthLevel(forSetting: $0) }, [0, 1, 2])
|
|
55
|
+
XCTAssertEqual(G2CfwRenderer.depthLevel(forSetting: Int.min), 0)
|
|
56
|
+
XCTAssertEqual(G2CfwRenderer.depthLevel(forSetting: Int.max), 2)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func testRasterFallbackBakesTextIntoStereoWindows() async throws {
|
|
60
|
+
let renderer = G2CfwRenderer()
|
|
61
|
+
for font in ["builtin", "mono"] {
|
|
62
|
+
let text = SceneElement(
|
|
63
|
+
id: "text", type: "text", x: 10, y: 10, w: 200, h: 60,
|
|
64
|
+
text: "Depth", data: nil, border: 0, radius: 0,
|
|
65
|
+
change: "created", contentHash: font, font: font, fontSize: 20
|
|
66
|
+
)
|
|
67
|
+
for setting in [1, 3] {
|
|
68
|
+
let b = boxes(height: 4, depth: G2CfwRenderer.depthLevel(forSetting: setting))
|
|
69
|
+
let result = await renderer.encodeUpdate(
|
|
70
|
+
frame([text]), boxes: b, previousRaster: nil, forcedRects: [],
|
|
71
|
+
fids: (1 ... G2CfwRenderer.requiredFidCount).map { UInt16($0) },
|
|
72
|
+
textDraws: [], allowRectCopy: true, blankBoxes: nil, omitBuiltinText: false,
|
|
73
|
+
textureGlyphsEnabled: true, textureImagesEnabled: true
|
|
74
|
+
)
|
|
75
|
+
let update = try XCTUnwrap(result)
|
|
76
|
+
XCTAssertTrue(update.raster.contains { $0 > 0 }, "\(font) text must remain visible")
|
|
77
|
+
let payload = try XCTUnwrap(update.payload)
|
|
78
|
+
XCTAssertEqual(payload[0], 0x83, "a stereo bitmap, without lens-equal text/cache commands")
|
|
79
|
+
XCTAssertEqual(Int(payload[1]) * 4, Int(b.left.x))
|
|
80
|
+
XCTAssertEqual(Int(payload[5]) * 4, Int(b.right.x))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func testSustainedHalfBlockFramesStaySmallOnTheObjectCache() async throws {
|
|
86
|
+
let renderer = G2CfwRenderer()
|
|
87
|
+
let b = boxes(height: 4, depth: 1)
|
|
88
|
+
let glyphs: [Character] = [" ", "▀", "▄", "█"]
|
|
89
|
+
var rng: UInt32 = 42
|
|
90
|
+
var uploads = 0
|
|
91
|
+
var largest = 0
|
|
92
|
+
for index in 0 ..< 120 {
|
|
93
|
+
var lines: [String] = []
|
|
94
|
+
for _ in 0 ..< 20 {
|
|
95
|
+
var line = ""
|
|
96
|
+
for _ in 0 ..< 78 {
|
|
97
|
+
rng = rng &* 1_664_525 &+ 1_013_904_223
|
|
98
|
+
line.append(glyphs[Int(rng >> 30)])
|
|
99
|
+
}
|
|
100
|
+
lines.append(line)
|
|
101
|
+
}
|
|
102
|
+
let element = SceneElement(
|
|
103
|
+
id: "badapple", type: "text", x: 0, y: 0, w: 576, h: 288,
|
|
104
|
+
text: lines.joined(separator: "\n"), data: nil, border: 0, radius: 0,
|
|
105
|
+
change: "updated", contentHash: "frame-\(index)", font: "mono", fontSize: 12
|
|
106
|
+
)
|
|
107
|
+
let result = await renderer.encodeVector(
|
|
108
|
+
frame([element]), boxes: b, cacheGeneration: 1, invalidate: index == 0, inlineText: true
|
|
109
|
+
)
|
|
110
|
+
let encoded = try XCTUnwrap(result)
|
|
111
|
+
uploads += encoded.uploads.count
|
|
112
|
+
XCTAssertEqual(encoded.shows.map(\.lenses), [G2CfwTransport.lensBoth], "flat frames are one SHOW for both lenses")
|
|
113
|
+
let payload = try XCTUnwrap(encoded.shows.first?.payload)
|
|
114
|
+
let bytes = payload.count + (index == 0 ? 0 : encoded.uploads.reduce(0) { $0 + $1.count })
|
|
115
|
+
largest = max(largest, bytes)
|
|
116
|
+
XCTAssertLessThan(bytes, 4500, "frame \(index) fell back to image tiles")
|
|
117
|
+
XCTAssertEqual(payload.first, 38)
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
XCTAssertGreaterThan(uploads, 1, "the font goes once, then the changed strings")
|
|
121
|
+
print("120 half-block frames: largest encoded frame = \(largest) bytes, uploads = \(uploads)")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func testElementDepthSplitsTheSceneIntoLensSpecificShows() async throws {
|
|
125
|
+
let renderer = G2CfwRenderer()
|
|
126
|
+
let b = boxes(height: 4, depth: 1)
|
|
127
|
+
let content = rect(x: 10, y: 10, w: 100, h: 50, border: 1)
|
|
128
|
+
let card = SceneElement(
|
|
129
|
+
id: "card", type: "rect", x: 200, y: 40, w: 200, h: 120, text: nil, data: nil, border: 2, radius: 8,
|
|
130
|
+
change: "created", contentHash: "card", fill: true, color: 3, depth: 8
|
|
131
|
+
)
|
|
132
|
+
let flatResult = await renderer.encodeVector(frame([content]), boxes: b, cacheGeneration: 1, invalidate: true, inlineText: true)
|
|
133
|
+
let flat = try XCTUnwrap(flatResult)
|
|
134
|
+
XCTAssertEqual(flat.shows.map(\.lenses), [G2CfwTransport.lensBoth], "flat frames are one SHOW for both lenses")
|
|
135
|
+
XCTAssertTrue(flat.changed)
|
|
136
|
+
|
|
137
|
+
let stereoResult = await renderer.encodeVector(frame([content, card]), boxes: b, cacheGeneration: 1, invalidate: false, inlineText: true)
|
|
138
|
+
let stereo = try XCTUnwrap(stereoResult)
|
|
139
|
+
XCTAssertEqual(stereo.shows.map(\.lenses), [G2CfwTransport.lensLeft, G2CfwTransport.lensRight])
|
|
140
|
+
XCTAssertNotEqual(stereo.shows[0].payload, stereo.shows[1].payload)
|
|
141
|
+
XCTAssertEqual(stereo.puts, 4, "the card's fill and frame are defined once per lens; the content is a hit on both")
|
|
142
|
+
XCTAssertEqual(stereo.hits, 2)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
let sameResult = await renderer.encodeVector(frame([content, card]), boxes: b, cacheGeneration: 1, invalidate: false, inlineText: true)
|
|
146
|
+
let same = try XCTUnwrap(sameResult)
|
|
147
|
+
XCTAssertFalse(same.changed, "an identical frame changes nothing on either lens")
|
|
148
|
+
XCTAssertEqual(same.shows.count, 2, "but a caller that always sends still gets one SHOW per lens")
|
|
149
|
+
|
|
150
|
+
let backResult = await renderer.encodeVector(frame([content]), boxes: b, cacheGeneration: 1, invalidate: false, inlineText: true)
|
|
151
|
+
let back = try XCTUnwrap(backResult)
|
|
152
|
+
XCTAssertEqual(back.shows.map(\.lenses), [G2CfwTransport.lensBoth], "with the card gone the lenses agree again")
|
|
153
|
+
XCTAssertTrue(back.changed)
|
|
154
|
+
|
|
155
|
+
let resetResult = await renderer.encodeVector(frame([content]), boxes: b, cacheGeneration: 2, invalidate: false, inlineText: true)
|
|
156
|
+
let reset = try XCTUnwrap(resetResult)
|
|
157
|
+
XCTAssertEqual(reset.shows.map(\.lenses), [G2CfwTransport.lensBoth])
|
|
158
|
+
XCTAssertEqual(reset.puts, 1, "a new generation redefines from the shared mirror")
|
|
159
|
+
}
|
|
160
|
+
|
|
8
161
|
private func rect(x: Int32, y: Int32, w: Int32, h: Int32, border: Int32) -> SceneElement {
|
|
162
|
+
|
|
9
163
|
SceneElement(
|
|
10
164
|
id: "r", type: "rect", x: x, y: y, w: w, h: h, text: nil, data: nil,
|
|
11
165
|
border: border, radius: 0, change: "created", contentHash: ""
|
|
@@ -108,7 +262,8 @@ final class G2CfwRendererTests: XCTestCase {
|
|
|
108
262
|
let pixels = Data(count: 576 * 288)
|
|
109
263
|
let payload = try XCTUnwrap(G2CfwRenderer.stereoFrame(pixels: pixels, boxes: b, fid: 0x1234))
|
|
110
264
|
XCTAssertEqual(Array(payload.prefix(11)), [0x83, 6, 64, 144, 144, 10, 64, 144, 144, 0x34, 0x12])
|
|
111
|
-
|
|
265
|
+
// Plain RLE follows the header (the transport compresses): 576*288 black pixels.
|
|
266
|
+
XCTAssertEqual(Array(payload[11 ..< 15]), [0x00, 0x00, 0xFF, 0xFF])
|
|
112
267
|
}
|
|
113
268
|
|
|
114
269
|
func testSolidStereoFrameMatchesPixelFrameHeader() throws {
|
|
@@ -143,7 +298,8 @@ final class G2CfwRendererTests: XCTestCase {
|
|
|
143
298
|
func testBlackKeyframeIsMode6() throws {
|
|
144
299
|
let payload = try XCTUnwrap(G2CfwRenderer.blackPayload())
|
|
145
300
|
XCTAssertEqual(payload.first, 6)
|
|
146
|
-
|
|
301
|
+
// 307,200 black pixels as RLE: four 65,535 runs and a 45,060 remainder.
|
|
302
|
+
XCTAssertEqual(Array(payload.dropFirst()), [0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x04, 0xB0])
|
|
147
303
|
}
|
|
148
304
|
|
|
149
305
|
func testSeededFramePayloadCompositesContentAtLensEqualWindow() throws {
|
|
@@ -242,8 +398,10 @@ final class G2CfwRendererTests: XCTestCase {
|
|
|
242
398
|
func testVerticalScrollBecomesRectCopyPlusResidue() {
|
|
243
399
|
let width = 64
|
|
244
400
|
let height = 96
|
|
245
|
-
|
|
246
|
-
func content(_ row: Int) -> UInt8 {
|
|
401
|
+
/// Distinct rows of "content" scrolled up by 8, with a fresh bottom row band.
|
|
402
|
+
func content(_ row: Int) -> UInt8 {
|
|
403
|
+
UInt8((row * 7) % 251)
|
|
404
|
+
}
|
|
247
405
|
let prev = raster(width: width, height: height) { px in
|
|
248
406
|
for y in 0 ..< height {
|
|
249
407
|
for x in 0 ..< width {
|
|
@@ -305,7 +463,8 @@ final class G2CfwRendererTests: XCTestCase {
|
|
|
305
463
|
[0x83, UInt8((24 + 100) / 4), UInt8((128 + 50) / 2), 8, 8,
|
|
306
464
|
UInt8((40 + 100) / 4), UInt8((128 + 50) / 2), 8, 8, 5, 0]
|
|
307
465
|
)
|
|
308
|
-
|
|
466
|
+
// Plain RLE of the 32x16 box follows (the transport compresses).
|
|
467
|
+
XCTAssertEqual(Array(payload[11...]), Array(G2CfwRenderer.rleEncode(G2CfwRenderer.pack4Bpp(Data(count: 32 * 16)))))
|
|
309
468
|
}
|
|
310
469
|
|
|
311
470
|
func testBoxDeltaFrameRejectsUnalignedRects() {
|