@glassly/bluetooth-sdk 0.3.1-dev.537 → 0.3.1-dev.538

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.
@@ -3,9 +3,9 @@ package com.glassly.bluetoothsdk
3
3
  /** Generated by release CI. Do not edit in a release checkout. */
4
4
  internal object GeneratedReleaseMetadata {
5
5
  const val FAMILY_BASE_VERSION: String = "0.3.1"
6
- const val RELEASE_IDENTITY: String = "0.3.1-dev.537"
7
- const val RELEASE_SET_ID: String = "glassly-0.3.1-dev.537"
8
- const val SOURCE_COMMIT: String = "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a"
6
+ const val RELEASE_IDENTITY: String = "0.3.1-dev.538"
7
+ const val RELEASE_SET_ID: String = "glassly-0.3.1-dev.538"
8
+ const val SOURCE_COMMIT: String = "3c004fd397951d67d68c6a6dcfda389afb5a2bc0"
9
9
  const val OTA_MANIFEST_URL: String = ""
10
10
  const val OTA_MANIFEST_SHA256: String = ""
11
11
  }
@@ -49,6 +49,31 @@ private object R1BLE {
49
49
  /** `source` on emitted touch events; the same value the G2 CFW tags
50
50
  * ring input it relays with, so JS treats both paths alike. */
51
51
  const val RING_SOURCE = 2
52
+
53
+ /**
54
+ * Scan-response manufacturer data (openCFW: `0x5245` company id, the six
55
+ * address bytes in SoftDevice order, then an optional 15-byte serial).
56
+ * Android hands us the payload with the company id already stripped. The
57
+ * GAP name is `EVEN R1_` + address bytes 3,2,1, which pins the byte order;
58
+ * the returned string keeps SoftDevice order, the form the glasses have
59
+ * accepted in RING_CONNECT_INFO. Taking the LAST six bytes was wrong for
60
+ * rings with a serial (it returned ASCII digits).
61
+ */
62
+ fun macAddress(mfgBytes: ByteArray, ringId: String?): String? {
63
+ if (mfgBytes.size < 6) return null
64
+ val addr = mfgBytes.copyOfRange(0, 6)
65
+ val hex = { b: ByteArray -> b.joinToString("") { String.format("%02X", it) } }
66
+ val id = ringId?.uppercase()
67
+ val chosen = when {
68
+ id.isNullOrEmpty() -> addr
69
+ hex(byteArrayOf(addr[3], addr[2], addr[1])) == id -> addr
70
+ else -> {
71
+ val flipped = addr.reversedArray()
72
+ if (hex(byteArrayOf(flipped[3], flipped[2], flipped[1])) == id) flipped else return null
73
+ }
74
+ }
75
+ return chosen.joinToString(":") { String.format("%02X", it) }
76
+ }
52
77
  }
53
78
 
54
79
  // MARK: - Ring → phone touch report (channel 1)
@@ -483,14 +508,14 @@ class R1 : ControllerManager() {
483
508
  val mfgHex = mfgBytes?.joinToString(" ") { String.format("%02X", it) } ?: "none"
484
509
  Bridge.log("R1: Discovered: ${deviceName ?: "?"} (RSSI: ${result.rssi}) mfgData: $mfgHex")
485
510
 
486
- // Extract ring MAC from manufacturer data (last 6 bytes) and store name->MAC map.
487
- // Android's manufacturerSpecificData strips the 2-byte company ID; iOS keeps it. The
488
- // resulting "last 6 bytes" land in reversed byte order between platforms (verified in
489
- // the field), so reverse here to match iOS's stored string ("1B:08:26:8E:0E:E6").
490
- if (deviceName != null && mfgBytes != null && mfgBytes.size >= 6) {
491
- val tail = mfgBytes.copyOfRange(mfgBytes.size - 6, mfgBytes.size)
492
- val macStr = tail.joinToString(":") { String.format("%02X", it) }
493
- putRingMacInMap(deviceName, macStr)
511
+ // Extract the ring MAC from manufacturer data and store name->MAC map.
512
+ if (deviceName != null && mfgBytes != null) {
513
+ val macStr = R1BLE.macAddress(mfgBytes, extractRingId(deviceName))
514
+ if (macStr != null) {
515
+ putRingMacInMap(deviceName, macStr)
516
+ } else {
517
+ Bridge.log("R1: mfgData carries no address matching $deviceName")
518
+ }
494
519
  }
495
520
 
496
521
  // Emit discovered device
@@ -640,15 +665,11 @@ class R1 : ControllerManager() {
640
665
  }
641
666
 
642
667
  /**
643
- * One emit path for both direct parsers: dropped while the ring is
644
- * debug-routed via the glasses so a lingering phone-side report can't
645
- * double up with the CFW relay.
668
+ * One emit path for both direct parsers. The ring reports touch to a
669
+ * single peer, so whatever arrives here is the only copy — forward it
670
+ * whichever route is configured.
646
671
  */
647
672
  private fun emitGesture(gestureName: String, timestamp: Long, via: String) {
648
- if (routeViaGlasses) {
649
- Bridge.log("R1: $via $gestureName ignored (ring routed via glasses)")
650
- return
651
- }
652
673
  Bridge.log("R1: $via: $gestureName")
653
674
  Bridge.sendTouchEvent(ControllerTypes.R1, gestureName, timestamp, R1BLE.RING_SOURCE)
654
675
  }
@@ -5605,6 +5605,9 @@ class G2 : SGCManager() {
5605
5605
  private val ringViaGlasses: Boolean
5606
5606
  get() = DeviceStore.get("bluetooth", "ring_via_glasses") as? Boolean ?: false
5607
5607
 
5608
+ private var lastRingReleaseAt = 0L
5609
+ private val RING_RELEASE_INTERVAL_MS = 5_000L
5610
+
5608
5611
  /** RING_CONNECT_INFO for the paired ring. False when the glasses aren't
5609
5612
  * booted or no ring MAC is known. */
5610
5613
  private fun sendRingConnectInfo(connect: Boolean, tag: String): Boolean {
@@ -6788,11 +6791,15 @@ class G2 : SGCManager() {
6788
6791
  // source unspecified, so the id alone also marks the ring.
6789
6792
  val fromRing = eventSource == 2 || eventType == OsEventType.RING_TOUCH_DOWN
6790
6793
  if (fromRing && !ringViaGlasses) {
6791
- // The phone link owns ring input; the glasses shouldn't hold
6792
- // the ring at all, so this is a stale link — drop it again.
6793
- Bridge.log("G2: ring $gestureName via glasses ignored (ring routed via phone)")
6794
- releaseController()
6795
- return@let
6794
+ // The ring only reports touch to one peer, so this input is
6795
+ // real and must flow — but the glasses shouldn't hold the ring
6796
+ // while it is phone-routed. Ask them to let go (not per event).
6797
+ val now = System.currentTimeMillis()
6798
+ if (now - lastRingReleaseAt > RING_RELEASE_INTERVAL_MS) {
6799
+ lastRingReleaseAt = now
6800
+ Bridge.log("G2: ring $gestureName via glasses while phone-routed — releasing ring")
6801
+ releaseController()
6802
+ }
6796
6803
  }
6797
6804
 
6798
6805
  Bridge.sendTouchEvent(DeviceTypes.G2, gestureName, timestamp, eventSource)
@@ -1,9 +1,9 @@
1
1
  export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
2
2
  "schemaVersion": 1,
3
3
  "familyBaseVersion": "0.3.1",
4
- "releaseIdentity": "0.3.1-dev.537",
5
- "releaseSetId": "glassly-0.3.1-dev.537",
6
- "sourceCommit": "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a",
4
+ "releaseIdentity": "0.3.1-dev.538",
5
+ "releaseSetId": "glassly-0.3.1-dev.538",
6
+ "sourceCommit": "3c004fd397951d67d68c6a6dcfda389afb5a2bc0",
7
7
  "otaManifestUrl": null,
8
8
  "otaManifestSha256": null
9
9
  });
@@ -1 +1 @@
1
- {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.3.1\",\n \"releaseIdentity\": \"0.3.1-dev.537\",\n \"releaseSetId\": \"glassly-0.3.1-dev.537\",\n \"sourceCommit\": \"2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
1
+ {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.3.1\",\n \"releaseIdentity\": \"0.3.1-dev.538\",\n \"releaseSetId\": \"glassly-0.3.1-dev.538\",\n \"sourceCommit\": \"3c004fd397951d67d68c6a6dcfda389afb5a2bc0\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
11
11
  static let voiceActivityDetectionEnabled = false
12
12
  static let loudnessGateEnabled = true
13
13
  private static let infoSdkVersionKey = "BluetoothSdkVersion"
14
- private static let swiftPackageSdkVersion = "0.3.1-dev.537"
14
+ private static let swiftPackageSdkVersion = "0.3.1-dev.538"
15
15
  private static let swiftPackageSdkVersionPlaceholder = "__GLASSLY" + "_BLUETOOTH_SDK_VERSION__"
16
16
 
17
17
  private static func normalizedSdkVersion(_ value: String?) -> String? {
@@ -3,9 +3,9 @@ import Foundation
3
3
  /// Generated by release CI. Do not edit in a release checkout.
4
4
  enum GeneratedReleaseMetadata {
5
5
  static let familyBaseVersion = "0.3.1"
6
- static let releaseIdentity = "0.3.1-dev.537"
7
- static let releaseSetId = "glassly-0.3.1-dev.537"
8
- static let sourceCommit = "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a"
6
+ static let releaseIdentity = "0.3.1-dev.538"
7
+ static let releaseSetId = "glassly-0.3.1-dev.538"
8
+ static let sourceCommit = "3c004fd397951d67d68c6a6dcfda389afb5a2bc0"
9
9
  static let otaManifestUrl = ""
10
10
  static let otaManifestSha256 = ""
11
11
  }
@@ -12,6 +12,24 @@ import Foundation
12
12
  // MARK: - R1 BLE Constants
13
13
 
14
14
  private enum R1BLE {
15
+ /// Scan-response manufacturer data (openCFW: `0x5245` company id, the six
16
+ /// address bytes in SoftDevice order, then an optional 15-byte serial).
17
+ /// The GAP name is `EVEN R1_` + address bytes 3,2,1, which pins the byte
18
+ /// order; the stored string keeps SoftDevice order, the form the glasses
19
+ /// have accepted in RING_CONNECT_INFO. Taking the LAST six bytes was wrong
20
+ /// for rings with a serial (it returned ASCII digits).
21
+ static func macAddress(mfgData: Data, ringId: String?) -> Data? {
22
+ let bytes = [UInt8](mfgData)
23
+ guard bytes.count >= 8, bytes[0] == 0x45, bytes[1] == 0x52 else { return nil }
24
+ let addr = Array(bytes[2 ..< 8])
25
+ guard let ringId, !ringId.isEmpty else { return Data(addr) }
26
+ let hex = { (b: [UInt8]) in b.map { String(format: "%02X", $0) }.joined() }
27
+ if hex([addr[3], addr[2], addr[1]]) == ringId.uppercased() { return Data(addr) }
28
+ let flipped = Array(addr.reversed())
29
+ if hex([flipped[3], flipped[2], flipped[1]]) == ringId.uppercased() { return Data(flipped) }
30
+ return nil
31
+ }
32
+
15
33
  /// Ring custom service
16
34
  static let SERVICE_UUID = CBUUID(string: "BAE80001-4F05-4503-8E65-3AF1F7329D1F")
17
35
 
@@ -491,14 +509,10 @@ class R1: NSObject, ControllerManager {
491
509
  applyRoute(viaGlasses: viaGlasses)
492
510
  }
493
511
 
494
- /// One emit path for both direct parsers: dropped while the ring is
495
- /// debug-routed via the glasses so a lingering phone-side report can't
496
- /// double up with the CFW relay.
512
+ /// One emit path for both direct parsers. The ring reports touch to a
513
+ /// single peer, so whatever arrives here is the only copy — forward it
514
+ /// whichever route is configured.
497
515
  private func emitGesture(_ gestureName: String, timestamp: Int64, via: String) {
498
- guard !routeViaGlasses else {
499
- Bridge.log("R1: \(via) \(gestureName) ignored (ring routed via glasses)")
500
- return
501
- }
502
516
  Bridge.log("R1: \(via): \(gestureName)")
503
517
  Bridge.sendTouchEvent(
504
518
  deviceModel: ControllerTypes.R1,
@@ -761,8 +775,10 @@ extension R1: CBCentralManagerDelegate {
761
775
  Bridge.log(
762
776
  "R1: mfgData: \(mfgData.map { String(format: "%02X", $0) }.joined(separator: " "))"
763
777
  )
764
- if mfgData.count >= 6 {
765
- self.ringMacAddressMap[name ?? ""] = Data(mfgData.suffix(6))
778
+ if let mac = R1BLE.macAddress(mfgData: mfgData, ringId: name.flatMap(self.extractRingId)) {
779
+ self.ringMacAddressMap[name ?? ""] = mac
780
+ } else {
781
+ Bridge.log("R1: mfgData carries no address matching \(name ?? "?")")
766
782
  }
767
783
  }
768
784
 
@@ -5820,6 +5820,9 @@ final class G2: NSObject, SGCManager {
5820
5820
  DeviceStore.shared.get("bluetooth", "ring_via_glasses") as? Bool ?? false
5821
5821
  }
5822
5822
 
5823
+ private var lastRingReleaseAt: TimeInterval = 0
5824
+ private let RING_RELEASE_INTERVAL_S: TimeInterval = 5
5825
+
5823
5826
  /// RING_CONNECT_INFO for the paired ring. False when the glasses aren't
5824
5827
  /// booted or no ring MAC is known.
5825
5828
  @discardableResult
@@ -7095,11 +7098,15 @@ final class G2: NSObject, SGCManager {
7095
7098
  // source unspecified, so the id alone also marks the ring.
7096
7099
  let fromRing = eventSource == 2 || eventType == .ringTouchDown
7097
7100
  if fromRing, !ringViaGlasses {
7098
- // The phone link owns ring input; the glasses shouldn't hold
7099
- // the ring at all, so this is a stale link — drop it again.
7100
- Bridge.log("G2: ring \(gestureName) via glasses ignored (ring routed via phone)")
7101
- releaseController()
7102
- return
7101
+ // The ring only reports touch to one peer, so this input is
7102
+ // real and must flow — but the glasses shouldn't hold the ring
7103
+ // while it is phone-routed. Ask them to let go (not per event).
7104
+ let now = Date().timeIntervalSince1970
7105
+ if now - lastRingReleaseAt > RING_RELEASE_INTERVAL_S {
7106
+ lastRingReleaseAt = now
7107
+ Bridge.log("G2: ring \(gestureName) via glasses while phone-routed — releasing ring")
7108
+ releaseController()
7109
+ }
7103
7110
  }
7104
7111
 
7105
7112
  Bridge.sendTouchEvent(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/bluetooth-sdk",
3
- "version": "0.3.1-dev.537",
3
+ "version": "0.3.1-dev.538",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -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.3.1",
15
- "releaseIdentity": "0.3.1-dev.537",
16
- "releaseSetId": "glassly-0.3.1-dev.537",
17
- "sourceCommit": "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a",
15
+ "releaseIdentity": "0.3.1-dev.538",
16
+ "releaseSetId": "glassly-0.3.1-dev.538",
17
+ "sourceCommit": "3c004fd397951d67d68c6a6dcfda389afb5a2bc0",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })