Tremorix is an end-to-end assistive technology system for people living with hand tremors (e.g. Parkinson's disease). It pairs a custom-built hardware spoon with a web dashboard that tracks stabilisation performance, surfaces clinical metrics, and automatically adapts the device's compensation algorithm based on the user's tremor history.
- System Overview
- Hardware
- Project Structure
- Architecture
- Getting Started
- How It Works
- Web Application
- API Reference
- Database Schema
- Tech Stack
┌─────────────────┐ BLE ┌──────────────────┐ WebSocket ┌─────────────────┐
│ Tremor Spoon │ ──────────► │ ble_bridge.py │ ────────────► │ server.js │
│ (ESP32 + IMU) │ ◄────────── │ (Python bridge) │ ◄──────────── │ (Analytics WS) │
└─────────────────┘ mode cmds └──────────────────┘ set_mode └────────┬────────┘
│ REST
┌────────▼────────┐
┌────┤ server/ │
│ │ (App API :5000)│
│ └─────────────────┘
│
┌──────▼──────┐
│ React App │
│ (Vite :5173)│
└─────────────┘
The device continuously measures pitch and roll at 100 Hz and actively counteracts tremor using two servo motors. Sensor readings are streamed over BLE to a Python bridge, which relays them to the analytics server. Every 60 seconds the server analyses the user's 7-day tremor history, determines the optimal stabilisation mode, and sends the mode command back to the device.
| Component | Spec |
|---|---|
| Microcontroller | ESP32 WROOM-32 |
| IMU | MPU-6050 (I²C, SDA GPIO 21, SCL GPIO 22) |
| Pitch servo | MG90S Micro Servo on GPIO 19 |
| Roll servo | MG90S Micro Servo on GPIO 18 |
| Firmware loop rate | 100 Hz |
| BLE TX rate | 10 Hz (every 10 loops) |
| Servo range | 20°–160° (centre 90°) |
Tremorix/
├── tremor_spoon.ino # ESP32 Arduino firmware
├── ble_bridge.py # Python BLE ↔ WebSocket bridge
├── server.js # Analytics WebSocket + HTTP server (port 3000)
├── client/ # React + Vite frontend
│ ├── src/
│ │ ├── App.jsx # Router and route definitions
│ │ └── pages/
│ │ ├── ProfileSelect.jsx # Landing / onboarding page
│ │ ├── Login.jsx # Login with role selection
│ │ ├── PatientList.jsx # Doctor: list of patients
│ │ ├── Dashboard.jsx # Waveform chart + metrics
│ │ ├── Exercises.jsx # Gentle hand exercise program
│ │ ├── Messages.jsx # Doctor notes and messages
│ │ └── ProfileMenu.jsx # Settings and device management
│ └── package.json
├── server/ # App REST API (port 5000)
│ ├── index.js # Express entry point
│ ├── db/
│ │ ├── schema.sql # SQLite schema (profiles + telemetry)
│ │ └── tremorix.db # SQLite database (git-ignored)
│ └── routes/
│ ├── profiles.js # GET /api/profiles
│ └── telemetry.js # GET /api/profiles/:id/telemetry
└── package.json # Root scripts (dev + seed)
The system has four independent processes:
| Process | Runtime | Port | Role |
|---|---|---|---|
tremor_spoon.ino |
ESP32 | BLE | Stabilises the spoon; streams IMU data |
ble_bridge.py |
Python | — | Bridges BLE notifications to WebSocket |
server.js |
Node.js | 3000 | Stores device data; computes adaptive mode |
server/index.js |
Node.js | 5000 | Serves profile + telemetry data to the React app |
client/ |
Vite | 5173 | React dashboard UI |
The two servers are intentionally separate: server.js owns the real-time device pipeline, while server/index.js owns the user-facing profile and historical data API.
- Hardware: ESP32 + MPU-6050 + 2× MG90S servos wired as above
- Arduino IDE with the following libraries installed:
ESP32ServoArduinoJsonBLEDevice(ESP32 Arduino BLE stack, included in ESP32 board package)
- Python 3.9+
- Node.js 18+
Open tremor_spoon.ino in Arduino IDE, select the correct ESP32 board and COM port, then upload.
On boot the device:
- Calibrates the gyro bias (keep it still for ~1 second)
- Starts advertising as
TremorSpoonover BLE
# Root (analytics server + concurrently)
npm install
# App server
cd server && npm install && cd ..
# React frontend
cd client && npm install && cd ..
# Python bridge
pip install bleak websocketsSeed the database with synthetic 7-day telemetry for both demo profiles:
npm run seednode server.jsThis starts the WebSocket + HTTP server on port 3000. It will:
- Accept the BLE bridge connection
- Store all incoming sensor readings in
tremor_history.db - Run a mode analysis every 60 seconds and push the recommended mode to the device
python ble_bridge.pyThe bridge will scan for the TremorSpoon BLE device, connect, and begin forwarding data. Both the BLE and WebSocket connections auto-reconnect on failure.
npm run devThis starts both the Express API on port 5000 and the Vite dev server on port 5173 concurrently.
Open http://localhost:5173 in a browser.
The firmware uses a complementary filter to fuse accelerometer angle estimates with gyroscope integration, which gives a stable, low-latency pose estimate:
pitch = α × (pitch + gyro_rate_x × dt) + (1 − α) × pitch_acc
roll = α × (roll + gyro_rate_y × dt) + (1 − α) × roll_acc
The servo counter-correction is then:
servo_target = 90° + angle − gyro_rate × gyroGain
servo_pos = smoothing × prev_pos + (1 − smoothing) × servo_target
Three modes tune the filter parameters to match the user's tremor profile:
| Mode | Alpha (α) | Smoothing | Gyro Gain | Max Rate | Tremor Range |
|---|---|---|---|---|---|
| LOW | 0.90 | 0.70 | 0.4 | 80°/s | < 3 Hz (resting) |
| MILD | 0.93 | 0.80 | 0.8 | 120°/s | 3–7 Hz (typical Parkinson's) |
| HIGH | 0.97 | 0.88 | 1.2 | 200°/s | > 7 Hz (severe / action) |
Every 60 seconds, server.js analyses the last 7 days of stored readings and maps the 95th-percentile tremor frequency to a mode recommendation, which is sent to the device as a set_mode command.
ESP32 → Bridge (TX Characteristic, notify, 10 Hz):
{ "ts": 123456, "p": 2.3, "r": -1.1, "gx": 4.5, "gy": -2.0,
"ps": 92, "rs": 88, "hz": 4.2, "mode": "MILD" }Bridge → server.js (WebSocket):
{ "type": "bridge_hello", "version": "1.0" }
{ "type": "data", "payload": { ...sensor packet... } }server.js → Bridge → ESP32 (RX Characteristic, write):
{ "cmd": "set_mode", "mode": "HIGH" }
{ "cmd": "calibrate" }Since telemetry is stored at coarse intervals, tremor frequency is computed as an episode rate — the number of distinct rising-edge crossings above the mean + 1σ threshold, expressed as episodes per hour:
threshold = mean(correction_angle) + σ(correction_angle)
episodes = count of rising edges crossing the threshold
rate = episodes / (7 × 24) → episodes / hour
The firmware independently estimates instantaneous tremor frequency via gyro zero-crossing counting and streams it as the hz field.
| Route | Page | Description |
|---|---|---|
/ |
ProfileSelect | Landing page with animated spoon, benefit cards, Get Started / Log in |
/login |
Login | Email + password form with patient / caregiver / doctor role selection |
/patients |
PatientList | Doctor view: list of all patients |
/profile/:id |
Dashboard | 7-day waveform chart + clinical metrics table |
/profile/:id/exercises |
Exercises | 5 guided hand exercises with completion tracking and confetti |
/profile/:id/messages |
Messages | Doctor notes with a reply compose bar |
/profile/:id/settings |
ProfileMenu | Device pairing, privacy, preferences, support, log out |
The main clinical view for each profile:
- Waveform chart — Recharts
LineChartshowingcorrection_angleover the past 7 days. Line colour#0D8ABC, Y-axis fixed at 5–130°, day-of-week X-axis labels, light gray horizontal grid lines. - Metrics table — Three rows:
- Avg X-axis deviation (mean correction angle, 1 d.p.)
- Avg Y-axis deviation (same signal, shown per-axis)
- Tremor frequency (episode rate, expressed as
N episodes/hr)
Five gentle hand exercises designed for tremor management:
- Gentle finger stretch
- Breathing pause
- Slow grip practice
- Wrist rotations
- Thumb touches
Completing an exercise triggers a confetti animation and increments the weekly counter. Completed exercises are visually distinguished and their buttons are disabled.
A read-only message thread of doctor notes linked to the user's shared reports. Includes a compose bar to attach a note to the next weekly report share.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/profiles |
Returns all profiles [{ id, name, avatar_url }] |
GET |
/api/profiles/:id/telemetry |
Returns correction angle readings for the past 7 days [{ recorded_at, pitch, roll, sp, sr }] |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/recent |
Last 60 seconds of sensor readings |
GET |
/api/analysis |
7-day analysis: recommended mode, p95/avg/max tremor Hz, daily summaries |
GET |
/api/history |
daily_summary rows for the past 7 days |
POST |
/api/mode |
Override device mode { "mode": "LOW" | "MILD" | "HIGH" } |
POST |
/api/calibrate |
Trigger gyro recalibration on the device |
tremorix.db (App server — profiles and telemetry)
CREATE TABLE profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
avatar_url TEXT
);
CREATE TABLE telemetry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id INTEGER NOT NULL REFERENCES profiles(id),
recorded_at TEXT NOT NULL,
pitch REAL NOT NULL,
roll REAL NOT NULL,
sp REAL NOT NULL, -- servo pitch position
sr REAL NOT NULL -- servo roll position
);tremor_history.db (Analytics server — live device readings)
CREATE TABLE readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL, -- Unix ms
pitch REAL, roll REAL,
gyro_x REAL, gyro_y REAL,
pitch_srv INTEGER, roll_srv INTEGER,
tremor_hz REAL,
mode TEXT
);
CREATE TABLE daily_summary (
day TEXT PRIMARY KEY, -- YYYY-MM-DD
avg_hz REAL,
max_hz REAL,
p95_hz REAL,
severity TEXT, -- low / mild / high
session_min REAL,
updated_at INTEGER
);Readings older than 8 days are automatically pruned hourly.
| Layer | Technology |
|---|---|
| Hardware firmware | C++ (Arduino / ESP32), ESP32Servo, ArduinoJson, ESP32 BLE stack |
| BLE bridge | Python 3, bleak, websockets |
| Analytics server | Node.js, Express, ws, better-sqlite3, cors |
| App server | Node.js, Express, better-sqlite3, cors |
| Frontend | React 18, Vite, Tailwind CSS, Recharts, Framer Motion, Lucide React |
| Database | SQLite (two separate files) |
| Dev tooling | concurrently, nodemon |