> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MQTT Transport

> Configure MQTT for constrained devices and unreliable networks

## 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

<Note>
  The Lua Device Gateway runs an MQTT broker at `wss://mqtt.heylua.ai/mqtt`. You do not need to run your own broker.
</Note>

## Configuration

### Node.js

```typescript theme={null}
import { DeviceClient } from '@lua-ai-global/device-client';

const device = new DeviceClient({
  agentId: 'your-agent-id',
  apiKey: 'your-api-key',
  deviceName: 'pico-sensor-01',
  transport: 'mqtt',
  mqttUrl: 'wss://mqtt.heylua.ai/mqtt', // 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();
```

### Python

```python theme={null}
import asyncio
from lua_device import DeviceClient, DeviceCommandDefinition

client = DeviceClient(
    agent_id="your-agent-id",
    api_key="your-api-key",
    device_name="pico-sensor-01",
    transport="mqtt",
    mqtt_url="wss://mqtt.heylua.ai/mqtt",  # default, can be omitted
    commands=[
        DeviceCommandDefinition(name="read_sensor", description="Read temperature and humidity"),
    ],
)

@client.on_command("read_sensor")
async def handle_read_sensor(payload):
    return {"temperature": 22.5, "humidity": 60}

asyncio.run(client.connect())
```

### MicroPython

```python theme={null}
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=443,
)

@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                                                 |
| ---------------- | ---------------- | --- | -------- | ------------------------------------------------------- |
| `status`         | Device publish   | 1   | Yes      | Online/offline status (retained for broker persistence) |
| `heartbeat`      | Device publish   | 0   | No       | Keepalive signal every 30 seconds                       |
| `response`       | Device publish   | 1   | No       | Command execution results                               |
| `trigger`        | Device publish   | 1   | No       | Device-to-agent event triggers                          |
| `command`        | Device subscribe | 1   | No       | Incoming commands from the agent                        |
| `connected`      | Device subscribe | 1   | No       | Server connection confirmation                          |
| `trigger_ack`    | Device subscribe | 1   | No       | Trigger receipt acknowledgment                          |
| `trigger_result` | Device subscribe | 1   | No       | Trigger execution results (optional)                    |
| `error`          | Device subscribe | 1   | No       | Server-side error messages                              |
| `pong`           | Device 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.

```json theme={null}
{
  "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.

<Warning>
  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.
</Warning>

## Authentication

MQTT authentication uses the `username` and `password` fields of the MQTT CONNECT packet:

| Field      | Value                        |
| ---------- | ---------------------------- |
| `clientId` | `lua-{agentId}-{deviceName}` |
| `username` | `{agentId}:{deviceName}`     |
| `password` | Your 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:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { DeviceClient } from '@lua-ai-global/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);
  ```

  ```python Python theme={null}
  import asyncio
  import os
  import random
  from datetime import datetime, timezone
  from lua_device import DeviceClient, DeviceCommandDefinition

  irrigation_on = False

  client = DeviceClient(
      agent_id=os.environ["LUA_AGENT_ID"],
      api_key=os.environ["LUA_API_KEY"],
      device_name="greenhouse-sensor",
      transport="mqtt",
      group="greenhouse-sensors",
      commands=[
          DeviceCommandDefinition(
              name="read_environment",
              description="Read temperature, humidity, and soil moisture",
          ),
          DeviceCommandDefinition(
              name="toggle_irrigation",
              description="Turn irrigation on or off",
              input_schema={
                  "type": "object",
                  "properties": {
                      "enabled": {"type": "boolean"},
                  },
                  "required": ["enabled"],
              },
          ),
      ],
  )

  @client.on_command("read_environment")
  async def handle_read_environment(payload):
      return {
          "temperature": 28.3,
          "humidity": 65,
          "soilMoisture": 42,
          "irrigationOn": irrigation_on,
          "timestamp": datetime.now(timezone.utc).isoformat(),
      }

  @client.on_command("toggle_irrigation")
  async def handle_toggle_irrigation(payload):
      global irrigation_on
      irrigation_on = payload["enabled"]
      return {"irrigationOn": irrigation_on}

  async def main():
      await client.connect()
      print("Sensor online (MQTT)")

      # Periodically check conditions and fire triggers
      while True:
          humidity = 65 + random.random() * 20
          if humidity > 80:
              await client.trigger("humidity_alert", {
                  "humidity": round(humidity),
                  "threshold": 80,
                  "sensor": "greenhouse-sensor",
              })
          await asyncio.sleep(10)

  asyncio.run(main())
  ```
</CodeGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="MicroPython Client" icon="microchip" href="/devices/micropython-client">
    Run on a Raspberry Pi Pico W with native MQTT
  </Card>

  <Card title="Pico W Setup Guide" icon="screwdriver-wrench" href="/devices/examples/raspberry-pi-pico">
    Step-by-step hardware setup with Thonny
  </Card>

  <Card title="Node.js Client" icon="node-js" href="/devices/node-client">
    Socket.IO transport for full-featured Node.js devices
  </Card>

  <Card title="Architecture" icon="diagram-project" href="/devices/how-it-works">
    Understand the full transport comparison
  </Card>
</CardGroup>
