@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.
Files changed (48) hide show
  1. package/README.md +4 -4
  2. package/android/src/main/assets/G2Firmware/README.md +3 -3
  3. package/android/src/main/assets/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
  4. package/android/src/main/assets/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
  5. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +0 -4
  6. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +0 -1
  7. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +30 -86
  8. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +1 -2
  9. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
  10. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +171 -51
  11. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +23 -3
  12. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +18 -6
  13. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
  14. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +8 -8
  15. package/build/BluetoothSdk.types.d.ts +37 -1
  16. package/build/BluetoothSdk.types.d.ts.map +1 -1
  17. package/build/BluetoothSdk.types.js.map +1 -1
  18. package/build/_private/BluetoothSdkModule.d.ts +0 -1
  19. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  20. package/build/_private/BluetoothSdkModule.js.map +1 -1
  21. package/build/generated/releaseMetadata.js +3 -3
  22. package/build/generated/releaseMetadata.js.map +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +0 -1
  25. package/build/index.js.map +1 -1
  26. package/ios/BluetoothSdkModule.swift +0 -6
  27. package/ios/Source/BluetoothSDK.swift +0 -4
  28. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  29. package/ios/Source/DeviceManager.swift +35 -88
  30. package/ios/Source/DeviceStore.swift +16 -2
  31. package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
  32. package/ios/Source/Resources/G2Firmware/README.md +3 -3
  33. package/ios/Source/Resources/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
  34. package/ios/Source/Resources/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
  35. package/ios/Source/controllers/R1.swift +145 -49
  36. package/ios/Source/sgcs/G2.swift +311 -54
  37. package/ios/Source/sgcs/G2CfwRenderer.swift +24 -8
  38. package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
  39. package/ios/Source/sgcs/G2Flasher.swift +8 -8
  40. package/ios/Source/sgcs/SGCManager.swift +13 -0
  41. package/ios/Source/utils/G2NotifyFile.swift +116 -0
  42. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +2 -2
  43. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +91 -0
  44. package/package.json +1 -1
  45. package/src/BluetoothSdk.types.ts +37 -1
  46. package/src/_private/BluetoothSdkModule.ts +0 -1
  47. package/src/generated/releaseMetadata.ts +3 -3
  48. 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, plus a move blank).
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 + 2
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. `omitBuiltinText` skips `font == "builtin"` text during
237
- /// rasterization — the firmware draws those lines itself.
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?, omitBuiltinText: Bool,
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. Width is overestimated (the phone has no advance tables).
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: "f4dfb0b49ad3de3c2daf17f8a27a157c3dc98411d6a0d3ab2cfd0918f41b9afa",
292
- ps: 3_523_396, progEnd: 0x0079_4324, pass: true
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.6.10",
31
- fileName: "g2-stock-2.2.6.10.evenota.bin",
32
- sha256: "f4dfb0b49ad3de3c2daf17f8a27a157c3dc98411d6a0d3ab2cfd0918f41b9afa",
33
- size: 4_301_227
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.6.10 CFW",
39
- fileName: "g2-glassly-cfw-2.2.6.10.evenota.bin",
40
- sha256: "82726bbe4d1d82f06a3d345ba3e5a4a39161077e5d26dcd7af24c4677df87fd1",
41
- size: 4_336_071
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/bluetooth-sdk",
3
- "version": "0.1.0-dev.62",
3
+ "version": "0.1.0-dev.90",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -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.62",
16
- "releaseSetId": "glassly-0.1.0-dev.62",
17
- "sourceCommit": "69eee438b882fca1c9b0ad466d71fc2c24053662",
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"),