Skip to content

Web Sockets


There are two separate WebSocket endpoints. They use different URI paths and subprotocols; a client must use the correct pair. Negotiating the wrong subprotocol or path means the connection is handled by the wrong service — file transfer will not work.

EndpointTypical URI (ESP-IDF)Subprotocol (Sec-WebSocket-Protocol)Role
Terminal / WebUI/wswebui-v3BINARY = raw machine/serial bytes (no V1 header). TEXT = PING, NOTIFICATION:, currentID:, etc.
Data (external tools)/wsdataesp3d-v1ESP commands + G-code in TEXT; BINARY = V1 status and file upload/download (SR, SU, …).
    

Common mistake: sending V1 binary opcodes on /ws. The server treats binary as stream data, not file-transfer frames — transfers are impossible on that socket by design.


Implementation differences: ESP-IDF vs Arduino

Section titled “Implementation differences: ESP-IDF vs Arduino”

Depending on the underlying HTTP server framework, the deployment of these two endpoints differs:

  • ESP-IDF (httpd_ws): Both /ws and /wsdata operate on the same HTTP port (port 80). The server routes connections to the correct handler strictly based on the requested URI path.
  • Arduino (WebSocketsServer): The server cannot natively multiplex WebSockets on the main HTTP port 80. Instead, ESP3D creates two separate TCP listeners on different ports. Typically, the Terminal WebSocket uses HTTP_PORT + 1 (e.g., 81), and the Data WebSocket uses the port configured in settings (ESP_WEBSOCKET_PORT, typically 82). The URI path (/ws vs /wsdata) is ignored; the client must connect to the correct port directly.

1. Terminal WebSocket — /ws, subprotocol webui-v3

Section titled “1. Terminal WebSocket — /ws, subprotocol webui-v3”

Reserved messages between WebUI and ESP.
Format: <label>:<message>

ESP → WebUI:

  • currentID:<id>
    Sent when client connects. It is the last used ID and becomes the active ID.

  • activeID:<id>
    Broadcast when a new client connects. Clients without this ID should close.
    The ESP WS server closes all open WS connections except this one.

  • PING:<time left>:<time out>
    When authentication is enabled: response to a PING:<sessionId> from the client, informing the time remaining before inactivity timeout.

  • PONG:<server_millis>
    When authentication is disabled (or client sends bare PING without sessionId): firmware answers with a monotonic millisecond value. Used as keepalive reply.

  • ERROR:<code>:<message>
    Raised during upload when the HTTP answer cannot cancel it.
    This is a workaround: there is no API in the current webserver to cancel an active upload.

  • NOTIFICATION:<message>
    Forwards the message sent by [ESP600] to the WebUI toast system.

  • SENSOR: <value>[<unit>] <value2>[<unit2>] …
    Sensor data (e.g. DHT22 temperature/humidity).

WebUI → ESP:

  • PING:<sessionId>
    When authentication is enabled: keepalive ping sending the current session cookie ID.

  • PING:0
    Optional keepalive without authentication; ESP answers PONG:<server_millis>.

  • Clients should read TEXT frames from the server (PONG:…, currentID, stream lines, etc.).
  • On ESP-IDF 5.4.x, WebSocket DATA handlers may see req->content_len == 0 while a TEXT frame is still pending; the firmware must use httpd_ws_recv_frame() to read it, otherwise client PING is never handled and heartbeats fail.

ESP → client: payloads are raw bytes from the CNC/serial stream forwarded to the WebUI. There is no application header — not the 4-byte opcode + u16 length layout used on /wsdata.

How the embedded WebUI decodes binary (embedded/src/index.js): each byte is mapped with String.fromCharCode(byte) (ISO-8859-1 style). Buffered until \n (0x0A) or \r (0x0D), then flushed as a line to the console.

Client → ESP: inbound binary on /ws is not interpreted (stub). User commands and G-code are sent as TEXT frames with \n/\r line discipline.


2. Data WebSocket — /wsdata, subprotocol esp3d-v1

Section titled “2. Data WebSocket — /wsdata, subprotocol esp3d-v1”

This endpoint carries the ESP3D WebSocket Protocol V1: binary file-transfer and status frames, plus a text channel for ESP commands.

