Skip to main content
DEJA

šŸ“ Pico W Setup

The Raspberry Pi Pico W (deja-mqtt) connects to your layout wirelessly over WiFi and talks to the DEJA Server over MQTT (the default) or WebSocket — ideal for devices placed inside scenery or buildings. Pick the transport under Transport when you add the device; deja deploy writes the matching settings.toml.

šŸ›œ See also: deja-esp32-wifi — the ESP32-based WiFi/MQTT alternative. It uses the same MQTT topology (DEJA/{LAYOUT_ID}/{DEVICE_ID} for commands, …/messages for sensor events), the same WebSocket endpoint, and the same single-object command format as the Pico W. Pick whichever hardware you have on hand: Pico W if you prefer CircuitPython and a smaller board, ESP32 if you prefer the Arduino C++ toolchain and more GPIOs.

Hardware Requirements

  • Raspberry Pi Pico W (~$6)
  • USB-C cable for initial setup and power
  • Adafruit PCA9685 16-Channel PWM Servo Driver (optional, for servos)
  • WiFi network on the same network as your DEJA Server
  • DEJA Server running — it provides the MQTT broker (port 1883) or, for WebSocket devices, the device endpoint (port 8082)

Wiring the PCA9685

PCA9685 Pin Pico W Pin
VCC 3V3
GND GND
SDA GP0
SCL GP1

Installing CircuitPython

  1. Download CircuitPython 9.2.9 for Pico W: adafruit-circuitpython-raspberry_pi_pico_w-en_US-9.2.9.uf2. Use version 9, not the newest 10.x: DEJA's bundled libraries are built for CircuitPython 9.
  2. Hold the BOOTSEL button while plugging the Pico W in. It shows up as a drive called RPI-RP2.
  3. Drag the .uf2 file onto RPI-RP2.
  4. The Pico W restarts as CIRCUITPY. From now on, plug it in normally (no button) and run deja deploy.

🐧 On a Raspberry Pi without a desktop, USB drives don't mount on their own. deja deploy tries udisksctl mount for you. If that's not installed, find the drive with lsblk and mount it yourself (no sudo needed from DEJA).

🪟 On Windows, the wizard finds the drive labelled CIRCUITPY by itself.

Configuration

WiFi and transport — settings.toml

deja deploy generates settings.toml for you. This is what it writes (a WebSocket device shown; an MQTT device has TRANSPORT = "mqtt" and a filled-in MQTT_BROKER):

# šŸ“ DEJA Pico W Configuration
# Generated for device: my-pico-device

CIRCUITPY_WIFI_SSID = "YourWiFiNetwork"
CIRCUITPY_WIFI_PASSWORD = "YourWiFiPassword"

ENABLE_CONFIG = "true"
ENABLE_PWM = "true"

# šŸ”Œ Transport: "mqtt" or "websocket"
TRANSPORT = "websocket"

MQTT_BROKER = ""
MQTT_PORT = "1883"

WS_HOST = "192.168.1.100"
WS_PORT = "8082"
WS_TOKEN = "<64 hex characters from deja deploy>"

LAYOUT_ID = "your-layout-id"
DEVICE_ID = "my-pico-device"
TOPIC_ID = "DEJA"
  • TRANSPORT = "mqtt" needs MQTT_BROKER (the DEJA Server machine's LAN IP). TRANSPORT = "websocket" needs WS_HOST and WS_TOKEN; the other transport's keys are ignored.
  • WS_TOKEN is generated by deja deploy; only its hash is stored on the device in DEJA Cloud. Don't share it.
  • ENABLE_PWM is "true" when servo turnouts are assigned to the device, unless the device's own enablePwm setting says otherwise — an explicit setting wins.

Pins, turnouts and sensors — config.json

config.json maps logical pin numbers to physical GPIO names, and lists the device's Kato turnouts and sensors:

{
  "pins": {
    "8": "GP8",
    "9": "GP9"
  },
  "turnouts": [
    { "index": 0, "straight": "GP14", "divergent": "GP15", "pulseMs": 250 }
  ],
  "sensors": [
    { "index": 0, "pin": "GP20", "debounceMs": 100 }
  ]
}

Turnouts and sensors

  • Turnouts are the layout's Kato turnouts assigned to this device, in the same order the ESP32 uses. A turnout command with state: false pulses the straight pin, true pulses the divergent pin, for pulseMs (250 ms). Servo turnouts don't appear here — they're driven with servo commands.
  • Sensors are the layout's sensors assigned to this device. When a pin changes, the Pico sends { "device": "<id>", "sensor": <index>, "state": 0|1 } using the sensor's own index. debounceMs (default 100 ms) is the minimum time between two reported changes of the same sensor — like the ESP32's SENSOR_DEBOUNCE_MS, not a hold time.

Pulses and debounce never sleep: servos keep moving and sensors keep being read while a pulse runs or while the Pico waits between reconnect attempts. A single connect attempt to an unreachable host does pause the loop — for up to about 10 s on WebSocket (a 5 s socket connect plus a 5 s wait for the handshake reply) — so the Pico never starts one while a turnout pulse is running. A sensor change that happens while disconnected is sent after the Pico reconnects.

Deploying the Firmware

Copy all files from io/src/deja-pico-w/ to the CIRCUITPY drive:

  • code.py — boot, WiFi and the main loop
  • commands.py — pin, servo and turnout commands and sensor polling
  • transport_mqtt.py / transport_ws.py — the two transports
  • config.json — pins, turnouts and sensors
  • lib/ — Adafruit libraries (MQTT, motor, servo, LED animation), deja_ws.py, the WebSocket client, and backoff.py, the reconnect schedule both transports share

Or use the DEJA deploy script:

cd io
pnpm deploy

The script generates settings.toml and config.json from your device configuration in DEJA Cloud and copies everything to the mounted Pico W.

MQTT Topics

With TRANSPORT = "mqtt" the Pico W subscribes and publishes to topics based on its identity:

  • Subscribe: DEJA/{LAYOUT_ID}/{DEVICE_ID} — receives commands
  • Publish: DEJA/{LAYOUT_ID}/{DEVICE_ID}/messages — sends status and sensor events

WebSocket

With TRANSPORT = "websocket" the Pico W opens ws://{WS_HOST}:{WS_PORT}/device, sending its token and device ID as headers, and receives the same commands over that connection. If the connection drops it retries after 1 s, doubling up to 30 s; no attempt starts while a turnout pulse is running. It pings the server after 30 s of silence and reconnects if it hears nothing for 75 s. See Protocol Details.

Command Format

Pico W devices receive JSON objects over MQTT or WebSocket:

{
  "action": "pin",
  "payload": { "pin": 8, "state": 1 },
  "device": "my-pico-device"
}

The device field filters messages — only commands matching this device's ID are processed. See the Command Reference for all supported actions.

Status display (optional)

A 0.96" 128Ɨ64 SSD1306 OLED on the Pico's I²C pins — SDA → GP0 (pin 1), SCL → GP1 (pin 2), GND → pin 3, VCC → 3V3(OUT) (pin 36) — shows the board's WiFi, server link and servo-driver state plus stats such as its IP and uptime. The Pico draws it itself, so it is right even when the server can't reach the board. Turn it on under the device's Firmware → Display in Throttle, choose the rows, save, and copy the regenerated config.json onto the CIRCUITPY drive (or run deja deploy). The display shares the bus with the PCA9685; the Pico firmware fixes I²C to GP0/GP1.