A swarm robotics platform built around autonomous chess. The system runs 34 small differential-drive robots across an 8x8 board, each independently localized and coordinated by a central Python application running on a Raspberry Pi.
The design is general-purpose — chess is the primary use case, but the platform works for any scenario requiring coordinated movement of many small autonomous robots in a bounded space.
Per-bot hardware cost is approximately $4 excluding PCB.
- System Architecture
- Hardware
- Communication Pathways
- Server-Bot Sync
- Localization
- PCB and CAD
- Firmware
The system has three layers:
Coordinator is a Python/PyQt6 application running on a Raspberry Pi. It holds board state, runs path planning, drives the GUI, and sends motion commands to the Server over USB serial.
Server is an ESP32 on the motherboard. It acts as a wireless gateway: it receives CSV commands from the Coordinator over USB serial, translates them to binary ESP-NOW messages, and broadcasts them to the bots. It also drives the electromagnets used for localization and collects ACK/NACK responses back up to the Coordinator.
MiniBots are the robots themselves. Each runs an ESP32-C3 with five concurrent FreeRTOS tasks handling motion control, wireless communication, position estimation, and battery management entirely onboard.
| Component | Details |
|---|---|
| MCU | ESP32-C3 (RISC-V, 2.4 GHz WiFi) |
| Motors | 2x PMO8-2 miniature stepper motors |
| Motor drivers | STSPIN220 |
| Magnetometer | MMC5633NJL (I2C) |
| Power | ~170 mAh LiPo |
| Chassis | Fully 3D printed; 2x M2x5mm bolts, 2x 7mm ID o-rings for tires |
| PCB | Custom 4-layer; fits in the base of the chassis |
| Component | Details |
|---|---|
| MCU | ESP32-C3 (RISC-V, 2.4 GHz WiFi) |
| Electromagnets | Multiple, positioned at fixed known coordinates on the board |
| Interface | USB serial to Raspberry Pi |
| WiFi | Soft AP; bots connect on a dedicated channel |
The electromagnets fire in a timed sequence that the MiniBots detect with their onboard magnetometers. This is the primary localization mechanism.
flowchart TD
subgraph Coordinator["Coordinator (Raspberry Pi)"]
GUI["GUI / Path Planner"]
SH["SerialHandler\n(QThread)"]
GUI -->|MoveCommand list| SH
end
subgraph Server["Server (Motherboard ESP32)"]
ST["SerialTask"]
CT["CommunicatorTask"]
ET["ElectromagnetTask"]
ST -->|CommandMessage| CT
CT -->|GUIStatus| ST
end
subgraph Bots["MiniBot #1 ... #34 (ESP32-C3)"]
COMM["EspNowCommunicator"]
KIN["KinematicsController"]
PE["PositionEstimator"]
COMM -->|MotionCommand| KIN
COMM -->|sync time| PE
end
SH -->|"Serial CSV Commands"| ST
ST -->|"Serial CSV Responses"| SH
CT -->|"ESP-NOW\nbroadcast / unicast Commands"| COMM
COMM -->|"ESP-NOW unicast\nACK / NACK"| CT
ET -.->|"electromagnetic\nframe pulses"| PE
All messages are prefixed with >. The Server parses these and forwards the
appropriate ESP-NOW message to the targeted bot(s).
| Type | Direction | Format |
|---|---|---|
| 0 | Host to ESP32 | >0,{id},{mode},{duty1},{duty2} -- motor test |
| 1 | Host to ESP32 | >1,{id},{x},{y},{theta_rad},{duration_ms} -- position command |
| 2 | Host to ESP32 | >2,{id} -- position request |
| 3 | ESP32 to Host | >3,{id},{x},{y},{theta_rad},{timestamp_ms},{battery_v} -- ACK |
| 4 | ESP32 to Host | >4,{id},{err_type},{timestamp_ms} -- NACK |
| 5 | Host to ESP32 | >5,{id} -- mag field request |
| 6 | ESP32 to Host | >6,{id},{bx},{by},{bz},{timestamp_ms} -- mag field response |
| 7 | Host to ESP32 | >7 -- sync broadcast |
| 254 | Host to ESP32 | >254,{0 or 1} -- electromagnet enable/disable |
| 255 | Both | >255 -- ping/pong |
Theta is transmitted in radians on the wire. The Coordinator converts internally; all application-level code uses degrees.
Binary packed structs defined in ESPNowMessages.h (Server) and
messages_espnow.h (MiniBot). Key message types: PositionCommand,
MotTestCommand, PositionRequest, PosSync, AckMessage, NackMessage,
MagneticFieldRequest, MagneticFieldResponse.
Before a localization measurement can be taken, every MiniBot needs to know precisely when the electromagnet frame will begin so it can timestamp its magnetometer samples against a common reference.
Initiating a sync is triggered by the Coordinator sending a >7 sync command.
The Server's CommunicatorTask handles it as follows:
- A
PosSyncCommandis broadcast to all bots. The message includes a timeout indicating how long the bots should stay awake listening for the frame. - After a short delay, the Server sends a rapid burst of
PosSyncpulses. Each carries anext_frame_usfield indicating how many microseconds until the electromagnet frame starts. - The
ElectromagnetTaskfires the frame immediately after the burst completes.
On each MiniBot, when PosSyncCommand is received, EspNowCommunicator sets
a deadline and raises waiting_for_pos_sync. The ESP-NOW receive callback captures
a high-resolution timestamp the instant each PosSync pulse arrives, before any
further processing. The task keeps only the earliest-arriving pulse from the burst —
minimum latency gives the best timing accuracy. The estimated frame start is
receive_time + next_frame_us from that best pulse.
Once the deadline passes, the bot calls PositionEstimator_SetSyncTime() with the
estimated frame-start, then sends an ACK with a small random delay to spread
simultaneous responses from many bots. If no pulse arrived in time, it sends
ERR_SYNC_TIMEOUT.
Radio duty cycling is tied to sync health. After a successful sync, each bot enables duty cycling to save power. If sync becomes stale, the radio reverts to always-on so the Server can reach the bot to re-establish sync.
The Server fires each electromagnet in sequence as part of a repeating frame. Each slot has a brief forward and reverse pulse to produce a detectable field, with a known timing offset between slots.
On each MiniBot, PositionEstimator_SensorTask reads the magnetometer
continuously. Once a valid sync time is set, it timestamps each sample relative to
the frame start and assigns samples to their electromagnet slot by timing alone.
Once a complete frame is collected, PositionEstimator_CalcTask performs
trilateration using the field strength readings from the closest electromagnets and
their known board positions. This yields an (x, y, orientation) estimate with a
confidence score, which is used to weight a low-pass filter before the result is
committed to the robot's true pose via robot.setTruePose().
Current board designs are in pcbs/:
pcbs/MiniBot_MainBoard/-- MiniBot PCB (ESP32-C3, stepper drivers, magnetometer, LiPo charger)pcbs/Server_Motherboard/-- Motherboard (ESP32-C3, electromagnet drivers, connectors)
Gerber files are in the GERBERS/ subdirectory of each board folder.
CAD (OnShape): https://cad.onshape.com/documents/4f8eaef75458146767928ab5/w/f159d6d65b9091531e1ead34/e/13b51722310f27710438c727
Each firmware project has its own README with task breakdowns, key objects, and build instructions.
firmware/MiniBot_Coordinator/-- Python/PyQt6 host applicationfirmware/MiniBot_Server/-- ESP32 motherboard firmware (PlatformIO)firmware/MiniBot_StepperClient/-- ESP32-C3 per-bot firmware (PlatformIO)