@tempivo/sensor-beacon 1.1.2 → 1.1.4

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 (30) hide show
  1. package/CHANGELOG.md +29 -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 +672 -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 +142 -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 +12 -0
  24. package/dist/proto/partner-to-proto.js +211 -0
  25. package/dist/tempivo-sensor-beacon.aar +0 -0
  26. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  27. package/ios/Sources/TempivoSensorBeacon/EfentoRawConfigTransfer.swift +601 -0
  28. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +99 -29
  29. package/ios/TempivoSensorBeaconModule.swift +9 -0
  30. package/package.json +6 -2
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Map ProtoConfig rules (wire) → partner temperatureAlerts + schedule + intervals.
3
+ * Mirrors scripts/efento-local-mac/partner_rules.py layout (interleaved L/H/OR).
4
+ */
5
+ import type { TempivoSensorConfiguration } from '../session-types.js';
6
+ import { type ProtoConfigObject } from './decode-proto-config.js';
7
+ /** Decompile ProtoConfig object into partner configuration. */
8
+ export declare function partnerConfigFromProtoObject(cfg: ProtoConfigObject): TempivoSensorConfiguration;
9
+ export declare function partnerConfigFromProtoBase64(b64: string): TempivoSensorConfiguration;
@@ -0,0 +1,187 @@
1
+ import { decodeProtoConfigBase64 } from './decode-proto-config.js';
2
+ const CONDITION_HIGH = 'CONDITION_HIGH_THRESHOLD';
3
+ const CONDITION_LOW = 'CONDITION_LOW_THRESHOLD';
4
+ const CONDITION_LOGIC = 'CONDITION_LOGIC_OPERATOR';
5
+ const CONDITION_DISABLED = 'CONDITION_DISABLED';
6
+ const LOGIC_OR = 'LOGIC_OPERATOR_OR';
7
+ function asNum(v) {
8
+ if (typeof v === 'number' && Number.isFinite(v))
9
+ return v;
10
+ if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
11
+ return Number(v);
12
+ return null;
13
+ }
14
+ function wireTempToC(v) {
15
+ return v / 10;
16
+ }
17
+ function channelFromMask(mask) {
18
+ // bit0 = channel 1 ambient, bit2 = channel 3 probe
19
+ if (mask & 0x4)
20
+ return 'probe';
21
+ if (mask & 0x1)
22
+ return 'ambient';
23
+ return null;
24
+ }
25
+ function conditionName(c) {
26
+ if (typeof c === 'string')
27
+ return c;
28
+ if (typeof c === 'number') {
29
+ const map = {
30
+ 1: CONDITION_DISABLED,
31
+ 2: CONDITION_HIGH,
32
+ 3: CONDITION_LOW,
33
+ 6: CONDITION_LOGIC,
34
+ };
35
+ return map[c] ?? String(c);
36
+ }
37
+ return '';
38
+ }
39
+ function normalizeRules(cfg) {
40
+ const raw = (cfg.rules ?? cfg.Rules ?? []);
41
+ if (!Array.isArray(raw))
42
+ return [];
43
+ return raw.map((r) => {
44
+ const o = (r ?? {});
45
+ const params = (o.parameters ?? o.Parameters ?? []);
46
+ return {
47
+ condition: conditionName(o.condition ?? o.Condition),
48
+ channel_mask: asNum(o.channel_mask ?? o.channelMask) ?? 0,
49
+ action: o.action ?? o.Action,
50
+ parameters: params.map((p) => asNum(p) ?? 0),
51
+ };
52
+ });
53
+ }
54
+ function actionIsTransmit(action) {
55
+ const s = String(action ?? '');
56
+ if (s.includes('TRIGGER_TRANSMISSION'))
57
+ return true;
58
+ const n = asNum(action);
59
+ return n === 3;
60
+ }
61
+ function logicIsOr(params) {
62
+ if (params.length === 0)
63
+ return false;
64
+ const op = params[0];
65
+ return op === 2 || String(op) === LOGIC_OR || String(op).includes('OR');
66
+ }
67
+ function selectedRuleIds(mask) {
68
+ const ids = [];
69
+ for (let i = 0; i < 16; i++) {
70
+ if (mask & (1 << i))
71
+ ids.push(i + 1);
72
+ }
73
+ return ids;
74
+ }
75
+ /** Decompile ProtoConfig object into partner configuration. */
76
+ export function partnerConfigFromProtoObject(cfg) {
77
+ const rules = normalizeRules(cfg);
78
+ const padded = [];
79
+ for (let i = 0; i < 12; i++) {
80
+ padded[i] = rules[i] ?? {
81
+ condition: CONDITION_DISABLED,
82
+ channel_mask: 0,
83
+ action: 2,
84
+ parameters: [],
85
+ };
86
+ }
87
+ const orPairs = new Set();
88
+ for (const r of padded) {
89
+ if (r.condition !== CONDITION_LOGIC && !r.condition.includes('LOGIC'))
90
+ continue;
91
+ if (!logicIsOr(r.parameters))
92
+ continue;
93
+ const ids = selectedRuleIds(r.parameters[1] ?? 0);
94
+ if (ids.length === 2)
95
+ orPairs.add(`${ids[0]},${ids[1]}`);
96
+ }
97
+ const used = new Array(12).fill(false);
98
+ const alerts = [];
99
+ for (let i = 0; i <= 10; i++) {
100
+ if (used[i] || used[i + 1])
101
+ continue;
102
+ const low = padded[i];
103
+ const high = padded[i + 1];
104
+ const lowOk = low.condition === CONDITION_LOW || low.condition.includes('LOW_THRESHOLD');
105
+ const highOk = high.condition === CONDITION_HIGH || high.condition.includes('HIGH_THRESHOLD');
106
+ if (!lowOk || !highOk)
107
+ continue;
108
+ const inferred = alerts.length === 0 ? 'ambient' : 'probe';
109
+ const ch = channelFromMask(low.channel_mask) ?? channelFromMask(high.channel_mask) ?? inferred;
110
+ const lowC = wireTempToC(low.parameters[0] ?? 0);
111
+ const highC = wireTempToC(high.parameters[0] ?? 0);
112
+ const hyst = wireTempToC(low.parameters[1] ?? 10);
113
+ const hasReturn = orPairs.has(`${i + 1},${i + 2}`);
114
+ const breachOnly = !hasReturn && (actionIsTransmit(low.action) || actionIsTransmit(high.action));
115
+ alerts.push({
116
+ type: 'range',
117
+ channel: ch,
118
+ lowC,
119
+ highC,
120
+ hysteresisC: hyst,
121
+ transmitOnBreach: hasReturn || breachOnly,
122
+ transmitOnReturn: hasReturn,
123
+ });
124
+ used[i] = true;
125
+ used[i + 1] = true;
126
+ if (hasReturn) {
127
+ for (let j = 0; j < 12; j++) {
128
+ const r = padded[j];
129
+ if (r.condition !== CONDITION_LOGIC && !r.condition.includes('LOGIC'))
130
+ continue;
131
+ if (!logicIsOr(r.parameters))
132
+ continue;
133
+ const ids = selectedRuleIds(r.parameters[1] ?? 0);
134
+ if (ids.length === 2 && ids[0] === i + 1 && ids[1] === i + 2) {
135
+ used[j] = true;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ for (let i = 0; i < 12; i++) {
142
+ if (used[i])
143
+ continue;
144
+ const r = padded[i];
145
+ const ch = channelFromMask(r.channel_mask);
146
+ if (!ch)
147
+ continue;
148
+ const hyst = wireTempToC(r.parameters[1] ?? 10);
149
+ if (r.condition === CONDITION_LOW || r.condition.includes('LOW_THRESHOLD')) {
150
+ alerts.push({
151
+ type: 'min',
152
+ channel: ch,
153
+ minC: wireTempToC(r.parameters[0] ?? 0),
154
+ hysteresisC: hyst,
155
+ transmitOnBreach: actionIsTransmit(r.action),
156
+ });
157
+ used[i] = true;
158
+ }
159
+ else if (r.condition === CONDITION_HIGH || r.condition.includes('HIGH_THRESHOLD')) {
160
+ alerts.push({
161
+ type: 'max',
162
+ channel: ch,
163
+ maxC: wireTempToC(r.parameters[0] ?? 0),
164
+ hysteresisC: hyst,
165
+ transmitOnBreach: actionIsTransmit(r.action),
166
+ });
167
+ used[i] = true;
168
+ }
169
+ }
170
+ const out = {
171
+ temperatureAlerts: alerts,
172
+ schedule: { always: true },
173
+ };
174
+ const measBase = asNum(cfg.measurement_period_base ?? cfg.measurementPeriodBase);
175
+ const measFactor = asNum(cfg.measurement_period_factor ?? cfg.measurementPeriodFactor) ?? 1;
176
+ if (measBase != null && measBase > 0) {
177
+ const factor = measFactor === 0 ? 1 : measFactor;
178
+ out.measurementIntervalMinutes = (measBase * factor) / 60;
179
+ }
180
+ const tx = asNum(cfg.transmission_interval ?? cfg.transmissionInterval);
181
+ if (tx != null && tx > 0)
182
+ out.transmissionIntervalSeconds = tx;
183
+ return out;
184
+ }
185
+ export function partnerConfigFromProtoBase64(b64) {
186
+ return partnerConfigFromProtoObject(decodeProtoConfigBase64(b64));
187
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Partner temperatureAlerts + schedule + intervals → ProtoConfig bytes.
3
+ * Mirrors scripts/efento-local-mac/partner_rules.py (interleaved L/H/OR).
4
+ */
5
+ import type { TempivoSensorConfiguration } from '../session-types.js';
6
+ import { type ProtoConfigObject } from './decode-proto-config.js';
7
+ /** Patch partner alerts/schedule/intervals onto a baseline ProtoConfig object. */
8
+ export declare function applyPartnerConfigToProtoObject(baseline: ProtoConfigObject, partner: TempivoSensorConfiguration): ProtoConfigObject;
9
+ export declare function encodeProtoConfigObject(cfg: ProtoConfigObject): Uint8Array;
10
+ export declare function bytesToBase64(bytes: Uint8Array): string;
11
+ /** Apply partner config onto baseline ProtoConfig base64 → new base64. */
12
+ export declare function partnerConfigToProtoBase64(partner: TempivoSensorConfiguration, baselineProtoBase64: string): string;
@@ -0,0 +1,211 @@
1
+ import { decodeProtoConfigBase64 } from './decode-proto-config.js';
2
+ import protobuf from 'protobufjs/light.js';
3
+ import descriptor from './efento-protobuf-descriptor.js';
4
+ const root = protobuf.Root.fromJSON(descriptor);
5
+ const ProtoConfig = root.lookupType('ProtoConfig');
6
+ const CONDITION_DISABLED = 1;
7
+ const CONDITION_HIGH = 2;
8
+ const CONDITION_LOW = 3;
9
+ const CONDITION_LOGIC = 6;
10
+ const LOGIC_OR = 2;
11
+ const ACTION_NO_ACTION = 2;
12
+ const ACTION_TRIGGER_ACK = 3;
13
+ const CALENDAR_DISABLED = 1;
14
+ const CALENDAR_WEEK = 2;
15
+ const MEASUREMENT_TEMP = 1;
16
+ const TRIGGER_CONSECUTIVE = 3;
17
+ function tempToWire(c) {
18
+ return Math.round(c * 10);
19
+ }
20
+ function channelMask(channel) {
21
+ return 1 << (channel - 1);
22
+ }
23
+ function ruleBitmask(ids) {
24
+ let mask = 0;
25
+ for (const id of ids)
26
+ mask |= 1 << (id - 1);
27
+ return mask;
28
+ }
29
+ function disabledRule() {
30
+ return {
31
+ channelMask: 0,
32
+ condition: CONDITION_DISABLED,
33
+ action: ACTION_NO_ACTION,
34
+ parameters: [],
35
+ };
36
+ }
37
+ function thresholdRule(opts) {
38
+ return {
39
+ channelMask: channelMask(opts.channel),
40
+ condition: opts.low ? CONDITION_LOW : CONDITION_HIGH,
41
+ action: opts.action,
42
+ parameters: [
43
+ tempToWire(opts.thresholdC),
44
+ tempToWire(opts.hysteresisC),
45
+ TRIGGER_CONSECUTIVE,
46
+ 1,
47
+ MEASUREMENT_TEMP,
48
+ ],
49
+ };
50
+ }
51
+ function orReturnRule(lowSlot) {
52
+ return {
53
+ channelMask: 0,
54
+ condition: CONDITION_LOGIC,
55
+ action: ACTION_TRIGGER_ACK,
56
+ parameters: [LOGIC_OR, ruleBitmask([lowSlot + 1, lowSlot + 2]), 0, 0, 0],
57
+ };
58
+ }
59
+ function compileAlerts(alerts) {
60
+ const rules = Array.from({ length: 12 }, () => disabledRule());
61
+ const used = new Array(12).fill(false);
62
+ const findPair = () => {
63
+ for (let i = 0; i < 11; i++)
64
+ if (!used[i] && !used[i + 1])
65
+ return i;
66
+ return null;
67
+ };
68
+ const findFree = () => {
69
+ for (let i = 0; i < 12; i++)
70
+ if (!used[i])
71
+ return i;
72
+ return null;
73
+ };
74
+ for (const alert of alerts) {
75
+ const channel = alert.channel === 'probe' ? 3 : 1;
76
+ const hyst = alert.hysteresisC ?? 1;
77
+ if (alert.type === 'range') {
78
+ const pair = findPair();
79
+ if (pair == null)
80
+ throw new Error('Not enough free alarm rule slots for another range.');
81
+ const ret = Boolean(alert.transmitOnReturn);
82
+ const action = ret
83
+ ? ACTION_NO_ACTION
84
+ : alert.transmitOnBreach
85
+ ? ACTION_TRIGGER_ACK
86
+ : ACTION_NO_ACTION;
87
+ rules[pair] = thresholdRule({
88
+ channel,
89
+ low: true,
90
+ thresholdC: alert.lowC,
91
+ hysteresisC: hyst,
92
+ action,
93
+ });
94
+ rules[pair + 1] = thresholdRule({
95
+ channel,
96
+ low: false,
97
+ thresholdC: alert.highC,
98
+ hysteresisC: hyst,
99
+ action,
100
+ });
101
+ used[pair] = used[pair + 1] = true;
102
+ if (ret) {
103
+ const logic = findFree();
104
+ if (logic == null)
105
+ throw new Error('Not enough free slots for transmit-on-return OR.');
106
+ rules[logic] = orReturnRule(pair);
107
+ used[logic] = true;
108
+ }
109
+ }
110
+ else if (alert.type === 'min' || alert.type === 'max') {
111
+ const slot = findFree();
112
+ if (slot == null)
113
+ throw new Error('Not enough free alarm rule slots.');
114
+ const action = alert.transmitOnBreach ? ACTION_TRIGGER_ACK : ACTION_NO_ACTION;
115
+ const thresholdC = alert.type === 'min' ? alert.minC : alert.maxC;
116
+ rules[slot] = thresholdRule({
117
+ channel,
118
+ low: alert.type === 'min',
119
+ thresholdC,
120
+ hysteresisC: hyst,
121
+ action,
122
+ });
123
+ used[slot] = true;
124
+ }
125
+ else {
126
+ throw new Error(`Unsupported temperature alert type: ${alert.type}`);
127
+ }
128
+ }
129
+ const mask = ruleBitmask(used.map((u, i) => (u ? i + 1 : 0)).filter((id) => id > 0));
130
+ return { rules, mask };
131
+ }
132
+ function alwaysCalendar(ruleMask) {
133
+ return {
134
+ ruleMask,
135
+ type: CALENDAR_WEEK,
136
+ parameters: [0x7f, 0, 1439, 0],
137
+ };
138
+ }
139
+ function disabledCalendars() {
140
+ return Array.from({ length: 6 }, () => ({
141
+ type: CALENDAR_DISABLED,
142
+ ruleMask: 0,
143
+ parameters: [],
144
+ }));
145
+ }
146
+ /**
147
+ * Fields carried from the device baseline into a set (0x0D).
148
+ *
149
+ * A device read returns the full config, including server endpoints, APN, cloud token and
150
+ * telemetry. Echoing all of that back exceeds the single-write ATT limit (~512 B) and would
151
+ * rewrite cellular settings we never intended to touch, so only rules-related fields are kept.
152
+ */
153
+ const WRITE_BASELINE_FIELDS = [
154
+ 'serialNumber',
155
+ 'channelTypes',
156
+ 'measurementPeriodBase',
157
+ 'measurementPeriodFactor',
158
+ 'transmissionInterval',
159
+ ];
160
+ /** Patch partner alerts/schedule/intervals onto a baseline ProtoConfig object. */
161
+ export function applyPartnerConfigToProtoObject(baseline, partner) {
162
+ const { rules, mask } = compileAlerts(partner.temperatureAlerts ?? []);
163
+ const out = { rules };
164
+ for (const field of WRITE_BASELINE_FIELDS) {
165
+ if (baseline[field] !== undefined && baseline[field] !== null)
166
+ out[field] = baseline[field];
167
+ }
168
+ const cals = disabledCalendars();
169
+ const always = partner.schedule == null ||
170
+ ('always' in partner.schedule && partner.schedule.always === true) ||
171
+ !('weekdays' in partner.schedule);
172
+ if (mask && always)
173
+ cals[0] = alwaysCalendar(mask);
174
+ out.calendars = cals;
175
+ if (partner.measurementIntervalMinutes != null && partner.measurementIntervalMinutes > 0) {
176
+ out.measurementPeriodBase = Math.round(partner.measurementIntervalMinutes * 60);
177
+ out.measurementPeriodFactor = 1;
178
+ }
179
+ if (partner.transmissionIntervalSeconds != null && partner.transmissionIntervalSeconds > 0) {
180
+ out.transmissionInterval = partner.transmissionIntervalSeconds;
181
+ }
182
+ return out;
183
+ }
184
+ export function encodeProtoConfigObject(cfg) {
185
+ // Baseline from decode uses enums: String; fromObject maps names → wire ints.
186
+ const msg = ProtoConfig.fromObject(cfg);
187
+ const err = ProtoConfig.verify(msg);
188
+ if (err)
189
+ throw new Error(err);
190
+ return ProtoConfig.encode(msg).finish();
191
+ }
192
+ export function bytesToBase64(bytes) {
193
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
194
+ let out = '';
195
+ for (let i = 0; i < bytes.length; i += 3) {
196
+ const a = bytes[i];
197
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
198
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
199
+ out += chars[a >> 2];
200
+ out += chars[((a & 3) << 4) | (b >> 4)];
201
+ out += i + 1 < bytes.length ? chars[((b & 15) << 2) | (c >> 6)] : '=';
202
+ out += i + 2 < bytes.length ? chars[c & 63] : '=';
203
+ }
204
+ return out;
205
+ }
206
+ /** Apply partner config onto baseline ProtoConfig base64 → new base64. */
207
+ export function partnerConfigToProtoBase64(partner, baselineProtoBase64) {
208
+ const baseline = decodeProtoConfigBase64(baselineProtoBase64);
209
+ const next = applyPartnerConfigToProtoObject(baseline, partner);
210
+ return bytesToBase64(encodeProtoConfigObject(next));
211
+ }
Binary file