@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class G2ImageAckTests: XCTestCase {
|
|
6
|
+
func testOmittedZeroSessionAndFragmentDecodeAsDefaults() throws {
|
|
7
|
+
let ack = try XCTUnwrap(ImgAckBox.decodeResult(Data([0x40, 0x04])))
|
|
8
|
+
XCTAssertEqual(ack.session, 0)
|
|
9
|
+
XCTAssertEqual(ack.fragment, 0)
|
|
10
|
+
XCTAssertTrue(ack.success)
|
|
11
|
+
let explicit = try XCTUnwrap(ImgAckBox.decodeResult(Data([0x18, 0, 0x30, 0, 0x40, 4])))
|
|
12
|
+
XCTAssertEqual(explicit.session, ack.session)
|
|
13
|
+
XCTAssertEqual(explicit.fragment, ack.fragment)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func testNonzeroSessionAndNackRemainCorrelated() throws {
|
|
17
|
+
let ack = try XCTUnwrap(ImgAckBox.decodeResult(Data([0x18, 0xFF, 1, 0x30, 2, 0x40, 5])))
|
|
18
|
+
XCTAssertEqual(ack.session, 255)
|
|
19
|
+
XCTAssertEqual(ack.fragment, 2)
|
|
20
|
+
XCTAssertFalse(ack.success)
|
|
21
|
+
XCTAssertNil(ImgAckBox.decodeResult(Data()))
|
|
22
|
+
XCTAssertNil(ImgAckBox.decodeResult(Data([0x18, 1])))
|
|
23
|
+
XCTAssertNil(ImgAckBox.decodeResult(Data([0x1A, 0, 0x40, 4])))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@MainActor
|
|
27
|
+
func testWireAckAtSessionWrapResumesWithoutTimingOut() async throws {
|
|
28
|
+
let box = ImgAckBox()
|
|
29
|
+
for session in [254, 255, 0, 1] {
|
|
30
|
+
// Protobuf encoders can omit default-valued scalar fields.
|
|
31
|
+
var wire: [UInt8] = [0x40, 4]
|
|
32
|
+
if session > 0 {
|
|
33
|
+
wire += session < 128 ? [0x18, UInt8(session)] : [0x18, UInt8(session & 0x7F) | 0x80, 1]
|
|
34
|
+
}
|
|
35
|
+
let ack = try XCTUnwrap(ImgAckBox.decodeResult(Data(wire)))
|
|
36
|
+
let ok = await box.wait(session: session, fragment: 0, timeoutNs: 100_000_000) {
|
|
37
|
+
XCTAssertTrue(box.resolve(session: ack.session, fragment: ack.fragment, success: ack.success))
|
|
38
|
+
}
|
|
39
|
+
XCTAssertTrue(ok)
|
|
40
|
+
XCTAssertEqual(box.outcome()?.reason, "ack")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@MainActor
|
|
45
|
+
func testImmediateAckAndDuplicateDuringSustainedTransfers() async {
|
|
46
|
+
let box = ImgAckBox()
|
|
47
|
+
// Cross the session wrap repeatedly. No timer from an earlier arming
|
|
48
|
+
// may complete a newer transfer, including one with the same wire key.
|
|
49
|
+
for index in 0 ..< 1024 {
|
|
50
|
+
let session = index % 256
|
|
51
|
+
let ok = await box.wait(session: session, fragment: 0, timeoutNs: 1_000_000_000) {
|
|
52
|
+
XCTAssertFalse(box.resolve(session: session, fragment: 1, success: true))
|
|
53
|
+
XCTAssertTrue(box.resolve(session: session, fragment: 0, success: true))
|
|
54
|
+
XCTAssertFalse(box.resolve(session: session, fragment: 0, success: true))
|
|
55
|
+
}
|
|
56
|
+
XCTAssertTrue(ok)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@MainActor
|
|
61
|
+
func testCancelledBeforeArmingDoesNotSend() async {
|
|
62
|
+
let box = ImgAckBox()
|
|
63
|
+
let task = Task { @MainActor in
|
|
64
|
+
await box.wait(session: 1, fragment: 0, timeoutNs: 1_000_000_000) {
|
|
65
|
+
XCTFail("cancelled transfer sent a packet")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
task.cancel()
|
|
69
|
+
let ok = await task.value
|
|
70
|
+
XCTAssertFalse(ok)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@MainActor
|
|
74
|
+
func testCancellationReleasesAnArmedWaiter() async {
|
|
75
|
+
let box = ImgAckBox()
|
|
76
|
+
let sent = expectation(description: "sent")
|
|
77
|
+
let task = Task { @MainActor in
|
|
78
|
+
await box.wait(session: 1, fragment: 0, timeoutNs: 60_000_000_000) {
|
|
79
|
+
sent.fulfill()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await fulfillment(of: [sent], timeout: 1)
|
|
83
|
+
task.cancel()
|
|
84
|
+
let ok = await task.value
|
|
85
|
+
XCTAssertFalse(ok)
|
|
86
|
+
XCTAssertFalse(box.resolve(session: 1, fragment: 0, success: true))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func testOldTimeoutCannotResolveReusedSession() async {
|
|
90
|
+
let box = ImgAckBox()
|
|
91
|
+
var oldToken: UInt64 = 0
|
|
92
|
+
let first = await withCheckedContinuation { cont in
|
|
93
|
+
oldToken = box.arm(session: 1, fragment: 0, cont: cont)
|
|
94
|
+
box.resolve(session: 1, fragment: 0, success: true)
|
|
95
|
+
}
|
|
96
|
+
XCTAssertTrue(first)
|
|
97
|
+
let second = await withCheckedContinuation { cont in
|
|
98
|
+
_ = box.arm(session: 1, fragment: 0, cont: cont)
|
|
99
|
+
box.timeout(token: oldToken)
|
|
100
|
+
XCTAssertTrue(box.resolve(session: 1, fragment: 0, success: true))
|
|
101
|
+
}
|
|
102
|
+
XCTAssertTrue(second)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
/// Pins the keyboard bridge wire format and HID decoding shared with g2flash's
|
|
6
|
+
/// patches/keyboard_core.h and demos/keyboard.test.ts (same vectors).
|
|
7
|
+
final class G2KeyboardTests: XCTestCase {
|
|
8
|
+
private static let boot: [UInt8] = [
|
|
9
|
+
0x05, 1, 0x09, 6, 0xA1, 1, 0x05, 7, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0, 0x25, 1, 0x75, 1, 0x95, 8, 0x81, 2,
|
|
10
|
+
0x75, 8, 0x95, 1, 0x81, 1, 0x95, 6, 0x75, 8, 0x15, 0, 0x25, 0x65, 0x05, 7, 0x19, 0, 0x29, 0x65, 0x81, 0, 0xC0,
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
private static let numbered: [UInt8] = Array(boot[0 ..< 6]) + [0x85, 1] + Array(boot[6 ..< (boot.count - 1)]) + [
|
|
14
|
+
0x85, 2, 0x05, 0x0C, 0x15, 0, 0x25, 1, 0x75, 1, 0x95, 3, 0x09, 0xE9, 0x09, 0xEA, 0x09, 0xE2, 0x81, 2,
|
|
15
|
+
0x75, 5, 0x95, 1, 0x81, 1, 0xC0,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
private func le16(_ v: Int) -> [UInt8] {
|
|
19
|
+
[UInt8(v & 0xFF), UInt8((v >> 8) & 0xFF)]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private func le32(_ v: UInt32) -> [UInt8] {
|
|
23
|
+
[UInt8(v & 0xFF), UInt8((v >> 8) & 0xFF), UInt8((v >> 16) & 0xFF), UInt8(v >> 24)]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private func frame(
|
|
27
|
+
_ kind: UInt8, _ seq: UInt32, _ epoch: Int, _ request: Int, _ code: Int, _ handle: Int,
|
|
28
|
+
_ data: [UInt8], offset: Int = 0, total: Int? = nil
|
|
29
|
+
) -> Data {
|
|
30
|
+
var p: [UInt8] = [75, 66, 1, kind] + le32(seq) + le16(epoch) + le16(request) + le16(code) + le16(handle)
|
|
31
|
+
p += le16(total ?? data.count) + le16(offset) + le32(1234) + data
|
|
32
|
+
return Data(p)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private func usages(_ keys: [G2KeyboardHid.Usage]) -> [[Int]] {
|
|
36
|
+
keys.map { [$0.page, $0.usage] }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private func events(_ keys: [G2KeyboardHid.KeyEvent]) -> [[Int]] {
|
|
40
|
+
keys.map { [$0.page, $0.usage, $0.down ? 1 : 0] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - HID decoding
|
|
44
|
+
|
|
45
|
+
func testBootArrayModifiersReleaseRepeatsAndRollover() throws {
|
|
46
|
+
let r = try XCTUnwrap(G2KeyboardHid.parseReportMap(Self.boot)[0])
|
|
47
|
+
var keys = G2KeyboardHid.HeldKeys()
|
|
48
|
+
XCTAssertEqual(try events(keys.update(handle: 12, report: r, data: [2, 0, 4, 0, 0, 0, 0, 0])), [[7, 0xE1, 1], [7, 4, 1]])
|
|
49
|
+
XCTAssertEqual(try keys.update(handle: 12, report: r, data: [2, 0, 4, 0, 0, 0, 0, 0]), [])
|
|
50
|
+
XCTAssertThrowsError(try keys.update(handle: 12, report: r, data: [0, 0, 1, 1, 1, 1, 1, 1])) { error in
|
|
51
|
+
XCTAssertTrue("\(error)".contains("rollover"))
|
|
52
|
+
}
|
|
53
|
+
XCTAssertEqual(keys.reset().map(\.down), [false, false])
|
|
54
|
+
XCTAssertThrowsError(try G2KeyboardHid.reportKeys(r, [0])) { XCTAssertTrue("\($0)".contains("length")) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func testHogpReportIdsAreOutOfBandWithConsumerKeys() throws {
|
|
58
|
+
let maps = try G2KeyboardHid.parseReportMap(Self.numbered)
|
|
59
|
+
XCTAssertEqual(try usages(G2KeyboardHid.reportKeys(XCTUnwrap(maps[1]), [0, 0, 5, 0, 0, 0, 0, 0])), [[7, 5]])
|
|
60
|
+
XCTAssertEqual(try usages(G2KeyboardHid.reportKeys(XCTUnwrap(maps[2]), [5])), [[12, 0xE9], [12, 0xE2]])
|
|
61
|
+
XCTAssertThrowsError(try G2KeyboardHid.reportKeys(XCTUnwrap(maps[2]), [2, 5]))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func testNkroBitmapWithUnalignedFieldsAndReportUnion() throws {
|
|
65
|
+
let map: [UInt8] = [5, 7, 0x19, 4, 0x29, 19, 0x15, 0, 0x25, 1, 0x75, 1, 0x95, 16, 0x81, 2]
|
|
66
|
+
let r = try XCTUnwrap(G2KeyboardHid.parseReportMap(map)[0])
|
|
67
|
+
XCTAssertEqual(try usages(G2KeyboardHid.reportKeys(r, [0x81, 0x80])), [[7, 4], [7, 11], [7, 19]])
|
|
68
|
+
var keys = G2KeyboardHid.HeldKeys()
|
|
69
|
+
XCTAssertEqual(try keys.update(handle: 10, report: r, data: [1, 0]).count, 1)
|
|
70
|
+
XCTAssertEqual(try keys.update(handle: 11, report: r, data: [1, 0]), [])
|
|
71
|
+
XCTAssertEqual(try keys.update(handle: 10, report: r, data: [0, 0]), [])
|
|
72
|
+
XCTAssertEqual(try events(keys.update(handle: 11, report: r, data: [0, 0])), [[7, 4, 0]])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func testArrayValuesIndexUsagesRelativeToLogicalMinimum() throws {
|
|
76
|
+
let r = try XCTUnwrap(G2KeyboardHid.parseReportMap([5, 7, 0x19, 4, 0x29, 6, 0x15, 1, 0x25, 3, 0x75, 8, 0x95, 1, 0x81, 0])[0])
|
|
77
|
+
XCTAssertEqual(try usages(G2KeyboardHid.reportKeys(r, [2])), [[7, 5]])
|
|
78
|
+
XCTAssertEqual(try G2KeyboardHid.reportKeys(r, [0]), [])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func testDescriptorBoundsAndMalformedItems() {
|
|
82
|
+
let malformed: [[UInt8]] = [
|
|
83
|
+
[], [UInt8](repeating: 0, count: 513), [0xFE], [0x75], [0xB4], [0xC0], [0x85, 0],
|
|
84
|
+
[0x75, 33, 0x95, 1, 0x81, 2], [0x75, 32, 0x96, 0, 2, 0x81, 2], [0xA1, 1],
|
|
85
|
+
]
|
|
86
|
+
for data in malformed {
|
|
87
|
+
XCTAssertThrowsError(try G2KeyboardHid.parseReportMap(data))
|
|
88
|
+
}
|
|
89
|
+
// Deterministic malformed corpus; parsing must be bounded and never hang.
|
|
90
|
+
var seed: UInt32 = 1
|
|
91
|
+
for i in 0 ..< 1000 {
|
|
92
|
+
let data = (0 ..< (i % 128 + 1)).map { _ -> UInt8 in
|
|
93
|
+
seed = seed &* 1_664_525 &+ 1_013_904_223
|
|
94
|
+
return UInt8(seed >> 24)
|
|
95
|
+
}
|
|
96
|
+
_ = try? G2KeyboardHid.parseReportMap(data)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// MARK: - Relay protocol
|
|
101
|
+
|
|
102
|
+
func testShortUuidsEncodedWithTheBluetoothBase() throws {
|
|
103
|
+
let uuid = try G2KeyboardClient.AttributeUuid([0xFB, 0x34, 0x9B, 0x5F, 0x80, 0, 0, 0x80, 0, 0x10, 0, 0, 0x12, 0x18, 0, 0])
|
|
104
|
+
XCTAssertEqual(uuid, .short(0x1812))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func testWireControlHasTheDedicatedFieldAndEpoch() {
|
|
108
|
+
let packet = [UInt8](G2KeyboardProtocol.controlPacket(op: .connect, request: 0x1234, epoch: 0x5678, body: [1, 2, 3, 4, 5, 6, 7], magic: 5))
|
|
109
|
+
XCTAssertEqual(packet, [8, 2, 16, 5, 0x22, 2, 8, 1, 0x92, 8, 15, 75, 66, 1, 4, 0x34, 0x12, 0x78, 0x56, 1, 2, 3, 4, 5, 6, 7])
|
|
110
|
+
// Magic ids above 127 take two varint bytes.
|
|
111
|
+
XCTAssertEqual(Array([UInt8](G2KeyboardProtocol.controlPacket(op: .query, request: 1, epoch: 0, magic: 200))[0 ..< 5]), [8, 2, 16, 0xC8, 0x01])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func testFragmentsDuplicatesGapsEpochsAndSequenceWrap() {
|
|
115
|
+
var stream = G2KeyboardProtocol.RecordAssembler()
|
|
116
|
+
let data = (0 ..< 200).map { UInt8($0) }
|
|
117
|
+
XCTAssertNil(stream.push(frame(3, 0xFFFF_FFFF, 1, 0, 13, 33, Array(data[0 ..< 96]), offset: 0, total: 200)))
|
|
118
|
+
XCTAssertNil(stream.push(frame(3, 0xFFFF_FFFF, 1, 0, 13, 33, Array(data[96 ..< 192]), offset: 96, total: 200)))
|
|
119
|
+
XCTAssertEqual(stream.push(frame(3, 0xFFFF_FFFF, 1, 0, 13, 33, Array(data[192...]), offset: 192, total: 200))?.data, data)
|
|
120
|
+
let status = frame(1, 0, 1, 0, 0, 0, [UInt8](repeating: 0, count: 10))
|
|
121
|
+
XCTAssertEqual(stream.push(status)?.sequence, 0)
|
|
122
|
+
XCTAssertNil(stream.push(status))
|
|
123
|
+
XCTAssertEqual(stream.gaps, 1)
|
|
124
|
+
_ = stream.push(frame(1, 2, 1, 0, 0, 0, [UInt8](repeating: 0, count: 10)))
|
|
125
|
+
XCTAssertEqual(stream.gaps, 2)
|
|
126
|
+
XCTAssertNil(stream.push(status))
|
|
127
|
+
XCTAssertEqual(stream.gaps, 2)
|
|
128
|
+
_ = stream.push(frame(1, 3, 2, 0, 0, 0, [UInt8](repeating: 0, count: 10)))
|
|
129
|
+
XCTAssertEqual(stream.gaps, 3)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func testMismatchedFragmentsNeverEmitPartialInput() {
|
|
133
|
+
var stream = G2KeyboardProtocol.RecordAssembler()
|
|
134
|
+
_ = stream.push(frame(3, 1, 1, 0, 13, 10, [UInt8](repeating: 0, count: 96), offset: 0, total: 100))
|
|
135
|
+
XCTAssertNil(stream.push(frame(3, 1, 1, 0, 13, 11, [UInt8](repeating: 0, count: 4), offset: 96, total: 100)))
|
|
136
|
+
XCTAssertEqual(stream.gaps, 2)
|
|
137
|
+
XCTAssertNil(stream.push(frame(3, 1, 1, 0, 13, 10, [UInt8](repeating: 0, count: 4), offset: 96, total: 100)))
|
|
138
|
+
XCTAssertNil(stream.push(Data(repeating: 0, count: 513)))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func testAdvertParsingNamesHidDevicesAndReversesAddresses() throws {
|
|
142
|
+
let body: [UInt8] = [1, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0xC4, 0, 12, 3, 3, 0x12, 0x18, 5, 9, 0x4B, 0x65, 0x79, 0x73]
|
|
143
|
+
let advert = try XCTUnwrap(G2KeyboardProtocol.Advert.parse(body))
|
|
144
|
+
XCTAssertEqual(advert.addressText, "aa:bb:cc:dd:ee:ff")
|
|
145
|
+
XCTAssertEqual(advert.rssi, -60)
|
|
146
|
+
XCTAssertTrue(advert.hid)
|
|
147
|
+
XCTAssertEqual(advert.name, "Keys")
|
|
148
|
+
XCTAssertEqual(G2KeyboardProtocol.address(fromText: advert.addressText), advert.address)
|
|
149
|
+
XCTAssertNil(G2KeyboardProtocol.address(fromText: "aa:bb"))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func testAdvertAppearanceMarksHidAndScanResponseMerges() throws {
|
|
153
|
+
// ADV_IND with only a keyboard appearance (0x03C1), no 0x1812 service list.
|
|
154
|
+
let adv: [UInt8] = [1, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0xC4, 0, 4, 3, 0x19, 0xC1, 0x03]
|
|
155
|
+
let first = try XCTUnwrap(G2KeyboardProtocol.Advert.parse(adv))
|
|
156
|
+
XCTAssertTrue(first.hid)
|
|
157
|
+
XCTAssertEqual(first.name, "")
|
|
158
|
+
// SCAN_RSP for the same address carries the name and a better RSSI.
|
|
159
|
+
let rsp: [UInt8] = [1, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0xCE, 4, 6, 5, 9, 0x4B, 0x65, 0x79, 0x73]
|
|
160
|
+
let second = try XCTUnwrap(G2KeyboardProtocol.Advert.parse(rsp))
|
|
161
|
+
XCTAssertFalse(second.hid)
|
|
162
|
+
let merged = first.merging(second)
|
|
163
|
+
XCTAssertEqual(merged.name, "Keys")
|
|
164
|
+
XCTAssertTrue(merged.hid)
|
|
165
|
+
XCTAssertEqual(merged.rssi, -50)
|
|
166
|
+
XCTAssertEqual(merged.address, first.address)
|
|
167
|
+
// A non-HID appearance (generic phone) does not qualify.
|
|
168
|
+
let phone: [UInt8] = [1, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0xC4, 0, 4, 3, 0x19, 0x40, 0x00]
|
|
169
|
+
XCTAssertFalse(try XCTUnwrap(G2KeyboardProtocol.Advert.parse(phone)).hid)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - Simulated HOGP keyboard
|
|
173
|
+
|
|
174
|
+
@MainActor
|
|
175
|
+
func testFullSimulatedHogpDiscoveryTypingAndLossRecovery() async throws {
|
|
176
|
+
struct Attr { let uuid: Int; let value: [UInt8] }
|
|
177
|
+
var attrs: [Int: Attr] = [:]
|
|
178
|
+
func add(_ h: Int, _ uuid: Int, _ bytes: [UInt8]) {
|
|
179
|
+
attrs[h] = Attr(uuid: uuid, value: bytes)
|
|
180
|
+
}
|
|
181
|
+
let mockMap = Array(Self.numbered[0 ..< (Self.numbered.count - 1)]) + [0x85, 3, 0x05, 6, 0x09, 0x20, 0x15, 0, 0x25, 100, 0x75, 8, 0x95, 1, 0x81, 2, 0xC0]
|
|
182
|
+
add(1, 0x2800, [1, 0x18]); add(2, 0x2803, [0x20, 3, 0, 5, 0x2A]); add(3, 0x2A05, []); add(4, 0x2902, [0, 0])
|
|
183
|
+
add(20, 0x2800, [0x12, 0x18])
|
|
184
|
+
add(21, 0x2803, [2, 22, 0, 0x4B, 0x2A]); add(22, 0x2A4B, mockMap); add(23, 0x2907, [0x19, 0x2A])
|
|
185
|
+
add(24, 0x2803, [6, 25, 0, 0x4E, 0x2A]); add(25, 0x2A4E, [1])
|
|
186
|
+
add(26, 0x2803, [0x12, 27, 0, 0x4D, 0x2A]); add(27, 0x2A4D, []); add(28, 0x2902, [0, 0]); add(29, 0x2908, [1, 1])
|
|
187
|
+
add(30, 0x2803, [0x12, 31, 0, 0x4D, 0x2A]); add(31, 0x2A4D, []); add(32, 0x2902, [0, 0]); add(33, 0x2908, [2, 1])
|
|
188
|
+
add(40, 0x2800, [0x0F, 0x18]); add(41, 0x2803, [0x12, 42, 0, 0x19, 0x2A]); add(42, 0x2A19, [100]); add(43, 0x2902, [0, 0]); add(44, 0x2908, [3, 1])
|
|
189
|
+
|
|
190
|
+
var seq: UInt32 = 0
|
|
191
|
+
var epoch = 0
|
|
192
|
+
var writes: [Int] = []
|
|
193
|
+
var watches: [Int] = []
|
|
194
|
+
var typed: [[Int]] = []
|
|
195
|
+
var client: G2KeyboardClient!
|
|
196
|
+
func emit(_ kind: UInt8, _ request: Int, _ code: Int, _ handle: Int, _ data: [UInt8]) {
|
|
197
|
+
seq += 1
|
|
198
|
+
client.accept(frame(kind, seq, epoch, request, code, handle, data))
|
|
199
|
+
}
|
|
200
|
+
let sorted = { attrs.keys.sorted() }
|
|
201
|
+
client = G2KeyboardClient(send: { pb, _ in
|
|
202
|
+
let p = [UInt8](pb)
|
|
203
|
+
// Field 130 (tag 0x92 0x08) follows a magic varint of one or two bytes.
|
|
204
|
+
let tag = p.indices.first { p[$0] == 0x92 && p[$0 + 1] == 0x08 }!
|
|
205
|
+
let body = Array(p[(tag + 3)...])
|
|
206
|
+
let op = Int(body[3]), req = Int(G2KeyboardProtocol.u16(body, 4))
|
|
207
|
+
let h = body.count >= 10 ? Int(G2KeyboardProtocol.u16(body, 8)) : 0
|
|
208
|
+
switch op {
|
|
209
|
+
case 1: emit(1, req, 0, 0, [1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
210
|
+
case 4:
|
|
211
|
+
epoch += 1
|
|
212
|
+
emit(1, req, 0, 0, [1, 3, 1, 1, 0, 0, 0, 0, 0, 0])
|
|
213
|
+
case 9:
|
|
214
|
+
let batch = sorted().filter { $0 >= h }.prefix(4)
|
|
215
|
+
var payload: [UInt8] = []
|
|
216
|
+
if !batch.isEmpty {
|
|
217
|
+
payload = [1] + batch.flatMap { handle in self.le16(handle) + self.le16(attrs[handle]!.uuid) }
|
|
218
|
+
}
|
|
219
|
+
emit(2, req, batch.isEmpty ? (10 << 8) | 2 : 2, h, payload)
|
|
220
|
+
case 10:
|
|
221
|
+
let data = attrs[h]!.value
|
|
222
|
+
let offset = Int(G2KeyboardProtocol.u16(body, 10))
|
|
223
|
+
let status = offset >= data.count ? (7 << 8) | 6 : (offset > 0 ? 6 : 5)
|
|
224
|
+
emit(2, req, status, h, Array(data[min(offset, data.count) ..< min(offset + 20, data.count)]))
|
|
225
|
+
case 11:
|
|
226
|
+
writes.append(h)
|
|
227
|
+
emit(2, req, body[10] != 0 ? 10 : 9, h, [])
|
|
228
|
+
case 12:
|
|
229
|
+
let count = Int(body[8])
|
|
230
|
+
watches = (0 ..< count).map { Int(G2KeyboardProtocol.u16(body, 9 + 2 * $0)) }
|
|
231
|
+
emit(1, req, 0, 0, [1, 3, 1, 1, 0, 0, 0, 0, 0, 0])
|
|
232
|
+
default:
|
|
233
|
+
XCTFail("Unexpected op \(op)")
|
|
234
|
+
}
|
|
235
|
+
}, onEvent: { event in
|
|
236
|
+
if case let .keys(events) = event {
|
|
237
|
+
typed += events.map { [$0.usage, $0.down ? 1 : 0] }
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
try await client.enable()
|
|
242
|
+
try await client.connect(addressType: 1, address: [0, 0, 0, 0, 0, 0])
|
|
243
|
+
let config = try await client.configure()
|
|
244
|
+
XCTAssertEqual(watches, [27, 31, 42, 3])
|
|
245
|
+
XCTAssertEqual(writes, [25, 4, 28, 32, 43])
|
|
246
|
+
XCTAssertEqual(config.reports.map(\.id), [1, 2, 3])
|
|
247
|
+
|
|
248
|
+
emit(3, 0, 13, 27, [0, 0, 4, 0, 0, 0, 0, 0])
|
|
249
|
+
emit(3, 0, 13, 31, [1])
|
|
250
|
+
XCTAssertEqual(typed, [[4, 1], [0xE9, 1]])
|
|
251
|
+
seq += 1 // release lost in the firmware queue or phone notification path
|
|
252
|
+
emit(1, 0, 0, 0, [1, 3, 1, 1, 0, 0, 0, 0, 0, 0])
|
|
253
|
+
XCTAssertEqual(typed.suffix(2).map { $0[1] }, [0, 0])
|
|
254
|
+
|
|
255
|
+
let restored = G2KeyboardClient(send: { _, _ in }, onEvent: { event in
|
|
256
|
+
if case let .keys(events) = event {
|
|
257
|
+
typed += events.map { [$0.usage, $0.down ? 1 : 0] }
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
try restored.restore(config)
|
|
261
|
+
seq += 1
|
|
262
|
+
restored.accept(frame(3, seq, epoch, 0, 13, 27, [0, 0, 5, 0, 0, 0, 0, 0]))
|
|
263
|
+
XCTAssertEqual(typed.last, [5, 1])
|
|
264
|
+
restored.lostPhoneLink()
|
|
265
|
+
XCTAssertEqual(typed.last, [5, 0])
|
|
266
|
+
|
|
267
|
+
emit(3, 0, 14, 3, [1, 0, 255, 255]) // Service Changed invalidates the mapping
|
|
268
|
+
let count = typed.count
|
|
269
|
+
emit(3, 0, 13, 27, [0, 0, 6, 0, 0, 0, 0, 0])
|
|
270
|
+
XCTAssertEqual(typed.count, count)
|
|
271
|
+
client.close()
|
|
272
|
+
restored.close()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -22,7 +22,7 @@ final class G2NotifyFileTests: XCTestCase {
|
|
|
22
22
|
XCTAssertEqual(G2NotifyFile.crc32c(content), 0xE96D_1DA8)
|
|
23
23
|
|
|
24
24
|
let header = G2NotifyFile.fileCheckHeader(
|
|
25
|
-
filename: G2NotifyFile.whitelistFilename, content: content
|
|
25
|
+
filename: G2NotifyFile.whitelistFilename, content: content, fileType: .whitelist
|
|
26
26
|
)
|
|
27
27
|
XCTAssertEqual(header.count, 93)
|
|
28
28
|
|
|
@@ -33,10 +33,18 @@ final class G2NotifyFileTests: XCTestCase {
|
|
|
33
33
|
}
|
|
34
34
|
return value
|
|
35
35
|
}
|
|
36
|
-
XCTAssertEqual(
|
|
37
|
-
XCTAssertEqual(u32(
|
|
38
|
-
XCTAssertEqual(u32(
|
|
39
|
-
XCTAssertEqual(
|
|
36
|
+
XCTAssertEqual(header[0], 0x00) // SEND_START
|
|
37
|
+
XCTAssertEqual(u32(1), 0) // NOTIFICATION_JSON_WHITELIST
|
|
38
|
+
XCTAssertEqual(u32(5), 162) // size
|
|
39
|
+
XCTAssertEqual(u32(9), 0xE96D_1DA8) // crc32c
|
|
40
|
+
|
|
41
|
+
// The docs' shifted view of the same bytes: "mode" 0x100 would have been
|
|
42
|
+
// the Android-notification type, which is what put blank cards in the
|
|
43
|
+
// stock viewer.
|
|
44
|
+
XCTAssertEqual(u32(0), 0x000) // mode == fileType << 8
|
|
45
|
+
XCTAssertEqual(u32(4), 41472) // size * 256
|
|
46
|
+
XCTAssertEqual(u32(8), 0x6D1D_A800) // crc << 8
|
|
47
|
+
XCTAssertEqual(header[12], 0xE9) // crc >> 24
|
|
40
48
|
|
|
41
49
|
let nameBytes = header.subdata(in: 13 ..< 93)
|
|
42
50
|
let expectedName = Array(G2NotifyFile.whitelistFilename.utf8)
|
|
@@ -44,6 +52,14 @@ final class G2NotifyFileTests: XCTestCase {
|
|
|
44
52
|
XCTAssertTrue(nameBytes.dropFirst(expectedName.count).allSatisfy { $0 == 0 })
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
func testFileCheckHeaderTypesTheAndroidNotification() {
|
|
56
|
+
let header = G2NotifyFile.fileCheckHeader(
|
|
57
|
+
filename: G2NotifyFile.whitelistFilename, content: Data("{}".utf8), fileType: .androidNotification
|
|
58
|
+
)
|
|
59
|
+
XCTAssertEqual(header[0], 0x00)
|
|
60
|
+
XCTAssertEqual(Array(header[1 ..< 5]), [0x01, 0x00, 0x00, 0x00])
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
func testWhitelistJsonShape() throws {
|
|
48
64
|
let json = G2NotifyFile.whitelistJson(
|
|
49
65
|
filterEnabled: true,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class G2RingBatteryTests: XCTestCase {
|
|
6
|
+
func testConnectedBatteryIncludingEmptyFullAndCharging() {
|
|
7
|
+
for level: UInt8 in [0, 62, 100] {
|
|
8
|
+
XCTAssertEqual(G2RingBattery.decode(Data([0x52, 0x42, 1, 3, level])),
|
|
9
|
+
G2RingBattery(connected: true, level: Int(level), charging: false))
|
|
10
|
+
XCTAssertEqual(G2RingBattery.decode(Data([0x52, 0x42, 1, 7, level])),
|
|
11
|
+
G2RingBattery(connected: true, level: Int(level), charging: true))
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func testDisconnectedAndUnknownReportsClearCachedBattery() {
|
|
16
|
+
XCTAssertEqual(G2RingBattery.decode(Data([0x52, 0x42, 1, 0, 255])), .disconnected)
|
|
17
|
+
XCTAssertEqual(G2RingBattery.decode(Data([0x52, 0x42, 1, 6, 62])), .disconnected)
|
|
18
|
+
XCTAssertEqual(G2RingBattery.decode(Data([0x52, 0x42, 1, 1, 255])),
|
|
19
|
+
G2RingBattery(connected: true, level: -1, charging: false))
|
|
20
|
+
XCTAssertEqual(G2RingBattery.decode(Data([0x52, 0x42, 1, 5, 62])),
|
|
21
|
+
G2RingBattery(connected: true, level: -1, charging: false))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testBatterySurvivesSnapshotAndPartialStatusBridge() throws {
|
|
25
|
+
let battery = try XCTUnwrap(G2RingBattery.decode(Data([0x52, 0x42, 1, 7, 62])))
|
|
26
|
+
let values: [String: Any] = [
|
|
27
|
+
"ringConnected": battery.connected, "ringBatteryLevel": battery.level, "ringCharging": battery.charging,
|
|
28
|
+
]
|
|
29
|
+
let status = GlassesStatus(values: values)
|
|
30
|
+
XCTAssertEqual(status.dictionary["ringBatteryLevel"] as? Int, 62)
|
|
31
|
+
XCTAssertTrue(status.ringConnected)
|
|
32
|
+
XCTAssertTrue(status.ringCharging)
|
|
33
|
+
XCTAssertFalse(status.controllerConnected)
|
|
34
|
+
XCTAssertEqual(status.controllerBatteryLevel, -1)
|
|
35
|
+
let update = GlassesStatusUpdate(values: values)
|
|
36
|
+
XCTAssertEqual(update.dictionary as NSDictionary, values as NSDictionary)
|
|
37
|
+
XCTAssertEqual(update.ringBatteryLevel, 62)
|
|
38
|
+
XCTAssertNil(GlassesStatusUpdate(values: [:]).ringBatteryLevel)
|
|
39
|
+
XCTAssertEqual(GlassesStatus(values: [:]).ringBatteryLevel, -1)
|
|
40
|
+
let disconnected = status.disconnected()
|
|
41
|
+
XCTAssertFalse(disconnected.ringConnected)
|
|
42
|
+
XCTAssertEqual(disconnected.ringBatteryLevel, -1)
|
|
43
|
+
XCTAssertFalse(disconnected.ringCharging)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func testMalformedReportsAreIgnored() {
|
|
47
|
+
let malformed: [[UInt8]] = [
|
|
48
|
+
[], [0x52, 0x42, 1, 3], [0x52, 0x42, 1, 3, 62, 0],
|
|
49
|
+
[0x41, 0x42, 1, 3, 62], [0x52, 0x41, 1, 3, 62],
|
|
50
|
+
[0x52, 0x42, 2, 3, 62], [0x52, 0x42, 1, 3, 101], [0x52, 0x42, 1, 3, 255],
|
|
51
|
+
]
|
|
52
|
+
for bytes in malformed {
|
|
53
|
+
XCTAssertNil(G2RingBattery.decode(Data(bytes)), "Invalid report: \(bytes)")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class G2SensorsTests: XCTestCase {
|
|
5
|
+
func testNorthMayOmitTheProtobufScalarButMalformedHeadingsAreRejected() {
|
|
6
|
+
XCTAssertEqual(G2Sensors.heading([:]), 0)
|
|
7
|
+
XCTAssertEqual(G2Sensors.heading([1: Int32(0)]), 0)
|
|
8
|
+
XCTAssertEqual(G2Sensors.heading([1: Int32(359)]), 359)
|
|
9
|
+
XCTAssertNil(G2Sensors.heading([1: Int32(360)]))
|
|
10
|
+
XCTAssertNil(G2Sensors.heading([1: Data([0])]))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func testControlsMatchUpstreamModesAndLittleEndianOptions() {
|
|
14
|
+
XCTAssertEqual(G2Sensors.command(sensor: "light", operation: "query", options: [:]), Data([16, 0]))
|
|
15
|
+
XCTAssertEqual(G2Sensors.command(sensor: "light", operation: "start", options: [:]), Data([16, 1, 1, 244, 1, 1, 0, 136, 19]))
|
|
16
|
+
XCTAssertEqual(G2Sensors.command(sensor: "compass", operation: "start", options: ["intervalMs": 250, "minChangeDegrees": 0]), Data([10, 2, 250, 0, 0, 0]))
|
|
17
|
+
XCTAssertEqual(G2Sensors.command(sensor: "light", operation: "stop", options: [:]), Data([16, 2]))
|
|
18
|
+
XCTAssertEqual(G2Sensors.command(sensor: "compass", operation: "stop", options: [:]), Data([10, 0]))
|
|
19
|
+
XCTAssertNil(G2Sensors.command(sensor: "light", operation: "start", options: ["intervalMs": 99]))
|
|
20
|
+
XCTAssertNil(G2Sensors.command(sensor: "compass", operation: "start", options: ["intervalMs": .nan]))
|
|
21
|
+
XCTAssertNil(G2Sensors.command(sensor: "compass", operation: "start", options: ["minChangeDegrees": 0.5]))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testLightReportPreservesRawValuesAndFreshness() throws {
|
|
25
|
+
let bytes: [UInt8] = [65, 76, 1, 2, 15, 3, 210, 4, 0, 0, 46, 22, 0, 0, 250, 0, 9, 3, 0, 4, 66, 0, 0, 240]
|
|
26
|
+
let report = try XCTUnwrap(G2Sensors.light(Data(bytes)))
|
|
27
|
+
XCTAssertEqual(report["reason"] as? String, "poll")
|
|
28
|
+
XCTAssertEqual(report["value"] as? Int64, 1234)
|
|
29
|
+
XCTAssertEqual(report["peak"] as? Int64, 5678)
|
|
30
|
+
XCTAssertEqual(report["sampleTimestampMs"] as? Int64, 4_026_531_906)
|
|
31
|
+
XCTAssertEqual(report["readSucceeded"] as? Bool, true)
|
|
32
|
+
XCTAssertEqual(report["autoBrightness"] as? Bool, true)
|
|
33
|
+
XCTAssertEqual(report["passive"] as? Bool, true)
|
|
34
|
+
XCTAssertNil(G2Sensors.light(Data(bytes.dropLast())))
|
|
35
|
+
var stale = bytes; stale[4] = 0
|
|
36
|
+
XCTAssertEqual(G2Sensors.light(Data(stale))?["readSucceeded"] as? Bool, false)
|
|
37
|
+
stale[2] = 2
|
|
38
|
+
XCTAssertNil(G2Sensors.light(Data(stale)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testCompassDiagnosticsAndUnknownMarkers() throws {
|
|
42
|
+
var bytes: [UInt8] = [67, 77, 1, 3, 1, 2, 143, 0, 66, 0, 0, 240]
|
|
43
|
+
let report = try XCTUnwrap(G2Sensors.compass(Data(bytes)))
|
|
44
|
+
XCTAssertEqual(report["source"] as? String, "gmrv")
|
|
45
|
+
XCTAssertEqual(report["accuracy"] as? Int, 3)
|
|
46
|
+
XCTAssertEqual(report["sampleMatched"] as? Bool, true)
|
|
47
|
+
XCTAssertEqual(report["magneticDataFresh"] as? Bool, true)
|
|
48
|
+
XCTAssertEqual(report["sampleTimestampMs"] as? Int64, 4_026_531_906)
|
|
49
|
+
bytes[3] = 255; bytes[4] = 255; bytes[5] = 0; bytes[6] = 0
|
|
50
|
+
XCTAssertTrue(G2Sensors.compass(Data(bytes))?["accuracy"] is NSNull)
|
|
51
|
+
XCTAssertEqual(G2Sensors.compass(Data(bytes))?["sampleMatched"] as? Bool, false)
|
|
52
|
+
bytes[3] = 4
|
|
53
|
+
XCTAssertNil(G2Sensors.compass(Data(bytes)))
|
|
54
|
+
XCTAssertNil(G2Sensors.compass(Data(bytes.dropLast())))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class G2TouchInputTests: XCTestCase {
|
|
6
|
+
private func fields(event: Int32, wire: UInt8, tick: UInt32 = 123) -> [Int: Any] {
|
|
7
|
+
[1: event, 2: Int32(2), 100: Data([
|
|
8
|
+
0x52, 0x49, 1, 1, wire, 0, 0, 0,
|
|
9
|
+
UInt8(truncatingIfNeeded: tick), UInt8(truncatingIfNeeded: tick >> 8),
|
|
10
|
+
UInt8(truncatingIfNeeded: tick >> 16), UInt8(truncatingIfNeeded: tick >> 24),
|
|
11
|
+
])]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func testRawRingCompletionsSurviveCloselySpacedInput() throws {
|
|
15
|
+
var debounce = G2TouchDebounce()
|
|
16
|
+
// swipe, next touch_down, click: the old global 100 ms gate lost the
|
|
17
|
+
// click and left GestureDotsService holding the second touch_down.
|
|
18
|
+
for (event, wire) in [(Int32(1), UInt8(4)), (14, 10), (0, 1)] {
|
|
19
|
+
let ring = try XCTUnwrap(G2RingInput(fields: fields(event: event, wire: wire)))
|
|
20
|
+
XCTAssertTrue(debounce.accepts(event: event, ring: ring, receivedAt: 1))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testDelayedProcessingUsesReceiptTimeForLegacyInput() {
|
|
25
|
+
var debounce = G2TouchDebounce()
|
|
26
|
+
// These calls run together, but the BLE callbacks arrived a second apart.
|
|
27
|
+
XCTAssertTrue(debounce.accepts(event: 0, ring: nil, receivedAt: 1))
|
|
28
|
+
XCTAssertTrue(debounce.accepts(event: 0, ring: nil, receivedAt: 2))
|
|
29
|
+
XCTAssertFalse(debounce.accepts(event: 0, ring: nil, receivedAt: 2.01))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testEdgesAndUnknownReportsDoNotConsumeLegacyDebounce() {
|
|
33
|
+
var debounce = G2TouchDebounce()
|
|
34
|
+
for event: Int32 in [9, 10, 14, 127] {
|
|
35
|
+
XCTAssertTrue(debounce.accepts(event: event, ring: nil, receivedAt: 1))
|
|
36
|
+
}
|
|
37
|
+
XCTAssertTrue(debounce.accepts(event: 0, ring: nil, receivedAt: 1))
|
|
38
|
+
XCTAssertFalse(debounce.accepts(event: 0, ring: nil, receivedAt: 1.01))
|
|
39
|
+
XCTAssertTrue(debounce.accepts(event: 10, ring: nil, receivedAt: 1.02))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func testRingInputDoesNotSuppressIndependentTempleInput() throws {
|
|
43
|
+
var debounce = G2TouchDebounce()
|
|
44
|
+
let ring = try XCTUnwrap(G2RingInput(fields: fields(event: 0, wire: 1)))
|
|
45
|
+
XCTAssertTrue(debounce.accepts(event: 0, ring: ring, receivedAt: 1))
|
|
46
|
+
XCTAssertTrue(debounce.accepts(event: 0, ring: nil, receivedAt: 1))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func testRingMetadataValidatesSourceFormatAndEventMapping() throws {
|
|
50
|
+
let valid = fields(event: 10, wire: 8, tick: 0xF123_4567)
|
|
51
|
+
let ring = try XCTUnwrap(G2RingInput(fields: valid))
|
|
52
|
+
XCTAssertEqual(ring.wireType, 8)
|
|
53
|
+
XCTAssertEqual(ring.tick, 0xF123_4567)
|
|
54
|
+
for key in [1, 2, 100] {
|
|
55
|
+
var malformed = valid
|
|
56
|
+
malformed.removeValue(forKey: key)
|
|
57
|
+
XCTAssertNil(G2RingInput(fields: malformed))
|
|
58
|
+
}
|
|
59
|
+
for index in [0, 1, 2, 3, 4, 7] {
|
|
60
|
+
var malformed = valid
|
|
61
|
+
var bytes = try XCTUnwrap(valid[100] as? Data)
|
|
62
|
+
bytes[index] = 255
|
|
63
|
+
malformed[100] = bytes
|
|
64
|
+
XCTAssertNil(G2RingInput(fields: malformed))
|
|
65
|
+
}
|
|
66
|
+
var truncated = valid
|
|
67
|
+
truncated[100] = Data([0x52, 0x49])
|
|
68
|
+
XCTAssertNil(G2RingInput(fields: truncated))
|
|
69
|
+
XCTAssertNotNil(G2RingInput(fields: fields(event: 127, wire: 255)))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import UIKit
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
final class G2WritePacingTests: XCTestCase {
|
|
6
|
+
func testBackgroundingUnblocksAWriterWithNoCreditsAndForegroundingRestoresTheGate() {
|
|
7
|
+
// Model a false readiness flag across lifecycle transitions. The
|
|
8
|
+
// drainer must reselect its policy while a packet waits to be sent.
|
|
9
|
+
var submitted = 0
|
|
10
|
+
let transitions: [(UIApplication.State, Int)] = [(.active, 0), (.inactive, 1), (.background, 2), (.active, 2)]
|
|
11
|
+
for (state, expectedSubmissions) in transitions {
|
|
12
|
+
let pacing = G2WritePacing(cfwEnabled: true, applicationState: state)
|
|
13
|
+
if pacing.canWrite(peripheralReady: false) {
|
|
14
|
+
submitted += 1
|
|
15
|
+
XCTAssertEqual(pacing, .timed)
|
|
16
|
+
XCTAssertEqual(G2WritePacing.packetDelayNs, 6_000_000)
|
|
17
|
+
}
|
|
18
|
+
XCTAssertEqual(submitted, expectedSubmissions)
|
|
19
|
+
}
|
|
20
|
+
XCTAssertEqual(submitted, 2)
|
|
21
|
+
let resumed = G2WritePacing(cfwEnabled: true, applicationState: .active)
|
|
22
|
+
XCTAssertFalse(resumed.canWrite(peripheralReady: false))
|
|
23
|
+
XCTAssertTrue(resumed.canWrite(peripheralReady: true))
|
|
24
|
+
XCTAssertEqual(resumed, .creditBurst)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testStockKeepsTimedPacingRegardlessOfReadinessOrApplicationState() {
|
|
28
|
+
for state in [UIApplication.State.active, .inactive, .background] {
|
|
29
|
+
let pacing = G2WritePacing(cfwEnabled: false, applicationState: state)
|
|
30
|
+
XCTAssertEqual(pacing, .timed)
|
|
31
|
+
XCTAssertTrue(pacing.canWrite(peripheralReady: false))
|
|
32
|
+
XCTAssertTrue(pacing.canWrite(peripheralReady: true))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|