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 Suffix Direction QoS Retained Purpose statusDevice publish 1 Yes Online/offline status (retained for broker persistence) heartbeatDevice publish 0 No Keepalive signal every 30 seconds responseDevice publish 1 No Command execution results triggerDevice publish 1 No Device-to-agent event triggers commandDevice subscribe 1 No Incoming commands from the agent connectedDevice subscribe 1 No Server connection confirmation trigger_ackDevice subscribe 1 No Trigger receipt acknowledgment trigger_resultDevice subscribe 1 No Trigger execution results (optional) errorDevice subscribe 1 No Server-side error messages pongDevice subscribe 0 No Heartbeat 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
QoS Meaning Used For 0 At most once (fire-and-forget) Heartbeats — acceptable to miss one 1 At 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:
Field Value 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