Skip to main content

When to Use MQTT

Use MQTT instead of the default Socket.IO transport when:
  • Your device has limited RAM (microcontrollers, Pico W)
  • The network connection is unreliable or intermittent
  • You need offline message queueing (commands delivered when the device reconnects)
  • You are already running an MQTT infrastructure
  • The device is battery-powered and needs a lightweight protocol
The Lua Device Gateway runs an MQTT broker at mqtts://mqtt.heylua.ai:8883. You do not need to run your own broker.

Configuration

Node.js

import { DeviceClient } from '@lua-ai/device-client';

const device = new DeviceClient({
  agentId: 'your-agent-id',
  apiKey: 'your-api-key',
  deviceName: 'pico-sensor-01',
  transport: 'mqtt',
  mqttUrl: 'mqtts://mqtt.heylua.ai:8883', // default, can be omitted
  commands: [
    { name: 'read_sensor', description: 'Read temperature and humidity' },
  ],
});

device.onCommand('read_sensor', async () => {
  return { temperature: 22.5, humidity: 60 };
});

await device.connect();

MicroPython

from lua_device import LuaDevice

device = LuaDevice(
    agent_id="your-agent-id",
    api_key="your-api-key",
    device_name="pico-sensor-01",
    server="mqtt.heylua.ai",
    port=8883,
)

@device.command("read_sensor")
def read_sensor(payload):
    return {"temperature": 22.5, "humidity": 60}

device.connect()
device.run()  # blocks, listens for commands

Topic Structure

All MQTT topics follow a consistent prefix pattern:
lua/devices/{agentId}/{deviceName}/{suffix}
Topic SuffixDirectionQoSRetainedPurpose
statusDevice publish1YesOnline/offline status (retained for broker persistence)
heartbeatDevice publish0NoKeepalive signal every 30 seconds
responseDevice publish1NoCommand execution results
triggerDevice publish1NoDevice-to-agent event triggers
commandDevice subscribe1NoIncoming commands from the agent
connectedDevice subscribe1NoServer connection confirmation
trigger_ackDevice subscribe1NoTrigger receipt acknowledgment
trigger_resultDevice subscribe1NoTrigger execution results (optional)
errorDevice subscribe1NoServer-side error messages
pongDevice subscribe0NoHeartbeat response

Last Will and Testament (LWT)

The MQTT client automatically sets a Last Will and Testament message on the status topic. If the device disconnects unexpectedly (network failure, power loss), the broker publishes the LWT message, which the gateway uses to immediately mark the device as offline.
{
  "status": "offline",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
This is more reliable than heartbeat-based detection alone, since the broker publishes the LWT within seconds of losing the TCP connection.

QoS Levels

QoSMeaningUsed For
0At most once (fire-and-forget)Heartbeats — acceptable to miss one
1At least once (with acknowledgment)Commands, responses, triggers, status — must not be lost
The device client uses QoS 1 for all messages except heartbeats. Combined with persistent sessions (clean: false), this means commands are queued by the broker when the device is temporarily offline and delivered when it reconnects.
The idempotency dedup layer on the device (LRU cache of recent commandId values) ensures that redelivered QoS 1 messages do not cause duplicate command execution.

Authentication

MQTT authentication uses the username and password fields of the MQTT CONNECT packet:
FieldValue
clientIdlua-{agentId}-{deviceName}
username{agentId}:{deviceName}
passwordYour API key
The API key is also sent in a separate non-retained status message after connection for server-side validation. Retained status messages never contain the API key.

Complete Example

A humidity and temperature monitor that fires an alert trigger when conditions are out of range:
import { DeviceClient } from '@lua-ai/device-client';

const device = new DeviceClient({
  agentId: process.env.LUA_AGENT_ID!,
  apiKey: process.env.LUA_API_KEY!,
  deviceName: 'greenhouse-sensor',
  transport: 'mqtt',
  group: 'greenhouse-sensors',
  commands: [
    {
      name: 'read_environment',
      description: 'Read temperature, humidity, and soil moisture',
    },
    {
      name: 'toggle_irrigation',
      description: 'Turn irrigation on or off',
      inputSchema: {
        type: 'object',
        properties: {
          enabled: { type: 'boolean' },
        },
        required: ['enabled'],
      },
    },
  ],
});

let irrigationOn = false;

device.onCommand('read_environment', async () => {
  return {
    temperature: 28.3,
    humidity: 65,
    soilMoisture: 42,
    irrigationOn,
    timestamp: new Date().toISOString(),
  };
});

device.onCommand('toggle_irrigation', async (payload) => {
  irrigationOn = payload.enabled;
  return { irrigationOn };
});

async function main() {
  device.on('connected', () => console.log('Sensor online (MQTT)'));
  device.on('reconnected', () => console.log('Sensor reconnected'));
  device.on('error', (err) => console.error('MQTT error:', err));

  await device.connect();

  // Periodically check conditions and fire triggers
  setInterval(async () => {
    const humidity = 65 + Math.random() * 20;
    if (humidity > 80) {
      await device.trigger('humidity_alert', {
        humidity: Math.round(humidity),
        threshold: 80,
        sensor: 'greenhouse-sensor',
      });
    }
  }, 10000);
}

main().catch(console.error);

Next Steps

MicroPython Client

Run on a Raspberry Pi Pico W with native MQTT

Pico W Setup Guide

Step-by-step hardware setup with Thonny

Node.js Client

Socket.IO transport for full-featured Node.js devices

Architecture

Understand the full transport comparison