@tempivo/sensor-beacon 0.1.1 → 0.3.0
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 +222 -78
- package/android/build.gradle +38 -2
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +233 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +122 -0
- package/android-aar/build.gradle +4 -0
- package/{android → android-aar}/library/build.gradle +3 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +279 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +168 -0
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +6 -23
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +0 -2
- package/{android → android-aar}/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -4
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +36 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +18 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +171 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +82 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +127 -0
- package/{android → android-aar}/settings.gradle +1 -0
- package/app.plugin.cjs +142 -0
- package/dist/calibration.d.ts +7 -0
- package/dist/calibration.js +42 -0
- package/dist/decoder.js +4 -12
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +8 -0
- package/dist/expo-modules-core.web-stub.d.ts +5 -0
- package/dist/expo-modules-core.web-stub.js +9 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +6 -1
- package/dist/native-bridge.d.ts +14 -0
- package/dist/native-bridge.js +51 -0
- package/dist/native-helpers.d.ts +5 -0
- package/dist/native-helpers.js +33 -0
- package/dist/native-module.d.ts +14 -0
- package/dist/native-module.js +104 -0
- package/dist/native-types.d.ts +41 -0
- package/dist/native-types.js +1 -0
- package/dist/profile.d.ts +10 -0
- package/dist/profile.js +226 -0
- package/dist/qr.d.ts +16 -0
- package/dist/qr.js +90 -0
- package/dist/react-native.d.ts +1 -0
- package/dist/react-native.js +1 -0
- package/dist/session-types.d.ts +64 -0
- package/dist/session-types.js +1 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/dist/types.d.ts +3 -3
- package/expo-module.config.json +12 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +43 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +159 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Info.plist +35 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
- 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 +159 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Info.plist +31 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Modules/module.modulemap +8 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +4 -8
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +3 -6
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +22 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +24 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +123 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +78 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +161 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +93 -0
- package/ios/TempivoSensorBeacon.podspec +35 -0
- package/ios/TempivoSensorBeaconModule.swift +208 -0
- package/package.json +55 -7
- /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.jar +0 -0
- /package/{android → android-aar}/gradle/wrapper/gradle-wrapper.properties +0 -0
- /package/{android → android-aar}/gradle.properties +0 -0
- /package/{android → android-aar}/gradlew +0 -0
- /package/{android → android-aar}/library/consumer-rules.pro +0 -0
- /package/{android → android-aar}/library/src/main/AndroidManifest.xml +0 -0
package/dist/decoder.js
CHANGED
|
@@ -151,9 +151,7 @@ function fw5Summary(frame) {
|
|
|
151
151
|
return `FW 5.x ${maj}.${min} (limited broadcast decode)`;
|
|
152
152
|
}
|
|
153
153
|
function serialMacFromAdv03(frame) {
|
|
154
|
-
return
|
|
155
|
-
.map((x) => (x & 0xff).toString(16).padStart(2, '0').toUpperCase())
|
|
156
|
-
.join(':');
|
|
154
|
+
return serialKeyFromAdv03Frame(frame) ?? '';
|
|
157
155
|
}
|
|
158
156
|
function parseFw6Adv03(frame) {
|
|
159
157
|
if (frame.length < ADV_FRAME_03_LEN)
|
|
@@ -183,9 +181,7 @@ function parseFw6Adv03(frame) {
|
|
|
183
181
|
measurementCounter: ts >>> 0,
|
|
184
182
|
readingTimestampUnix: ts,
|
|
185
183
|
readingTimestampIso,
|
|
186
|
-
|
|
187
|
-
periodFactor: pfact,
|
|
188
|
-
periodLabel: `${pbase}s × ${pfact}`,
|
|
184
|
+
measurementIntervalSeconds: pbase * pfact,
|
|
189
185
|
};
|
|
190
186
|
}
|
|
191
187
|
function parseScan04(frame) {
|
|
@@ -230,9 +226,7 @@ export function buildSensorBeaconReading(adv03, adv04, rawHex = '') {
|
|
|
230
226
|
measurementCounter: null,
|
|
231
227
|
readingTimestampUnix: null,
|
|
232
228
|
readingTimestampIso: null,
|
|
233
|
-
|
|
234
|
-
periodFactor: null,
|
|
235
|
-
periodLabel: '',
|
|
229
|
+
measurementIntervalSeconds: null,
|
|
236
230
|
};
|
|
237
231
|
if (adv03?.length) {
|
|
238
232
|
const tag = u8(adv03, 0);
|
|
@@ -282,9 +276,7 @@ export function buildSensorBeaconReading(adv03, adv04, rawHex = '') {
|
|
|
282
276
|
measurementCounter: base.measurementCounter ?? null,
|
|
283
277
|
readingTimestampUnix: base.readingTimestampUnix ?? null,
|
|
284
278
|
readingTimestampIso: base.readingTimestampIso ?? null,
|
|
285
|
-
|
|
286
|
-
periodFactor: base.periodFactor ?? null,
|
|
287
|
-
periodLabel: base.periodLabel || '—',
|
|
279
|
+
measurementIntervalSeconds: base.measurementIntervalSeconds ?? null,
|
|
288
280
|
measurements,
|
|
289
281
|
summary: sumOut,
|
|
290
282
|
temperatures,
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Partner-facing error codes. Native runtimes map internal exceptions to these. */
|
|
2
|
+
export type TempivoSensorErrorCode = 'invalidPin' | 'invalidQr' | 'invalidConfig' | 'notConnected' | 'unsupportedCommand' | 'runtimeUnavailable' | 'connectFailed' | 'unknown';
|
|
3
|
+
export declare class TempivoSensorError extends Error {
|
|
4
|
+
readonly code: TempivoSensorErrorCode;
|
|
5
|
+
constructor(code: TempivoSensorErrorCode, message: string);
|
|
6
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web/Next stub for optional Expo peer. Native BLE uses the real module in Expo/RN builds.
|
|
3
|
+
*/
|
|
4
|
+
export function requireNativeModule(_name) {
|
|
5
|
+
throw new Error('expo-modules-core is not available in the web runtime');
|
|
6
|
+
}
|
|
7
|
+
export function requireOptionalNativeModule(_name) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload,
|
|
1
|
+
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
|
|
2
2
|
export { ingestManufacturerPayload } from './ingest.js';
|
|
3
3
|
export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js';
|
|
4
4
|
export { MEASURE_SPECS, type MeasureSpec } from './measure-specs.js';
|
|
5
5
|
export { createSensorBeaconFrameCache, type SensorBeaconFrameCache, type SensorBeaconMeasurement, type SensorBeaconReading, } from './types.js';
|
|
6
|
+
export { TempivoSensorError, type TempivoSensorErrorCode } from './errors.js';
|
|
7
|
+
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
|
|
8
|
+
export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
|
|
9
|
+
export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
|
|
10
|
+
export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
|
|
11
|
+
export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
|
|
12
|
+
export type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativeMeasurement, TempivoNativePermissionResponse, TempivoNativePermissionStatus, TempivoNativeQr, TempivoNativeTelemetry, TempivoSensorBeaconDevice, } from './native-types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload,
|
|
1
|
+
export { ADV_FRAME_03_LEN, TEMPVO_SENSOR_MANUFACTURER_ID, buildSensorBeaconReading, decodeMeasurementSlot, decodeSensorBeaconPayload, lookupDeviceKeys, macKeyFromAddress, serialKeyFromAdv03Frame, splitSensorBeaconFrames, } from './decoder.js';
|
|
2
2
|
export { ingestManufacturerPayload } from './ingest.js';
|
|
3
3
|
export { dataViewToUint8Array, normalizeManufacturerBytes } from './normalize.js';
|
|
4
4
|
export { MEASURE_SPECS } from './measure-specs.js';
|
|
5
5
|
export { createSensorBeaconFrameCache, } from './types.js';
|
|
6
|
+
export { TempivoSensorError } from './errors.js';
|
|
7
|
+
export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
|
|
8
|
+
export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
|
|
9
|
+
export { DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
|
|
10
|
+
export { addDeviceFoundListener, connect, disconnect, getCalibration, getConfiguration, isNativeSensorBeaconAvailable, isScanning, requestPermissions, setConfigurationJson, startScan, stopScan, triggerTransmission, } from './native-bridge.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativePermissionResponse, TempivoNativeQr, TempivoSensorBeaconDevice } from './native-types.js';
|
|
2
|
+
import type { TempivoSensorConfiguration, TempivoSensorQr, TempivoTriggerTransmissionResult } from './session-types.js';
|
|
3
|
+
export declare function isNativeSensorBeaconAvailable(): Promise<boolean>;
|
|
4
|
+
export declare function requestPermissions(): Promise<TempivoNativePermissionResponse>;
|
|
5
|
+
export declare function startScan(): Promise<void>;
|
|
6
|
+
export declare function stopScan(): Promise<void>;
|
|
7
|
+
export declare function isScanning(): Promise<boolean>;
|
|
8
|
+
export declare function addDeviceFoundListener(listener: (device: TempivoSensorBeaconDevice) => void): Promise<TempivoDeviceFoundSubscription>;
|
|
9
|
+
export declare function connect(qr: string | TempivoSensorQr): Promise<TempivoNativeQr>;
|
|
10
|
+
export declare function disconnect(): Promise<void>;
|
|
11
|
+
export declare function getConfiguration(): Promise<TempivoSensorConfiguration>;
|
|
12
|
+
export declare function setConfigurationJson(config: string | TempivoSensorConfiguration): Promise<void>;
|
|
13
|
+
export declare function triggerTransmission(): Promise<TempivoTriggerTransmissionResult>;
|
|
14
|
+
export declare function getCalibration(): Promise<TempivoNativeCalibration>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { TempivoSensorError } from './errors.js';
|
|
2
|
+
async function loadNative() {
|
|
3
|
+
try {
|
|
4
|
+
return await import('./native-module.js');
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
throw new TempivoSensorError('runtimeUnavailable', 'Native BLE needs an Expo development build or React Native with Expo modules. Expo Go is not supported.');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function isNativeSensorBeaconAvailable() {
|
|
11
|
+
try {
|
|
12
|
+
const native = await import('./native-module.js');
|
|
13
|
+
return native.isNativeSensorBeaconAvailable();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function requestPermissions() {
|
|
20
|
+
return (await loadNative()).requestPermissions();
|
|
21
|
+
}
|
|
22
|
+
export async function startScan() {
|
|
23
|
+
return (await loadNative()).startScan();
|
|
24
|
+
}
|
|
25
|
+
export async function stopScan() {
|
|
26
|
+
(await loadNative()).stopScan();
|
|
27
|
+
}
|
|
28
|
+
export async function isScanning() {
|
|
29
|
+
return (await loadNative()).isScanning();
|
|
30
|
+
}
|
|
31
|
+
export async function addDeviceFoundListener(listener) {
|
|
32
|
+
return (await loadNative()).addDeviceFoundListener(listener);
|
|
33
|
+
}
|
|
34
|
+
export async function connect(qr) {
|
|
35
|
+
return (await loadNative()).connect(qr);
|
|
36
|
+
}
|
|
37
|
+
export async function disconnect() {
|
|
38
|
+
return (await loadNative()).disconnect();
|
|
39
|
+
}
|
|
40
|
+
export async function getConfiguration() {
|
|
41
|
+
return (await loadNative()).getConfiguration();
|
|
42
|
+
}
|
|
43
|
+
export async function setConfigurationJson(config) {
|
|
44
|
+
return (await loadNative()).setConfigurationJson(config);
|
|
45
|
+
}
|
|
46
|
+
export async function triggerTransmission() {
|
|
47
|
+
return (await loadNative()).triggerTransmission();
|
|
48
|
+
}
|
|
49
|
+
export async function getCalibration() {
|
|
50
|
+
return (await loadNative()).getCalibration();
|
|
51
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { TempivoSensorError } from './errors.js';
|
|
2
|
+
import type { TempivoSensorConfiguration, TempivoSensorQr } from './session-types.js';
|
|
3
|
+
export declare function qrToConnectJson(qr: string | TempivoSensorQr): string;
|
|
4
|
+
export declare function configToJson(config: string | TempivoSensorConfiguration): string;
|
|
5
|
+
export declare function mapNativeError(error: unknown): TempivoSensorError;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TempivoSensorError } from './errors.js';
|
|
2
|
+
const SENSOR_CODES = [
|
|
3
|
+
'invalidPin',
|
|
4
|
+
'invalidQr',
|
|
5
|
+
'invalidConfig',
|
|
6
|
+
'notConnected',
|
|
7
|
+
'unsupportedCommand',
|
|
8
|
+
'runtimeUnavailable',
|
|
9
|
+
'connectFailed',
|
|
10
|
+
'unknown',
|
|
11
|
+
];
|
|
12
|
+
export function qrToConnectJson(qr) {
|
|
13
|
+
if (typeof qr === 'string')
|
|
14
|
+
return qr;
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
sn: qr.serial,
|
|
17
|
+
pin: qr.pin,
|
|
18
|
+
model: qr.model ?? 'HC7',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function configToJson(config) {
|
|
22
|
+
return typeof config === 'string' ? config : JSON.stringify(config);
|
|
23
|
+
}
|
|
24
|
+
function isSensorCode(value) {
|
|
25
|
+
return !!value && SENSOR_CODES.includes(value);
|
|
26
|
+
}
|
|
27
|
+
export function mapNativeError(error) {
|
|
28
|
+
if (error instanceof TempivoSensorError)
|
|
29
|
+
return error;
|
|
30
|
+
const raw = error;
|
|
31
|
+
const code = isSensorCode(raw?.code) ? raw.code : 'unknown';
|
|
32
|
+
return new TempivoSensorError(code, raw?.message || 'Sensor request failed.');
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativePermissionResponse, TempivoNativeQr, TempivoSensorBeaconDevice } from './native-types.js';
|
|
2
|
+
import type { TempivoSensorConfiguration, TempivoSensorQr, TempivoTriggerTransmissionResult } from './session-types.js';
|
|
3
|
+
export declare function isNativeSensorBeaconAvailable(): boolean;
|
|
4
|
+
export declare function requestPermissions(): Promise<TempivoNativePermissionResponse>;
|
|
5
|
+
export declare function startScan(): Promise<void>;
|
|
6
|
+
export declare function stopScan(): void;
|
|
7
|
+
export declare function isScanning(): boolean;
|
|
8
|
+
export declare function addDeviceFoundListener(listener: (device: TempivoSensorBeaconDevice) => void): TempivoDeviceFoundSubscription;
|
|
9
|
+
export declare function connect(qr: string | TempivoSensorQr): Promise<TempivoNativeQr>;
|
|
10
|
+
export declare function disconnect(): Promise<void>;
|
|
11
|
+
export declare function getConfiguration(): Promise<TempivoSensorConfiguration>;
|
|
12
|
+
export declare function setConfigurationJson(config: string | TempivoSensorConfiguration): Promise<void>;
|
|
13
|
+
export declare function triggerTransmission(): Promise<TempivoTriggerTransmissionResult>;
|
|
14
|
+
export declare function getCalibration(): Promise<TempivoNativeCalibration>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
2
|
+
import { TempivoSensorError } from './errors.js';
|
|
3
|
+
import { configToJson, mapNativeError, qrToConnectJson } from './native-helpers.js';
|
|
4
|
+
import { parseSensorConfigurationJson } from './profile.js';
|
|
5
|
+
function getNative() {
|
|
6
|
+
const native = requireOptionalNativeModule('TempivoSensorBeacon');
|
|
7
|
+
if (!native) {
|
|
8
|
+
throw new TempivoSensorError('runtimeUnavailable', 'Native BLE needs an Expo development build or React Native with Expo modules. Expo Go is not supported.');
|
|
9
|
+
}
|
|
10
|
+
return native;
|
|
11
|
+
}
|
|
12
|
+
export function isNativeSensorBeaconAvailable() {
|
|
13
|
+
return requireOptionalNativeModule('TempivoSensorBeacon') != null;
|
|
14
|
+
}
|
|
15
|
+
export async function requestPermissions() {
|
|
16
|
+
try {
|
|
17
|
+
const result = await getNative().requestPermissions();
|
|
18
|
+
if (result && typeof result.granted === 'boolean')
|
|
19
|
+
return result;
|
|
20
|
+
return {
|
|
21
|
+
granted: true,
|
|
22
|
+
status: 'granted',
|
|
23
|
+
canAskAgain: true,
|
|
24
|
+
expires: 'never',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw mapNativeError(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function ensureBlePermission() {
|
|
32
|
+
const perms = await requestPermissions();
|
|
33
|
+
if (perms.granted || perms.status === 'undetermined')
|
|
34
|
+
return;
|
|
35
|
+
throw new TempivoSensorError('runtimeUnavailable', 'Bluetooth permission is required.');
|
|
36
|
+
}
|
|
37
|
+
export async function startScan() {
|
|
38
|
+
await ensureBlePermission();
|
|
39
|
+
try {
|
|
40
|
+
await getNative().startScan();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw mapNativeError(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function stopScan() {
|
|
47
|
+
getNative().stopScan();
|
|
48
|
+
}
|
|
49
|
+
export function isScanning() {
|
|
50
|
+
return getNative().isScanning();
|
|
51
|
+
}
|
|
52
|
+
export function addDeviceFoundListener(listener) {
|
|
53
|
+
return getNative().addListener('onDeviceFound', listener);
|
|
54
|
+
}
|
|
55
|
+
export async function connect(qr) {
|
|
56
|
+
await ensureBlePermission();
|
|
57
|
+
try {
|
|
58
|
+
return await getNative().connect(qrToConnectJson(qr));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw mapNativeError(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function disconnect() {
|
|
65
|
+
try {
|
|
66
|
+
await getNative().disconnect();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw mapNativeError(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function getConfiguration() {
|
|
73
|
+
try {
|
|
74
|
+
const json = await getNative().getConfigurationJson();
|
|
75
|
+
return parseSensorConfigurationJson(json);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw mapNativeError(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function setConfigurationJson(config) {
|
|
82
|
+
try {
|
|
83
|
+
await getNative().setConfigurationJson(configToJson(config));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw mapNativeError(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function triggerTransmission() {
|
|
90
|
+
try {
|
|
91
|
+
return await getNative().triggerTransmission();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw mapNativeError(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function getCalibration() {
|
|
98
|
+
try {
|
|
99
|
+
return await getNative().getCalibration();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw mapNativeError(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TempivoSensorCalibration, TempivoSensorQr, TempivoSensorSessionType } from './session-types.js';
|
|
2
|
+
export type TempivoNativePermissionStatus = 'granted' | 'denied' | 'undetermined';
|
|
3
|
+
export type TempivoNativePermissionResponse = {
|
|
4
|
+
granted: boolean;
|
|
5
|
+
status: TempivoNativePermissionStatus;
|
|
6
|
+
canAskAgain?: boolean;
|
|
7
|
+
expires?: string;
|
|
8
|
+
};
|
|
9
|
+
export type TempivoNativeMeasurement = {
|
|
10
|
+
typeHex: string;
|
|
11
|
+
raw24: number;
|
|
12
|
+
text: string;
|
|
13
|
+
};
|
|
14
|
+
export type TempivoNativeTelemetry = {
|
|
15
|
+
firmware?: string | null;
|
|
16
|
+
batteryOk?: boolean | null;
|
|
17
|
+
encryptionEnabled?: boolean | null;
|
|
18
|
+
cellularStatus?: string | null;
|
|
19
|
+
measurementCounter?: number | null;
|
|
20
|
+
readingTimestampUnix?: number | null;
|
|
21
|
+
readingTimestampIso?: string | null;
|
|
22
|
+
measurementIntervalSeconds?: number | null;
|
|
23
|
+
readingsCount?: number;
|
|
24
|
+
summary?: string | null;
|
|
25
|
+
readings?: TempivoNativeMeasurement[];
|
|
26
|
+
};
|
|
27
|
+
export type TempivoSensorBeaconDevice = {
|
|
28
|
+
deviceId: string;
|
|
29
|
+
rssi: number;
|
|
30
|
+
bluetoothMacAddress?: string | null;
|
|
31
|
+
serialNumber?: string | null;
|
|
32
|
+
summary?: string | null;
|
|
33
|
+
telemetry?: TempivoNativeTelemetry | null;
|
|
34
|
+
};
|
|
35
|
+
export type TempivoNativeQr = TempivoSensorQr & {
|
|
36
|
+
sessionType: TempivoSensorSessionType;
|
|
37
|
+
};
|
|
38
|
+
export type TempivoNativeCalibration = TempivoSensorCalibration;
|
|
39
|
+
export type TempivoDeviceFoundSubscription = {
|
|
40
|
+
remove: () => void;
|
|
41
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TempivoSensorConfiguration } from './session-types.js';
|
|
2
|
+
/** Same default as Cellular API config profiles. */
|
|
3
|
+
export declare const DEFAULT_ALERT_HYSTERESIS_C = 1;
|
|
4
|
+
/** Validate Cellular API config-profile JSON for BLE setConfiguration. */
|
|
5
|
+
export declare function parseSensorConfiguration(input: unknown): TempivoSensorConfiguration;
|
|
6
|
+
/** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
|
|
7
|
+
export declare const configurationFromApiProfile: typeof parseSensorConfiguration;
|
|
8
|
+
export declare function parseSensorConfigurationJson(json: string): TempivoSensorConfiguration;
|
|
9
|
+
export declare function compileSensorProfileToBleJson(input: unknown): string;
|
|
10
|
+
export declare function pickPartnerConfigurationJson(json: string): string;
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { TempivoSensorError } from './errors.js';
|
|
2
|
+
const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
3
|
+
/** Same default as Cellular API config profiles. */
|
|
4
|
+
export const DEFAULT_ALERT_HYSTERESIS_C = 1;
|
|
5
|
+
function isRecord(v) {
|
|
6
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
function parseFiniteNumber(v) {
|
|
9
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
10
|
+
return v;
|
|
11
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
12
|
+
const n = Number(v);
|
|
13
|
+
if (Number.isFinite(n))
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
function parseChannel(v) {
|
|
19
|
+
return v === 'ambient' || v === 'probe' ? v : null;
|
|
20
|
+
}
|
|
21
|
+
function parseOptionalBool(v, fallback, field) {
|
|
22
|
+
if (v === undefined)
|
|
23
|
+
return fallback;
|
|
24
|
+
if (typeof v === 'boolean')
|
|
25
|
+
return v;
|
|
26
|
+
throw new TempivoSensorError('invalidConfig', `${field} must be a boolean.`);
|
|
27
|
+
}
|
|
28
|
+
function findPairStart(used) {
|
|
29
|
+
for (let i = 0; i < 11; i++) {
|
|
30
|
+
if (!used[i] && !used[i + 1])
|
|
31
|
+
return i;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function findFreeSlot(used) {
|
|
36
|
+
for (let i = 0; i < 12; i++) {
|
|
37
|
+
if (!used[i])
|
|
38
|
+
return i;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** Same 12-slot layout as Cellular API, including one extra slot per range with transmitOnReturn. */
|
|
43
|
+
function assertAlertSlotBudget(alerts) {
|
|
44
|
+
const used = Array(12).fill(false);
|
|
45
|
+
const returnLowSlots = [];
|
|
46
|
+
for (const alert of alerts) {
|
|
47
|
+
if (alert.type === 'range') {
|
|
48
|
+
const pair = findPairStart(used);
|
|
49
|
+
if (pair === null) {
|
|
50
|
+
throw new TempivoSensorError('invalidConfig', 'Not enough free alarm rule slots for another range (need two consecutive slots).');
|
|
51
|
+
}
|
|
52
|
+
used[pair] = true;
|
|
53
|
+
used[pair + 1] = true;
|
|
54
|
+
if (alert.transmitOnReturn)
|
|
55
|
+
returnLowSlots.push(pair);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const slot = findFreeSlot(used);
|
|
59
|
+
if (slot === null) {
|
|
60
|
+
throw new TempivoSensorError('invalidConfig', 'Not enough free alarm rule slots.');
|
|
61
|
+
}
|
|
62
|
+
used[slot] = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const _low of returnLowSlots) {
|
|
66
|
+
const slot = findFreeSlot(used);
|
|
67
|
+
if (slot === null) {
|
|
68
|
+
throw new TempivoSensorError('invalidConfig', 'Not enough free alarm rule slots for transmit-on-return (OR logic).');
|
|
69
|
+
}
|
|
70
|
+
used[slot] = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function parseSchedule(raw) {
|
|
74
|
+
if (raw == null)
|
|
75
|
+
return { always: true };
|
|
76
|
+
if (!isRecord(raw)) {
|
|
77
|
+
throw new TempivoSensorError('invalidConfig', 'schedule must be an object.');
|
|
78
|
+
}
|
|
79
|
+
if (raw.always === true)
|
|
80
|
+
return { always: true };
|
|
81
|
+
const weekdays = raw.weekdays;
|
|
82
|
+
if (!Array.isArray(weekdays) || weekdays.length === 0) {
|
|
83
|
+
throw new TempivoSensorError('invalidConfig', 'schedule.weekdays is required.');
|
|
84
|
+
}
|
|
85
|
+
const days = [];
|
|
86
|
+
for (const d of weekdays) {
|
|
87
|
+
const n = parseFiniteNumber(d);
|
|
88
|
+
if (n === undefined || !Number.isInteger(n) || n < 0 || n > 6) {
|
|
89
|
+
throw new TempivoSensorError('invalidConfig', 'schedule.weekdays must be 0 to 6.');
|
|
90
|
+
}
|
|
91
|
+
days.push(n);
|
|
92
|
+
}
|
|
93
|
+
const from = typeof raw.from === 'string' ? raw.from.trim() : '';
|
|
94
|
+
const to = typeof raw.to === 'string' ? raw.to.trim() : '';
|
|
95
|
+
if (!TIME_RE.test(from) || !TIME_RE.test(to)) {
|
|
96
|
+
throw new TempivoSensorError('invalidConfig', 'schedule.from and schedule.to must be HH:MM.');
|
|
97
|
+
}
|
|
98
|
+
const utc = parseFiniteNumber(raw.utcOffsetMinutes);
|
|
99
|
+
if (utc === undefined || !Number.isInteger(utc)) {
|
|
100
|
+
throw new TempivoSensorError('invalidConfig', 'schedule.utcOffsetMinutes is required.');
|
|
101
|
+
}
|
|
102
|
+
return { weekdays: days, from, to, utcOffsetMinutes: utc };
|
|
103
|
+
}
|
|
104
|
+
function parseAlert(item, index) {
|
|
105
|
+
if (!isRecord(item)) {
|
|
106
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}] must be an object.`);
|
|
107
|
+
}
|
|
108
|
+
const channel = parseChannel(item.channel);
|
|
109
|
+
if (!channel) {
|
|
110
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}].channel must be ambient or probe.`);
|
|
111
|
+
}
|
|
112
|
+
if (item.hysteresisC !== undefined) {
|
|
113
|
+
const hystRaw = parseFiniteNumber(item.hysteresisC);
|
|
114
|
+
if (hystRaw === undefined || hystRaw < 0) {
|
|
115
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}].hysteresisC must be a number 0 or greater.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const hysteresisC = parseFiniteNumber(item.hysteresisC) ?? DEFAULT_ALERT_HYSTERESIS_C;
|
|
119
|
+
const transmitOnBreach = parseOptionalBool(item.transmitOnBreach, true, `temperatureAlerts[${index}].transmitOnBreach`);
|
|
120
|
+
if (item.type === 'range') {
|
|
121
|
+
const lowC = parseFiniteNumber(item.lowC);
|
|
122
|
+
const highC = parseFiniteNumber(item.highC);
|
|
123
|
+
if (lowC === undefined || highC === undefined) {
|
|
124
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}] needs lowC and highC.`);
|
|
125
|
+
}
|
|
126
|
+
if (highC <= lowC) {
|
|
127
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}]: highC must be greater than lowC.`);
|
|
128
|
+
}
|
|
129
|
+
const transmitOnReturn = parseOptionalBool(item.transmitOnReturn, true, `temperatureAlerts[${index}].transmitOnReturn`);
|
|
130
|
+
return {
|
|
131
|
+
type: 'range',
|
|
132
|
+
channel,
|
|
133
|
+
lowC,
|
|
134
|
+
highC,
|
|
135
|
+
hysteresisC,
|
|
136
|
+
transmitOnBreach,
|
|
137
|
+
transmitOnReturn,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (item.type === 'min') {
|
|
141
|
+
const minC = parseFiniteNumber(item.minC);
|
|
142
|
+
if (minC === undefined) {
|
|
143
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}] needs minC.`);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
type: 'min',
|
|
147
|
+
channel,
|
|
148
|
+
minC,
|
|
149
|
+
hysteresisC,
|
|
150
|
+
transmitOnBreach,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (item.type === 'max') {
|
|
154
|
+
const maxC = parseFiniteNumber(item.maxC);
|
|
155
|
+
if (maxC === undefined) {
|
|
156
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}] needs maxC.`);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
type: 'max',
|
|
160
|
+
channel,
|
|
161
|
+
maxC,
|
|
162
|
+
hysteresisC,
|
|
163
|
+
transmitOnBreach,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
throw new TempivoSensorError('invalidConfig', `temperatureAlerts[${index}].type must be range, min, or max.`);
|
|
167
|
+
}
|
|
168
|
+
function looksLikeSimpleProfile(rec) {
|
|
169
|
+
return (rec.temperatureAlerts !== undefined ||
|
|
170
|
+
rec.schedule !== undefined ||
|
|
171
|
+
typeof rec.slug === 'string');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* GET /devices/config-profiles/{slug} returns `{ profile }`. Also accepts the profile object.
|
|
175
|
+
*/
|
|
176
|
+
function unwrapApiProfile(input) {
|
|
177
|
+
if (Array.isArray(input.profiles)) {
|
|
178
|
+
throw new TempivoSensorError('invalidConfig', 'Pass one profile from GET /devices/config-profiles/{slug}, not the list.');
|
|
179
|
+
}
|
|
180
|
+
const nested = input.profile;
|
|
181
|
+
if (isRecord(nested) && looksLikeSimpleProfile(nested)) {
|
|
182
|
+
return nested;
|
|
183
|
+
}
|
|
184
|
+
return input;
|
|
185
|
+
}
|
|
186
|
+
function rejectWireAlarmRules(rec) {
|
|
187
|
+
if (rec.temperatureAlerts === undefined && rec.alarmRules !== undefined) {
|
|
188
|
+
throw new TempivoSensorError('invalidConfig', 'Use a config profile with temperatureAlerts (GET /devices/config-profiles/{slug}), not alarmRules.');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Validate Cellular API config-profile JSON for BLE setConfiguration. */
|
|
192
|
+
export function parseSensorConfiguration(input) {
|
|
193
|
+
if (!isRecord(input)) {
|
|
194
|
+
throw new TempivoSensorError('invalidConfig', 'Configuration must be a JSON object.');
|
|
195
|
+
}
|
|
196
|
+
const body = unwrapApiProfile(input);
|
|
197
|
+
rejectWireAlarmRules(body);
|
|
198
|
+
const alertsRaw = body.temperatureAlerts;
|
|
199
|
+
if (alertsRaw !== undefined && !Array.isArray(alertsRaw)) {
|
|
200
|
+
throw new TempivoSensorError('invalidConfig', 'temperatureAlerts must be an array.');
|
|
201
|
+
}
|
|
202
|
+
const temperatureAlerts = (alertsRaw ?? []).map((item, i) => parseAlert(item, i));
|
|
203
|
+
assertAlertSlotBudget(temperatureAlerts);
|
|
204
|
+
return {
|
|
205
|
+
temperatureAlerts,
|
|
206
|
+
schedule: parseSchedule(body.schedule),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Same as {@link parseSensorConfiguration}: GET `{ profile }` or the profile object. */
|
|
210
|
+
export const configurationFromApiProfile = parseSensorConfiguration;
|
|
211
|
+
export function parseSensorConfigurationJson(json) {
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(json);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new TempivoSensorError('invalidConfig', 'Configuration is not valid JSON.');
|
|
218
|
+
}
|
|
219
|
+
return parseSensorConfiguration(parsed);
|
|
220
|
+
}
|
|
221
|
+
export function compileSensorProfileToBleJson(input) {
|
|
222
|
+
return JSON.stringify(parseSensorConfiguration(input));
|
|
223
|
+
}
|
|
224
|
+
export function pickPartnerConfigurationJson(json) {
|
|
225
|
+
return JSON.stringify(parseSensorConfigurationJson(json));
|
|
226
|
+
}
|
package/dist/qr.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TempivoSensorQr, TempivoSensorSessionType } from './session-types.js';
|
|
2
|
+
export declare function normalizeSensorSerial(value: string): string;
|
|
3
|
+
/** `282C024F0012` → `28:2C:02:4F:00:12`. */
|
|
4
|
+
export declare function bluetoothMacFromSerial(serial: string): string;
|
|
5
|
+
/** BLE default when QR omits `model`. HC5 / FW 6.x still select legacy. */
|
|
6
|
+
export declare const DEFAULT_SENSOR_MODEL = "HC7";
|
|
7
|
+
export declare function sessionTypeFromModel(model: string | undefined | null): TempivoSensorSessionType;
|
|
8
|
+
/**
|
|
9
|
+
* 3-byte big-endian numeric PIN for GATT command payloads that check a reset code
|
|
10
|
+
* (`setConfiguration`, trigger, and many other privileged writes).
|
|
11
|
+
*/
|
|
12
|
+
export declare function encodeSensorPinPayload(pin: string): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Parse sensor label QR JSON: `{"sn":"…","pin":"…","model":"HC7"}`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSensorQrJson(text: string): TempivoSensorQr;
|