Skip to main content
DEJA

๐Ÿ”ง 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-wifi target โ€” 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 device field 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 servo and turnouts actions 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_minimqtt into the lib/ 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

  1. Start the DEJA Server โ€” it runs the MQTT broker on port 1883 (any external broker works too)
  2. Enable MQTT on the DEJA Server โ€” Set ENABLE_MQTT=true in your server's .env
  3. Use an MQTT explorer โ€” Tools like MQTT Explorer let you watch messages in real time
  4. Send test commands โ€” Use the Python or Node.js scripts above to publish commands to your device's topic
  5. 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