ProjekteEmbedded

AD8232-EMG

Wireless ESP32 + AD8232 surface EMG system with real-time WiFi streaming, OTA updates, automatic calibration, and a lightweight Python GUI for live signal monitoring.

Repository
13
Sterne
2
Forks
Mai 2026
Erstellt
vor 5 Monaten
Letzter Push

Readme

Dieses Readme enthält 7 Bilder von GitHub. Sie laden erst nach einem Klick unten.

AD8232 EMG — ESP32-C5 WROOM + OTA

Firmware: ESP32-C5 WROOM Channels: 2 Signal Path: AD8232 to WiFi TCP Backend: Python Stdlib API GUI: Tkinter and Matplotlib Updates: OTA Preferred

Safety

Note

Use OTA for normal firmware updates whenever possible. Avoid connecting the EMG setup directly to a PC over USB while it is attached to the body, because ground faults, wiring mistakes, or accidental shorts can create an unnecessary safety risk. Use USB only for the initial flash and prefer battery-powered operation during measurements.

Modular multi-channel surface EMG using one or more AD8232 modules and an ESP32-C5 WROOM. Channel count, pin assignment and labels are defined in a single table in firmware/emg_ota/config.h — add a row to add a channel; the API and GUI pick up the new layout automatically. Data streams over WiFi to a Python GUI. The ESP32-C5 WROOM runs on a USB powerbank; flashing is done wirelessly via OTA after the first USB flash.

See also: Roadmap · Single-Channel EMG Guide · Multi-Channel EMG Guide · Changelog · API Reference · License

EMG Monitor GUI

Wiring — 2× AD8232 → ESP32-C5 WROOM

Each AD8232 module connects to 3V3, GND, SDN (tied high) and three GPIOs. The current two-channel configuration for the ESP32-C5 WROOM:

AD8232 (CH0 — FCR, flexor)       ESP32-C5 WROOM
─────────────────────────────    ──────────────────────────────────────
3.3V    ──────────────────────── 3.3V       (shared rail)
GND     ──────────────────────── GND        (shared rail)
OUTPUT  ──────────────────────── GPIO 4     (ADC1_CH4 — EMG signal CH0)
LO+     ──────────────────────── GPIO 6     (lead-off detect +, CH0)
LO-     ──────────────────────── GPIO 7     (lead-off detect −, CH0)
SDN     ──────────────────────── 3.3V       (always on)

AD8232 (CH1 — ED, extensor)      ESP32-C5 WROOM
─────────────────────────────    ──────────────────────────────────────
3.3V    ──────────────────────── 3.3V       (shared rail)
GND     ──────────────────────── GND        (shared rail)
OUTPUT  ──────────────────────── GPIO 5     (ADC1_CH5 — EMG signal CH1)
LO+     ──────────────────────── GPIO 8     (lead-off detect +, CH1)
LO-     ──────────────────────── GPIO 10    (lead-off detect −, CH1)
SDN     ──────────────────────── 3.3V       (always on)

GPIO 0–3 avoided for ADC output: GPIO 0 carries a BOOT pull-down on most dev boards which corrupts readings. GPIO 9 avoided for LO pins: it is the BOOT/strapping pin with an internal pull-up that causes permanent LEAD_OFF.

ESP32-C5 ADC constraint: only ADC1 pins (GPIO 0–6) work while WiFi is active. Never wire AD8232 OUTPUT to any other GPIO. LO+ / LO- are digital inputs and accept any free GPIO.

Adding or removing modules

Edit firmware/emg_ota/config.h. One row = one module. Update NUM_CHANNELS to match. Re-flash. The API and GUI discover the new layout automatically.

#define NUM_CHANNELS 2   // ← change this number

static const ChannelPins CHANNELS[NUM_CHANNELS] = {
    {  4,  6,  7, "FCR" },   // CH0 — forearm flexor   (palm-up,   near elbow)
    {  5,  8, 10, "ED"  },   // CH1 — forearm extensor (palm-down, near elbow)
 // {  2, 11, 12, "FCU" },   // CH2 — ulnar flexor              (uncomment to add)
 // {  3, 13, 14, "BRD" },   // CH3 — brachioradialis / radials (uncomment to add)
};

Full ADC1 pin reference (ESP32-C5)

Channel GPIO Use
ADC1_CH0 GPIO 0 avoid — BOOT pull-down on dev boards
ADC1_CH1 GPIO 1 avoid — strapping pin on some boards
ADC1_CH2 GPIO 2 CH2 OUTPUT (future)
ADC1_CH3 GPIO 3 CH3 OUTPUT (future)
ADC1_CH4 GPIO 4 CH0 OUTPUT
ADC1_CH5 GPIO 5 CH1 OUTPUT
ADC1_CH6 GPIO 6 digital only here (LO+ CH0)

Electrode Placement (forearm, surface EMG)

Each AD8232 module has three leads: YELLOW (LA+), RED (RA−), GREEN (RL ref). Clean skin with alcohol before attaching. The two active electrodes must be aligned along the local muscle fiber direction, 2–3 cm apart.

CH0 — FCR / flexor side (palm up)

Palm facing UP — inner forearm (volar side)

  Elbow                                          Wrist
    │                                              │
────┤███████████████████████████████████████───────┤────
    │                                              │
    │  [YELLOW LA+]    [RED RA-]      [GREEN RL]   │
    │       │               │              │       │
    │   ~5 cm from      ~3 cm toward   wrist bone  │
    │   elbow           wrist          (ulnar       │
    │   (muscle belly)                 styloid)     │
Cable Pin Position
YELLOW LA+ Muscle belly, ~5 cm from elbow, inner forearm
RED RA− 2–3 cm toward wrist from YELLOW, same line
GREEN RL Wrist bone (ulnar styloid) — bony, no muscle

CH1 — ED / extensor side (palm down)

Palm facing DOWN — outer forearm (dorsal side)

  Elbow                                          Wrist
    │                                              │
────┤███████████████████████████████████████───────┤────
    │                                              │
    │  [YELLOW LA+]    [RED RA-]      [GREEN RL]   │
    │       │               │              │       │
    │   ~4 cm from      ~3 cm toward   wrist bone  │
    │   elbow           wrist          (radial      │
    │   (ED belly,                     styloid)     │
    │    outer forearm)                             │
Cable Pin Position
YELLOW LA+ Extensor digitorum belly, ~4 cm from elbow, outer forearm
RED RA− 2–3 cm toward wrist from YELLOW, same line
GREEN RL Wrist bone (radial styloid) — bony, no muscle

The GREEN reference electrode can be shared between both modules if you place it on a neutral bony site (e.g. ulnar styloid). Use a separate wire from a single electrode to both RL inputs. This reduces electrode count from 6 to 5.


Setup

1. Credentials

cp .env.example .env
# edit .env — fill in WIFI_SSID, WIFI_PASSWORD, OTA_PASSWORD

2. First flash (USB required once)

./flash.sh

Use USB for the first flash only, with the electrodes disconnected from the body.

3. All subsequent flashes (OTA — powerbank only)

./flash.sh

flash.sh auto-detects: ESP32-C5 WROOM reachable via WiFi → OTA, otherwise → USB fallback. OTA is the recommended update path for safety.

Override OTA host:

OTA_HOST=your-esp32.local ./flash.sh

Running

# terminal 1 — API server (connects to ESP32-C5 WROOM, exposes REST)
python3 emg_api.py

# terminal 2 — monitor + auto-launch GUI
./monitor.sh

monitor.sh starts the local API automatically on 127.0.0.1:5555 if it is not already running, then launches the GUI.

Or launch GUI standalone (requires API server running):

python3 gui/emg_gui.py

Calibration (in GUI)

Each channel is calibrated independently.

  1. Pick the channel from the CH dropdown
  2. Press CALIBRATE
  3. Relax that muscle → press START → hold still for 5 s
  4. Tense that muscle / make a fist → press START → hold for 5 s
  5. Per-channel threshold is set automatically and saved to calibration.json
  6. Calibration loads automatically on next start — no need to redo it

Use RESET ALL in the bottom bar to clear every channel's calibration in one go, or POST /channel/{id}/calibration/reset for a single channel.


Data Stream

The ESP32-C5 WROOM sends a self-describing CSV stream over TCP on port 8888:

#CH:2,FCR,ED         ← header sent on every new connection
2105,42.3,1890,38.1  ← one line per sample tick: rawN,rmsN per channel
2089,38.7,1875,35.2
LEAD_OFF:1           ← per-channel lead-off pulse, 10 Hz while detached
  • raw — 12-bit ADC value (0–4095, center ≈ 2048)
  • rms — RMS envelope over a 100 ms window (after DC tracker + 20 Hz HP filter)
  • header label list comes from CHANNELS[] in config.h

