@tempivo/sensor-beacon 1.1.2 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +35 -22
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +8 -0
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +8 -0
  6. package/android-aar/library/build.gradle +5 -0
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt +780 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +124 -10
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +19 -8
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +136 -11
  11. package/android-aar/library/src/test/java/com/tempivo/sensor/beacon/PartnerBleRulesTest.kt +209 -0
  12. package/dist/native-module.d.ts +10 -0
  13. package/dist/native-module.js +46 -3
  14. package/dist/partner-dual-range-fixture.d.ts +28 -0
  15. package/dist/partner-dual-range-fixture.js +29 -0
  16. package/dist/profile.js +33 -19
  17. package/dist/proto/decode-proto-config.d.ts +3 -0
  18. package/dist/proto/decode-proto-config.js +46 -0
  19. package/dist/proto/efento-protobuf-descriptor.d.ts +964 -0
  20. package/dist/proto/efento-protobuf-descriptor.js +727 -0
  21. package/dist/proto/partner-from-proto.d.ts +9 -0
  22. package/dist/proto/partner-from-proto.js +187 -0
  23. package/dist/proto/partner-to-proto.d.ts +32 -0
  24. package/dist/proto/partner-to-proto.js +282 -0
  25. package/dist/tempivo-sensor-beacon.aar +0 -0
  26. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
  27. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  28. package/ios/Sources/TempivoSensorBeacon/EfentoRawConfigTransfer.swift +601 -0
  29. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +99 -29
  30. package/ios/TempivoSensorBeaconModule.swift +9 -0
  31. package/package.json +6 -2
@@ -1,7 +1,9 @@
1
1
  import { requireOptionalNativeModule } from 'expo-modules-core';
2
2
  import { TempivoSensorError } from './errors.js';
3
3
  import { configToJson, mapNativeError, qrToConnectJson } from './native-helpers.js';
