@tempivo/sensor-beacon 0.3.2 → 0.4.3

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 (45) hide show
  1. package/README.md +22 -11
  2. package/android/build.gradle +13 -5
  3. package/android/src/main/AndroidManifest.xml +2 -1
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +6 -16
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +13 -6
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +17 -6
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +34 -124
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +99 -18
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +165 -65
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -17
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +59 -2
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -17
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +9 -38
  15. package/dist/decoder.d.ts +0 -2
  16. package/dist/decoder.js +0 -17
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +3 -3
  19. package/dist/native-helpers.d.ts +0 -1
  20. package/dist/native-helpers.js +3 -18
  21. package/dist/native-module.js +5 -2
  22. package/dist/normalize.d.ts +6 -0
  23. package/dist/normalize.js +29 -0
  24. package/dist/profile.d.ts +5 -0
  25. package/dist/profile.js +47 -1
  26. package/dist/qr.d.ts +4 -7
  27. package/dist/qr.js +9 -24
  28. package/dist/react-native.d.ts +24 -1
  29. package/dist/react-native.js +22 -1
  30. package/dist/session-types.d.ts +11 -4
  31. package/dist/tempivo-sensor-beacon.aar +0 -0
  32. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
  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 +1 -3
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  36. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +12 -2
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +4 -80
  38. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +0 -15
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +1 -15
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +57 -3
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +9 -23
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +10 -36
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +10 -2
  44. package/ios/TempivoSensorBeaconModule.swift +139 -25
  45. package/package.json +6 -1
@@ -1,10 +1,6 @@
1
1
  import CoreBluetooth
2
2
  import Foundation
3
3
 
4
- #if canImport(TempivoSensorBridge)
5
- import TempivoSensorBridge
6
- #endif
7
-
8
4
  public protocol TempivoSensorBeaconScannerDelegate: AnyObject {
9
5
  func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didDiscover discovery: SensorBeaconDiscovery)
10
6
  func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didUpdateState state: CBManagerState)
@@ -14,21 +10,15 @@ public extension TempivoSensorBeaconScannerDelegate {
14
10
  func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didUpdateState state: CBManagerState) {}
15
11
  }
16
12
 
