๐ง Building Custom Devices
Want to go beyond the stock DEJA firmware? You can build your own MQTT-connected devices using any microcontroller with WiFi โ ESP32, Raspberry Pi Pico W, ESP8266, or even a full Linux board. As long as your device can publish and subscribe to MQTT topics, it can talk to the DEJA Server.
This guide covers the MQTT protocol, command format, and provides minimal working examples you can use as a starting point.
๐ก If you just want a working ESP32 WiFi build without writing your own firmware, use the stock
deja-esp32-wifitarget โ see Arduino & ESP32 Setup. It uses exactly the topology and command format described on this page, so you can reuse the protocol details below to add custom actions on top of the stock sketch.
๐ก MQTT Topic Structure
DEJA uses a simple hierarchical topic scheme:
{topicId}/{layoutId}/{deviceId}
| Segment | Description | Example |
|---|---|---|
topicId |
MQTT topic prefix (default: DEJA) |
DEJA |
layoutId |
Your layout identifier from DEJA Cloud | tamarack |
deviceId |
Unique ID for this device | my-custom-pico |
๐ฅ Subscribing for Commands
Your device subscribes to its own command topic to receive instructions from the server:
DEJA/tamarack/my-custom-pico
๐ค Publishing Status Messages
Your device publishes status and sensor data back to the server on the /messages subtopic:
DEJA/tamarack/my-custom-pico/messages
๐ฆ Command Format
All commands arrive as JSON messages on your subscription topic with three fields:
{
"action": "pin",
"device": "my-custom-pico",
"payload": { "pin": 6, "state": true }
}
| Field | Type | Description |
|---|---|---|
action |
string |
The type of command to execute |
device |
string |
Target device ID โ always check this against your own device ID and ignore messages for other devices |
payload |
object |
Action-specific data (see below) |
โ ๏ธ Important: Multiple devices can share the same MQTT topic. Always compare the
devicefield against your device's ID and ignore messages not intended for you.
๐ฏ Supported Actions
pin โ Digital Pin Control
Turn a GPIO pin on or off. Used for LEDs, relays, and other digital outputs.
{
"action": "pin",
"device": "my-custom-pico",
"payload": { "pin": 6, "state": true }
}
| Payload Field | Type | Description |
|---|---|---|
pin |
number |
Logical pin number (mapped to physical GPIO in your config) |
state |
boolean |
true = on, false = off |
servo โ Servo Positioning
Set a servo to a specific angle via a PCA9685 servo driver.
{
"action": "servo",
"device": "my-custom-pico",
"payload": { "servo": 0, "value": 90 }
}
| Payload Field | Type | Description |
|---|---|---|
servo |
number |
Servo channel on the PCA9685 (0โ15) |
value |
number |
Target angle in degrees (0โ180) |
turnout โ Turnout Control
Throw or close a turnout (uses servo positioning under the hood).
{
"action": "turnouts",
"device": "my-custom-pico",
"payload": { "servo": 0, "value": 90 }
}
| Payload Field | Type | Description |
|---|---|---|
servo |
number |
Servo channel for this turnout |
value |
number |
Servo angle for thrown/closed position |
๐ก Both
servoandturnoutsactions route to the same servo handler in the reference firmware.
effects โ Effects Control
Trigger lighting effects, animations, or other GPIO-based effects. Uses the same payload format as pin.
{
"action": "effects",
"device": "my-custom-pico",
"payload": { "pin": 8, "state": true }
}
ialed โ Addressable LED Control
Control individually addressable LEDs (NeoPixel, WS2812, etc.) for signal heads, building lights, and animated effects.
โ Server Acknowledgment
When a device first connects and subscribes, it should publish a status message to its /messages topic. The server sends an acknowledgment back:
{
"action": "ack",
"payload": { "layoutId": "tamarack" }
}
Use this to confirm your device is properly registered and communicating with the server.
๐๏ธ Device Registration
For the DEJA Server and Throttle's Devices page to track your device, register it in Throttle under Devices. Create an entry with at minimum:
| Field | Value | Description |
|---|---|---|
deviceId |
"my-custom-pico" |
Must match the device ID in your MQTT config |
connection |
"wifi" |
Connection type (wifi for MQTT devices, serial for USB) |
label |
"My Custom Pico" |
Human-readable name shown in Throttle |
๐๏ธ Minimal CircuitPython Example (Pico W)
This is a stripped-down version of the DEJA Pico W firmware โ just enough to connect to WiFi, subscribe to MQTT, and handle a pin command. Use this as a starting point for your own device.
settings.toml
CIRCUITPY_WIFI_SSID = "YourNetwork"
CIRCUITPY_WIFI_PASSWORD = "YourPassword"
MQTT_BROKER = "192.168.1.100"
LAYOUT_ID = "tamarack"
DEVICE_ID = "my-custom-pico"
TOPIC_ID = "DEJA"
code.py
import board
import digitalio
import os
import json
import socketpool
import wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT
# ๐ง Configuration from settings.toml
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
broker = os.getenv("MQTT_BROKER")
layout_id = os.getenv("LAYOUT_ID")
device_id = os.getenv("DEVICE_ID")
topic_id = os.getenv("TOPIC_ID")
subscribe_topic = f"{topic_id}/{layout_id}/{device_id}"
publish_topic = f"{topic_id}/{layout_id}/{device_id}/messages"
# ๐ก Set up a single LED pin for demo purposes
led = digitalio.DigitalInOut(board.GP6)
led.direction = digitalio.Direction.OUTPUT
# ๐ฅ Handle incoming MQTT messages
def on_message(client, topic, message):
data = json.loads(message)
# โ ๏ธ Ignore messages for other devices
if data.get("device") != device_id:
return
action = data.get("action")
payload = data.get("payload")
if action == "pin" and payload:
pin_num = payload.get("pin")
state = payload.get("state", False)
if pin_num == 6:
led.value = state
client.publish(publish_topic, f"Pin 6 set to {state}")
print(f"โ
Pin 6 โ {state}")
def on_connect(client, userdata, flags, rc):
client.subscribe(subscribe_topic)
client.publish(publish_topic, f"Connected: {device_id}")
print(f"๐ก Subscribed to {subscribe_topic}")
# ๐ Connect to WiFi
print("Connecting to WiFi...")
wifi.radio.connect(ssid, password)
print(f"Connected! IP: {wifi.radio.ipv4_address}")
# ๐ก Connect to MQTT broker
pool = socketpool.SocketPool(wifi.radio)
mqtt_client = MQTT.MQTT(broker=broker, port=1883, socket_pool=pool)
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
print("Connecting to MQTT...")
mqtt_client.connect()
# ๐ Main loop โ poll for messages
while True:
mqtt_client.loop()
๐ฆ Required libraries: Copy
adafruit_minimqttinto thelib/folder on your Pico W's CIRCUITPY drive. Get it from the Adafruit CircuitPython Bundle.
๐ WebSocket instead of MQTT
A device can skip the broker and dial the DEJA Server directly. Register it as deja-esp32-wifi or deja-mqtt with Transport โ WebSocket (the only types that may use it), then run deja deploy or pnpm build:firmware for it โ the generated settings.toml / config.h holds its WS_TOKEN. The server only stores the token's hash.
Open ws://<server>:8082/device with these handshake headers:
Authorization: Bearer <WS_TOKEN>
X-Deja-Device: <deviceId>
A missing or wrong token gets HTTP 401. Once connected, the device receives the same { action, device, payload } objects as on MQTT, one per text frame, and sends its hello, sensor events and log lines back the same way (frames up to 4 KiB). Answer the server's pings โ every 30 s โ or it drops the connection. See Protocol Details.
The Pico W firmware's client, io/src/deja-pico-w/lib/deja_ws.py, also runs under desktop Python:
import json, socket, time
from deja_ws import WebSocketClient, WebSocketError # io/src/deja-pico-w/lib/deja_ws.py
DEVICE_ID, TOKEN = "my-custom-pico", "<WS_TOKEN from deja deploy>"
ws = WebSocketClient(lambda h, p: socket.create_connection((h, p), timeout=5), "192.168.1.100", 8082, "/device",
headers={"Authorization": "Bearer " + TOKEN, "X-Deja-Device": DEVICE_ID})
ws.connect() # raises on 401
ws.send_text(json.dumps({"status": "connected", "device": DEVICE_ID}))
try:
while ws.connected:
for text in ws.poll(): # never blocks; answers pings for you
print("command:", json.loads(text))
time.sleep(0.05)
except (WebSocketError, OSError) as e: # server gone or socket reset
print("disconnected:", e) # a real device reconnects here, backing off 1 s โ 30 s
๐ป Minimal Python Script (Desktop/Raspberry Pi)
Control your layout from any computer on the network using the paho-mqtt library.
pip install paho-mqtt
import json
import paho.mqtt.client as mqtt
# ๐ง Configuration
BROKER = "192.168.1.100"
LAYOUT_ID = "tamarack"
DEVICE_ID = "my-custom-pico"
TOPIC_ID = "DEJA"
topic = f"{TOPIC_ID}/{LAYOUT_ID}/{DEVICE_ID}"
client = mqtt.Client()
client.connect(BROKER, 1883)
# ๐ก Turn on pin 6
command = {
"action": "pin",
"device": DEVICE_ID,
"payload": {"pin": 6, "state": True}
}
client.publish(topic, json.dumps(command))
print(f"๐ค Sent: {json.dumps(command)}")
# ๐ Turn a servo to 90 degrees
servo_command = {
"action": "servo",
"device": DEVICE_ID,
"payload": {"servo": 0, "value": 90}
}
client.publish(topic, json.dumps(servo_command))
print(f"๐ค Sent: {json.dumps(servo_command)}")
client.disconnect()
๐ข Minimal Node.js Script
Control your layout from Node.js using the mqtt npm package.
npm install mqtt
import mqtt from 'mqtt'
// ๐ง Configuration
const BROKER = 'mqtt://192.168.1.100'
const LAYOUT_ID = 'tamarack'
const DEVICE_ID = 'my-custom-pico'
const TOPIC_ID = 'DEJA'
const topic = `${TOPIC_ID}/${LAYOUT_ID}/${DEVICE_ID}`
const client = mqtt.connect(BROKER)
client.on('connect', () => {
console.log('๐ก Connected to MQTT broker')
// ๐ก Turn on pin 6
client.publish(topic, JSON.stringify({
action: 'pin',
device: DEVICE_ID,
payload: { pin: 6, state: true }
}))
// ๐ Move servo to 90 degrees
client.publish(topic, JSON.stringify({
action: 'servo',
device: DEVICE_ID,
payload: { servo: 0, value: 90 }
}))
console.log('โ
Commands sent!')
client.end()
})
๐งช Testing Your Device
- Start the DEJA Server โ it runs the MQTT broker on port 1883 (any external broker works too)
- Enable MQTT on the DEJA Server โ Set
ENABLE_MQTT=truein your server's.env - Use an MQTT explorer โ Tools like MQTT Explorer let you watch messages in real time
- Send test commands โ Use the Python or Node.js scripts above to publish commands to your device's topic
- Check Throttle โ Devices โ If your device is registered in Firestore, its connection status appears on throttle.dejadcc.com/devices, and sensor/effect changes show up under Activity
๐ Next Steps
- ๐ IO Devices & Firmware Overview โ Learn about the stock Arduino and Pico W firmware
- ๐ก Protocol Details โ Serial, MQTT and WebSocket connection specs
- ๐ DEJA Throttle โ Register devices and manage your layout configuration