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

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 +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"drivers.js","sources":["../../../src/features/devices/drivers.ts"],"sourcesContent":["/**\n * Talking to one connected device, per kind: what to subscribe to, what to\n * write first, and when a measurement is complete. Written against GattLink\n * (one connected device's services), not against react-native-ble-plx, so\n * every exchange is tested with a fake device (bleTransport.ts is the real one).\n *\n * - Scale: a body-composition scale sends weight (0x2A9D) and body composition\n * (0x2A9C) as separate indications, sometimes in several packets; they are\n * merged into ONE reading. A scale with user slots (User Data Service) only\n * measures body fat for a registered user who gave consent: Yantra registers\n * once, keeps the slot, consents on every connection and fills in age, sex\n * and height. A weighing for another user's slot is not this person's.\n * - Blood pressure, thermometer, pulse oximeter spot checks: every indication is\n * a reading (a monitor sends its stored ones on connect; ids de-duplicate).\n * - Glucose: stored records are asked for with the Record Access Control\n * Point; a measurement waits for its meal context when one follows.\n * - Heart rate and continuous oximetry: live values for the screen, which\n * records a session (heart rate) or saves the value the person sees.\n *\n * A device clock that can be set (Current Time Service) is set first, so the\n * stamps on what it sends are right.\n */\n\nimport type { DeviceKindId } from './catalog';\nimport { encodeCurrentTime } from './gatt/dateTime';\nimport {\n RACP_REPORT_ALL,\n bloodPressureProblems,\n parseBloodPressure,\n parseBodyComposition,\n parseGlucoseContext,\n parseGlucoseMeasurement,\n parseHeartRate,\n parsePlxContinuous,\n parsePlxSpotCheck,\n parseRacpResponse,\n parseTemperature,\n parseWeightMeasurement,\n type GlucoseMeasurement,\n type MealContext,\n} from './gatt/measurements';\nimport {\n encodeAge,\n encodeChangeIncrement,\n encodeGender,\n encodeHeight,\n giveConsent,\n newConsentCode,\n parseUserControlResponse,\n registerNewUser,\n} from './gatt/userData';\nimport { CHAR, SERVICE } from './gatt/uuids';\nimport { readingId, type BodyReading, type DeviceReading } from './readings';\nimport type { MealRelation } from '../health/types';\n\n/** One connected device, as the drivers need it. Values are raw bytes. */\nexport interface GattLink {\n readonly id: string;\n readonly name: string;\n /** The device offers this service (and characteristic). */\n has(service: string, characteristic?: string): boolean;\n /** Notifications or indications; returns the unsubscribe. Errors end the connection (see onError). */\n monitor(service: string, characteristic: string, onValue: (bytes: Uint8Array) => void): () => void;\n write(service: string, characteristic: string, bytes: number[]): Promise<void>;\n read(service: string, characteristic: string): Promise<Uint8Array>;\n}\n\nexport interface PersonFacts {\n age?: number;\n sex?: 'male' | 'female' | 'unspecified';\n heightCm?: number;\n}\n\n/** The slot a scale gave this person, and the consent code that opens it. */\nexport interface ScaleUser {\n userIndex: number;\n consentCode: number;\n}\n\nexport interface LiveValue {\n bpm?: number;\n spo2?: number;\n pulse?: number;\n}\n\nexport interface DriverContext {\n now(): number;\n person: PersonFacts;\n scaleUser?: ScaleUser;\n saveScaleUser(user: ScaleUser | null): Promise<void>;\n onReading(reading: DeviceReading): void;\n onLive?(value: LiveValue): void;\n /** Something the person should know, in plain words. */\n onNotice?(message: string): void;\n}\n\nexport interface DriverHandle {\n /** Unsubscribe; a measurement still being assembled is finished first. */\n stop(): void;\n}\n\n/** Weight and body composition of one weighing arrive within this window of each other. */\nexport const SCALE_MERGE_MS = 4_000;\nconst UCP_TIMEOUT_MS = 6_000;\n\nexport async function startDriver(kind: DeviceKindId, link: GattLink, ctx: DriverContext): Promise<DriverHandle> {\n await setDeviceClock(link, ctx);\n switch (kind) {\n case 'scale':\n return startScale(link, ctx);\n case 'blood-pressure':\n return startBloodPressure(link, ctx);\n case 'thermometer':\n return startThermometer(link, ctx);\n case 'pulse-oximeter':\n return startPulseOximeter(link, ctx);\n case 'glucose':\n return startGlucose(link, ctx);\n case 'heart-rate':\n return startHeartRate(link, ctx);\n default:\n throw new Error(`Yantra does not know how to read a ${String(kind)}.`);\n }\n}\n\nasync function setDeviceClock(link: GattLink, ctx: DriverContext): Promise<void> {\n if (!link.has(SERVICE.currentTime, CHAR.currentTime)) return;\n try {\n await link.write(SERVICE.currentTime, CHAR.currentTime, encodeCurrentTime(new Date(ctx.now())));\n } catch {\n // Many devices keep their clock read-only; their own stamps are used as they are.\n }\n}\n\n/** Await indications one at a time (User Control Point answers). */\nfunction indications(link: GattLink, service: string, characteristic: string) {\n const queued: Uint8Array[] = [];\n const waiting: Array<(b: Uint8Array) => void> = [];\n const close = link.monitor(service, characteristic, (b) => {\n const w = waiting.shift();\n if (w) w(b);\n else queued.push(b);\n });\n return {\n next(timeoutMs: number): Promise<Uint8Array> {\n const ready = queued.shift();\n if (ready) return Promise.resolve(ready);\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n const i = waiting.indexOf(done);\n if (i >= 0) waiting.splice(i, 1);\n reject(new Error('The scale did not answer.'));\n }, timeoutMs);\n const done = (b: Uint8Array) => {\n clearTimeout(timer);\n resolve(b);\n };\n waiting.push(done);\n });\n },\n close,\n };\n}\n\n// ------------------------------------------------------------------- Scale\n\nasync function ensureScaleUser(link: GattLink, ctx: DriverContext): Promise<number | undefined> {\n const ucp = indications(link, SERVICE.userData, CHAR.userControlPoint);\n const ask = async (bytes: number[]) => {\n await link.write(SERVICE.userData, CHAR.userControlPoint, bytes);\n for (;;) {\n const answer = parseUserControlResponse(await ucp.next(UCP_TIMEOUT_MS));\n if (answer) return answer;\n }\n };\n try {\n if (ctx.scaleUser) {\n const consent = await ask(giveConsent(ctx.scaleUser.userIndex, ctx.scaleUser.consentCode));\n if (consent.ok) return ctx.scaleUser.userIndex;\n // The slot was deleted on the scale (or reset): register again.\n await ctx.saveScaleUser(null);\n }\n const consentCode = newConsentCode();\n const registered = await ask(registerNewUser(consentCode));\n const userIndex = registered.op === 'register' && registered.ok ? registered.userIndex : undefined;\n if (userIndex === undefined) {\n ctx.onNotice?.(registered.reason ?? 'The scale did not register this phone.');\n return undefined;\n }\n const consent = await ask(giveConsent(userIndex, consentCode));\n if (!consent.ok) {\n ctx.onNotice?.(consent.reason ?? 'The scale refused this phone.');\n return undefined;\n }\n await ctx.saveScaleUser({ userIndex, consentCode });\n await writePersonOnScale(link, ctx.person);\n return userIndex;\n } catch {\n ctx.onNotice?.('The scale did not set up a user for this phone. Weight still comes through; body fat may not.');\n return undefined;\n } finally {\n ucp.close();\n }\n}\n\nasync function writePersonOnScale(link: GattLink, person: PersonFacts): Promise<void> {\n const writes: Array<[string, number[] | undefined]> = [\n [CHAR.age, person.age !== undefined ? encodeAge(person.age) : undefined],\n [CHAR.gender, person.sex ? encodeGender(person.sex) : undefined],\n [CHAR.height, person.heightCm ? encodeHeight(person.heightCm) : undefined],\n ];\n let changed = false;\n for (const [characteristic, bytes] of writes) {\n if (!bytes || !link.has(SERVICE.userData, characteristic)) continue;\n try {\n await link.write(SERVICE.userData, characteristic, bytes);\n changed = true;\n } catch {\n // A scale that does not take one of them still measures with the rest.\n }\n }\n if (!changed || !link.has(SERVICE.userData, CHAR.databaseChangeIncrement)) return;\n try {\n const current = await link.read(SERVICE.userData, CHAR.databaseChangeIncrement);\n const n =\n current.length >= 4 ? (current[0] | (current[1] << 8) | (current[2] << 16) | (current[3] << 24)) >>> 0 : 0;\n await link.write(SERVICE.userData, CHAR.databaseChangeIncrement, encodeChangeIncrement(n + 1));\n } catch {\n // The scale picks the data up on its next sync either way.\n }\n}\n\nasync function startScale(link: GattLink, ctx: DriverContext): Promise<DriverHandle> {\n const hasBcs = link.has(SERVICE.bodyComposition, CHAR.bodyCompositionMeasurement);\n const hasWss = link.has(SERVICE.weightScale, CHAR.weightMeasurement);\n if (!hasBcs && !hasWss) throw new Error('This device does not send weight the standard way.');\n const subs: Array<() => void> = [];\n let myUser: number | undefined = ctx.scaleUser?.userIndex;\n let pending: Omit<BodyReading, 'id'> | null = null;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const flush = () => {\n if (timer) clearTimeout(timer);\n timer = undefined;\n const r = pending;\n pending = null;\n if (!r || (r.weightKg === undefined && r.bodyFatPct === undefined)) return;\n ctx.onReading({ ...r, id: readingId(link.id, 'body', r.at) });\n };\n const merge = (patch: Partial<BodyReading>, at: number) => {\n if (!pending) pending = { kind: 'body', at, deviceId: link.id, deviceName: link.name };\n for (const [k, v] of Object.entries(patch)) {\n // The weight scale's own weight wins over the copy inside body composition.\n if (v !== undefined && (k !== 'weightKg' || pending.weightKg === undefined)) {\n (pending as unknown as Record<string, unknown>)[k] = v;\n }\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, SCALE_MERGE_MS);\n };\n const mine = (userId: number | undefined) => userId === undefined || myUser === undefined || userId === myUser;\n\n if (hasWss) {\n subs.push(\n link.monitor(SERVICE.weightScale, CHAR.weightMeasurement, (bytes) => {\n const m = safe(() => parseWeightMeasurement(bytes, ctx.now()));\n if (!m || !mine(m.userId)) return;\n merge({ weightKg: m.weightKg, bmi: m.bmi }, m.at ?? ctx.now());\n if (!hasBcs) flush();\n }),\n );\n }\n if (hasBcs) {\n subs.push(\n link.monitor(SERVICE.bodyComposition, CHAR.bodyCompositionMeasurement, (bytes) => {\n const m = safe(() => parseBodyComposition(bytes, ctx.now()));\n if (!m || !mine(m.userId)) return;\n merge(\n { bodyFatPct: m.bodyFatPct, leanMassKg: m.fatFreeMassKg, weightKg: m.weightKg },\n pending?.at ?? m.at ?? ctx.now(),\n );\n if (!m.multiPacket && (!hasWss || pending?.weightKg !== undefined)) flush();\n }),\n );\n }\n if (link.has(SERVICE.userData, CHAR.userControlPoint)) myUser = await ensureScaleUser(link, ctx);\n\n return {\n stop() {\n flush();\n subs.forEach((u) => u());\n },\n };\n}\n\n/** A packet that does not parse is skipped; the next one is still read. */\nfunction safe<T>(parse: () => T): T | null {\n try {\n return parse();\n } catch {\n return null;\n }\n}\n\n// ------------------------------------------------------- Blood pressure etc.\n\nfunction startBloodPressure(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.bloodPressure, CHAR.bloodPressureMeasurement)) {\n throw new Error('This device does not send blood pressure the standard way.');\n }\n const unsub = link.monitor(SERVICE.bloodPressure, CHAR.bloodPressureMeasurement, (bytes) => {\n const m = safe(() => parseBloodPressure(bytes, ctx.now()));\n if (!m) return;\n const at = m.at ?? ctx.now();\n const problems = bloodPressureProblems(m.status);\n ctx.onReading({\n kind: 'blood-pressure',\n id: readingId(link.id, 'blood-pressure', at, `${m.systolic}-${m.diastolic}`),\n at,\n deviceId: link.id,\n deviceName: link.name,\n systolic: m.systolic,\n diastolic: m.diastolic,\n ...(m.pulse !== undefined ? { pulse: m.pulse } : {}),\n ...(problems.length ? { problems } : {}),\n });\n });\n return { stop: unsub };\n}\n\nfunction startThermometer(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.healthThermometer, CHAR.temperatureMeasurement)) {\n throw new Error('This device does not send temperature the standard way.');\n }\n const unsub = link.monitor(SERVICE.healthThermometer, CHAR.temperatureMeasurement, (bytes) => {\n const m = safe(() => parseTemperature(bytes, ctx.now()));\n if (!m) return;\n const at = m.at ?? ctx.now();\n ctx.onReading({\n kind: 'temperature',\n id: readingId(link.id, 'temperature', at, m.celsius),\n at,\n deviceId: link.id,\n deviceName: link.name,\n celsius: m.celsius,\n });\n });\n return { stop: unsub };\n}\n\nfunction startPulseOximeter(link: GattLink, ctx: DriverContext): DriverHandle {\n const subs: Array<() => void> = [];\n if (link.has(SERVICE.pulseOximeter, CHAR.plxSpotCheck)) {\n subs.push(\n link.monitor(SERVICE.pulseOximeter, CHAR.plxSpotCheck, (bytes) => {\n const m = safe(() => parsePlxSpotCheck(bytes, ctx.now()));\n if (!m) return;\n const at = m.at ?? ctx.now();\n ctx.onReading({\n kind: 'spo2',\n id: readingId(link.id, 'spo2', at, m.spo2),\n at,\n deviceId: link.id,\n deviceName: link.name,\n spo2: m.spo2,\n ...(m.pulse !== undefined ? { pulse: m.pulse } : {}),\n });\n }),\n );\n }\n if (link.has(SERVICE.pulseOximeter, CHAR.plxContinuous)) {\n subs.push(\n link.monitor(SERVICE.pulseOximeter, CHAR.plxContinuous, (bytes) => {\n const m = safe(() => parsePlxContinuous(bytes));\n if (m) ctx.onLive?.({ spo2: m.spo2, pulse: m.pulse });\n }),\n );\n }\n if (!subs.length) throw new Error('This device does not send blood oxygen the standard way.');\n return { stop: () => subs.forEach((u) => u()) };\n}\n\nfunction startHeartRate(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.heartRate, CHAR.heartRateMeasurement)) {\n throw new Error('This device does not send heart rate the standard way.');\n }\n const unsub = link.monitor(SERVICE.heartRate, CHAR.heartRateMeasurement, (bytes) => {\n const m = safe(() => parseHeartRate(bytes));\n if (m) ctx.onLive?.({ bpm: m.bpm });\n });\n return { stop: unsub };\n}\n\n// -------------------------------------------------------------------- Glucose\n\nconst MEAL_RELATION: Record<MealContext, MealRelation> = {\n 'before-meal': 'before-meal',\n 'after-meal': 'after-meal',\n fasting: 'fasting',\n bedtime: 'bedtime',\n casual: 'other',\n};\n\nasync function startGlucose(link: GattLink, ctx: DriverContext): Promise<DriverHandle> {\n if (!link.has(SERVICE.glucose, CHAR.glucoseMeasurement)) {\n throw new Error('This device does not send glucose the standard way.');\n }\n const waiting = new Map<number, GlucoseMeasurement>();\n const meals = new Map<number, MealRelation>();\n const subs: Array<() => void> = [];\n\n const emit = (sequence: number) => {\n const m = waiting.get(sequence);\n if (!m || m.mgdl === undefined) return;\n waiting.delete(sequence);\n const at = m.at ?? ctx.now();\n const meal = meals.get(sequence);\n ctx.onReading({\n kind: 'glucose',\n id: readingId(link.id, 'glucose', at, sequence),\n at,\n deviceId: link.id,\n deviceName: link.name,\n mgdl: m.mgdl,\n ...(meal ? { meal } : {}),\n });\n };\n\n subs.push(\n link.monitor(SERVICE.glucose, CHAR.glucoseMeasurement, (bytes) => {\n const m = safe(() => parseGlucoseMeasurement(bytes, ctx.now()));\n if (!m || m.mgdl === undefined) return;\n waiting.set(m.sequence, m);\n if (!m.hasContext) emit(m.sequence);\n }),\n );\n if (link.has(SERVICE.glucose, CHAR.glucoseContext)) {\n subs.push(\n link.monitor(SERVICE.glucose, CHAR.glucoseContext, (bytes) => {\n const c = safe(() => parseGlucoseContext(bytes));\n if (!c) return;\n if (c.meal) meals.set(c.sequence, MEAL_RELATION[c.meal]);\n emit(c.sequence);\n }),\n );\n }\n if (link.has(SERVICE.glucose, CHAR.recordAccessControlPoint)) {\n subs.push(\n link.monitor(SERVICE.glucose, CHAR.recordAccessControlPoint, (bytes) => {\n const result = parseRacpResponse(bytes);\n if (!result) return;\n // The transfer is over: anything still waiting for a context goes without one.\n for (const sequence of [...waiting.keys()]) emit(sequence);\n if (result === 'failed') ctx.onNotice?.('The meter could not send its stored readings.');\n }),\n );\n try {\n await link.write(SERVICE.glucose, CHAR.recordAccessControlPoint, RACP_REPORT_ALL);\n } catch {\n ctx.onNotice?.('Pair the meter when your phone asks, then try again to get its stored readings.');\n }\n }\n return {\n stop() {\n for (const sequence of [...waiting.keys()]) emit(sequence);\n subs.forEach((u) => u());\n },\n };\n}\n"],"names":["consent","_a"],"mappings":"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"}
1
+ {"version":3,"file":"drivers.js","sources":["../../../src/features/devices/drivers.ts"],"sourcesContent":["/**\n * Talking to one connected device, per kind: what to subscribe to, what to\n * write first, and when a measurement is complete. Written against GattLink\n * (one connected device's services), not against react-native-ble-plx, so\n * every exchange is tested with a fake device (bleTransport.ts is the real one).\n *\n * - Scale: a body-composition scale sends weight (0x2A9D) and body composition\n * (0x2A9C) as separate indications, sometimes in several packets; they are\n * merged into ONE reading. A scale with user slots (User Data Service) only\n * measures body fat for a registered user who gave consent: Yantra registers\n * once, keeps the slot, consents on every connection and fills in age, sex\n * and height. A weighing for another user's slot is not this person's.\n * - Blood pressure, thermometer, pulse oximeter spot checks: every indication is\n * a reading (a monitor sends its stored ones on connect; ids de-duplicate).\n * - Glucose: stored records are asked for with the Record Access Control\n * Point; a measurement waits for its meal context when one follows.\n * - Heart rate and continuous oximetry: live values for the screen, which\n * records a session (heart rate) or saves the value the person sees.\n *\n * A device clock that can be set (Current Time Service) is set first, so the\n * stamps on what it sends are right.\n */\n\nimport type { DeviceKindId } from './catalog';\nimport { encodeCurrentTime } from './gatt/dateTime';\nimport {\n RACP_REPORT_ALL,\n bloodPressureProblems,\n parseBloodPressure,\n parseBodyComposition,\n parseGlucoseContext,\n parseGlucoseMeasurement,\n parseHeartRate,\n parsePlxContinuous,\n parsePlxSpotCheck,\n parseRacpResponse,\n parseTemperature,\n parseWeightMeasurement,\n type GlucoseMeasurement,\n type MealContext,\n} from './gatt/measurements';\nimport {\n encodeAge,\n encodeChangeIncrement,\n encodeGender,\n encodeHeight,\n giveConsent,\n newConsentCode,\n parseUserControlResponse,\n registerNewUser,\n} from './gatt/userData';\nimport { CHAR, SERVICE } from './gatt/uuids';\nimport { startFtmsTreadmill } from './gatt/ftms';\nimport { startBrandDriver } from './brands';\nimport type { DeviceIdentity } from './link';\nimport { readingId, type BodyReading, type DeviceReading } from './readings';\nimport type { MealRelation } from '../health/types';\n\nexport interface WriteOptions {\n /** A write the device does not acknowledge (some brand protocols expect it). */\n withoutResponse?: boolean;\n}\n\n/** One connected device, as the drivers need it. Values are raw bytes. */\nexport interface GattLink {\n readonly id: string;\n readonly name: string;\n /** The device offers this service (and characteristic). */\n has(service: string, characteristic?: string): boolean;\n /** The characteristic takes writes without a response. */\n canWriteWithoutResponse?(service: string, characteristic: string): boolean;\n /** Notifications or indications; returns the unsubscribe. Errors end the connection (see onError). */\n monitor(service: string, characteristic: string, onValue: (bytes: Uint8Array) => void): () => void;\n write(service: string, characteristic: string, bytes: number[], options?: WriteOptions): Promise<void>;\n read(service: string, characteristic: string): Promise<Uint8Array>;\n}\n\nexport interface PersonFacts {\n age?: number;\n sex?: 'male' | 'female' | 'unspecified';\n heightCm?: number;\n /** The last weight Yantra knows, for scales that want a starting weight. */\n weightKg?: number;\n /** Birth date YYYY-MM-DD, for scales that take one instead of an age. */\n birthDate?: string;\n}\n\n/** The slot a scale gave this person, and the consent code that opens it. */\nexport interface ScaleUser {\n userIndex: number;\n consentCode: number;\n}\n\n/** What the screen shows while a device is measuring. */\nexport interface LiveValue {\n bpm?: number;\n spo2?: number;\n pulse?: number;\n /** A scale's number as it settles. */\n weightKg?: number;\n /** A cuff's pressure while it inflates. */\n cuffMmHg?: number;\n /** Treadmill. */\n speedKmh?: number;\n distanceM?: number;\n inclinePct?: number;\n steps?: number;\n kcal?: number;\n elapsedS?: number;\n /** Jump rope. */\n jumps?: number;\n /** Tape measure, in cm, and the body part it is measuring. */\n lengthCm?: number;\n bodyPart?: string;\n /** Kitchen scale, in grams. */\n grams?: number;\n /** Ring. */\n skinTempC?: number;\n /** Percent, when the device reports it. */\n battery?: number;\n}\n\nexport interface DriverContext {\n now(): number;\n person: PersonFacts;\n /** What the advertisement said about the device (brand protocol, its address ...). */\n identity?: DeviceIdentity;\n scaleUser?: ScaleUser;\n saveScaleUser(user: ScaleUser | null): Promise<void>;\n onReading(reading: DeviceReading): void;\n onLive?(value: LiveValue): void;\n /** Something the person should know, in plain words. */\n onNotice?(message: string): void;\n}\n\nexport interface DriverHandle {\n /** Unsubscribe; a measurement still being assembled is finished first. */\n stop(): void;\n}\n\n/** Weight and body composition of one weighing arrive within this window of each other. */\nexport const SCALE_MERGE_MS = 4_000;\nconst UCP_TIMEOUT_MS = 6_000;\n\nexport async function startDriver(kind: DeviceKindId, link: GattLink, ctx: DriverContext): Promise<DriverHandle> {\n // A brand device speaks its own protocol, not the Bluetooth SIG profile of its kind.\n if (ctx.identity?.protocol) return startBrandDriver(ctx.identity.protocol, link, ctx);\n await setDeviceClock(link, ctx);\n switch (kind) {\n case 'scale':\n return startScale(link, ctx);\n case 'blood-pressure':\n return startBloodPressure(link, ctx);\n case 'thermometer':\n return startThermometer(link, ctx);\n case 'pulse-oximeter':\n return startPulseOximeter(link, ctx);\n case 'glucose':\n return startGlucose(link, ctx);\n case 'heart-rate':\n return startHeartRate(link, ctx);\n case 'treadmill':\n // Any treadmill that speaks the Fitness Machine profile, RENPHO's included.\n return startFtmsTreadmill(link, ctx);\n default:\n throw new Error(`Yantra does not know how to read a ${String(kind)}.`);\n }\n}\n\nasync function setDeviceClock(link: GattLink, ctx: DriverContext): Promise<void> {\n if (!link.has(SERVICE.currentTime, CHAR.currentTime)) return;\n try {\n await link.write(SERVICE.currentTime, CHAR.currentTime, encodeCurrentTime(new Date(ctx.now())));\n } catch {\n // Many devices keep their clock read-only; their own stamps are used as they are.\n }\n}\n\n/** Await indications one at a time (User Control Point answers). */\nfunction indications(link: GattLink, service: string, characteristic: string) {\n const queued: Uint8Array[] = [];\n const waiting: Array<(b: Uint8Array) => void> = [];\n const close = link.monitor(service, characteristic, (b) => {\n const w = waiting.shift();\n if (w) w(b);\n else queued.push(b);\n });\n return {\n next(timeoutMs: number): Promise<Uint8Array> {\n const ready = queued.shift();\n if (ready) return Promise.resolve(ready);\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n const i = waiting.indexOf(done);\n if (i >= 0) waiting.splice(i, 1);\n reject(new Error('The scale did not answer.'));\n }, timeoutMs);\n const done = (b: Uint8Array) => {\n clearTimeout(timer);\n resolve(b);\n };\n waiting.push(done);\n });\n },\n close,\n };\n}\n\n// ------------------------------------------------------------------- Scale\n\nasync function ensureScaleUser(link: GattLink, ctx: DriverContext): Promise<number | undefined> {\n const ucp = indications(link, SERVICE.userData, CHAR.userControlPoint);\n const ask = async (bytes: number[]) => {\n await link.write(SERVICE.userData, CHAR.userControlPoint, bytes);\n for (;;) {\n const answer = parseUserControlResponse(await ucp.next(UCP_TIMEOUT_MS));\n if (answer) return answer;\n }\n };\n try {\n if (ctx.scaleUser) {\n const consent = await ask(giveConsent(ctx.scaleUser.userIndex, ctx.scaleUser.consentCode));\n if (consent.ok) return ctx.scaleUser.userIndex;\n // The slot was deleted on the scale (or reset): register again.\n await ctx.saveScaleUser(null);\n }\n const consentCode = newConsentCode();\n const registered = await ask(registerNewUser(consentCode));\n const userIndex = registered.op === 'register' && registered.ok ? registered.userIndex : undefined;\n if (userIndex === undefined) {\n ctx.onNotice?.(registered.reason ?? 'The scale did not register this phone.');\n return undefined;\n }\n const consent = await ask(giveConsent(userIndex, consentCode));\n if (!consent.ok) {\n ctx.onNotice?.(consent.reason ?? 'The scale refused this phone.');\n return undefined;\n }\n await ctx.saveScaleUser({ userIndex, consentCode });\n await writePersonOnScale(link, ctx.person);\n return userIndex;\n } catch {\n ctx.onNotice?.('The scale did not set up a user for this phone. Weight still comes through; body fat may not.');\n return undefined;\n } finally {\n ucp.close();\n }\n}\n\nasync function writePersonOnScale(link: GattLink, person: PersonFacts): Promise<void> {\n const writes: Array<[string, number[] | undefined]> = [\n [CHAR.age, person.age !== undefined ? encodeAge(person.age) : undefined],\n [CHAR.gender, person.sex ? encodeGender(person.sex) : undefined],\n [CHAR.height, person.heightCm ? encodeHeight(person.heightCm) : undefined],\n ];\n let changed = false;\n for (const [characteristic, bytes] of writes) {\n if (!bytes || !link.has(SERVICE.userData, characteristic)) continue;\n try {\n await link.write(SERVICE.userData, characteristic, bytes);\n changed = true;\n } catch {\n // A scale that does not take one of them still measures with the rest.\n }\n }\n if (!changed || !link.has(SERVICE.userData, CHAR.databaseChangeIncrement)) return;\n try {\n const current = await link.read(SERVICE.userData, CHAR.databaseChangeIncrement);\n const n =\n current.length >= 4 ? (current[0] | (current[1] << 8) | (current[2] << 16) | (current[3] << 24)) >>> 0 : 0;\n await link.write(SERVICE.userData, CHAR.databaseChangeIncrement, encodeChangeIncrement(n + 1));\n } catch {\n // The scale picks the data up on its next sync either way.\n }\n}\n\nasync function startScale(link: GattLink, ctx: DriverContext): Promise<DriverHandle> {\n const hasBcs = link.has(SERVICE.bodyComposition, CHAR.bodyCompositionMeasurement);\n const hasWss = link.has(SERVICE.weightScale, CHAR.weightMeasurement);\n if (!hasBcs && !hasWss) throw new Error('This device does not send weight the standard way.');\n const subs: Array<() => void> = [];\n let myUser: number | undefined = ctx.scaleUser?.userIndex;\n let pending: Omit<BodyReading, 'id'> | null = null;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const flush = () => {\n if (timer) clearTimeout(timer);\n timer = undefined;\n const r = pending;\n pending = null;\n if (!r || (r.weightKg === undefined && r.bodyFatPct === undefined)) return;\n ctx.onReading({ ...r, id: readingId(link.id, 'body', r.at) });\n };\n const merge = (patch: Partial<BodyReading>, at: number) => {\n if (!pending) pending = { kind: 'body', at, deviceId: link.id, deviceName: link.name };\n for (const [k, v] of Object.entries(patch)) {\n // The weight scale's own weight wins over the copy inside body composition.\n if (v !== undefined && (k !== 'weightKg' || pending.weightKg === undefined)) {\n (pending as unknown as Record<string, unknown>)[k] = v;\n }\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, SCALE_MERGE_MS);\n };\n const mine = (userId: number | undefined) => userId === undefined || myUser === undefined || userId === myUser;\n\n if (hasWss) {\n subs.push(\n link.monitor(SERVICE.weightScale, CHAR.weightMeasurement, (bytes) => {\n const m = safe(() => parseWeightMeasurement(bytes, ctx.now()));\n if (!m || !mine(m.userId)) return;\n merge({ weightKg: m.weightKg, bmi: m.bmi }, m.at ?? ctx.now());\n if (!hasBcs) flush();\n }),\n );\n }\n if (hasBcs) {\n subs.push(\n link.monitor(SERVICE.bodyComposition, CHAR.bodyCompositionMeasurement, (bytes) => {\n const m = safe(() => parseBodyComposition(bytes, ctx.now()));\n if (!m || !mine(m.userId)) return;\n merge(\n { bodyFatPct: m.bodyFatPct, leanMassKg: m.fatFreeMassKg, weightKg: m.weightKg },\n pending?.at ?? m.at ?? ctx.now(),\n );\n if (!m.multiPacket && (!hasWss || pending?.weightKg !== undefined)) flush();\n }),\n );\n }\n if (link.has(SERVICE.userData, CHAR.userControlPoint)) myUser = await ensureScaleUser(link, ctx);\n\n return {\n stop() {\n flush();\n subs.forEach((u) => u());\n },\n };\n}\n\n/** A packet that does not parse is skipped; the next one is still read. */\nfunction safe<T>(parse: () => T): T | null {\n try {\n return parse();\n } catch {\n return null;\n }\n}\n\n// ------------------------------------------------------- Blood pressure etc.\n\nfunction startBloodPressure(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.bloodPressure, CHAR.bloodPressureMeasurement)) {\n throw new Error('This device does not send blood pressure the standard way.');\n }\n const unsub = link.monitor(SERVICE.bloodPressure, CHAR.bloodPressureMeasurement, (bytes) => {\n const m = safe(() => parseBloodPressure(bytes, ctx.now()));\n if (!m) return;\n const at = m.at ?? ctx.now();\n const problems = bloodPressureProblems(m.status);\n ctx.onReading({\n kind: 'blood-pressure',\n id: readingId(link.id, 'blood-pressure', at, `${m.systolic}-${m.diastolic}`),\n at,\n deviceId: link.id,\n deviceName: link.name,\n systolic: m.systolic,\n diastolic: m.diastolic,\n ...(m.pulse !== undefined ? { pulse: m.pulse } : {}),\n ...(problems.length ? { problems } : {}),\n });\n });\n return { stop: unsub };\n}\n\nfunction startThermometer(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.healthThermometer, CHAR.temperatureMeasurement)) {\n throw new Error('This device does not send temperature the standard way.');\n }\n const unsub = link.monitor(SERVICE.healthThermometer, CHAR.temperatureMeasurement, (bytes) => {\n const m = safe(() => parseTemperature(bytes, ctx.now()));\n if (!m) return;\n const at = m.at ?? ctx.now();\n ctx.onReading({\n kind: 'temperature',\n id: readingId(link.id, 'temperature', at, m.celsius),\n at,\n deviceId: link.id,\n deviceName: link.name,\n celsius: m.celsius,\n });\n });\n return { stop: unsub };\n}\n\nfunction startPulseOximeter(link: GattLink, ctx: DriverContext): DriverHandle {\n const subs: Array<() => void> = [];\n if (link.has(SERVICE.pulseOximeter, CHAR.plxSpotCheck)) {\n subs.push(\n link.monitor(SERVICE.pulseOximeter, CHAR.plxSpotCheck, (bytes) => {\n const m = safe(() => parsePlxSpotCheck(bytes, ctx.now()));\n if (!m) return;\n const at = m.at ?? ctx.now();\n ctx.onReading({\n kind: 'spo2',\n id: readingId(link.id, 'spo2', at, m.spo2),\n at,\n deviceId: link.id,\n deviceName: link.name,\n spo2: m.spo2,\n ...(m.pulse !== undefined ? { pulse: m.pulse } : {}),\n });\n }),\n );\n }\n if (link.has(SERVICE.pulseOximeter, CHAR.plxContinuous)) {\n subs.push(\n link.monitor(SERVICE.pulseOximeter, CHAR.plxContinuous, (bytes) => {\n const m = safe(() => parsePlxContinuous(bytes));\n if (m) ctx.onLive?.({ spo2: m.spo2, pulse: m.pulse });\n }),\n );\n }\n if (!subs.length) throw new Error('This device does not send blood oxygen the standard way.');\n return { stop: () => subs.forEach((u) => u()) };\n}\n\nfunction startHeartRate(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.heartRate, CHAR.heartRateMeasurement)) {\n throw new Error('This device does not send heart rate the standard way.');\n }\n const unsub = link.monitor(SERVICE.heartRate, CHAR.heartRateMeasurement, (bytes) => {\n const m = safe(() => parseHeartRate(bytes));\n if (m) ctx.onLive?.({ bpm: m.bpm });\n });\n return { stop: unsub };\n}\n\n// -------------------------------------------------------------------- Glucose\n\nconst MEAL_RELATION: Record<MealContext, MealRelation> = {\n 'before-meal': 'before-meal',\n 'after-meal': 'after-meal',\n fasting: 'fasting',\n bedtime: 'bedtime',\n casual: 'other',\n};\n\nasync function startGlucose(link: GattLink, ctx: DriverContext): Promise<DriverHandle> {\n if (!link.has(SERVICE.glucose, CHAR.glucoseMeasurement)) {\n throw new Error('This device does not send glucose the standard way.');\n }\n const waiting = new Map<number, GlucoseMeasurement>();\n const meals = new Map<number, MealRelation>();\n const subs: Array<() => void> = [];\n\n const emit = (sequence: number) => {\n const m = waiting.get(sequence);\n if (!m || m.mgdl === undefined) return;\n waiting.delete(sequence);\n const at = m.at ?? ctx.now();\n const meal = meals.get(sequence);\n ctx.onReading({\n kind: 'glucose',\n id: readingId(link.id, 'glucose', at, sequence),\n at,\n deviceId: link.id,\n deviceName: link.name,\n mgdl: m.mgdl,\n ...(meal ? { meal } : {}),\n });\n };\n\n subs.push(\n link.monitor(SERVICE.glucose, CHAR.glucoseMeasurement, (bytes) => {\n const m = safe(() => parseGlucoseMeasurement(bytes, ctx.now()));\n if (!m || m.mgdl === undefined) return;\n waiting.set(m.sequence, m);\n if (!m.hasContext) emit(m.sequence);\n }),\n );\n if (link.has(SERVICE.glucose, CHAR.glucoseContext)) {\n subs.push(\n link.monitor(SERVICE.glucose, CHAR.glucoseContext, (bytes) => {\n const c = safe(() => parseGlucoseContext(bytes));\n if (!c) return;\n if (c.meal) meals.set(c.sequence, MEAL_RELATION[c.meal]);\n emit(c.sequence);\n }),\n );\n }\n if (link.has(SERVICE.glucose, CHAR.recordAccessControlPoint)) {\n subs.push(\n link.monitor(SERVICE.glucose, CHAR.recordAccessControlPoint, (bytes) => {\n const result = parseRacpResponse(bytes);\n if (!result) return;\n // The transfer is over: anything still waiting for a context goes without one.\n for (const sequence of [...waiting.keys()]) emit(sequence);\n if (result === 'failed') ctx.onNotice?.('The meter could not send its stored readings.');\n }),\n );\n try {\n await link.write(SERVICE.glucose, CHAR.recordAccessControlPoint, RACP_REPORT_ALL);\n } catch {\n ctx.onNotice?.('Pair the meter when your phone asks, then try again to get its stored readings.');\n }\n }\n return {\n stop() {\n for (const sequence of [...waiting.keys()]) emit(sequence);\n subs.forEach((u) => u());\n },\n };\n}\n"],"names":["consent","_a"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import {SERVICE,CHAR}from'./uuids.js';import {readingId}from'../readings.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
+ function parseTreadmillData(bytes) {
18
+ if (bytes.length < 2) return {};
19
+ const flags = bytes[0] | bytes[1] << 8;
20
+ let i = 2;
21
+ const u16 = () => {
22
+ const v = bytes[i] | bytes[i + 1] << 8;
23
+ i += 2;
24
+ return v;
25
+ };
26
+ const s16 = () => {
27
+ const v = u16();
28
+ return v >= 32768 ? v - 65536 : v;
29
+ };
30
+ const u24 = () => {
31
+ const v = bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16;
32
+ i += 3;
33
+ return v;
34
+ };
35
+ const u8 = () => bytes[i++];
36
+ const out = {};
37
+ if ((flags & 1) === 0 && i + 2 <= bytes.length) out.speedKmh = u16() / 100;
38
+ if (flags & 2 && i + 2 <= bytes.length) out.avgSpeedKmh = u16() / 100;
39
+ if (flags & 4 && i + 3 <= bytes.length) out.distanceM = u24();
40
+ if (flags & 8 && i + 4 <= bytes.length) {
41
+ out.inclinePct = s16() / 10;
42
+ s16();
43
+ }
44
+ if (flags & 16 && i + 4 <= bytes.length) {
45
+ u16();
46
+ u16();
47
+ }
48
+ if (flags & 32 && i + 1 <= bytes.length) u8();
49
+ if (flags & 64 && i + 1 <= bytes.length) u8();
50
+ if (flags & 128 && i + 5 <= bytes.length) {
51
+ out.kcal = u16();
52
+ u16();
53
+ u8();
54
+ }
55
+ if (flags & 256 && i + 1 <= bytes.length) out.heartRate = u8();
56
+ if (flags & 512 && i + 1 <= bytes.length) u8();
57
+ if (flags & 1024 && i + 2 <= bytes.length) out.elapsedS = u16();
58
+ return out;
59
+ }
60
+ function startFtmsTreadmill(link, ctx) {
61
+ if (!link.has(SERVICE.fitnessMachine, CHAR.treadmillData)) {
62
+ throw new Error("This treadmill does not send its data the standard way.");
63
+ }
64
+ const startedAt = ctx.now();
65
+ let lastSpeed = 0;
66
+ let maxDistanceM = 0;
67
+ let maxKcal = 0;
68
+ let lastHr;
69
+ let maxHr = 0;
70
+ let hrSum = 0;
71
+ let hrCount = 0;
72
+ let lastElapsedS;
73
+ let moved = false;
74
+ const unsub = link.monitor(SERVICE.fitnessMachine, CHAR.treadmillData, (bytes) => {
75
+ var _a, _b, _c;
76
+ const d = parseTreadmillData(bytes);
77
+ if (d.speedKmh !== void 0) lastSpeed = d.speedKmh;
78
+ if (d.distanceM !== void 0) maxDistanceM = Math.max(maxDistanceM, d.distanceM);
79
+ if (d.kcal !== void 0) maxKcal = Math.max(maxKcal, d.kcal);
80
+ if (d.elapsedS !== void 0) lastElapsedS = d.elapsedS;
81
+ if (d.heartRate) {
82
+ lastHr = d.heartRate;
83
+ maxHr = Math.max(maxHr, d.heartRate);
84
+ hrSum += d.heartRate;
85
+ hrCount += 1;
86
+ }
87
+ if (((_a = d.speedKmh) != null ? _a : 0) > 0 || ((_b = d.distanceM) != null ? _b : 0) > 0) moved = true;
88
+ (_c = ctx.onLive) == null ? void 0 : _c.call(ctx, __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
89
+ speedKmh: lastSpeed
90
+ }, d.distanceM !== void 0 ? {
91
+ distanceM: maxDistanceM
92
+ } : {}), d.inclinePct !== void 0 ? {
93
+ inclinePct: d.inclinePct
94
+ } : {}), d.kcal !== void 0 ? {
95
+ kcal: maxKcal
96
+ } : {}), d.elapsedS !== void 0 ? {
97
+ elapsedS: d.elapsedS
98
+ } : {}), lastHr !== void 0 ? {
99
+ bpm: lastHr
100
+ } : {}));
101
+ });
102
+ return {
103
+ stop() {
104
+ unsub();
105
+ if (!moved) return;
106
+ const end = ctx.now();
107
+ const durationMin = lastElapsedS !== void 0 ? lastElapsedS / 60 : (end - startedAt) / 6e4;
108
+ if (durationMin < 0.5 && maxDistanceM === 0) return;
109
+ const reading = __spreadValues(__spreadValues(__spreadValues({
110
+ kind: "workout",
111
+ id: readingId(link.id, "workout", startedAt),
112
+ at: startedAt,
113
+ end,
114
+ deviceId: link.id,
115
+ deviceName: link.name,
116
+ workoutType: lastSpeed > 7 ? "run" : "walk",
117
+ durationMin
118
+ }, maxDistanceM > 0 ? {
119
+ distanceMeters: maxDistanceM
120
+ } : {}), maxKcal > 0 ? {
121
+ calories: maxKcal
122
+ } : {}), hrCount ? {
123
+ avgHr: Math.round(hrSum / hrCount),
124
+ maxHr
125
+ } : {});
126
+ ctx.onReading(reading);
127
+ }
128
+ };
129
+ }export{parseTreadmillData,startFtmsTreadmill};//# sourceMappingURL=ftms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ftms.js","sources":["../../../../src/features/devices/gatt/ftms.ts"],"sourcesContent":["/**\n * The Bluetooth SIG Fitness Machine profile, Treadmill Data (0x2ACD). RENPHO's\n * treadmill (R-Q004) speaks it, and so does any standard treadmill, so this is\n * not reverse-engineered: it follows the FTMS spec, which matches what RENPHO's\n * own app decodes (the C function at 0x101ae28f8).\n *\n * The treadmill streams instantaneous speed/distance/calories; Yantra shows\n * them live and, when the session ends after real movement, keeps one workout.\n */\nimport type { DriverContext, DriverHandle, GattLink } from '../drivers';\nimport { CHAR, SERVICE } from './uuids';\nimport { readingId, type WorkoutReading } from '../readings';\n\ninterface TreadmillData {\n speedKmh?: number;\n avgSpeedKmh?: number;\n distanceM?: number;\n inclinePct?: number;\n kcal?: number;\n heartRate?: number;\n elapsedS?: number;\n}\n\n/** Decode one Treadmill Data notification by its flags field (FTMS 4.9.1.1). */\nexport function parseTreadmillData(bytes: Uint8Array): TreadmillData {\n if (bytes.length < 2) return {};\n const flags = bytes[0] | (bytes[1] << 8);\n let i = 2;\n const u16 = () => {\n const v = bytes[i] | (bytes[i + 1] << 8);\n i += 2;\n return v;\n };\n const s16 = () => {\n const v = u16();\n return v >= 0x8000 ? v - 0x10000 : v;\n };\n const u24 = () => {\n const v = bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16);\n i += 3;\n return v;\n };\n const u8 = () => bytes[i++];\n const out: TreadmillData = {};\n // bit0 \"More Data\" == 0 means the instantaneous speed is in this packet.\n if ((flags & 0x0001) === 0 && i + 2 <= bytes.length) out.speedKmh = u16() / 100;\n if (flags & 0x0002 && i + 2 <= bytes.length) out.avgSpeedKmh = u16() / 100;\n if (flags & 0x0004 && i + 3 <= bytes.length) out.distanceM = u24();\n if (flags & 0x0008 && i + 4 <= bytes.length) {\n out.inclinePct = s16() / 10;\n s16(); // ramp angle, unused\n }\n if (flags & 0x0010 && i + 4 <= bytes.length) {\n u16(); // positive elevation gain, unused\n u16(); // negative elevation gain, unused\n }\n if (flags & 0x0020 && i + 1 <= bytes.length) u8(); // instantaneous pace\n if (flags & 0x0040 && i + 1 <= bytes.length) u8(); // average pace\n if (flags & 0x0080 && i + 5 <= bytes.length) {\n out.kcal = u16(); // total energy\n u16(); // energy per hour\n u8(); // energy per minute\n }\n if (flags & 0x0100 && i + 1 <= bytes.length) out.heartRate = u8();\n if (flags & 0x0200 && i + 1 <= bytes.length) u8(); // metabolic equivalent\n if (flags & 0x0400 && i + 2 <= bytes.length) out.elapsedS = u16();\n return out;\n}\n\nexport function startFtmsTreadmill(link: GattLink, ctx: DriverContext): DriverHandle {\n if (!link.has(SERVICE.fitnessMachine, CHAR.treadmillData)) {\n throw new Error('This treadmill does not send its data the standard way.');\n }\n const startedAt = ctx.now();\n let lastSpeed = 0;\n let maxDistanceM = 0;\n let maxKcal = 0;\n let lastHr: number | undefined;\n let maxHr = 0;\n let hrSum = 0;\n let hrCount = 0;\n let lastElapsedS: number | undefined;\n let moved = false;\n\n const unsub = link.monitor(SERVICE.fitnessMachine, CHAR.treadmillData, (bytes) => {\n const d = parseTreadmillData(bytes);\n if (d.speedKmh !== undefined) lastSpeed = d.speedKmh;\n if (d.distanceM !== undefined) maxDistanceM = Math.max(maxDistanceM, d.distanceM);\n if (d.kcal !== undefined) maxKcal = Math.max(maxKcal, d.kcal);\n if (d.elapsedS !== undefined) lastElapsedS = d.elapsedS;\n if (d.heartRate) {\n lastHr = d.heartRate;\n maxHr = Math.max(maxHr, d.heartRate);\n hrSum += d.heartRate;\n hrCount += 1;\n }\n if ((d.speedKmh ?? 0) > 0 || (d.distanceM ?? 0) > 0) moved = true;\n ctx.onLive?.({\n speedKmh: lastSpeed,\n ...(d.distanceM !== undefined ? { distanceM: maxDistanceM } : {}),\n ...(d.inclinePct !== undefined ? { inclinePct: d.inclinePct } : {}),\n ...(d.kcal !== undefined ? { kcal: maxKcal } : {}),\n ...(d.elapsedS !== undefined ? { elapsedS: d.elapsedS } : {}),\n ...(lastHr !== undefined ? { bpm: lastHr } : {}),\n });\n });\n\n return {\n stop() {\n unsub();\n if (!moved) return;\n const end = ctx.now();\n const durationMin = lastElapsedS !== undefined ? lastElapsedS / 60 : (end - startedAt) / 60_000;\n // A walk under half a minute with no distance is not worth keeping.\n if (durationMin < 0.5 && maxDistanceM === 0) return;\n const reading: WorkoutReading = {\n kind: 'workout',\n id: readingId(link.id, 'workout', startedAt),\n at: startedAt,\n end,\n deviceId: link.id,\n deviceName: link.name,\n workoutType: lastSpeed > 7 ? 'run' : 'walk',\n durationMin,\n ...(maxDistanceM > 0 ? { distanceMeters: maxDistanceM } : {}),\n ...(maxKcal > 0 ? { calories: maxKcal } : {}),\n ...(hrCount ? { avgHr: Math.round(hrSum / hrCount), maxHr } : {}),\n };\n ctx.onReading(reading);\n },\n };\n}\n"],"names":[],"mappings":"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"}
@@ -16,7 +16,9 @@ const SERVICE = {
16
16
  pulseOximeter: uuid16("1822"),
17
17
  glucose: uuid16("1808"),
18
18
  currentTime: uuid16("1805"),
19
- deviceInformation: uuid16("180a")
19
+ deviceInformation: uuid16("180a"),
20
+ /** Fitness Machine (treadmills, bikes, rowers). */
21
+ fitnessMachine: uuid16("1826")
20
22
  };