Project Structure

AD8232-EMG/
├── firmware/
│   └── emg_ota/
│       ├── emg_ota.ino     ESP32-C5 WROOM firmware (WiFi + OTA + EMG sampling)
│       ├── config.h        pin definitions, sampling config
│       └── secrets.h       generated by flash.sh — do not commit
├── gui/
│   └── emg_gui.py          live GUI with built-in calibration wizard
├── emg_api.py              HTTP REST API server (stdlib only, no pip)
├── monitor.sh              terminal monitor + auto-starts local API + GUI
├── flash.sh                compile + OTA/USB upload
├── .env                    WiFi credentials (gitignored)
├── .env.example            credential template
├── calibration.json        saved calibration (gitignored)
├── API.md                  REST API reference for AI agents
└── README.md

Ports

Service Port
EMG REST API 5555
ESP32-C5 WROOM TCP stream 8888
ESP32-C5 WROOM OTA 3232

Contact

arn-c0de@protonmail.com

License

Released under the MIT License.

Roadmap

docs/ROADMAP.md

Roadmap

Current Focus

Status Area Goal Notes
In Progress Multi-channel / Multi-module support Evolve the project from single-channel EMG to a modular, expandable architecture. Tracked in Issue #1.

Planned Expansion

Status Area Goal Notes
Planned Firmware Channel-aware acquisition and transport layer Start with dual-channel support, then scale further.
Planned API Structured multi-channel data model Add per-channel metadata and timestamps.
Planned GUI Multi-channel visualization and controls Add labeling, grouping, and per-channel thresholds.
Planned Signal Processing Cross-channel analysis and gesture detection Begin with simple multi-muscle features before advanced classifiers.
Planned Modularity Expandable sensor/module architecture Keep the design open for additional sensor types later.

Immediate Next Step

Priority Deliverable Reason
Next Dual-channel forearm EMG support Smallest useful step beyond the current single-channel implementation.

Ganze Datei auf GitHub

Änderungen

CHANGELOG.md

Changelog

All notable changes to this project are documented here.

[1.0.1] - 2026-05-14

Added

  • Multi-channel gesture classification mode.
  • GUI mode switcher between Threshold and Gesture detection.
  • Unified English UI labels.

Multi-channel architecture

  • config.h: replaced fixed single-channel pin defines with a ChannelPins table. One row now defines one AD8232 module, and NUM_CHANNELS controls the layout.
  • emg_ota.ino: firmware now processes all configured channels in a loop using per-channel filter, RMS window and lead-off state.
  • Wire protocol: the ESP sends a self-describing header #CH:<n>,<label0>,... on every new TCP connection, followed by packed CSV samples raw0,rms0,raw1,rms1,.... Lead-off is now channel-specific via LEAD_OFF:<idx>.
  • emg_api.py: REST API upgraded to a channel-aware model with GET /channels, GET /channel/{id}, GET /channel/{id}/threshold, POST /channel/{id}/threshold, POST /channel/{id}/record, GET /channel/{id}/calibration, POST /channel/{id}/calibration/reset and POST /calibration/reset.
  • Calibration persistence: calibration.json now stores threshold and recordings per channel, while legacy single-channel calibration files are still accepted and migrated on load.

ESP32-C5 WROOM migration

  • Hardware target changed to ESP32-C5 WROOM.
  • config.h: current default pin layout is CH0 OUT=GPIO 0 / LO+=GPIO 6 / LO-=GPIO 7, CH1 OUT=GPIO 1 / LO+=GPIO 8 / LO-=GPIO 9.
  • ADC note: on ESP32-C5 only ADC1 pins GPIO 0-6 are safe for analog sampling while WiFi is active.
  • flash.sh: board target updated from classic ESP32 to esp32:esp32:esp32c5; OTA upload helper path is resolved against newer arduino-esp32 3.x core installs.
  • Core requirement: moved from arduino-esp32 2.x to 3.3.8 for ESP32-C5 support.

