Skip to main content

๐Ÿ”ง Building Custom Devices

Want to go beyond the stock DEJA.js 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.js 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.js uses a simple hierarchical topic scheme:

{topicId}/{layoutId}/{deviceId}
SegmentDescriptionExample
topicIdMQTT topic prefix (default: DEJA)DEJA
layoutIdYour layout identifier from DEJA Cloudtamarack
deviceIdUnique ID for this devicemy-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 }
}
FieldTypeDescription
actionstringThe type of command to execute
devicestringTarget device ID โ€” always check this against your own device ID and ignore messages for other devices
payloadobjectAction-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 FieldTypeDescription
pinnumberLogical pin number (mapped to physical GPIO in your config)
statebooleantrue = 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 FieldTypeDescription
servonumberServo channel on the PCA9685 (0โ€“15)
valuenumberTarget 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 FieldTypeDescription
servonumberServo channel for this turnout
valuenumberServo 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.js server and Monitor app to track your device, register it in Firestore at:

layouts/{layoutId}/devices

Create a document with at minimum:

FieldValueDescription
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 Monitor

You can register devices through the DEJA Cloud app or directly in the Firebase console.


๐Ÿ—๏ธ Minimal CircuitPython Example (Pico W)

This is a stripped-down version of the DEJA.js 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.


๐Ÿ’ป 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 an MQTT broker โ€” Install Mosquitto or use any MQTT broker on your network
  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 the Monitor app โ€” If your device is registered in Firestore, its connection status appears in the DEJA Monitor dashboard

๐Ÿ“š Next Steps