Morpheus is a two-wheel, Segway-style desk robot with a face. A wide bar display renders a pair of expressive sky-blue eyes and a live waveform "voice"; the whole thing is driven hands-free from a Muse S EEG headband. You furrow your brow to move, tilt your head to steer, and blink to brake — no phone, no remote.
The design goal is a robot that feels alive: a reactive synthwave UI up front, and a clean, scalable firmware behind it.
ESP32-S3R8 · Xtensa LX7 dual-core @ 240 MHz
| PSRAM | 8 MB OPI (octal) |
| Flash | 16 MB (W25Q128) |
| Display | 3.49" IPS 172×640 |
| Audio | onboard mic + speaker |
| IMU | QMI8658 6-axis |
| Link | Wi-Fi + BLE 5 · USB-C |
// sense → think → act
Muse EEG ─BLE─▶ control.h ─▶ drive
Muse accel─BLE─▶ head roll ─▶ steer
onboard mic─I2S▶ level ─▶ waveform
state.h (shared bus)
├─▶ ui.h ─▶ eye display
└─▶ motors.h─▶ servos
| Subsystem | Part | Interface | Notes |
|---|---|---|---|
| Display | AXS15231B | QSPI ≤ 40 MHz | 172×640 native; drawn on a 640×172 canvas, software-rotated |
| Mic (in) | ES7210 ADC | I2S · I2C 0x40 | dual MEMS, 16 kHz; own I2C bus on GPIO47/48 |
| Speaker (out) | ES8311 DAC | I2S · I2C 0x18 | wired; playback is roadmap |
| IMU | QMI8658 | I2C 17/18 | 6-axis, for future self-balancing |
| GPIO expander | TCA9554 | I2C 17/18 | LCD reset + backlight (P2) + audio amp |
| Motion | 2× 360° servos | LEDC PWM | GPIO1 = L, GPIO2 = R; external 5V rail, common ground |
| Control | Muse S (Gen 1/2) | BLE 5 | 4-ch EEG + 6-axis accel; preset p21 |
Servos must draw from the external 5V rail, never board logic. A railed head-tilt pinning both servos at full can otherwise brown out the display and mic. Grounds must be tied.
Control is entirely gestural. The Muse S streams four EEG channels
(TP9 AF7 AF8 TP10) plus accelerometer at 256 Hz. Firmware watches two envelopes per
channel — a slow one (lf, eyelid movement) and a fast one
(hf, muscle/EMG) — and tells four gestures apart. Detection is
baseline-relative: each region tracks its own resting level, so "rest ≈ 1.0×" and a
deliberate gesture is "2×+", regardless of electrode contact.
Accelerate forward and hold the speed you reach. Cruise ramps
up +0.010 per 50 Hz tick while you hold the furrow, capped at full forward.
If moving → brake to a stop. If already stopped → drop into reverse at half the forward cap. (Braking rides on blinks because the Muse temples read too flat for a reliable jaw-clench brake.)
Roll your head left/right to steer. A full ~28° tilt is full deflection; steering gain blends the two wheels. Tilt left → turn left.
The eyes blink back at you. A single blink is purely expressive — only a deliberate double blink acts on the drive.
| State | Entered by | Speed | Leaves on |
|---|---|---|---|
| STOP | boot · link lost · brake | 0 | furrow → FWD |
| FWD | furrow brow | 0 … +1.00 (held) | double-blink → STOP |
| REV | double-blink while stopped | −0.50 (half) | double-blink → STOP |
Safety: if the Muse disconnects, control resets — steering zeroed, cruise
dropped, state forced to STOP. Drive output is additionally gated behind the DRIVE_ENABLE
compile flag with a deadzone, speed cap, and per-tick ramp.
| Constant | Value | Meaning |
|---|---|---|
| EEG_BLINK_FACTOR | 2.2× | frontal LF ratio to register a blink (refractory 280 ms) |
| EEG_FURROW_FACTOR | 1.8× | frontal HF ratio to accelerate (held ≥ 220 ms) |
| EEG_DOUBLE_MS | 2000 | window for a "double" blink |
| DRIVE_ACCEL | 0.010 | cruise increment per tick while furrowing |
| DRIVE_FWD_MAX / REV_MAX | 1.00 / 0.50 | forward / reverse speed caps |
| DRIVE_STEER_GAIN | 0.60 | how hard head roll bends the wheels |
| HEAD_RANGE | 0.5 rad | head tilt (~28°) for full steering deflection |
Thresholds are dialled in live from the once-per-second [eeg] serial line:
watch the ratio peaks and nudge each *_FACTOR so a real gesture clears it and resting never does.
The display is a "porcelain" cartoon face: a white background, two flat sky-blue eyes that gaze and lean toward your head roll, a soft mouth, and a live onboard-mic waveform strip along the bottom. The panel is 172×640 native, so the firmware draws on a landscape 640×172 canvas in PSRAM and software-rotates it into the portrait buffer.
Double-buffered canvas in PSRAM → transpose/rotate → QSPI DMA blit.
Rendering is gated on gfxReady, so a bad display config can never crash the Muse or
mic loops. Runs a steady ~31 fps.
Eyes track head roll (gaze leans up to 16 px), pupils dilate with mic level, and blink back on every detected blink. The waveform scroll is time-based (px/sec) so it looks live at any frame rate.
# PlatformIO (platform = pioarduino; needs arduino-esp32 3.3.0+) pio run # compile pio run -t upload # flash over USB-C pio device monitor # serial log @ 115200
QSPI clock must stay ≤ 40 MHz (the SPI source is 80 MHz;
only 80/N clocks exist). PSRAM must be OPI (memory_type = qio_opi) — the
canvas lives there. Display I2C is GPIO17/18 (old 8/9 collided with QSPI CS9). LCD_IPS = false
for true colour. ESP32-S3 LEDC is 14-bit — a 16-bit ledcAttach fails silently and the servos go dead.
| Signal | GPIO | Signal | GPIO |
|---|---|---|---|
| QSPI CS / CLK | 9 / 10 | Left servo | 1 |
| QSPI D0–D3 | 11 · 12 · 13 · 14 | Right servo | 2 |
| Display I2C SDA / SCL | 17 / 18 | Audio I2C SDA / SCL | 47 / 48 |
| I2S MCLK / BCLK | 7 / 15 | I2S WS · DIN · DOUT | 46 · 6 · 45 |
| Backlight | TCA9554 P2 | Battery sense | ADC1 ch3 |
On-device EEG threshold calibration · confirm drive sign with wheels up · arm-gate before enabling drive.
Segway self-balancing via the QMI8658 IMU · voice / sound via the ES8311 speaker · richer per-eye gaze and expression.
This manual is generated from the Morpheus living docs (docs/) and firmware (src/config.h · control.h · motors.h). Source of truth is the repo; if code and manual disagree, trust the code.