Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Smart Wellness Ring Firmware Starter

Low-level C firmware skeleton for a smart wellness ring with secure BLE.

Assumptions

  • ARM Cortex-M BLE SoC
  • RTOS optional; this skeleton uses a simple super-loop
  • BLE stack is vendor-provided and integrated through ble_adapter.*
  • Sensors: optical HR/SpO2, accelerometer, skin temperature, battery fuel gauge

Security goals

  • BLE LE Secure Connections only
  • Bonding enabled
  • MITM protection enabled where possible
  • Reject legacy pairing
  • Encrypt authenticated characteristics
  • Store bonds in secure flash/NVM
  • Application-level message counters for replay resistance

Build

This is a portable skeleton, not tied to a vendor SDK. Add your vendor SDK include paths and implement platform_hal.* + ble_adapter.* using your chip's BLE stack.

mkdir build && cd build
cmake ..
make
ctest        # runs host-mode unit tests

If you don't have CMake, you can also build directly:

gcc -std=c99 -Wall -Wextra -Iinclude -Ible -o wellness_ring_fw \
    src/main.c src/sensors.c src/wellness_model.c src/platform_hal_stub.c ble/ble_adapter.c \
    src/activity_model.c src/sleep_model.c src/recovery_model.c src/temperature_model.c \
    src/cycle_model.c src/sensors_extended.c

Expanded metrics (this pass)

Everything below is new: activity, sleep, recovery, temperature-trend, and (opt-in) cycle tracking, plus honest stubs for two things this hardware genuinely can't do. Safety features (fall detection, SOS) were explicitly excluded from this pass by product decision - see the full metrics discussion in chat history for the reasoning on what a smart ring can/should track.

All of this lives in include/extended_metrics.h plus one new .c file per category (activity_model.c, sleep_model.c, recovery_model.c, temperature_model.c, cycle_model.c, sensors_extended.c), wired into main.c's loop alongside the existing HR/SpO2/step/HRV pipeline. None of these new metrics are exposed over BLE yet - ble_adapter_send_measurement() only carries the original wellness_sample_t. Adding GATT characteristics for the new data is a follow-up task, not done here.

Activity (activity_model.c) - distance, calories, activity-type recognition (sedentary/walking/running/cycling via step cadence + accelerometer motion variance), standing/inactive time, floors climbed (barometer-derived). Distance and calorie formulas are rough population-average estimates (fixed stride length, ~24 kcal/kg/day BMR approximation), not clinically validated - tune against real reference data before shipping. "Standing time" is a best-effort label for "not currently walking/running/cycling" - a finger-worn ring's accelerometer fundamentally cannot distinguish sitting from standing the way a hip or thigh-worn sensor could.

  • Fixed a real precision bug during development: the original per-call (step_delta * stride) / 100 integer division truncated to exactly 0 for the realistic case of one step per tick, so distance and active calories would never have accumulated at all. Now uses remainder-preserving accumulators (same technique as the existing resting-calorie counter), verified in tests/test_activity_model.c.

Sleep (sleep_model.c) - onset/wake detection (20 min quiet → asleep, 5 min sustained motion → awake), sleep stage cycling, efficiency, restlessness count, apnea indicator (repeated SpO2 dips below 90%). The stage cycling (light/deep/awake/REM) is a simplified fixed-pattern stub based on elapsed time within a nominal 90-minute cycle, not EEG-validated staging - real sleep staging needs more signals (and more sophisticated modeling) than a ring's accelerometer + PPG can provide alone. Treat stages as illustrative, not diagnostic.

Recovery (recovery_model.c) - a 0-100 readiness score blending HRV (40%), sleep efficiency (40%), and resting HR vs. a slow-moving personal baseline (20%), plus guided-breathing session tracking (start/tick/end, with average stress score during the session). These weights are a reasonable starting point, not a clinically validated formula - same caveat as the stress-score calibration.

Temperature (temperature_model.c) - tracks a slow-moving (EMA) personal baseline from one reading per day and reports deviation from it; needs 3 days of history before it's considered meaningful.

