Version: 2.0.0
Author: D. McLean / BufferRoot
Platform: M5Stack Tab5 (ESP32-P4 + ESP32-C6)
Stack: Zig 0.16 + ESP-IDF 6
Hackster: Modulus pendant · M5Stack GIC 2026
License: MIT
One Device, One Software. Real control for any machine — no lag, no brand lock-in, no compromise.
Handheld DRO + MPG client on Tab5 — Zig dual-core anti-lag firmware talking to grblHAL (and other engines) over ESP-NOW or RS-485. It does not replace your motion controller.
Most M5 projects cram UI and radio onto one busy chip. Modulus uses Tab5 as designed: P4 dual-core HMI/control, C6 for Wi-Fi/BLE/ESP-NOW, NanoH2 for Zigbee — so motion RF and shop-IoT RF never fight.
[Operator touch UI + MPG wheel]
│
▼
┌──────────────────── ESP32-P4 (Tab5 main MCU) ────────────────────┐
│ Core 0: LVGL 720p Material 3 UI · settings · audio (async) │
│ Core 1: heap-free ~100 Hz MPG poll · envelope · command stream │
└─────────┬──────────────────────┬───────────────────────┬─────────┘
│ SDIO2 │ UART GPIO6/7 │ UART1
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ ESP32-C6 │ │ NanoH2 (ESP32-H2) │ │ SIT3088 RS-485 │
│ Wi-Fi 6 / BLE / │ │ ZBOSS Zigbee hub │ │ wired field bus │
│ ESP-NOW │ │ shop automation │ │ │
└─────────┬─────────┘ └───────────────────┘ └───────────────────┘
│ ESP-NOW
▼
┌───────────────────┐
│ ESP32-S3 bridge │ ── UART ──► CNC controller (e.g. grblHAL)
└───────────────────┘
| Target | Job |
|---|---|
| P4 | Core 0 = UI · Core 1 = heap-free ~100 Hz MPG / envelope / streams |
| C6 | Wi-Fi 6, BLE, ESP-NOW only (never Zigbee-exclusive builds) |
| NanoH2 | Zigbee coordinator @ 460800 baud UART — vacuums, lights, fans |
| S3 bridge | Cabinet ESP-NOW → UART to controller |
Why Zig: no GC. Core 1 never touches the allocator, so a heavy Core 0 frame cannot stall the handwheel. Error unions + defer for deterministic transport cleanup. Zig owns state/jog/ABI (src/modulus/); C shims own IDF/BSP/LVGL.
| Shop failure | Fix |
|---|---|
| UI redraw delays jog | Dual-core — Core 1 never blocks on LVGL |
| Wi-Fi reconnect freezes pendant | ESP-NOW is connectionless — drop bad frame, run next |
| Bridge backlog under RF hit | Bounded S3 queue — stale frames drop (50 ms TTL) |
| 10 clicks ≠ 10 steps | envelope.zig clamp + NVS divider/polarity before send |
| UI refresh starves Core 0 | Dashboard timer floor ≥ 33 ms (never 16 ms under sw_rotate) |
Bench numbers (grblHAL + ESP-NOW + S3):
| Metric | Value |
|---|---|
| Wheel-to-motion latency | 2 ms |
| MPG poll (Core 1) | ~100 Hz |
| ESP-NOW RTT (P4↔S3) | 25 ms |
| Late-frame drop TTL | 50 ms |
| UI refresh floor @ 720p | ≥ 33 ms |
| ESP-NOW PHY default | 24M OFDM |
| Cold boot → live DRO | ~16 s |
| Runtime per pack | 8–10+ h (NP-F hot-swap) |
| Feature | Notes |
|---|---|
| Live multi-axis DRO + MPG | ExtEncoder; 0.001–1.0 mm detent steps |
| Overrides / hold / cycle / macros | Native commands — no HID keyboard hacks |
| Multi-engine client | grblHAL, Grbl, FluidNC, LinuxCNC, Mach3/4, Masso |
| Multi-transport | ESP-NOW, RS-485, USB serial, WebSocket, Telnet, BLE |
| Zigbee shop IoT | NanoH2 hub — Run can start dust extraction; Hold/Alarm spins down |
| Material 3 UI | Dark/light + accents · 10 settings tabs · Quick Settings · PIN lock |
| Power | INA226 (%, V, A, W) · NP-F hot-swap · PMIC soft shutdown |
Honest status: pendant stack complete; grblHAL over ESP-NOW (Tab5↔S3) field-verified. Full motion soak + live Zigbee soak still open. Other engines/transports implemented, not all field-verified. Keep the machine E-Stop in reach.
✅ field-verified · 🔧 implemented · ⏳ planned
| Transport | Path | Status |
|---|---|---|
| ESP-NOW | C6 → air → S3 → UART | ✅ |
| RS-485 | UART1 TX20 / RX21 / DE34 | 🔧 |
| USB serial | Direct USB / UART | 🔧 |
| WebSocket | C6 Wi-Fi | 🔧 |
| Telnet | C6 Wi-Fi | 🔧 |
| BLE | NimBLE on C6 | 🔧 |
| I2C | Bus peer | 🔧 |
| CAN | via COMMU | ⏳ |
| Engine | Typical transport | Status |
|---|---|---|
| grblHAL | ESP-NOW / RS-485 | ✅ |
| Classic Grbl | RS-485 / serial | 🔧 |
| FluidNC | WebSocket | 🔧 |
| LinuxCNC | Telnet | 🔧 |
| Mach3 / Mach4 | Telnet | 🔧 |
| Masso | WebSocket / UDP | 🔧 |
Pendant E-Stop is a convenience layer, not a safety-rated cutoff. It rides the active link (GPIO16 NO → feed-hold ! then soft reset). If the wireless link is down, pendant E-Stop will not stop the machine. Machine mushroom E-Stop is primary. Software: envelope.zig soft limits, zero-while-running confirm, link-offline warnings, low-battery MPG lockout.
Out of scope (roadmap): SC2356 camera UI · on-screen FFT · OTA dual-partition · PCNT GPIO quadrature (replace I²C ExtEncoder).
src/modulus/ Zig: state, jog math, cnc_proto, envelope, ABI
firmware/tab5/ P4 app: IDF/BSP/LVGL C shims + modulus_zig
firmware/tab5-c6/ C6 ESP-Hosted wireless slave
firmware/nanoh2/ H2 Zigbee coordinator (ZBOSS)
firmware/s3-bridge/ ESP-NOW → UART bridge
cad/ STL + CAD (enclosure / mounts)
schematics/ Schematics + wiring diagrams
assets/ README media (hero image)
scripts/ Build / flash helpers
tools/ NVS manifest generator (used by zig build)
LICENSE MIT
Download v2.0.0 assets (bootloader + partition table + app per target):
https://github.com/BufferRoot/Modulus-Firmware/releases/tag/v2.0.0
| Zip | Chip | What it is | Typical port |
|---|---|---|---|
modulus-tab5-p4-v2.0.0.zip |
ESP32-P4 | Pendant UI + control | COM5 (Tab5 USB) |
modulus-tab5-c6-v2.0.0.zip |
ESP32-C6 | ESP-Hosted / ESP-NOW radio | COM6 (C6 USB; hold BOOT if needed) |
modulus-nanoh2-v2.0.0.zip |
ESP32-H2 | Zigbee shop hub | NanoH2 USB-C (hold BUTTON) |
modulus-s3-bridge-v2.0.0.zip |
ESP32-S3 | Cabinet ESP-NOW → UART | COM8 (bridge board) |
SHA256SUMS.txt |
— | Checksums for every .bin |
— |
- esptool (
pip install esptool) or ESP-IDF 6idf.py/python -m esptool - USB cables + drivers for each chip
- Unzip each package into its own folder (commands below assume you
cdinto that folder)
Flash order (first bring-up): C6 → P4 → S3 bridge → NanoH2 (optional). Power-cycle the Tab5 after C6+P4. Keep the machine E-Stop in reach.
COM ports on your PC may differ — change -p COMx to match Device Manager.
Unzip modulus-tab5-c6-v2.0.0.zip, then:
cd path\to\tab5-c6
esptool.py --chip esp32c6 -p COM6 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 80m --flash-size 4MB `
0x0 bootloader.bin `
0x8000 partition-table.bin `
0xd000 ota_data_initial.bin `
0x10000 network_adapter.binIf the port never appears: hold BOOT on the C6 while plugging USB, then run the command.
Unzip modulus-tab5-p4-v2.0.0.zip, then:
cd path\to\tab5-p4
esptool.py --chip esp32p4 -p COM5 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 40m --flash-size 16MB `
0x2000 bootloader.bin `
0x8000 partition-table.bin `
0x10000 modulus_tab5.binPower-cycle the Tab5. Cold boot should show wireless / SDIO ready (not 0x107).
Unzip modulus-s3-bridge-v2.0.0.zip, then:
cd path\to\s3-bridge
esptool.py --chip esp32s3 -p COM8 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 80m --flash-size 8MB `
0x0 bootloader.bin `
0x8000 partition-table.bin `
0x10000 s3_espnow_uart_bridge.binWire S3 UART to your CNC (grblHAL) serial. On the Tab5: Settings → Wireless → ESP-NOW → enter S3 MAC, lock channel 1 / 6 / 11.
Enable EXT5V for Grove/H2 power. Unzip modulus-nanoh2-v2.0.0.zip, hold BUTTON on the Stamp, then:
cd path\to\nanoh2
esptool.py --chip esp32h2 -p COM7 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 48m --flash-size 4MB `
0x0 bootloader.bin `
0x8000 partition-table.bin `
0x10000 modulus_nanoh2.binUART to Tab5 is GPIO6 TX / GPIO7 RX @ 460800. Never flash C6 with Zigbee-exclusive builds — Zigbee stays on NanoH2.
- Compare downloaded
.binhashes toSHA256SUMS.txt - After P4+C6: idle dashboard ≥ ~55 s with no IDLE0 WDT
- ESP-NOW path: status shows Connected once S3 MAC/channel are set
Each zip also includes FLASH.md with the same offsets.
Prerequisites: Zig 0.16+ · ESP-IDF 6.0
git clone https://github.com/BufferRoot/Modulus-Firmware.git
cd Modulus-Firmware
zig build test # host logic + ABI parity
zig build tab5-lib # freestanding Zig library| Target | Command |
|---|---|
| Tab5 P4 | .\scripts\build_tab5.ps1 then .\scripts\flash_tab5.ps1 -Port COM5 |
| Tab5 C6 + P4 | .\scripts\flash_tab5_dual.ps1 -C6Port COM6 -P4Port COM5 (never -ZigbeeExclusive) |
| NanoH2 | idf.py -C firmware/nanoh2 flash (hold BUTTON; enable EXT5V) |
| S3 bridge | .\scripts\build_s3_bridge.ps1 -Action flash -Port COM8 |
Re-pack local builds into release zips: .\scripts\package_flash_images.ps1.
| Signal | Pin | Note |
|---|---|---|
| RS-485 TX / RX / DE | GPIO 20 / 21 / 34 | onboard SIT3088 |
| ExtEncoder MPG | Grove Port A (I2C) | powered via EXT5V_EN |
| Hardware E-Stop | GPIO16 (M5-Bus pin 2) | NO to GND, pull-up |
| NanoH2 UART | GPIO 6 TX / 7 RX | 460800 baud |
Tab5 · ExtEncoder + wheel · industrial NO E-Stop · Stamp NanoH2 · ESP32-S3 cabinet bridge · NP-F pack(s) · optional COMMU Module Extend.
Mechanical files: cad/ · Wiring / schematics: schematics/.
