@adminide-stack/yantra-mobile 12.0.131-alpha.1 → 12.0.131-alpha.2

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 (42) hide show
  1. package/lib/features/devices/bleTransport.js +54 -20
  2. package/lib/features/devices/bleTransport.js.map +1 -1
  3. package/lib/features/devices/brands/channel.js +19 -0
  4. package/lib/features/devices/brands/channel.js.map +1 -0
  5. package/lib/features/devices/brands/detect.js +318 -0
  6. package/lib/features/devices/brands/detect.js.map +1 -0
  7. package/lib/features/devices/brands/frames.js +103 -0
  8. package/lib/features/devices/brands/frames.js.map +1 -0
  9. package/lib/features/devices/brands/index.js +30 -0
  10. package/lib/features/devices/brands/index.js.map +1 -0
  11. package/lib/features/devices/brands/qnScale.js +98 -0
  12. package/lib/features/devices/brands/qnScale.js.map +1 -0
  13. package/lib/features/devices/brands/renphoBloodPressure.js +92 -0
  14. package/lib/features/devices/brands/renphoBloodPressure.js.map +1 -0
  15. package/lib/features/devices/brands/renphoFoodScale.js +42 -0
  16. package/lib/features/devices/brands/renphoFoodScale.js.map +1 -0
  17. package/lib/features/devices/brands/renphoJumpRope.js +71 -0
  18. package/lib/features/devices/brands/renphoJumpRope.js.map +1 -0
  19. package/lib/features/devices/brands/renphoRing.js +82 -0
  20. package/lib/features/devices/brands/renphoRing.js.map +1 -0
  21. package/lib/features/devices/brands/renphoScale.js +181 -0
  22. package/lib/features/devices/brands/renphoScale.js.map +1 -0
  23. package/lib/features/devices/brands/renphoTape.js +68 -0
  24. package/lib/features/devices/brands/renphoTape.js.map +1 -0
  25. package/lib/features/devices/brands/uuids.js +38 -0
  26. package/lib/features/devices/brands/uuids.js.map +1 -0
  27. package/lib/features/devices/catalog.js +40 -0
  28. package/lib/features/devices/catalog.js.map +1 -1
  29. package/lib/features/devices/deviceStore.js +5 -2
  30. package/lib/features/devices/deviceStore.js.map +1 -1
  31. package/lib/features/devices/drivers.js +5 -1
  32. package/lib/features/devices/drivers.js.map +1 -1
  33. package/lib/features/devices/gatt/ftms.js +129 -0
  34. package/lib/features/devices/gatt/ftms.js.map +1 -0
  35. package/lib/features/devices/gatt/uuids.js +8 -2
  36. package/lib/features/devices/gatt/uuids.js.map +1 -1
  37. package/lib/features/devices/link.js.map +1 -1
  38. package/lib/features/devices/readings.js +32 -1
  39. package/lib/features/devices/readings.js.map +1 -1
  40. package/lib/features/devices/session.js +24 -5
  41. package/lib/features/devices/session.js.map +1 -1
  42. package/package.json +3 -3
