Protocol Details
DEJA IO supports three communication protocols — USB serial, MQTT and WebSocket. All use JSON messages.
🔌 Serial USB
Direct wired connection between the DEJA Server and any of the USB-serial Arduino-family device types: deja-arduino (Arduino Mega) or deja-esp32 (ESP32 Dev Module). Both share the same firmware sketch and identical wire format.
| Parameter | Value |
|---|---|
| Baud rate | 115200 |
| Data bits | 8 |
| Stop bits | 1 |
| Parity | None |
| Encoding | UTF-8 JSON array |
How It Works
- The board connects via USB to the server machine
- DEJA Server auto-detects the serial port
- Server opens the port at 115200 baud
- JSON command arrays are written to the port; responses are read line-by-line
- Auto-reconnect with exponential backoff (1s → 30s max) on disconnect
Wire Format
Commands arrive as a JSON array (multiple commands can be batched per write):
[
{ "action": "pin", "payload": { "pin": 8, "state": 1 } },
{ "action": "servo", "payload": { "servo": 0, "value": 90, "profile": "medium" } }
]
Sensor state changes are published back over the same serial line as single objects (e.g. { "sensor": 0, "state": 1 }), debounced by SENSOR_DEBOUNCE_MS (500 ms unless set in the device's advanced config).
Heartbeat
The board says {"status":"connected","device":"<id>"} at boot and sends {"status":"alive",…} every 60 s. The server writes the bare line ping every 30 s, starting 3 s after it opens the port (or at once when it reads the boot hello), and the board answers pong. The board's USB-serial status LED goes from OK to failed after 60 s without hearing the server. Redeploy older firmware: it doesn't understand ping.
Port Detection
The server lists available serial ports and matches them to registered devices. You can also manually specify a port path in your device configuration.
📬 MQTT over WiFi
Wireless publish/subscribe messaging via an MQTT broker (built into the DEJA Server). The default transport for both wireless device types: deja-mqtt (Raspberry Pi Pico W, CircuitPython) and deja-esp32-wifi (ESP32, Arduino C++). Both share the same topic scheme and the same single-object command format — the DEJA Server treats them identically on the wire.
| Parameter | Value |
|---|---|
| Default port | 1883 (unencrypted) |
| Protocol | MQTT v3.1.1 |
| QoS | 0 (at most once) |
| Encoding | UTF-8 JSON object |
Topic Structure
{TOPIC_ID}/{LAYOUT_ID}/{DEVICE_ID} ← device subscribes (receives commands)
{TOPIC_ID}/{LAYOUT_ID}/{DEVICE_ID}/messages ← device publishes (sensor events, status)
TOPIC_ID is DEJA — the server always publishes on DEJA/…. Example for a device on the "tam" layout:
DEJA/tam/tj-eagle-nest-pico ← commands IN
DEJA/tam/tj-eagle-nest-pico/messages ← sensor events / status OUT
Wire Format
Commands arrive as single JSON objects (one command per MQTT message) with a device field for filtering:
{
"action": "pin",
"device": "my-esp32-wifi",
"payload": { "pin": 18, "state": 1 }
}
Sensor events the device publishes back to …/messages flow through the same pipeline as USB-serial sensors, so state updates show up in Throttle the same way.
Device Filtering
Multiple devices can share the same MQTT topic prefix. Each message includes a device field, and devices only process messages matching their own DEVICE_ID. This is true for both deja-mqtt and deja-esp32-wifi.
Broker Setup
Nothing to install — the DEJA Server runs its own MQTT broker on port 1883 whenever MQTT is enabled (the default). Point your devices at the server machine's LAN IP (the deploy wizard suggests it automatically, and deja config prints it).
Already running Mosquitto or another broker? The server detects a broker on the port (or an MQTT_BROKER pointing at another machine) and connects to it instead. Set MQTT_EMBEDDED=false in ~/.deja/.env to never start the built-in broker.
WebSocket (device transport)
The alternative to MQTT for deja-esp32-wifi and deja-mqtt (Pico W). Pick Transport → WebSocket when adding the device, then run deja deploy. The device dials the DEJA Server directly — no broker.
| Parameter | Value |
|---|---|
| Endpoint | ws://{serverHost}:8082/device (port = VITE_WS_PORT, default 8082) |
| Encoding | UTF-8 JSON, one value per text frame; binary frames are ignored |
| Frame limit | 4 KiB — larger frames from a device are dropped |
| Heartbeat | server pings every 30 s; a socket that missed the previous ping is closed |
| Reconnect | device retries after 1 s, doubling up to 30 s. The backoff never sleeps, but one attempt to an unreachable server pauses the device's loop (servos, sensors) for up to ~1 s on the ESP32 (5 s from an Arduino IDE build) or ~10 s on the Pico W. No attempt starts during a turnout pulse |
Handshake
The device sends two headers with the upgrade request:
Authorization: Bearer <token>
X-Deja-Device: <deviceId>
deja deploy generates the token (32 random bytes, 64 hex characters), bakes it into the firmware, and stores only its SHA-256 hash on the device in DEJA Cloud. The server answers HTTP 401 when the device ID is unknown in the layout, the device isn't set to WebSocket, it has no stored hash, or the token doesn't match (Rejected device … token mismatch in the server log). Run deja deploy --rotate-token to issue a new token; the old board is refused until you reflash it.
A second connection for the same device replaces the first (the old socket is closed with code 4000, reason replaced), so a rebooted board never waits for a stale socket.
Messages
The same shapes as MQTT:
| Direction | Frame | Meaning |
|---|---|---|
| server → device | { "action": "pin", "device": id, "payload": { "pin": n, "state": b } } |
set an output |
| server → device | { "action": "servo", "device": id, "payload": { "servo": n, "value": deg, "current"?: deg, "profile"?: name } } |
move a servo |
| server → device | { "action": "turnout", "device": id, "payload": { "turnout": idx, "state": b } } |
pulse a Kato turnout |
| device → server | { "status": "connected", "device": id } |
hello, sent once after connecting |
| device → server | { "device": id, "sensor": i, "state": 0|1 } |
sensor change |
| device → server | any non-JSON string | device log line (console only) |
When the port is open
The server opens its WebSocket port only when it's needed:
- The layout has a WebSocket device — the port opens at startup, or as soon as a device is added or switched to WebSocket, with no restart. In this mode only
/deviceis served; every other path is refused with 401. ENABLE_WS=true(orws.enabledin~/.deja/config.json) — the port opens at startup and also accepts non-device connections on other paths.
Once open, the port stays open until the server restarts. Keep it on the LAN — never port-forward or proxy it to the internet.
The Pico W also pings the server after 30 s of silence (the server stops pinging during standby) and reconnects if it hears nothing for 75 s. The ESP32 uses the WebSockets library's heartbeat: it pings every 15 s and reconnects after two unanswered pings.
WebSocket (reserved, non-device clients)
[!NOTE] No DEJA app connects to the server's WebSocket — device serial logs reach Throttle through DEJA Cloud. Non-device connections are accepted only when
ENABLE_WS=true.
When enabled, the server sends an acknowledgment on connect and fans out server broadcasts:
{ "action": "ack", "payload": { "layoutId": "tam", "serverId": "DEJA" } }
It accepts no client commands. WS_AUTH_TOKEN and WS_ALLOWED_ORIGINS apply to these connections only; /device always requires a device token. See apps/server/WEBSOCKET_PROTOCOL.md for the connection details.