@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.
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
@@ -31,22 +31,97 @@ private enum R1BLE {
31
31
  /// Name filters for scanning
32
32
  static let NAME_FILTERS = ["EVEN R1", "BCL60"]
33
33
 
34
- // Init sequence config writes
35
- static let CONFIG_FC = Data([0xFC])
36
- static let CONFIG_11 = Data([0x11])
37
-
38
- // BleRing1 command header (cmd, module, subCmd) for advStart
39
- // From RE: BleRing1Cmd_system=0, BleRing1Module_system=0, BleRing1SubCmd_advStart=9
40
- static let CMD_SYSTEM: UInt8 = 0x00
41
- static let MODULE_SYSTEM: UInt8 = 0x00
42
- static let SUBCMD_ADV_START: UInt8 = 0x09
43
-
44
34
  /// Gesture protocol marker
45
35
  static let GESTURE_MARKER: UInt8 = 0xFF
46
36
 
47
37
  static let SCAN_TIMEOUT: TimeInterval = 15.0
48
38
  }
49
39
 
40
+ // MARK: - EUS transport encoder (phone → ring, channel 2)
41
+
42
+ /// Recovered firmware contract (openCFW r1/ research): every phone→ring packet
43
+ /// is a 12-byte "model" header
44
+ /// [version, module, moduleVersion, serial LE16, status, command, subcommand,
45
+ /// totalLength LE16, crc16 LE16]
46
+ /// followed by the payload, where the phone-direction CRC-16 (CCITT rounds,
47
+ /// init 0xFFFF) runs over the compact bytes [0,1,2,3,5,6,7,8,0x00] and then
48
+ /// continues over the payload bytes. The model is carried on channel 2 inside
49
+ /// a fragment train [sequence, crc32 LE32, ≤239 bytes]; the CRC-32 (poly
50
+ /// 0x1EDC6F41, MSB-first, zero seed, no final xor) covers the whole model and
51
+ /// sequence 0 terminates the train — all our packets fit a single fragment.
52
+ private enum R1Eus {
53
+ static let VERSION: UInt8 = 100
54
+ static let MODULE_SYSTEM: UInt8 = 0x01
55
+ static let MODULE_VERSION: UInt8 = 100
56
+ /// status bit 0x02 marks a SET; advStart requires it
57
+ static let STATUS_SET: UInt8 = 0x02
58
+
59
+ /// system subcommand 0x08: payload [0x01] claims the phone role; the ring
60
+ /// requests link encryption/bonding in response
61
+ static let SUBCMD_PAIR_AUTH: UInt8 = 0x08
62
+ /// system subcommand 0x0A: 12-byte payload = two 6-byte glasses arm MACs;
63
+ /// the ring stores both targets and fast-advertises for them
64
+ static let SUBCMD_ADV_START: UInt8 = 0x0A
65
+
66
+ private static func crc16Step(_ crc: UInt16, _ byte: UInt8) -> UInt16 {
67
+ var c = (crc >> 8) | (crc << 8)
68
+ c ^= UInt16(byte)
69
+ c ^= (c & 0x00FF) >> 4
70
+ c ^= c << 12
71
+ c ^= (c & 0x00FF) << 5
72
+ return c
73
+ }
74
+
75
+ private static func phoneCrc16(header: [UInt8], payload: Data) -> UInt16 {
76
+ var crc: UInt16 = 0xFFFF
77
+ // compact header: bytes 0-3 and 5-8, then a zero placeholder
78
+ for b in [header[0], header[1], header[2], header[3], header[5], header[6], header[7], header[8], 0] {
79
+ crc = crc16Step(crc, b)
80
+ }
81
+ for b in payload {
82
+ crc = crc16Step(crc, b)
83
+ }
84
+ return crc
85
+ }
86
+
87
+ private static func crc32(_ data: Data) -> UInt32 {
88
+ var crc: UInt32 = 0
89
+ for byte in data {
90
+ crc ^= UInt32(byte) << 24
91
+ for _ in 0 ..< 8 {
92
+ crc = (crc & 0x8000_0000) != 0 ? (crc << 1) ^ 0x1EDC_6F41 : crc << 1
93
+ }
94
+ }
95
+ return crc
96
+ }
97
+
98
+ /// Build one single-fragment system-module command packet.
99
+ static func frame(subcommand: UInt8, status: UInt8, serial: UInt16, payload: Data) -> Data {
100
+ let length = UInt16(12 + payload.count)
101
+ var header: [UInt8] = [
102
+ VERSION, MODULE_SYSTEM, MODULE_VERSION,
103
+ UInt8(serial & 0xFF), UInt8(serial >> 8),
104
+ status, 0x00, subcommand,
105
+ UInt8(length & 0xFF), UInt8(length >> 8),
106
+ 0, 0,
107
+ ]
108
+ let crc16 = phoneCrc16(header: header, payload: payload)
109
+ header[10] = UInt8(crc16 & 0xFF)
110
+ header[11] = UInt8(crc16 >> 8)
111
+ var model = Data(header)
112
+ model.append(payload)
113
+
114
+ let outer = crc32(model)
115
+ var packet = Data([0x00]) // sequence 0: single-fragment train
116
+ packet.append(UInt8(outer & 0xFF))
117
+ packet.append(UInt8((outer >> 8) & 0xFF))
118
+ packet.append(UInt8((outer >> 16) & 0xFF))
119
+ packet.append(UInt8((outer >> 24) & 0xFF))
120
+ packet.append(model)
121
+ return packet
122
+ }
123
+ }
124
+
50
125
  // MARK: - R1 Gesture Types
