Wearable / Device Signals (PADRE M46)¶
Metricis passively ingests wearable/device signals — heart rate, steps, active minutes, sleep, blood pressure, and HRV — from the participant's phone health store and turns them into patient-understandable derived metrics. This is the objective, continuous layer of the PADRE measurement system (alongside PROMs and the daily diary).
Nothing here is clinical-grade: wrist PPG is affected by motion, cold, and tremor, so every metric is a research estimate, poor-quality samples are excluded, and signal quality / wear-time travel with the data.
Data flow¶
Native health store Capacitor plugin Sync service Server
(HealthKit / Health Connect) → HealthBridge.query() → deviceSignalSync.ts → POST /submit/device-signal
(map + watermark) → DeviceSignalSession
+ DeviceSignalReading
+ derived metrics
- iOS reads Apple HealthKit.
- Android reads Health Connect — the on-device successor to Google Fit, which also surfaces data written by Google Fit, Fitbit, Samsung Health, etc. (Google Fit's Android APIs are deprecated and shutting down; Health Connect is the correct target.)
- Web has no health store: the plugin reports unavailable and passive sync no-ops. Manual entry is the web path.
Client pieces¶
| File | Role |
|---|---|
packages/capacitor-health-bridge/ |
Shared workspace Capacitor plugin (@metricis/capacitor-health-bridge, since M76; client adopted M77): TS contract + web fallback in src/, iOS Swift (ios/Sources — CAPBridgedPlugin, HealthKit) and Android Kotlin (android/src — Health Connect) natives. Discovered by cap sync in any consuming app (npm-package plugins register via packageClassList — no app-local registration code). |
client/src/services/health/mappers.ts |
Pure: native samples → DeviceSignalPayload, incremental sync windows, idempotency keys |
client/src/services/health/deviceSignalSync.ts |
Orchestrator: availability → authorization → per-signal query → submit, with per-signal watermarks |
The patient-portal has its own ports of the mappers/sync service
(patient-portal/src/services/health/) consuming the same plugin — see
Mobile and offline delivery.
Triggering a sync¶
syncDeviceSignals() is native-only, offline-aware, and idempotent (per-signal
watermark + server dedup on clientSubmissionId). Call it opportunistically —
e.g. on app resume or after an assessment — once the participant is known:
import { requestDeviceSignalPermissions, syncDeviceSignals } from './services/health';
await requestDeviceSignalPermissions(); // presents the platform permission sheet
const result = await syncDeviceSignals(participantCode);
// result.outcomes: per-signal { status: 'synced' | 'empty' | 'skipped' | 'error', sampleCount? }
It is intentionally not wired into the assessment timeline — when to sync is a product decision. On web / when offline / when unauthorized, it returns a skipped result rather than throwing.
Signal → native mapping¶
| Signal | HealthKit | Health Connect | Unit sent |
|---|---|---|---|
heart_rate |
HKQuantityType.heartRate |
HeartRateRecord |
count/min → bpm |
steps |
stepCount |
StepsRecord |
count |
active_minutes |
appleExerciseTime |
ExerciseSessionRecord (duration) |
min |
sleep |
sleepAnalysis (category) |
SleepSessionRecord (stages) |
min (+ context stage) |
blood_pressure |
systolic/diastolic quantities | BloodPressureRecord |
mmHg (+ context systolic/diastolic) |
hrv |
heartRateVariabilitySDNN |
HeartRateVariabilityRmssdRecord |
ms |
Native unit strings are passed through verbatim; the server-side source adapter
(device_signal_adapters.py) normalizes aliases to canonical units.
Manual native integration steps¶
The JS layer is verified by pnpm --filter @metricis/client run typecheck,
lint, and build. The native code must be compiled in Xcode / Android Studio
(not possible in a headless environment) and needs these one-time steps:
iOS¶
- In Xcode, select the App target → Signing & Capabilities → + Capability → HealthKit (adds the entitlement + provisioning profile capability). The
com.apple.developer.healthkitentitlement is already inApp.entitlements; the usage strings are inInfo.plist. - Ensure the provisioning profile / App ID has HealthKit enabled in the Apple Developer portal.
cd client && pnpm run build:iosthen run on a real device (the simulator has no Health data).- Localize
NSHealthShareUsageDescription(fr) viaInfoPlist.stringsbefore release.
Android¶
- The Kotlin toolchain +
androidx.health.connect:connect-clientpins live in the plugin package's ownpackages/capacitor-health-bridge/android/build.gradle(self-containedbuildscript; consuming apps need no Kotlin wiring). BumphealthConnectVersionthere to the latest stable at integration time and confirm the Kotlin version matches AGP 8.13. - On Android 13 and below, Health Connect is a separate app — the store must be installed for
isAvailable()to return true. On Android 14+ it is part of the system. - Complete Google Play's Health Connect / health data declaration and privacy-policy requirements before release (health permissions are restricted).
cd client && pnpm run build:androidthen run on a device/emulator with Health Connect data.
Privacy & consent¶
- Read-only: the plugin never writes to the health store.
- The server enforces the consent gate (
operation="device_data") on every upload and writes an audit entry with counts/quality only — never raw vitals. - Device model is stored as an accuracy covariate, not an identifier.