21
23
  const CHAR = {
22
24
  weightMeasurement: uuid16("2a9d"),
@@ -37,5 +39,9 @@ const CHAR = {
37
39
  gender: uuid16("2a8c"),
38
40
  height: uuid16("2a8e"),
39
41
  manufacturerName: uuid16("2a29"),
40
- modelNumber: uuid16("2a24")
42
+ modelNumber: uuid16("2a24"),
43
+ /** Fitness Machine: treadmill data (notify) and control point. */
44
+ treadmillData: uuid16("2acd"),
45
+ fitnessMachineControlPoint: uuid16("2ad9"),
46
+ fitnessMachineStatus: uuid16("2ada")
41
47
  };export{CHAR,SERVICE,sameUuid,uuid16};//# sourceMappingURL=uuids.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"uuids.js","sources":["../../../../src/features/devices/gatt/uuids.ts"],"sourcesContent":["/**\n * The Bluetooth SIG health services and characteristics Yantra speaks. Only\n * these standard profiles: a brand scale with its own protocol (RENPHO,\n * Withings, Xiaomi) keeps its own app, which writes to Apple Health / Health\n * Connect, and Yantra reads it there.\n *\n * react-native-ble-plx reports UUIDs in their full lowercase 128-bit form.\n */\n\nconst BASE = '-0000-1000-8000-00805f9b34fb';\n\n/** \"181d\" -> \"0000181d-0000-1000-8000-00805f9b34fb\". */\nexport function uuid16(short: string): string {\n return `0000${short.toLowerCase().padStart(4, '0')}${BASE}`;\n}\n\n/** Same UUID whatever form or case a platform reports it in. */\nexport function sameUuid(a: string, b: string): boolean {\n const full = (u: string) => (u.length === 4 ? uuid16(u) : u.toLowerCase());\n return full(a) === full(b);\n}\n\nexport const SERVICE = {\n weightScale: uuid16('181d'),\n bodyComposition: uuid16('181b'),\n userData: uuid16('181c'),\n bloodPressure: uuid16('1810'),\n heartRate: uuid16('180d'),\n healthThermometer: uuid16('1809'),\n pulseOximeter: uuid16('1822'),\n glucose: uuid16('1808'),\n currentTime: uuid16('1805'),\n deviceInformation: uuid16('180a'),\n} as const;\n\nexport const CHAR = {\n weightMeasurement: uuid16('2a9d'),\n bodyCompositionMeasurement: uuid16('2a9c'),\n bloodPressureMeasurement: uuid16('2a35'),\n heartRateMeasurement: uuid16('2a37'),\n temperatureMeasurement: uuid16('2a1c'),\n plxSpotCheck: uuid16('2a5e'),\n plxContinuous: uuid16('2a5f'),\n glucoseMeasurement: uuid16('2a18'),\n glucoseContext: uuid16('2a34'),\n recordAccessControlPoint: uuid16('2a52'),\n currentTime: uuid16('2a2b'),\n userControlPoint: uuid16('2a9f'),\n databaseChangeIncrement: uuid16('2a99'),\n userIndex: uuid16('2a9a'),\n age: uuid16('2a80'),\n gender: uuid16('2a8c'),\n height: uuid16('2a8e'),\n manufacturerName: uuid16('2a29'),\n modelNumber: uuid16('2a24'),\n} as const;\n"],"names":[],"mappings":"AASA,MAAM,IAAO,GAAA,8BAAA;AAGN,SAAS,OAAO,KAAuB,EAAA;AAC5C,EAAO,OAAA,CAAA,IAAA,EAAO,MAAM,WAAY,EAAA,CAAE,SAAS,CAAG,EAAA,GAAG,CAAC,CAAA,EAAG,IAAI,CAAA,CAAA;AAC3D;AAGgB,SAAA,QAAA,CAAS,GAAW,CAAoB,EAAA;AACtD,EAAM,MAAA,IAAA,GAAO,CAAC,CAAA,KAAc,CAAE,CAAA,MAAA,KAAW,IAAI,MAAO,CAAA,CAAC,CAAI,GAAA,CAAA,CAAE,WAAY,EAAA;AACvE,EAAA,OAAO,IAAK,CAAA,CAAC,CAAM,KAAA,IAAA,CAAK,CAAC,CAAA;AAC3B;AACO,MAAM,OAAU,GAAA;AAAA,EACrB,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA,EAC1B,eAAA,EAAiB,OAAO,MAAM,CAAA;AAAA,EAC9B,QAAA,EAAU,OAAO,MAAM,CAAA;AAAA,EACvB,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,SAAA,EAAW,OAAO,MAAM,CAAA;AAAA,EACxB,iBAAA,EAAmB,OAAO,MAAM,CAAA;AAAA,EAChC,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA,EAC1B,iBAAA,EAAmB,OAAO,MAAM;AAClC;AACO,MAAM,IAAO,GAAA;AAAA,EAClB,iBAAA,EAAmB,OAAO,MAAM,CAAA;AAAA,EAChC,0BAAA,EAA4B,OAAO,MAAM,CAAA;AAAA,EACzC,wBAAA,EAA0B,OAAO,MAAM,CAAA;AAAA,EACvC,oBAAA,EAAsB,OAAO,MAAM,CAAA;AAAA,EACnC,sBAAA,EAAwB,OAAO,MAAM,CAAA;AAAA,EACrC,YAAA,EAAc,OAAO,MAAM,CAAA;AAAA,EAC3B,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,kBAAA,EAAoB,OAAO,MAAM,CAAA;AAAA,EACjC,cAAA,EAAgB,OAAO,MAAM,CAAA;AAAA,EAC7B,wBAAA,EAA0B,OAAO,MAAM,CAAA;AAAA,EACvC,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA,EAC1B,gBAAA,EAAkB,OAAO,MAAM,CAAA;AAAA,EAC/B,uBAAA,EAAyB,OAAO,MAAM,CAAA;AAAA,EACtC,SAAA,EAAW,OAAO,MAAM,CAAA;AAAA,EACxB,GAAA,EAAK,OAAO,MAAM,CAAA;AAAA,EAClB,MAAA,EAAQ,OAAO,MAAM,CAAA;AAAA,EACrB,MAAA,EAAQ,OAAO,MAAM,CAAA;AAAA,EACrB,gBAAA,EAAkB,OAAO,MAAM,CAAA;AAAA,EAC/B,WAAA,EAAa,OAAO,MAAM;AAC5B"}
1
+ {"version":3,"file":"uuids.js","sources":["../../../../src/features/devices/gatt/uuids.ts"],"sourcesContent":["/**\n * The Bluetooth SIG health services and characteristics Yantra speaks. Only\n * these standard profiles: a brand scale with its own protocol (RENPHO,\n * Withings, Xiaomi) keeps its own app, which writes to Apple Health / Health\n * Connect, and Yantra reads it there.\n *\n * react-native-ble-plx reports UUIDs in their full lowercase 128-bit form.\n */\n\nconst BASE = '-0000-1000-8000-00805f9b34fb';\n\n/** \"181d\" -> \"0000181d-0000-1000-8000-00805f9b34fb\". */\nexport function uuid16(short: string): string {\n return `0000${short.toLowerCase().padStart(4, '0')}${BASE}`;\n}\n\n/** Same UUID whatever form or case a platform reports it in. */\nexport function sameUuid(a: string, b: string): boolean {\n const full = (u: string) => (u.length === 4 ? uuid16(u) : u.toLowerCase());\n return full(a) === full(b);\n}\n\nexport const SERVICE = {\n weightScale: uuid16('181d'),\n bodyComposition: uuid16('181b'),\n userData: uuid16('181c'),\n bloodPressure: uuid16('1810'),\n heartRate: uuid16('180d'),\n healthThermometer: uuid16('1809'),\n pulseOximeter: uuid16('1822'),\n glucose: uuid16('1808'),\n currentTime: uuid16('1805'),\n deviceInformation: uuid16('180a'),\n /** Fitness Machine (treadmills, bikes, rowers). */\n fitnessMachine: uuid16('1826'),\n} as const;\n\nexport const CHAR = {\n weightMeasurement: uuid16('2a9d'),\n bodyCompositionMeasurement: uuid16('2a9c'),\n bloodPressureMeasurement: uuid16('2a35'),\n heartRateMeasurement: uuid16('2a37'),\n temperatureMeasurement: uuid16('2a1c'),\n plxSpotCheck: uuid16('2a5e'),\n plxContinuous: uuid16('2a5f'),\n glucoseMeasurement: uuid16('2a18'),\n glucoseContext: uuid16('2a34'),\n recordAccessControlPoint: uuid16('2a52'),\n currentTime: uuid16('2a2b'),\n userControlPoint: uuid16('2a9f'),\n databaseChangeIncrement: uuid16('2a99'),\n userIndex: uuid16('2a9a'),\n age: uuid16('2a80'),\n gender: uuid16('2a8c'),\n height: uuid16('2a8e'),\n manufacturerName: uuid16('2a29'),\n modelNumber: uuid16('2a24'),\n /** Fitness Machine: treadmill data (notify) and control point. */\n treadmillData: uuid16('2acd'),\n fitnessMachineControlPoint: uuid16('2ad9'),\n fitnessMachineStatus: uuid16('2ada'),\n} as const;\n"],"names":[],"mappings":"AASA,MAAM,IAAO,GAAA,8BAAA;AAGN,SAAS,OAAO,KAAuB,EAAA;AAC5C,EAAO,OAAA,CAAA,IAAA,EAAO,MAAM,WAAY,EAAA,CAAE,SAAS,CAAG,EAAA,GAAG,CAAC,CAAA,EAAG,IAAI,CAAA,CAAA;AAC3D;AAGgB,SAAA,QAAA,CAAS,GAAW,CAAoB,EAAA;AACtD,EAAM,MAAA,IAAA,GAAO,CAAC,CAAA,KAAc,CAAE,CAAA,MAAA,KAAW,IAAI,MAAO,CAAA,CAAC,CAAI,GAAA,CAAA,CAAE,WAAY,EAAA;AACvE,EAAA,OAAO,IAAK,CAAA,CAAC,CAAM,KAAA,IAAA,CAAK,CAAC,CAAA;AAC3B;AACO,MAAM,OAAU,GAAA;AAAA,EACrB,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA,EAC1B,eAAA,EAAiB,OAAO,MAAM,CAAA;AAAA,EAC9B,QAAA,EAAU,OAAO,MAAM,CAAA;AAAA,EACvB,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,SAAA,EAAW,OAAO,MAAM,CAAA;AAAA,EACxB,iBAAA,EAAmB,OAAO,MAAM,CAAA;AAAA,EAChC,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,OAAA,EAAS,OAAO,MAAM,CAAA;AAAA,EACtB,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA,EAC1B,iBAAA,EAAmB,OAAO,MAAM,CAAA;AAAA;AAAA,EAEhC,cAAA,EAAgB,OAAO,MAAM;AAC/B;AACO,MAAM,IAAO,GAAA;AAAA,EAClB,iBAAA,EAAmB,OAAO,MAAM,CAAA;AAAA,EAChC,0BAAA,EAA4B,OAAO,MAAM,CAAA;AAAA,EACzC,wBAAA,EAA0B,OAAO,MAAM,CAAA;AAAA,EACvC,oBAAA,EAAsB,OAAO,MAAM,CAAA;AAAA,EACnC,sBAAA,EAAwB,OAAO,MAAM,CAAA;AAAA,EACrC,YAAA,EAAc,OAAO,MAAM,CAAA;AAAA,EAC3B,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,kBAAA,EAAoB,OAAO,MAAM,CAAA;AAAA,EACjC,cAAA,EAAgB,OAAO,MAAM,CAAA;AAAA,EAC7B,wBAAA,EAA0B,OAAO,MAAM,CAAA;AAAA,EACvC,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA,EAC1B,gBAAA,EAAkB,OAAO,MAAM,CAAA;AAAA,EAC/B,uBAAA,EAAyB,OAAO,MAAM,CAAA;AAAA,EACtC,SAAA,EAAW,OAAO,MAAM,CAAA;AAAA,EACxB,GAAA,EAAK,OAAO,MAAM,CAAA;AAAA,EAClB,MAAA,EAAQ,OAAO,MAAM,CAAA;AAAA,EACrB,MAAA,EAAQ,OAAO,MAAM,CAAA;AAAA,EACrB,gBAAA,EAAkB,OAAO,MAAM,CAAA;AAAA,EAC/B,WAAA,EAAa,OAAO,MAAM,CAAA;AAAA;AAAA,EAE1B,aAAA,EAAe,OAAO,MAAM,CAAA;AAAA,EAC5B,0BAAA,EAA4B,OAAO,MAAM,CAAA;AAAA,EACzC,oBAAA,EAAsB,OAAO,MAAM;AACrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"link.js","sources":["../../../src/features/devices/link.ts"],"sourcesContent":["/**\n * What the screens and the session need from the Bluetooth radio, without the\n * native library: the transport's shape, a found device, and errors in plain\n * words. bleTransport.ts is the react-native-ble-plx implementation; tests use\n * fakes.\n */\nimport type { DeviceKindId } from './catalog';\nimport type { GattLink } from './drivers';\n\nexport type RadioState = 'ready' | 'off' | 'unauthorized' | 'unsupported' | 'unknown';\n\nexport interface FoundDevice {\n id: string;\n name: string;\n rssi?: number;\n /** From the services it advertises; undefined when it does not say. */\n kind?: DeviceKindId;\n}\n\nexport interface ConnectedLink extends GattLink {\n close(): Promise<void>;\n}\n\nexport interface BleTransport {\n radio(): Promise<RadioState>;\n onRadioChange(listener: (state: RadioState) => void): () => void;\n /** Android's runtime permissions (scan, connect). Resolves false when refused. */\n requestPermissions(): Promise<boolean>;\n /** Calls `onFound` for each device advertising one of `services`. Returns the stop. */\n scan(\n services: readonly string[],\n onFound: (d: FoundDevice) => void,\n onError: (message: string) => void,\n ): () => void;\n connect(id: string, name: string, onDisconnect: () => void): Promise<ConnectedLink>;\n}\n\n/** Plain words for what went wrong; never the library's error codes. */\nexport function describeBleError(err: unknown): string {\n const message = err instanceof Error ? err.message : String(err ?? '');\n if (/timed? ?out/i.test(message)) return 'The device did not answer. Wake it up and try again.';\n if (/auth|encrypt|bond|pair/i.test(message)) return 'Pair the device when your phone asks, then try again.';\n if (/disconnect/i.test(message)) return 'The device disconnected. Wake it up and try again.';\n if (/powered ?off/i.test(message)) return 'Bluetooth is off. Turn it on and try again.';\n return 'Could not talk to the device. Wake it up and try again.';\n}\n"],"names":[],"mappings":"AA8BO,SAAS,iBAAiB,GAAsB,EAAA;AACrD,EAAA,MAAM,UAAU,GAAe,YAAA,KAAA,GAAQ,IAAI,OAAU,GAAA,MAAA,CAAO,oBAAO,EAAE,CAAA;AACrE,EAAA,IAAI,cAAe,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,sDAAA;AACzC,EAAA,IAAI,yBAA0B,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,uDAAA;AACpD,EAAA,IAAI,aAAc,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,oDAAA;AACxC,EAAA,IAAI,eAAgB,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,6CAAA;AAC1C,EAAO,OAAA,yDAAA;AACT"}
1
+ {"version":3,"file":"link.js","sources":["../../../src/features/devices/link.ts"],"sourcesContent":["/**\n * What the screens and the session need from the Bluetooth radio, without the\n * native library: the transport's shape, a found device, and errors in plain\n * words. bleTransport.ts is the react-native-ble-plx implementation; tests use\n * fakes.\n */\nimport type { DeviceKindId } from './catalog';\nimport type { GattLink } from './drivers';\nimport type { DeviceProtocolId } from './brands/detect';\n\nexport type RadioState = 'ready' | 'off' | 'unauthorized' | 'unsupported' | 'unknown';\n\n/** One advertisement as the radio heard it. */\nexport interface Advertisement {\n id: string;\n /** The advertised local name, else the cached device name. */\n name?: string;\n rssi?: number;\n /** Raw manufacturer data, company id first (as both platforms report it). */\n manufacturerData?: Uint8Array;\n serviceUUIDs?: readonly string[];\n serviceData?: Readonly<Record<string, Uint8Array>>;\n}\n\n/** What an advertisement says about a device beyond its id, name and signal. */\nexport interface DeviceIdentity {\n /** From the services it advertises or the brand it was recognised as; undefined when it does not say. */\n kind?: DeviceKindId;\n /** A brand protocol (RENPHO, QN ...) recognised from the advertisement; undefined = Bluetooth SIG profiles. */\n protocol?: DeviceProtocolId;\n /** The model the advertisement names (R-A017, ES-CS20M ...), when it does. */\n model?: string;\n /** The device's own address from its advertisement (iOS hides it otherwise); some protocols need it. */\n mac?: string;\n /** The brand's own device type number (RENPHO), when advertised. */\n deviceType?: number;\n}\n\nexport interface FoundDevice extends DeviceIdentity {\n id: string;\n name: string;\n rssi?: number;\n}\n\nexport interface ConnectedLink extends GattLink {\n close(): Promise<void>;\n}\n\nexport interface ScanRequest {\n /** Bluetooth SIG services to look for. */\n services: readonly string[];\n /**\n * Brand devices do not advertise a standard service, so with a matcher the\n * scan listens to everything and asks it about each advertisement.\n */\n match?: (adv: Advertisement) => DeviceIdentity | undefined;\n /** Every advertisement, repeats included (broadcast-only scales put the weight in them). */\n onAdvertisement?: (adv: Advertisement) => void;\n}\n\nexport interface BleTransport {\n radio(): Promise<RadioState>;\n onRadioChange(listener: (state: RadioState) => void): () => void;\n /** Android's runtime permissions (scan, connect). Resolves false when refused. */\n requestPermissions(): Promise<boolean>;\n /** Calls `onFound` once per device advertising one of the services or matched. Returns the stop. */\n scan(request: ScanRequest, onFound: (d: FoundDevice) => void, onError: (message: string) => void): () => void;\n connect(id: string, name: string, onDisconnect: () => void): Promise<ConnectedLink>;\n}\n\n/** Plain words for what went wrong; never the library's error codes. */\nexport function describeBleError(err: unknown): string {\n const message = err instanceof Error ? err.message : String(err ?? '');\n if (/timed? ?out/i.test(message)) return 'The device did not answer. Wake it up and try again.';\n if (/auth|encrypt|bond|pair/i.test(message)) return 'Pair the device when your phone asks, then try again.';\n if (/disconnect/i.test(message)) return 'The device disconnected. Wake it up and try again.';\n if (/powered ?off/i.test(message)) return 'Bluetooth is off. Turn it on and try again.';\n return 'Could not talk to the device. Wake it up and try again.';\n}\n"],"names":[],"mappings":"AAkEO,SAAS,iBAAiB,GAAsB,EAAA;AACrD,EAAA,MAAM,UAAU,GAAe,YAAA,KAAA,GAAQ,IAAI,OAAU,GAAA,MAAA,CAAO,oBAAO,EAAE,CAAA;AACrE,EAAA,IAAI,cAAe,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,sDAAA;AACzC,EAAA,IAAI,yBAA0B,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,uDAAA;AACpD,EAAA,IAAI,aAAc,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,oDAAA;AACxC,EAAA,IAAI,eAAgB,CAAA,IAAA,CAAK,OAAO,CAAA,EAAU,OAAA,6CAAA;AAC1C,EAAO,OAAA,yDAAA;AACT"}
@@ -61,6 +61,8 @@ function deviceHealthDays(readings, opts) {
61
61
  if (temp) day.bodyTempC = temp.celsius;
62
62
  const spo2 = latestOf(list.filter((r) => r.kind === "spo2"), () => true);
63
63
  if (spo2) day.spo2 = spo2.spo2;
64
+ const waist = latestOf(list.filter((r) => r.kind === "body-measurement" && r.bodyPart === "waist"), () => true);
65
+ if (waist) day.waistCm = waist.cm;
64
66
  const readingsOut = [];
65
67
  for (const r of list) {
66
68
  if (r.kind === "blood-pressure") {
@@ -107,7 +109,25 @@ function deviceHealthDays(readings, opts) {
107
109
  } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;
108
110
  return w;
109
111
  });
110
- if (workouts.length) day.workouts = workouts;
112
+ for (const s of list.filter((r) => r.kind === "workout").sort((a, b) => a.at - b.at)) {
113
+ const w = __spreadValues(__spreadValues(__spreadValues(__spreadValues({
114
+ id: s.id,
115
+ start: new Date(s.at).toISOString(),
116
+ end: new Date(s.end).toISOString(),
117
+ type: s.workoutType,
118
+ durationMin: round1(s.durationMin)
119
+ }, s.calories !== void 0 ? {
120
+ calories: s.calories
121
+ } : {}), s.distanceMeters !== void 0 ? {
122
+ distanceMeters: s.distanceMeters
123
+ } : {}), s.avgHr !== void 0 ? {
124
+ avgHr: s.avgHr
125
+ } : {}), s.maxHr !== void 0 ? {
126
+ maxHr: s.maxHr
127
+ } : {});
128
+ workouts.push(w);
129
+ }
130
+ if (workouts.length) day.workouts = workouts.sort((a, b) => a.start.localeCompare(b.start));
111
131
  const clean = sanitizeDay(day, dropped);
112
132
  if (hasData(clean)) days.push(__spreadProps(__spreadValues({}, clean), {
113
133
  syncedAt: opts.syncedAt
@@ -140,6 +160,17 @@ function describeReading(r, units = "metric") {
140
160
  const minutes = Math.round((r.end - r.at) / 6e4);
141
161
  return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;
142
162
  }
163
+ case "workout": {
164
+ const parts = [`${Math.round(r.durationMin)} min`];
165
+ if (r.jumps !== void 0) parts.unshift(`${r.jumps} jumps`);
166
+ if (r.distanceMeters !== void 0) parts.push(`${round1(r.distanceMeters / 1e3)} km`);
167
+ if (r.calories !== void 0) parts.push(`${Math.round(r.calories)} kcal`);
168
+ return parts.join(", ");
169
+ }
170
+ case "body-measurement": {
171
+ const part = r.bodyPart.replace(/_/g, " ");
172
+ return units === "imperial" ? `${part} ${round1(r.cm / 2.54)} in` : `${part} ${round1(r.cm)} cm`;
173
+ }
143
174
  default:
144
175
  return "";
145
176
  }
@@ -1 +1 @@
1
- {"version":3,"file":"readings.js","sources":["../../../src/features/devices/readings.ts"],"sourcesContent":["/**\n * What a paired Bluetooth device measured, and how it reaches the Fitness\n * backend.\n *\n * The backend keeps ONE day per source and replaces it whole on every upload,\n * so a reading is never sent alone: the app keeps every reading it took (the\n * device store), and each upload rebuilds the full `device` day for the dates\n * that changed. A blood-pressure reading in the evening never wipes the\n * morning's weigh-in.\n *\n * Reading ids are stable (device + what + when): a monitor that sends its\n * whole memory on every connection, or the same reading arriving twice, is\n * stored once.\n */\n\nimport { sanitizeDay, hasData } from '../health/buildDays';\nimport type { TimedValue } from '../health/model';\nimport type { HealthDay, HealthReading, HealthWorkout, MealRelation } from '../health/types';\nimport type { ZoneClock } from '../health/time';\nimport { zoneMinutes } from '../health/zones';\n\ninterface ReadingBase {\n id: string;\n /** Epoch ms of the measurement. */\n at: number;\n deviceId: string;\n deviceName: string;\n}\n\nexport interface BodyReading extends ReadingBase {\n kind: 'body';\n weightKg?: number;\n bodyFatPct?: number;\n leanMassKg?: number;\n bmi?: number;\n}\n\nexport interface BloodPressureReading extends ReadingBase {\n kind: 'blood-pressure';\n systolic: number;\n diastolic: number;\n pulse?: number;\n /** What the monitor flagged (moved, loose cuff, irregular pulse), in words. */\n problems?: string[];\n}\n\nexport interface TemperatureReading extends ReadingBase {\n kind: 'temperature';\n celsius: number;\n}\n\nexport interface OxygenReading extends ReadingBase {\n kind: 'spo2';\n spo2: number;\n pulse?: number;\n}\n\nexport interface GlucoseReading extends ReadingBase {\n kind: 'glucose';\n mgdl: number;\n meal?: MealRelation;\n}\n\n/** A heart-rate strap worn for a session: the samples, summed up when it ended. */\nexport interface HeartRateSession extends ReadingBase {\n kind: 'heart-rate';\n /** `at` is the start. */\n end: number;\n /** run | walk | cycle | strength | hiit | other, as the person picked it. */\n workoutType: string;\n avgHr: number;\n maxHr: number;\n /** At most one sample every few seconds (see thinSamples); emptied once saved, see compactSession. */\n samples: TimedValue[];\n /** Minutes per heart-rate zone, kept when the samples are let go. */\n zoneMinutes?: number[];\n}\n\nexport type DeviceReading =\n | BodyReading\n | BloodPressureReading\n | TemperatureReading\n | OxygenReading\n | GlucoseReading\n | HeartRateSession;\n\nconst round1 = (n: number) => Math.round(n * 10) / 10;\n\n/** A stable id: the same measurement from the same device always gets the same one. */\nexport function readingId(deviceId: string, kind: DeviceReading['kind'], at: number, extra?: string | number): string {\n return [deviceId, kind, Math.round(at / 1000), extra].filter((p) => p !== undefined && p !== '').join(':');\n}\n\n/** Keep one heart-rate sample per `everyMs`: enough for zones, few enough to store and save. */\nexport function thinSamples(samples: readonly TimedValue[], everyMs = 5_000): TimedValue[] {\n const out: TimedValue[] = [];\n let last = -Infinity;\n for (const s of [...samples].sort((a, b) => a.t - b.t)) {\n if (s.t - last >= everyMs) {\n out.push(s);\n last = s.t;\n }\n }\n return out;\n}\n\nconst latestOf = <T extends DeviceReading>(list: readonly T[], has: (r: T) => boolean): T | undefined =>\n list.filter(has).reduce<T | undefined>((a, b) => (!a || b.at >= a.at ? b : a), undefined);\n\nexport interface DeviceDaysOptions {\n clock: ZoneClock;\n syncedAt: string;\n /** 208 - 0.7 x age, for heart-rate zones; undefined when the birth date is unknown. */\n maxHr?: number;\n /** Only these dates (the ones a new reading touched); every date when omitted. */\n dates?: ReadonlySet<string>;\n}\n\n/** Every reading -> one `device` HealthDay per local date, cleaned exactly as the backend cleans. */\nexport function deviceHealthDays(\n readings: readonly DeviceReading[],\n opts: DeviceDaysOptions,\n): { days: HealthDay[]; dropped: string[] } {\n const byDate = new Map<string, DeviceReading[]>();\n for (const r of readings) {\n const date = opts.clock.dateOf(r.at);\n if (opts.dates && !opts.dates.has(date)) continue;\n byDate.set(date, [...(byDate.get(date) ?? []), r]);\n }\n const dropped: string[] = [];\n const days: HealthDay[] = [];\n for (const [date, list] of [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n const day: HealthDay = { date, source: 'device' };\n const body = list.filter((r): r is BodyReading => r.kind === 'body');\n const weight = latestOf(body, (r) => r.weightKg !== undefined);\n if (weight) day.weightKg = weight.weightKg;\n const fat = latestOf(body, (r) => r.bodyFatPct !== undefined);\n if (fat) day.bodyFatPct = fat.bodyFatPct;\n const lean = latestOf(body, (r) => r.leanMassKg !== undefined);\n if (lean) day.leanMassKg = lean.leanMassKg;\n const bmi = latestOf(body, (r) => r.bmi !== undefined);\n if (bmi) day.bmi = bmi.bmi;\n const temp = latestOf(\n list.filter((r): r is TemperatureReading => r.kind === 'temperature'),\n () => true,\n );\n if (temp) day.bodyTempC = temp.celsius;\n const spo2 = latestOf(\n list.filter((r): r is OxygenReading => r.kind === 'spo2'),\n () => true,\n );\n if (spo2) day.spo2 = spo2.spo2;\n\n const readingsOut: HealthReading[] = [];\n for (const r of list) {\n if (r.kind === 'blood-pressure') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'blood-pressure',\n systolic: r.systolic,\n diastolic: r.diastolic,\n ...(r.pulse !== undefined ? { pulse: r.pulse } : {}),\n device: r.deviceName,\n });\n } else if (r.kind === 'glucose') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'glucose',\n glucoseMgDl: r.mgdl,\n ...(r.meal ? { meal: r.meal } : {}),\n device: r.deviceName,\n });\n }\n }\n if (readingsOut.length) day.readings = readingsOut.sort((a, b) => a.at.localeCompare(b.at));\n\n const workouts: HealthWorkout[] = list\n .filter((r): r is HeartRateSession => r.kind === 'heart-rate')\n .sort((a, b) => a.at - b.at)\n .map((s) => {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1((s.end - s.at) / 60_000),\n avgHr: s.avgHr,\n maxHr: s.maxHr,\n };\n if (opts.maxHr && s.samples.length) {\n w.zoneMinutes = zoneMinutes(s.samples, { start: s.at, end: s.end }, opts.maxHr);\n } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;\n return w;\n });\n if (workouts.length) day.workouts = workouts;\n\n const clean = sanitizeDay(day, dropped);\n if (hasData(clean)) days.push({ ...clean, syncedAt: opts.syncedAt });\n }\n return { days, dropped };\n}\n\n/** One line for a reading, as the person reads it (\"82.4 kg, body fat 23.1%\"). */\nexport function describeReading(r: DeviceReading, units: 'metric' | 'imperial' = 'metric'): string {\n const kg = (v: number) => (units === 'imperial' ? `${round1(v / 0.45359237)} lb` : `${round1(v)} kg`);\n switch (r.kind) {\n case 'body': {\n const parts: string[] = [];\n if (r.weightKg !== undefined) parts.push(kg(r.weightKg));\n if (r.bodyFatPct !== undefined) parts.push(`body fat ${round1(r.bodyFatPct)}%`);\n if (r.bmi !== undefined) parts.push(`BMI ${round1(r.bmi)}`);\n return parts.join(', ') || 'A measurement with no weight';\n }\n case 'blood-pressure':\n return `${Math.round(r.systolic)}/${Math.round(r.diastolic)} mmHg${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'temperature':\n return units === 'imperial' ? `${round1((r.celsius * 9) / 5 + 32)} °F` : `${round1(r.celsius)} °C`;\n case 'spo2':\n return `Blood oxygen ${Math.round(r.spo2)}%${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'glucose':\n return `${Math.round(r.mgdl)} mg/dL${r.meal ? `, ${r.meal.replace('-', ' ')}` : ''}`;\n case 'heart-rate': {\n const minutes = Math.round((r.end - r.at) / 60_000);\n return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;\n }\n default:\n return '';\n }\n}\n\n/**\n * A session's samples are only needed until they are uploaded and saved to\n * the health store; after that the zones are kept and the samples let go, so\n * the phone's store stays small.\n */\nexport function compactSession(s: HeartRateSession, maxHr: number | undefined): HeartRateSession {\n if (!s.samples.length) return s;\n const zones = maxHr ? zoneMinutes(s.samples, { start: s.at, end: s.end }, maxHr) : s.zoneMinutes;\n return { ...s, samples: [], ...(zones ? { zoneMinutes: zones } : {}) };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"readings.js","sources":["../../../src/features/devices/readings.ts"],"sourcesContent":["/**\n * What a paired Bluetooth device measured, and how it reaches the Fitness\n * backend.\n *\n * The backend keeps ONE day per source and replaces it whole on every upload,\n * so a reading is never sent alone: the app keeps every reading it took (the\n * device store), and each upload rebuilds the full `device` day for the dates\n * that changed. A blood-pressure reading in the evening never wipes the\n * morning's weigh-in.\n *\n * Reading ids are stable (device + what + when): a monitor that sends its\n * whole memory on every connection, or the same reading arriving twice, is\n * stored once.\n */\n\nimport { sanitizeDay, hasData } from '../health/buildDays';\nimport type { TimedValue } from '../health/model';\nimport type { HealthDay, HealthReading, HealthWorkout, MealRelation } from '../health/types';\nimport type { ZoneClock } from '../health/time';\nimport { zoneMinutes } from '../health/zones';\n\ninterface ReadingBase {\n id: string;\n /** Epoch ms of the measurement. */\n at: number;\n deviceId: string;\n deviceName: string;\n}\n\nexport interface BodyReading extends ReadingBase {\n kind: 'body';\n weightKg?: number;\n bodyFatPct?: number;\n leanMassKg?: number;\n bmi?: number;\n /**\n * Whole-body impedance in ohms, when the scale sends it. A scale that only\n * reports weight and impedance (RENPHO's common protocol) leaves body fat\n * to Yantra's own estimate; one that computes body fat itself (R-A017) also\n * fills bodyFatPct and bmi.\n */\n impedanceOhm?: number;\n}\n\nexport interface BloodPressureReading extends ReadingBase {\n kind: 'blood-pressure';\n systolic: number;\n diastolic: number;\n pulse?: number;\n /** What the monitor flagged (moved, loose cuff, irregular pulse), in words. */\n problems?: string[];\n}\n\nexport interface TemperatureReading extends ReadingBase {\n kind: 'temperature';\n celsius: number;\n}\n\nexport interface OxygenReading extends ReadingBase {\n kind: 'spo2';\n spo2: number;\n pulse?: number;\n}\n\nexport interface GlucoseReading extends ReadingBase {\n kind: 'glucose';\n mgdl: number;\n meal?: MealRelation;\n}\n\n/** A heart-rate strap worn for a session: the samples, summed up when it ended. */\nexport interface HeartRateSession extends ReadingBase {\n kind: 'heart-rate';\n /** `at` is the start. */\n end: number;\n /** run | walk | cycle | strength | hiit | other, as the person picked it. */\n workoutType: string;\n avgHr: number;\n maxHr: number;\n /** At most one sample every few seconds (see thinSamples); emptied once saved, see compactSession. */\n samples: TimedValue[];\n /** Minutes per heart-rate zone, kept when the samples are let go. */\n zoneMinutes?: number[];\n}\n\n/**\n * A finished exercise a device recorded itself: a jump-rope session or a\n * treadmill walk/run. Becomes one HealthWorkout in the day (readings with no\n * heart-rate samples, unlike HeartRateSession).\n */\nexport interface WorkoutReading extends ReadingBase {\n kind: 'workout';\n /** `at` is the start. */\n end: number;\n /** jump-rope | run | walk | ... lowercased. */\n workoutType: string;\n durationMin: number;\n calories?: number;\n distanceMeters?: number;\n /** Jump rope only: the number of skips. */\n jumps?: number;\n avgHr?: number;\n maxHr?: number;\n}\n\n/** A tape-measure girth reading of one body part. Only the waist has a home in the day. */\nexport interface BodyMeasurementReading extends ReadingBase {\n kind: 'body-measurement';\n /** waist | chest | hip | left_arm | ... as the tape reported it. */\n bodyPart: string;\n cm: number;\n}\n\nexport type DeviceReading =\n | BodyReading\n | BloodPressureReading\n | TemperatureReading\n | OxygenReading\n | GlucoseReading\n | HeartRateSession\n | WorkoutReading\n | BodyMeasurementReading;\n\nconst round1 = (n: number) => Math.round(n * 10) / 10;\n\n/** A stable id: the same measurement from the same device always gets the same one. */\nexport function readingId(deviceId: string, kind: DeviceReading['kind'], at: number, extra?: string | number): string {\n return [deviceId, kind, Math.round(at / 1000), extra].filter((p) => p !== undefined && p !== '').join(':');\n}\n\n/** Keep one heart-rate sample per `everyMs`: enough for zones, few enough to store and save. */\nexport function thinSamples(samples: readonly TimedValue[], everyMs = 5_000): TimedValue[] {\n const out: TimedValue[] = [];\n let last = -Infinity;\n for (const s of [...samples].sort((a, b) => a.t - b.t)) {\n if (s.t - last >= everyMs) {\n out.push(s);\n last = s.t;\n }\n }\n return out;\n}\n\nconst latestOf = <T extends DeviceReading>(list: readonly T[], has: (r: T) => boolean): T | undefined =>\n list.filter(has).reduce<T | undefined>((a, b) => (!a || b.at >= a.at ? b : a), undefined);\n\nexport interface DeviceDaysOptions {\n clock: ZoneClock;\n syncedAt: string;\n /** 208 - 0.7 x age, for heart-rate zones; undefined when the birth date is unknown. */\n maxHr?: number;\n /** Only these dates (the ones a new reading touched); every date when omitted. */\n dates?: ReadonlySet<string>;\n}\n\n/** Every reading -> one `device` HealthDay per local date, cleaned exactly as the backend cleans. */\nexport function deviceHealthDays(\n readings: readonly DeviceReading[],\n opts: DeviceDaysOptions,\n): { days: HealthDay[]; dropped: string[] } {\n const byDate = new Map<string, DeviceReading[]>();\n for (const r of readings) {\n const date = opts.clock.dateOf(r.at);\n if (opts.dates && !opts.dates.has(date)) continue;\n byDate.set(date, [...(byDate.get(date) ?? []), r]);\n }\n const dropped: string[] = [];\n const days: HealthDay[] = [];\n for (const [date, list] of [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n const day: HealthDay = { date, source: 'device' };\n const body = list.filter((r): r is BodyReading => r.kind === 'body');\n const weight = latestOf(body, (r) => r.weightKg !== undefined);\n if (weight) day.weightKg = weight.weightKg;\n const fat = latestOf(body, (r) => r.bodyFatPct !== undefined);\n if (fat) day.bodyFatPct = fat.bodyFatPct;\n const lean = latestOf(body, (r) => r.leanMassKg !== undefined);\n if (lean) day.leanMassKg = lean.leanMassKg;\n const bmi = latestOf(body, (r) => r.bmi !== undefined);\n if (bmi) day.bmi = bmi.bmi;\n const temp = latestOf(\n list.filter((r): r is TemperatureReading => r.kind === 'temperature'),\n () => true,\n );\n if (temp) day.bodyTempC = temp.celsius;\n const spo2 = latestOf(\n list.filter((r): r is OxygenReading => r.kind === 'spo2'),\n () => true,\n );\n if (spo2) day.spo2 = spo2.spo2;\n const waist = latestOf(\n list.filter((r): r is BodyMeasurementReading => r.kind === 'body-measurement' && r.bodyPart === 'waist'),\n () => true,\n );\n if (waist) day.waistCm = waist.cm;\n\n const readingsOut: HealthReading[] = [];\n for (const r of list) {\n if (r.kind === 'blood-pressure') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'blood-pressure',\n systolic: r.systolic,\n diastolic: r.diastolic,\n ...(r.pulse !== undefined ? { pulse: r.pulse } : {}),\n device: r.deviceName,\n });\n } else if (r.kind === 'glucose') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'glucose',\n glucoseMgDl: r.mgdl,\n ...(r.meal ? { meal: r.meal } : {}),\n device: r.deviceName,\n });\n }\n }\n if (readingsOut.length) day.readings = readingsOut.sort((a, b) => a.at.localeCompare(b.at));\n\n const workouts: HealthWorkout[] = list\n .filter((r): r is HeartRateSession => r.kind === 'heart-rate')\n .sort((a, b) => a.at - b.at)\n .map((s) => {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1((s.end - s.at) / 60_000),\n avgHr: s.avgHr,\n maxHr: s.maxHr,\n };\n if (opts.maxHr && s.samples.length) {\n w.zoneMinutes = zoneMinutes(s.samples, { start: s.at, end: s.end }, opts.maxHr);\n } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;\n return w;\n });\n // Device-recorded workouts (jump rope, treadmill): same HealthWorkout shape, no HR samples.\n for (const s of list.filter((r): r is WorkoutReading => r.kind === 'workout').sort((a, b) => a.at - b.at)) {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1(s.durationMin),\n ...(s.calories !== undefined ? { calories: s.calories } : {}),\n ...(s.distanceMeters !== undefined ? { distanceMeters: s.distanceMeters } : {}),\n ...(s.avgHr !== undefined ? { avgHr: s.avgHr } : {}),\n ...(s.maxHr !== undefined ? { maxHr: s.maxHr } : {}),\n };\n workouts.push(w);\n }\n if (workouts.length) day.workouts = workouts.sort((a, b) => a.start.localeCompare(b.start));\n\n const clean = sanitizeDay(day, dropped);\n if (hasData(clean)) days.push({ ...clean, syncedAt: opts.syncedAt });\n }\n return { days, dropped };\n}\n\n/** One line for a reading, as the person reads it (\"82.4 kg, body fat 23.1%\"). */\nexport function describeReading(r: DeviceReading, units: 'metric' | 'imperial' = 'metric'): string {\n const kg = (v: number) => (units === 'imperial' ? `${round1(v / 0.45359237)} lb` : `${round1(v)} kg`);\n switch (r.kind) {\n case 'body': {\n const parts: string[] = [];\n if (r.weightKg !== undefined) parts.push(kg(r.weightKg));\n if (r.bodyFatPct !== undefined) parts.push(`body fat ${round1(r.bodyFatPct)}%`);\n if (r.bmi !== undefined) parts.push(`BMI ${round1(r.bmi)}`);\n return parts.join(', ') || 'A measurement with no weight';\n }\n case 'blood-pressure':\n return `${Math.round(r.systolic)}/${Math.round(r.diastolic)} mmHg${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'temperature':\n return units === 'imperial' ? `${round1((r.celsius * 9) / 5 + 32)} °F` : `${round1(r.celsius)} °C`;\n case 'spo2':\n return `Blood oxygen ${Math.round(r.spo2)}%${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'glucose':\n return `${Math.round(r.mgdl)} mg/dL${r.meal ? `, ${r.meal.replace('-', ' ')}` : ''}`;\n case 'heart-rate': {\n const minutes = Math.round((r.end - r.at) / 60_000);\n return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;\n }\n case 'workout': {\n const parts: string[] = [`${Math.round(r.durationMin)} min`];\n if (r.jumps !== undefined) parts.unshift(`${r.jumps} jumps`);\n if (r.distanceMeters !== undefined) parts.push(`${round1(r.distanceMeters / 1000)} km`);\n if (r.calories !== undefined) parts.push(`${Math.round(r.calories)} kcal`);\n return parts.join(', ');\n }\n case 'body-measurement': {\n const part = r.bodyPart.replace(/_/g, ' ');\n return units === 'imperial' ? `${part} ${round1(r.cm / 2.54)} in` : `${part} ${round1(r.cm)} cm`;\n }\n default:\n return '';\n }\n}\n\n/**\n * A session's samples are only needed until they are uploaded and saved to\n * the health store; after that the zones are kept and the samples let go, so\n * the phone's store stays small.\n */\nexport function compactSession(s: HeartRateSession, maxHr: number | undefined): HeartRateSession {\n if (!s.samples.length) return s;\n const zones = maxHr ? zoneMinutes(s.samples, { start: s.at, end: s.end }, maxHr) : s.zoneMinutes;\n return { ...s, samples: [], ...(zones ? { zoneMinutes: zones } : {}) };\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {deviceKind}from'./catalog.js';import {describeBleError}from'./link.js';import {startDriver}from'./drivers.js';import {thinSamples,readingId}from'./readings.js';var __defProp = Object.defineProperty;
1
+ import {deviceKind}from'./catalog.js';import {describeBleError}from'./link.js';import {thinSamples,readingId}from'./readings.js';import'./brands/frames.js';import'./brands/uuids.js';import'./gatt/uuids.js';import {detectBrand}from'./brands/detect.js';import {startDriver}from'./drivers.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -117,7 +117,15 @@ class DeviceSession {
117
117
  });
118
118
  }
119
119
  }, SCAN_HINT_MS);
120
- this.stopScan = transport.scan(deviceKind(this.deps.kind).services, (found) => {
120
+ const kind = this.deps.kind;
121
+ this.stopScan = transport.scan({
122
+ services: deviceKind(kind).services,
123
+ // Brand devices do not advertise a standard service: recognise this kind of them.
124
+ match: (adv) => {
125
+ const brand = detectBrand(adv);
126
+ return brand && brand.kind === kind ? brand : void 0;
127
+ }
128
+ }, (found) => {
121
129
  const s = this.snapshot.state;
122
130
  if (s.step !== "scanning") return;
123
131
  if (this.deps.deviceId && found.id === this.deps.deviceId) {
@@ -163,11 +171,20 @@ class DeviceSession {
163
171
  const facts = {
164
172
  age: ageFrom(person == null ? void 0 : person.birthDate, this.now()),
165
173
  sex: person == null ? void 0 : person.sex,
166
- heightCm: person == null ? void 0 : person.heightCm
174
+ heightCm: person == null ? void 0 : person.heightCm,
175
+ birthDate: person == null ? void 0 : person.birthDate,
176
+ weightKg: paired == null ? void 0 : paired.lastWeightKg
167
177
  };
168
178
  this.driver = await startDriver(this.deps.kind, link, {
169
179
  now: () => this.now(),
170
180
  person: facts,
181
+ identity: found.protocol ? {
182
+ protocol: found.protocol,
183
+ mac: found.mac,
184
+ deviceType: found.deviceType,
185
+ model: found.model,
186
+ kind: found.kind
187
+ } : void 0,
171
188
  scaleUser: paired == null ? void 0 : paired.scaleUser,
172
189
  saveScaleUser: (user) => store.saveScaleUser(link.id, user),
173
190
  onReading: (r) => void this.keep([r]),
@@ -176,11 +193,13 @@ class DeviceSession {
176
193
  notices: [...this.snapshot.notices, m]
177
194
  })
178
195
  });
179
- await store.pair({
196
+ await store.pair(__spreadValues({
180
197
  id: link.id,
181
198
  name: link.name,
182
199
  kind: this.deps.kind
183
- });
200
+ }, found.protocol ? {
201
+ protocol: found.protocol
202
+ } : {}));
184
203
  this.set({
185
204
  state: {
186
205
  step: "connected",