@@ -0,0 +1,181 @@
1
+ import {readingId}from'../readings.js';import {parseRenphoFrame,u32be,u16be,be16,be32}from'./frames.js';import {openRenphoChannel}from'./channel.js';import {RENPHO_SCALE}from'./uuids.js';var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+ var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
18
+ const FINAL_STATUS = /* @__PURE__ */ new Set([1, 3, 17, 19]);
19
+ function encodeScaleUserConfig(person, now) {
20
+ var _a;
21
+ const sexBit = person.sex === "male" ? 16 : 32;
22
+ const {
23
+ year,
24
+ month,
25
+ day
26
+ } = birthParts(person, now);
27
+ const heightX10 = person.heightCm && person.heightCm >= 30 ? Math.round(person.heightCm * 10) : 1800;
28
+ const weightX100 = Math.round(((_a = person.weightKg) != null ? _a : 0) * 100);
29
+ const flags = 8 | 32 | 128;
30
+ return [
31
+ 9 | sexBit,
32
+ // slot 9 = visitor
33
+ ...be16(year),
34
+ month,
35
+ day,
36
+ ...be16(heightX10),
37
+ ...be32(weightX100),
38
+ flags,
39
+ 255,
40
+ // occupied-slot mask: visitor
41
+ 0
42
+ // fatMeasurement | secretKey
43
+ ];
44
+ }
45
+ function birthParts(person, now) {
46
+ var _a;
47
+ if (person.birthDate && /^\d{4}-\d{2}-\d{2}$/.test(person.birthDate)) {
48
+ const [y, m, d] = person.birthDate.split("-").map(Number);
49
+ return {
50
+ year: y,
51
+ month: m,
52
+ day: d
53
+ };
54
+ }
55
+ const year = new Date(now).getFullYear() - ((_a = person.age) != null ? _a : 30);
56
+ return {
57
+ year,
58
+ month: 1,
59
+ day: 1
60
+ };
61
+ }
62
+ function startRenphoScale(family, link, ctx) {
63
+ let channel = null;
64
+ const offline = new OfflineAssembler();
65
+ const emit = (r) => ctx.onReading(r);
66
+ const handleFrame = (frame) => {
67
+ var _a;
68
+ const p = frame.payload;
69
+ switch (frame.cmd) {
70
+ case 17:
71
+ void (channel == null ? void 0 : channel.send(145, [1]));
72
+ break;
73
+ case 18:
74
+ void (channel == null ? void 0 : channel.send(145, [1]));
75
+ break;
76
+ case 20: {
77
+ if (p.length >= 5) {
78
+ const status = p[0];
79
+ const weightKg = u32be(p, 1) / 100;
80
+ (_a = ctx.onLive) == null ? void 0 : _a.call(ctx, {
81
+ weightKg
82
+ });
83
+ if (family === "common" && FINAL_STATUS.has(status)) {
84
+ emit(bodyReading(link, ctx.now(), {
85
+ weightKg,
86
+ impedanceOhm: u16be(p, 5) || void 0
87
+ }));
88
+ } else if (family === "a017" && p.length >= 11) {
89
+ emit(bodyReading(link, ctx.now(), {
90
+ weightKg,
91
+ impedanceOhm: u16be(p, 5) || void 0,
92
+ bmi: u16be(p, 7) / 10 || void 0,
93
+ bodyFatPct: u16be(p, 9) / 10 || void 0
94
+ }));
95
+ }
96
+ }
97
+ if (p[0] === 1) void (channel == null ? void 0 : channel.send(148, new Array(10).fill(0)));
98
+ break;
99
+ }
100
+ case 21: {
101
+ void (channel == null ? void 0 : channel.send(149, [1]));
102
+ if (p.length >= 8) {
103
+ const weightKg = u32be(p, 0) / 100;
104
+ const secondsAgo = p.length >= 10 ? u32be(p, 6) : 0;
105
+ const at = secondsAgo ? ctx.now() - secondsAgo * 1e3 : ctx.now();
106
+ emit(bodyReading(link, at, {
107
+ weightKg,
108
+ impedanceOhm: u16be(p, 4) || void 0
109
+ }));
110
+ }
111
+ break;
112
+ }
113
+ case 24: {
114
+ void (channel == null ? void 0 : channel.send(152, [1]));
115
+ if (p.length >= 13) {
116
+ emit(bodyReading(link, ctx.now(), {
117
+ weightKg: u32be(p, 3) / 100,
118
+ impedanceOhm: u16be(p, 7) || void 0,
119
+ bmi: u16be(p, 9) / 10 || void 0,
120
+ bodyFatPct: u16be(p, 11) / 10 || void 0
121
+ }));
122
+ }
123
+ break;
124
+ }
125
+ case 25: {
126
+ void (channel == null ? void 0 : channel.send(153, [1]));
127
+ if (p.length >= 20) {
128
+ const at = u32be(p, 16) ? u32be(p, 16) * 1e3 : ctx.now();
129
+ emit(bodyReading(link, at, {
130
+ weightKg: u32be(p, 4) / 100,
131
+ impedanceOhm: u16be(p, 8) || void 0,
132
+ bmi: u16be(p, 10) / 10 || void 0,
133
+ bodyFatPct: u16be(p, 12) / 10 || void 0
134
+ }));
135
+ }
136
+ break;
137
+ }
138
+ }
139
+ };
140
+ channel = openRenphoChannel(link, RENPHO_SCALE, (frame) => handleFrame(frame));
141
+ const rawUnsub = family === "a017" ? link.monitor(RENPHO_SCALE.service, RENPHO_SCALE.notify, (bytes) => {
142
+ const frame = offline.push(bytes);
143
+ if (frame) handleFrame(frame);
144
+ }) : void 0;
145
+ void channel.send(150, encodeScaleUserConfig(ctx.person, ctx.now())).catch(() => void 0);
146
+ return {
147
+ stop() {
148
+ channel == null ? void 0 : channel.stop();
149
+ rawUnsub == null ? void 0 : rawUnsub();
150
+ }
151
+ };
152
+ }
153
+ function bodyReading(link, at, fields) {
154
+ return __spreadValues({
155
+ kind: "body",
156
+ id: readingId(link.id, "body", at),
157
+ at,
158
+ deviceId: link.id,
159
+ deviceName: link.name
160
+ }, fields);
161
+ }
162
+ class OfflineAssembler {
163
+ constructor() {
164
+ __publicField(this, "parts", /* @__PURE__ */ new Map());
165
+ }
166
+ push(bytes) {
167
+ var _a;
168
+ const first = bytes[0];
169
+ if ((first & 253) !== 173) return null;
170
+ const rec = bytes[1];
171
+ const existing = (_a = this.parts.get(rec)) != null ? _a : [];
172
+ const next = [...existing, ...Array.from(bytes.subarray(3))];
173
+ this.parts.set(rec, next);
174
+ const frame = parseRenphoFrame(Uint8Array.from(next));
175
+ if (frame) {
176
+ this.parts.delete(rec);
177
+ return frame;
178
+ }
179
+ return null;
180
+ }
181
+ }export{encodeScaleUserConfig,startRenphoScale};//# sourceMappingURL=renphoScale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renphoScale.js","sources":["../../../../src/features/devices/brands/renphoScale.ts"],"sourcesContent":["/**\n * RENPHO body-fat scales over the common frame (PROTOCOLS.md sections 5, 5a).\n *\n * Two families share this driver:\n * - \"common\" (R-A001/2/9, ES-24M ...): the scale sends weight and impedance;\n * Yantra's own estimate turns impedance into body composition later.\n * - \"R-A017\" (R-A017, ES-CS20M, ES-32MD, R-A031, R-MSB0x ...): the scale\n * computes body fat and BMI itself and sends them, so Yantra keeps them.\n *\n * Both send the person's profile to the scale first (a visitor-mode user\n * config, so no cloud seat is needed) so a fat-computing scale has the inputs.\n */\nimport type { DriverContext, DriverHandle, GattLink, PersonFacts } from '../drivers';\nimport { readingId, type BodyReading } from '../readings';\nimport { be16, be32, parseRenphoFrame, u16be, u32be, type Frame } from './frames';\nimport { openRenphoChannel, type RenphoChannel } from './channel';\nimport { RENPHO_SCALE } from './uuids';\n\ntype Family = 'common' | 'a017';\n\n/** Weight is final (settled) when the status byte is one of these. */\nconst FINAL_STATUS = new Set([1, 3, 17, 19]);\n\n/** The 14-byte visitor-mode user config the scale needs to compute body fat. */\nexport function encodeScaleUserConfig(person: PersonFacts, now: number): number[] {\n const sexBit = person.sex === 'male' ? 0x10 : 0x20;\n const { year, month, day } = birthParts(person, now);\n const heightX10 = person.heightCm && person.heightCm >= 30 ? Math.round(person.heightCm * 10) : 1800;\n const weightX100 = Math.round((person.weightKg ?? 0) * 100);\n // flags: method auto (0) | region other (0x08) | body-age default (0x20) | person normal (0x80).\n const flags = 0x08 | 0x20 | 0x80;\n return [\n 9 | sexBit, // slot 9 = visitor\n ...be16(year),\n month,\n day,\n ...be16(heightX10),\n ...be32(weightX100),\n flags,\n 0xff, // occupied-slot mask: visitor\n 0x00, // fatMeasurement | secretKey\n ];\n}\n\nfunction birthParts(person: PersonFacts, now: number): { year: number; month: number; day: number } {\n if (person.birthDate && /^\\d{4}-\\d{2}-\\d{2}$/.test(person.birthDate)) {\n const [y, m, d] = person.birthDate.split('-').map(Number);\n return { year: y, month: m, day: d };\n }\n const year = new Date(now).getFullYear() - (person.age ?? 30);\n return { year, month: 1, day: 1 };\n}\n\nexport function startRenphoScale(family: Family, link: GattLink, ctx: DriverContext): DriverHandle {\n let channel: RenphoChannel | null = null;\n // The A017 family splits stored records into AD/AF chunks before they become a frame.\n const offline = new OfflineAssembler();\n let lastWeight: number | undefined;\n\n const emit = (r: BodyReading) => ctx.onReading(r);\n\n const handleFrame = (frame: Frame) => {\n const p = frame.payload;\n switch (frame.cmd) {\n case 0x11: // scale state (power, battery)\n void channel?.send(0x91, [0x01]);\n break;\n case 0x12: // device config / state (A017 battery)\n void channel?.send(0x91, [0x01]);\n break;\n case 0x14: {\n // Realtime weight.\n if (p.length >= 5) {\n const status = p[0];\n const weightKg = u32be(p, 1) / 100;\n lastWeight = weightKg;\n ctx.onLive?.({ weightKg });\n if (family === 'common' && FINAL_STATUS.has(status)) {\n emit(bodyReading(link, ctx.now(), { weightKg, impedanceOhm: u16be(p, 5) || undefined }));\n } else if (family === 'a017' && p.length >= 11) {\n emit(\n bodyReading(link, ctx.now(), {\n weightKg,\n impedanceOhm: u16be(p, 5) || undefined,\n bmi: u16be(p, 7) / 10 || undefined,\n bodyFatPct: u16be(p, 9) / 10 || undefined,\n }),\n );\n }\n }\n if (p[0] === 1) void channel?.send(0x94, new Array(10).fill(0));\n break;\n }\n case 0x15: {\n // Offline record (common family).\n void channel?.send(0x95, [0x01]);\n if (p.length >= 8) {\n const weightKg = u32be(p, 0) / 100;\n const secondsAgo = p.length >= 10 ? u32be(p, 6) : 0;\n const at = secondsAgo ? ctx.now() - secondsAgo * 1000 : ctx.now();\n emit(bodyReading(link, at, { weightKg, impedanceOhm: u16be(p, 4) || undefined }));\n }\n break;\n }\n case 0x18: {\n // A017 steady (final) reading.\n void channel?.send(0x98, [0x01]);\n if (p.length >= 13) {\n emit(\n bodyReading(link, ctx.now(), {\n weightKg: u32be(p, 3) / 100,\n impedanceOhm: u16be(p, 7) || undefined,\n bmi: u16be(p, 9) / 10 || undefined,\n bodyFatPct: u16be(p, 11) / 10 || undefined,\n }),\n );\n }\n break;\n }\n case 0x19: {\n // A017 stored record (reassembled from AD/AF chunks).\n void channel?.send(0x99, [0x01]);\n if (p.length >= 20) {\n const at = u32be(p, 16) ? u32be(p, 16) * 1000 : ctx.now();\n emit(\n bodyReading(link, at, {\n weightKg: u32be(p, 4) / 100,\n impedanceOhm: u16be(p, 8) || undefined,\n bmi: u16be(p, 10) / 10 || undefined,\n bodyFatPct: u16be(p, 12) / 10 || undefined,\n }),\n );\n }\n break;\n }\n default:\n break;\n }\n };\n\n channel = openRenphoChannel(link, RENPHO_SCALE, (frame) => handleFrame(frame));\n\n // On the A017 family the stored records arrive as AD/AF chunks outside the frame layer.\n const rawUnsub =\n family === 'a017'\n ? link.monitor(RENPHO_SCALE.service, RENPHO_SCALE.notify, (bytes) => {\n const frame = offline.push(bytes);\n if (frame) handleFrame(frame);\n })\n : undefined;\n\n // Send the profile so a fat-computing scale has the inputs.\n void channel.send(0x96, encodeScaleUserConfig(ctx.person, ctx.now())).catch(() => undefined);\n\n return {\n stop() {\n channel?.stop();\n rawUnsub?.();\n void lastWeight; // referenced only to keep live weight on screen\n },\n };\n}\n\nfunction bodyReading(\n link: GattLink,\n at: number,\n fields: Pick<BodyReading, 'weightKg' | 'bodyFatPct' | 'bmi' | 'impedanceOhm'>,\n): BodyReading {\n return {\n kind: 'body',\n id: readingId(link.id, 'body', at),\n at,\n deviceId: link.id,\n deviceName: link.name,\n ...fields,\n };\n}\n\n/**\n * The A017 family sends a stored record split as `AD <rec> <part> <17 bytes>`\n * plus `AF <rec> <part> <9 bytes>`; concatenating their 17 + 9 payload bytes\n * gives one common frame (cmd 0x19). This gathers the pieces per record id.\n */\nclass OfflineAssembler {\n private parts = new Map<number, number[]>();\n\n push(bytes: Uint8Array): Frame | null {\n const first = bytes[0];\n if ((first & 0xfd) !== 0xad) return null; // not AD or AF\n const rec = bytes[1];\n const existing = this.parts.get(rec) ?? [];\n // AD carries a 17-byte slice, AF a 9-byte slice; both start at byte 3.\n const next = [...existing, ...Array.from(bytes.subarray(3))];\n this.parts.set(rec, next);\n const frame = parseRenphoFrame(Uint8Array.from(next));\n if (frame) {\n this.parts.delete(rec);\n return frame;\n }\n return null;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import {readingId}from'../readings.js';import {u16be}from'./frames.js';import {openRenphoChannel}from'./channel.js';import {RENPHO_OLD_TAPE,RENPHO_COMMON}from'./uuids.js';const TAPE_BODY_PARTS = ["neck", "shoulder", "right_arm", "left_arm", "chest", "waist", "abdomen", "hip", "right_thigh", "left_thigh", "right_calf", "left_calf", "custom", "custom1", "custom2", "custom3", "custom4", "custom5", "whr"];
2
+ function bodyPartName(index) {
3
+ var _a;
4
+ return (_a = TAPE_BODY_PARTS[index - 1]) != null ? _a : `part_${index}`;
5
+ }
6
+ const IN_TO_CM = 2.54;
7
+ function startOldTape(link, ctx) {
8
+ let lastLine = "";
9
+ const unsub = link.monitor(RENPHO_OLD_TAPE.service, RENPHO_OLD_TAPE.notify, (bytes) => {
10
+ var _a;
11
+ if (bytes.length < 20 || bytes[0] !== 42 || bytes[19] !== 10) return;
12
+ const line = String.fromCharCode(...bytes.subarray(1, 6));
13
+ if (line === lastLine) return;
14
+ lastLine = line;
15
+ const value = Number(line);
16
+ if (!Number.isFinite(value)) return;
17
+ const inches = bytes[18] === 73;
18
+ const cm = inches ? value * IN_TO_CM : value;
19
+ (_a = ctx.onLive) == null ? void 0 : _a.call(ctx, {
20
+ lengthCm: cm
21
+ });
22
+ });
23
+ return {
24
+ stop: unsub
25
+ };
26
+ }
27
+ function startNewTape(link, ctx) {
28
+ let channel = null;
29
+ let lastSavedId = "";
30
+ const handle = (frame) => {
31
+ var _a;
32
+ if (frame.cmd !== 20 && frame.cmd !== 149 && (frame.cmd < 144 || frame.cmd > 147)) return;
33
+ const p = frame.payload;
34
+ if (p.length < 7) return;
35
+ const unit = p[5];
36
+ const cm = u16be(p, 0) / 100 * (unit === 1 ? IN_TO_CM : 1);
37
+ const part = bodyPartName(p[3]);
38
+ const status = p[4];
39
+ (_a = ctx.onLive) == null ? void 0 : _a.call(ctx, {
40
+ lengthCm: cm,
41
+ bodyPart: part,
42
+ battery: p.length > 10 ? p[10] : void 0
43
+ });
44
+ if (status === 2) {
45
+ const at = ctx.now();
46
+ const id = readingId(link.id, "body-measurement", at, part);
47
+ if (id === lastSavedId) return;
48
+ lastSavedId = id;
49
+ const reading = {
50
+ kind: "body-measurement",
51
+ id,
52
+ at,
53
+ deviceId: link.id,
54
+ deviceName: link.name,
55
+ bodyPart: part,
56
+ cm
57
+ };
58
+ ctx.onReading(reading);
59
+ }
60
+ };
61
+ channel = openRenphoChannel(link, RENPHO_COMMON, handle);
62
+ void channel.send(149, [0]).catch(() => void 0);
63
+ return {
64
+ stop() {
65
+ channel == null ? void 0 : channel.stop();
66
+ }
67
+ };
68
+ }export{TAPE_BODY_PARTS,bodyPartName,startNewTape,startOldTape};//# sourceMappingURL=renphoTape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renphoTape.js","sources":["../../../../src/features/devices/brands/renphoTape.ts"],"sourcesContent":["/**\n * RENPHO smart tape measures (PROTOCOLS.md section 10).\n *\n * - Old ES-Tape: its own service, a 20-byte ASCII line per reading. No body\n * part, so Yantra shows the length live and does not store it (the person\n * chooses what they measured in the app).\n * - New R-Y002/R-Y001: common frames that carry the body part, so a saved\n * measurement becomes a body-measurement reading.\n */\nimport type { DriverContext, DriverHandle, GattLink } from '../drivers';\nimport { readingId, type BodyMeasurementReading } from '../readings';\nimport { u16be, type Frame } from './frames';\nimport { openRenphoChannel, type RenphoChannel } from './channel';\nimport { RENPHO_COMMON, RENPHO_OLD_TAPE } from './uuids';\n\n/** Body part numbers the new tape reports (1-indexed), from the app's table. */\nexport const TAPE_BODY_PARTS: readonly string[] = [\n 'neck',\n 'shoulder',\n 'right_arm',\n 'left_arm',\n 'chest',\n 'waist',\n 'abdomen',\n 'hip',\n 'right_thigh',\n 'left_thigh',\n 'right_calf',\n 'left_calf',\n 'custom',\n 'custom1',\n 'custom2',\n 'custom3',\n 'custom4',\n 'custom5',\n 'whr',\n];\n\nexport function bodyPartName(index: number): string {\n return TAPE_BODY_PARTS[index - 1] ?? `part_${index}`;\n}\n\nconst IN_TO_CM = 2.54;\n\n// --------------------------------------------------------------- Old ES-Tape\n\nexport function startOldTape(link: GattLink, ctx: DriverContext): DriverHandle {\n let lastLine = '';\n const unsub = link.monitor(RENPHO_OLD_TAPE.service, RENPHO_OLD_TAPE.notify, (bytes) => {\n if (bytes.length < 20 || bytes[0] !== 0x2a /* '*' */ || bytes[19] !== 0x0a /* '\\n' */) return;\n const line = String.fromCharCode(...bytes.subarray(1, 6));\n if (line === lastLine) return;\n lastLine = line;\n const value = Number(line);\n if (!Number.isFinite(value)) return;\n const inches = bytes[18] === 0x49; // 'I'\n const cm = inches ? value * IN_TO_CM : value;\n ctx.onLive?.({ lengthCm: cm });\n });\n return { stop: unsub };\n}\n\n// -------------------------------------------------------------- New R-Y002\n\nexport function startNewTape(link: GattLink, ctx: DriverContext): DriverHandle {\n let channel: RenphoChannel | null = null;\n let lastSavedId = '';\n\n const handle = (frame: Frame) => {\n // Measurement frames: 0x14 and 0x90..0x93, 0x95.\n if (frame.cmd !== 0x14 && frame.cmd !== 0x95 && (frame.cmd < 0x90 || frame.cmd > 0x93)) return;\n const p = frame.payload;\n if (p.length < 7) return;\n const unit = p[5];\n const cm = (u16be(p, 0) / 100) * (unit === 1 ? IN_TO_CM : 1);\n const part = bodyPartName(p[3]);\n const status = p[4]; // 0 measuring, 1 confirm, 2 saved\n ctx.onLive?.({ lengthCm: cm, bodyPart: part, battery: p.length > 10 ? p[10] : undefined });\n if (status === 2) {\n const at = ctx.now();\n const id = readingId(link.id, 'body-measurement', at, part);\n if (id === lastSavedId) return;\n lastSavedId = id;\n const reading: BodyMeasurementReading = {\n kind: 'body-measurement',\n id,\n at,\n deviceId: link.id,\n deviceName: link.name,\n bodyPart: part,\n cm,\n };\n ctx.onReading(reading);\n }\n };\n\n channel = openRenphoChannel(link, RENPHO_COMMON, handle);\n // Wake the machine and ask for its state.\n void channel.send(0x95, [0x00]).catch(() => undefined);\n return {\n stop() {\n channel?.stop();\n },\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import {uuid16}from'../gatt/uuids.js';const RENPHO_SCALE = {
2
+ service: uuid16("1a10"),
3
+ write: uuid16("2a11"),
4
+ notify: uuid16("2a10")
5
+ };
6
+ const RENPHO_BP = {
7
+ service: uuid16("1a10"),
8
+ write: uuid16("1a11"),
9
+ notify: uuid16("1a12"),
10
+ indicate: uuid16("1a13")
11
+ };
12
+ const RENPHO_COMMON = {
13
+ service: uuid16("1a10"),
14
+ write: uuid16("1a11"),
15
+ notify: uuid16("1a12")
16
+ };
17
+ const RENPHO_ROPE = {
18
+ service: "00005301-0000-0041-4c50-574953450000",
19
+ write: "00005302-0000-0041-4c50-574953450000",
20
+ notify: "00005303-0000-0041-4c50-574953450000"
21
+ };
22
+ const RENPHO_OLD_TAPE = {
23
+ service: "0783b03e-8535-b5a0-7140-a304d2495cb7",
24
+ notify: "0783b03e-8535-b5a0-7140-a304d2495cb8"
25
+ };
26
+ const RENPHO_RING = {
27
+ service: uuid16("fff0"),
28
+ write: uuid16("fff6"),
29
+ notify: uuid16("fff7")
30
+ };
31
+ const QN = {
32
+ service: uuid16("ffe0"),
33
+ serviceAlt: uuid16("fff0"),
34
+ notify: uuid16("ffe1"),
35
+ write13: uuid16("ffe3"),
36
+ write20: uuid16("ffe4"),
37
+ writeAlt: uuid16("fff2")
38
+ };export{QN,RENPHO_BP,RENPHO_COMMON,RENPHO_OLD_TAPE,RENPHO_RING,RENPHO_ROPE,RENPHO_SCALE};//# sourceMappingURL=uuids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uuids.js","sources":["../../../../src/features/devices/brands/uuids.ts"],"sourcesContent":["/**\n * The service and characteristic UUIDs each RENPHO / QN protocol uses, from\n * the devices' own handlers (initWithDeviceModel, scanSubscribeCharacteristic;\n * ~/Downloads/renpho-rea/PROTOCOLS.md).\n *\n * react-native-ble-plx reports and takes full 128-bit lowercase UUIDs, so the\n * 16-bit ones are expanded; the two custom services keep their literal value.\n */\nimport { uuid16 } from '../gatt/uuids';\n\n/** RENPHO common-frame body scale (service 1A10, write 2A11 with response, notify 2A10). */\nexport const RENPHO_SCALE = {\n service: uuid16('1a10'),\n write: uuid16('2a11'),\n notify: uuid16('2a10'),\n} as const;\n\n/** RENPHO blood pressure (service 1A10, write 1A11, notify 1A12, indicate 1A13). */\nexport const RENPHO_BP = {\n service: uuid16('1a10'),\n write: uuid16('1a11'),\n notify: uuid16('1a12'),\n indicate: uuid16('1a13'),\n} as const;\n\n/** RENPHO food scale and newer tape measure (service 1A10, write 1A11, notify 1A12). */\nexport const RENPHO_COMMON = {\n service: uuid16('1a10'),\n write: uuid16('1a11'),\n notify: uuid16('1a12'),\n} as const;\n\n/** Jump rope (Alpwise service). */\nexport const RENPHO_ROPE = {\n service: '00005301-0000-0041-4c50-574953450000',\n write: '00005302-0000-0041-4c50-574953450000',\n notify: '00005303-0000-0041-4c50-574953450000',\n} as const;\n\n/** Old ES-Tape. */\nexport const RENPHO_OLD_TAPE = {\n service: '0783b03e-8535-b5a0-7140-a304d2495cb7',\n notify: '0783b03e-8535-b5a0-7140-a304d2495cb8',\n} as const;\n\n/** Smart ring (service FFF0, write FFF6, notify FFF7). */\nexport const RENPHO_RING = {\n service: uuid16('fff0'),\n write: uuid16('fff6'),\n notify: uuid16('fff7'),\n} as const;\n\n/** QN / Yolanda scale. Notify FFE1; writes go to FFE3/FFE4, or FFF2 in the FFF0 service. */\nexport const QN = {\n service: uuid16('ffe0'),\n serviceAlt: uuid16('fff0'),\n notify: uuid16('ffe1'),\n write13: uuid16('ffe3'),\n write20: uuid16('ffe4'),\n writeAlt: uuid16('fff2'),\n} as const;\n"],"names":[],"mappings":"sCAWO,MAAM,YAAe,GAAA;AAAA,EAC1B,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,KAAA,EAAO,OAAO,MAAM,CAAA;AAAA,EACpB,MAAA,EAAQ,OAAO,MAAM;AACvB;AAGO,MAAM,SAAY,GAAA;AAAA,EACvB,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,KAAA,EAAO,OAAO,MAAM,CAAA;AAAA,EACpB,MAAA,EAAQ,OAAO,MAAM,CAAA;AAAA,EACrB,QAAA,EAAU,OAAO,MAAM;AACzB;AAGO,MAAM,aAAgB,GAAA;AAAA,EAC3B,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,KAAA,EAAO,OAAO,MAAM,CAAA;AAAA,EACpB,MAAA,EAAQ,OAAO,MAAM;AACvB;AAGO,MAAM,WAAc,GAAA;AAAA,EACzB,OAAS,EAAA,sCAAA;AAAA,EACT,KAAO,EAAA,sCAAA;AAAA,EACP,MAAQ,EAAA;AACV;AAGO,MAAM,eAAkB,GAAA;AAAA,EAC7B,OAAS,EAAA,sCAAA;AAAA,EACT,MAAQ,EAAA;AACV;AAGO,MAAM,WAAc,GAAA;AAAA,EACzB,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,KAAA,EAAO,OAAO,MAAM,CAAA;AAAA,EACpB,MAAA,EAAQ,OAAO,MAAM;AACvB;AAGO,MAAM,EAAK,GAAA;AAAA,EAChB,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,UAAA,EAAY,OAAO,MAAM,CAAA;AAAA,EACzB,MAAA,EAAQ,OAAO,MAAM,CAAA;AAAA,EACrB,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,QAAA,EAAU,OAAO,MAAM;AACzB"}
@@ -46,6 +46,46 @@ import {SERVICE,sameUuid}from'./gatt/uuids.js';const DEVICE_KINDS = [{
46
46
  services: [SERVICE.glucose],
47
47
  steps: ["Turn the meter on. Readings it already stored come over too."],
48
48
  waitingFor: "Take a reading, or wait for the stored ones"
49
+ }, {
50
+ id: "kitchen-scale",
51
+ title: "Kitchen scale",
52
+ subtitle: "Food weight",
53
+ icon: "restaurant-outline",
54
+ services: [],
55
+ steps: ["Turn the scale on and put something on it."],
56
+ waitingFor: "Put food on the scale"
57
+ }, {
58
+ id: "tape",
59
+ title: "Smart tape",
60
+ subtitle: "Body measurements",
61
+ icon: "resize-outline",
62
+ services: [],
63
+ steps: ["Pull the tape out around the body part to wake it.", "Press to save when the number settles."],
64
+ waitingFor: "Take a measurement"
65
+ }, {
66
+ id: "jump-rope",
67
+ title: "Smart jump rope",
68
+ subtitle: "Jumps, time, calories",
69
+ icon: "ellipse-outline",
70
+ services: [],
71
+ steps: ["Press the button to wake the rope.", "Start skipping; the count comes over live."],
72
+ waitingFor: "Start skipping"
73
+ }, {
74
+ id: "treadmill",
75
+ title: "Smart treadmill",
76
+ subtitle: "Speed, distance, calories",
77
+ icon: "walk-outline",
78
+ services: [SERVICE.fitnessMachine],
79
+ steps: ["Turn the treadmill on and start a walk or run."],
80
+ waitingFor: "Start the treadmill"
81
+ }, {
82
+ id: "ring",
83
+ title: "Smart ring",
84
+ subtitle: "Heart rate, oxygen, sleep",
85
+ icon: "ellipse-outline",
86
+ services: [],
87
+ steps: ["Wear the ring and keep it near the phone.", "Its stored days sync when it connects."],
88
+ waitingFor: "Syncing the ring"
49
89
  }];
50
90
  function deviceKind(id) {
51
91
  var _a;
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sources":["../../../src/features/devices/catalog.ts"],"sourcesContent":["/**\n * The kinds of device the \"Add a device\" picker offers, what each speaks, and\n * the plain steps that wake it up. Only the Bluetooth SIG health profiles:\n * a brand device with its own protocol keeps its own app, which writes to\n * Apple Health / Health Connect, and Yantra reads it there.\n */\n\nimport { SERVICE, sameUuid } from './gatt/uuids';\n\nexport type DeviceKindId = 'scale' | 'blood-pressure' | 'heart-rate' | 'pulse-oximeter' | 'thermometer' | 'glucose';\n\nexport interface DeviceKind {\n id: DeviceKindId;\n title: string;\n subtitle: string;\n /** Ionicons glyph. */\n icon: string;\n /** Advertised services that identify it, in preference order. */\n services: string[];\n /** How to make it show up, one short line each. */\n steps: string[];\n /** What the waiting screen says once connected. */\n waitingFor: string;\n}\n\nexport const DEVICE_KINDS: readonly DeviceKind[] = [\n {\n id: 'scale',\n title: 'Scale',\n subtitle: 'Weight, body fat, BMI',\n icon: 'body-outline',\n services: [SERVICE.bodyComposition, SERVICE.weightScale],\n steps: ['Step on the scale barefoot to wake it up.', 'Stay on it until the number stops changing.'],\n waitingFor: 'Step on the scale',\n },\n {\n id: 'blood-pressure',\n title: 'Blood pressure monitor',\n subtitle: 'Cuff on the arm or wrist',\n icon: 'speedometer-outline',\n services: [SERVICE.bloodPressure],\n steps: [\n 'Sit for a few minutes, cuff on your upper arm at heart level.',\n 'Press start. The reading comes over when it finishes.',\n ],\n waitingFor: 'Take a measurement',\n },\n {\n id: 'heart-rate',\n title: 'Heart-rate monitor',\n subtitle: 'Chest strap or armband',\n icon: 'heart-outline',\n services: [SERVICE.heartRate],\n steps: ['Wet the sensor pads and put the strap on.', 'It wakes up when it touches your skin.'],\n waitingFor: 'Put the strap on',\n },\n {\n id: 'pulse-oximeter',\n title: 'Pulse oximeter',\n subtitle: 'Blood oxygen and pulse',\n icon: 'pulse-outline',\n services: [SERVICE.pulseOximeter],\n steps: ['Put a finger in and keep your hand still.'],\n waitingFor: 'Keep your finger in',\n },\n {\n id: 'thermometer',\n title: 'Thermometer',\n subtitle: 'Body temperature',\n icon: 'thermometer-outline',\n services: [SERVICE.healthThermometer],\n steps: ['Turn it on and take your temperature.'],\n waitingFor: 'Take your temperature',\n },\n {\n id: 'glucose',\n title: 'Glucose meter',\n subtitle: 'Blood sugar',\n icon: 'water-outline',\n services: [SERVICE.glucose],\n steps: ['Turn the meter on. Readings it already stored come over too.'],\n waitingFor: 'Take a reading, or wait for the stored ones',\n },\n];\n\nexport function deviceKind(id: DeviceKindId): DeviceKind {\n return DEVICE_KINDS.find((k) => k.id === id) ?? DEVICE_KINDS[0];\n}\n\n/** Every service the picker scans for when the person does not say what the device is. */\nexport const ALL_HEALTH_SERVICES = [...new Set(DEVICE_KINDS.flatMap((k) => k.services))];\n\n/** Which kind a device is, from the services it advertises or offers; undefined when none is a health profile. */\nexport function kindForServices(services: readonly string[] | null | undefined): DeviceKindId | undefined {\n if (!services?.length) return undefined;\n return DEVICE_KINDS.find((k) => k.services.some((s) => services.some((x) => sameUuid(x, s))))?.id;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","sources":["../../../src/features/devices/catalog.ts"],"sourcesContent":["/**\n * The kinds of device the \"Add a device\" picker offers, what each speaks, and\n * the plain steps that wake it up.\n *\n * The first group speaks the Bluetooth SIG health profiles, which many brands\n * share. The brand group (scale, kitchen scale, tape, jump rope, treadmill,\n * ring, blood pressure) also covers devices that speak their maker's own\n * protocol: the scan recognises those from their advertisement (brands/detect.ts)\n * and the session runs the matching brand driver (brands/index.ts).\n */\n\nimport { SERVICE, sameUuid } from './gatt/uuids';\n\nexport type DeviceKindId =\n | 'scale'\n | 'blood-pressure'\n | 'heart-rate'\n | 'pulse-oximeter'\n | 'thermometer'\n | 'glucose'\n | 'kitchen-scale'\n | 'tape'\n | 'jump-rope'\n | 'treadmill'\n | 'ring';\n\nexport interface DeviceKind {\n id: DeviceKindId;\n title: string;\n subtitle: string;\n /** Ionicons glyph. */\n icon: string;\n /** Advertised services that identify it, in preference order. Empty = brand-only (recognised by advertisement). */\n services: string[];\n /** How to make it show up, one short line each. */\n steps: string[];\n /** What the waiting screen says once connected. */\n waitingFor: string;\n}\n\nexport const DEVICE_KINDS: readonly DeviceKind[] = [\n {\n id: 'scale',\n title: 'Scale',\n subtitle: 'Weight, body fat, BMI',\n icon: 'body-outline',\n services: [SERVICE.bodyComposition, SERVICE.weightScale],\n steps: ['Step on the scale barefoot to wake it up.', 'Stay on it until the number stops changing.'],\n waitingFor: 'Step on the scale',\n },\n {\n id: 'blood-pressure',\n title: 'Blood pressure monitor',\n subtitle: 'Cuff on the arm or wrist',\n icon: 'speedometer-outline',\n services: [SERVICE.bloodPressure],\n steps: [\n 'Sit for a few minutes, cuff on your upper arm at heart level.',\n 'Press start. The reading comes over when it finishes.',\n ],\n waitingFor: 'Take a measurement',\n },\n {\n id: 'heart-rate',\n title: 'Heart-rate monitor',\n subtitle: 'Chest strap or armband',\n icon: 'heart-outline',\n services: [SERVICE.heartRate],\n steps: ['Wet the sensor pads and put the strap on.', 'It wakes up when it touches your skin.'],\n waitingFor: 'Put the strap on',\n },\n {\n id: 'pulse-oximeter',\n title: 'Pulse oximeter',\n subtitle: 'Blood oxygen and pulse',\n icon: 'pulse-outline',\n services: [SERVICE.pulseOximeter],\n steps: ['Put a finger in and keep your hand still.'],\n waitingFor: 'Keep your finger in',\n },\n {\n id: 'thermometer',\n title: 'Thermometer',\n subtitle: 'Body temperature',\n icon: 'thermometer-outline',\n services: [SERVICE.healthThermometer],\n steps: ['Turn it on and take your temperature.'],\n waitingFor: 'Take your temperature',\n },\n {\n id: 'glucose',\n title: 'Glucose meter',\n subtitle: 'Blood sugar',\n icon: 'water-outline',\n services: [SERVICE.glucose],\n steps: ['Turn the meter on. Readings it already stored come over too.'],\n waitingFor: 'Take a reading, or wait for the stored ones',\n },\n {\n id: 'kitchen-scale',\n title: 'Kitchen scale',\n subtitle: 'Food weight',\n icon: 'restaurant-outline',\n services: [],\n steps: ['Turn the scale on and put something on it.'],\n waitingFor: 'Put food on the scale',\n },\n {\n id: 'tape',\n title: 'Smart tape',\n subtitle: 'Body measurements',\n icon: 'resize-outline',\n services: [],\n steps: ['Pull the tape out around the body part to wake it.', 'Press to save when the number settles.'],\n waitingFor: 'Take a measurement',\n },\n {\n id: 'jump-rope',\n title: 'Smart jump rope',\n subtitle: 'Jumps, time, calories',\n icon: 'ellipse-outline',\n services: [],\n steps: ['Press the button to wake the rope.', 'Start skipping; the count comes over live.'],\n waitingFor: 'Start skipping',\n },\n {\n id: 'treadmill',\n title: 'Smart treadmill',\n subtitle: 'Speed, distance, calories',\n icon: 'walk-outline',\n services: [SERVICE.fitnessMachine],\n steps: ['Turn the treadmill on and start a walk or run.'],\n waitingFor: 'Start the treadmill',\n },\n {\n id: 'ring',\n title: 'Smart ring',\n subtitle: 'Heart rate, oxygen, sleep',\n icon: 'ellipse-outline',\n services: [],\n steps: ['Wear the ring and keep it near the phone.', 'Its stored days sync when it connects.'],\n waitingFor: 'Syncing the ring',\n },\n];\n\nexport function deviceKind(id: DeviceKindId): DeviceKind {\n return DEVICE_KINDS.find((k) => k.id === id) ?? DEVICE_KINDS[0];\n}\n\n/** Every service the picker scans for when the person does not say what the device is. */\nexport const ALL_HEALTH_SERVICES = [...new Set(DEVICE_KINDS.flatMap((k) => k.services))];\n\n/** Which kind a device is, from the services it advertises or offers; undefined when none is a health profile. */\nexport function kindForServices(services: readonly string[] | null | undefined): DeviceKindId | undefined {\n if (!services?.length) return undefined;\n return DEVICE_KINDS.find((k) => k.services.some((s) => services.some((x) => sameUuid(x, s))))?.id;\n}\n"],"names":[],"mappings":"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"}
@@ -129,10 +129,13 @@ class DeviceStore {
129
129
  async pair(device) {
130
130
  await this.load();
131
131
  const existing = this.snapshot.devices.find((d) => d.id === device.id);
132
- const devices = existing ? this.snapshot.devices.map((d) => d.id === device.id ? __spreadProps(__spreadValues({}, d), {
132
+ const protocol = device.protocol !== void 0 ? {
133
+ protocol: device.protocol
134
+ } : {};
135
+ const devices = existing ? this.snapshot.devices.map((d) => d.id === device.id ? __spreadValues(__spreadProps(__spreadValues({}, d), {
133
136
  name: device.name,
134
137
  kind: device.kind
135
- }) : d) : [...this.snapshot.devices, __spreadProps(__spreadValues({}, device), {
138
+ }), protocol) : d) : [...this.snapshot.devices, __spreadProps(__spreadValues(__spreadValues({}, device), protocol), {
136
139
  pairedAt: this.now()
137
140
  })];
138
141
  await this.persist({
@@ -1 +1 @@
1
- {"version":3,"file":"deviceStore.js","sources":["../../../src/features/devices/deviceStore.ts"],"sourcesContent":["/**\n * The person's Bluetooth devices and everything they measured, kept on the\n * phone, and where those readings go:\n *\n * 1. The Fitness backend, as source `device`. It keeps one day per source and\n * replaces it whole, so an upload rebuilds every touched date from ALL the\n * readings kept here (readings.ts deviceHealthDays). Dates still to send\n * survive a restart (`pendingDates`) and go with the next session.\n * 2. Apple Health / Health Connect, when the person connected it in Yantra:\n * only what Yantra measured (writer.ts). Readings still to save survive a\n * restart too (`unsaved`).\n *\n * The record belongs to the signed-in account: another account signing in on\n * the same phone starts empty instead of inheriting someone else's devices.\n *\n * A plain external store (useSyncExternalStore), like healthSyncStore, so the\n * root runner and the screens share one instance.\n */\n\nimport { chunkDays } from '../health/buildDays';\nimport { createFitnessClient, type FitnessPerson } from '../health/fitnessClient';\nimport type { HealthSession } from '../health/healthSyncStore';\nimport type { StorageLike } from '../health/syncState';\nimport { deviceTimeZone, isValidTimeZone, zoneClock } from '../health/time';\nimport type { HealthWriter } from '../health/writer';\nimport type { DeviceKindId } from './catalog';\nimport type { ScaleUser } from './drivers';\nimport { compactSession, deviceHealthDays, type DeviceReading } from './readings';\n\nexport const DEVICE_STORE_KEY = 'yantra.devices.v1';\n/** Readings older than this are let go (the backend keeps its copy). */\nexport const KEEP_DAYS = 120;\nconst DAY_MS = 86_400_000;\n\nexport interface PairedDevice {\n /** The platform's id for the peripheral (iOS UUID, Android MAC). */\n id: string;\n name: string;\n kind: DeviceKindId;\n pairedAt: number;\n lastReadingAt?: number;\n /** The user slot a scale gave this person (drivers.ts). */\n scaleUser?: ScaleUser;\n}\n\nexport interface DeviceStoreState {\n version: 1;\n owner?: string;\n devices: PairedDevice[];\n readings: DeviceReading[];\n /** Local dates whose device day still has to be sent. */\n pendingDates: string[];\n /** Reading ids still to save to the phone's health store. */\n unsaved: string[];\n lastUploadAt?: number;\n /** Plain words from the last failed upload; cleared by a good one. */\n lastError?: string;\n}\n\nexport interface DeviceSnapshot extends DeviceStoreState {\n loaded: boolean;\n uploading: boolean;\n /** The person facts the drivers use (age, sex, height, units), once known. */\n person?: FitnessPerson;\n}\n\nconst EMPTY: DeviceStoreState = { version: 1, devices: [], readings: [], pendingDates: [], unsaved: [] };\n\nexport interface DeviceStoreDeps {\n storage: StorageLike;\n /** Null when this build cannot save to the phone's health store. */\n createWriter: () => Promise<HealthWriter | null>;\n /** Whether the person connected Apple Health / Health Connect in Yantra; saving follows it. */\n healthConnected: () => boolean;\n fetchImpl?: typeof fetch;\n now?: () => number;\n}\n\nexport class DeviceStore {\n private snapshot: DeviceSnapshot = { ...EMPTY, loaded: false, uploading: false };\n\n private listeners = new Set<() => void>();\n\n private loading: Promise<void> | null = null;\n\n private flushing: Promise<void> | null = null;\n\n private session: HealthSession | null = null;\n\n private writer: Promise<HealthWriter | null> | null = null;\n\n constructor(private readonly deps: DeviceStoreDeps) {}\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): DeviceSnapshot => this.snapshot;\n\n private now(): number {\n return this.deps.now ? this.deps.now() : Date.now();\n }\n\n private set(patch: Partial<DeviceSnapshot>) {\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n private async persist(patch: Partial<DeviceStoreState>) {\n this.set(patch);\n const { loaded: _l, uploading: _u, person: _p, ...state } = this.snapshot;\n try {\n await this.deps.storage.setItem(DEVICE_STORE_KEY, JSON.stringify(state));\n } catch {\n // A lost write only costs a re-send; readings are de-duplicated by id.\n }\n }\n\n load(): Promise<void> {\n if (!this.loading) {\n this.loading = (async () => {\n let state = EMPTY;\n try {\n const raw = await this.deps.storage.getItem(DEVICE_STORE_KEY);\n const parsed = raw ? (JSON.parse(raw) as Partial<DeviceStoreState>) : null;\n if (parsed?.version === 1) {\n state = {\n ...EMPTY,\n ...parsed,\n devices: Array.isArray(parsed.devices) ? parsed.devices : [],\n readings: Array.isArray(parsed.readings) ? parsed.readings : [],\n pendingDates: Array.isArray(parsed.pendingDates) ? parsed.pendingDates : [],\n unsaved: Array.isArray(parsed.unsaved) ? parsed.unsaved : [],\n };\n }\n } catch {\n state = EMPTY;\n }\n this.set({ ...state, loaded: true });\n })();\n }\n return this.loading;\n }\n\n /** The root runner hands over the sign-in session; anything waiting goes out. */\n setSession(session: HealthSession | null) {\n this.session = session;\n if (!session) return;\n void (async () => {\n await this.load();\n if (session.owner && this.snapshot.owner && session.owner !== this.snapshot.owner) {\n // Another account on this phone: its devices and readings are not this person's.\n await this.persist({ ...EMPTY, owner: session.owner });\n } else if (session.owner && !this.snapshot.owner) {\n await this.persist({ owner: session.owner });\n }\n await this.flush();\n })();\n }\n\n async pair(device: Pick<PairedDevice, 'id' | 'name' | 'kind'>): Promise<void> {\n await this.load();\n const existing = this.snapshot.devices.find((d) => d.id === device.id);\n const devices = existing\n ? this.snapshot.devices.map((d) =>\n d.id === device.id ? { ...d, name: device.name, kind: device.kind } : d,\n )\n : [...this.snapshot.devices, { ...device, pairedAt: this.now() }];\n await this.persist({ devices });\n }\n\n /** Forget the device; what it measured stays (it is the person's history). */\n async forget(id: string): Promise<void> {\n await this.load();\n await this.persist({ devices: this.snapshot.devices.filter((d) => d.id !== id) });\n }\n\n async saveScaleUser(id: string, user: ScaleUser | null): Promise<void> {\n await this.load();\n await this.persist({\n devices: this.snapshot.devices.map((d) => (d.id === id ? { ...d, scaleUser: user ?? undefined } : d)),\n });\n }\n\n /** Keep new readings (by id), then send and save them. Resolves with the ones that were new. */\n async addReadings(readings: readonly DeviceReading[]): Promise<DeviceReading[]> {\n await this.load();\n const known = new Set(this.snapshot.readings.map((r) => r.id));\n const fresh: DeviceReading[] = [];\n for (const r of readings) {\n // A device can repeat a record within one transfer too.\n if (known.has(r.id)) continue;\n known.add(r.id);\n fresh.push(r);\n }\n if (!fresh.length) return [];\n const clock = zoneClock(this.timeZone());\n const cutoff = this.now() - KEEP_DAYS * DAY_MS;\n const kept = [...this.snapshot.readings, ...fresh].filter((r) => r.at >= cutoff).sort((a, b) => a.at - b.at);\n const touched = new Set(fresh.map((r) => clock.dateOf(r.at)));\n const lastByDevice = new Map<string, number>();\n for (const r of fresh) lastByDevice.set(r.deviceId, Math.max(lastByDevice.get(r.deviceId) ?? 0, r.at));\n await this.persist({\n readings: kept,\n pendingDates: [...new Set([...this.snapshot.pendingDates, ...touched])].sort(),\n unsaved: [...new Set([...this.snapshot.unsaved, ...fresh.map((r) => r.id)])],\n devices: this.snapshot.devices.map((d) =>\n lastByDevice.has(d.id)\n ? { ...d, lastReadingAt: Math.max(d.lastReadingAt ?? 0, lastByDevice.get(d.id) ?? 0) }\n : d,\n ),\n });\n void this.flush();\n return fresh;\n }\n\n private timeZone(): string {\n const tz = this.snapshot.person?.timezone;\n return tz && isValidTimeZone(tz) ? tz : deviceTimeZone();\n }\n\n private getWriter(): Promise<HealthWriter | null> {\n if (!this.writer) this.writer = this.deps.createWriter().catch(() => null);\n return this.writer;\n }\n\n /** The person facts the drivers need; fetched once per session. */\n async person(): Promise<FitnessPerson | undefined> {\n if (this.snapshot.person) return this.snapshot.person;\n if (!this.session) return undefined;\n try {\n const person = await createFitnessClient({ ...this.session, fetchImpl: this.deps.fetchImpl }).me();\n this.set({ person });\n return person;\n } catch {\n return undefined;\n }\n }\n\n /** Send what is pending and save what is unsaved. Single-flight; safe to call any time. */\n flush(): Promise<void> {\n if (!this.flushing) {\n this.flushing = (async () => {\n try {\n await this.upload();\n await this.saveToHealth();\n } finally {\n this.flushing = null;\n }\n })();\n }\n return this.flushing;\n }\n\n private async upload(): Promise<void> {\n await this.load();\n const session = this.session;\n if (!session || !this.snapshot.pendingDates.length) return;\n this.set({ uploading: true });\n try {\n const person = await this.person();\n const birth = person?.birthDate ? Date.parse(`${person.birthDate}T00:00:00Z`) : NaN;\n const age = Number.isFinite(birth) ? (this.now() - birth) / (365.25 * DAY_MS) : undefined;\n const dates = new Set(this.snapshot.pendingDates);\n const { days } = deviceHealthDays(this.snapshot.readings, {\n clock: zoneClock(this.timeZone()),\n syncedAt: new Date(this.now()).toISOString(),\n maxHr: age ? 208 - 0.7 * age : undefined,\n dates,\n });\n const client = createFitnessClient({ ...session, fetchImpl: this.deps.fetchImpl });\n for (const chunk of chunkDays(days)) await client.ingest(chunk);\n await this.persist({\n pendingDates: this.snapshot.pendingDates.filter((d) => !dates.has(d)),\n lastUploadAt: this.now(),\n lastError: undefined,\n });\n } catch (err) {\n await this.persist({ lastError: err instanceof Error ? err.message : 'Could not send the readings.' });\n } finally {\n this.set({ uploading: false });\n }\n }\n\n private async saveToHealth(): Promise<void> {\n if (!this.snapshot.unsaved.length) return;\n if (!this.deps.healthConnected()) return;\n const writer = await this.getWriter();\n if (!writer) return;\n const unsaved = new Set(this.snapshot.unsaved);\n const done = new Set<string>();\n const birth = this.snapshot.person?.birthDate ? Date.parse(`${this.snapshot.person.birthDate}T00:00:00Z`) : NaN;\n const maxHr = Number.isFinite(birth) ? 208 - 0.7 * ((this.now() - birth) / (365.25 * DAY_MS)) : undefined;\n for (const r of this.snapshot.readings) {\n if (!unsaved.has(r.id)) continue;\n const outcome = await writer.save(r).catch(() => 'failed' as const);\n // Not allowed is final: the person chose it; asking again is the Health screen's job.\n if (outcome !== 'failed') done.add(r.id);\n }\n if (!done.size) return;\n await this.persist({\n unsaved: this.snapshot.unsaved.filter((id) => !done.has(id)),\n readings: this.snapshot.readings.map((r) =>\n r.kind === 'heart-rate' && done.has(r.id) && !this.snapshot.pendingDates.length\n ? compactSession(r, maxHr)\n : r,\n ),\n });\n }\n\n /** Readings of one device, newest first. */\n readingsOf(deviceId: string): DeviceReading[] {\n return this.snapshot.readings.filter((r) => r.deviceId === deviceId).sort((a, b) => b.at - a.at);\n }\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"deviceStore.js","sources":["../../../src/features/devices/deviceStore.ts"],"sourcesContent":["/**\n * The person's Bluetooth devices and everything they measured, kept on the\n * phone, and where those readings go:\n *\n * 1. The Fitness backend, as source `device`. It keeps one day per source and\n * replaces it whole, so an upload rebuilds every touched date from ALL the\n * readings kept here (readings.ts deviceHealthDays). Dates still to send\n * survive a restart (`pendingDates`) and go with the next session.\n * 2. Apple Health / Health Connect, when the person connected it in Yantra:\n * only what Yantra measured (writer.ts). Readings still to save survive a\n * restart too (`unsaved`).\n *\n * The record belongs to the signed-in account: another account signing in on\n * the same phone starts empty instead of inheriting someone else's devices.\n *\n * A plain external store (useSyncExternalStore), like healthSyncStore, so the\n * root runner and the screens share one instance.\n */\n\nimport { chunkDays } from '../health/buildDays';\nimport { createFitnessClient, type FitnessPerson } from '../health/fitnessClient';\nimport type { HealthSession } from '../health/healthSyncStore';\nimport type { StorageLike } from '../health/syncState';\nimport { deviceTimeZone, isValidTimeZone, zoneClock } from '../health/time';\nimport type { HealthWriter } from '../health/writer';\nimport type { DeviceKindId } from './catalog';\nimport type { ScaleUser } from './drivers';\nimport { compactSession, deviceHealthDays, type DeviceReading } from './readings';\n\nexport const DEVICE_STORE_KEY = 'yantra.devices.v1';\n/** Readings older than this are let go (the backend keeps its copy). */\nexport const KEEP_DAYS = 120;\nconst DAY_MS = 86_400_000;\n\nexport interface PairedDevice {\n /** The platform's id for the peripheral (iOS UUID, Android MAC). */\n id: string;\n name: string;\n kind: DeviceKindId;\n pairedAt: number;\n lastReadingAt?: number;\n /** The user slot a scale gave this person (drivers.ts). */\n scaleUser?: ScaleUser;\n /** The brand protocol it was recognised as, when it is not a standard-profile device. */\n protocol?: string;\n /** The last weight seen, handed to a brand scale as its starting weight. */\n lastWeightKg?: number;\n}\n\nexport interface DeviceStoreState {\n version: 1;\n owner?: string;\n devices: PairedDevice[];\n readings: DeviceReading[];\n /** Local dates whose device day still has to be sent. */\n pendingDates: string[];\n /** Reading ids still to save to the phone's health store. */\n unsaved: string[];\n lastUploadAt?: number;\n /** Plain words from the last failed upload; cleared by a good one. */\n lastError?: string;\n}\n\nexport interface DeviceSnapshot extends DeviceStoreState {\n loaded: boolean;\n uploading: boolean;\n /** The person facts the drivers use (age, sex, height, units), once known. */\n person?: FitnessPerson;\n}\n\nconst EMPTY: DeviceStoreState = { version: 1, devices: [], readings: [], pendingDates: [], unsaved: [] };\n\nexport interface DeviceStoreDeps {\n storage: StorageLike;\n /** Null when this build cannot save to the phone's health store. */\n createWriter: () => Promise<HealthWriter | null>;\n /** Whether the person connected Apple Health / Health Connect in Yantra; saving follows it. */\n healthConnected: () => boolean;\n fetchImpl?: typeof fetch;\n now?: () => number;\n}\n\nexport class DeviceStore {\n private snapshot: DeviceSnapshot = { ...EMPTY, loaded: false, uploading: false };\n\n private listeners = new Set<() => void>();\n\n private loading: Promise<void> | null = null;\n\n private flushing: Promise<void> | null = null;\n\n private session: HealthSession | null = null;\n\n private writer: Promise<HealthWriter | null> | null = null;\n\n constructor(private readonly deps: DeviceStoreDeps) {}\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): DeviceSnapshot => this.snapshot;\n\n private now(): number {\n return this.deps.now ? this.deps.now() : Date.now();\n }\n\n private set(patch: Partial<DeviceSnapshot>) {\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n private async persist(patch: Partial<DeviceStoreState>) {\n this.set(patch);\n const { loaded: _l, uploading: _u, person: _p, ...state } = this.snapshot;\n try {\n await this.deps.storage.setItem(DEVICE_STORE_KEY, JSON.stringify(state));\n } catch {\n // A lost write only costs a re-send; readings are de-duplicated by id.\n }\n }\n\n load(): Promise<void> {\n if (!this.loading) {\n this.loading = (async () => {\n let state = EMPTY;\n try {\n const raw = await this.deps.storage.getItem(DEVICE_STORE_KEY);\n const parsed = raw ? (JSON.parse(raw) as Partial<DeviceStoreState>) : null;\n if (parsed?.version === 1) {\n state = {\n ...EMPTY,\n ...parsed,\n devices: Array.isArray(parsed.devices) ? parsed.devices : [],\n readings: Array.isArray(parsed.readings) ? parsed.readings : [],\n pendingDates: Array.isArray(parsed.pendingDates) ? parsed.pendingDates : [],\n unsaved: Array.isArray(parsed.unsaved) ? parsed.unsaved : [],\n };\n }\n } catch {\n state = EMPTY;\n }\n this.set({ ...state, loaded: true });\n })();\n }\n return this.loading;\n }\n\n /** The root runner hands over the sign-in session; anything waiting goes out. */\n setSession(session: HealthSession | null) {\n this.session = session;\n if (!session) return;\n void (async () => {\n await this.load();\n if (session.owner && this.snapshot.owner && session.owner !== this.snapshot.owner) {\n // Another account on this phone: its devices and readings are not this person's.\n await this.persist({ ...EMPTY, owner: session.owner });\n } else if (session.owner && !this.snapshot.owner) {\n await this.persist({ owner: session.owner });\n }\n await this.flush();\n })();\n }\n\n async pair(device: Pick<PairedDevice, 'id' | 'name' | 'kind' | 'protocol'>): Promise<void> {\n await this.load();\n const existing = this.snapshot.devices.find((d) => d.id === device.id);\n const protocol = device.protocol !== undefined ? { protocol: device.protocol } : {};\n const devices = existing\n ? this.snapshot.devices.map((d) =>\n d.id === device.id ? { ...d, name: device.name, kind: device.kind, ...protocol } : d,\n )\n : [...this.snapshot.devices, { ...device, ...protocol, pairedAt: this.now() }];\n await this.persist({ devices });\n }\n\n /** Forget the device; what it measured stays (it is the person's history). */\n async forget(id: string): Promise<void> {\n await this.load();\n await this.persist({ devices: this.snapshot.devices.filter((d) => d.id !== id) });\n }\n\n async saveScaleUser(id: string, user: ScaleUser | null): Promise<void> {\n await this.load();\n await this.persist({\n devices: this.snapshot.devices.map((d) => (d.id === id ? { ...d, scaleUser: user ?? undefined } : d)),\n });\n }\n\n /** Keep new readings (by id), then send and save them. Resolves with the ones that were new. */\n async addReadings(readings: readonly DeviceReading[]): Promise<DeviceReading[]> {\n await this.load();\n const known = new Set(this.snapshot.readings.map((r) => r.id));\n const fresh: DeviceReading[] = [];\n for (const r of readings) {\n // A device can repeat a record within one transfer too.\n if (known.has(r.id)) continue;\n known.add(r.id);\n fresh.push(r);\n }\n if (!fresh.length) return [];\n const clock = zoneClock(this.timeZone());\n const cutoff = this.now() - KEEP_DAYS * DAY_MS;\n const kept = [...this.snapshot.readings, ...fresh].filter((r) => r.at >= cutoff).sort((a, b) => a.at - b.at);\n const touched = new Set(fresh.map((r) => clock.dateOf(r.at)));\n const lastByDevice = new Map<string, number>();\n for (const r of fresh) lastByDevice.set(r.deviceId, Math.max(lastByDevice.get(r.deviceId) ?? 0, r.at));\n await this.persist({\n readings: kept,\n pendingDates: [...new Set([...this.snapshot.pendingDates, ...touched])].sort(),\n unsaved: [...new Set([...this.snapshot.unsaved, ...fresh.map((r) => r.id)])],\n devices: this.snapshot.devices.map((d) =>\n lastByDevice.has(d.id)\n ? { ...d, lastReadingAt: Math.max(d.lastReadingAt ?? 0, lastByDevice.get(d.id) ?? 0) }\n : d,\n ),\n });\n void this.flush();\n return fresh;\n }\n\n private timeZone(): string {\n const tz = this.snapshot.person?.timezone;\n return tz && isValidTimeZone(tz) ? tz : deviceTimeZone();\n }\n\n private getWriter(): Promise<HealthWriter | null> {\n if (!this.writer) this.writer = this.deps.createWriter().catch(() => null);\n return this.writer;\n }\n\n /** The person facts the drivers need; fetched once per session. */\n async person(): Promise<FitnessPerson | undefined> {\n if (this.snapshot.person) return this.snapshot.person;\n if (!this.session) return undefined;\n try {\n const person = await createFitnessClient({ ...this.session, fetchImpl: this.deps.fetchImpl }).me();\n this.set({ person });\n return person;\n } catch {\n return undefined;\n }\n }\n\n /** Send what is pending and save what is unsaved. Single-flight; safe to call any time. */\n flush(): Promise<void> {\n if (!this.flushing) {\n this.flushing = (async () => {\n try {\n await this.upload();\n await this.saveToHealth();\n } finally {\n this.flushing = null;\n }\n })();\n }\n return this.flushing;\n }\n\n private async upload(): Promise<void> {\n await this.load();\n const session = this.session;\n if (!session || !this.snapshot.pendingDates.length) return;\n this.set({ uploading: true });\n try {\n const person = await this.person();\n const birth = person?.birthDate ? Date.parse(`${person.birthDate}T00:00:00Z`) : NaN;\n const age = Number.isFinite(birth) ? (this.now() - birth) / (365.25 * DAY_MS) : undefined;\n const dates = new Set(this.snapshot.pendingDates);\n const { days } = deviceHealthDays(this.snapshot.readings, {\n clock: zoneClock(this.timeZone()),\n syncedAt: new Date(this.now()).toISOString(),\n maxHr: age ? 208 - 0.7 * age : undefined,\n dates,\n });\n const client = createFitnessClient({ ...session, fetchImpl: this.deps.fetchImpl });\n for (const chunk of chunkDays(days)) await client.ingest(chunk);\n await this.persist({\n pendingDates: this.snapshot.pendingDates.filter((d) => !dates.has(d)),\n lastUploadAt: this.now(),\n lastError: undefined,\n });\n } catch (err) {\n await this.persist({ lastError: err instanceof Error ? err.message : 'Could not send the readings.' });\n } finally {\n this.set({ uploading: false });\n }\n }\n\n private async saveToHealth(): Promise<void> {\n if (!this.snapshot.unsaved.length) return;\n if (!this.deps.healthConnected()) return;\n const writer = await this.getWriter();\n if (!writer) return;\n const unsaved = new Set(this.snapshot.unsaved);\n const done = new Set<string>();\n const birth = this.snapshot.person?.birthDate ? Date.parse(`${this.snapshot.person.birthDate}T00:00:00Z`) : NaN;\n const maxHr = Number.isFinite(birth) ? 208 - 0.7 * ((this.now() - birth) / (365.25 * DAY_MS)) : undefined;\n for (const r of this.snapshot.readings) {\n if (!unsaved.has(r.id)) continue;\n const outcome = await writer.save(r).catch(() => 'failed' as const);\n // Not allowed is final: the person chose it; asking again is the Health screen's job.\n if (outcome !== 'failed') done.add(r.id);\n }\n if (!done.size) return;\n await this.persist({\n unsaved: this.snapshot.unsaved.filter((id) => !done.has(id)),\n readings: this.snapshot.readings.map((r) =>\n r.kind === 'heart-rate' && done.has(r.id) && !this.snapshot.pendingDates.length\n ? compactSession(r, maxHr)\n : r,\n ),\n });\n }\n\n /** Readings of one device, newest first. */\n readingsOf(deviceId: string): DeviceReading[] {\n return this.snapshot.readings.filter((r) => r.deviceId === deviceId).sort((a, b) => b.at - a.at);\n }\n}\n"],"names":["_a"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {encodeCurrentTime}from'./gatt/dateTime.js';import {parseRacpResponse,RACP_REPORT_ALL,bloodPressureProblems,parseHeartRate,parseGlucoseMeasurement,parseGlucoseContext,parsePlxSpotCheck,parsePlxContinuous,parseTemperature,parseBloodPressure,parseWeightMeasurement,parseBodyComposition}from'./gatt/measurements.js';import {giveConsent,newConsentCode,registerNewUser,parseUserControlResponse,encodeAge,encodeGender,encodeHeight,encodeChangeIncrement}from'./gatt/userData.js';import {SERVICE,CHAR}from'./gatt/uuids.js';import {readingId}from'./readings.js';var __defProp = Object.defineProperty;
1
+ import {encodeCurrentTime}from'./gatt/dateTime.js';import {parseRacpResponse,RACP_REPORT_ALL,bloodPressureProblems,parseHeartRate,parseGlucoseMeasurement,parseGlucoseContext,parsePlxSpotCheck,parsePlxContinuous,parseTemperature,parseBloodPressure,parseWeightMeasurement,parseBodyComposition}from'./gatt/measurements.js';import {giveConsent,newConsentCode,registerNewUser,parseUserControlResponse,encodeAge,encodeGender,encodeHeight,encodeChangeIncrement}from'./gatt/userData.js';import {SERVICE,CHAR}from'./gatt/uuids.js';import {startFtmsTreadmill}from'./gatt/ftms.js';import {startBrandDriver}from'./brands/index.js';import {readingId}from'./readings.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -20,6 +20,8 @@ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
20
  const SCALE_MERGE_MS = 4e3;
21
21
  const UCP_TIMEOUT_MS = 6e3;
22
22
  async function startDriver(kind, link, ctx) {
23
+ var _a;
24
+ if ((_a = ctx.identity) == null ? void 0 : _a.protocol) return startBrandDriver(ctx.identity.protocol, link, ctx);
23
25
  await setDeviceClock(link, ctx);
24
26
  switch (kind) {
25
27
  case "scale":
@@ -34,6 +36,8 @@ async function startDriver(kind, link, ctx) {
34
36
  return startGlucose(link, ctx);
35
37
  case "heart-rate":
36
38
  return startHeartRate(link, ctx);
39
+ case "treadmill":
40
+ return startFtmsTreadmill(link, ctx);
37
41
  default:
38
42
  throw new Error(`Yantra does not know how to read a ${String(kind)}.`);
39
43
  }