In scopeOut of scope
Binary frames: status, upload, download, abortTerminal WebSocket (webui-v3)
Text channel on the same socket (ESP commands, listings)HTTP upload / WebDAV
Ordering, timeouts, capability probeWebSocket session / cookie auth (future)
  

V1 is specified for the no-WebSocket-auth case: after connect, the client reads the welcome TEXT frame, then may send binary SR or text commands without tokens.

Builds with ESP3D_AUTHENTICATION_FEATURE may expose /wsdata without the same checks as HTTP until a future revision documents the handshake. Simplest interop: open socket → welcome → SR / SU / … with no extra steps.

V1 is identified explicitly when the server answers SR while idle: see Capability probe below. Future revisions may increment the version byte or add opcodes; clients must ignore unknown opcodes if they only need a subset.

StepWhat it validatesFailure means
(1) WebSocket handshakeTCP + HTTP upgrade to /wsdata with esp3d-v1 negotiatedWrong host/port, 404, subprotocol rejected — no data channel
(2) Status command SR → RSAfter socket open, send binary SR and receive RS within timeoutEndpoint open but no V1 binary handler — do not infer V1 from handshake alone
   

The server sends one TEXT frame first: a welcome line (Welcome to ESP3D-X V…). Clients must read it before assuming the next message is a command reply.

Used for ESP commands and G-code relay (e.g. [ESP720] list flash, [ESP740] list SD). Use json=yes for directory listings — one JSON object, faster and unambiguous. Plain-text listings end with Total:… / Available:… lines without a final ok.

Line termination: client TEXT is accumulated until \n or \r. A frame without a line ending is received but does not run the command. Tools should send [ESP720]…\n.

Flash paths: [ESP720] takes paths relative to the flash logical root; use / for root.

Packet data size defaults to 1024 bytes (ESP3D_WS_TRANSFER_PACKET_SIZE).

All frames share the same header:

Byte 0Byte 1Byte 2–3Byte 4+
Opcode MSBOpcode LSBPayload length (uint16_t LE)Payload
  • Opcode: 2 ASCII bytes identifying the frame type (e.g. SR, UP).
  • Payload length: number of bytes that follow the header, little-endian uint16_t.
  • All multi-byte integer fields in the payload are little-endian.

Start / ACK status byte (first byte of US, DS, PU, UE where applicable):

ByteHexMeaning
O0x4Fok / idle
E0x45error
B0x42busy
A0x41abort
   

Client → ESP (no payload):

SR0x000x00

ESP → Client:

Response to SR. First payload byte = transfer state:

ByteHexMeaning
O0x4Fidle / ok
B0x42busy
E0x45error
A0x41abort
D0x44download ongoing
U0x55upload ongoing
   

Idle (V1): payload = O (0x4F) + 1 (0x01) — protocol version byte.

RS0x020x00O1

Note: legacy servers may send only O (1-byte payload, no version).

Error:

After the status byte: 1 byte error code, 1 byte string length, then the string.

RSPL_LOPL_HIEERR_CODESTR_LENSTR…

Upload in progress:

After the status byte: path size (1 byte), path, filename size (1 byte), filename,
total file size (uint32_t LE), processed bytes (uint32_t LE), last packet ID (uint32_t LE).

RSPL_LOPL_HIUPATH_SZPATH…NAME_SZNAME…TOTAL(4)DONE(4)LAST_ID(4)

Download in progress:

Same layout as Upload ongoing, with D status byte.

RSPL_LOPL_HIDPATH_SZPATH…NAME_SZNAME…TOTAL(4)DONE(4)LAST_ID(4)

Client → ESP:

Payload: path size (1 byte), path, filename size (1 byte), filename, total file size (uint32_t LE).

SUPL_LOPL_HIPATH_SZPATH…NAME_SZNAME…FILE_SIZE(4)

ESP → Client (OK — transfer can start):

US0x010x00O

ESP → Client (Error — transfer rejected):

US0x010x00E

Client → ESP:

Payload: packet ID (uint32_t LE), then data bytes.

UPPL_LOPL_HIID(4)DATA…

ESP → Client (ACK):

Success (5 bytes): O + packet_id (uint32_t LE).

