# MQTT 接入

MQTT 接入用于把已有设备改造成可被设备智能体控制和查询的真实设备。设备端不需要使用设备端 SDK，也不需要重写原有驱动或业务逻辑，只需要增加一层 MQTT 适配：订阅命令、调用原有设备能力、返回执行结果，并按设备规格上报状态和事件。

## 获取接入信息

进入设备智能体工作区，点击 **接入设备**，选择 **已有设备**。控制台会给出接入所需的信息：

- `productId`：当前设备智能体标识。
- `namespace`：当前运行空间，默认是 `default`。
- `deviceId`：真实设备的唯一标识，同一个设备智能体下不要重复。
- MQTT 服务地址、用户名和密码状态。
- 命令、响应、遥测、事件和时间同步主题。
- 命令、响应、上线状态、状态遥测和设备事件的消息体示例。
- 当前设备规格中的命令、遥测和事件字段。

如果运行配置中修改过 MQTT 主题模板，以控制台显示的主题为准。

![已有设备 MQTT 接入信息](../images/docs/device-access/mqtt/zh/01-existing-device-mqtt.png)

## 选择适配位置

适配层放在哪里，取决于哪里最容易读取状态并执行动作：

- **设备固件**：设备本身支持 MQTT，直接在固件中订阅命令并上报状态。
- **边缘网关**：网关通过 Modbus、BLE、串口或私有协议连接设备，再转换为 MQTT。
- **后端服务**：已有云端或本地服务可以控制设备，新增一个 MQTT 适配进程，把命令转成原有 API 调用。

无论适配层在哪里，对设备智能体来说都是同一套 MQTT 合约。

## 改造步骤

先把设备规格和已有设备能力对齐：

| 设备规格 | 映射到已有设备 | MQTT 动作 |
| --- | --- | --- |
| 命令 | 驱动调用、HTTP API、串口指令或业务函数 | 订阅命令主题，执行后发布响应 |
| 遥测 | 当前状态、传感器读数、运行模式 | 发布上线状态和 `state` 遥测 |
| 事件 | 告警、异常、任务完成、按键回调 | 发布 `event` 事件 |

最小适配逻辑如下：

```text
启动:
  连接 MQTT
  订阅命令主题
  发布上线状态和当前状态快照

收到命令:
  根据 cmd 和 params 调用原有设备能力
  发布命令响应，requestId 保持一致
  如果状态变化，发布最新状态快照

设备状态变化:
  发布 state 遥测

设备发生告警、异常或任务完成:
  发布 event 事件
```

设备智能体会从主题路径解析 `productId` 和 `deviceId`。消息体里的 `metadata.productId` 也建议保留，便于自动注册、调试和自定义主题模板下的绑定。如果需要强绑定 MQTT Client ID 和设备 ID，应在 MQTT 服务侧配置认证或 ACL。

## 主题

默认主题如下。实际接入时复制控制台中的主题。

| 方向 | 用途 | 默认主题 |
| --- | --- | --- |
| 设备订阅 | 接收命令 | `device-agent/{productId}/device/{deviceId}/commands` |
| 设备发布 | 返回命令响应 | `device-agent/{productId}/device/{deviceId}/responses` |
| 设备发布 | 上报在线状态和遥测 | `v1/{productId}/{deviceId}/telemetry` |
| 设备发布 | 上报设备事件 | `v1/{productId}/{deviceId}/event` |
| 设备发布 | 发起时间同步请求 | `device-agent/{productId}/device/{deviceId}/ntp/request` |
| 设备订阅 | 接收时间同步响应 | `device-agent/{productId}/device/{deviceId}/ntp/response` |

建议使用 QoS 1、`retain: false`，避免旧状态或旧事件被重复消费。

## 消息体规则

所有消息体都是 UTF-8 JSON 对象。不要使用旧的单主题 envelope，也不要新增自定义 shadow topic。

| 字段 | 说明 |
| --- | --- |
| `ts` | 可选，Unix 毫秒时间戳，用于调试和排序。 |
| `metadata` | 可选对象。建议至少包含 `productId`，例如 `{ "productId": "thermostat", "source": "existing-device" }`。 |
| `data` | 上报数据对象。状态和遥测字段必须来自当前设备规格。 |

设备规格是运行时校验依据：

