MatrixHub is an open-source firmware and web dashboard for the Waveshare ESP32-S3-Matrix board. It combines environmental monitoring (CO₂, temperature, humidity), a rich web UI, BLE scanning, Wi-Fi motion sensing, USB HID emulation, Telegram/Webhook/Pushover notifications, Shelly smart-plug integration, and a scriptable macro engine — all running on a dual-core ESP32-S3 with PSRAM.
License: GPL-3.0 — free for personal/hobbyist use; commercial redistribution of closed-source derivatives is not permitted. See LICENSE.
- Hardware
- Features
- First Boot and Device Access
- Build and Flash
- Module Reference
- Environmental Sensor (SCD41)
- LED Matrix Display
- Alarm System
- Notifications
- Telegram Bot
- Webhook Integration
- Pushover Integration
- UDP Push
- Shelly Integration
- BLE Scanner
- Wi-Fi CSI Motion Sensing
- Air Mouse (USB HID)
- USB Keyboard & Mouse Jiggler
- Macro Engine
- IMU (QMI8658)
- Sensor Compensation
- Data Logger
- Power Management
- Thermal Protection
- System Button
- Web UI
- Repository Layout
- Architecture Notes
- Video Demos
- Contributing
| Component | Details |
|---|---|
| Board | Waveshare ESP32-S3-Matrix |
| MCU | ESP32-S3, dual-core Xtensa LX7 @ 240 MHz |
| PSRAM | 8 MB SPIRAM (all large buffers live here) |
| LED Matrix | 8 × 8 WS2812B on GPIO 14 |
| IMU | QMI8658 (gyro + accelerometer) on I2C1 — SDA=11, SCL=12, INT1=8 |
| CO₂ Sensor | SCD41 on I2C2 (Wire1) — SDA=6, SCL=7, address 0x62 |
| Boot button | GPIO 0 (active LOW) — multi-function: short/medium/long press |
| USB | Native USB (HID Mouse + Keyboard via TinyUSB) |
| Category | Highlights |
|---|---|
| Sensing | CO₂ (400–10 000 ppm), temperature, humidity via SCD41 |
| Display | 8×8 RGB matrix — live readings, alarm icons, custom icons, menu |
| Alarms | Per-metric rules with severity, cooldown, and multi-channel dispatch |
| Notifications | Telegram bot (send + receive commands), Webhook (HTTP POST), Pushover |
| Smart home | Shelly smart-plug monitoring and control |
| BLE | Passive BLE device scanner with configurable scan windows |
| Wi-Fi CSI | Channel-state-information motion sensing (WebSocket streaming) |
| USB HID | Air Mouse (gyro-driven cursor), keyboard jiggler, macro shortcuts |
| Macros | Scriptable action sequences triggered by button clicks or API calls |
| Power | Configurable deep-sleep after inactivity, periodic wake |
| Thermal | CPU throttle at 60 °C / 68 °C, emergency sleep at 80 °C |
| Data | Binary ring-log on LittleFS; charts in the web UI |
| Web UI | SvelteKit dashboard served from embedded LittleFS (HTTPS) |
| Security | JWT auth, HTTPS (mbedTLS), rate limiting, BLE bonding with MITM |
| UDP push | Real-time telemetry broadcast to local network listeners |
If no Wi-Fi STA network is saved the device starts in Access Point mode.
| Parameter | Default value |
|---|---|
| AP SSID | matrixhub.local |
| AP password | (open) |
| AP address | 192.168.4.1 |
| Hostname | matrixhub |
| Browser URL | matrixhub.local or 192.168.4.1 |
Recommended first-run flow
- Flash the firmware.
- Connect to the Wi-Fi AP
matrixhub.local. - Open
matrixhub.localor192.168.4.1in a browser. The device redirects to HTTPS automatically; accept the self-signed certificate warning. - Sign in with the configured credentials.
- Go to WiFi Station and add your home network.
- Open System Status to confirm the device is online.
- Configure alarm thresholds and notification channels.
Prerequisites: PlatformIO CLI or IDE extension, Node.js ≥ 18 for the SvelteKit frontend.
Create a local HTTPS certificate header before the first build:
cp src/config/certificates.local.example.h src/config/certificates.local.h
# Replace the placeholder certificate and private key with your own device keypair.src/config/certificates.local.h is intentionally ignored by Git. Do not commit
device certificates or private keys.
# Kill any open monitor first (required before every upload)
pkill -f monitor
# Full build (UI + firmware) and upload
pio run -e waveshare_esp32s3_matrix -t upload
# Fast firmware-only iteration (skips UI rebuild)
SKIP_UI=1 pio run -e waveshare_esp32s3_matrix -t upload
# Device monitor (after flashing)
pio device monitor
# Backend unit tests on the host (Unity)
pio test -e nativeFrontend standalone build:
cd interface
npm install
npm run buildsrc/sensors/scd4x/
Reads CO₂ concentration, temperature, and humidity from the Sensirion SCD41 over I²C. Samples every 5 s; the result is pushed to the live dashboard via callbacks and also fed to the alarm engine. The sensor undergoes automatic self-healing with exponential back-off if consecutive reads fail.
| Metric | Range |
|---|---|
| CO₂ | 400–10 000 ppm |
| Temperature | −20 °C – +70 °C |
| Humidity | 0–100 % RH |
src/matrix/
Controls the 8×8 WS2812B matrix via the RMT peripheral (DMA buffer in internal DRAM).
- Live data mode — scrolls current CO₂ / temperature / humidity reading
- Alarm mode — displays a dedicated alarm icon with configurable colour
- Custom icons — up to N user-defined 8×8 bitmaps stored in LittleFS
- Effect modes — selectable animation effects per state
- Menu overlay — matrix-based navigation menu accessed via the boot button
Settings (brightness, scroll speed, colour palette) are persisted in LittleFS and applied at runtime without reboot.
src/alarms/
Flexible rule engine evaluated every 10 s.
- Rules are defined per metric (CO₂, temperature, humidity) with:
- threshold value and direction (above / below)
- severity level (info / warning / critical)
- cooldown period (10 s – 24 h)
- On trigger, the alarm notifier dispatches to all enabled channels (matrix, Telegram, Webhook, Pushover).
- Rules are stored in LittleFS and editable via the web UI.
src/notifications/
A unified dispatcher routes alarm events to one or more channels:
| Channel | Module |
|---|---|
| Telegram | notifications/telegram/ |
| Webhook | notifications/webhook/ |
| Pushover | notifications/pushover/ |
Each channel has independent enable/disable, credentials, and validation logic.
src/notifications/telegram/
Full two-way Telegram integration with TLS (mbedTLS).
- Outbound: sends alarm notifications and status summaries to a configured chat.
- Inbound: polls for commands (long-poll, 1 s timeout, exponential back-off up
to 5 min) and responds to
/status,/co2,/temp,/humidity, and custom commands. - TLS connection is shared and reused; a warm-up handshake with retry ensures reliability on flaky networks.
- Maximum message length: 768 characters. Secrets (bot token, chat ID) are stored in LittleFS, not hard-coded.
src/notifications/webhook/
Sends an HTTP POST with a JSON payload to any user-configured URL when an alarm fires.
- Configurable URL, custom headers possible.
- JSON body includes metric name, value, severity, and timestamp.
- Backed by a queue + worker task to avoid blocking the alarm engine.
src/notifications/pushover/
Sends push notifications to the Pushover service (iOS / Android app).
- TLS connection to
api.pushover.net. - Priority and sound configurable per notification.
src/udp/
Broadcasts real-time sensor telemetry as UDP packets to a configurable host/port. Useful for feeding data to local dashboards (e.g. InfluxDB via Telegraf) without requiring a cloud service.
src/shelly/
Monitors and controls Shelly smart plugs over the local network (HTTP / Gen1 & Gen2 API).
- Polls power consumption, relay state, and device status.
- Allows toggling relay state via the web UI or alarm actions.
- Supports multiple Shelly devices, each with configurable IP and name.
- Alarm wiring: a Shelly relay can be automatically switched by an alarm rule.
src/ble/
Passive BLE advertiser scanner powered by NimBLE.
- Scans for nearby BLE devices on a configurable interval.
- Reports RSSI, device name, and last-seen timestamp via the web UI and API.
- Turbo discovery mode increases scan duty cycle for faster device enumeration.
- BLE stack runs on Core 0 alongside the Wi-Fi stack.
- Scan parameters: 5 s scan duration, 500 ms interval, 450 ms window (90 % duty cycle) in normal mode.
src/wifisensing/
Uses 802.11 Channel State Information (CSI) to detect motion without a dedicated PIR sensor.
- CSI packets arrive via an ISR callback at up to 10 Hz and are queued (zero-copy) into a PSRAM-backed FreeRTOS queue.
- A processing task computes variance across subcarrier amplitudes; a configurable threshold triggers a motion event.
- Live CSI data is streamed to the browser over a binary WebSocket.
- Feature is disabled by default and requires STA mode (not available in AP-only mode).
src/airmouse/
Turns the ESP32-S3-Matrix into a gyroscope-driven wireless USB mouse.
- IMU (QMI8658) gyro data drives cursor movement via USB HID.
- Calibration removes gyro drift at startup or on demand.
- Single / double / triple button clicks map to left click, right click, and configurable macro execution.
- Shares the USB HID interface with the keyboard service (mutex-protected).
- Configurable sensitivity and dead-zone via the web UI.
src/keyboard/ · src/airmouse/features/JigglerController.cpp
- USB Keyboard: injects HID keystroke sequences programmatically (used by macros and the jiggler).
- Mouse Jiggler: periodically moves the cursor by a tiny amount to prevent screen lock / screen savers. Jiggle interval, distance, and pattern are configurable. The jiggler can also send keystrokes to simulate activity.
src/macros/
A lightweight scripting engine for automating keyboard/mouse actions.
- Macros are defined as action sequences (key press, delay, mouse click, etc.) and stored in LittleFS.
- Triggered by: physical button clicks (single / double / triple), Telegram commands, or HTTP API calls.
- The engine runs on a dedicated FreeRTOS task; execution is atomic per macro.
- Macros are editable and reloaded at runtime without a reboot.
src/sensors/imu/
Driver and manager for the QMI8658 6-axis IMU.
- Provides calibrated gyroscope (°/s) and accelerometer (g) data.
- Used by the Air Mouse module for cursor control.
- Interrupt-driven data-ready signalling on INT1 (GPIO 8).
- Shared consumer flag system allows multiple subsystems to subscribe to movement and click events without polling.
src/compensation/
Corrects the SCD41 temperature reading for self-heating caused by the ESP32-S3 MCU.
- Applies a dynamic offset:
offset = base + slope × (cpu_temp − ref_cpu_temp). - CPU temperature is sampled from the internal ADC and smoothed with an exponential moving average (α = 0.05).
- Compensation parameters (base offset, slope, reference CPU temp, min/max clamp) are configurable via the web UI and persist in LittleFS.
- When disabled, a fixed factory default offset is applied.
src/system/datalogger/
Writes sensor history to LittleFS in a compact binary format.
- Aggregates samples over a 5-minute window before writing to flash (reduces wear and keeps files representative of wall-clock time).
- Old log files are rotated when free space drops below 50 KB.
- Log files are downloadable and viewable as charts in the web UI.
- A separate in-RAM ring buffer (500 entries) feeds the live log tail endpoint without touching flash.
src/system/power/
Configurable deep-sleep to extend battery life or reduce idle power.
| Parameter | Default | Range |
|---|---|---|
| Inactivity timeout | 5 min | 5 min – 24 h |
| Grace period after boot | 1 min | 1 min – 10 min |
| Wake interval | 10 min | — |
- Activity is tracked from HTTP requests, button presses, and BLE events.
- Before sleeping, a config snapshot is saved to RTC memory for fast warm-boot restore.
- Deep sleep is disabled by default and must be enabled in the web UI.
- When deep sleep is enabled, at least one wake source must stay enabled: timer, BOOT button, or touch wake.
src/system/thermal/
Three-stage CPU temperature protection:
| Stage | Threshold | Action |
|---|---|---|
| Soft throttle | 60 °C | Reduce CPU to 160 MHz |
| Hard throttle | 68 °C | Lock CPU at 160 MHz |
| Critical / emergency | 80 °C | Deep sleep for 30 s to cool down |
Hysteresis of 5 °C prevents rapid toggling between states. Temperature is monitored every 5 s.
src/system/button/
The boot button (GPIO 0) is multi-function:
| Gesture | Action |
|---|---|
| Short press (< 1 s) | Single click — configurable (macro / air mouse click) |
| Medium hold (2 s) | Open / close matrix navigation menu |
| Long hold (10 s) | Arm factory reset (device warns at 7 s) |
| Double-click after 10 s hold | Confirm factory reset (within 5 s window) |
During a medium-press hold, periodic actions (e.g. macro triggers) are fired every configured interval — the button does not need to be released first.
interface/ (SvelteKit, embedded in LittleFS)
The dashboard is served over HTTPS directly from the device.
| Section | Content |
|---|---|
| Dashboard | Live CO₂, temperature, humidity with mini-charts |
| Charts | Historical sensor data from the binary data logger |
| Alarms | Rule editor — thresholds, severity, cooldowns |
| Notifications | Telegram, Webhook, Pushover configuration |
| Shelly | Device list, relay control, power readings |
| BLE | Nearby device list with RSSI and last-seen |
| Wi-Fi Sensing | Enable/disable CSI, live WebSocket data view |
| Air Mouse | Enable/disable, sensitivity, calibration |
| Macros | Create, edit, and trigger macro sequences |
| Matrix | Brightness, speed, custom icons, display mode |
| Compensation | SCD41 temperature offset parameters |
| Power | Sleep settings, inactivity timeout |
| WiFi Station | Add/remove Wi-Fi networks |
| System Status | Heap, PSRAM, uptime, task list, CPU temperature |
| Logs | Live log tail + downloadable daily log files |
| System | Reboot, factory reset, firmware update, time sync |
| Help | Quick-start guide and feature overview |
MatrixHub/
├── src/ # Firmware (C++17 / ESP-IDF + Arduino)
│ ├── airmouse/ # USB HID air mouse & jiggler
│ ├── alarms/ # Alarm rule engine & notifier
│ ├── api/ # HTTP API controllers (/api/…)
│ ├── ble/ # BLE scanner (NimBLE)
│ ├── compensation/ # SCD41 temperature compensation
│ ├── config/ # System.h, Hardware.h, App.h — source of truth
│ ├── keyboard/ # USB HID keyboard service
│ ├── macros/ # Macro engine, parser, persistence
│ ├── matrix/ # LED matrix driver & menu
│ ├── notifications/ # Telegram / Webhook / Pushover dispatchers
│ ├── sensors/ # SCD41, IMU (QMI8658), logging
│ ├── shelly/ # Shelly smart-plug integration
│ ├── system/ # Boot, power, thermal, button, data logger, watchdog
│ ├── udp/ # UDP telemetry push
│ ├── utils/ # Shared utilities
│ └── wifisensing/ # RSSI sensing and CSI diagnostics
├── interface/ # SvelteKit web dashboard
│ └── src/
├── lib/
│ ├── framework/ # Upstream web/API framework base (LGPL-3.0)
│ ├── PsychicHttp/ # HTTPS server (MIT)
│ ├── ArduinoJson/ # JSON (MIT)
│ ├── NimBLE-Arduino/ # BLE stack (Apache-2.0)
│ └── sensirion_scd4x/ # SCD41 driver (BSD-3-Clause)
├── test/ # Unity unit tests (native host)
├── boards/ # PlatformIO board definitions
├── docs/ # Maintained engineering, workflow, and user notes
├── LICENSE # GPL-3.0
└── platformio.ini # Build configuration
- Dual-core pinning: Wi-Fi / BLE / networking on Core 0; UI logic, matrix, and application tasks on Core 1.
- Memory strategy: everything > 512 B that does not require DMA lives in
PSRAM (
MALLOC_CAP_SPIRAM). DMA buffers (matrix, SPI) stay in internal DRAM. - Service pattern: long-lived services are owned and wired by
ServiceRegistry; API services stay transport-focused. - Config persistence: settings live in a PSRAM working store, persist to LittleFS JSON, and use an RTC shadow copy for deep-sleep restore.
- No magic numbers: all timeouts, thresholds, and pin assignments are in
src/config/Hardware.h,App.h, andTaskConfig.h.
- This Device Warns Me About High CO₂ and Sends It to Telegram
- MatrixHub Full Interface Tour: Alerts, Telegram, BLE, Shelly, and System Pages
- Keep all task configs in
src/config/TaskConfig.h. - Do not modify
lib/framework/orsrc/wifisensing/csi/vendor/without explicit approval. - Run backend tests before submitting:
pio test -e native. - Use
LOGI()/LOGW()/LOGE()— neverSerial.print(). - All new large buffers (> 512 B) must use
MALLOC_CAP_SPIRAM.
See docs/engineering/README.md for the full engineering reference.