Skip to content
Sean Lamet

WorkPhysDAQ

PhysDAQ

A wearable research instrument that records pulse and motion from several points on the body, from the sensor board to the CSV a researcher opens.

Context
Research
Role
Software and embedded systems research intern, BCIT MAKE+
Status
Released
Period
Jun–Aug 2026
Stack
Zephyr RTOS, C, BLE, Python, Electron, TypeScript

Problem

The research need was simple: put a few sensors on a person, record pulse and movement together, and get files that open in Excel, R or MATLAB, without wiring anything or writing code. Between the skin and that file sit a wearable board, a cable or a radio, several devices recorded as one session, and a format that must still mean the same thing weeks later. Each of those links has to hold for a whole session.

Exploded render of a PhysDAQ sensor node, top to bottom: the 3D-printed lid, a flat LiPo battery, the Seeed XIAO nRF52840 board with its USB-C connector, the small green MAX30102 pulse-oximeter module, and the base of the case with the round optical window that sits against the skin.
A sensor node, exploded. Render from the repository.

What I built

I built it with and for Dr. Mahsa Khalili, the researcher who needed it, during my research internship at BCIT MAKE+ in Burnaby. The repository shows I wrote the firmware for two devices in C on Zephyr, a Python bridge, an Electron desktop app, the offline analysis pipeline and the data contracts between them.

A node is a Seeed XIAO nRF52840 Sense, whose IMU gives motion, with a MAX30102 pulse sensor read at 100 Hz, on one LiPo cell. A hub carries two pulse sensors and a microSD card. Each device is its own USB or Bluetooth peripheral, and the app runs one bridge per device, so several record into one session.

PhysDAQ architecture Sensor devices run Zephyr 3.6 on a XIAO nRF52840 Sense: single-PPG nodes, each with a MAX30102 pulse sensor and the onboard IMU, and a hub with two MAX30102 behind an I2C mux and a microSD card. Every device sends the same ASCII sample line over USB at 100 Hz or over Bluetooth Low Energy at about 25 Hz. A Python bridge, one process per device, parses the lines, filters each PPG channel, fuses the IMU into an orientation and estimates heart rate, and passes JSON to the Electron desktop app, which shows the body map, live charts and 3D orientation and records one CSV per body position. The offline analysis pipeline cannot read those session CSVs yet; it reads the logger CSV written by a separate script that talks to a device directly. DEVICES Zephyr 3.6 · XIAO nRF52840 Sense · C NODE ×N MAX30102 · red + IR, 100 Hz, 18-bit onboard 6-axis IMU · LiPo wake-on-motion sleep HUB 2× MAX30102 behind an I2C mux onboard 6-axis IMU microSD session files · commands USB · every sample BLE · ~25 Hz same ASCII line BRIDGE Python · one process per device parse · per-channel filter · AHRS + ZUPT · FFT heart rate JSON · stdout commands · stdin DESKTOP APP Electron · React · three.js body map · live charts · 3D orientation · recording one file per body position logger CSV SESSION CSV 17 columns · host timestamps OFFLINE ANALYSIS beats · RMSSD · SpO2 · motion no converter yet
The path of a sample: everything multi-device happens on the host.

Where it stands

Released, with working hardware, firmware and software: installers for Windows, macOS and Linux, firmware flashed from the app, and documentation for whoever uses it next. It is a research instrument, not a medical device. Credit to BCIT MAKE+, which hosted the work.

Design
  • A bridge between hardware and UI. The app never touches a serial port or a radio. The bridge does the I/O and the live signal processing, and talks to the app in JSON over stdin and stdout. It ships packaged, so researchers never install Python.
  • Four formats as interfaces. The ASCII line the firmware sends, the commands the host sends back, the JSON the bridge hands the app and the CSV on disk. Change one and something downstream breaks, so they are documented in one place as data contracts.
  • Why Zephyr. A wearable that sleeps, streams over BLE, drives an I2C multiplexer (the hub’s two pulse modules share one fixed address) and writes a FAT file system needs all of that in one image, and Zephyr has it. I wrote what was missing, chiefly a register-level driver for the MAX30102. Version 3.6 is pinned in the workspace manifest.
  • Faults that report themselves. The firmware keeps a fault record across a reboot and sends it on the next connection, so a reset is never mistaken for a radio dropout.
Architecture
  1. The MAX30102 interrupts 100 times a second. The firmware drains its FIFO, pairs each sample with an IMU reading and formats one line: PPG red=… ir=… | IMU ax=… ay=… az=… gx=… gy=… gz=….
  2. Over USB every line is sent. Over BLE, one line every 40 ms, about 25 Hz: the link carries about 4 kB/s and the full stream needs about 12.
  3. The bridge filters each pulse channel, fuses the IMU into an orientation quaternion, estimates heart rate with an FFT over 8 seconds, and writes one JSON object per sample.
  4. The app plots it on a body map, live charts and a 3D orientation view, and records one 17-column CSV per body position, all sharing the session’s start time.
  5. Offline, a Python pipeline computes beats, heart-rate variability (RMSSD), a relative SpO2 estimate and motion cancellation, from the CSV of a separate logger script that records one device.
The PhysDAQ desktop app on a node's detail page, in a dark theme. On the left, three live charts: the filtered PPG waveform showing a regular pulse, the raw red and infrared signals in 18-bit ADC counts, and the gyroscope's angular velocity on three axes. On the right, a 3D view of the board's orientation, a heart-rate card reading 68 BPM, a wear-status card reading Worn, and the battery level at 86 percent. The header still reads MAID, the project's earlier name.
A node’s detail page, captured before the project was renamed from MAID.
Trade-offs
  • ASCII on the wire. Easy to read on a serial console and easy to extend, but its size is what limits BLE to about 25 Hz. Full-rate wireless capture would need binary packing, left until a study needs it.
  • No clock on the link. The session’s only time base is the host’s arrival time: fine for viewing nodes together, not for comparing waveform timing between body sites. Time synchronisation is unbuilt.
  • Sleep that does not cut a recording. A node enters nRF System Off, about 0.4 µA, only with no motion, no skin contact and no host, and wakes on movement. The hub never sleeps.
  • Two signal paths. The bridge’s processing is cheap and causal so the live view keeps up; the offline pipeline is zero-phase and is the reference.