Autonomous wireless physiological monitoring system, built during a hackathon.
The core idea: patients wear small wireless sensor nodes that continuously collect biometrics and beam them back to a bedside station over an encrypted mesh. That station aggregates everything and pushes it up to a cloud backend, which a web dashboard talks to. Think hospital vitals monitoring, but untethered.
We designed Pulsar around four values — safety, reliability, mobility, and comfort — which drove most of our architectural decisions.
Okami (Blazor Web App — server + WASM client)
↕ HTTP · SignalR / WebSockets (Lightspark)
Thunderlink (.NET 8 REST API + PostgreSQL + SQLite)
↕ MQTT 5.0 / TLS
Station-Main — ESP32-S3 gateway
↕ ESP-NOW (encrypted)
Station-Sensor — ESP32-C6 sensor node
↕ I2C
Capillary biometric sensor (HR + SpO2)
The sensor nodes talk to the gateway over ESP-NOW — a connectionless 802.11 protocol with built-in encryption. The gateway validates incoming packets with CRC16, tracks up to 10 concurrent sensor nodes, and relays readings up the stack via MQTT. Thunderlink sits in the middle managing all the data, and Okami is what clinical staff would see.
Station-Aux is a third ESP32 that drives a bedside LCD display.
Written in C with ESP-IDF. The sensor node reads HR and SpO2 from a capillary I2C sensor every 4 seconds, packs the measurements into a CRC16-validated frame, and fires it off over ESP-NOW. The gateway receives from all nearby nodes, manages their session state, bridges data to a coprocessor over SPI, and publishes up to the cloud via MQTT 5.0 over TLS. Station-Aux handles a local bedside display.
ASP.NET Core Minimal API on .NET 8. Two databases: PostgreSQL (via Npgsql + EF Core) for the main patient and sensor records, and a local SQLite database for configuration. Patient records include room/wing assignment, severity level, and a linked sensor.
Patient creation goes through Garmr, a validation layer that enforces medical data constraints before anything touches the database — age range (0–122), gender, severity scale (1–5) — and generates IDs via Aurora (covered in its own section below).
The endpoint surface:
GET/POST/PATCH/DELETE /data/records patient records
GET/POST/PATCH/DELETE /data/sensors
GET/POST/PATCH/DELETE /data/stations
GET/POST/PATCH/DELETE /data/config
GET /status
Real-time updates are handled by Lightspark, a SignalR hub (Photon) mounted at /photon. It receives incoming readings forwarded from the MQTT broker and broadcasts them out to all connected dashboard clients — one channel each for sensors (SensorGateway), patients (PatientGateway), and stations (StationGateway). This is what closes the loop between the firmware and the live dashboard.
Blazor Web App on .NET 8, split into a server host (Okami) and a WASM client (Okami.Client). Styled with TailwindCSS v4. Patient list, sensor view, and station view, with search and filtering handled by the Surfer service. The Lightspark client service manages the SignalR connection and fires events into the UI as readings come in live.
C and ESP-IDF on the embedded side. .NET 8 (ASP.NET Core Minimal APIs + Blazor Web App) across the backend and frontend. PostgreSQL for patient/sensor data, SQLite for local config. ESP-NOW for the wireless mesh, MQTT 5.0 over TLS for the cloud hop, SignalR over WebSockets for the live dashboard feed, CRC16 for packet integrity throughout.
Aurora is a custom ID generation scheme we built into Garmr. The short version: time-ordered, lexicographically sortable, URL-safe, Crockford Base32-encoded IDs with a rolling epoch and a configurable nonlinear time distribution baked in.
Aurora has two modes depending on the required precision:
| Second precision | Millisecond precision | |
|---|---|---|
| Timestamp | 5 bytes (40 bits) | 7 bytes (56 bits allocated, 40 active) |
| CSPRNG entropy | 4 bytes (32 bits) | 8 bytes (64 bits) |
| Total | 9 bytes / 72 bits | 15 bytes / 120 bits |
| Encoded length | 15 chars + prefix | 25 chars + prefix |
| Epoch lifespan | ~34,865 years | ~34.8 years |
Encoded with Crockford Base32 (5 bits per character, no I/L/O/U), which makes every ID URL-safe with no escaping needed. Because the timestamp is big-endian and leads the ID, lexicographic sort order equals time order — you get chronological sorting for free on any index or log.
The epoch is a custom starting point stored in the SQLite config database, not Unix time. All timestamps are measured as seconds or milliseconds since that epoch. The 40-bit timestamp space has a hard ceiling at 0xFFFFFFFFFF — when the counter approaches it, Aurora detects the overflow and automatically rolls the epoch forward, pulling the new value from the config endpoint and writing it back to SQLite. The ID space never saturates, and the system self-heals without downtime.
Before the timestamp is encoded, it goes through Gnaw — a nonlinear mapping that compresses the raw value using a blend of two curves:
- Piecewise linear interpolation over a set of configurable control points (
Hadrons) - A closed-form formula:
11√t + 7t + arcsinh(t/161051) + ln(t)
The blend weight shifts dynamically as time progresses (heavier formula influence early, heavier interpolation influence late), so the distribution of timestamp values in the ID space is deliberately non-uniform.
The Hadrons control points are loaded from the config database at startup and define the shape of the interpolation curve used in Gnaw. Because the curve is configurable, two systems with different Hadrons produce structurally incompatible ID spaces — even with the same epoch and the same timestamp, the encoded values diverge. This makes Hadrons act as a kind of mathematical fingerprint or key: you can tell IDs apart by origin, and you get a degree of namespace isolation between deployments without any explicit partitioning scheme.
This project is archived and in hiatus — built during a hackathon and never publicly released. One day, we'll get back to it!