@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.
- package/README.md +22 -11
- package/android/build.gradle +13 -5
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +6 -16
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +13 -6
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +17 -6
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +34 -124
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +99 -18
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +165 -65
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +0 -17
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +59 -2
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -17
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +9 -38
- package/dist/decoder.d.ts +0 -2
- package/dist/decoder.js +0 -17
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/native-helpers.d.ts +0 -1
- package/dist/native-helpers.js +3 -18
- package/dist/native-module.js +5 -2
- package/dist/normalize.d.ts +6 -0
- package/dist/normalize.js +29 -0
- package/dist/profile.d.ts +5 -0
- package/dist/profile.js +47 -1
- package/dist/qr.d.ts +4 -7
- package/dist/qr.js +9 -24
- package/dist/react-native.d.ts +24 -1
- package/dist/react-native.js +22 -1
- package/dist/session-types.d.ts +11 -4
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +1 -3
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +12 -2
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +4 -80
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +0 -15
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +1 -15
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +57 -3
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +9 -23
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +10 -36
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +10 -2
- package/ios/TempivoSensorBeaconModule.swift +139 -25
- 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
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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 =
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
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
|
|
37
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
41
|
-
deviceId:
|
|
42
|
-
encryptionKey:
|
|
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
|
|
53
|
-
|
|
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,
|
|
185
|
-
case 6: throw TempivoSensorError(.invalidConfig,
|
|
186
|
-
default: throw TempivoSensorError(.unknown,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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":
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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",
|