@tempivo/sensor-beacon 0.4.3 → 0.4.7

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 (44) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +155 -5
  3. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +17 -2
  4. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
  5. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +21 -12
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +5 -31
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +1 -1
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +5 -6
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +20 -1
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +19 -7
  15. package/dist/connect-helpers.d.ts +52 -0
  16. package/dist/connect-helpers.js +153 -0
  17. package/dist/decoder.d.ts +1 -0
  18. package/dist/decoder.js +7 -1
  19. package/dist/index.d.ts +6 -2
  20. package/dist/index.js +4 -2
  21. package/dist/model.d.ts +5 -0
  22. package/dist/model.js +25 -0
  23. package/dist/native-helpers.js +5 -2
  24. package/dist/native-module.js +2 -2
  25. package/dist/native-types.d.ts +2 -0
  26. package/dist/profile.d.ts +8 -1
  27. package/dist/profile.js +42 -19
  28. package/dist/react-native.d.ts +5 -1
  29. package/dist/react-native.js +3 -1
  30. package/dist/session-types.d.ts +2 -1
  31. package/dist/tempivo-sensor-beacon.aar +0 -0
  32. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  33. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  34. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  36. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
  38. package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +38 -1
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -12
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -3
  43. package/ios/TempivoSensorBeaconModule.swift +83 -16
  44. package/package.json +3 -2
@@ -60,6 +60,8 @@ export type TempivoSensorQr = {
60
60
  /** Always `modern` (HC7 GATT). */
61
61
  sessionType: TempivoSensorSessionType;
62
62
  bluetoothMac: string;
63
+ /** SDK scan `deviceId` when it differs from sticker serial. */
64
+ deviceId?: string;
63
65
  };
