<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Wearables | Soroush Dianaty, M.D.</title><link>https://soroushdianaty.com/tags/wearables/</link><atom:link href="https://soroushdianaty.com/tags/wearables/index.xml" rel="self" type="application/rss+xml"/><description>Wearables</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 23 Sep 2026 00:00:00 +0000</lastBuildDate><image><url>https://soroushdianaty.com/media/icon_hu_a589f346fc4c3e9d.png</url><title>Wearables</title><link>https://soroushdianaty.com/tags/wearables/</link></image><item><title>GaitScope: A Browser Lab for Movement-Sensor Signal Processing</title><link>https://soroushdianaty.com/projects/gaitscope/</link><pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate><guid>https://soroushdianaty.com/projects/gaitscope/</guid><description>&lt;p&gt;GaitScope is an interactive lab, in the browser, for learning how movement-sensor signals are processed. You record or load a walk from a phone&amp;rsquo;s accelerometer, then filter it, look at its spectrum, and put several step detectors on the same plot to see where and why they disagree. There is nothing to install, and files never leave the browser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;
&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="at-a-glance"&gt;At a glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Early development. Started 2026-09-23; version 0.1.0, no tagged release yet. Features still change from week to week.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maintainer: directs and reviews all work; code, tests and documentation largely written with Claude Code (Anthropic&amp;rsquo;s AI coding assistant) — see
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Origin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A class project in BME 598/494 &lt;em&gt;Wearable Devices for Sport, Health, and Wellness&lt;/em&gt; (ASU, Dr. Aurel Coza), Fall 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wearable sensing, digital signal processing, education&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT for now; a move to AGPL-3.0 is planned, pending the course instructor&amp;rsquo;s permission for the port of the course detector (
)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Live app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="why-it-exists"&gt;Why it exists&lt;/h2&gt;
&lt;p&gt;The course provides a MATLAB step-detection script, &lt;code&gt;LabStepDet_2025.m&lt;/code&gt;, and a sample recording. Students may skip MATLAB and use an AI tool to run the code instead. GaitScope started there, with a Python port of the script and a browser dashboard to run it on other recordings.&lt;/p&gt;
&lt;p&gt;Working through the script made a broader point visible. A step count depends on choices that are easy to miss: the sampling rate the code assumes, the threshold, which axis of the phone you analyse, where the phone sits on the body, and how ties and stray peaks are handled. GaitScope puts those choices on the screen, so a student can change one and see what happens to the steps.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="what-it-does"&gt;What it does&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Bring a walk.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upload a MATLAB &lt;code&gt;.mat&lt;/code&gt; file (v5 to v7.3), a Physics Toolbox Sensor Suite CSV export, a phyphox export zip, or any CSV with numbers in columns.&lt;/li&gt;
&lt;li&gt;Or record a walk on a phone with the browser&amp;rsquo;s motion sensors, with no app to install. The page asks how many steps you counted and where the phone was, and compares your count with the detectors.&lt;/li&gt;
&lt;li&gt;Reopen a previous analysis from a GaitScope JSON export.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Check the file.&lt;/strong&gt; Every file is checked against a documented
before analysis. The rule is to be strict where bad input would give wrong numbers with no visible sign, and lenient where a person looking at the plot can judge. Each problem comes with a concrete fix, such as the MATLAB line to re-save a file in a readable format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prepare the signal.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick x, y, z or the magnitude; when the recording includes gravity, also the vertical and horizontal acceleration.&lt;/li&gt;
&lt;li&gt;Resample to a chosen rate (linear or monotone cubic, with optional anti-aliasing).&lt;/li&gt;
&lt;li&gt;Filter: Butterworth, Bessel, Chebyshev I and II, elliptic, moving average, median, Savitzky–Golay, wavelet denoising and notch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Detect steps.&lt;/strong&gt; Detectors and envelopes work like indicators on a chart: any number on the plot at once, each with its own settings, colour and source signal (filtered or recorded), and its own metrics column.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Coza:&lt;/strong&gt; the course&amp;rsquo;s detector, ported exactly as written.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coza (modified):&lt;/strong&gt; a variant of the same detector. It counts tied peaks once, drops start and stop artefacts, uses a window in seconds and real timestamps, and reports cadence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Threshold peaks, Peak-to-valley and Zero-crossing:&lt;/strong&gt; three textbook detectors, each credited to its source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Envelopes and bands:&lt;/strong&gt; sliding window, peak-trough, dynamic threshold, mean ± k·SD, Hilbert envelope and percentile band. They are a view only and never change the steps.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Read the results.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt; per detector: steps, step duration, cadence, step-time variability, gait asymmetry, walking span and harmonic ratio. All detectors&amp;rsquo; metrics come from the same function, working only from step times, so the columns compare directly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frequency domain:&lt;/strong&gt; a whole-recording view (Welch&amp;rsquo;s spectrum, Lomb–Scargle, autocorrelation) and an over-time view (spectrogram, Morlet wavelet scalogram, wavelet bands, Hilbert–Huang spectrum). Walking shows as a peak, which gives a cadence without detecting any steps. That is an independent cross-check on the detectors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step intervals&lt;/strong&gt;, a &lt;strong&gt;steps table&lt;/strong&gt;, and &lt;strong&gt;notes&lt;/strong&gt; pinned to the plot or the spectrum (&amp;ldquo;turned around&amp;rdquo;, &amp;ldquo;counted twice&amp;rdquo;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Export&lt;/strong&gt; in the format each tool expects: CSV, a zip of CSVs, a MATLAB &lt;code&gt;.mat&lt;/code&gt; struct, NumPy &lt;code&gt;.npz&lt;/code&gt; (no pickle), or JSON, which reopens the whole analysis in GaitScope. Sample numbers are 1-based, as in MATLAB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python port.&lt;/strong&gt; &lt;code&gt;python/lab_step_det.py&lt;/code&gt; is a 1:1 translation of the course script. It reads the same file types and writes the same export layout.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="how-the-methods-are-checked"&gt;How the methods are checked&lt;/h2&gt;
&lt;p&gt;The repository separates three kinds of evidence and states the limit of each.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The course detector matches the original script.&lt;/strong&gt; On the course&amp;rsquo;s sample walk, the dashboard, the Python port and GNU Octave running &lt;code&gt;LabStepDet_2025.m&lt;/code&gt; give identical steps and metrics on all three axes.
&lt;em&gt;Limit:&lt;/em&gt; the course files are not in the repository, so this check runs only on a machine that has them, not in continuous integration. CI checks the same code on a synthetic walk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Each method agrees with an established library.&lt;/strong&gt; Test fixtures are generated by the reference implementation, and the tests compare against them on every pull request:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Tolerance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IIR filters (Butterworth, Bessel, Chebyshev I/II, elliptic)&lt;/td&gt;
&lt;td&gt;scipy&lt;/td&gt;
&lt;td&gt;1e-9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moving average, median, Savitzky–Golay, wavelet denoising, notch&lt;/td&gt;
&lt;td&gt;scipy, PyWavelets&lt;/td&gt;
&lt;td&gt;1e-9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resampling&lt;/td&gt;
&lt;td&gt;the Python port (numpy, scipy)&lt;/td&gt;
&lt;td&gt;linear bit for bit; cubic and anti-aliased 1e-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FFT, Welch, spectrogram&lt;/td&gt;
&lt;td&gt;numpy, scipy&lt;/td&gt;
&lt;td&gt;1e-12 (relative)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lomb–Scargle, autocorrelation&lt;/td&gt;
&lt;td&gt;scipy, numpy&lt;/td&gt;
&lt;td&gt;1e-12, 1e-13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Morlet wavelet transform, Daubechies-4 bands&lt;/td&gt;
&lt;td&gt;PyWavelets&lt;/td&gt;
&lt;td&gt;1e-10 (relative), 1e-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empirical mode decomposition&lt;/td&gt;
&lt;td&gt;PyEMD&lt;/td&gt;
&lt;td&gt;1e-9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every export format is read back with an independent reader (scipy, numpy, Python&amp;rsquo;s &lt;code&gt;zipfile&lt;/code&gt; and &lt;code&gt;csv&lt;/code&gt;, and Octave when installed).
&lt;em&gt;Limit:&lt;/em&gt; agreement is shown on test inputs. It is evidence that the code computes the intended method, not proof for every input.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. A few real walks with hand-counted steps.&lt;/strong&gt; On 2026-10-07 the maintainer recorded four walks with the dashboard and counted the steps: three on a Pixel 9a (10, 28 and 60 steps) and one on an iPad (24 steps). Inside the walk, Coza (modified), Threshold peaks, Peak-to-valley and Zero-crossing were within 2 of the count on the total acceleration and within 1 on the vertical. The unmodified Coza found 45 of 60 at the phone&amp;rsquo;s rate of about 60 Hz, because its window is counted in samples and assumes 100 Hz; resampled to 100 Hz, it found all 60.
&lt;em&gt;Limit:&lt;/em&gt; one person, two devices, a normal pace, two phone positions. That is too little to rank the detectors or to call any of them accurate.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="signal-pipeline"&gt;Signal pipeline&lt;/h2&gt;
&lt;div class="mermaid"&gt;
flowchart TD
A["Load or record&lt;br/&gt;.mat, Physics Toolbox CSV, phyphox zip,&lt;br/&gt;any CSV, phone recording, JSON export"] --&gt; B["Input-schema checks&lt;br/&gt;error / warn / info, each with a fix"]
B --&gt; C["Signal to analyse&lt;br/&gt;x, y, z, magnitude, vertical, horizontal"]
C --&gt; D["Resample (optional)&lt;br/&gt;linear or pchip, anti-aliasing"]
D --&gt; E["Filter (optional)"]
E --&gt; F["Step detectors&lt;br/&gt;Coza, Coza (modified), Threshold peaks,&lt;br/&gt;Peak-to-valley, Zero-crossing"]
D -. "recorded signal" .-&gt; F
E --&gt; G["Envelopes and bands&lt;br/&gt;view only"]
F --&gt; H["Metrics&lt;br/&gt;one function for every detector"]
E --&gt; I["Frequency domain&lt;br/&gt;spectral cadence, steps from the rhythm"]
I -. "cross-check" .-&gt; H
H --&gt; J["Export&lt;br/&gt;CSV, zip, .mat, .npz, JSON"]
G --&gt; J
&lt;/div&gt;
&lt;p&gt;The browser app is a static page with no build step: plain JavaScript, with Plotly for the charts. The computational core (&lt;code&gt;src/core.js&lt;/code&gt;) has no DOM access, so the same code runs under Node in the tests.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="status-and-roadmap"&gt;Status and roadmap&lt;/h2&gt;
&lt;p&gt;Development started on 2026-09-23. The app is live and usable, but there is no tagged release yet and behaviour still changes. The plan lives in the
. The main open threads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Licence and purpose:&lt;/strong&gt; move to AGPL-3.0 and settle the educational aim, after the instructor&amp;rsquo;s permission (
,
).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More counted walks:&lt;/strong&gt; a back pocket, slow and fast walks, other people, and an iPhone, which has not been tested yet (
,
).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A citable release:&lt;/strong&gt; tag v0.1.0 and archive it on Zenodo for a DOI (
). Until then, cite the software through its &lt;code&gt;CITATION.cff&lt;/code&gt;, with the version used.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An optional tutor:&lt;/strong&gt; research into what a language-model tutor running in the browser would cost, and whether small models are good enough (
,
).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins&lt;/strong&gt; for user-supplied detectors, envelopes and filters (
).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="what-it-is-and-what-it-isnt"&gt;What it is, and what it isn&amp;rsquo;t&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It is a teaching and exploration tool.&lt;/strong&gt; It is for students and instructors who want to see what a filter, a spectrum or a step detector does to a real signal, and why two detectors disagree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is not a validated gait-measurement instrument.&lt;/strong&gt; Step counts, cadence and the other metrics have been compared with hand-counted steps on a few walks by one person, nothing more. They have not been validated across people, devices or walking conditions, or against a reference system such as an instrumented walkway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is not for clinical use.&lt;/strong&gt; Do not use it to diagnose, monitor or make decisions about anyone&amp;rsquo;s health.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other known limits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Phone recording needs https and the screen on. Browsers stop sending motion data when the screen locks, so a recording ends there (
).&lt;/li&gt;
&lt;li&gt;Whether each peak is a step or a stride depends on the axis and where the phone is carried; how the page should decide is still under investigation (
).&lt;/li&gt;
&lt;li&gt;The Python port covers only the course detector; the other detectors and the harmonic ratio are not ported yet (
).&lt;/li&gt;
&lt;li&gt;The page loads its charting libraries from a CDN, so it needs an internet connection.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="use-in-teaching"&gt;Use in teaching&lt;/h2&gt;
&lt;p&gt;GaitScope is built for the questions a signal-processing or wearables lab tends to raise: what does this filter remove, why does the spectrum show a peak at half the step rate, why does the same detector find different steps at 60 Hz and 100 Hz. Each method shows its settings and credits its authors, with a DOI where one exists, on the page and in exports.&lt;/p&gt;
&lt;p&gt;Its use in a course has not been settled. How the educational side will work is still to be decided (
), and questions to the course instructor, including whether a Python version is acceptable for submissions, are open (
). Students taking BME 598/494 are asked to check the course&amp;rsquo;s rules before contributing, since a contribution may count as shared work on an assignment.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="credits"&gt;Credits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Coza&lt;/strong&gt; and the course&amp;rsquo;s sample walk come from BME 598/494 &lt;em&gt;Wearable Devices for Sport, Health, and Wellness&lt;/em&gt; (ASU, Dr. Aurel Coza). The course files are not in the repository; the detector is ported and credited to Dr. Coza.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coza (modified):&lt;/strong&gt; Soroush Dianaty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every other method&lt;/strong&gt; credits its authors on the page and in exports; the full list is under Credits in
.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who did what&lt;/strong&gt;, including the use of AI, is recorded in
.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="related-work-on-this-site"&gt;Related work on this site&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
: the research pillar on wearable sensor data. GaitScope is a teaching tool rather than part of that research, but it works with the same kind of signal.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>vitals-on-fhir: Consumer Wearables and Home Health Devices to HL7 FHIR R4</title><link>https://soroushdianaty.com/projects/vitals-on-fhir/</link><pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate><guid>https://soroushdianaty.com/projects/vitals-on-fhir/</guid><description>&lt;p&gt;Many consumer wearables and home health devices keep their data inside vendor apps, yet many of them also speak open Bluetooth health standards. vitals-on-fhir reads vital signs from those open channels and exposes them as HL7 FHIR R4 Observations that follow the US Core vital-signs profiles, so any FHIR-capable system can use them without a device-specific integration. It currently covers heart rate, blood pressure, oxygen saturation, body temperature, and body weight.&lt;/p&gt;
&lt;div class="not-prose flex justify-center my-6"&gt;
&lt;img src="featured.webp" alt="vitals-on-fhir logo" width="613" height="638" class="w-64 dark:hidden"&gt;
&lt;img src="logo-dark.webp" alt="vitals-on-fhir logo" width="613" height="638" class="w-64 hidden dark:block"&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;&lt;strong&gt;Not a medical device.&lt;/strong&gt; vitals-on-fhir is a research and educational proof-of-concept for healthcare interoperability. It is not for diagnosis, treatment, or any clinical decision-making. It is not HIPAA-compliant, keeps data in memory only, and is not designed for untrusted networks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="at-a-glance"&gt;At a glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Early development, version 0.1.0 (no tagged release yet). Heart rate is tested on one real device; the other four vital signs are tested without hardware.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maintainer and developer. Started as a BMI 540 team project at Arizona State University (team: Soroush Dianaty, MD; Nusrat Ashrafi, MS; Isha Uttham, BS; instructor: Sheetal Shetty, PhD). Developed spec-first, with Claude (via Claude Code) as author or co-author of most commits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Health interoperability, patient-generated health data, Bluetooth health devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GNU AGPL-3.0-or-later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="problem"&gt;Problem&lt;/h2&gt;
&lt;p&gt;Wearables and home devices such as blood-pressure cuffs, pulse oximeters, thermometers, and scales produce data that patients and caregivers might want in a health record or a research pipeline. Usually that data reaches other systems only through each vendor&amp;rsquo;s app and cloud. Building one integration per device does not scale.&lt;/p&gt;
&lt;p&gt;Many of these devices also support standard Bluetooth Low Energy health services defined by the Bluetooth SIG, such as the Heart Rate Service and the Blood Pressure Service. FHIR, US Core, LOINC, and UCUM already define how vital signs should look in a health record. What is missing is a small, inspectable piece that connects the two: it reads the open Bluetooth channel and produces FHIR that validates.&lt;/p&gt;
&lt;p&gt;The repository&amp;rsquo;s standards review also raises a second problem. Data that a patient collects at home should not look the same as vital signs recorded by a clinician, and simulated data should never be mistaken for real measurements.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="approach"&gt;Approach&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open channels only.&lt;/strong&gt; The project reads only standard Bluetooth health services and documented formats. Proprietary or reverse-engineered protocols are permanently out of scope. Every payload parser cites the Bluetooth SIG specification it implements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The bridge, not the app.&lt;/strong&gt; The core takes in readings and produces FHIR. The dashboard is a demonstration consumer of the FHIR API. Analytics and clinical decision support are out of scope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata-driven mapping.&lt;/strong&gt; Each vital-sign class declares its LOINC codes, UCUM unit, and US Core profile. Generic mappers build the Observation from that metadata, so adding a vital sign needs no new mapper code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Careful with device-reported context.&lt;/strong&gt; Measurement time, the thermometer&amp;rsquo;s body site, readings the device flags as unreliable, and which user of a shared cuff or scale took the reading are each either kept or acted on. Each choice is recorded in a decision record.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checked against the standard.&lt;/strong&gt; An Observation from each simulated adapter was last checked by hand with the HL7 FHIR validator (6.10.4) against US Core 9.0.0, with no errors. Adding this check to CI is an open issue.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;div class="mermaid"&gt;
flowchart LR
D["BLE device&lt;br/&gt;(standard health service)"] --&gt; A["Adapter + payload parser&lt;br/&gt;yields VitalSign objects"]
M["Mock adapter&lt;br/&gt;(simulated, labelled HTEST)"] --&gt; A
A --&gt; V["Validator chain&lt;br/&gt;range, sensor contact,&lt;br/&gt;device status, device user,&lt;br/&gt;duplicates"]
V --&gt; F["FHIR mapper&lt;br/&gt;VitalSign to US Core Observation"]
F --&gt; S{"Output sinks"}
S --&gt; ST[("In-memory&lt;br/&gt;Observation store")]
S --&gt; WS["Dashboard broadcaster&lt;br/&gt;(WebSocket)"]
ST --&gt; API["Read-only FHIR R4 API&lt;br/&gt;(bearer token)"]
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;An &lt;strong&gt;adapter&lt;/strong&gt; connects to one device and yields immutable vital-sign objects. Adapters do not import FHIR code. One shared Bluetooth connection helper handles scanning, subscribing, and reconnecting for every standard service.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;validator chain&lt;/strong&gt; accepts or rejects each reading. Rejections are logged without the measurement value.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;FHIR mapper&lt;/strong&gt; turns each accepted reading into an R4 Observation from the class metadata. A registry chooses the mapper by type, with one mapper for single-value vital signs and one for multi-component vital signs such as blood pressure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sinks&lt;/strong&gt; receive each Observation: an in-memory store behind the read-only API, and a broadcaster that pushes it to the dashboard.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every extension point is an abstract base class: vital-sign types, adapters, parsers, validators, mappers, stores, sinks, and authenticators. A third-party adapter can be selected by its import path without changing the repository. Tests that parse every module enforce the package boundaries, so adapters cannot import FHIR code, for example.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FHIR API (read-only).&lt;/strong&gt; &lt;code&gt;GET /fhir/metadata&lt;/code&gt;, &lt;code&gt;/fhir/Observation&lt;/code&gt; (search by &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;_sort=-date&lt;/code&gt;, &lt;code&gt;_count&lt;/code&gt;), &lt;code&gt;/fhir/Observation/{id}&lt;/code&gt;, &lt;code&gt;/fhir/Patient/{id}&lt;/code&gt;, and &lt;code&gt;/fhir/Device/{id}&lt;/code&gt;. Responses use &lt;code&gt;application/fhir+json&lt;/code&gt;, and searches return &lt;code&gt;searchset&lt;/code&gt; Bundles. Token and date search follow FHIR semantics, including date precision and the &lt;code&gt;eq&lt;/code&gt;/&lt;code&gt;ge&lt;/code&gt;/&lt;code&gt;gt&lt;/code&gt;/&lt;code&gt;le&lt;/code&gt;/&lt;code&gt;lt&lt;/code&gt; prefixes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dashboard.&lt;/strong&gt; A local web page shows safety notes and asks for the token on its start page. It then shows the live heart rate, the source device, the connection status with the reason when no device is connected, a scrolling heart-rate chart, and each reading as the FHIR Observation the server produced. With the mock adapter, it can also simulate heart-rate patterns such as sinus rhythm, atrial fibrillation, SVT, an exercise ramp, an off-wrist sensor, or a disconnect. These imitate heart &lt;em&gt;rate&lt;/em&gt; only, with no ECG.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="supported-vital-signs"&gt;Supported vital signs&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vital sign&lt;/th&gt;
&lt;th&gt;Bluetooth service&lt;/th&gt;
&lt;th&gt;Adapters (real / simulated)&lt;/th&gt;
&lt;th&gt;LOINC&lt;/th&gt;
&lt;th&gt;UCUM&lt;/th&gt;
&lt;th&gt;US Core 9 profile&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heart rate&lt;/td&gt;
&lt;td&gt;Heart Rate &lt;code&gt;0x180D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;miband10&lt;/code&gt; / &lt;code&gt;mock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8867-4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/min&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heart Rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blood pressure&lt;/td&gt;
&lt;td&gt;Blood Pressure &lt;code&gt;0x1810&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bp&lt;/code&gt; / &lt;code&gt;mock-bp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;85354-9&lt;/code&gt; panel; &lt;code&gt;8480-6&lt;/code&gt; systolic, &lt;code&gt;8462-4&lt;/code&gt; diastolic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mm[Hg]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blood Pressure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oxygen saturation&lt;/td&gt;
&lt;td&gt;Pulse Oximeter &lt;code&gt;0x1822&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spo2&lt;/code&gt; / &lt;code&gt;mock-spo2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;59408-5&lt;/code&gt; and &lt;code&gt;2708-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pulse Oximetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body temperature&lt;/td&gt;
&lt;td&gt;Health Thermometer &lt;code&gt;0x1809&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;temp&lt;/code&gt; / &lt;code&gt;mock-temp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8310-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Body Temperature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body weight&lt;/td&gt;
&lt;td&gt;Weight Scale &lt;code&gt;0x181D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weight&lt;/code&gt; / &lt;code&gt;mock-weight&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;29463-7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Body Weight&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hardware status.&lt;/strong&gt; Heart rate is tested on a Xiaomi Smart Band 10. The cuff, oximeter, thermometer, and scale adapters follow the Bluetooth specifications and pass hardware-free tests, but none has been tried with a specific device yet.&lt;/li&gt;
&lt;li&gt;Units are normalized: kPa to mmHg, °F to °C, and pounds to kilograms. Mean arterial pressure is not modelled because the device calculates it rather than measuring it directly.&lt;/li&gt;
&lt;li&gt;For oxygen saturation, only the continuous-measurement stream is read. Spot-check measurements are deferred.&lt;/li&gt;
&lt;li&gt;The default range limits only reject values a living body cannot produce, such as 20–250 bpm or 10–47 °C. They are not clinical thresholds.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="design-decisions"&gt;Design decisions&lt;/h2&gt;
&lt;p&gt;The project records its decisions as ADRs (architecture decision records) and records findings in briefs before deciding.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ADR-0001: Open phase 2 (home health devices).&lt;/strong&gt; The four new vital signs were delivered one spec at a time, with blood pressure first because it was the first multi-component vital sign. Components are typed class metadata, so the domain model stays FHIR-free. The Bluetooth lifecycle is shared through composition, not another level of inheritance. SMART on FHIR and durable storage were deliberately deferred because the project has no remote or multi-user deployment to protect yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADR-0002: Mark patient-generated and simulated data.&lt;/strong&gt; Every Observation sets &lt;code&gt;performer&lt;/code&gt; to the Patient and adds the HL7 Personal Health Device IG&amp;rsquo;s &lt;code&gt;phd&lt;/code&gt; category next to &lt;code&gt;vital-signs&lt;/code&gt;. Simulated readings and their Device carry the &lt;code&gt;HTEST&lt;/code&gt; security label. Each candidate marker was run through the HL7 validator first. The proposed US Core &lt;code&gt;patient-supplied&lt;/code&gt; tag was not used because it produced validation errors against the published code system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADR-0004: Shared cuffs and scales.&lt;/strong&gt; Multi-user devices tag each reading with a user ID. The service records only the configured user&amp;rsquo;s readings, and by default it rejects every reading from a multi-user device until that user is set. The ID is compared and then discarded. It is never logged, stored, or put into FHIR.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADR-0005: Honour device-reported status.&lt;/strong&gt; Blood-pressure and oximeter readings that the device marks as invalid, questionable, or not final are rejected rather than published as &lt;code&gt;final&lt;/code&gt;. A cuff&amp;rsquo;s &amp;ldquo;irregular pulse&amp;rdquo; flag is accepted because it is a clinical finding, not a faulty reading. Rejecting it would drop every reading from a person with atrial fibrillation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ADR-0006: Temperature measurement site.&lt;/strong&gt; The thermometer&amp;rsquo;s reported site goes into &lt;code&gt;Observation.bodySite&lt;/code&gt; as a SNOMED CT concept. The site is never guessed: &amp;ldquo;Body (general)&amp;rdquo; or a missing field gives no &lt;code&gt;bodySite&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brief: validation modes.&lt;/strong&gt; If personal or clinical thresholds are added later, they should flag readings, for example through &lt;code&gt;interpretation&lt;/code&gt;, and never drop them. An abnormal but real reading is the one a caregiver most needs to see.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ADR-0003 is reserved for a planned session capture and review feature, which is currently a brief. A standards-alignment brief compares the project with published guidance on bringing patient-generated data into EHRs (Dinh-Le et al., 2019; the IFCC C-MHBLM recommendations, 2024), the HL7 Personal Health Records IG, and SMART App Launch. The gaps it found became tracked issues, and most of them led to the ADRs above.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="getting-started"&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Requirements: Python 3.12+,
, and a Bluetooth LE adapter only if you use a real device.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/soroushdty/vitals-on-fhir.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; vitals-on-fhir
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv sync
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv run vitals-on-fhir &lt;span class="c1"&gt;# demo mode: simulated heart rate, no token needed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then open &lt;code&gt;http://127.0.0.1:8000/&lt;/code&gt;. Other simulated vital signs use &lt;code&gt;--adapter mock-bp&lt;/code&gt;, &lt;code&gt;mock-spo2&lt;/code&gt;, &lt;code&gt;mock-temp&lt;/code&gt;, or &lt;code&gt;mock-weight&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A real device always requires a token. Set &lt;code&gt;VOF_API_TOKEN&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; (copy it from &lt;code&gt;.env.example&lt;/code&gt;), enable heart-rate broadcast on the band, and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv run vitals-on-fhir --adapter miband10
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To query the API:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$VOF_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;http://localhost:8000/fhir/Observation?code=http://loinc.org|8867-4&amp;amp;_sort=-date&amp;amp;_count=10&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Development checks: &lt;code&gt;uv run pytest&lt;/code&gt; (hardware-free suite), &lt;code&gt;uv run pytest -m hardware&lt;/code&gt; (needs a real band), &lt;code&gt;uv run ruff check .&lt;/code&gt;, and &lt;code&gt;uv run mypy src&lt;/code&gt;. CI runs lint, a format check, strict type checks, and the hardware-free tests on every pull request.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="status-and-roadmap"&gt;Status and roadmap&lt;/h2&gt;
&lt;p&gt;The project is young. The first commit is from 21 September 2026, and the most recent changes are from 8 October 2026.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Heart rate from wearables:&lt;/strong&gt; delivered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Home health devices&lt;/strong&gt; over standard Bluetooth profiles: in progress. All four vital signs (blood pressure, SpO2, temperature, weight) have shipped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phone health aggregators&lt;/strong&gt; (Android Health Connect, Apple HealthKit): planned. This would be the route for devices with proprietary Bluetooth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistence and outbound integration&lt;/strong&gt; (durable storage, pushing to external FHIR servers, SMART on FHIR): planned. TLS, audit logging, and encryption at rest are listed as prerequisites.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Additional adapters&lt;/strong&gt; as separately licensed packages: planned.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each later phase is opened by its own decision record.&lt;/p&gt;
&lt;p&gt;The project plans a preprint. The open work toward it includes running the HL7 validator in CI, measuring from git history what each new vital sign took to add, measuring device-to-Observation latency, documenting the development method (including AI assistance), and tagging a citable v0.1.0 release. None of these measurements exist yet.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="limitations-and-safety"&gt;Limitations and safety&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Not a medical device.&lt;/strong&gt; Research and education only. Not for diagnosis, treatment, or clinical decisions. The simulated heart-rate scenarios are not a diagnosis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not for real patient data in production.&lt;/strong&gt; It is not HIPAA-compliant and has no TLS, audit log, or encryption at rest. It binds to &lt;code&gt;127.0.0.1&lt;/code&gt; by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bluetooth is unauthenticated.&lt;/strong&gt; While heart-rate broadcast is on, any nearby device can read the stream. Turn broadcast off when you are not using it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited hardware testing.&lt;/strong&gt; Only heart rate has been tested on a real device, the Xiaomi Smart Band 10. The other four adapters have not yet been tried with real devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard-channel limits.&lt;/strong&gt; The service receives live data only, with no stored history. Wearables send device-computed beats per minute, not raw PPG. Some devices need a workout mode for broadcast. Apple Watch, Oura, and most Wear OS watches have no open real-time channel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope.&lt;/strong&gt; The service reads one device at a time, for one local patient, and keeps data in memory only, so data is lost on restart.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="related-work-on-this-site"&gt;Related work on this site&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
: this project handles the interoperability side of consumer wearable data, getting device readings into a standard record format. It does not process signals and works with device-computed values, not waveforms.&lt;/li&gt;
&lt;li&gt;
: the same FHIR, LOINC, and SNOMED CT foundations, applied here to patient-generated vital signs.&lt;/li&gt;
&lt;li&gt;
and
: both rely on FHIR &lt;code&gt;meta.security&lt;/code&gt; labels. vitals-on-fhir uses the same mechanism to label simulated data as test data (&lt;code&gt;HTEST&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
: a related educational tool for consumer sensor data. It covers movement-sensor signal processing in the browser.&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>