Cycle tracking (cycle_model.c) - opt-in only (cycle_model_init(false) by default in main.c - this infers sensitive personal health data and doesn't apply to every user). Uses the well-established post-ovulatory basal-temperature rise to estimate cycle day, predicted next period, and an ovulation window. This is a simple threshold heuristic on top of a real physiological signal, not a medical- or contraceptive-grade algorithm - it doesn't use period start dates, cervical mucus, LH tests, or the other signals real fertility-tracking methods combine. Self-corrects after the first full cycle is observed but should be presented to users as a rough estimate.

Body composition & respiration, device health, environmental (sensors_extended.c) - respiratory rate and device firmware/charging status are simple stub reads, same category as the existing HR/SpO2 stubs. Body fat percentage always reports 0 (unavailable): it needs bioimpedance electrodes this ring's sensor set doesn't have. Ambient light, altitude, and UV index are placeholder reads pending real photodiode/barometer/UV-sensor hardware.

Honest hardware-limited stubs - sensors_ecg_supported() always returns false and sensors_read_ecg_rhythm()/sensors_read_blood_pressure_trend() always return "unsupported" (RING_ERR_INVALID), on purpose. ECG needs multi-point electrode contact (e.g. a finger from each hand) a single-contact ring can't provide; PPG-only blood-pressure trend estimation needs either a pulse-transit-time reference (a second sensor site) or per-user cuff calibration this device has no way to collect. Returning a fabricated reading for either would be worse than clearly reporting the capability doesn't exist on this hardware.

All of the above have unit tests in tests/ (test_activity_model.c, test_sleep_model.c, test_recovery_model.c, test_cycle_model.c, test_temperature_model.c) with reference values computed independently (in Python, outside this codebase) rather than derived from the C implementation itself, so the tests actually check the math.

Implemented and tested (host/simulator build):

  • HRV-based stress tracking (wellness_model.c): ingests beat-to-beat (RR) intervals as they arrive (via the new sensors_read_rr_interval()) and computes RMSSD (root mean square of successive differences), the standard short-term HRV metric, over a rolling window of the most recent ~20 beats. Lower HRV maps to a higher stress_score (0-100). Exposed as three new fields on wellness_sample_t: hrv_rmssd_ms, stress_score, and hrv_valid (false until enough beats have been collected - stress scores need a handful of beats to mean anything, not a single reading). Tested in tests/test_stress_model.c against independently hand-computed RMSSD reference values for both a low-variability ("stressed") and high-variability ("relaxed") RR sequence, plus edge cases (not-enough-beats-yet, a zero/invalid RR interval being ignored rather than corrupting the running calculation).
    • The RMSSD-to-stress-score calibration (15ms=100, 100ms=0) is a reasonable default based on typical adult resting HRV ranges, not a clinical calibration. Real deployments should tune STRESS_RMSSD_LOW_MS/STRESS_RMSSD_HIGH_MS against ECG-validated reference data for the target population before presenting a stress number to users as authoritative.
    • sensors_read_rr_interval() in sensors.c is, like the other sensor reads, a stub: it synthesizes a plausible ~72bpm beat signal with small jitter so the model has something realistic to run against on host builds. Real hardware gets RR intervals from the optical HR sensor's own PPG beat-detection algorithm (the same one that already derives heart_rate_bpm) - this needs to be replaced with actual beat timestamps from your sensor driver.
  • Fixed four stale #includes left over from a header consolidation (several separate headers were merged into include/ring.h, but ble_adapter.h, ble_adapter.c, sensors.c, and tests/test_wellness_model.c still referenced the old, now-nonexistent filenames - ring_types.h, ring_config.h, platform_hal.h, sensors.h, wellness_model.h - which meant nothing actually compiled).
  • Debounced, hysteresis + EMA-smoothed step detection (wellness_model.c), with a unit test in tests/test_wellness_model.c that simulates walking and a noise burst.
  • Application-level replay protection on BLE control commands (main.c): each command must carry a strictly increasing 32-bit counter; stale/replayed counters are rejected. This was listed as a security goal in the original skeleton but not implemented.
  • A working "sync time" control command that actually sets an RTC (platform_rtc_set/now), instead of being a no-op stub.
  • include/health_types.h (previously health_types_uploaded.h, which contained an empty enum {} - invalid C that didn't compile, and wasn't referenced anywhere). It's now a valid, optional extension point for occasional health data points (sleep episodes, blood pressure) alongside the always-on wellness_sample_t.
  • Removed a dead/misleading step counter in sensors.c that was always 0 and silently overwritten by the wellness model.
  • Battery percentage is clamped defensively to 0-100.

Still stubs that need real hardware/vendor SDK before shipping:

  • platform_rng_get() in platform_hal_stub.c returns a fixed, predictable byte pattern. This is fine for host builds but must be replaced with the SoC's hardware TRNG before use - it currently feeds BLE LE Secure Connections key generation, so shipping it as-is would break the security model entirely.
  • HR/SpO2/skin-temp/RR-interval values in sensors.c are hardcoded or synthetic placeholders; real optical PPG and thermistor driver code and signal-processing algorithms (e.g. FFT-based HR extraction, PPG beat detection, ambient-light rejection) are chip/sensor-specific and need to be written against your actual sensor's datasheet.
  • ble_adapter.c logs intended behavior instead of calling a real BLE stack; every function needs to be rewritten against your vendor's SDK (Nordic SoftDevice, ESP-IDF NimBLE, etc.).
  • Bond/key storage (platform_secure_store_*) is a no-op; needs real encrypted flash/NVM backing.
  • The stress-score calibration constants need validation against real HRV reference data (see above) before being presented to users as more than an experimental/relative indicator.
  • Body fat % has no bioimpedance hardware to read from (sensors_read_body_composition() always reports 0/unavailable); ambient light, altitude, and UV index in sensors_read_environmental() are placeholder values pending real sensor hardware.
  • ECG and blood-pressure-trend are deliberately unsupported (see "Honest hardware-limited stubs" above) - this isn't a TODO to fill in with the current sensor set, it needs different/additional hardware.
  • None of the new activity/sleep/recovery/temperature/cycle data is exposed over BLE yet - needs new GATT characteristic design.

Components

  • SoC nRF54L15
  • Antenna Johanson 2450AT07A0100
  • HF crystal32MHz, load capacitance CL = 6–9pF
  • LF clockInternal LFRC (±250ppm after periodic HFXO calibration) add an external 32.768kHz LFXO
  • RF matching networkPi (shunt-series-shunt) footprint, values TBD per device
  • NFCUse the nRF54L15's built-in NFC-A tag hardware
  • PMIC/chargernPM1304 (not nPM1300)
  • BatterySmall curved rechargeable Li-poly cell (~15–30mAh), charged via pogo-pin contacts
  • PPG AFE (HR/SpO2/HRV)MAX86141 (dual-channel optical AFE)
  • AccelerometerBosch BMA400
  • Skin temperatureNTC thermistor + the nRF54L15's built-in 14-bit SAADC
  • Barometer (floors climbed)Bosch BMP390 or similar

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages