Skip to content

MicroPython WebREPL: raw REPL input buffer limit causes silent timeout for commands >255 bytes #1

Description

@bakedpanda

Summary

When sending commands to an ESP32 via MicroPython WebREPL, any command longer than 255 bytes silently times out. The board never executes the command and the connection hangs until the client's socket timeout fires. This manifests as a wifi_timeout error even though the board is reachable and WebREPL is running.

This is not obvious because short commands work perfectly, and there is no error message from the board — it just never responds.


Root Cause

MicroPython's raw REPL mode uses a ~256-byte input buffer. When sending a command over WebREPL, the client breaks the command into WebSocket frames. In this project's implementation, commands are sent in 64-byte chunks (separate WebSocket frames) followed by a Ctrl-D (0x04) frame to trigger execution.

  • A 255-byte command → 4 chunks (64 + 64 + 64 + 63 bytes) → Ctrl-D arrives → works
  • A 256+-byte command → 5+ chunks → the 5th chunk (and Ctrl-D) never gets processed → hangs

The ESP32 fills its 256-byte buffer and stops reading. Ctrl-D never arrives. The board waits forever for more input while the client waits forever for a response.

This is not a WebSocket frame size issue. Large binary frames work fine (e.g. OTA uploads use 1024-byte chunks). The limit is specific to the raw REPL input buffer.


Isolation Testing

We isolated the cause through binary-search testing using webrepl_exec directly on the Pi (same network path as production):

Command Size Result
print(1) 9 bytes ✅ works
import network; w=WLAN; print(w.isconnected()) 62 bytes ✅ works
import json,network,os + ifconfig + statvfs + json.dumps 150 bytes ✅ works
import json,sys,network,os + dhcp_hostname + firmware + statvfs 255 bytes ✅ works
Same + gc.mem_free() (no gc.collect) 280 bytes ❌ timeout
Same + gc.collect() + gc.mem_free() 293 bytes ❌ timeout
Full multi-line STATUS_SCRIPT 755 bytes ❌ timeout

The cutoff is between 255 and 280 bytes. 255 bytes = exactly 4 × 64-byte chunks. 280 bytes = 5 chunks. The threshold aligns precisely with the 256-byte buffer boundary.

We also confirmed gc.collect() alone was not the cause — removing it still timed out at 280 bytes. It's purely the byte count.


Fix

Add a WiFi-specific script (STATUS_SCRIPT_WIFI) that stays within the 256-byte limit. The key techniques:

  • os.uname()[2] instead of ".".join(str(x) for x in sys.version_info[:3]) — gives the MicroPython release version (e.g. 1.27.0) and saves ~17 bytes
  • network.WLAN(0) instead of network.WLAN(network.STA_IF)STA_IF == 0, saves 14 bytes
  • Inline all values directly in json.dumps() instead of assigning intermediate variables
  • Drop gc/free_memoryimport gc + gc.mem_free() adds ~36 bytes and pushes over the limit
  • No sys import needed without the version_info approach

Final script (240 bytes, 16 bytes of headroom):

STATUS_SCRIPT_WIFI = (
    'import json,network,os;W=network.WLAN(0);c=W.isconnected();s=os.statvfs("/");'
    'print(json.dumps({"firmware":os.uname()[2],"wifi_connected":c,'
    '"ip_address":W.ifconfig()[0]if c else"",'
    '"board":W.config("dhcp_hostname"),"free_storage":s[0]*s[3]}))'
)

This is only used on the WiFi path. The USB path continues to use the full multi-line STATUS_SCRIPT (no size constraint over serial).

Fix committed in 4f60d4c.


Affected Versions

Tested on:

  • MicroPython v1.27.0 on ESP32-S3
  • Python 3.x client on Raspberry Pi

General Advice for Other Projects

If you're sending commands to an ESP32 via WebREPL and hitting silent timeouts:

  1. Measure your command in bytes (len(command.encode()))
  2. Keep WiFi commands under 256 bytes
  3. Use os.uname()[2] instead of sys.version_info for firmware version
  4. Use WLAN(0) instead of WLAN(network.STA_IF)
  5. Inline values in json.dumps() rather than assigning variables first
  6. If you need gc.mem_free(), add it as a separate WebREPL call

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions