HRBR Sleep Monitor V2.0 — Internal Manual

Comprehensive technical manual for the HRBR radar-based sleep monitoring system. Covers all dashboard features, classification algorithms, firmware behaviour, and deployment infrastructure.

1. System Overview

Architecture

The HRBR system consists of four main components connected via MQTT:

ESP32C6 Device MR60BHA2 Radar BH1750 Lux MQTT TLS:8883 HiveMQ Cloud Managed MQTT Broker Free: 100 conn, 10 GB/mo MQTT WSS:8884 Railway Python Backend + API Sleep Classifier PostgreSQL REST API fetch /api/* Cloudflare Pages Static Dashboard Chart.js + Paho MQTT Global CDN Device Broker Backend Dashboard Data flow API request/response
ComponentServiceURL
MQTT BrokerHiveMQ Cloud (free)*.s1.eu.hivemq.cloud
Backend + APIRailwayhrbr-v2-production.up.railway.app
DatabaseRailway PostgreSQL(internal)
DashboardCloudflare Pageshrbr-kc.pages.dev
DeviceXIAO ESP32C6Seeed MR60BHA2 radar + BH1750 lux

Data Flow

  1. ESP32 reads radar sensor (HR, BR, distance, presence, phase data) and lux sensor every ~1s
  2. Applies EMA filtering and outlier rejection on-device
  3. Publishes JSON to vitals topic every 5s (configurable) via MQTT over TLS
  4. HiveMQ Cloud routes messages to all subscribers
  5. Railway backend receives vitals, stores in PostgreSQL, runs sleep classification every 30s
  6. Dashboard receives vitals via WebSocket (WSS:8884), displays live data and fetches history from Railway API

MQTT Topics

TopicDirectionDescription
vitalsDevice → CloudRaw sensor readings (HR, BR, distance, presence, lux, phase data)
targetDevice → CloudRadar target tracking (x, y, speed per detected person)
statusDevice → CloudDevice status (firmware, WiFi, intervals, uptime)
lwtDevice → CloudLast Will & Testament (online/offline status)
cmd/V005Cloud → DeviceCommands (ping, reboot, set_wifi, set_config)
sleep/V005Backend → CloudSleep stage classifications (every 30s)
sleep/V005/summaryBackend → CloudSleep session summaries (on session end)
Topic naming: V005 is the short device ID derived from KC2507V005 by stripping the KC2507 prefix.

2. Dashboard Features

Header & Status Bar

Dashboard header showing broker status, device status, device selector, and data age
Fig 1: Dashboard header with connection status indicators
IndicatorStatesMeaning
Broker Connected / Reconnecting / DisconnectedWebSocket connection to HiveMQ Cloud MQTT broker
Device Online / Waiting / Offline / StaleWhether the selected device is actively sending vitals
Device SelectorDropdownAuto-populated as devices are discovered via MQTT messages
Data Age"Live" / "Xs ago"Time since last vitals message. Turns red if >15s (stale)
MsgsCounterTotal MQTT messages received this session

Device auto-discovery: When the dashboard connects to the broker, it subscribes to all topics. Any message containing a "board" field automatically registers that device in the selector dropdown. The first discovered device is auto-selected.

Vital Sign Cards

Vital sign cards showing HR, BR, distance, presence, lux, sleep stage, movement, HRV, breath amplitude, breath regularity
Fig 2: Vital sign cards with live sensor data
CardSourceDescription
Heart RateFirmware EMAEMA-filtered heart rate in BPM (40–200 range, outliers rejected)
Breathing RateFirmware EMAEMA-filtered respiratory rate in RPM (5–40 range)
DistanceRadarDistance to target in meters (sensor returns cm, converted)
PresenceRadarGreen dot = person detected. Uses 3s timeout with latch logic
Ambient LightBH1750Ambient light level in lux (useful for sleep environment analysis)
Sleep StageBackend classifierCurrent classified stage: AWAKE LIGHT DEEP REM
MovementBackendPhase movement index (std of detrended total phase, in radians)
HRVBackendHeart rate variability proxy (HR standard deviation over window)
Breath AmpBackendBreathing depth (peak-to-peak of detrended breath phase)
Breath RegBackendBreathing regularity (autocorrelation, 0–1, 1 = perfectly periodic)
Stale data: When no vitals arrive for >15 seconds, all stat cards become semi-transparent (40% opacity) to indicate stale data. The device status changes to "Stale".

Real-Time Charts

Real-time charts showing HR, BR, and distance trends over the last 30 minutes
Fig 3: Real-time trend charts (HR, BR, Distance)

The dashboard displays four main chart sections:

Charts are built with Chart.js and update in real-time as messages arrive. Each chart holds up to 360 data points (30 min at 5s intervals). During playback mode, charts are populated from stored data.

Sleep Stage Display

Sleep stage display with hypnogram and epoch details
Fig 4: Sleep stage section with hypnogram

The sleep section shows:

Device Controls

Device control section showing ping, reboot, WiFi config, and interval settings
Fig 5: Device information and remote control panel

Ping / Status

Sends "ping" to the device command topic. The device responds with a status message containing firmware version, radar firmware, MAC address, WiFi SSID, RSSI, IP address, intervals, and uptime.

Reboot

Sends "reboot" command. Device publishes a {"status":"rebooting"} message, waits 3 seconds, then restarts. Requires confirmation dialog.

WiFi Configuration

Remotely change the device's WiFi credentials. Sends a JSON command:

{"set_wifi": {"ssid": "NewNetwork", "password": "pass123"}}

The device saves credentials to NVS (non-volatile storage), reboots, and attempts to connect. If the new network fails, it automatically reverts to the hardcoded default (Airtel_KuboCareLab).

Vitals Interval

Change how frequently the device publishes vitals (default: 5 seconds). Minimum: 1 second. Value is persisted in NVS across reboots.

{"set_config": {"vitals_interval": 3000}}

Clear WiFi

Resets device WiFi to default (clears NVS-stored credentials). Device reboots and connects to Airtel_KuboCareLab.

History & Playback

History modal showing session list and custom time range picker
Fig 6: History modal with session browser and time range picker

Click the "History" button to open the session browser. Two ways to load data:

Session Browser

Lists completed sleep sessions with duration, efficiency, and epoch count. Click a session to load its vitals and epochs into playback mode. Sessions are created automatically by the backend when sleep onset is detected.

Custom Time Range

Select any start/end time to load raw vitals data. Useful for reviewing data that doesn't fall within a detected sleep session.

Playback Controls

Playback control bar with play/pause, speed, scrubber, and CSV download
Fig 7: Playback control bar
ControlFunction
← LiveExit playback and return to live data mode
▶ / ⏸Play / Pause playback
Speed (1x–10x)Playback speed multiplier
ScrubberDrag to jump to any point in the loaded data
CSVDownload the currently loaded data as a CSV file

CSV Download

Two ways to download data as CSV:

  1. From History modal: Set a time range → click "Download CSV". Downloads directly without entering playback mode.
  2. During playback: Click the "CSV" button in the playback bar to download all loaded data.

CSV columns:

timestamp, datetime, board_id, hr, br, distance, presence, lux,
total_phase, breath_phase, heart_phase, uptime_ms

3. ESP32 Firmware (V4.2.0)

Sensors

SensorModelDataInterface
mmWave RadarSeeed MR60BHA2 (60GHz)Heart rate, breathing rate, distance, presence, phase data, target trackingUART (Serial)
Light SensorBH1750Ambient light in luxI2C
RGB LEDNeoPixelStatus indicatorDigital (D1)

EMA Filtering & Outlier Rejection

The firmware applies Exponential Moving Average (EMA) filtering to smooth HR and BR readings before publishing:

EMA(t) = α × raw(t) + (1 - α) × EMA(t-1)

α = 0.2 (smoothing factor)

Before EMA, each reading is checked against valid bounds:

VitalMinMaxAction if out of bounds
Heart Rate40 BPM200 BPMReading discarded (not added to EMA)
Breathing Rate5 RPM40 RPMReading discarded

The EMA filter is initialized with the first valid reading and only publishes after both HR and BR have been seen at least once.

Presence Detection & Warmup

The firmware uses a latch-on / timeout-off pattern for presence:

Warmup period: After presence is first detected, the device waits 60 seconds before publishing HR/BR values. This allows the radar to stabilize and the EMA filter to converge. Distance and phase data are published immediately.

LED Status Indicators

ColorMeaning
RedConnecting to WiFi
BlueConnecting to MQTT broker
GreenFully connected (WiFi + MQTT) — normal operation
YellowBoot button held / sensor error
PurpleStatus message published (response to ping)
Brief off-blinkData published (vitals or target)

Remote Commands

The device subscribes to cmd/V005 and supports:

CommandPayloadAction
Ping"ping" or "status"Responds with full device status on status topic
Reboot"reboot"Reboots after 3s delay
Set WiFi{"set_wifi":{"ssid":"...","password":"..."}}Saves to NVS, reboots
Clear WiFi{"clear_wifi":true}Clears NVS WiFi, reverts to default
Set Intervals{"set_config":{"vitals_interval":5000,"targets_interval":5000}}Updates publish intervals (ms), saved to NVS
WiFi auto-revert: If the device cannot connect to MQTT within 30 seconds on custom WiFi credentials, it automatically clears the stored credentials and reboots to the default network.

4. Algorithms (Backend)

The backend runs on Railway and processes incoming MQTT vitals to classify sleep stages. All algorithms run in Python on the server, not on the ESP32.

Data Buffer (Sliding Window)

A circular buffer holds the last 5 minutes (300 samples at 1Hz) of vitals data. Each sample includes HR, BR, distance, presence, lux, and three phase values from the radar.

ParameterValuePurpose
BUFFER_WINDOW_SEC300 (5 min)Sliding window size
MIN_SAMPLES_FOR_CLASSIFY60 (1 min)Minimum samples before classification runs
MIN_PHASE_SAMPLES30Minimum phase samples for phase-derived features

Outlier rejection is applied on buffer entry: HR outside 40–200 BPM or BR outside 5–40 RPM is set to null.

Feature Extraction

Every 30 seconds, the classifier extracts these features from the sliding window:

Traditional Features (from HR/BR/distance)

FeatureFormulaUsed For
hr_meanMean of valid HR values in windowBaseline comparison, stage rules
hr_stdStd deviation of HR (HRV proxy)Deep (<1.5 BPM) vs REM (>3.0 BPM)
hr_cvCoefficient of variation (std/mean)Regular (<4%) vs variable (>8%)
br_mean, br_std, br_cvSame as HR but for breathingRegularity assessment
movementStd deviation of distance valuesFallback movement detection

Phase-Derived Features (from radar phase data)

The MR60BHA2 radar outputs three phase signals that capture sub-millimeter body movement:

FeatureDerivationMeaning
phase_movementStd of detrended, unwrapped total_phaseOverall body movement (sub-mm sensitivity). Higher = more movement.
breath_amplitudePeak-to-peak of detrended breath_phaseBreathing depth in radians. Deeper breathing = higher value.
breath_regularityAutocorrelation peak of breath_phase0–1 scale. 1.0 = perfectly periodic breathing.
Phase unwrapping: Raw phase values wrap around at ±π. We apply numpy.unwrap() before computing features to get continuous phase signals. Then scipy.signal.detrend() removes linear drift.

Baseline Calibration

The first 10 minutes (600 samples) of data are used to establish the subject's baseline (assumed awake):

Once set, the baseline remains fixed for the session. If no baseline is available when classification starts, the current hr_mean is used as a temporary baseline.

The key derived feature is HR delta:

hr_delta = (hr_baseline - hr_mean) / hr_baseline

Positive delta means HR has dropped below baseline (sleeping). Negative means HR is above baseline (active/awake).

Sleep Stage Classifier

A rule-based classifier evaluates features in priority order. The first matching rule wins:

Start Rule 1: Significant Movement? phase_movement > 0.5 rad OR distance_std > 0.05 m YES AWAKE NO Rule 2: HR At or Above Baseline? hr_delta < 3% (HR_LIGHT_DROP_PCT) YES AWAKE NO HR below baseline — subject is sleeping Rule 3: HR Deeply Dropped + Very Regular? hr_delta ≥ 10% AND hr_cv < 4% YES DEEP NO Rule 4: HR Variable? hr_cv > 8% (HR_CV_VARIABLE) YES REM NO Rule 5: Default — Sleeping but not Deep or REM HR below baseline, moderate variability LIGHT Confidence boosters at each stage (secondary features): br_cv | breath_regularity | breath_amplitude | hr_std (HRV proxy) | phase_movement

Classification Thresholds

ThresholdValuePurpose
PHASE_MOVEMENT_THRESHOLD0.5 radAbove = significant body movement (AWAKE)
MOVEMENT_THRESHOLD0.05 mFallback distance-based movement detection
HR_LIGHT_DROP_PCT3%Minimum HR drop from baseline for any sleep
HR_DEEP_DROP_PCT10%HR drop threshold for deep sleep candidate
HR_CV_REGULAR4%HR CV below this = very regular (deep)
HR_CV_VARIABLE8%HR CV above this = variable (REM)
BR_CV_REGULAR6%BR CV below this = regular breathing
BR_CV_IRREGULAR10%BR CV above this = irregular (REM)
BREATH_REG_DEEP0.7Breath regularity above = deep candidate
BREATH_REG_REM0.4Breath regularity below = REM candidate
BREATH_AMP_DEEP1.0 radBreath amplitude above = deep breathing
HRV_DEEP_LOW1.5 BPMHR std below = very stable (deep boost)
HRV_REM_HIGH3.0 BPMHR std above = variable (REM boost)

Confidence Scoring

Each rule produces a base confidence that is boosted by confirming secondary features:

Session Management

The session manager detects sleep onset and wake transitions:

EventConditionAction
Sleep Onset6 consecutive non-AWAKE epochs (3 minutes)Creates a new session in the database. Session start time is backdated to 3 min ago.
Wake Detection10 consecutive AWAKE epochs (5 minutes)Ends the session, calculates summary, publishes to MQTT.
Presence TimeoutNo presence for 5 minutesEnds the session (person left the bed).

Session Summary

When a session ends, the backend calculates and stores:

Breath Regularity Algorithm

Breath regularity measures how periodic the breathing pattern is using autocorrelation via the Wiener-Khinchin theorem:

  1. Take the detrended breath phase signal (30+ samples)
  2. Compute autocorrelation using FFT: ACF = IFFT(FFT(x) × conj(FFT(x)))
  3. Normalize by zero-lag (variance)
  4. Search for peak in lag range 3–8 samples (corresponds to 7.5–20 BPM at 1Hz)
  5. Peak value (0–1) is the regularity score

A score of 0.7+ indicates very regular breathing (typical of deep sleep). A score below 0.4 suggests irregular breathing (REM or wakefulness).

5. Infrastructure

Cloud Services

ServiceProviderTierPurpose
MQTT BrokerHiveMQ CloudFree (100 connections, 10 GB/mo)Managed MQTT with TLS + WebSocket
BackendRailwayFree ($5/mo credit)Python sleep service + FastAPI
DatabaseRailway PostgreSQLIncludedVitals, epochs, sessions storage
DashboardCloudflare PagesFree (unlimited BW)Static site hosting with global CDN
Railway sleep: Railway's free tier sleeps services after ~30 min of inactivity. Set up UptimeRobot to ping /api/health every 5 min to keep it alive.

Configuration Files

FilePurpose
dashboard/config.jsonLocal dev config (MQTT ws://localhost:9001, API localhost:8081)
dashboard/config.production.jsonProduction config (HiveMQ WSS, Railway API URL)
scripts/.deploy-configSaved deployment credentials (gitignored)
sleep_service/ProcfileRailway entry point: web: python sleep_service.py --api
sleep_service/nixpacks.tomlRailway build config (ensures libpq-dev for psycopg2)
docker-compose.ymlLocal dev stack (Mosquitto + PostgreSQL + sleep-service)

Environment Variables

VariableDefaultDescription
MQTT_BROKERlocalhostMQTT broker hostname
MQTT_PORT1883MQTT port (8883 for TLS)
MQTT_USERNAME(empty)MQTT auth username
MQTT_PASSWORD(empty)MQTT auth password
MQTT_USE_TLSfalseEnable TLS for MQTT
MQTT_CLIENT_IDhrbr-sleep-serviceMQTT client identifier
DEVICE_BOARD_IDKC2507V005Target device board ID
DATABASE_URL(empty)PostgreSQL URL; empty = SQLite fallback
PORT8081API server port (Railway injects this)

Deployment Commands

Full deployment (first time)

./scripts/deploy.sh

Quick dashboard redeploy (after code changes)

./scripts/redeploy-dashboard.sh

Local development

# Start local stack
docker compose up -d

# Verify
curl http://localhost:8081/api/health

# Open dashboard
cd dashboard && python3 -m http.server 8080

# Stop
docker compose down

Flash firmware

# Compile and upload
arduino-cli compile --fqbn esp32:esp32:XIAO_ESP32C6 firmware/HRBR_V4.0/HRBR_V4.0.ino
arduino-cli upload --fqbn esp32:esp32:XIAO_ESP32C6 --port /dev/ttyACM0 firmware/HRBR_V4.0/HRBR_V4.0.ino

# Monitor serial output
arduino-cli monitor --port /dev/ttyACM0 --config baudrate=115200

6. Troubleshooting

Dashboard shows "Broker: Disconnected"

Dashboard connected but no data

HR/BR shows "--" but distance works

History shows "Failed to load sessions"

Railway returns 502

Device keeps rebooting

Sleep classification shows only AWAKE


HRBR V2.0 — KuboCare — Internal Use Only