@glassly/bluetooth-sdk 0.1.0-dev.62 → 0.1.0-dev.89
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
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -44,6 +44,14 @@ private enum G2BLE {
|
|
|
44
44
|
static let CHAR_WRITE = CBUUID(string: "00002760-08C2-11E1-9073-0E8AC72E5401")
|
|
45
45
|
static let CHAR_NOTIFY = CBUUID(string: "00002760-08C2-11E1-9073-0E8AC72E5402")
|
|
46
46
|
static let AUDIO_NOTIFY = CBUUID(string: "00002760-08C2-11E1-9073-0E8AC72E6402")
|
|
47
|
+
// File-transfer characteristics (notification JSON / whitelist pushes ride
|
|
48
|
+
// the 0xC4/0xC5 file services here, not the main 5401 protocol char)
|
|
49
|
+
static let FILE_WRITE = CBUUID(string: "00002760-08C2-11E1-9073-0E8AC72E7401")
|
|
50
|
+
static let FILE_NOTIFY = CBUUID(string: "00002760-08C2-11E1-9073-0E8AC72E7402")
|
|
51
|
+
|
|
52
|
+
// File-transfer service IDs (same AA-21 envelope as every other service)
|
|
53
|
+
static let FILE_SERVICE_CMD: UInt8 = 0xC4 // FILE_CHECK header, START (0x01), END (0x02)
|
|
54
|
+
static let FILE_SERVICE_DATA: UInt8 = 0xC5 // raw file bytes
|
|
47
55
|
|
|
48
56
|
/// We discover services by scanning for these characteristics
|
|
49
57
|
/// The service UUID that contains these chars
|
|
@@ -136,6 +144,14 @@ private enum G2SettingCommandId: Int32 {
|
|
|
136
144
|
case deviceRespondToApp = 4 // Glasses responds to app
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
/// eNotificationCommandId from notification.proto (service 0x04)
|
|
148
|
+
private enum NotificationCmd: Int32 {
|
|
149
|
+
case ctrl = 1 // NOTIFICATION_CTRL — NotificationControl in field 3
|
|
150
|
+
case ios = 2 // NOTIFICATION_IOS — glasses report an ANCS source app in field 4
|
|
151
|
+
case whitelistCtrl = 3 // NOTIFICATION_WHITELIST_CTRL — NotificationWhitelistCtrl in field 6
|
|
152
|
+
case commRsp = 161 // NOTIFICATION_COMM_RSP
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
/// DevCfgCommandId from dev_config_protocol.proto
|
|
140
156
|
private enum DevCfgCommandId: Int32 {
|
|
141
157
|
case authentication = 4
|
|
@@ -1608,7 +1624,9 @@ private final class ImgAckBox {
|
|
|
1608
1624
|
self.session = nil
|
|
1609
1625
|
self.fragment = nil
|
|
1610
1626
|
cont = nil
|
|
1611
|
-
if success {
|
|
1627
|
+
if success {
|
|
1628
|
+
lastSuccessMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
1629
|
+
}
|
|
1612
1630
|
lock.unlock()
|
|
1613
1631
|
c.resume(returning: success)
|
|
1614
1632
|
return true
|
|
@@ -1699,7 +1717,7 @@ final class G2: NSObject, SGCManager {
|
|
|
1699
1717
|
private var cfwSceneActive = false
|
|
1700
1718
|
/// Rate limit on resurrecting the page for a blank scene; a non-blank one rebuilds immediately.
|
|
1701
1719
|
private var cfwLastIdleResurrectAt: Date?
|
|
1702
|
-
private static let cfwIdleResurrectMinIntervalMs: Double =
|
|
1720
|
+
private static let cfwIdleResurrectMinIntervalMs: Double = 5000
|
|
1703
1721
|
private static let cfwSceneContainerID: Int32 = 10
|
|
1704
1722
|
private let cfwRenderer = G2CfwRenderer()
|
|
1705
1723
|
/// Deltas composite onto a shadow seeded by a mode-6 keyframe, so payloads drain in order.
|
|
@@ -1762,13 +1780,20 @@ final class G2: NSObject, SGCManager {
|
|
|
1762
1780
|
|
|
1763
1781
|
/// Mode-9 rect copies have no capability token; they shipped with
|
|
1764
1782
|
/// protocol revision 15 alongside the texture/font modes.
|
|
1765
|
-
private var supportsCfwRectCopy: Bool {
|
|
1783
|
+
private var supportsCfwRectCopy: Bool {
|
|
1784
|
+
cfwProtocolVersion >= 15
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1766
1787
|
/// Mode-15 builtin-font string draws (`font15` capability token).
|
|
1767
|
-
private var supportsCfwBuiltinText: Bool {
|
|
1788
|
+
private var supportsCfwBuiltinText: Bool {
|
|
1789
|
+
cfwFeatures.contains("font15")
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1768
1792
|
/// Mode-12 uploads + mode-14 cached-glyph runs (mono text as draws).
|
|
1769
1793
|
private var supportsCfwTextureGlyphs: Bool {
|
|
1770
1794
|
cfwFeatures.contains("texcache12") && cfwFeatures.contains("texstr14")
|
|
1771
1795
|
}
|
|
1796
|
+
|
|
1772
1797
|
/// Mode-12 uploads + mode-13 cached-image draws (repeated icons as draws).
|
|
1773
1798
|
private var supportsCfwTextureImages: Bool {
|
|
1774
1799
|
cfwFeatures.contains("texcache12") && cfwFeatures.contains("teximg13")
|
|
@@ -1787,6 +1812,10 @@ final class G2: NSObject, SGCManager {
|
|
|
1787
1812
|
private var rightNotifyChar: CBCharacteristic?
|
|
1788
1813
|
private var rightAudioChar: CBCharacteristic?
|
|
1789
1814
|
private var leftAudioChar: CBCharacteristic?
|
|
1815
|
+
private var leftFileWriteChar: CBCharacteristic?
|
|
1816
|
+
private var rightFileWriteChar: CBCharacteristic?
|
|
1817
|
+
private var leftFileNotifyChar: CBCharacteristic?
|
|
1818
|
+
private var rightFileNotifyChar: CBCharacteristic?
|
|
1790
1819
|
private var leftInitialized: Bool = false
|
|
1791
1820
|
private var rightInitialized: Bool = false
|
|
1792
1821
|
private var leftAuthenticated: Bool = false
|
|
@@ -1813,6 +1842,15 @@ final class G2: NSObject, SGCManager {
|
|
|
1813
1842
|
// and is dropped entirely — see the SysEvent handler.
|
|
1814
1843
|
private let PHANTOM_EXIT_ACK_WINDOW_MS: Int64 = 2500
|
|
1815
1844
|
|
|
1845
|
+
/// Shared connect options. RequiresANCS raises the iOS "Share System
|
|
1846
|
+
/// Notifications" consent so the glasses' on-firmware ANCS client can read
|
|
1847
|
+
/// the phone's notification stream (the phone app itself never can).
|
|
1848
|
+
private static let peripheralConnectOptions: [String: Any] = [
|
|
1849
|
+
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
1850
|
+
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
1851
|
+
CBConnectPeripheralOptionRequiresANCS: true,
|
|
1852
|
+
]
|
|
1853
|
+
|
|
1816
1854
|
/// Device search
|
|
1817
1855
|
var DEVICE_SEARCH_ID = "NOT_SET"
|
|
1818
1856
|
/// map device names to serial numbers:
|
|
@@ -2415,6 +2453,12 @@ final class G2: NSObject, SGCManager {
|
|
|
2415
2453
|
let calendarEvents =
|
|
2416
2454
|
DeviceStore.shared.get("bluetooth", "calendar_events") as? [[String: Any]] ?? []
|
|
2417
2455
|
sendCalendarEvents(calendarEvents)
|
|
2456
|
+
|
|
2457
|
+
// push ANCS notification config (display options + app filter) and
|
|
2458
|
+
// surface the current ANCS authorization state to the UI
|
|
2459
|
+
publishAncsAuthorization()
|
|
2460
|
+
sendNotificationCtrl()
|
|
2461
|
+
sendNotificationFilter()
|
|
2418
2462
|
}
|
|
2419
2463
|
|
|
2420
2464
|
// MARK: - Heartbeats
|
|
@@ -2521,6 +2565,106 @@ final class G2: NSObject, SGCManager {
|
|
|
2521
2565
|
setDashboardMenu(menuItems)
|
|
2522
2566
|
}
|
|
2523
2567
|
|
|
2568
|
+
// MARK: - Notifications (on-glass ANCS display options + app filter)
|
|
2569
|
+
|
|
2570
|
+
/// Push NotificationControl (master toggle, auto-display popup, on-screen
|
|
2571
|
+
/// duration, avoid-disturb) from the DeviceStore `notification_ctrl` dict.
|
|
2572
|
+
/// Full state is retransmitted on every change — that matches the stock app
|
|
2573
|
+
/// and the glasses echo it back as the ack.
|
|
2574
|
+
func sendNotificationCtrl() {
|
|
2575
|
+
let ctrl = DeviceStore.shared.get("bluetooth", "notification_ctrl") as? [String: Any] ?? [:]
|
|
2576
|
+
let enabled = ctrl["enabled"] as? Bool ?? true
|
|
2577
|
+
let autoDisplay = ctrl["auto_display"] as? Bool ?? true
|
|
2578
|
+
let dispTime = ctrl["disp_time"] as? Int ?? 5
|
|
2579
|
+
let avoidDisturb = ctrl["avoid_disturb"] as? Bool ?? true
|
|
2580
|
+
|
|
2581
|
+
var ctrlW = ProtobufWriter()
|
|
2582
|
+
ctrlW.writeInt32Field(1, enabled ? 1 : 0) // notifEnable
|
|
2583
|
+
ctrlW.writeInt32Field(2, autoDisplay ? 1 : 0) // autoDispEnable
|
|
2584
|
+
ctrlW.writeInt32Field(3, Int32(dispTime)) // dispTime (0/5/7/10 s)
|
|
2585
|
+
ctrlW.writeInt32Field(5, avoidDisturb ? 1 : 0) // avoidDisturbEnable
|
|
2586
|
+
|
|
2587
|
+
var pkg = ProtobufWriter()
|
|
2588
|
+
pkg.writeInt32Field(1, NotificationCmd.ctrl.rawValue)
|
|
2589
|
+
pkg.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2590
|
+
pkg.writeMessageField(3, ctrlW.data)
|
|
2591
|
+
sendNotificationCommand(pkg.data)
|
|
2592
|
+
Bridge.log(
|
|
2593
|
+
"G2: Sent notification ctrl enabled=\(enabled) autoDisplay=\(autoDisplay) dispTime=\(dispTime) avoidDisturb=\(avoidDisturb)"
|
|
2594
|
+
)
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/// Push the ANCS app allowlist from the DeviceStore `notification_filter`
|
|
2598
|
+
/// dict ({enabled, apps: [{id, name}]}). The list travels as the
|
|
2599
|
+
/// `user/notify_whitelist.json` file (the protobuf whitelist message only
|
|
2600
|
+
/// carries the enable toggle); the WHITELIST_CTRL toggle goes after the
|
|
2601
|
+
/// file so the firmware reloads with the new list in place.
|
|
2602
|
+
func sendNotificationFilter() {
|
|
2603
|
+
let filter = DeviceStore.shared.get("bluetooth", "notification_filter") as? [String: Any] ?? [:]
|
|
2604
|
+
let enabled = filter["enabled"] as? Bool ?? false
|
|
2605
|
+
let apps = filter["apps"] as? [[String: Any]] ?? []
|
|
2606
|
+
Task { [weak self] in
|
|
2607
|
+
await self?.pushNotificationFilter(enabled: enabled, apps: apps)
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
private func pushNotificationFilter(enabled: Bool, apps: [[String: Any]]) async {
|
|
2612
|
+
let entries = apps.compactMap { app -> (id: String, name: String)? in
|
|
2613
|
+
guard let id = app["id"] as? String, !id.isEmpty else { return nil }
|
|
2614
|
+
return (id: id, name: app["name"] as? String ?? id)
|
|
2615
|
+
}
|
|
2616
|
+
// The file is written even when filtering is off so a later enable
|
|
2617
|
+
// toggles against the user's current list, not a stale one.
|
|
2618
|
+
let json = G2NotifyFile.whitelistJson(filterEnabled: enabled, apps: entries)
|
|
2619
|
+
await sendNotifyFile(named: G2NotifyFile.whitelistFilename, content: json)
|
|
2620
|
+
|
|
2621
|
+
var wlW = ProtobufWriter()
|
|
2622
|
+
wlW.writeInt32Field(1, enabled ? 0 : 1) // whitelistDisable (disabled = fail-open)
|
|
2623
|
+
var pkg = ProtobufWriter()
|
|
2624
|
+
pkg.writeInt32Field(1, NotificationCmd.whitelistCtrl.rawValue)
|
|
2625
|
+
pkg.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2626
|
+
pkg.writeMessageField(6, wlW.data)
|
|
2627
|
+
sendNotificationCommand(pkg.data)
|
|
2628
|
+
Bridge.log("G2: Sent notification filter enabled=\(enabled) apps=\(entries.count)")
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
/// File-transfer push (services 0xC4/0xC5 on the dedicated file
|
|
2632
|
+
/// characteristic): FILE_CHECK header → START → DATA → END, per arm. The
|
|
2633
|
+
/// glasses cache by checksum but still expect the data packets either way,
|
|
2634
|
+
/// so no branching on the CACHE_HIT/MISS response is needed.
|
|
2635
|
+
private func sendNotifyFile(named filename: String, content: Data) async {
|
|
2636
|
+
guard !firmwareFlashLifecycleActive, !firmwareFlasher.isActive else { return }
|
|
2637
|
+
let header = G2NotifyFile.fileCheckHeader(filename: filename, content: content)
|
|
2638
|
+
var pushedSides: [String] = []
|
|
2639
|
+
for right in [true, false] {
|
|
2640
|
+
guard let peripheral = right ? rightPeripheral : leftPeripheral,
|
|
2641
|
+
let char = right ? rightFileWriteChar : leftFileWriteChar
|
|
2642
|
+
else { continue }
|
|
2643
|
+
let writeType: CBCharacteristicWriteType =
|
|
2644
|
+
char.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
|
|
2645
|
+
|
|
2646
|
+
func write(_ packets: [Data]) async {
|
|
2647
|
+
for packet in packets {
|
|
2648
|
+
peripheral.writeValue(packet, for: char, type: writeType)
|
|
2649
|
+
try? await Task.sleep(nanoseconds: 8_000_000)
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
await write(sendManager.buildPackets(serviceId: G2BLE.FILE_SERVICE_CMD, payload: header))
|
|
2654
|
+
try? await Task.sleep(nanoseconds: 150_000_000)
|
|
2655
|
+
await write(sendManager.buildPackets(serviceId: G2BLE.FILE_SERVICE_CMD, payload: Data([0x01]))) // START
|
|
2656
|
+
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
2657
|
+
await write(sendManager.buildPackets(serviceId: G2BLE.FILE_SERVICE_DATA, payload: content))
|
|
2658
|
+
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
2659
|
+
await write(sendManager.buildPackets(serviceId: G2BLE.FILE_SERVICE_CMD, payload: Data([0x02]))) // END
|
|
2660
|
+
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
2661
|
+
pushedSides.append(right ? "R" : "L")
|
|
2662
|
+
}
|
|
2663
|
+
Bridge.log(
|
|
2664
|
+
"G2: Pushed \(filename) (\(content.count) bytes) to [\(pushedSides.joined(separator: ","))]"
|
|
2665
|
+
)
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2524
2668
|
// MARK: - SGCManager: Display Control
|
|
2525
2669
|
|
|
2526
2670
|
func sendTextWall(_ text: String) async {
|
|
@@ -2855,7 +2999,8 @@ final class G2: NSObject, SGCManager {
|
|
|
2855
2999
|
// hatch may have been flipped off mid-session — without this
|
|
2856
3000
|
// check the lease renews forever while frames go stock.
|
|
2857
3001
|
if self.cfwSceneActive, self.pageCreated, self.dashboardShowing == 0,
|
|
2858
|
-
self.supportsCfwSceneRendering
|
|
3002
|
+
self.supportsCfwSceneRendering
|
|
3003
|
+
{
|
|
2859
3004
|
self.sendFaceclawFbControl(acquire: true)
|
|
2860
3005
|
} else {
|
|
2861
3006
|
self.stopFbLease(sendRelease: true)
|
|
@@ -2868,7 +3013,9 @@ final class G2: NSObject, SGCManager {
|
|
|
2868
3013
|
private func stopFbLease(sendRelease: Bool) {
|
|
2869
3014
|
fbLeaseTask?.cancel()
|
|
2870
3015
|
fbLeaseTask = nil
|
|
2871
|
-
if sendRelease {
|
|
3016
|
+
if sendRelease {
|
|
3017
|
+
sendFaceclawFbControl(acquire: false)
|
|
3018
|
+
}
|
|
2872
3019
|
// Lease end frees the on-glass texture cache; the model must re-learn.
|
|
2873
3020
|
cfwCacheGeneration += 1
|
|
2874
3021
|
}
|
|
@@ -2879,7 +3026,11 @@ final class G2: NSObject, SGCManager {
|
|
|
2879
3026
|
/// expire) restores stock behavior and launches any deferred dashboard.
|
|
2880
3027
|
func wakeLeaseEnabled(_ enabled: Bool) {
|
|
2881
3028
|
wakeLeaseWanted = enabled
|
|
2882
|
-
if enabled {
|
|
3029
|
+
if enabled {
|
|
3030
|
+
startWakeLease()
|
|
3031
|
+
} else {
|
|
3032
|
+
stopWakeLease(sendRelease: true)
|
|
3033
|
+
}
|
|
2883
3034
|
}
|
|
2884
3035
|
|
|
2885
3036
|
/// Like the fb lease, the wake lease is per lens and must reach both arms.
|
|
@@ -2908,7 +3059,9 @@ final class G2: NSObject, SGCManager {
|
|
|
2908
3059
|
private func stopWakeLease(sendRelease: Bool) {
|
|
2909
3060
|
wakeLeaseTask?.cancel()
|
|
2910
3061
|
wakeLeaseTask = nil
|
|
2911
|
-
if sendRelease {
|
|
3062
|
+
if sendRelease {
|
|
3063
|
+
sendFaceclawWakeControl(acquire: false)
|
|
3064
|
+
}
|
|
2912
3065
|
}
|
|
2913
3066
|
|
|
2914
3067
|
/// The firmware lease is volatile and dies with the link, while
|
|
@@ -2951,11 +3104,28 @@ final class G2: NSObject, SGCManager {
|
|
|
2951
3104
|
let textDraws = useBuiltinText ? G2CfwRenderer.builtinTextDraws(frame, boxes: boxes) : []
|
|
2952
3105
|
// Text pixels are never in the raster: force old + new bounds dirty so stale glyphs erase.
|
|
2953
3106
|
var forced: [G2CfwRenderer.CanvasRect] = []
|
|
3107
|
+
var marginBlank: G2CfwRenderer.LensBoxes?
|
|
2954
3108
|
if textDraws != cfwLastTextDraws {
|
|
2955
3109
|
forced = G2CfwRenderer.builtinTextBounds(
|
|
2956
3110
|
cfwLastTextDraws + textDraws, boxes: boxes,
|
|
2957
3111
|
canvasWidth: Int(boxes.width), canvasHeight: Int(boxes.height)
|
|
2958
3112
|
)
|
|
3113
|
+
// Mode 15 doesn't clip at the window's right edge: a line the
|
|
3114
|
+
// phone's metrics under-measured can spill into the outer panel
|
|
3115
|
+
// margin, where no in-window rect or full frame ever repaints.
|
|
3116
|
+
// Blank the old window's right margin whenever draws change.
|
|
3117
|
+
// (Builtin frames pin depth, so the old window is lens-equal.)
|
|
3118
|
+
if !cfwLastTextDraws.isEmpty, let last = cfwLastBoxes, last.left == last.right {
|
|
3119
|
+
let edgeX = Int32(last.left.x) + last.width
|
|
3120
|
+
let bandW = Int32(G2CfwRenderer.frameWidth) - edgeX
|
|
3121
|
+
if bandW >= 4 {
|
|
3122
|
+
marginBlank = G2CfwRenderer.LensBoxes(
|
|
3123
|
+
left: CGPoint(x: CGFloat(edgeX), y: last.left.y),
|
|
3124
|
+
right: CGPoint(x: CGFloat(edgeX), y: last.right.y),
|
|
3125
|
+
width: bandW, height: last.height
|
|
3126
|
+
)
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
2959
3129
|
}
|
|
2960
3130
|
let fids = (0 ..< G2CfwRenderer.requiredFidCount).map { _ in nextCfwFrameId() }
|
|
2961
3131
|
// Copies would smear firmware-drawn glyphs the raster knows nothing about.
|
|
@@ -2966,6 +3136,7 @@ final class G2: NSObject, SGCManager {
|
|
|
2966
3136
|
forcedRects: forced, fids: fids, textDraws: textDraws,
|
|
2967
3137
|
allowRectCopy: allowCopy,
|
|
2968
3138
|
blankBoxes: movedBoxes ? cfwLastBoxes : nil,
|
|
3139
|
+
marginBlankBoxes: marginBlank,
|
|
2969
3140
|
omitBuiltinText: useBuiltinText,
|
|
2970
3141
|
textureGlyphsEnabled: useTextureGlyphs,
|
|
2971
3142
|
textureImagesEnabled: supportsCfwTextureImages && pinDepth,
|
|
@@ -3030,7 +3201,9 @@ final class G2: NSObject, SGCManager {
|
|
|
3030
3201
|
/// whether they need a page rebuild.
|
|
3031
3202
|
@discardableResult
|
|
3032
3203
|
private func installCfwCarrierIfNeeded() -> Bool {
|
|
3033
|
-
if textContainers.isEmpty && hasCfwCarrier {
|
|
3204
|
+
if textContainers.isEmpty && hasCfwCarrier {
|
|
3205
|
+
return true
|
|
3206
|
+
}
|
|
3034
3207
|
textContainers.removeAll()
|
|
3035
3208
|
sceneTextByElement.removeAll()
|
|
3036
3209
|
sceneImageByElement.removeAll()
|
|
@@ -3093,7 +3266,9 @@ final class G2: NSObject, SGCManager {
|
|
|
3093
3266
|
// its normal paths.
|
|
3094
3267
|
guard cfwClassified, cfwCapabilityString != nil else { return }
|
|
3095
3268
|
if enabled {
|
|
3096
|
-
if installCfwCarrierIfNeeded() {
|
|
3269
|
+
if installCfwCarrierIfNeeded() {
|
|
3270
|
+
return
|
|
3271
|
+
} // already the carrier page
|
|
3097
3272
|
Bridge.log("G2: cfw_scene_renderer ON — swapping stock containers for the CFW carrier")
|
|
3098
3273
|
await requestPageRebuild()
|
|
3099
3274
|
} else {
|
|
@@ -3381,7 +3556,9 @@ final class G2: NSObject, SGCManager {
|
|
|
3381
3556
|
// the idle state is the same page everywhere.
|
|
3382
3557
|
let carrierWasConfigured = installCfwCarrierIfNeeded()
|
|
3383
3558
|
cfwClear()
|
|
3384
|
-
if !carrierWasConfigured || !pageCreated {
|
|
3559
|
+
if !carrierWasConfigured || !pageCreated {
|
|
3560
|
+
signalDisplayDirty()
|
|
3561
|
+
}
|
|
3385
3562
|
if !carrierWasConfigured, pageCreated {
|
|
3386
3563
|
// The live on-glass page still holds the stock containers the
|
|
3387
3564
|
// carrier just replaced locally (legacy text drawn before CFW
|
|
@@ -3728,7 +3905,9 @@ final class G2: NSObject, SGCManager {
|
|
|
3728
3905
|
if ok {
|
|
3729
3906
|
Bridge.log("G2: page-down probe acked — phantom exit, page alive (no rebuild)")
|
|
3730
3907
|
pageCreated = true
|
|
3731
|
-
if !cfwPendingPayloads.isEmpty {
|
|
3908
|
+
if !cfwPendingPayloads.isEmpty {
|
|
3909
|
+
cfwPendingPayloads.removeFirst()
|
|
3910
|
+
}
|
|
3732
3911
|
signalDisplayDirty() // drain the rest normally
|
|
3733
3912
|
return
|
|
3734
3913
|
}
|
|
@@ -3778,7 +3957,9 @@ final class G2: NSObject, SGCManager {
|
|
|
3778
3957
|
// loop (rebuild→systemExit→rebuild every debounce tick).
|
|
3779
3958
|
if pageCreated, supportsCfwSceneRendering, hasCfwCarrier {
|
|
3780
3959
|
cfwResetShadow()
|
|
3781
|
-
if let frame = cfwLastFrame {
|
|
3960
|
+
if let frame = cfwLastFrame {
|
|
3961
|
+
_ = await enqueueCfwFrame(frame)
|
|
3962
|
+
}
|
|
3782
3963
|
}
|
|
3783
3964
|
break
|
|
3784
3965
|
}
|
|
@@ -4721,6 +4902,10 @@ final class G2: NSObject, SGCManager {
|
|
|
4721
4902
|
rightNotifyChar = nil
|
|
4722
4903
|
rightAudioChar = nil
|
|
4723
4904
|
leftAudioChar = nil
|
|
4905
|
+
leftFileWriteChar = nil
|
|
4906
|
+
rightFileWriteChar = nil
|
|
4907
|
+
leftFileNotifyChar = nil
|
|
4908
|
+
rightFileNotifyChar = nil
|
|
4724
4909
|
DEVICE_SEARCH_ID = "NOT_SET"
|
|
4725
4910
|
centralManager?.delegate = nil
|
|
4726
4911
|
}
|
|
@@ -4859,10 +5044,12 @@ final class G2: NSObject, SGCManager {
|
|
|
4859
5044
|
}
|
|
4860
5045
|
|
|
4861
5046
|
func dbg2() {
|
|
4862
|
-
|
|
4863
5047
|
Task {
|
|
5048
|
+
dashboardShowing = 1
|
|
5049
|
+
dashboardOpening = true
|
|
4864
5050
|
let msg = EvenHubProto.shutdownMessage()
|
|
4865
5051
|
sendEvenHubCommand(msg)
|
|
5052
|
+
pageCreated = false
|
|
4866
5053
|
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
4867
5054
|
await showNotificationsPanel()
|
|
4868
5055
|
}
|
|
@@ -5308,6 +5495,10 @@ final class G2: NSObject, SGCManager {
|
|
|
5308
5495
|
rightNotifyChar = nil
|
|
5309
5496
|
leftAudioChar = nil
|
|
5310
5497
|
rightAudioChar = nil
|
|
5498
|
+
leftFileWriteChar = nil
|
|
5499
|
+
rightFileWriteChar = nil
|
|
5500
|
+
leftFileNotifyChar = nil
|
|
5501
|
+
rightFileNotifyChar = nil
|
|
5311
5502
|
authStarted = false
|
|
5312
5503
|
leftAuthenticated = false
|
|
5313
5504
|
rightAuthenticated = false
|
|
@@ -5383,10 +5574,7 @@ final class G2: NSObject, SGCManager {
|
|
|
5383
5574
|
device.discoverServices([G2BLE.SERVICE_UUID])
|
|
5384
5575
|
centralManager!.connect(
|
|
5385
5576
|
leftPeripheral!,
|
|
5386
|
-
options:
|
|
5387
|
-
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
5388
|
-
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
5389
|
-
]
|
|
5577
|
+
options: G2.peripheralConnectOptions
|
|
5390
5578
|
)
|
|
5391
5579
|
} else if name.contains("_R_") && serialNumber.contains(DEVICE_SEARCH_ID) {
|
|
5392
5580
|
rightPeripheral = device
|
|
@@ -5394,10 +5582,7 @@ final class G2: NSObject, SGCManager {
|
|
|
5394
5582
|
device.discoverServices([G2BLE.SERVICE_UUID])
|
|
5395
5583
|
centralManager!.connect(
|
|
5396
5584
|
rightPeripheral!,
|
|
5397
|
-
options:
|
|
5398
|
-
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
5399
|
-
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
5400
|
-
]
|
|
5585
|
+
options: G2.peripheralConnectOptions
|
|
5401
5586
|
)
|
|
5402
5587
|
}
|
|
5403
5588
|
// we can't emit the serial number here unfortunately:
|
|
@@ -5461,8 +5646,8 @@ final class G2: NSObject, SGCManager {
|
|
|
5461
5646
|
rightPeripheral = right
|
|
5462
5647
|
left.delegate = self
|
|
5463
5648
|
right.delegate = self
|
|
5464
|
-
centralManager?.connect(left, options:
|
|
5465
|
-
centralManager?.connect(right, options:
|
|
5649
|
+
centralManager?.connect(left, options: G2.peripheralConnectOptions)
|
|
5650
|
+
centralManager?.connect(right, options: G2.peripheralConnectOptions)
|
|
5466
5651
|
return true
|
|
5467
5652
|
}
|
|
5468
5653
|
|
|
@@ -5602,31 +5787,39 @@ final class G2: NSObject, SGCManager {
|
|
|
5602
5787
|
// path Android's NotificationListenerService feeds. iOS can't read
|
|
5603
5788
|
// other apps' notifications, but the glasses CAN (ANCS) and report the
|
|
5604
5789
|
// source app here; title/content are empty because this packet only
|
|
5605
|
-
// carries app identity.
|
|
5606
|
-
//
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5790
|
+
// carries app identity. The dedicated notification_app_seen event feeds
|
|
5791
|
+
// the engine's seen-apps store, which backs the ANCS filter UI.
|
|
5792
|
+
if cmd == NotificationCmd.ios.rawValue, let iosData = fields[4] as? Data {
|
|
5793
|
+
var ios = ProtobufReader(iosData)
|
|
5794
|
+
let iosFields = ios.parseFields()
|
|
5795
|
+
let appID = (iosFields[1] as? Data).flatMap { String(data: $0, encoding: .utf8) }?
|
|
5796
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "\0")) ?? ""
|
|
5797
|
+
let name = (iosFields[2] as? Data).flatMap { String(data: $0, encoding: .utf8) }?
|
|
5798
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "\0")) ?? ""
|
|
5799
|
+
if !appID.isEmpty {
|
|
5800
|
+
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
|
5801
|
+
Bridge.sendTypedMessage(
|
|
5802
|
+
"notification_app_seen",
|
|
5803
|
+
body: [
|
|
5804
|
+
"appId": appID,
|
|
5805
|
+
"displayName": name,
|
|
5806
|
+
"timestamp": now,
|
|
5807
|
+
]
|
|
5808
|
+
)
|
|
5809
|
+
Bridge.sendTypedMessage(
|
|
5810
|
+
"phone_notification",
|
|
5811
|
+
body: [
|
|
5812
|
+
"notificationId": "ancs-\(appID)-\(now)",
|
|
5813
|
+
"app": name.isEmpty ? appID : name,
|
|
5814
|
+
"title": "",
|
|
5815
|
+
"content": "",
|
|
5816
|
+
"priority": "0",
|
|
5817
|
+
"timestamp": now,
|
|
5818
|
+
"packageName": appID,
|
|
5819
|
+
]
|
|
5820
|
+
)
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5630
5823
|
}
|
|
5631
5824
|
|
|
5632
5825
|
/// EvenAI service (0x07). Logs the decoded EvenAIDataPackage so we can read the
|
|
@@ -6145,9 +6338,10 @@ final class G2: NSObject, SGCManager {
|
|
|
6145
6338
|
// )
|
|
6146
6339
|
|
|
6147
6340
|
if connStatus == 22 {
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6341
|
+
// The ring↔glasses link dropped. The phone↔ring link owns
|
|
6342
|
+
// controllerConnected/FullyBooted, so don't clear them here
|
|
6343
|
+
// (that would flap pairing state) — just re-request the link.
|
|
6344
|
+
Bridge.log("G2: Ring-glasses link dropped, re-requesting")
|
|
6151
6345
|
reconnectController()
|
|
6152
6346
|
}
|
|
6153
6347
|
|
|
@@ -6218,9 +6412,22 @@ final class G2: NSObject, SGCManager {
|
|
|
6218
6412
|
definitive: cmdValue == G2SettingCommandId.deviceReceiveRequest.rawValue
|
|
6219
6413
|
)
|
|
6220
6414
|
publishFirmwareVersion()
|
|
6415
|
+
publishSceneCanvas()
|
|
6221
6416
|
}
|
|
6222
6417
|
}
|
|
6223
6418
|
|
|
6419
|
+
/// Publish the CFW scene canvas into the glasses status once classification
|
|
6420
|
+
/// lands. The engine's getEffectiveCapabilities keys its lifted element
|
|
6421
|
+
/// caps off this (stock's 6-text/4-image container pools don't bound a
|
|
6422
|
+
/// phone-rasterized CFW frame). Zeros on stock so a stale value can't
|
|
6423
|
+
/// survive a CFW→stock swap.
|
|
6424
|
+
private func publishSceneCanvas() {
|
|
6425
|
+
guard cfwClassified else { return }
|
|
6426
|
+
let active = supportsCfwSceneRendering
|
|
6427
|
+
DeviceStore.shared.apply("glasses", "sceneCanvasWidth", active ? 576 : 0)
|
|
6428
|
+
DeviceStore.shared.apply("glasses", "sceneCanvasHeight", active ? 288 : 0)
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6224
6431
|
private func updateCfwCapabilities(_ data: Data?, definitive: Bool) {
|
|
6225
6432
|
// The sticky/definitive rules live in G2CfwClassification (unit-tested);
|
|
6226
6433
|
// nil means the packet proved nothing and prior identity stands.
|
|
@@ -6533,14 +6740,14 @@ extension G2: CBCentralManagerDelegate {
|
|
|
6533
6740
|
if self.leftPeripheral == nil {
|
|
6534
6741
|
self.leftPeripheral = peripheral
|
|
6535
6742
|
peripheral.delegate = self
|
|
6536
|
-
central.connect(peripheral, options:
|
|
6743
|
+
central.connect(peripheral, options: G2.peripheralConnectOptions)
|
|
6537
6744
|
// Bridge.log("G2: Connecting to LEFT: \(name)")
|
|
6538
6745
|
}
|
|
6539
6746
|
} else if name.contains("_R_") {
|
|
6540
6747
|
if self.rightPeripheral == nil {
|
|
6541
6748
|
self.rightPeripheral = peripheral
|
|
6542
6749
|
peripheral.delegate = self
|
|
6543
|
-
central.connect(peripheral, options:
|
|
6750
|
+
central.connect(peripheral, options: G2.peripheralConnectOptions)
|
|
6544
6751
|
// Bridge.log("G2: Connecting to RIGHT: \(name)")
|
|
6545
6752
|
}
|
|
6546
6753
|
}
|
|
@@ -6553,6 +6760,26 @@ extension G2: CBCentralManagerDelegate {
|
|
|
6553
6760
|
}
|
|
6554
6761
|
}
|
|
6555
6762
|
|
|
6763
|
+
nonisolated func centralManager(
|
|
6764
|
+
_: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral
|
|
6765
|
+
) {
|
|
6766
|
+
let authorized = peripheral.ancsAuthorized
|
|
6767
|
+
DispatchQueue.main.async { [weak self] in
|
|
6768
|
+
guard let self = self else { return }
|
|
6769
|
+
let side = peripheral === self.leftPeripheral ? "LEFT" : "RIGHT"
|
|
6770
|
+
Bridge.log("G2: ANCS authorization changed on \(side): \(authorized)")
|
|
6771
|
+
self.publishAncsAuthorization()
|
|
6772
|
+
}
|
|
6773
|
+
}
|
|
6774
|
+
|
|
6775
|
+
/// Either lens being ANCS-authorized is enough — the firmware's ANCS client
|
|
6776
|
+
/// runs on whichever arm holds the phone link.
|
|
6777
|
+
private func publishAncsAuthorization() {
|
|
6778
|
+
let authorized = (leftPeripheral?.ancsAuthorized ?? false)
|
|
6779
|
+
|| (rightPeripheral?.ancsAuthorized ?? false)
|
|
6780
|
+
DeviceStore.shared.apply("glasses", "ancsAuthorized", authorized)
|
|
6781
|
+
}
|
|
6782
|
+
|
|
6556
6783
|
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
6557
6784
|
DispatchQueue.main.async { [weak self] in
|
|
6558
6785
|
guard let self = self else { return }
|
|
@@ -6601,12 +6828,16 @@ extension G2: CBCentralManagerDelegate {
|
|
|
6601
6828
|
self.leftWriteChar = nil
|
|
6602
6829
|
self.leftNotifyChar = nil
|
|
6603
6830
|
self.leftAudioChar = nil
|
|
6831
|
+
self.leftFileWriteChar = nil
|
|
6832
|
+
self.leftFileNotifyChar = nil
|
|
6604
6833
|
self.leftAuthenticated = false
|
|
6605
6834
|
} else {
|
|
6606
6835
|
self.rightInitialized = false
|
|
6607
6836
|
self.rightWriteChar = nil
|
|
6608
6837
|
self.rightNotifyChar = nil
|
|
6609
6838
|
self.rightAudioChar = nil
|
|
6839
|
+
self.rightFileWriteChar = nil
|
|
6840
|
+
self.rightFileNotifyChar = nil
|
|
6610
6841
|
self.rightAuthenticated = false
|
|
6611
6842
|
}
|
|
6612
6843
|
Bridge.log("G2: \(side) rebooted during firmware flash; preserving the peer link")
|
|
@@ -6626,6 +6857,10 @@ extension G2: CBCentralManagerDelegate {
|
|
|
6626
6857
|
self.rightNotifyChar = nil
|
|
6627
6858
|
self.leftAudioChar = nil
|
|
6628
6859
|
self.rightAudioChar = nil
|
|
6860
|
+
self.leftFileWriteChar = nil
|
|
6861
|
+
self.rightFileWriteChar = nil
|
|
6862
|
+
self.leftFileNotifyChar = nil
|
|
6863
|
+
self.rightFileNotifyChar = nil
|
|
6629
6864
|
self.authStarted = false
|
|
6630
6865
|
self.leftAuthenticated = false
|
|
6631
6866
|
self.rightAuthenticated = false
|
|
@@ -6749,6 +6984,21 @@ extension G2: CBPeripheralDelegate {
|
|
|
6749
6984
|
self.rightAudioChar = char
|
|
6750
6985
|
}
|
|
6751
6986
|
peripheral.setNotifyValue(true, for: char)
|
|
6987
|
+
} else if uuid == G2BLE.FILE_WRITE {
|
|
6988
|
+
Bridge.log("G2: Found FILE WRITE char on \(side)")
|
|
6989
|
+
if peripheral === self.leftPeripheral {
|
|
6990
|
+
self.leftFileWriteChar = char
|
|
6991
|
+
} else {
|
|
6992
|
+
self.rightFileWriteChar = char
|
|
6993
|
+
}
|
|
6994
|
+
} else if uuid == G2BLE.FILE_NOTIFY {
|
|
6995
|
+
Bridge.log("G2: Found FILE NOTIFY char on \(side)")
|
|
6996
|
+
if peripheral === self.leftPeripheral {
|
|
6997
|
+
self.leftFileNotifyChar = char
|
|
6998
|
+
} else {
|
|
6999
|
+
self.rightFileNotifyChar = char
|
|
7000
|
+
}
|
|
7001
|
+
peripheral.setNotifyValue(true, for: char)
|
|
6752
7002
|
}
|
|
6753
7003
|
}
|
|
6754
7004
|
|
|
@@ -6812,6 +7062,13 @@ extension G2: CBPeripheralDelegate {
|
|
|
6812
7062
|
} else if characteristic.uuid == G2BLE.CHAR_NOTIFY {
|
|
6813
7063
|
// Protocol data
|
|
6814
7064
|
await self.handleNotifyData(data, from: peripheral)
|
|
7065
|
+
} else if characteristic.uuid == G2BLE.FILE_NOTIFY {
|
|
7066
|
+
// File-transfer responses (CACHE_HIT/MISS, transfer status) —
|
|
7067
|
+
// observability for the notify-whitelist push.
|
|
7068
|
+
let side = peripheral === self.leftPeripheral ? "L" : "R"
|
|
7069
|
+
Bridge.log(
|
|
7070
|
+
"G2: FILE notify (\(side)): \(data.map { String(format: "%02X", $0) }.joined())"
|
|
7071
|
+
)
|
|
6815
7072
|
}
|
|
6816
7073
|
}
|
|
6817
7074
|
}
|