-
Notifications
You must be signed in to change notification settings - Fork 765
feat: add dual-board ESP-NOW architecture for Python script execution #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # Board B - MicroPython Executor | ||
|
|
||
| Board B is an ESP32 (or ESP32-S3) running MicroPython that receives Python scripts from Board A (MimiClaw) via ESP-NOW and executes them. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - ESP32 or ESP32-S3 board | ||
| - MicroPython firmware with ESP-NOW support (v1.20+) | ||
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Flash MicroPython firmware | ||
|
|
||
| Download the latest MicroPython firmware for your board from https://micropython.org/download/ | ||
|
|
||
| ```bash | ||
| # Erase flash | ||
| esptool.py --chip esp32s3 erase_flash | ||
|
|
||
| # Flash MicroPython (adjust firmware filename) | ||
| esptool.py --chip esp32s3 write_flash -z 0 ESP32_GENERIC_S3-20240602-v1.23.0.bin | ||
| ``` | ||
|
|
||
| ### 2. Upload scripts | ||
|
|
||
| ```bash | ||
| # Install mpremote | ||
| pip install mpremote | ||
|
|
||
| # Upload boot.py and main.py | ||
| mpremote cp boot.py :boot.py | ||
| mpremote cp main.py :main.py | ||
|
|
||
| # Reset the board | ||
| mpremote reset | ||
| ``` | ||
|
|
||
| ### 3. Get Board B MAC address | ||
|
|
||
| After uploading and resetting, Board B will print its MAC address: | ||
|
|
||
| ``` | ||
| Board B MAC: aa:bb:cc:dd:ee:ff | ||
| On Board A, run: set_espnow_peer aa:bb:cc:dd:ee:ff | ||
| ``` | ||
|
|
||
| ### 4. Configure Board A | ||
|
|
||
| On Board A's serial console: | ||
|
|
||
| ``` | ||
| set_espnow_peer aa:bb:cc:dd:ee:ff | ||
| restart | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. Board A's AI agent decides to run a Python script (via `run_python` tool) | ||
| 2. The script is sent to Board B over ESP-NOW (chunked, 243 bytes per packet) | ||
| 3. Board B receives and reassembles the script | ||
| 4. Board B executes the script with `exec()`, capturing stdout | ||
| 5. The result (stdout + any exceptions) is sent back to Board A | ||
| 6. Board A returns the result to the AI agent | ||
|
|
||
| ## Limitations | ||
|
|
||
| - ESP-NOW range: ~200m line of sight, ~50m indoors | ||
| - MicroPython memory: limited by Board B's available RAM | ||
| - No persistent state between script executions (each `exec()` gets a fresh namespace) | ||
| - No WiFi connection on Board B (WiFi STA is active but not connected, used only for ESP-NOW) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| """ | ||
| MimiClaw Board B (Executor) - boot.py | ||
| Initializes WiFi STA interface for ESP-NOW (no connection needed). | ||
| Prints MAC address for pairing with Board A. | ||
| """ | ||
|
|
||
| import network | ||
| import ubinascii | ||
|
|
||
| # Enable WiFi STA (required for ESP-NOW, but no need to connect) | ||
| sta = network.WLAN(network.STA_IF) | ||
| sta.active(True) | ||
|
|
||
| # Print MAC address for Board A configuration | ||
| mac = sta.config('mac') | ||
| mac_str = ubinascii.hexlify(mac, ':').decode() | ||
| print('Board B MAC:', mac_str) | ||
| print('On Board A, run: set_espnow_peer', mac_str) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,157 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MimiClaw Board B (Executor) - main.py | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Listens for Python scripts via ESP-NOW from Board A, executes them, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and sends back the result (stdout + exceptions). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Protocol: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Header (7 bytes): [type(1) | seq(2 LE) | total_len(4 LE)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Types: 0x01=SCRIPT_START, 0x02=SCRIPT_CHUNK, 0x03=SCRIPT_END | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 0x11=RESULT_START, 0x12=RESULT_CHUNK, 0x13=RESULT_END | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import espnow | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import network | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import struct | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- Constants --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MSG_SCRIPT_START = 0x01 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MSG_SCRIPT_CHUNK = 0x02 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MSG_SCRIPT_END = 0x03 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MSG_RESULT_START = 0x11 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MSG_RESULT_CHUNK = 0x12 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MSG_RESULT_END = 0x13 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| HEADER_SIZE = 7 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MAX_PAYLOAD = 250 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CHUNK_DATA = MAX_PAYLOAD - HEADER_SIZE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- ESP-NOW Setup --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sta = network.WLAN(network.STA_IF) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sta.active(True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e = espnow.ESPNow() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.active(True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Board A peer - will be set on first received message | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peer_mac = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def build_header(msg_type, seq, total_len=0): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return struct.pack('<BHI', msg_type, seq, total_len) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def send_result(mac, result_text): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Send result back to Board A in chunks.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = result_text.encode('utf-8') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total = len(data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| offset = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seq = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while offset < total: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunk_size = min(CHUNK_DATA, total - offset) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_first = (seq == 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_last = (offset + chunk_size >= total) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if is_first: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg_type = MSG_RESULT_START | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif is_last: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg_type = MSG_RESULT_END | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg_type = MSG_RESULT_CHUNK | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header = build_header(msg_type, seq, total if is_first else 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pkt = header + data[offset:offset + chunk_size] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.send(mac, pkt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print('ESP-NOW send error:', ex) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| offset += chunk_size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seq += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not is_last: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| time.sleep_ms(5) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Handle empty result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if total == 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header = build_header(MSG_RESULT_START, 0, 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.send(mac, header) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header = build_header(MSG_RESULT_END, 1, 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.send(mac, header) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def execute_script(code): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Execute Python code and capture stdout + exceptions.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| old_stdout = sys.stdout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| captured = io.StringIO() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sys.stdout = captured | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exec(code, {'__name__': '__main__'}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print('Error:', type(ex).__name__, '-', ex) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sys.stdout = old_stdout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return captured.getvalue() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- Main Loop --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print('Board B executor ready. Waiting for scripts...') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_buf = bytearray() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_total = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_seq = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| receiving = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while True: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mac, msg = e.recv(timeout_ms=1000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(msg) < HEADER_SIZE: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg_type, seq, total_len = struct.unpack('<BHI', msg[:HEADER_SIZE]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = msg[HEADER_SIZE:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Remember the peer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if peer_mac is None and mac is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peer_mac = bytes(mac) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.add_peer(peer_mac) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass # peer may already exist | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print('Paired with Board A:', ':'.join('%02x' % b for b in peer_mac)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if msg_type == MSG_SCRIPT_START: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_total = total_len | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_buf = bytearray(payload) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_seq = seq + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| receiving = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif msg_type == MSG_SCRIPT_CHUNK and receiving: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if seq == script_seq: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_buf.extend(payload) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_seq = seq + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif msg_type == MSG_SCRIPT_END: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if payload: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_buf.extend(payload) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code = bytes(script_buf).decode('utf-8') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print('Received script ({} bytes), executing...'.format(len(code))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = execute_script(code) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print('Result ({} bytes): {}'.format(len(result), result[:100])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if peer_mac: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send_result(peer_mac, result) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Reset state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_buf = bytearray() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| receiving = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+142
to
+157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add
🛡️ Proposed fix- elif msg_type == MSG_SCRIPT_END:
+ elif msg_type == MSG_SCRIPT_END and receiving:
if payload:
script_buf.extend(payload)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add language specifiers to fenced code blocks.
The code blocks at lines 42 and 51 are missing language specifiers. Adding
textorconsoleimproves rendering consistency and satisfies markdown linting.📝 Proposed fix
Verify each finding against the current code and only fix it if needed.
In
@executor/README.mdaround lines 42 - 54, Add language specifiers to the twofenced code blocks containing the "Board B MAC: aa:bb:cc:dd:ee:ff" / "On Board
A, run: set_espnow_peer aa:bb:cc:dd:ee:ff" example and the "set_espnow_peer
aa:bb:cc:dd:ee:ff" / "restart" commands by changing the opening backticks from
totext (or ```console) so the blocks render consistently; update theblocks that include the "Board B MAC" line and the block with "set_espnow_peer"
and "restart" accordingly.