GUI and plotting

  • gui/emg_gui.py: GUI layout is now fully dynamic and rebuilds automatically from the live channel header.
  • One combined raw plot is shown at the top, with one RMS subplot per channel below it.
  • Each channel now has its own threshold row, label, manual threshold input and live state display.
  • Threshold dragging works independently per RMS plot.
  • Calibration flow now supports selecting the active channel.
  • Multi-channel plot refresh was fixed so later channels such as CH1 are redrawn reliably after figure rebuilds instead of getting stuck on stale zero-state data.
  • RMS panels now show clearer live feedback with current-value titles, endpoint markers and filled RMS areas.
  • Global status banner now handles mixed states correctly, for example ACTIVE on one channel while another is LEAD OFF.

Signal and runtime fixes

  • Added guards in firmware against invalid RMS calculations and unstable square-root input.
  • Updated the analog setup for the new board target, including attenuation handling and safer channel behavior.
  • emg_api.py: root route GET / now returns API info instead of 404.
  • emg_api.py: standard deviation calculation was rewritten without statistics.stdev, improving Python 3.12 compatibility.

Documentation and assets

  • README.md: updated throughout for ESP32-C5 WROOM, multi-channel wiring, ADC constraints, runtime notes and terminology cleanup.
  • API.md: documents the channel-aware API and wire format.
  • images/main-gui.png: screenshot replaced with the current multi-channel GUI.

[1.0.1] — 2026-05-09

Signal Quality

  • Adaptive DC tracker replaces fixed 2048 offset — tracks electrode drift and ADC bias in real time (α = 0.0005, ~2 Hz)
  • IIR high-pass filter at ~20 Hz applied before RMS — removes motion artifacts and low-frequency noise so only the muscle-band signal enters the RMS window
  • RMS window reduced from 200 ms to 100 ms — faster muscle onset detection with lower latency
  • Backend smoothing reduced (SMOOTH_ALPHA 0.3 → 0.15) — firmware now handles heavy filtering; API smoothing no longer adds unnecessary latency

Firmware

  • WiFi connect loop now times out after 20 s and reboots instead of blocking forever
  • LEAD_OFF transmission throttled to 10 Hz in firmware and API — prevents flooding the TCP stream when electrodes are detached
  • rmsBuffer size is now derived from SAMPLES_PER_WINDOW at compile time instead of being hard-coded to 50

API (emg_api.py)

  • Switched from HTTPServer to ThreadingMixIn — /live requests are now served concurrently while /record is running, preventing broken pipe errors and GUI disconnects during calibration
  • /record now collects only unique samples by timestamp — eliminates duplicate-sample bias in calibration statistics
  • /threshold returns 400 Bad Request with a clear message on invalid float input instead of crashing with HTTP 500
  • ESP32 socket is explicitly closed and connected is set to False on EOF — no more stale connection state or tight reconnect loops

GUI (gui/emg_gui.py)

  • Rebuilt with native tkinter buttons — no more flickering or missed clicks from matplotlib widget redraws
  • Draggable threshold line in RMS plot (toggle with DRAG button) — poll loop no longer overwrites local threshold position during active drag
  • Manual threshold input field with SET button in side panel
  • RESET CAL button clears stored calibration without restarting
  • Canvas redraws only when new data arrives (dirty flag) — reduces CPU usage on slower systems
  • Exceptions logged to stderr instead of being silently swallowed

Monitor (monitor.sh)

  • --restart flag kills existing API and GUI processes before starting fresh
  • --stop flag cleanly stops API and GUI and exits
  • API server auto-started if not already running; GUI launched automatically
  • Fixed USB serial port detection (find /dev replaces broken ls glob)

Documentation

  • README rewritten in English with updated electrode placement diagram, calibration workflow, and project structure
  • API.md added — complete REST API reference for AI agents
  • GUI screenshot added to README
  • Contact section moved to bottom of README

[1.0.0] — 2026-05-09

Initial release.

  • ESP32 firmware with ArduinoOTA support — wireless flashing after first USB flash
  • Single AD8232 channel, forearm surface EMG
  • flash.sh — compiles with arduino-cli, flashes via OTA or USB fallback; WiFi credentials injected from .env at build time via generated secrets.h
  • emg_api.py — local HTTP REST API (stdlib only, no pip dependencies) with calibration persistence (calibration.json)
  • gui/emg_gui.py — live matplotlib plots embedded in tkinter
  • monitor.sh — terminal ASCII bar monitor
  • Calibration wizard: record relaxed and tense states, auto-compute threshold

Ganze Datei auf GitHub