17
- /// Active BLE scan. iPhone uses the same hardware runtime scan as Tempivo.
18
- /// Manufacturer-only CoreBluetooth misses HC7 / FW 7+ on iPhone.
13
+ /// Active BLE scan for Tempivo sensor beacons (manufacturer `0x026C`). Advertising only. No GATT or connect.
19
14
  public final class TempivoSensorBeaconScanner: NSObject {
20
15
 
21
16
  public weak var delegate: TempivoSensorBeaconScannerDelegate?
22
17
 
23
18
  private var central: CBCentralManager!
24
19
  private var frameCache = SensorBeaconFrameCache()
25
- private var lastSeen: [String: TempivoSensorSeenDevice] = [:]
26
20
  private var isScanning = false
27
21
  private var pendingStart = false
28
- #if canImport(TempivoSensorBridge)
29
- private let runtime = TempivoSensorBridge()
30
- private var useRuntimeScan: Bool { !ProcessInfo.processInfo.isiOSAppOnMac }
31
- #endif
32
22
 
33
23
  public override init() {
34
24
  super.init()
@@ -46,25 +36,10 @@ public final class TempivoSensorBeaconScanner: NSObject {
46
36
  /// Clears cached `0x03` / `0x04` frames. Does not stop an active scan.
47
37
  public func clearCache() {
48
38
  frameCache.clear()
49
- lastSeen.removeAll()
50
- }
51
-
52
- public func lookup(serial: String) -> TempivoSensorSeenDevice? {
53
- lastSeen[TempivoSensorBeaconDecoder.macKey(serial)]
54
39
  }
55
40
 
56
- /// Starts an active scan. iPhone: hardware runtime. Mac: manufacturer advertisements.
41
+ /// Starts an active scan (duplicate advertisements allowed). Requires Bluetooth powered on.
57
42
  public func startScanning() {
58
- #if canImport(TempivoSensorBridge)
59
- if useRuntimeScan {
60
- runtime.initialize()
61
- isScanning = true
62
- runtime.startDeviceScan { [weak self] json in
63
- self?.handleRuntimeScanJson(json)
64
- }
65
- return
66
- }
67
- #endif
68
43
  pendingStart = true
69
44
  if central.state == .poweredOn {
70
45
  beginScanningIfNeeded()
@@ -74,12 +49,8 @@ public final class TempivoSensorBeaconScanner: NSObject {
74
49
  /// Stops scanning and clears pending start.
75
50
  public func stopScanning() {
76
51
  pendingStart = false
77
- #if canImport(TempivoSensorBridge)
78
- runtime.stopDeviceScan()
79
- #endif
80
- if isScanning {
81
- central.stopScan()
82
- }
52
+ guard isScanning else { return }
53
+ central.stopScan()
83
54
  isScanning = false
84
55
  }
85
56
 
@@ -162,55 +133,8 @@ public final class TempivoSensorBeaconScanner: NSObject {
162
133
  bluetoothMacAddress: bluetoothMacAddress,
163
134
  reading: reading
164
135
  )
165
- if let serial = (serialNumber ?? reading?.serialNumber).map({ TempivoSensorBeaconDecoder.macKey($0) }),
166
- !serial.isEmpty {
167
- let seen = TempivoSensorSeenDevice(
168
- deviceId: deviceId,
169
- bluetoothMac: bluetoothMacAddress ?? reading?.bluetoothMacAddress,
170
- serial: serial,
171
- firmware: reading?.firmware
172
- )
173
- lastSeen[serial] = seen
174
- }
175
136
  delegate?.sensorBeaconScanner(self, didDiscover: discovery)
176
137
  }
177
-
178
- private func handleRuntimeScanJson(_ raw: String) {
179
- guard let data = raw.data(using: .utf8),
180
- let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
181
- else { return }
182
- let deviceId = (obj["deviceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
183
- let serial = (obj["serialNumber"] as? String).map { TempivoSensorBeaconDecoder.macKey($0) } ?? ""
184
- guard !deviceId.isEmpty, !serial.isEmpty else { return }
185
- let mac = obj["bluetoothMacAddress"] as? String
186
- let rssi = (obj["rssi"] as? NSNumber)?.intValue ?? (obj["rssi"] as? Int) ?? 0
187
- let firmware = obj["firmware"] as? String
188
- let reading = SensorBeaconReading(
189
- serialNumber: serial,
190
- bluetoothMacAddress: mac,
191
- firmware: firmware
192
- )
193
- rememberSeen(deviceId: deviceId, bluetoothMac: mac, serial: serial, firmware: firmware)
194
- delegate?.sensorBeaconScanner(
195
- self,
196
- didDiscover: SensorBeaconDiscovery(
197
- deviceId: deviceId,
198
- rssi: rssi,
199
- serialNumber: serial,
200
- bluetoothMacAddress: mac,
201
- reading: reading
202
- )
203
- )
204
- }
205
-
206
- private func rememberSeen(deviceId: String, bluetoothMac: String?, serial: String, firmware: String?) {
207
- lastSeen[serial] = TempivoSensorSeenDevice(
208
- deviceId: deviceId,
209
- bluetoothMac: bluetoothMac,
210
- serial: serial,
211
- firmware: firmware
212
- )
213
- }
214
138
  }
215
139
 
216
140
  extension TempivoSensorBeaconScanner: CBCentralManagerDelegate {
@@ -101,18 +101,3 @@ public struct SensorBeaconDiscovery: Sendable, Equatable {
101
101
  self.reading = reading
102
102
  }
103
103
  }
104
-
105
- /// Last advertisement used to resolve GATT connect (scan device id + firmware).
106
- public struct TempivoSensorSeenDevice: Sendable, Equatable {
107
- public let deviceId: String
108
- public let bluetoothMac: String?
109
- public let serial: String
110
- public let firmware: String?
111
-
112
- public init(deviceId: String, bluetoothMac: String?, serial: String, firmware: String?) {
113
- self.deviceId = deviceId
114
- self.bluetoothMac = bluetoothMac
115
- self.serial = serial
116
- self.firmware = firmware
117
- }
118
- }
@@ -17,21 +17,7 @@ public struct TempivoSensorError: Error, LocalizedError, Sendable {
17
17
 
18
18
  public init(_ code: TempivoSensorErrorCode, _ message: String) {
19
19
  self.code = code
20
- self.message = Self.sanitizeClientMessage(message)
21
- }
22
-
23
- public static func sanitizeClientMessage(_ raw: String) -> String {
24
- var text = raw
25
- let full = NSRange(location: 0, length: text.utf16.count)
26
- if let regex = try? NSRegularExpression(pattern: #"(?i)pl\.[A-Za-z][A-Za-z0-9_.$]*"#) {
27
- text = regex.stringByReplacingMatches(in: text, range: full, withTemplate: "")
28
- }
29
- let full2 = NSRange(location: 0, length: text.utf16.count)
30
- if let regex = try? NSRegularExpression(pattern: #"(?i)\befento[A-Za-z0-9_$]*"#) {
31
- text = regex.stringByReplacingMatches(in: text, range: full2, withTemplate: "")
32
- }
33
- text = text.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
34
- return text.trimmingCharacters(in: CharacterSet(charactersIn: " :-.\n\t/"))
20
+ self.message = message
35
21
  }
36
22
 
37
23
  public var errorDescription: String? { message }
@@ -16,7 +16,11 @@ public enum TempivoSensorProfile {
16
16
  }
17
17
  let body: [String: Any]
18
18
  if let nested = obj["profile"] as? [String: Any],
19
- nested["temperatureAlerts"] != nil || nested["schedule"] != nil || nested["slug"] != nil {
19
+ nested["temperatureAlerts"] != nil ||
20
+ nested["schedule"] != nil ||
21
+ nested["slug"] != nil ||
22
+ nested["measurementIntervalMinutes"] != nil ||
23
+ nested["transmissionIntervalSeconds"] != nil {
20
24
  body = nested
21
25
  } else {
22
26
  body = obj
@@ -26,7 +30,13 @@ public enum TempivoSensorProfile {
26
30
  }
27
31
  let rawAlerts = body["temperatureAlerts"] as? [Any] ?? []
28
32
  let alerts = try rawAlerts.enumerated().map { try parseAlert($0.element, index: $0.offset) }
29
- return TempivoSensorConfiguration(temperatureAlerts: alerts, schedule: try parseSchedule(body["schedule"] as? [String: Any]))
33
+ let intervals = try parseOptionalIntervals(body)
34
+ return TempivoSensorConfiguration(
35
+ temperatureAlerts: alerts,
36
+ schedule: try parseSchedule(body["schedule"] as? [String: Any]),
37
+ measurementIntervalMinutes: intervals.measurement,
38
+ transmissionIntervalSeconds: intervals.transmission
39
+ )
30
40
  }
31
41
 
32
42
  public static func jsonString(_ cfg: TempivoSensorConfiguration) throws -> String {
@@ -56,11 +66,55 @@ public enum TempivoSensorProfile {
56
66
  "utcOffsetMinutes": cfg.schedule.utcOffsetMinutes,
57
67
  ]
58
68
  }
59
- let root: [String: Any] = ["temperatureAlerts": alerts, "schedule": schedule]
69
+ var root: [String: Any] = ["temperatureAlerts": alerts, "schedule": schedule]
70
+ if let m = cfg.measurementIntervalMinutes { root["measurementIntervalMinutes"] = m }
71
+ if let t = cfg.transmissionIntervalSeconds { root["transmissionIntervalSeconds"] = t }
60
72
  let data = try JSONSerialization.data(withJSONObject: root, options: [])
61
73
  return String(data: data, encoding: .utf8) ?? "{}"
62
74
  }
63
75
 
76
+ private static let minMeasurementMinutes: Double = 60
77
+ private static let maxMeasurementMinutes: Double = 600
78
+ private static let minTransmissionSeconds = 3600
79
+ private static let maxTransmissionSeconds = 604_800
80
+
81
+ private static func parseOptionalIntervals(_ body: [String: Any]) throws -> (measurement: Double?, transmission: Int?) {
82
+ var measurement: Double?
83
+ var transmission: Int?
84
+ if body["measurementIntervalMinutes"] != nil {
85
+ guard let n = number(body["measurementIntervalMinutes"]), n > 0 else {
86
+ throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be a positive number.")
87
+ }
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)).")
93
+ }
94
+ measurement = n
95
+ }
96
+ if body["transmissionIntervalSeconds"] != nil {
97
+ guard let n = number(body["transmissionIntervalSeconds"]), n > 0, n == floor(n) else {
98
+ throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be a positive integer.")
99
+ }
100
+ 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).")
106
+ }
107
+ transmission = sec
108
+ }
109
+ if let measurement, let transmission {
110
+ let maxTx = measurement * 60 * 60
111
+ if Double(transmission) > maxTx {
112
+ throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most 60 times the measurement interval.")
113
+ }
114
+ }
115
+ return (measurement, transmission)
116
+ }
117
+
64
118
  private static func parseAlert(_ raw: Any, index: Int) throws -> TempivoTemperatureAlert {
65
119
  guard let item = raw as? [String: Any] else {
66
120
  throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)] must be an object.")
@@ -29,30 +29,15 @@ public enum TempivoSensorQrParser {
29
29
 
30
30
  public static let defaultModel = "HC7"
31
31
 
32
- public static func sessionType(fromModel model: String?) -> TempivoSensorSessionType {
33
- sessionType(fromModel: model, firmware: nil)
32
+ public static func assertSupportedModel(_ model: String) throws {
33
+ let m = model.trimmingCharacters(in: .whitespacesAndNewlines).uppercased().replacingOccurrences(of: "-", with: "")
34
+ if m == "HC5" {
35
+ throw TempivoSensorError(.invalidQr, "HC5 is not supported by the partner SDK. Use HC7 sensors.")
36
+ }
34
37
  }
35
38
 
36
- public static func sessionType(fromModel model: String?, firmware: String?) -> TempivoSensorSessionType {
37
- if let firmware {
38
- let t = firmware.trimmingCharacters(in: .whitespacesAndNewlines)
39
- if t.range(of: "^FW\\s*5", options: [.regularExpression, .caseInsensitive]) != nil {
40
- return .legacy
41
- }
42
- if let match = t.range(of: "^(\\d+)\\.\\d+", options: .regularExpression) {
43
- let majorStr = String(t[match]).split(separator: ".").first.flatMap { Int($0) }
44
- if let major = majorStr {
45
- if major >= 7 { return .modern }
46
- if major >= 1 { return .legacy }
47
- }
48
- }
49
- }
50
- guard let model, !model.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
51
- return .modern
52
- }
53
- let m = model.trimmingCharacters(in: .whitespacesAndNewlines).uppercased().replacingOccurrences(of: "-", with: "")
54
- if m == "HC5" || m.hasPrefix("6") { return .legacy }
55
- return .modern
39
+ public static func sessionType(fromModel _: String?) -> TempivoSensorSessionType {
40
+ .modern
56
41
  }
57
42
 
58
43
  public static func parse(_ json: String) throws -> TempivoSensorQr {
@@ -76,12 +61,13 @@ public enum TempivoSensorQrParser {
76
61
  }
77
62
  let rawModel = (obj["model"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
78
63
  let model = rawModel.isEmpty ? defaultModel : rawModel
64
+ try assertSupportedModel(model)
79
65
  let hex = serial.count == 12 ? serial : String(serial.suffix(12))
80
66
  return TempivoSensorQr(
81
67
  serial: serial,
82
68
  pin: pin,
83
69
  model: model,
84
- sessionType: sessionType(fromModel: model),
70
+ sessionType: .modern,
85
71
  bluetoothMac: try bluetoothMacFromSerial(hex)
86
72
  )
87
73
  }
@@ -15,19 +15,10 @@ public final class TempivoSensorSession: @unchecked Sendable {
15
15
 
16
16
  public init() {}
17
17
 
18
- public func connect(
19
- serial: String,
20
- bluetoothMac: String,
21
- pin: String,
22
- sessionType: TempivoSensorSessionType = .modern,
23
- deviceId: String? = nil,
24
- firmware: String? = nil
25
- ) throws {
26
- let trimmedPin = pin.trimmingCharacters(in: .whitespacesAndNewlines)
27
- guard let pinInt = Int32(trimmedPin) else {
18
+ public func connect(serial: String, bluetoothMac: String, pin: String) throws {
19
+ guard let pinInt = Int32(pin.trimmingCharacters(in: .whitespacesAndNewlines)) else {
28
20
  throw TempivoSensorError(.invalidPin, "PIN must be numeric.")
29
21
  }
30
- let resolved = TempivoSensorQrParser.sessionType(fromModel: sessionType == .legacy ? "HC5" : "HC7", firmware: firmware)
31
22
  #if canImport(TempivoSensorBridge)
32
23
  lock.lock()
33
24
  defer { lock.unlock() }
@@ -37,9 +28,9 @@ public final class TempivoSensorSession: @unchecked Sendable {
37
28
  serial: TempivoSensorQrParser.normalizeSerial(serial),
38
29
  bluetoothMac: bluetoothMac,
39
30
  pin: pinInt,
40
- legacy: resolved == .legacy,
41
- deviceId: deviceId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "",
42
- encryptionKey: trimmedPin
31
+ legacy: false,
32
+ deviceId: TempivoSensorQrParser.normalizeSerial(serial),
33
+ encryptionKey: ""
43
34
  )
44
35
  )
45
36
  try throwIfBridgeError(result)
@@ -49,22 +40,8 @@ public final class TempivoSensorSession: @unchecked Sendable {
49
40
  #endif
50
41
  }
51
42
 
52
- public func connect(qr: TempivoSensorQr, seen: TempivoSensorSeenDevice? = nil) throws {
53
- let deviceId = seen?.deviceId.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
54
- if deviceId.isEmpty {
55
- throw TempivoSensorError(
56
- .connectFailed,
57
- "Sensor not in last scan. Scan first, then connect."
58
- )
59
- }
60
- try connect(
61
- serial: qr.serial,
62
- bluetoothMac: seen?.bluetoothMac ?? qr.bluetoothMac,
63
- pin: qr.pin,
64
- sessionType: qr.sessionType,
65
- deviceId: deviceId,
66
- firmware: seen?.firmware
67
- )
43
+ public func connect(qr: TempivoSensorQr) throws {
44
+ try connect(serial: qr.serial, bluetoothMac: qr.bluetoothMac, pin: qr.pin)
68
45
  }
69
46
 
70
47
  public func disconnect() {
@@ -173,17 +150,14 @@ public final class TempivoSensorSession: @unchecked Sendable {
173
150
 
174
151
  private func throwIfBridgeError(_ result: [String: Any]) throws {
175
152
  guard let code = intValue(result["errorCode"]), code != 0 else { return }
176
- let detail = TempivoSensorError.sanitizeClientMessage(
177
- (result["message"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
178
- )
179
153
  switch code {
180
154
  case 1: throw TempivoSensorError(.invalidPin, "Invalid PIN.")
181
155
  case 2: throw TempivoSensorError(.notConnected, "Not connected.")
182
156
  case 3: throw TempivoSensorError(.unsupportedCommand, "Unsupported command.")
183
157
  case 4: throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT is not available on this device.")
184
- case 5: throw TempivoSensorError(.connectFailed, detail.isEmpty ? "Could not connect." : "Could not connect. \(detail)")
185
- case 6: throw TempivoSensorError(.invalidConfig, detail.isEmpty ? "Invalid configuration." : detail)
186
- default: throw TempivoSensorError(.unknown, detail.isEmpty ? "Sensor request failed." : "Sensor request failed. \(detail)")
158
+ case 5: throw TempivoSensorError(.connectFailed, "Could not connect.")
159
+ case 6: throw TempivoSensorError(.invalidConfig, (result["message"] as? String) ?? "Invalid configuration.")
160
+ default: throw TempivoSensorError(.unknown, "Sensor request failed.")
187
161
  }
188
162
  }
189
163
  }
@@ -2,7 +2,6 @@ import Foundation
2
2
 
3
3
  public enum TempivoSensorSessionType: String, Sendable {
4
4
  case modern
5
- case legacy
6
5
  }
7
6
 
8
7
  public enum TempivoTemperatureChannel: String, Sendable {
@@ -65,10 +64,19 @@ public struct TempivoSchedule: Sendable, Equatable {
65
64
  public struct TempivoSensorConfiguration: Sendable, Equatable {
66
65
  public var temperatureAlerts: [TempivoTemperatureAlert]
67
66
  public var schedule: TempivoSchedule
67
+ public var measurementIntervalMinutes: Double?
68
+ public var transmissionIntervalSeconds: Int?
68
69
 
69
- public init(temperatureAlerts: [TempivoTemperatureAlert], schedule: TempivoSchedule) {
70
+ public init(
71
+ temperatureAlerts: [TempivoTemperatureAlert],
72
+ schedule: TempivoSchedule,
73
+ measurementIntervalMinutes: Double? = nil,
74
+ transmissionIntervalSeconds: Int? = nil
75
+ ) {
70
76
  self.temperatureAlerts = temperatureAlerts
71
77
  self.schedule = schedule
78
+ self.measurementIntervalMinutes = measurementIntervalMinutes
79
+ self.transmissionIntervalSeconds = transmissionIntervalSeconds
72
80
  }
73
81
  }
74
82
 
@@ -1,8 +1,18 @@
1
1
  import CoreBluetooth
2
2
  import ExpoModulesCore
3
+ import Foundation
3
4
 
4
- public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelegate {
5
+ #if canImport(TempivoSensorBridge)
6
+ import TempivoSensorBridge
7
+ #endif
8
+
9
+ public class TempivoSensorBeaconModule: Module {
10
+ #if canImport(TempivoSensorBridge)
11
+ private let scanBridge = TempivoSensorBridge()
12
+ private var sdkScanActive = false
13
+ #else
5
14
  private var scanner: TempivoSensorBeaconScanner?
15
+ #endif
6
16
  private let session = TempivoSensorSession()
7
17
 
8
18
  public func definition() -> ModuleDefinition {
@@ -10,31 +20,49 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
10
20
  Events("onDeviceFound")
11
21
 
12
22
  AsyncFunction("requestPermissions") { () -> [String: Any] in
13
- _ = self.requireScanner()
23
+ #if canImport(TempivoSensorBridge)
24
+ self.scanBridge.initialize()
25
+ #else
26
+ _ = self.requireCbScanner()
27
+ #endif
14
28
  return self.permissionPayload()
15
29
  }
16
30
 
17
31
  AsyncFunction("startScan") {
18
- self.requireScanner().startScanning()
32
+ #if canImport(TempivoSensorBridge)
33
+ self.scanBridge.initialize()
34
+ 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)
39
+ }
40
+ #else
41
+ self.requireCbScanner().startScanning()
42
+ #endif
19
43
  }
20
44
 
21
45
  Function("stopScan") {
46
+ #if canImport(TempivoSensorBridge)
47
+ self.sdkScanActive = false
48
+ self.scanBridge.stopDeviceScan()
49
+ #else
22
50
  self.scanner?.stopScanning()
51
+ #endif
23
52
  }
24
53
 
25
54
  Function("isScanning") { () -> Bool in
26
- self.scanner?.scanning ?? false
55
+ #if canImport(TempivoSensorBridge)
56
+ return self.sdkScanActive
57
+ #else
58
+ return self.scanner?.scanning ?? false
59
+ #endif
27
60
  }
28
61
 
29
62
  AsyncFunction("connect") { (qrJson: String) -> [String: Any] in
30
63
  do {
31
- self.scanner?.stopScanning()
32
- if !Thread.isMainThread {
33
- Thread.sleep(forTimeInterval: 0.2)
34
- }
35
64
  let qr = try TempivoSensorQrParser.parse(qrJson)
36
- let seen = self.scanner?.lookup(serial: qr.serial)
37
- try self.session.connect(qr: qr, seen: seen)
65
+ try self.session.connect(qr: qr)
38
66
  return self.qrPayload(qr)
39
67
  } catch {
40
68
  throw self.mapError(error)
@@ -87,19 +115,18 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
87
115
  }
88
116
 
89
117
  OnDestroy {
118
+ #if canImport(TempivoSensorBridge)
119
+ self.sdkScanActive = false
120
+ self.scanBridge.stopDeviceScan()
121
+ #else
90
122
  self.scanner?.stopScanning()
123
+ #endif
91
124
  self.session.disconnect()
92
125
  }
93
126
  }
94
127
 
95
- public func sensorBeaconScanner(
96
- _ scanner: TempivoSensorBeaconScanner,
97
- didDiscover discovery: SensorBeaconDiscovery
98
- ) {
99
- sendEvent("onDeviceFound", devicePayload(discovery))
100
- }
101
-
102
- private func requireScanner() -> TempivoSensorBeaconScanner {
128
+ #if !canImport(TempivoSensorBridge)
129
+ private func requireCbScanner() -> TempivoSensorBeaconScanner {
103
130
  if let scanner {
104
131
  return scanner
105
132
  }
@@ -108,11 +135,91 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
108
135
  scanner = created
109
136
  return created
110
137
  }
138
+ #endif
139
+
140
+ #if canImport(TempivoSensorBridge)
141
+ private func devicePayloadFromBridgeJson(_ json: String) -> [String: Any]? {
142
+ guard let data = json.data(using: .utf8),
143
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
144
+ return nil
145
+ }
146
+ guard let deviceId = dict["deviceId"] as? String else { return nil }
147
+ var payload: [String: Any] = ["deviceId": deviceId]
148
+ if let rssi = dict["rssi"] as? Int {
149
+ payload["rssi"] = rssi
150
+ } else if let rssi = dict["rssi"] as? Double {
151
+ payload["rssi"] = Int(rssi)
152
+ }
153
+ if let serial = dict["serialNumber"] as? String {
154
+ payload["serialNumber"] = serial
155
+ }
156
+ if let mac = dict["bluetoothMacAddress"] as? String {
157
+ payload["bluetoothMacAddress"] = mac
158
+ }
159
+ if let summary = dict["advertisementSummary"] as? String, !summary.isEmpty {
160
+ payload["summary"] = summary
161
+ }
162
+ if let telem = dict["advertisementTelemetry"] as? [String: Any] {
163
+ payload["telemetry"] = telemetryPayloadFromBridge(telem)
164
+ if payload["summary"] == nil, let s = telem["summary"] as? String, !s.isEmpty {
165
+ payload["summary"] = s
166
+ }
167
+ }
168
+ if payload["summary"] == nil {
169
+ payload["summary"] = payload["serialNumber"] as? String ?? deviceId
170
+ }
171
+ return payload
172
+ }
173
+
174
+ private func telemetryPayloadFromBridge(_ telem: [String: Any]) -> [String: Any] {
175
+ var payload: [String: Any] = [:]
176
+ if let firmware = telem["firmware"] as? String { payload["firmware"] = firmware }
177
+ if let batteryOk = telem["batteryOk"] as? Bool { payload["batteryOk"] = batteryOk }
178
+ if let encryptionEnabled = telem["encryptionEnabled"] as? Bool {
179
+ payload["encryptionEnabled"] = encryptionEnabled
180
+ }
181
+ if let cellularStatus = telem["cellularStatus"] as? String {
182
+ payload["cellularStatus"] = cellularStatus
183
+ }
184
+ if let measurementCounter = telem["measurementCounter"] as? Int {
185
+ payload["measurementCounter"] = measurementCounter
186
+ } else if let measurementCounter = telem["measurementCounter"] as? Int64 {
187
+ payload["measurementCounter"] = Int(measurementCounter)
188
+ }
189
+ if let readingTimestampUnix = telem["readingTimestampUnix"] as? Int {
190
+ payload["readingTimestampUnix"] = readingTimestampUnix
191
+ } else if let readingTimestampUnix = telem["readingTimestampUnix"] as? Int64 {
192
+ payload["readingTimestampUnix"] = Int(readingTimestampUnix)
193
+ }
194
+ if let periodBase = telem["periodBaseSeconds"] as? Int,
195
+ let periodFactor = telem["periodFactor"] as? Int,
196
+ periodBase > 0 {
197
+ payload["measurementIntervalSeconds"] = periodBase * max(periodFactor, 1)
198
+ }
199
+ if let readings = telem["readings"] as? [[String: Any]] {
200
+ payload["readings"] = readings.map { row in
201
+ var out: [String: Any] = [:]
202
+ if let typeHex = row["typeHex"] as? String { out["typeHex"] = typeHex }
203
+ if let text = row["text"] as? String { out["text"] = text }
204
+ if let raw24 = row["raw24"] as? Int { out["raw24"] = raw24 }
205
+ return out
206
+ }
207
+ } else {
208
+ payload["readings"] = [] as [[String: Any]]
209
+ }
210
+ if let readingsCount = telem["readingsCount"] as? Int {
211
+ payload["readingsCount"] = readingsCount
212
+ }
213
+ if let summary = telem["summary"] as? String {
214
+ payload["summary"] = summary
215
+ }
216
+ return payload
217
+ }
218
+ #endif
111
219
 
112
220
  private func permissionPayload() -> [String: Any] {
113
221
  if #available(iOS 13.1, *) {
114
222
  let status = CBManager.authorization
115
- let granted = status == .allowedAlways
116
223
  let statusName: String
117
224
  switch status {
118
225
  case .allowedAlways: statusName = "granted"
@@ -120,7 +227,7 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
120
227
  default: statusName = "denied"
121
228
  }
122
229
  return [
123
- "granted": granted,
230
+ "granted": status == .allowedAlways || status == .notDetermined,
124
231
  "status": statusName,
125
232
  "canAskAgain": status == .notDetermined,
126
233
  "expires": "never",
@@ -194,13 +301,20 @@ public class TempivoSensorBeaconModule: Module, TempivoSensorBeaconScannerDelega
194
301
  if let sensor = error as? TempivoSensorError {
195
302
  return SensorBeaconException(code: sensor.code.rawValue, reason: sensor.message)
196
303
  }
197
- let reason = TempivoSensorError.sanitizeClientMessage(error.localizedDescription)
198
- return SensorBeaconException(
199
- code: "unknown",
200
- reason: reason.isEmpty ? "Sensor request failed." : reason
201
- )
304
+ return SensorBeaconException(code: "unknown", reason: error.localizedDescription)
305
+ }
306
+ }
307
+
308
+ #if !canImport(TempivoSensorBridge)
309
+ extension TempivoSensorBeaconModule: TempivoSensorBeaconScannerDelegate {
310
+ public func sensorBeaconScanner(
311
+ _ scanner: TempivoSensorBeaconScanner,
312
+ didDiscover discovery: SensorBeaconDiscovery
313
+ ) {
314
+ sendEvent("onDeviceFound", devicePayload(discovery))
202
315
  }
203
316
  }
317
+ #endif
204
318
 
205
319
  internal final class SensorBeaconException: Exception {
206
320
  private let customCode: String
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@tempivo/sensor-beacon",
3
- "version": "0.3.2",
3
+ "version": "0.4.3",
4
4
  "description": "Tempivo sensor BLE SDK for Expo, React Native, Android, iOS, and Node.",
5
5
  "homepage": "https://app.tempivo.com/integrations",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/tempivo/tempivo-app.git",
9
+ "directory": "packages/tempivo-sensor-beacon"
10
+ },
6
11
  "author": "Tempivo",
7
12
  "type": "module",
8
13
  "main": "dist/index.js",