@glassly/bluetooth-sdk 0.1.0-dev.62 → 0.1.0-dev.90
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 +4 -4
- package/android/src/main/assets/G2Firmware/README.md +3 -3
- package/android/src/main/assets/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/android/src/main/assets/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +0 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +0 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +30 -86
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +1 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +171 -51
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +23 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +18 -6
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +8 -8
- package/build/BluetoothSdk.types.d.ts +37 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +0 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +0 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +0 -6
- package/ios/Source/BluetoothSDK.swift +0 -4
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +35 -88
- package/ios/Source/DeviceStore.swift +16 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
- package/ios/Source/Resources/G2Firmware/README.md +3 -3
- package/ios/Source/Resources/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/ios/Source/Resources/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
- package/ios/Source/controllers/R1.swift +145 -49
- package/ios/Source/sgcs/G2.swift +311 -54
- package/ios/Source/sgcs/G2CfwRenderer.swift +24 -8
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +8 -8
- package/ios/Source/sgcs/SGCManager.swift +13 -0
- package/ios/Source/utils/G2NotifyFile.swift +116 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +2 -2
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +91 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +37 -1
- package/src/_private/BluetoothSdkModule.ts +0 -1
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +0 -1
|
@@ -156,10 +156,13 @@ actor G2CfwRenderer {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/// One firmware-drawn (mode 15) text line at absolute panel coordinates.
|
|
159
|
+
/// `boxWidth` is the source element's canvas-space box width — the erase
|
|
160
|
+
/// bounds floor, since the phone ellipsized the line against it.
|
|
159
161
|
struct TextDraw: Equatable {
|
|
160
162
|
let x: Int
|
|
161
163
|
let y: Int
|
|
162
164
|
let text: String
|
|
165
|
+
let boxWidth: Int
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
/// The result of one incremental encode: the wire payload (nil when the
|
|
@@ -210,9 +213,10 @@ actor G2CfwRenderer {
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
/// Caps for the delta planner. maxDirtyRects bounds fids the caller must
|
|
213
|
-
/// pre-allocate (one per rect, plus full frame,
|
|
216
|
+
/// pre-allocate (one per rect, plus full frame, a move blank, and a
|
|
217
|
+
/// right-margin blank).
|
|
214
218
|
static let maxDirtyRects = 6
|
|
215
|
-
static let requiredFidCount = maxDirtyRects +
|
|
219
|
+
static let requiredFidCount = maxDirtyRects + 3
|
|
216
220
|
/// Cap on mode-14 runs per update (mode-8's count byte is the hard bound).
|
|
217
221
|
static let maxGlyphRuns = 180
|
|
218
222
|
/// Line pitch and default draw options of the glasses' builtin 20 px font
|
|
@@ -233,12 +237,17 @@ actor G2CfwRenderer {
|
|
|
233
237
|
/// contained). `textDraws` are appended after the pixel segments so glyphs
|
|
234
238
|
/// land on top of freshly pushed background. `blankBoxes` prepends a solid
|
|
235
239
|
/// black frame at a previous window position (window move) in the same
|
|
236
|
-
/// atomic bundle. `
|
|
237
|
-
///
|
|
240
|
+
/// atomic bundle. `marginBlankBoxes` prepends a solid black frame OUTSIDE
|
|
241
|
+
/// the window — mode-15 draws at absolute panel coordinates without
|
|
242
|
+
/// clipping to the window, so stale glyph tails past the window's right
|
|
243
|
+
/// edge can only be erased by a frame aimed at the margin itself.
|
|
244
|
+
/// `omitBuiltinText` skips `font == "builtin"` text during rasterization —
|
|
245
|
+
/// the firmware draws those lines itself.
|
|
238
246
|
func encodeUpdate(
|
|
239
247
|
_ frame: SceneFrame, boxes: LensBoxes, previousRaster: Data?,
|
|
240
248
|
forcedRects: [CanvasRect], fids: [UInt16], textDraws: [TextDraw],
|
|
241
|
-
allowRectCopy: Bool, blankBoxes: LensBoxes?,
|
|
249
|
+
allowRectCopy: Bool, blankBoxes: LensBoxes?, marginBlankBoxes: LensBoxes? = nil,
|
|
250
|
+
omitBuiltinText: Bool,
|
|
242
251
|
textureGlyphsEnabled: Bool = false, textureImagesEnabled: Bool = false,
|
|
243
252
|
cacheGeneration: Int = 0
|
|
244
253
|
) -> SceneUpdate? {
|
|
@@ -282,6 +291,11 @@ actor G2CfwRenderer {
|
|
|
282
291
|
if let blankBoxes, let blank = Self.solidStereoFrame(color: 0, boxes: blankBoxes, fid: takeFid()) {
|
|
283
292
|
segments.append(blank)
|
|
284
293
|
}
|
|
294
|
+
if let marginBlankBoxes,
|
|
295
|
+
let blank = Self.solidStereoFrame(color: 0, boxes: marginBlankBoxes, fid: takeFid())
|
|
296
|
+
{
|
|
297
|
+
segments.append(blank)
|
|
298
|
+
}
|
|
285
299
|
|
|
286
300
|
let plan: DeltaPlan
|
|
287
301
|
if let previousRaster, previousRaster.count == raster.count, blankBoxes == nil {
|
|
@@ -919,7 +933,7 @@ actor G2CfwRenderer {
|
|
|
919
933
|
for (index, line) in text.split(separator: "\n", omittingEmptySubsequences: false).enumerated() {
|
|
920
934
|
let y = baseY + index * builtinLineHeight
|
|
921
935
|
guard y >= 0, y + builtinLineHeight <= frameHeight, !line.isEmpty else { continue }
|
|
922
|
-
draws.append(TextDraw(x: baseX, y: y, text: String(line)))
|
|
936
|
+
draws.append(TextDraw(x: baseX, y: y, text: String(line), boxWidth: Int(element.w)))
|
|
923
937
|
}
|
|
924
938
|
}
|
|
925
939
|
return draws
|
|
@@ -927,7 +941,9 @@ actor G2CfwRenderer {
|
|
|
927
941
|
|
|
928
942
|
/// Conservative canvas-space bounds of firmware-drawn text, used to force
|
|
929
943
|
/// background re-pushes that erase glyphs the phone raster never
|
|
930
|
-
/// contained.
|
|
944
|
+
/// contained. The phone has no advance tables, so width is the char-count
|
|
945
|
+
/// estimate floored at the element's box width — wide glyphs overshoot
|
|
946
|
+
/// 13px/char, and an undershot erase leaves glyph tails on the panel.
|
|
931
947
|
static func builtinTextBounds(
|
|
932
948
|
_ draws: [TextDraw], boxes: LensBoxes, canvasWidth: Int, canvasHeight: Int
|
|
933
949
|
) -> [CanvasRect] {
|
|
@@ -935,7 +951,7 @@ actor G2CfwRenderer {
|
|
|
935
951
|
let x = max(0, draw.x - Int(boxes.left.x) - 4)
|
|
936
952
|
let y = max(0, draw.y - Int(boxes.left.y) - 2)
|
|
937
953
|
guard x < canvasWidth, y < canvasHeight else { return nil }
|
|
938
|
-
let width = min(canvasWidth - x, draw.text.count * 13 + 30)
|
|
954
|
+
let width = min(canvasWidth - x, max(draw.text.count * 13 + 30, draw.boxWidth + 8))
|
|
939
955
|
let height = min(canvasHeight - y, builtinLineHeight + 4)
|
|
940
956
|
return CanvasRect(x: x, y: y, w: width, h: height)
|
|
941
957
|
}
|
|
@@ -288,8 +288,8 @@ enum G2FlashProtocol {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
static let goldenStock = GoldenVector(
|
|
291
|
-
sha256: "
|
|
292
|
-
ps:
|
|
291
|
+
sha256: "a03fbea9f68a9de6bc271daabb9f3a41c59053d1086622c76a4e990f829cc561",
|
|
292
|
+
ps: 3_697_284, progEnd: 0x007B_EA64, pass: true
|
|
293
293
|
)
|
|
294
294
|
|
|
295
295
|
/// Run the parse+guard on `img` and assert it reproduces the golden vector. Returns
|
|
@@ -27,18 +27,18 @@ enum G2FirmwareCatalog {
|
|
|
27
27
|
G2FirmwareArtifact(
|
|
28
28
|
id: "stock",
|
|
29
29
|
displayName: "Stock firmware",
|
|
30
|
-
version: "2.2.
|
|
31
|
-
fileName: "g2-stock-2.2.
|
|
32
|
-
sha256: "
|
|
33
|
-
size:
|
|
30
|
+
version: "2.2.9.22",
|
|
31
|
+
fileName: "g2-stock-2.2.9.22.evenota.bin",
|
|
32
|
+
sha256: "a03fbea9f68a9de6bc271daabb9f3a41c59053d1086622c76a4e990f829cc561",
|
|
33
|
+
size: 4_476_518
|
|
34
34
|
),
|
|
35
35
|
G2FirmwareArtifact(
|
|
36
36
|
id: "glassly-cfw",
|
|
37
37
|
displayName: "Glassly CFW",
|
|
38
|
-
version: "2.2.
|
|
39
|
-
fileName: "g2-glassly-cfw-2.2.
|
|
40
|
-
sha256: "
|
|
41
|
-
size:
|
|
38
|
+
version: "2.2.9.22 CFW",
|
|
39
|
+
fileName: "g2-glassly-cfw-2.2.9.22.evenota.bin",
|
|
40
|
+
sha256: "ceeede999529505ebc0a1369f57ab3c941273379a90b16669ccd39cc6bd99135",
|
|
41
|
+
size: 4_515_846
|
|
42
42
|
),
|
|
43
43
|
]
|
|
44
44
|
|
|
@@ -221,6 +221,13 @@ protocol SGCManager {
|
|
|
221
221
|
|
|
222
222
|
func sendCalendarEvents(_ events: [[String: Any]])
|
|
223
223
|
|
|
224
|
+
// MARK: - Phone Notifications (on-glass ANCS display options + app filter)
|
|
225
|
+
|
|
226
|
+
/// Push NotificationControl from the DeviceStore `notification_ctrl` dict.
|
|
227
|
+
func sendNotificationCtrl()
|
|
228
|
+
/// Push the ANCS app allowlist from the DeviceStore `notification_filter` dict.
|
|
229
|
+
func sendNotificationFilter()
|
|
230
|
+
|
|
224
231
|
// MARK: - Dashboard Display Settings
|
|
225
232
|
|
|
226
233
|
func sendDashboardDisplaySettings()
|
|
@@ -469,6 +476,12 @@ extension SGCManager {
|
|
|
469
476
|
|
|
470
477
|
func sendCalendarEvents(_: [[String: Any]]) {}
|
|
471
478
|
|
|
479
|
+
// MARK: - Phone Notifications (default no-op — only G2 filters ANCS on-glass)
|
|
480
|
+
|
|
481
|
+
func sendNotificationCtrl() {}
|
|
482
|
+
|
|
483
|
+
func sendNotificationFilter() {}
|
|
484
|
+
|
|
472
485
|
// MARK: - Dashboard Display Settings (default no-op — only G2 supports this)
|
|
473
486
|
|
|
474
487
|
func sendDashboardDisplaySettings() {}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// G2NotifyFile.swift
|
|
3
|
+
// BluetoothSDK
|
|
4
|
+
//
|
|
5
|
+
// Pure helpers for the G2 notification whitelist file push
|
|
6
|
+
// (`user/notify_whitelist.json` over the 0xC4/0xC5 file-transfer services).
|
|
7
|
+
// Kept free of CoreBluetooth so the wire format is unit-testable.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import Foundation
|
|
11
|
+
|
|
12
|
+
enum G2NotifyFile {
|
|
13
|
+
static let whitelistFilename = "user/notify_whitelist.json"
|
|
14
|
+
|
|
15
|
+
/// Firmware caps: 100-app list, 80-byte records (64-byte identifier
|
|
16
|
+
/// including NUL, 16-byte display name including NUL).
|
|
17
|
+
static let maxApps = 100
|
|
18
|
+
static let maxIdentifierBytes = 63
|
|
19
|
+
static let maxDisplayNameBytes = 15
|
|
20
|
+
|
|
21
|
+
/// CRC-32C (Castagnoli), init 0, non-reflected (MSB-first) — the variant the
|
|
22
|
+
/// G2 file-transfer FILE_CHECK header uses, NOT the common reflected crc32c.
|
|
23
|
+
private static let crc32cTable: [UInt32] = {
|
|
24
|
+
var table = [UInt32](repeating: 0, count: 256)
|
|
25
|
+
for i in 0 ..< 256 {
|
|
26
|
+
var crc = UInt32(i) << 24
|
|
27
|
+
for _ in 0 ..< 8 {
|
|
28
|
+
crc = (crc & 0x8000_0000) != 0 ? (crc << 1) ^ 0x1EDC_6F41 : crc << 1
|
|
29
|
+
}
|
|
30
|
+
table[i] = crc
|
|
31
|
+
}
|
|
32
|
+
return table
|
|
33
|
+
}()
|
|
34
|
+
|
|
35
|
+
static func crc32c(_ data: Data) -> UInt32 {
|
|
36
|
+
var crc: UInt32 = 0
|
|
37
|
+
for byte in data {
|
|
38
|
+
let idx = Int((crc >> 24) ^ UInt32(byte)) & 0xFF
|
|
39
|
+
crc = (crc << 8) ^ crc32cTable[idx]
|
|
40
|
+
}
|
|
41
|
+
return crc
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// 93-byte FILE_CHECK payload: mode, size, split CRC-32C, 80-byte
|
|
45
|
+
/// NUL-padded filename. The glasses reconstruct the checksum as
|
|
46
|
+
/// `(extra << 24) | (checksum >> 8)`.
|
|
47
|
+
static func fileCheckHeader(filename: String, content: Data) -> Data {
|
|
48
|
+
let crc = crc32c(content)
|
|
49
|
+
var header = Data()
|
|
50
|
+
// Mode 0x100: the even-g2-protocol docs' prose says 0x00010000, but the
|
|
51
|
+
// working example (`notification.py`) packs `<I` 0x100 — trust the code.
|
|
52
|
+
header.appendLittleEndianUInt32(0x100) // mode
|
|
53
|
+
header.appendLittleEndianUInt32(UInt32(content.count) &* 256) // size
|
|
54
|
+
header.appendLittleEndianUInt32(crc << 8) // checksum (low 24 bits, shifted)
|
|
55
|
+
header.append(UInt8(crc >> 24)) // extra (high CRC byte)
|
|
56
|
+
var name = Array(filename.utf8)
|
|
57
|
+
if name.count > 80 {
|
|
58
|
+
name = Array(name.prefix(80))
|
|
59
|
+
}
|
|
60
|
+
header.append(contentsOf: name)
|
|
61
|
+
header.append(contentsOf: [UInt8](repeating: 0, count: 80 - name.count))
|
|
62
|
+
return header
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Whitelist JSON in the stock app's shape (AppNotificationInfo /
|
|
66
|
+
/// AppWhiteList / AppSimpleInfo from the decompiled Even app):
|
|
67
|
+
/// `{call_enable, msg_enable, calendar_enable, ios_mail_enable,
|
|
68
|
+
/// app: {enable, list: [{id, name}]}}`
|
|
69
|
+
/// Entries with identifiers over 63 UTF-8 bytes are invalid to the firmware
|
|
70
|
+
/// and dropped; display names truncate to 15 UTF-8 bytes.
|
|
71
|
+
static func whitelistJson(filterEnabled: Bool, apps: [(id: String, name: String)]) -> Data {
|
|
72
|
+
var list: [[String: String]] = []
|
|
73
|
+
for app in apps {
|
|
74
|
+
guard !app.id.isEmpty, app.id.utf8.count <= maxIdentifierBytes else { continue }
|
|
75
|
+
let name = app.name.isEmpty ? app.id : app.name
|
|
76
|
+
list.append(["id": app.id, "name": truncateUtf8(name, maxBytes: maxDisplayNameBytes)])
|
|
77
|
+
if list.count >= maxApps {
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let payload: [String: Any] = [
|
|
82
|
+
"call_enable": true,
|
|
83
|
+
"msg_enable": true,
|
|
84
|
+
"calendar_enable": true,
|
|
85
|
+
"ios_mail_enable": true,
|
|
86
|
+
"app": [
|
|
87
|
+
"enable": filterEnabled,
|
|
88
|
+
"list": list,
|
|
89
|
+
] as [String: Any],
|
|
90
|
+
]
|
|
91
|
+
// sortedKeys keeps the output deterministic (glasses cache by checksum).
|
|
92
|
+
return (try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys])) ?? Data()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Truncate to a UTF-8 byte budget without splitting a code point.
|
|
96
|
+
static func truncateUtf8(_ value: String, maxBytes: Int) -> String {
|
|
97
|
+
if value.utf8.count <= maxBytes {
|
|
98
|
+
return value
|
|
99
|
+
}
|
|
100
|
+
var result = ""
|
|
101
|
+
for char in value {
|
|
102
|
+
if (result + String(char)).utf8.count > maxBytes {
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
result.append(char)
|
|
106
|
+
}
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private extension Data {
|
|
112
|
+
mutating func appendLittleEndianUInt32(_ value: UInt32) {
|
|
113
|
+
var v = value.littleEndian
|
|
114
|
+
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -361,8 +361,8 @@ final class G2CfwRendererTests: XCTestCase {
|
|
|
361
361
|
)
|
|
362
362
|
let draws = G2CfwRenderer.builtinTextDraws(frame([element]), boxes: b)
|
|
363
363
|
XCTAssertEqual(draws, [
|
|
364
|
-
G2CfwRenderer.TextDraw(x: 32 + 10 + 2, y: 128 + 20 + 2, text: "one"),
|
|
365
|
-
G2CfwRenderer.TextDraw(x: 32 + 10 + 2, y: 128 + 20 + 2 + 27, text: "two"),
|
|
364
|
+
G2CfwRenderer.TextDraw(x: 32 + 10 + 2, y: 128 + 20 + 2, text: "one", boxWidth: 200),
|
|
365
|
+
G2CfwRenderer.TextDraw(x: 32 + 10 + 2, y: 128 + 20 + 2 + 27, text: "two", boxWidth: 200),
|
|
366
366
|
])
|
|
367
367
|
let bounds = G2CfwRenderer.builtinTextBounds(draws, boxes: b, canvasWidth: 576, canvasHeight: 288)
|
|
368
368
|
XCTAssertEqual(bounds.count, 2)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
@testable import BluetoothSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
import XCTest
|
|
4
|
+
|
|
5
|
+
/// Wire-format checks for the G2 notify-whitelist file push. Expected values
|
|
6
|
+
/// cross-computed with the community reference implementation
|
|
7
|
+
/// (even-g2-protocol examples/notif/notification.py).
|
|
8
|
+
final class G2NotifyFileTests: XCTestCase {
|
|
9
|
+
func testCrc32cMatchesReferenceVectors() {
|
|
10
|
+
// Non-reflected (MSB-first) Castagnoli, init 0 — NOT the common
|
|
11
|
+
// reflected crc32c, whose check value for "123456789" is 0xE3069283.
|
|
12
|
+
XCTAssertEqual(G2NotifyFile.crc32c(Data("123456789".utf8)), 0xC052_A8C8)
|
|
13
|
+
XCTAssertEqual(G2NotifyFile.crc32c(Data()), 0)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func testFileCheckHeaderLayout() {
|
|
17
|
+
let content = Data(
|
|
18
|
+
#"{"app":{"enable":true,"list":[{"id":"com.apple.MobileSMS","name":"Messages"}]},"calendar_enable":true,"call_enable":true,"ios_mail_enable":true,"msg_enable":true}"#
|
|
19
|
+
.utf8
|
|
20
|
+
)
|
|
21
|
+
XCTAssertEqual(content.count, 162)
|
|
22
|
+
XCTAssertEqual(G2NotifyFile.crc32c(content), 0xE96D_1DA8)
|
|
23
|
+
|
|
24
|
+
let header = G2NotifyFile.fileCheckHeader(
|
|
25
|
+
filename: G2NotifyFile.whitelistFilename, content: content
|
|
26
|
+
)
|
|
27
|
+
XCTAssertEqual(header.count, 93)
|
|
28
|
+
|
|
29
|
+
func u32(_ offset: Int) -> UInt32 {
|
|
30
|
+
var value: UInt32 = 0
|
|
31
|
+
for i in (0 ..< 4).reversed() {
|
|
32
|
+
value = (value << 8) | UInt32(header[offset + i])
|
|
33
|
+
}
|
|
34
|
+
return value
|
|
35
|
+
}
|
|
36
|
+
XCTAssertEqual(u32(0), 0x100) // mode
|
|
37
|
+
XCTAssertEqual(u32(4), 41472) // size = len * 256
|
|
38
|
+
XCTAssertEqual(u32(8), 0x6D1D_A800) // checksum = crc << 8
|
|
39
|
+
XCTAssertEqual(header[12], 0xE9) // extra = crc >> 24
|
|
40
|
+
|
|
41
|
+
let nameBytes = header.subdata(in: 13 ..< 93)
|
|
42
|
+
let expectedName = Array(G2NotifyFile.whitelistFilename.utf8)
|
|
43
|
+
XCTAssertEqual(Array(nameBytes.prefix(expectedName.count)), expectedName)
|
|
44
|
+
XCTAssertTrue(nameBytes.dropFirst(expectedName.count).allSatisfy { $0 == 0 })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testWhitelistJsonShape() throws {
|
|
48
|
+
let json = G2NotifyFile.whitelistJson(
|
|
49
|
+
filterEnabled: true,
|
|
50
|
+
apps: [
|
|
51
|
+
(id: "com.apple.MobileSMS", name: "Messages"),
|
|
52
|
+
(id: "", name: "dropped-empty-id"),
|
|
53
|
+
(id: String(repeating: "x", count: 64), name: "dropped-long-id"),
|
|
54
|
+
(id: "com.example.longname", name: "A name that is far too long to fit"),
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
let root = try XCTUnwrap(
|
|
58
|
+
try JSONSerialization.jsonObject(with: json) as? [String: Any]
|
|
59
|
+
)
|
|
60
|
+
XCTAssertEqual(root["call_enable"] as? Bool, true)
|
|
61
|
+
XCTAssertEqual(root["msg_enable"] as? Bool, true)
|
|
62
|
+
XCTAssertEqual(root["calendar_enable"] as? Bool, true)
|
|
63
|
+
XCTAssertEqual(root["ios_mail_enable"] as? Bool, true)
|
|
64
|
+
|
|
65
|
+
let app = try XCTUnwrap(root["app"] as? [String: Any])
|
|
66
|
+
XCTAssertEqual(app["enable"] as? Bool, true)
|
|
67
|
+
let list = try XCTUnwrap(app["list"] as? [[String: String]])
|
|
68
|
+
XCTAssertEqual(list.count, 2)
|
|
69
|
+
XCTAssertEqual(list[0], ["id": "com.apple.MobileSMS", "name": "Messages"])
|
|
70
|
+
// Display names truncate to the firmware's 15-byte record budget.
|
|
71
|
+
XCTAssertEqual(list[1]["id"], "com.example.longname")
|
|
72
|
+
XCTAssertLessThanOrEqual(try XCTUnwrap(list[1]["name"]?.utf8.count), G2NotifyFile.maxDisplayNameBytes)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func testWhitelistJsonCapsAtFirmwareLimit() throws {
|
|
76
|
+
let apps = (0 ..< 150).map { (id: "com.example.app\($0)", name: "App \($0)") }
|
|
77
|
+
let json = G2NotifyFile.whitelistJson(filterEnabled: false, apps: apps)
|
|
78
|
+
let root = try XCTUnwrap(try JSONSerialization.jsonObject(with: json) as? [String: Any])
|
|
79
|
+
let app = try XCTUnwrap(root["app"] as? [String: Any])
|
|
80
|
+
XCTAssertEqual(app["enable"] as? Bool, false)
|
|
81
|
+
XCTAssertEqual((app["list"] as? [[String: String]])?.count, G2NotifyFile.maxApps)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func testTruncateUtf8RespectsCodePointBoundaries() {
|
|
85
|
+
XCTAssertEqual(G2NotifyFile.truncateUtf8("héllo wörld beyond", maxBytes: 15).utf8.count <= 15, true)
|
|
86
|
+
XCTAssertEqual(G2NotifyFile.truncateUtf8("short", maxBytes: 15), "short")
|
|
87
|
+
// 4-byte emoji must not be split mid-sequence.
|
|
88
|
+
let truncated = G2NotifyFile.truncateUtf8("aaaaaaaaaaaaaa😀", maxBytes: 15)
|
|
89
|
+
XCTAssertEqual(truncated, "aaaaaaaaaaaaaa")
|
|
90
|
+
}
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1015,6 +1015,7 @@ export type BluetoothSdkModuleEvents = {
|
|
|
1015
1015
|
speaker_enroll_result: (event: SpeakerEnrollResultEvent) => void
|
|
1016
1016
|
phone_notification: (event: PhoneNotificationEvent) => void
|
|
1017
1017
|
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
|
|
1018
|
+
notification_app_seen: (event: NotificationAppSeenEvent) => void
|
|
1018
1019
|
wifi_status_change: (event: WifiStatusChangeEvent) => void
|
|
1019
1020
|
wifi_scan_result: (event: WifiScanResultEvent) => void
|
|
1020
1021
|
hotspot_status_change: (event: HotspotStatusChangeEvent) => void
|
|
@@ -1094,6 +1095,17 @@ export interface PhoneNotificationDismissedEvent {
|
|
|
1094
1095
|
timestamp: number
|
|
1095
1096
|
}
|
|
1096
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* iOS G2: the glasses' on-firmware ANCS client reported the source app of a
|
|
1100
|
+
* notification (bundle id + display name — never the content). Feeds the
|
|
1101
|
+
* seen-apps list backing the ANCS filter UI.
|
|
1102
|
+
*/
|
|
1103
|
+
export interface NotificationAppSeenEvent {
|
|
1104
|
+
appId: string
|
|
1105
|
+
displayName: string
|
|
1106
|
+
timestamp: number
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1097
1109
|
export type PublicGlassesStatus = Omit<
|
|
1098
1110
|
GlassesStatus,
|
|
1099
1111
|
"otaUpdateAvailable" | "otaProgress" | "otaInProgress" | "otaVersionUrl"
|
|
@@ -1209,7 +1221,6 @@ export interface BluetoothSdkPublicModule {
|
|
|
1209
1221
|
displayText(text: string, x?: number, y?: number, size?: number): Promise<void>
|
|
1210
1222
|
clearDisplay(): Promise<void>
|
|
1211
1223
|
showDashboard(): Promise<void>
|
|
1212
|
-
toggleHeadUp(): Promise<void>
|
|
1213
1224
|
/**
|
|
1214
1225
|
* Open the glasses' built-in notification panel (same effect as the user
|
|
1215
1226
|
* saying "Hey Even, show notifications"). G2 only; a no-op elsewhere.
|
|
@@ -1477,6 +1488,12 @@ export interface GlassesStatus {
|
|
|
1477
1488
|
controllerMacAddress: string
|
|
1478
1489
|
controllerBatteryLevel: number
|
|
1479
1490
|
controllerSignalStrength: number
|
|
1491
|
+
/**
|
|
1492
|
+
* iOS: a connected lens is authorized to read the phone's ANCS notification
|
|
1493
|
+
* stream (the "Share System Notifications" Bluetooth consent). Always false
|
|
1494
|
+
* on Android and for models that don't consume ANCS on-firmware.
|
|
1495
|
+
*/
|
|
1496
|
+
ancsAuthorized?: boolean
|
|
1480
1497
|
}
|
|
1481
1498
|
|
|
1482
1499
|
export interface CoreDashboardMenuItem {
|
|
@@ -1497,6 +1514,23 @@ export interface CoreSettings {
|
|
|
1497
1514
|
calendar_events: CalendarEvent[]
|
|
1498
1515
|
}
|
|
1499
1516
|
|
|
1517
|
+
/** G2 on-glass notification display behavior (NotificationControl protobuf). */
|
|
1518
|
+
export interface G2NotificationCtrlSetting {
|
|
1519
|
+
enabled: boolean
|
|
1520
|
+
auto_display: boolean
|
|
1521
|
+
/** On-screen popup duration in seconds: 0 (firmware default), 5, 7, or 10. */
|
|
1522
|
+
disp_time: number
|
|
1523
|
+
avoid_disturb: boolean
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/** G2 on-glass ANCS app filter (user/notify_whitelist.json push). */
|
|
1527
|
+
export interface G2NotificationFilterSetting {
|
|
1528
|
+
/** false = whitelisting off on-glass (fail-open, every app shows). */
|
|
1529
|
+
enabled: boolean
|
|
1530
|
+
/** Allowlist entries; identifiers are iOS bundle ids. */
|
|
1531
|
+
apps: Array<{id: string; name: string}>
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1500
1534
|
export interface Device {
|
|
1501
1535
|
/**
|
|
1502
1536
|
* Stable app-facing key for this scan result, within the limits of the
|
|
@@ -1595,6 +1629,8 @@ export type BluetoothSettingsUpdate = Partial<{
|
|
|
1595
1629
|
dashboard_depth: number
|
|
1596
1630
|
menu_apps: DashboardMenuItem[] | CoreDashboardMenuItem[] | Array<Record<string, unknown>> | null
|
|
1597
1631
|
calendar_events: CalendarEvent[]
|
|
1632
|
+
notification_ctrl: G2NotificationCtrlSetting
|
|
1633
|
+
notification_filter: G2NotificationFilterSetting
|
|
1598
1634
|
metric_system: boolean
|
|
1599
1635
|
twelve_hour_time: boolean
|
|
1600
1636
|
gallery_mode: boolean
|
|
@@ -108,7 +108,6 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
108
108
|
forget(): Promise<void>
|
|
109
109
|
forgetController(): Promise<void>
|
|
110
110
|
showDashboard(): Promise<void>
|
|
111
|
-
toggleHeadUp(): Promise<void>
|
|
112
111
|
setBrightness(level: number, autoMode?: boolean | null): Promise<void>
|
|
113
112
|
setAutoBrightness(enabled: boolean): Promise<void>
|
|
114
113
|
setDashboardPosition(height: number, depth: number): Promise<void>
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.1.0",
|
|
15
|
-
"releaseIdentity": "0.1.0-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.1.0-dev.90",
|
|
16
|
+
"releaseSetId": "glassly-0.1.0-dev.90",
|
|
17
|
+
"sourceCommit": "1140c7490313353cb1d8da0782bf74c87d9119eb",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -107,7 +107,6 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
|
|
|
107
107
|
displayText: bindPublicMethod("displayText"),
|
|
108
108
|
clearDisplay: bindPublicMethod("clearDisplay"),
|
|
109
109
|
showDashboard: bindPublicMethod("showDashboard"),
|
|
110
|
-
toggleHeadUp: bindPublicMethod("toggleHeadUp"),
|
|
111
110
|
showNativeNotificationPanel: bindPublicMethod("showNativeNotificationPanel"),
|
|
112
111
|
setDashboardPosition: bindPublicMethod("setDashboardPosition"),
|
|
113
112
|
setHeadUpAngle: bindPublicMethod("setHeadUpAngle"),
|