- 命令名和参数必须匹配设备规格中的命令定义。
- `status.data.state` 和 `state` 遥测必须使用设备规格中的遥测字段。
- 事件名和事件字段必须匹配设备规格中的事件定义。
- 未定义字段、错误类型或未知事件名会产生校验错误，不会写入当前设备状态。

## 上线状态

设备连接 MQTT 后，先向遥测主题发布上线状态。首次成功上报后，这台设备会出现在设备列表中。

```json
{
  "type": "status",
  "data": {
    "status": "online",
    "state": {
      "current_temperature": 26.5,
      "target_temperature": 24,
      "humidity": 61,
      "mode": "auto"
    }
  },
  "ts": 1710000000000,
  "metadata": {
    "productId": "thermostat",
    "source": "existing-device"
  }
}
```

`status` 使用 `online`、`offline` 或 `error`。`state` 建议上报完整状态快照。设备断开前可以主动上报 `offline`；如果支持 MQTT Will Message，也可以把离线状态配置为遗嘱消息。

## 状态遥测

设备运行中向遥测主题上报状态变化。常用 `type` 是 `state`，`data` 中只放设备规格中定义过的遥测字段。

```json
{
  "type": "state",
  "data": {
    "current_temperature": 27.1,
    "target_temperature": 24,
    "humidity": 60,
    "mode": "auto"
  },
  "ts": 1710000005000,
  "metadata": {
    "productId": "thermostat",
    "source": "existing-device"
  }
}
```

建议在启动后、命令执行后和关键状态变化后上报完整快照。只上报部分字段时，控制台只能更新这些字段。

## 命令响应

当用户通过对话控制设备时，设备智能体会向命令主题发布命令。

```json
{
  "cmd": "set_target_temperature",
  "params": {
    "target_temperature": 24
  },
  "requestId": "req-001",
  "ts": 1710000010000
}
```

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `cmd` | string | 是 | 设备规格中定义的命令名。 |
| `params` | object | 否 | 命令参数，必须匹配该命令的参数定义。无参数时使用 `{}` 或省略。 |
| `requestId` | string | 是 | 本次命令请求 ID。 |
| `ts` | number | 否 | Unix 毫秒时间戳。 |

设备执行后向响应主题发布结果。`requestId` 必须和收到的命令一致。

```json
{
  "code": 0,
  "msg": "ok",
  "requestId": "req-001",
  "data": {
    "target_temperature": 24,
    "mode": "auto"
  },
  "ts": 1710000011000,
  "metadata": {
    "productId": "thermostat",
    "source": "existing-device"
  }
}
```

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `code` | number | 是 | `0` 表示成功，非 `0` 表示失败。 |
| `msg` | string | 是 | 执行结果说明。 |
| `requestId` | string | 是 | 必须等于命令中的 `requestId`。 |
| `data` | any | 否 | 返回给设备智能体的执行结果。 |
| `ts` | number | 否 | Unix 毫秒时间戳。 |

如果命令改变了设备状态，响应后立即再上报一次最新 `status` 或 `state`，用户才能在状态面板中看到执行结果。

## 设备事件

事件表示设备主动发生的一次性变化，例如告警、异常、阈值触发、任务完成或用户按键。设备向事件主题发布 `type: "event"` 的消息体。

```json
{
  "type": "event",
  "data": {
    "event": "temperature_alert",
    "current_temperature": 38.5,
    "level": "warning"
  },
  "ts": 1710000020000,
  "metadata": {
    "productId": "thermostat",
    "source": "existing-device"
  }
}
```

`data.event` 必须是设备规格中定义过的事件名，其余字段只能使用该事件定义的输出字段。事件不会自动写入当前数据，但会出现在最近上报事件中。阈值类事件建议做边沿触发或冷却时间，避免重复上报。

## 用 MQTTX 快速验证

在改造真实设备前，可以先用 MQTTX CLI 模拟一台设备。下面示例假设：

- MQTT 服务：`127.0.0.1:1883`
- `productId`：`thermostat`
- `deviceId`：`thermostat-001`

如果 MQTT 服务没有启用用户名和密码，去掉命令中的 `-u` 和 `-P`。

先打开一个终端订阅命令主题，用来观察设备智能体下发的命令：

