# 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.

HTML version: https://sean.lamet.dekrow.com/work/physdaq/

- Period: Jun–Aug 2026
- Role: Software and embedded systems research intern, BCIT MAKE+
- Status: Released
- Stack: Zephyr RTOS, C, BLE, Python, Electron, TypeScript
- Source: https://github.com/haksolot/PhysDAQ

## 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.

> Figure: A sensor node, exploded. Render from the repository.
>
> 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.

## 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.

> Figure: 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.

> Figure: A node's detail page, captured before the project was renamed from MAID.
>
> 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.

**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.
