@tempivo/sensor-beacon 1.1.2 → 1.1.5

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 (31) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +35 -22
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +8 -0
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +8 -0
  6. package/android-aar/library/build.gradle +5 -0
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt +780 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +124 -10
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +19 -8
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +136 -11
  11. package/android-aar/library/src/test/java/com/tempivo/sensor/beacon/PartnerBleRulesTest.kt +209 -0
  12. package/dist/native-module.d.ts +10 -0
  13. package/dist/native-module.js +46 -3
  14. package/dist/partner-dual-range-fixture.d.ts +28 -0
  15. package/dist/partner-dual-range-fixture.js +29 -0
  16. package/dist/profile.js +33 -19
  17. package/dist/proto/decode-proto-config.d.ts +3 -0
  18. package/dist/proto/decode-proto-config.js +46 -0
  19. package/dist/proto/efento-protobuf-descriptor.d.ts +964 -0
  20. package/dist/proto/efento-protobuf-descriptor.js +727 -0
  21. package/dist/proto/partner-from-proto.d.ts +9 -0
  22. package/dist/proto/partner-from-proto.js +187 -0
  23. package/dist/proto/partner-to-proto.d.ts +32 -0
  24. package/dist/proto/partner-to-proto.js +282 -0
  25. package/dist/tempivo-sensor-beacon.aar +0 -0
  26. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
  27. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  28. package/ios/Sources/TempivoSensorBeacon/EfentoRawConfigTransfer.swift +601 -0
  29. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +99 -29
  30. package/ios/TempivoSensorBeaconModule.swift +9 -0
  31. package/package.json +6 -2
@@ -4,10 +4,16 @@ import Foundation
4
4
  import TempivoSensorBridge
5
5
  #endif
6
6
 
7
- /// GATT session: connect, read/write alert rules and schedule, trigger uplink, read lab calibration date.
7
+ /// GATT session: connect (SDK), alert rules via raw ProtoConfig 0x0C/0x0D, trigger/calibration via SDK.
8
8
  public final class TempivoSensorSession: @unchecked Sendable {
9
9
  private let lock = NSLock()
10
- private var connected = false
10
+ /// Vendor SDK GATT held (needed for trigger / calibration).
11
+ private var sdkConnected = false
12
+ /// Credentials from last successful `connect` (enough for raw config).
13
+ private var lastSerial = ""
14
+ private var lastMac = ""
15
+ private var lastPin = ""
16
+ private var lastDeviceId = ""
11
17
 
12
18
  #if canImport(TempivoSensorBridge)
13
19
  private let bridge = TempivoSensorBridge()
@@ -42,7 +48,11 @@ public final class TempivoSensorSession: @unchecked Sendable {
42
48
  )
43
49
  )
44
50
  try throwIfBridgeError(result)
45
- connected = true
51
+ sdkConnected = true
52
+ lastSerial = serial
53
+ lastMac = bluetoothMac
54
+ lastPin = pinValue
55
+ lastDeviceId = resolvedDeviceId
46
56
  #else
47
57
  throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
48
58
  #endif
@@ -58,14 +68,19 @@ public final class TempivoSensorSession: @unchecked Sendable {
58
68
  }
59
69
 
60
70
  public func disconnect() {
71
+ EfentoRawConfigTransfer.closeLink()
61
72
  #if canImport(TempivoSensorBridge)
62
73
  lock.lock()
63
74
  defer { lock.unlock() }
64
75
  bridge.disconnect()
65
- connected = false
76
+ sdkConnected = false
77
+ lastSerial = ""
78
+ lastMac = ""
79
+ lastPin = ""
80
+ lastDeviceId = ""
66
81
  #else
67
82
  lock.lock()
68
- connected = false
83
+ sdkConnected = false
69
84
  lock.unlock()
70
85
  #endif
71
86
  }
@@ -74,38 +89,51 @@ public final class TempivoSensorSession: @unchecked Sendable {
74
89
  try TempivoSensorProfile.parseFromDevice(json: getConfigurationJson())
75
90
  }
76
91
 
92
+ /// Always raw GATT ProtoConfig get (0x0C). Vendor SDK getConfiguration is not used for rules.
77
93
  public func getConfigurationJson() throws -> String {
78
- #if canImport(TempivoSensorBridge)
79
- try requireConnected()
80
- let result = parseBridgeJson(bridge.getConfigurationPayloadJson())
81
- try throwIfBridgeError(result)
82
- guard let json = result["configurationJson"] as? String else {
83
- throw TempivoSensorError(.unknown, "Could not read configuration.")
94
+ let proto = try withRawConfigLink {
95
+ try EfentoRawConfigTransfer.readProtoConfig(
96
+ peripheralId: $0,
97
+ bluetoothMac: lastMac,
98
+ pin: lastPin
99
+ )
84
100
  }
85
- return json
86
- #else
87
- throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
88
- #endif
101
+ let b64 = proto.base64EncodedString()
102
+ return #"{"temperatureAlerts":[],"schedule":{"always":true},"protoBase64":"\#(b64)","configSource":"rawGatt"}"#
89
103
  }
90
104
 
91
105
  public func setConfiguration(_ configuration: TempivoSensorConfiguration) throws {
92
106
  try setConfigurationJson(TempivoSensorProfile.jsonString(configuration))
93
107
  }
94
108
 
109
+ /// Partner JSON is not written via vendor SDK. Use `writeProtoConfigBase64` from JS after encode.
95
110
  public func setConfigurationJson(_ json: String) throws {
96
111
  _ = try TempivoSensorProfile.parse(json: json)
97
- #if canImport(TempivoSensorBridge)
98
- try requireConnected()
99
- let result = parseBridgeJson(bridge.setConfigurationPayloadJson(json: json))
100
- try throwIfBridgeError(result)
101
- #else
102
- throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
103
- #endif
112
+ throw TempivoSensorError(
113
+ .unsupportedCommand,
114
+ "Use writeProtoConfigBase64 after encoding partner config to ProtoConfig."
115
+ )
116
+ }
117
+
118
+ /// Raw GATT set (0x0D): full ProtoConfig bytes as base64.
119
+ public func writeProtoConfigBase64(_ base64: String) throws {
120
+ guard let proto = Data(base64Encoded: base64), !proto.isEmpty else {
121
+ throw TempivoSensorError(.invalidConfig, "protoBase64 is missing or invalid.")
122
+ }
123
+ _ = try withRawConfigLink {
124
+ try EfentoRawConfigTransfer.writeProtoConfig(
125
+ peripheralId: $0,
126
+ bluetoothMac: lastMac,
127
+ pin: lastPin,
128
+ proto: proto
129
+ )
130
+ return Data()
131
+ }
104
132
  }
105
133
 
106
134
  public func triggerTransmission() throws -> TempivoTriggerTransmissionResult {
107
135
  #if canImport(TempivoSensorBridge)
108
- try requireConnected()
136
+ try ensureSdkConnected()
109
137
  let result = parseBridgeJson(bridge.triggerTransmissionPayloadJson())
110
138
  if let code = intValue(result["errorCode"]), code != 0 {
111
139
  try throwIfBridgeError(result)
@@ -120,7 +148,7 @@ public final class TempivoSensorSession: @unchecked Sendable {
120
148
 
121
149
  public func getCalibration() throws -> TempivoSensorCalibration {
122
150
  #if canImport(TempivoSensorBridge)
123
- try requireConnected()
151
+ try ensureSdkConnected()
124
152
  let result = parseBridgeJson(bridge.getCalibrationPayloadJson())
125
153
  try throwIfBridgeError(result)
126
154
  let ts = int64Value(result["laboratoryCalibrationTimestamp"])
@@ -130,14 +158,48 @@ public final class TempivoSensorSession: @unchecked Sendable {
130
158
  #endif
131
159
  }
132
160
 
133
- private func requireConnected() throws {
161
+ // MARK: - Raw link helpers
162
+
163
+ private func withRawConfigLink(_ body: (UUID) throws -> Data) throws -> Data {
164
+ guard !lastDeviceId.isEmpty, !lastMac.isEmpty, !lastPin.isEmpty,
165
+ let uuid = UUID(uuidString: lastDeviceId)
166
+ else {
167
+ throw TempivoSensorError(.notConnected, "Not connected. Scan and connect first.")
168
+ }
169
+ releaseSdkForRaw()
170
+ return try body(uuid)
171
+ }
172
+
173
+ private func releaseSdkForRaw() {
174
+ #if canImport(TempivoSensorBridge)
175
+ lock.lock()
176
+ let didDisconnect = sdkConnected
177
+ if sdkConnected {
178
+ bridge.disconnect()
179
+ sdkConnected = false
180
+ }
181
+ lock.unlock()
182
+ // Only wait when the SDK actually held the link; the raw link is reused after that.
183
+ if didDisconnect { Thread.sleep(forTimeInterval: 1.0) }
184
+ #endif
185
+ }
186
+
187
+ #if canImport(TempivoSensorBridge)
188
+ private func ensureSdkConnected() throws {
134
189
  lock.lock()
135
- let ok = connected
190
+ let ok = sdkConnected
191
+ let serial = lastSerial
192
+ let mac = lastMac
193
+ let pin = lastPin
194
+ let deviceId = lastDeviceId
136
195
  lock.unlock()
137
- if !ok {
196
+ if ok { return }
197
+ guard !serial.isEmpty, !mac.isEmpty, !pin.isEmpty, !deviceId.isEmpty else {
138
198
  throw TempivoSensorError(.notConnected, "Not connected.")
139
199
  }
200
+ try connect(serial: serial, bluetoothMac: mac, pin: pin, deviceId: deviceId)
140
201
  }
202
+ #endif
141
203
 
142
204
  private func parseBridgeJson(_ raw: String) -> [String: Any] {
143
205
  guard let data = raw.data(using: .utf8),
@@ -163,13 +225,21 @@ public final class TempivoSensorSession: @unchecked Sendable {
163
225
  private func throwIfBridgeError(_ result: [String: Any]) throws {
164
226
  guard let code = intValue(result["errorCode"]), code != 0 else { return }
165
227
  switch code {
166
- case 1: throw TempivoSensorError(.invalidPin, "Invalid PIN.")
228
+ case 1:
229
+ throw TempivoSensorError(
230
+ .invalidPin,
231
+ "Invalid PIN. Forget this sensor in iOS Settings → Bluetooth, then scan and connect again."
232
+ )
167
233
  case 2: throw TempivoSensorError(.notConnected, "Not connected.")
168
234
  case 3: throw TempivoSensorError(.unsupportedCommand, "Unsupported command.")
169
235
  case 4: throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT is not available on this device.")
170
236
  case 5: throw TempivoSensorError(.connectFailed, "Could not connect.")
171
237
  case 6: throw TempivoSensorError(.invalidConfig, (result["message"] as? String) ?? "Invalid configuration.")
172
- default: throw TempivoSensorError(.unknown, "Sensor request failed.")
238
+ default:
239
+ throw TempivoSensorError(
240
+ .unknown,
241
+ (result["message"] as? String) ?? "Sensor request failed."
242
+ )
173
243
  }
174
244
  }
175
245
  }
@@ -94,8 +94,17 @@ public class TempivoSensorBeaconModule: Module {
94
94
  }
95
95
  }
96
96
 
97
+ AsyncFunction("writeProtoConfigBase64") { (base64: String) in
98
+ do {
99
+ try self.session.writeProtoConfigBase64(base64)
100
+ } catch {
101
+ throw self.mapError(error)
102
+ }
103
+ }
104
+
97
105
  AsyncFunction("setConfigurationJson") { (json: String) in
98
106
  do {
107
+ // Legacy name: partner JSON must be encoded in JS then written via writeProtoConfigBase64.
99
108
  try self.session.setConfigurationJson(json)
100
109
  } catch {
101
110
  throw self.mapError(error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tempivo/sensor-beacon",
3
- "version": "1.1.2",
3
+ "version": "1.1.5",
4
4
  "description": "Tempivo sensor BLE SDK. Native scan and GATT for Expo, React Native, Android, and iOS. Node helpers decode advertisements and parse QR or config (no radio).",
5
5
  "homepage": "https://app.tempivo.com/integrations",
6
6
  "repository": {
@@ -58,7 +58,8 @@
58
58
  "build:all": "npm run build:ts && npm run build:android",
59
59
  "prepare": "npm run build:ts",
60
60
  "test": "npm run test:all",
61
- "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.mjs",
61
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.mjs --testPathIgnorePatterns=ble.integration",
62
+ "test:ble": "TEMPIVO_BLE_INTEGRATION=1 node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.mjs --testPathPattern=ble.integration",
62
63
  "test:python": "PYTHONPATH=python python3 -m pytest python/tests -q",
63
64
  "test:all": "npm run test:unit && npm run test:python",
64
65
  "test:smoke": "node scripts/smoke-test.mjs",
@@ -98,6 +99,9 @@
98
99
  "optional": true
99
100
  }
100
101
  },
102
+ "dependencies": {
103
+ "protobufjs": "^7.6.6"
104
+ },
101
105
  "devDependencies": {
102
106
  "@types/jest": "^29.5.14",
103
107
  "jest": "^29.7.0",