4
- import { parseSensorConfigurationFromDeviceJson } from './profile.js';
4
+ import { partnerConfigFromProtoBase64 } from './proto/partner-from-proto.js';
5
+ import { partnerConfigToProtoBase64 } from './proto/partner-to-proto.js';
6
+ import { parseSensorConfigurationFromDevice } from './profile.js';
5
7
  function getNative() {
6
8
  const native = requireOptionalNativeModule('TempivoSensorBeacon');
7
9
  if (!native) {
@@ -67,24 +69,65 @@ export async function connect(qr) {
67
69
  }
68
70
  export async function disconnect() {
69
71
  try {
72
+ lastProtoBase64 = null;
70
73
  await getNative().disconnect();
71
74
  }
72
75
  catch (error) {
73
76
  throw mapNativeError(error);
74
77
  }
75
78
  }
79
+ /** Last ProtoConfig from iOS raw get; used as set baseline. Not part of the partner API. */
80
+ let lastProtoBase64 = null;
81
+ /**
82
+ * Partner configuration from the device.
83
+ * Return shape is only {@link TempivoSensorConfiguration} (no debug fields).
84
+ * iOS and Android use raw ProtoConfig GATT; return shape is only partner fields.
85
+ */
76
86
  export async function getConfiguration() {
77
87
  try {
78
88
  const json = await getNative().getConfigurationJson();
79
- return parseSensorConfigurationFromDeviceJson(json);
89
+ const parsed = JSON.parse(json);
90
+ const protoB64 = typeof parsed.protoBase64 === 'string' ? parsed.protoBase64 : null;
91
+ if (protoB64) {
92
+ lastProtoBase64 = protoB64;
93
+ return partnerConfigFromProtoBase64(protoB64);
94
+ }
95
+ lastProtoBase64 = null;
96
+ return parseSensorConfigurationFromDevice(parsed);
80
97
  }
81
98
  catch (error) {
82
99
  throw mapNativeError(error);
83
100
  }
84
101
  }
102
+ /**
103
+ * Write partner configuration to the device.
104
+ * Same public signature as before. Encodes partner config to ProtoConfig and writes raw GATT
105
+ * when `writeProtoConfigBase64` is available (iOS and Android).
106
+ */
85
107
  export async function setConfigurationJson(config) {
86
108
  try {
87
- await getNative().setConfigurationJson(configToJson(config));
109
+ const partner = typeof config === 'string'
110
+ ? JSON.parse(config)
111
+ : config;
112
+ const native = getNative();
113
+ if (typeof native.writeProtoConfigBase64 === 'function') {
114
+ let baseline = lastProtoBase64;
115
+ if (!baseline) {
116
+ await getConfiguration();
117
+ baseline = lastProtoBase64;
118
+ }
119
+ if (!baseline) {
120
+ throw new TempivoSensorError('invalidConfig', 'Cannot set: missing baseline ProtoConfig from device get.');
121
+ }
122
+ const nextB64 = partnerConfigToProtoBase64(partner, baseline);
123
+ await native.writeProtoConfigBase64(nextB64);
124
+ lastProtoBase64 = nextB64;
125
+ return;
126
+ }
127
+ if (typeof native.setConfigurationJson !== 'function') {
128
+ throw new TempivoSensorError('runtimeUnavailable', 'Native setConfigurationJson is unavailable in this build.');
129
+ }
130
+ await native.setConfigurationJson(configToJson(partner));
88
131
  }
89
132
  catch (error) {
90
133
  throw mapNativeError(error);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Partner BLE dual-range + always payload used in regression tests.
3
+ * Before SDK 1.1.2, get could return empty alerts and/or Sunday-only schedule.
4
+ */
5
+ export declare const PARTNER_DUAL_RANGE_ALWAYS_BODY: {
6
+ schedule: {
7
+ always: boolean;
8
+ };
9
+ temperatureAlerts: ({
10
+ type: "range";
11
+ channel: "ambient";
12
+ lowC: number;
13
+ highC: number;
14
+ hysteresisC: number;
15
+ transmitOnBreach: boolean;
16
+ transmitOnReturn: boolean;
17
+ } | {
18
+ type: "range";
19
+ channel: "probe";
20
+ lowC: number;
21
+ highC: number;
22
+ hysteresisC: number;
23
+ transmitOnBreach: boolean;
24
+ transmitOnReturn: boolean;
25
+ })[];
26
+ measurementIntervalMinutes: number;
27
+ transmissionIntervalSeconds: number;
28
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Partner BLE dual-range + always payload used in regression tests.
3
+ * Before SDK 1.1.2, get could return empty alerts and/or Sunday-only schedule.
4
+ */
5
+ export const PARTNER_DUAL_RANGE_ALWAYS_BODY = {
6
+ schedule: { always: true },
7
+ temperatureAlerts: [
8
+ {
9
+ type: 'range',
10
+ channel: 'ambient',
11
+ lowC: 2,
12
+ highC: 8,
13
+ hysteresisC: 0.5,
14
+ transmitOnBreach: true,
15
+ transmitOnReturn: true,
16
+ },
17
+ {
18
+ type: 'range',
19
+ channel: 'probe',
20
+ lowC: -30,
21
+ highC: -18,
22
+ hysteresisC: 0.5,
23
+ transmitOnBreach: true,
24
+ transmitOnReturn: true,
25
+ },
26
+ ],
27
+ measurementIntervalMinutes: 5,
28
+ transmissionIntervalSeconds: 3600,
29
+ };
package/dist/profile.js CHANGED
@@ -218,35 +218,49 @@ export const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604_800;
218
218
  function parseOptionalIntervals(body, options = {}) {
219
219
  const validatePartnerLimits = !options.fromDevice;
220
220
  const out = {};
221
- if (body.measurementIntervalMinutes !== undefined) {
221
+ if (body.measurementIntervalMinutes !== undefined && body.measurementIntervalMinutes !== null) {
222
222
  const n = parseFiniteNumber(body.measurementIntervalMinutes);
223
223
  if (n === undefined || !Number.isFinite(n) || n <= 0) {
224
- throw new TempivoSensorError('invalidConfig', 'measurementIntervalMinutes must be a positive number.');
225
- }
226
- if (validatePartnerLimits) {
227
- if (n < BLE_MIN_MEASUREMENT_INTERVAL_MINUTES) {
228
- throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES}.`);
224
+ if (options.fromDevice) {
225
+ // Vendor may emit 0/null; omit rather than fail the whole get.
226
+ }
227
+ else {
228
+ throw new TempivoSensorError('invalidConfig', 'measurementIntervalMinutes must be a positive number.');
229
229
  }
230
- if (n > BLE_MAX_MEASUREMENT_INTERVAL_MINUTES) {
231
- throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at most ${BLE_MAX_MEASUREMENT_INTERVAL_MINUTES}.`);
230
+ }
231
+ else {
232
+ if (validatePartnerLimits) {
233
+ if (n < BLE_MIN_MEASUREMENT_INTERVAL_MINUTES) {
234
+ throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES}.`);
235
+ }
236
+ if (n > BLE_MAX_MEASUREMENT_INTERVAL_MINUTES) {
237
+ throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at most ${BLE_MAX_MEASUREMENT_INTERVAL_MINUTES}.`);
238
+ }
232
239
  }
240
+ out.measurementIntervalMinutes = n;
233
241
  }
234
- out.measurementIntervalMinutes = n;
235
242
  }
236
- if (body.transmissionIntervalSeconds !== undefined) {
243
+ if (body.transmissionIntervalSeconds !== undefined && body.transmissionIntervalSeconds !== null) {
237
244
  const n = parseFiniteNumber(body.transmissionIntervalSeconds);
238
- if (n === undefined || !Number.isInteger(n) || n <= 0) {
239
- throw new TempivoSensorError('invalidConfig', 'transmissionIntervalSeconds must be a positive integer.');
240
- }
241
- if (validatePartnerLimits) {
242
- if (n < BLE_MIN_TRANSMISSION_INTERVAL_SECONDS) {
243
- throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at least ${BLE_MIN_TRANSMISSION_INTERVAL_SECONDS} (1 hour).`);
245
+ if (n === undefined || !Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
246
+ if (options.fromDevice) {
247
+ // omit
248
+ }
249
+ else {
250
+ throw new TempivoSensorError('invalidConfig', 'transmissionIntervalSeconds must be a positive integer.');
244
251
  }
245
- if (n > BLE_MAX_TRANSMISSION_INTERVAL_SECONDS) {
246
- throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at most ${BLE_MAX_TRANSMISSION_INTERVAL_SECONDS}.`);
252
+ }
253
+ else {
254
+ if (validatePartnerLimits) {
255
+ if (n < BLE_MIN_TRANSMISSION_INTERVAL_SECONDS) {
256
+ throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at least ${BLE_MIN_TRANSMISSION_INTERVAL_SECONDS} (1 hour).`);
257
+ }
258
+ if (n > BLE_MAX_TRANSMISSION_INTERVAL_SECONDS) {
259
+ throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at most ${BLE_MAX_TRANSMISSION_INTERVAL_SECONDS}.`);
260
+ }
247
261
  }
262
+ out.transmissionIntervalSeconds = n;
248
263
  }
249
- out.transmissionIntervalSeconds = n;
250
264
  }
251
265
  if (validatePartnerLimits &&
252
266
  out.measurementIntervalMinutes != null &&
@@ -0,0 +1,3 @@
1
+ export type ProtoConfigObject = Record<string, unknown>;
2
+ export declare function decodeProtoConfigBytes(bytes: Uint8Array): ProtoConfigObject;
3
+ export declare function decodeProtoConfigBase64(b64: string): ProtoConfigObject;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Decode Efento ProtoConfig bytes (same descriptor as web bluetooth module).
3
+ */
4
+ import protobuf from 'protobufjs/light.js';
5
+ import descriptor from './efento-protobuf-descriptor.js';
6
+ const root = protobuf.Root.fromJSON(descriptor);
7
+ const ProtoConfig = root.lookupType('ProtoConfig');
8
+ export function decodeProtoConfigBytes(bytes) {
9
+ const msg = ProtoConfig.decode(bytes);
10
+ return ProtoConfig.toObject(msg, {
11
+ enums: String,
12
+ longs: Number,
13
+ bytes: Array,
14
+ arrays: true,
15
+ objects: true,
16
+ oneofs: true,
17
+ });
18
+ }
19
+ function base64ToBytes(b64) {
20
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
21
+ const clean = b64.replace(/=+$/, '').replace(/[^A-Za-z0-9+/]/g, '');
22
+ const out = new Uint8Array((clean.length * 3) >> 2);
23
+ let o = 0;
24
+ for (let i = 0; i < clean.length; i += 4) {
25
+ const a = chars.indexOf(clean[i] ?? 'A');
26
+ const b = chars.indexOf(clean[i + 1] ?? 'A');
27
+ const c = chars.indexOf(clean[i + 2] ?? 'A');
28
+ const d = chars.indexOf(clean[i + 3] ?? 'A');
29
+ out[o++] = (a << 2) | (b >> 4);
30
+ if (clean[i + 2] != null)
31
+ out[o++] = ((b & 15) << 4) | (c >> 2);
32
+ if (clean[i + 3] != null)
33
+ out[o++] = ((c & 3) << 6) | d;
34
+ }
35
+ return out.subarray(0, o);
36
+ }
37
+ export function decodeProtoConfigBase64(b64) {
38
+ if (typeof atob === 'function') {
39
+ const binary = atob(b64);
40
+ const bytes = new Uint8Array(binary.length);
41
+ for (let i = 0; i < binary.length; i++)
42
+ bytes[i] = binary.charCodeAt(i);
43
+ return decodeProtoConfigBytes(bytes);
44
+ }
45
+ return decodeProtoConfigBytes(base64ToBytes(b64));
46
+ }