back to AI portfolio

Two-board ESP32 lab reachable by chat

May 2026

TL;DR

  • What: Stood up a permanent two-board ESP32 lab (boards wired to the host laptop, ESP-IDF in a container, and an always-on agent reachable from my phone), then validated the radio link with 1 Hz ESP-NOW pingpong and RSSI-based distance estimation.
  • Outcome: Lab is live and the workflow works. Pingpong ran 100 frames at 100% reliability; RSSI tracked relative distance changes correctly (~6 dBm drop when I moved a board from ~1 m to ~2 m), though absolute distance numbers were off.
  • Why interesting: The real artifact is the lab, not the protocols. Pingpong is just “hello world.” The point is having a bench that stays wired up between projects and an agent I can message (“how’s the test going?”) instead of having to sit down at the laptop to find out.
  • Lesson: Treating the bench as a persistent product, not a per-project setup, is the speed multiplier. The same is true for the agent: when it stays running and reachable over chat, the iteration loop stops being “open laptop, attach to serial, check logs” and becomes “send a message.”

What I built

The real artifact here is the lab, not the demos that prove it works. Two ESP32 DevKitC v1 boards stay wired to the host laptop over CP2102 USB-serial bridges. ESP-IDF 6.1.0 runs inside a Fedora Atomic toolbox container so the immutable host stays clean. A Hermes-based agent (my custom always-on chat gateway) runs on the host and is reachable from my phone over chat, so I can ask “what’s the responder showing?” or “rebuild and reflash” without sitting down at the bench. That setup is what I’ll reuse across every ESP32 project from here forward; pingpong and RSSI were the smoke tests that proved every piece is alive.

espnow_pingpong is the obvious starting point: one board sends a small frame over ESP-NOW every second, the other receives it, both log to USB serial. ESP-NOW has a reputation for fragility in its official examples, and the first build confirmed it. Sender and receiver had separately-defined frame structs that were silently misaligned, so every frame failed CRC validation with the unhelpful message “Receive error data.” The fix was to consolidate the struct into a shared header, mark it __attribute__((packed)) to disable compiler padding, and match the layout byte-for-byte. The second build ran 100 frames with zero errors.

espnow_rssi reuses the same two boards but adds a small Python script on the host that reads both serial streams and computes a real-time distance estimate using the standard log-distance propagation model: distance = 10^((A - rssi) / (10 * n)). The first version had a typo in the destination MAC: both boards were unicasting to themselves rather than to each other, so they were happily “transmitting” while not actually communicating. Switching to the broadcast MAC fixed it in one line.

What worked, what didn’t

  • Worked: Pingpong 100/100 frames after the struct fix. RSSI ranging tracked relative motion correctly: moving a board from ~1 m to ~2 m produced a ~6 dBm RSSI drop, which is what the log-distance model predicts. The persistent lab + chat-accessible agent worked exactly as I hoped: I checked in on RSSI runs from another room without touching the laptop.
  • Didn’t work: Absolute distances from RSSI were off. I’d picked n = 1.6 as the path-loss exponent, which is closer to free-space than the 2 to 4 range typical for indoor environments. Relative changes were fine; absolute readings would need either calibration in the actual environment or a higher n.
  • Hardware quirk: One of the two boards’ CP2102 USB-serial bridges went flaky after the first session. TX went silent after a successful flash, returned after a power cycle, eventually wouldn’t recover. Swapping in the spare board fixed it permanently. The corollary is that a two-board lab needs a third board on hand; without one you can’t distinguish “my code is broken” from “my hardware is broken.”

What I learned

The lab-as-persistent-artifact framing is the takeaway worth keeping. Most embedded tutorials assume you bring up the hardware for each project and tear it down at the end. For someone doing many small projects in sequence, that’s the wrong shape: the setup cost gets paid every time and dominates the actual learning. A bench that stays wired up between projects, with an agent that stays reachable over chat, makes “I have 20 minutes, let me check on the test” a viable mode of work instead of an hour-long context switch.

On ESP-NOW specifically: its error messages are uniformly unhelpful, and the only reliable debugging move is logging the raw frame bytes in hex on both ends from the start. Both bugs I hit here (struct misalignment, MAC typo) would have been caught immediately by hex-dump logging, and adding it earlier in the next project saved hours.