51
126
 
52
127
  private enum R1Gesture: String {
@@ -92,6 +167,7 @@ class R1: NSObject, ControllerManager {
92
167
  private var batteryLevelChar: CBCharacteristic?
93
168
  private var notifySubscriptionCount = 0
94
169
  private var initSequenceRun = false
170
+ private var eusSerial: UInt16 = 0
95
171
 
96
172
  /// Device search
97
173
  var DEVICE_SEARCH_ID = "NOT_SET"
@@ -223,67 +299,83 @@ class R1: NSObject, ControllerManager {
223
299
  return nil
224
300
  }
225
301
 
302
+ // MARK: - EUS commands
303
+
304
+ private func nextEusSerial() -> UInt16 {
305
+ eusSerial &+= 1
306
+ if eusSerial == 0 { eusSerial = 1 }
307
+ return eusSerial
308
+ }
309
+
310
+ /// EUS system commands go to WRITE_CHAR_2 only — channel 2 is the framed
311
+ /// transport; channel 1 speaks the separate legacy protocol.
312
+ private func sendEusCommand(_ subcommand: UInt8, payload: Data, log: String) {
313
+ guard let wc = writeChar2 else {
314
+ Bridge.log("R1: \(log): no channel-2 write characteristic")
315
+ return
316
+ }
317
+ let packet = R1Eus.frame(
318
+ subcommand: subcommand, status: R1Eus.STATUS_SET,
319
+ serial: nextEusSerial(), payload: payload
320
+ )
321
+ ringPeripheral?.writeValue(packet, for: wc, type: .withoutResponse)
322
+ Bridge.log("R1: \(log) sent (\(packet.count) bytes)")
323
+ }
324
+
226
325
  // MARK: - Init Sequence
227
326
 
228
- /// Runs after notify subscriptions are confirmed.
229
- /// Sends 0xFC and 0x11 config writes to initialize the ring.
327
+ /// Runs after notify subscriptions are confirmed: claim the phone role,
328
+ /// then mark the phone link ready.
230
329
  private func runInitSequence() {
231
330
  guard !initSequenceRun else { return }
232
331
  initSequenceRun = true
233
332
 
234
333
  Bridge.log("R1: Running init sequence")
235
334
 
236
- // Write init commands to both write characteristics
237
- let writeChars = [writeChar1, writeChar2].compactMap { $0 }
238
- guard !writeChars.isEmpty else {
239
- Bridge.log("R1: No write characteristics found, skipping init")
335
+ guard writeChar2 != nil else {
336
+ Bridge.log("R1: No channel-2 write characteristic, skipping init")
240
337
  markConnected()
241
338
  return
242
339
  }
243
340
 
244
- for wc in writeChars {
245
- ringPeripheral?.writeValue(R1BLE.CONFIG_FC, for: wc, type: .withoutResponse)
246
- }
341
+ sendEusCommand(R1Eus.SUBCMD_PAIR_AUTH, payload: Data([0x01]), log: "pairAuth")
247
342
 
248
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
249
- guard let self = self else { return }
250
- for wc in writeChars {
251
- self.ringPeripheral?.writeValue(R1BLE.CONFIG_11, for: wc, type: .withoutResponse)
252
- }
253
- self.markConnected()
343
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
344
+ self?.markConnected()
254
345
  }
255
346
  }
256
347
 
257
- /// Tells the ring to start advertising / connect to the glasses.
258
- /// Sends BleRing1 advStart (cmd=0, module=0, subCmd=9) with the 6-byte glasses MAC as payload
259
- /// to WRITE_CHAR_2 (BAE80012-…). Reverse-engineered from the Even Realities mobile app
260
- /// (BleRing1CmdProto::advStart -> BleRing1CmdPublicExt.sendCmd).
348
+ /// Tells the ring which glasses may connect to it: EUS advStart whose
349
+ /// 12-byte payload is the two glasses arm MACs. The ring stores both
350
+ /// targets, fast-advertises while a glasses role is open, and accepts a
351
+ /// connection from either arm. With only one arm MAC known it is sent for
352
+ /// both targets — the ring matches against either slot.
261
353
  private func connectToGlasses() {
262
- let glassesMac = (DeviceStore.shared.get("glasses", "bluetoothMacAddress") as? String)
354
+ func stored(_ key: String) -> String? {
355
+ guard let value = DeviceStore.shared.get("glasses", key) as? String, !value.isEmpty else { return nil }
356
+ return value
357
+ }
358
+ let left = stored("leftMacAddress")
359
+ ?? stored("bluetoothMacAddress")
263
360
  ?? UserDefaults.standard.string(forKey: "glasses_btMacAddress")
361
+ let right = stored("rightMacAddress")
362
+ ?? UserDefaults.standard.string(forKey: "glasses_btMacAddressRight")
264
363
 
265
- guard let glassesMac else {
364
+ guard let left, let leftBytes = parseMac(left) else {
266
365
  Bridge.log("R1: connectToGlasses: no glasses MAC")
267
366
  return
268
367
  }
368
+ let rightBytes = right.flatMap(parseMac) ?? leftBytes
269
369
 
270
- guard let macBytes = parseMac(glassesMac) else {
271
- Bridge.log("R1: connectToGlasses: could not parse glasses MAC")
272
- return
273
- }
274
-
275
- // Cache so we can reconnect even before the glasses are scanned.
276
- UserDefaults.standard.set(glassesMac, forKey: "glasses_btMacAddress")
277
-
278
- guard let wc = writeChar2 ?? writeChar1 else {
279
- Bridge.log("R1: connectToGlasses: no write characteristic")
280
- return
370
+ // Cache so we can re-send even before the glasses are next scanned.
371
+ UserDefaults.standard.set(left, forKey: "glasses_btMacAddress")
372
+ if let right {
373
+ UserDefaults.standard.set(right, forKey: "glasses_btMacAddressRight")
281
374
  }
282
375
 
283
- var payload = Data([R1BLE.CMD_SYSTEM, R1BLE.MODULE_SYSTEM, R1BLE.SUBCMD_ADV_START])
284
- payload.append(macBytes)
285
- Bridge.log("R1: advStart sent")
286
- ringPeripheral?.writeValue(payload, for: wc, type: .withoutResponse)
376
+ var payload = leftBytes
377
+ payload.append(rightBytes)
378
+ sendEusCommand(R1Eus.SUBCMD_ADV_START, payload: payload, log: "advStart")
287
379
  }
288
380
 
289
381
  /// Parse a MAC string like "AA:BB:CC:DD:EE:FF" or "AABBCCDDEEFF" into 6 raw bytes.
@@ -317,7 +409,10 @@ class R1: NSObject, ControllerManager {
317
409
  DeviceStore.shared.apply("glasses", "controllerMacAddress", mac)
318
410
 
319
411
  DeviceStore.shared.apply("glasses", "controllerConnected", true)
320
- // DeviceStore.shared.apply("glasses", "controllerFullyBooted", true)
412
+ // The phone link owns these flags: a ready phone link IS pairing
413
+ // success (fires handleControllerReady). The glasses link is layered
414
+ // on afterwards via advStart + RING_CONNECT_INFO.
415
+ DeviceStore.shared.apply("glasses", "controllerFullyBooted", true)
321
416
 
322
417
  // tell the ring to connect to the glasses if we have it's mac address:
323
418
  connectToGlasses()
@@ -420,7 +515,8 @@ class R1: NSObject, ControllerManager {
420
515
  batteryLevelChar = nil
421
516
  notifySubscriptionCount = 0
422
517
  initSequenceRun = false
423
- ringMacAddress = nil
518
+ // ringMacAddress/ringUUID persist across disconnects so reconnection
519
+ // can find the ring again; only forget() clears them.
424
520
  DeviceStore.shared.apply("glasses", "controllerConnected", false)
425
521
  DeviceStore.shared.apply("glasses", "controllerFullyBooted", false)
426
522
  }
@@ -666,7 +762,7 @@ extension R1: CBCentralManagerDelegate {
666
762
  if self.isDisconnecting { return }
667
763
 
668
764
  self.resetConnectionState()
669
- // self.startReconnectionTimer()
765
+ self.startReconnectionTimer()
670
766
  }
671
767
  }
672
768
  }