```bash
mqttx sub \
  -h 127.0.0.1 \
  -p 1883 \
  -u your-username \
  -P 'your-password' \
  -q 1 \
  -t 'device-agent/thermostat/device/thermostat-001/commands'
```

再用另一个终端模拟设备上线：

```bash
mqttx pub \
  -h 127.0.0.1 \
  -p 1883 \
  -u your-username \
  -P 'your-password' \
  -q 1 \
  -t 'v1/thermostat/thermostat-001/telemetry' \
  -m '{"type":"status","data":{"status":"online","state":{"current_temperature":26.5,"target_temperature":24,"humidity":61,"mode":"auto"}},"metadata":{"productId":"thermostat","source":"mqttx"}}'
```

设备列表中出现 `thermostat-001` 后，可以继续模拟状态变化：

```bash
mqttx pub \
  -h 127.0.0.1 \
  -p 1883 \
  -u your-username \
  -P 'your-password' \
  -q 1 \
  -t 'v1/thermostat/thermostat-001/telemetry' \
  -m '{"type":"state","data":{"current_temperature":27.1,"target_temperature":24,"humidity":60,"mode":"auto"},"metadata":{"productId":"thermostat","source":"mqttx"}}'
```

也可以模拟设备主动上报事件：

```bash
mqttx pub \
  -h 127.0.0.1 \
  -p 1883 \
  -u your-username \
  -P 'your-password' \
  -q 1 \
  -t 'v1/thermostat/thermostat-001/event' \
  -m '{"type":"event","data":{"event":"temperature_alert","current_temperature":38.5,"level":"warning"},"metadata":{"productId":"thermostat","source":"mqttx"}}'
```

最后在对话中让设备智能体控制这台设备。订阅命令的终端会收到一条带 `requestId` 的命令，把这个 `requestId` 复制到响应消息中：

```bash
mqttx pub \
  -h 127.0.0.1 \
  -p 1883 \
  -u your-username \
  -P 'your-password' \
  -q 1 \
  -t 'device-agent/thermostat/device/thermostat-001/responses' \
  -m '{"code":0,"msg":"ok","requestId":"替换为收到的 requestId","data":{"target_temperature":24,"mode":"auto"},"metadata":{"productId":"thermostat","source":"mqttx"}}'
```

这组命令可以验证完整链路：设备上线、状态上报、事件上报、命令接收和命令响应。真实设备接入时，把这些 MQTTX 命令替换为固件、网关或后端服务中的 MQTT 发布订阅逻辑即可。

## 时间同步

如果设备需要估算本地时间与网关服务的时间差，可以使用时间同步主题。

设备向请求主题发布：

```json
{
  "deviceSendTime": 1710000030000
}
```

设备从响应主题收到：

```json
{
  "deviceSendTime": 1710000030000,
  "serverRecvTime": 1710000030120,
  "serverSendTime": 1710000030122
}
```

`deviceSendTime` 是设备发送请求时的本地 Unix 毫秒时间戳。`serverRecvTime` 和 `serverSendTime` 分别是网关服务收到请求和发出响应的时间。

## 验证接入

完成设备端适配后，按下面顺序检查：

1. 设备已连接 MQTT，命令主题已订阅成功。
2. 遥测主题收到第一条 `status` 或 `state` 后，设备列表中出现这台设备。
3. 当前数据能看到设备规格中定义的遥测字段。
4. 对话中下发命令后，设备端能收到命令，并向响应主题返回同一个 `requestId`。
5. 命令改变状态后，设备端有新的状态或遥测上报。
6. 主动上报事件后，最近上报事件中能看到记录。

如果设备没有出现，先检查 MQTT 服务地址和认证信息，再检查 `productId`、`deviceId`、主题路径和 `metadata.productId`。如果设备出现但当前数据不更新，优先检查遥测字段名和类型是否与设备规格一致。更多运行配置见 [配置](../operate-reference/configuration.md)。

## 下一步

- 阅读 [SDK 接入](./sdk-generation.md)，使用生成的设备端工程接入真实硬件。
- 阅读 [设备模拟器](./simulator.md)，在没有真实硬件时验证设备规格。
- 阅读 [使用设备智能体](../usage/use-device-agent.md)，了解如何选择设备并通过对话控制设备。