64
66
  export type TempivoSensorCalibration = {
65
67
  laboratoryCalibrationDate: string | null;
@@ -67,5 +69,4 @@ export type TempivoSensorCalibration = {
67
69
  };
68
70
  export type TempivoTriggerTransmissionResult = {
69
71
  ok: boolean;
70
- supported: boolean;
71
72
  };
Binary file
@@ -6,6 +6,8 @@
6
6
  #import <Foundation/NSString.h>
7
7
  #import <Foundation/NSValue.h>
8
8
 
9
+ @class TSBSensorDeviceScanHandler;
10
+
9
11
  NS_ASSUME_NONNULL_BEGIN
10
12
  #pragma clang diagnostic push
11
13
  #pragma clang diagnostic ignored "-Wunknown-warning-option"
@@ -140,17 +142,42 @@ __attribute__((swift_name("KotlinBoolean")))
140
142
  + (instancetype)numberWithBool:(BOOL)value;
141
143
  @end
142
144
 
145
+
146
+ /**
147
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
148
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
149
+ */
150
+ __attribute__((swift_name("SensorDeviceScanHandler")))
151
+ @interface TSBSensorDeviceScanHandler : TSBBase
152
+
153
+ /**
154
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
155
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
156
+ */
157
+ - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
158
+
159
+ /**
160
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
161
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
162
+ */
163
+ + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
164
+ - (void)onDeviceDeviceJson:(NSString *)deviceJson __attribute__((swift_name("onDevice(deviceJson:)")));
165
+ @end
166
+
143
167
  __attribute__((objc_subclassing_restricted))
144
168
  __attribute__((swift_name("TempivoSensorBridge")))
145
169
  @interface TSBTempivoSensorBridge : TSBBase
146
170
  - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
147
171
  + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
148
- - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
172
+ - (NSString *)connectPayloadJsonDeviceId:(NSString *)deviceId bluetoothMac:(NSString *)bluetoothMac pin:(NSString *)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(deviceId:bluetoothMac:pin:legacy:)")));
149
173
  - (void)disconnect __attribute__((swift_name("disconnect()")));
150
174
  - (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
151
175
  - (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
152
176
  - (void)initialize __attribute__((swift_name("initialize()")));
153
177
  - (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
178
+ - (void)setDeviceScanHandlerHandler:(TSBSensorDeviceScanHandler * _Nullable)handler __attribute__((swift_name("setDeviceScanHandler(handler:)")));
179
+ - (void)startDeviceScan __attribute__((swift_name("startDeviceScan()")));
180
+ - (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
154
181
  - (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
155
182
  @end
156
183
 
@@ -6,6 +6,8 @@
6
6
  #import <Foundation/NSString.h>
7
7
  #import <Foundation/NSValue.h>
8
8
 
9
+ @class TSBSensorDeviceScanHandler;
10
+
9
11
  NS_ASSUME_NONNULL_BEGIN
10
12
  #pragma clang diagnostic push
11
13
  #pragma clang diagnostic ignored "-Wunknown-warning-option"
@@ -140,17 +142,42 @@ __attribute__((swift_name("KotlinBoolean")))
140
142
  + (instancetype)numberWithBool:(BOOL)value;
141
143
  @end
142
144
 
145
+
146
+ /**
147
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
148
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
149
+ */
150
+ __attribute__((swift_name("SensorDeviceScanHandler")))
151
+ @interface TSBSensorDeviceScanHandler : TSBBase
152
+
153
+ /**
154
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
155
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
156
+ */
157
+ - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
158
+
159
+ /**
160
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
161
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
162
+ */
163
+ + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
164
+ - (void)onDeviceDeviceJson:(NSString *)deviceJson __attribute__((swift_name("onDevice(deviceJson:)")));
165
+ @end
166
+
143
167
  __attribute__((objc_subclassing_restricted))
144
168
  __attribute__((swift_name("TempivoSensorBridge")))
145
169
  @interface TSBTempivoSensorBridge : TSBBase
146
170
  - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
147
171
  + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
148
- - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
172
+ - (NSString *)connectPayloadJsonDeviceId:(NSString *)deviceId bluetoothMac:(NSString *)bluetoothMac pin:(NSString *)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(deviceId:bluetoothMac:pin:legacy:)")));
149
173
  - (void)disconnect __attribute__((swift_name("disconnect()")));
150
174
  - (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
151
175
  - (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
152
176
  - (void)initialize __attribute__((swift_name("initialize()")));
153
177
  - (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
178
+ - (void)setDeviceScanHandlerHandler:(TSBSensorDeviceScanHandler * _Nullable)handler __attribute__((swift_name("setDeviceScanHandler(handler:)")));
179
+ - (void)startDeviceScan __attribute__((swift_name("startDeviceScan()")));
180
+ - (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
154
181
  - (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
155
182
  @end
156
183
 
@@ -250,6 +250,13 @@ public enum TempivoSensorBeaconDecoder {
250
250
  return (n >> 1) ^ (-(n & 1))
251
251
  }
252
252
 
253
+ private static func measurementTypeLabel(_ mtype: UInt8) -> String {
254
+ guard let spec = measureTypes[mtype] else {
255
+ return "type_\(String(format: "%02x", mtype))"
256
+ }
257
+ return spec.name.lowercased()
258
+ }
259
+
253
260
  private static func decodeSlot(mtype: UInt8, raw24: UInt32) -> String {
254
261
  guard let spec = measureTypes[mtype] else {
255
262
  return String(format: "0x%02X: 0x%06X", mtype, raw24)
@@ -356,7 +363,7 @@ public enum TempivoSensorBeaconDecoder {
356
363
  summaryParts.append(text)
357
364
  readings.append(
358
365
  SensorBeaconMeasurement(
359
- typeHex: String(format: "0x%02X", mtype),
366
+ typeHex: measurementTypeLabel(mtype),
360
367
  raw24: Int(raw24 & 0xFFFFFF),
361
368
  text: text
362
369
  )
@@ -17,6 +17,7 @@ public final class TempivoSensorBeaconScanner: NSObject {
17
17
 
18
18
  private var central: CBCentralManager!
19
19
  private var frameCache = SensorBeaconFrameCache()
20
+ private var peripheralIdToSerialKey: [String: String] = [:]
20
21
  private var isScanning = false
21
22
  private var pendingStart = false
22
23
 
@@ -36,6 +37,7 @@ public final class TempivoSensorBeaconScanner: NSObject {
36
37
  /// Clears cached `0x03` / `0x04` frames. Does not stop an active scan.
37
38
  public func clearCache() {
38
39
  frameCache.clear()
40
+ peripheralIdToSerialKey.removeAll(keepingCapacity: false)
39
41
  }
40
42
 
41
43
  /// Starts an active scan (duplicate advertisements allowed). Requires Bluetooth powered on.
@@ -109,13 +111,20 @@ public final class TempivoSensorBeaconScanner: NSObject {
109
111
  if let mac = TempivoSensorBeaconDecoder.macFromManufacturerData(mdata) {
110
112
  bluetoothMacAddress = mac
111
113
  serialNumber = TempivoSensorBeaconDecoder.macKey(mac)
114
+ if let serialNumber {
115
+ peripheralIdToSerialKey[deviceId] = serialNumber.uppercased()
116
+ }
112
117
  }
113
- let primaryKey = serialNumber ?? deviceId
118
+ let serialHint = serialNumber ?? peripheralIdToSerialKey[deviceId]
119
+ let primaryKey = serialHint ?? deviceId
114
120
  _ = TempivoSensorBeaconDecoder.ingestManufacturerData(
115
121
  mdata,
116
122
  primaryMacKey: primaryKey,
117
123
  cache: &frameCache
118
124
  )
125
+ if serialNumber == nil {
126
+ serialNumber = serialHint
127
+ }
119
128
  }
120
129
 
121
130
  let keys = cacheKeys(serial: serialNumber, macWithColons: bluetoothMacAddress, deviceId: deviceId)
@@ -0,0 +1,29 @@
1
+ import Foundation
2
+
3
+ enum TempivoSensorModelHint {
4
+ static let defaultModel = "HC7"
5
+
6
+ static func isMissingFirmware(_ firmware: String?) -> Bool {
7
+ let t = firmware?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
8
+ return t.isEmpty || t == "0.0.0"
9
+ }
10
+
11
+ static func fromFirmware(_ firmware: String?) -> String {
12
+ if isMissingFirmware(firmware) { return defaultModel }
13
+ let t = firmware!.trimmingCharacters(in: .whitespacesAndNewlines)
14
+ if t.range(of: "^FW 5", options: [.regularExpression, .caseInsensitive]) != nil {
15
+ return "HC5"
16
+ }
17
+ let pattern = #"^(?:FW\s*)?(\d+)\.\d+"#
18
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive),
19
+ let match = regex.firstMatch(in: t, range: NSRange(t.startIndex..., in: t)),
20
+ match.numberOfRanges > 1,
21
+ let majorRange = Range(match.range(at: 1), in: t),
22
+ let major = Int(t[majorRange]) else {
23
+ return defaultModel
24
+ }
25
+ if major >= 7 { return "HC7" }
26
+ if major >= 1 { return "HC5" }
27
+ return defaultModel
28
+ }
29
+ }
@@ -10,7 +10,24 @@ public enum TempivoSensorProfile {
10
10
  return try parse(obj)
11
11
  }
12
12
 
13
+ public static func parseFromDevice(_ obj: [String: Any]) throws -> TempivoSensorConfiguration {
14
+ try parseBody(obj, validatePartnerLimits: false)
15
+ }
16
+
17
+ public static func parseFromDevice(json: String) throws -> TempivoSensorConfiguration {
18
+ guard let data = json.data(using: .utf8),
19
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
20
+ else {
21
+ throw TempivoSensorError(.invalidConfig, "Configuration is not valid JSON.")
22
+ }
23
+ return try parseFromDevice(obj)
24
+ }
25
+
13
26
  public static func parse(_ obj: [String: Any]) throws -> TempivoSensorConfiguration {
27
+ try parseBody(obj, validatePartnerLimits: true)
28
+ }
29
+
30
+ private static func parseBody(_ obj: [String: Any], validatePartnerLimits: Bool) throws -> TempivoSensorConfiguration {
14
31
  if obj["profiles"] is [Any] {
15
32
  throw TempivoSensorError(.invalidConfig, "Pass one profile from GET /devices/config-profiles/{slug}, not the list.")
16
33
  }
@@ -30,7 +47,7 @@ public enum TempivoSensorProfile {
30
47
  }
31
48
  let rawAlerts = body["temperatureAlerts"] as? [Any] ?? []
32
49
  let alerts = try rawAlerts.enumerated().map { try parseAlert($0.element, index: $0.offset) }
33
- let intervals = try parseOptionalIntervals(body)
50
+ let intervals = try parseOptionalIntervals(body, validatePartnerLimits: validatePartnerLimits)
34
51
  return TempivoSensorConfiguration(
35
52
  temperatureAlerts: alerts,
36
53
  schedule: try parseSchedule(body["schedule"] as? [String: Any]),
@@ -78,18 +95,23 @@ public enum TempivoSensorProfile {
78
95
  private static let minTransmissionSeconds = 3600
79
96
  private static let maxTransmissionSeconds = 604_800
80
97
 
81
- private static func parseOptionalIntervals(_ body: [String: Any]) throws -> (measurement: Double?, transmission: Int?) {
98
+ private static func parseOptionalIntervals(
99
+ _ body: [String: Any],
100
+ validatePartnerLimits: Bool
101
+ ) throws -> (measurement: Double?, transmission: Int?) {
82
102
  var measurement: Double?
83
103
  var transmission: Int?
84
104
  if body["measurementIntervalMinutes"] != nil {
85
105
  guard let n = number(body["measurementIntervalMinutes"]), n > 0 else {
86
106
  throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be a positive number.")
87
107
  }
88
- if n < minMeasurementMinutes {
89
- throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at least \(Int(minMeasurementMinutes)) (1 hour).")
90
- }
91
- if n > maxMeasurementMinutes {
92
- throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at most \(Int(maxMeasurementMinutes)).")
108
+ if validatePartnerLimits {
109
+ if n < minMeasurementMinutes {
110
+ throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at least \(Int(minMeasurementMinutes)) (1 hour).")
111
+ }
112
+ if n > maxMeasurementMinutes {
113
+ throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at most \(Int(maxMeasurementMinutes)).")
114
+ }
93
115
  }
94
116
  measurement = n
95
117
  }
@@ -98,15 +120,17 @@ public enum TempivoSensorProfile {
98
120
  throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be a positive integer.")
99
121
  }
100
122
  let sec = Int(n)
101
- if sec < minTransmissionSeconds {
102
- throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at least \(minTransmissionSeconds) (1 hour).")
103
- }
104
- if sec > maxTransmissionSeconds {
105
- throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most \(maxTransmissionSeconds).")
123
+ if validatePartnerLimits {
124
+ if sec < minTransmissionSeconds {
125
+ throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at least \(minTransmissionSeconds) (1 hour).")
126
+ }
127
+ if sec > maxTransmissionSeconds {
128
+ throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most \(maxTransmissionSeconds).")
129
+ }
106
130
  }
107
131
  transmission = sec
108
132
  }
109
- if let measurement, let transmission {
133
+ if validatePartnerLimits, let measurement, let transmission {
110
134
  let maxTx = measurement * 60 * 60
111
135
  if Double(transmission) > maxTx {
112
136
  throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most 60 times the measurement interval.")
@@ -6,6 +6,23 @@ public struct TempivoSensorQr: Sendable, Equatable {
6
6
  public let model: String?
7
7
  public let sessionType: TempivoSensorSessionType
8
8
  public let bluetoothMac: String
9
+ public let deviceId: String?
10
+
11
+ public init(
12
+ serial: String,
13
+ pin: String,
14
+ model: String?,
15
+ sessionType: TempivoSensorSessionType,
16
+ bluetoothMac: String,
17
+ deviceId: String? = nil
18
+ ) {
19
+ self.serial = serial
20
+ self.pin = pin
21
+ self.model = model
22
+ self.sessionType = sessionType
23
+ self.bluetoothMac = bluetoothMac
24
+ self.deviceId = deviceId
25
+ }
9
26
  }
10
27
 
11
28
  public enum TempivoSensorQrParser {
@@ -27,6 +44,14 @@ public enum TempivoSensorQrParser {
27
44
  }.joined(separator: ":")
28
45
  }
29
46
 
47
+ public static func normalizeMac(_ value: String) throws -> String {
48
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
49
+ if trimmed.contains(":") {
50
+ return try bluetoothMacFromSerial(trimmed.replacingOccurrences(of: ":", with: ""))
51
+ }
52
+ return try bluetoothMacFromSerial(trimmed)
53
+ }
54
+
30
55
  public static let defaultModel = "HC7"
31
56
 
32
57
  public static func assertSupportedModel(_ model: String) throws {
@@ -63,12 +88,24 @@ public enum TempivoSensorQrParser {
63
88
  let model = rawModel.isEmpty ? defaultModel : rawModel
64
89
  try assertSupportedModel(model)
65
90
  let hex = serial.count == 12 ? serial : String(serial.suffix(12))
91
+ let macOverride =
92
+ (["bluetoothMac", "bluetoothMacAddress", "mac"] as [String])
93
+ .compactMap { key -> String? in
94
+ guard let raw = obj[key] as? String else { return nil }
95
+ let t = raw.trimmingCharacters(in: .whitespacesAndNewlines)
96
+ return t.isEmpty ? nil : t
97
+ }
98
+ .first
99
+ let deviceIdRaw = (obj["deviceId"] as? String)?
100
+ .trimmingCharacters(in: .whitespacesAndNewlines)
101
+ let deviceId = (deviceIdRaw?.isEmpty == false) ? deviceIdRaw : nil
66
102
  return TempivoSensorQr(
67
103
  serial: serial,
68
104
  pin: pin,
69
105
  model: model,
70
106
  sessionType: .modern,
71
- bluetoothMac: try bluetoothMacFromSerial(hex)
107
+ bluetoothMac: try macOverride.map(normalizeMac) ?? bluetoothMacFromSerial(hex),
108
+ deviceId: deviceId
72
109
  )
73
110
  }
74
111
 
@@ -15,22 +15,30 @@ public final class TempivoSensorSession: @unchecked Sendable {
15
15
 
16
16
  public init() {}
17
17
 
18
- public func connect(serial: String, bluetoothMac: String, pin: String) throws {
19
- guard let pinInt = Int32(pin.trimmingCharacters(in: .whitespacesAndNewlines)) else {
18
+ public func connect(
19
+ serial: String,
20
+ bluetoothMac: String,
21
+ pin: String,
22
+ deviceId: String? = nil
23
+ ) throws {
24
+ let pinValue = pin.trimmingCharacters(in: .whitespacesAndNewlines)
25
+ guard !pinValue.isEmpty, pinValue.range(of: "^[0-9]+$", options: .regularExpression) != nil else {
20
26
  throw TempivoSensorError(.invalidPin, "PIN must be numeric.")
21
27
  }
28
+ let trimmedDeviceId = deviceId?.trimmingCharacters(in: .whitespacesAndNewlines)
29
+ let resolvedDeviceId =
30
+ (trimmedDeviceId?.isEmpty == false ? trimmedDeviceId : nil)
31
+ ?? TempivoSensorQrParser.normalizeSerial(serial)
22
32
  #if canImport(TempivoSensorBridge)
23
33
  lock.lock()
24
34
  defer { lock.unlock() }
25
35
  bridge.initialize()
26
36
  let result = parseBridgeJson(
27
37
  bridge.connectPayloadJson(
28
- serial: TempivoSensorQrParser.normalizeSerial(serial),
38
+ deviceId: resolvedDeviceId,
29
39
  bluetoothMac: bluetoothMac,
30
- pin: pinInt,
31
- legacy: false,
32
- deviceId: TempivoSensorQrParser.normalizeSerial(serial),
33
- encryptionKey: ""
40
+ pin: pinValue,
41
+ legacy: false
34
42
  )
35
43
  )
36
44
  try throwIfBridgeError(result)
@@ -40,8 +48,13 @@ public final class TempivoSensorSession: @unchecked Sendable {
40
48
  #endif
41
49
  }
42
50
 
43
- public func connect(qr: TempivoSensorQr) throws {
44
- try connect(serial: qr.serial, bluetoothMac: qr.bluetoothMac, pin: qr.pin)
51
+ public func connect(qr: TempivoSensorQr, deviceId: String? = nil, bluetoothMac: String? = nil) throws {
52
+ try connect(
53
+ serial: qr.serial,
54
+ bluetoothMac: bluetoothMac ?? qr.bluetoothMac,
55
+ pin: qr.pin,
56
+ deviceId: deviceId ?? qr.deviceId
57
+ )
45
58
  }
46
59
 
47
60
  public func disconnect() {
@@ -58,7 +71,7 @@ public final class TempivoSensorSession: @unchecked Sendable {
58
71
  }
59
72
 
60
73
  public func getConfiguration() throws -> TempivoSensorConfiguration {
61
- try TempivoSensorProfile.parse(json: getConfigurationJson())
74
+ try TempivoSensorProfile.parseFromDevice(json: getConfigurationJson())
62
75
  }
63
76
 
64
77
  public func getConfigurationJson() throws -> String {
@@ -98,8 +111,7 @@ public final class TempivoSensorSession: @unchecked Sendable {
98
111
  try throwIfBridgeError(result)
99
112
  }
100
113
  return TempivoTriggerTransmissionResult(
101
- ok: (result["ok"] as? Bool) ?? false,
102
- supported: (result["supported"] as? Bool) ?? false
114
+ ok: (result["ok"] as? Bool) ?? false
103
115
  )
104
116
  #else
105
117
  throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
@@ -92,10 +92,8 @@ public struct TempivoSensorCalibration: Sendable, Equatable {
92
92
 
93
93
  public struct TempivoTriggerTransmissionResult: Sendable, Equatable {
94
94
  public var ok: Bool
95
- public var supported: Bool
96
95
 
97
- public init(ok: Bool, supported: Bool) {
96
+ public init(ok: Bool) {
98
97
  self.ok = ok
99
- self.supported = supported
100
98
  }
101
99
  }
@@ -9,7 +9,9 @@ import TempivoSensorBridge
9
9
  public class TempivoSensorBeaconModule: Module {
10
10
  #if canImport(TempivoSensorBridge)
11
11
  private let scanBridge = TempivoSensorBridge()
12
- private var sdkScanActive = false
12
+ private var sdkScanHandler: SensorSdkScanHandler?
13
+ fileprivate var sdkScanActive = false
14
+ private var lastSeenBySerial: [String: (deviceId: String, mac: String)] = [:]
13
15
  #else
14
16
  private var scanner: TempivoSensorBeaconScanner?
15
17
  #endif
@@ -32,11 +34,11 @@ public class TempivoSensorBeaconModule: Module {
32
34
  #if canImport(TempivoSensorBridge)
33
35
  self.scanBridge.initialize()
34
36
  self.sdkScanActive = true
35
- self.scanBridge.startDeviceScan { json in
36
- guard self.sdkScanActive else { return }
37
- guard let payload = self.devicePayloadFromBridgeJson(json) else { return }
38
- self.sendEvent("onDeviceFound", payload)
37
+ if self.sdkScanHandler == nil {
38
+ self.sdkScanHandler = SensorSdkScanHandler(module: self)
39
39
  }
40
+ self.scanBridge.setDeviceScanHandler(handler: self.sdkScanHandler)
41
+ self.scanBridge.startDeviceScan()
40
42
  #else
41
43
  self.requireCbScanner().startScanning()
42
44
  #endif
@@ -46,6 +48,7 @@ public class TempivoSensorBeaconModule: Module {
46
48
  #if canImport(TempivoSensorBridge)
47
49
  self.sdkScanActive = false
48
50
  self.scanBridge.stopDeviceScan()
51
+ self.scanBridge.setDeviceScanHandler(handler: nil)
49
52
  #else
50
53
  self.scanner?.stopScanning()
51
54
  #endif
@@ -61,8 +64,18 @@ public class TempivoSensorBeaconModule: Module {
61
64
 
62
65
  AsyncFunction("connect") { (qrJson: String) -> [String: Any] in
63
66
  do {
67
+ #if canImport(TempivoSensorBridge)
68
+ self.pauseSdkScanForGatt()
69
+ #else
70
+ self.scanner?.stopScanning()
71
+ #endif
64
72
  let qr = try TempivoSensorQrParser.parse(qrJson)
65
- try self.session.connect(qr: qr)
73
+ let resolved = self.resolveConnectTarget(qr: qr)
74
+ try self.session.connect(
75
+ qr: qr,
76
+ deviceId: resolved.deviceId,
77
+ bluetoothMac: resolved.bluetoothMac
78
+ )
66
79
  return self.qrPayload(qr)
67
80
  } catch {
68
81
  throw self.mapError(error)
@@ -92,7 +105,7 @@ public class TempivoSensorBeaconModule: Module {
92
105
  AsyncFunction("triggerTransmission") { () -> [String: Any] in
93
106
  do {
94
107
  let result = try self.session.triggerTransmission()
95
- return ["ok": result.ok, "supported": result.supported]
108
+ return ["ok": result.ok]
96
109
  } catch {
97
110
  throw self.mapError(error)
98
111
  }
@@ -101,14 +114,10 @@ public class TempivoSensorBeaconModule: Module {
101
114
  AsyncFunction("getCalibration") { () -> [String: Any] in
102
115
  do {
103
116
  let cal = try self.session.getCalibration()
104
- var payload: [String: Any] = [:]
105
- if let date = cal.laboratoryCalibrationDate {
106
- payload["laboratoryCalibrationDate"] = date
107
- }
108
- if let ts = cal.laboratoryCalibrationTimestamp {
109
- payload["laboratoryCalibrationTimestamp"] = Int(ts)
110
- }
111
- return payload
117
+ return [
118
+ "laboratoryCalibrationDate": cal.laboratoryCalibrationDate as Any,
119
+ "laboratoryCalibrationTimestamp": cal.laboratoryCalibrationTimestamp.map { Int($0) } as Any,
120
+ ]
112
121
  } catch {
113
122
  throw self.mapError(error)
114
123
  }
@@ -118,6 +127,7 @@ public class TempivoSensorBeaconModule: Module {
118
127
  #if canImport(TempivoSensorBridge)
119
128
  self.sdkScanActive = false
120
129
  self.scanBridge.stopDeviceScan()
130
+ self.scanBridge.setDeviceScanHandler(handler: nil)
121
131
  #else
122
132
  self.scanner?.stopScanning()
123
133
  #endif
@@ -138,7 +148,36 @@ public class TempivoSensorBeaconModule: Module {
138
148
  #endif
139
149
 
140
150
  #if canImport(TempivoSensorBridge)
141
- private func devicePayloadFromBridgeJson(_ json: String) -> [String: Any]? {
151
+ fileprivate func rememberSeenDevice(_ payload: [String: Any]) {
152
+ guard let deviceId = payload["deviceId"] as? String,
153
+ let mac = payload["bluetoothMacAddress"] as? String else { return }
154
+ let serial = (payload["serialNumber"] as? String)?
155
+ .trimmingCharacters(in: .whitespacesAndNewlines)
156
+ .uppercased()
157
+ if let serial, !serial.isEmpty {
158
+ lastSeenBySerial[serial] = (deviceId, mac)
159
+ }
160
+ let macKey = mac.replacingOccurrences(of: ":", with: "").uppercased()
161
+ if macKey.count == 12 {
162
+ lastSeenBySerial[macKey] = (deviceId, mac)
163
+ }
164
+ }
165
+
166
+ fileprivate func pauseSdkScanForGatt() {
167
+ sdkScanActive = false
168
+ scanBridge.stopDeviceScan()
169
+ scanBridge.setDeviceScanHandler(handler: nil)
170
+ }
171
+
172
+ private func resolveConnectTarget(qr: TempivoSensorQr) -> (deviceId: String, bluetoothMac: String) {
173
+ let serialKey = TempivoSensorQrParser.normalizeSerial(qr.serial)
174
+ let seen = lastSeenBySerial[serialKey] ?? lastSeenBySerial[String(serialKey.suffix(12))]
175
+ let deviceId = qr.deviceId ?? seen?.deviceId ?? serialKey
176
+ let mac = seen?.mac ?? qr.bluetoothMac
177
+ return (deviceId, mac)
178
+ }
179
+
180
+ fileprivate func devicePayloadFromBridgeJson(_ json: String) -> [String: Any]? {
142
181
  guard let data = json.data(using: .utf8),
143
182
  let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
144
183
  return nil
@@ -156,6 +195,14 @@ public class TempivoSensorBeaconModule: Module {
156
195
  if let mac = dict["bluetoothMacAddress"] as? String {
157
196
  payload["bluetoothMacAddress"] = mac
158
197
  }
198
+ if let model = dict["model"] as? String, !model.isEmpty {
199
+ payload["model"] = model
200
+ } else if let telem = dict["advertisementTelemetry"] as? [String: Any],
201
+ let firmware = telem["firmware"] as? String {
202
+ payload["model"] = TempivoSensorModelHint.fromFirmware(firmware)
203
+ } else {
204
+ payload["model"] = TempivoSensorModelHint.defaultModel
205
+ }
159
206
  if let summary = dict["advertisementSummary"] as? String, !summary.isEmpty {
160
207
  payload["summary"] = summary
161
208
  }
@@ -168,6 +215,7 @@ public class TempivoSensorBeaconModule: Module {
168
215
  if payload["summary"] == nil {
169
216
  payload["summary"] = payload["serialNumber"] as? String ?? deviceId
170
217
  }
218
+ rememberSeenDevice(payload)
171
219
  return payload
172
220
  }
173
221
 
@@ -268,6 +316,9 @@ public class TempivoSensorBeaconModule: Module {
268
316
  if let reading = discovery.reading {
269
317
  payload["summary"] = reading.summary
270
318
  payload["telemetry"] = telemetryPayload(reading)
319
+ payload["model"] = TempivoSensorModelHint.fromFirmware(reading.firmware)
320
+ } else {
321
+ payload["model"] = TempivoSensorModelHint.defaultModel
271
322
  }
272
323
  return payload
273
324
  }
@@ -305,6 +356,22 @@ public class TempivoSensorBeaconModule: Module {
305
356
  }
306
357
  }
307
358
 
359
+ #if canImport(TempivoSensorBridge)
360
+ private final class SensorSdkScanHandler: SensorDeviceScanHandler {
361
+ private weak var module: TempivoSensorBeaconModule?
362
+
363
+ init(module: TempivoSensorBeaconModule) {
364
+ self.module = module
365
+ }
366
+
367
+ override func onDevice(deviceJson: String) {
368
+ guard let module, module.sdkScanActive else { return }
369
+ guard let payload = module.devicePayloadFromBridgeJson(deviceJson) else { return }
370
+ module.sendEvent("onDeviceFound", payload)
371
+ }
372
+ }
373
+ #endif
374
+
308
375
  #if !canImport(TempivoSensorBridge)
309
376
  extension TempivoSensorBeaconModule: TempivoSensorBeaconScannerDelegate {
310
377
  public func sensorBeaconScanner(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tempivo/sensor-beacon",
3
- "version": "0.4.3",
3
+ "version": "0.4.7",
4
4
  "description": "Tempivo sensor BLE SDK for Expo, React Native, Android, iOS, and Node.",
5
5
  "homepage": "https://app.tempivo.com/integrations",
6
6
  "repository": {
@@ -40,7 +40,8 @@
40
40
  "expo-module.config.json",
41
41
  "ios",
42
42
  "LICENSE",
43
- "README.md"
43
+ "README.md",
44
+ "CHANGELOG.md"
44
45
  ],
45
46
  "publishConfig": {
46
47
  "access": "public"