PU0x050x00OID(4)

Error (5 bytes): E + packet_id (uint32_t LE). If UP payload was too short to contain an id, id = 0xFFFFFFFF.

PU0x050x00EID(4)

Note: clients may treat PU with length 1 and E only as a legacy pre-correlation response.


Sent by the client after the last upload packet.

Client → ESP (no payload):

EU0x000x00

ESP → Client (OK):

UE0x010x00O

ESP → Client (Error):

UE0x010x00E

Client → ESP:

Payload: path size (1 byte), path, filename size (1 byte), filename.

SDPL_LOPL_HIPATH_SZPATH…NAME_SZNAME…

ESP → Client (OK — transfer can start):

Payload: status O, then total file size (uint32_t LE).

DS0x050x00OFILE_SIZE(4)

ESP → Client (Error):

DS0x010x00E

ESP → Client:

Payload: packet ID (uint32_t LE), then data bytes.

DPPL_LOPL_HIID(4)DATA…

Client → ESP (ACK):

PD0x050x00OID(4)

Sent by the ESP after the last download packet.

ESP → Client (no payload):

ED0x000x00

Client → ESP (ACK):

DE0x010x00O

Client → ESP (1-byte payload: command code):

CM0x010x00CMD

Command codes:

CodeHexMeaning
A0x41Abort current transfer

Abort frame:

CM0x010x00A

Clients must assume no binary file-transfer support until check (2) succeeds, even if (1) succeeded.

  1. After WebSocket is open, consume the mandatory welcome TEXT frame.
  2. Send binary SR with empty payload (header only: S, R, length 0 LE).
  3. Wait for a binary response (recommended timeout: 2–5 s).
  4. Interpretation:
    • No binary RS before timeout → no binary support (wrong endpoint/subprotocol or old firmware).
    • Binary RS with non-empty payload → binary status channel present; first payload byte = transfer state.
    • Idle (O), payload ≥ 2 bytes: byte 1 = protocol version; value 1 = V1 as defined here.
    • Idle (O), payload 1 byte: legacy server — clients may still attempt V1 but explicit version byte is preferred.

ESPClientESPClientloop[Packet transfer]SU (path, name, size)US (O)UP (id=N, data)PU (O, id=N)EUUE (O) ESPClientESPClientloop[Packet transfer]SD (path, name)DS (O, file_size)DP (id=N, data)PD (O, id=N)EDDE (O) ESPClientESPClientCM (A)

Connection:

  • Path: /wsdata (distinct from /ws).
  • Subprotocol: negotiate esp3d-v1. Reject clients offering only webui-v3 for this URI.
  • Send one TEXT welcome frame immediately after accept.

Minimum V1 behavior:

  1. SR → RS — idle: O + version 1 (2-byte payload). Upload/download in progress: state byte + path + filename + sizes.
  2. Upload sequence — accept SU, UP, EU; reply US, PU (5-byte payload), UE.
  3. Download sequence — accept SD; reply DS with file size, then DP / ED; client sends PD / DE.
  4. Concurrency — one active transfer per connection; return B on US/DS if busy.
  5. Text channel — same socket carries TEXT frames for [ESP720]/[ESP740] and G-code.

Error handling:

  • Invalid opcode: log/drop; optional TEXT error is product-specific.
  • Payload too short: respond with E on the relevant ACK if the protocol defines one for that step.

Terminal window
python tools/websocket/ws_transfer_test.py --host <ip> --port <port> status
python tools/websocket/ws_transfer_test.py --host <ip> --port <port> roundtrip <file> /fs

Testing tools are located in the tools/websocket directory of the ESP3D repository.

wsterm.html client interface

wsterm.html , websocket client test


  • Server: main/modules/websocket_server/esp3d_ws_data_service.cpp / .h
  • Constants: WS_OP_*, WS_STATUS_*, WS_BINARY_PROTOCOL_V1
  • Packet size: ESP3D_WS_TRANSFER_PACKET_SIZE

The pendant’s WebSocket client build targets text streaming to the CNC; it does not implement the V1 binary file-transfer client. Third-party servers on the machine tool can implement this document so PC or phone clients perform file transfer over ws://.