# Distributed Collector Gateway - 使用说明
## 功能概述
本设备是一个分布式采集网关,支持通过 MQTT 远程控制 MODBUS RTU 轮询参数,并定期上报设备状态信息。
**新增功能**:
- ✅ **离线数据存储**:网络断开时自动存储数据到Flash
- ✅ **自动补传**:网络恢复后自动补传离线数据到MQTT服务器
- ✅ **SPIFFS文件系统**:ESP-IDF原生支持,稳定可靠
- ✅ **8MB存储容量**:可存储约11小时离线数据(注意:SPIFFS最小文件4KB)
## SNTP 时间同步
设备内置 SNTP(Simple Network Time Protocol)时间同步功能,在网络连接成功后会自动同步系统时间。
### 时间同步特性
- **自动同步**:获取 IP 地址后自动启动 SNTP 客户端
- **多服务器支持**:配置了中国地区多个 NTP 服务器
- `cn.pool.ntp.org` - 中国 NTP 服务器
- `ntp1.aliyun.com` - 阿里云 NTP 服务器
- `ntp.tencent.com` - 腾讯云 NTP 服务器
- **时区设置**:自动设置为北京时间(CST-8)
- **超时保护**:等待时间同步最长 10 秒,超时后使用本地时间继续运行
### 时间同步状态
设备会在启动日志中显示时间同步状态:
```
I (xxxx) SNTP_ESP: 初始化SNTP服务
I (xxxx) SNTP_ESP: 时区设置为北京时间 (CST-8)
I (xxxx) SNTP_ESP: 当前时间: 1970-01-01 08:00:00 Thursday (同步前)
I (xxxx) SNTP_ESP: 时间同步成功: 2026-02-01 15:30:45 (同步后)
I (xxxx) SNTP_ESP: 时间同步完成
```
### 时间在系统中的应用
同步后的时间会在以下场景中使用:
1. **设备状态上报** - `update_time` 字段显示精确的同步时间
2. **数据采集记录** - 可扩展用于记录数据采集时间戳
3. **日志时间戳** - 方便调试和问题追踪
### 注意事项
- 需要网络连接正常才能进行时间同步
- 首次启动时,时间同步可能需要几秒钟
- 如果网络连接异常,设备会使用本地时间继续工作
- 时间同步完成后,系统时间会持续由 SNTP 守护进程维护
## 设备状态上报
### 上报主题
`CONFIG_MQTT_PUB_TOPIC` (在 `sdkconfig` 中配置)
### 上报类型
设备会定期上报以下类型的数据:
1. **设备状态**(`message_type: "device_status"`)
2. **MODBUS 采集数据**(`function_code: 3`)
### 设备状态上报格式
```json
{
"message_type": "device_status",
"mac_address": "D0:CF:13:1B:C3:94",
"ip_address": "192.168.1.100",
"chip_model": "ESP32-S3",
"idf_version": "v5.5.2-dirty",
"uptime": 16,
"uptime_desc": "16秒",
"free_heap": 312996,
"status": "online",
"status_desc": "在线",
"update_time": "2026-02-01 15:30:45",
"led1_state": 1,
"led1_desc": "常亮",
"led1_function": "网络状态灯",
"led2_state": 4,
"led2_desc": "心跳",
"led2_function": "通信状态灯",
"modbus_enabled": 1,
"modbus_enabled_desc": "启用",
"modbus_channel": 0,
"modbus_channel_desc": "通道0 (UART0)",
"modbus_slave_addr": 1,
"modbus_interval": 1000,
"heap_status": "充足"
}
```
### 字段说明
#### 基本信息
| 字段 | 类型 | 说明 |
|------|------|------|
| `message_type` | string | 消息类型:`"device_status"` |
| `mac_address` | string | 设备 WiFi MAC 地址(唯一标识) |
| `ip_address` | string | 设备 IP 地址 |
| `chip_model` | string | 芯片型号 |
| `idf_version` | string | ESP-IDF 版本 |
#### 运行状态
| 字段 | 类型 | 说明 |
|------|------|------|
| `uptime` | number | 设备运行时间(秒) |
| `uptime_desc` | string | 运行时间中文描述(如:"1天5小时30分") |
| `free_heap` | number | 剩余堆内存(字节) |
| `heap_status` | string | 内存状态:`"充足"` / `"一般"` / `"紧张"` |
| `status` | string | 设备状态:`"online"` |
| `status_desc` | string | 设备状态中文描述:`"在线"` |
| `update_time` | string | 状态更新时间(YYYY-MM-DD HH:MM:SS,通过 SNTP 同步) |
#### LED 状态
| 字段 | 类型 | 说明 |
|------|------|------|
| `led1_state` | number | LED1 状态:0=关闭, 1=常亮, 2=慢闪, 3=快闪, 4=心跳 |
| `led1_desc` | string | LED1 状态中文描述 |
| `led1_function` | string | LED1 功能:`"网络状态灯"` |
| `led2_state` | number | LED2 状态:同 LED1 |
| `led2_desc` | string | LED2 状态中文描述 |
| `led2_function` | string | LED2 功能:`"通信状态灯"` |
#### MODBUS 轮询状态
| 字段 | 类型 | 说明 |
|------|------|------|
| `modbus_enabled` | number | MODBUS 轮询是否启用(0=禁用, 1=启用) |
| `modbus_enabled_desc` | string | 轮询状态中文描述:`"启用"` / `"禁用"` / `"未配置"` |
| `modbus_channel` | number | 当前使用的 RS485 通道(0 或 1) |
| `modbus_channel_desc` | string | 通道中文描述 |
| `modbus_slave_addr` | number | 当前轮询的从机地址 |
| `modbus_interval` | number | 当前轮询间隔(毫秒) |
### 中文提示字段说明
带有 `_desc` 后缀的字段是中文提示字段,设计用于:
- **直接显示在 Web 页面上**,无需额外转换
- **不会被程序解析**,仅用于展示
- **提升用户体验**,让状态更直观
Web 页面可以选择显示:
- 程序解析字段(如 `led1_state`)用于逻辑判断
- 中文描述字段(如 `led1_desc`)用于界面显示
### 上报时机
- MQTT 订阅成功后立即上报一次
- 之后每隔 10 秒上报一次(可在 main.c 中修改 `mqtt_start_device_status_task(10000)` 的参数)
---
## MODBUS 控制说明
本设备是一个分布式采集网关,支持通过 MQTT 远程控制 MODBUS RTU 轮询参数。
## MQTT 控制指令
### 控制主题
发布控制指令到订阅主题:`CONFIG_MQTT_SUB_TOPIC` (在 `sdkconfig` 中配置)
### 指令格式 (JSON)
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 0,
"reg_count": 2,
"interval": 1000,
"enabled": true
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `command` | string | 是 | 固定为 `"modbus_poll"` |
| `channel` | number | 是 | RS485 通道号 (0 或 1) |
| `slave_addr` | number | 是 | 从机地址 (1-247) |
| `start_addr` | number | 是 | 起始寄存器地址 (0-65535) |
| `reg_count` | number | 是 | 读取寄存器数量 (1-125) |
| `interval` | number | 是 | 轮询间隔(毫秒),最小 100ms |
| `enabled` | boolean | 否 | 是否启用轮询,默认 `true` |
## 使用示例
### 示例 1:读取设备地址 1 的寄存器
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 0,
"reg_count": 2,
"interval": 1000
}
```
设备会每 1 秒读取一次从机地址 1,从寄存器 0 开始的 2 个寄存器。
### 示例 2:读取设备地址 10 的寄存器
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 10,
"start_addr": 0,
"reg_count": 5,
"interval": 2000
}
```
设备会每 2 秒读取一次从机地址 10,从寄存器 0 开始的 5 个寄存器。
### 示例 3:读取指定范围的寄存器
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 10,
"reg_count": 4,
"interval": 500
}
```
设备会每 500ms 读取一次从机地址 1,从寄存器 10 开始的 4 个寄存器(地址 10, 11, 12, 13)。
### 示例 4:停止轮询
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 0,
"reg_count": 2,
"interval": 1000,
"enabled": false
}
```
设置 `"enabled": false` 可以暂停轮询任务。
### 示例 5:使用 RS485 通道 1
```json
{
"command": "modbus_poll",
"channel": 1,
"slave_addr": 5,
"start_addr": 0,
"reg_count": 10,
"interval": 1000
}
```
使用 RS485 通道 1 进行轮询。
## 数据上报
### 上报主题
`CONFIG_MQTT_PUB_TOPIC` (在 `sdkconfig` 中配置)
### 上报数据格式 (JSON)
```json
{
"channel": "RS485-1",
"slave_addr": 1,
"function_code": 3,
"status": "success",
"byte_count": 4,
"register_count": 2,
"registers": [
556,
998
]
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `channel` | string | RS485 通道名称 |
| `slave_addr` | number | 从机地址 |
| `function_code` | number | MODBUS 功能码(始终为 3) |
| `status` | string | 状态:`"success"` 或 `"exception"` |
| `byte_count` | number | 数据字节数 |
| `register_count` | number | 寄存器数量 |
| `registers` | array | 寄存器值数组 |
### 异常响应格式
```json
{
"channel": "RS485-1",
"slave_addr": 1,
"function_code": 131,
"status": "exception",
"exception_code": 2
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `exception_code` | number | 异常码:
1 - 非法功能
2 - 非法数据地址
3 - 非法数据值
4 - 服务器设备故障 |
## 动态更新
每次发送 MQTT 控制指令都会立即更新轮询参数,无需重启设备。
### 更新流程
1. 发送新的控制指令
2. 设备接收并解析 JSON
3. 立即更新轮询配置
4. 下一次轮询使用新参数
## MQTT 控制指令
### 控制主题
发布控制指令到订阅主题:`CONFIG_MQTT_SUB_TOPIC` (在 `sdkconfig` 中配置)
### 指令格式 (JSON)
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 0,
"reg_count": 2,
"interval": 1000,
"enabled": true
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `command` | string | 是 | 固定为 `"modbus_poll"` |
| `channel` | number | 是 | RS485 通道号 (0 或 1) |
| `slave_addr` | number | 是 | 从机地址 (1-247) |
| `start_addr` | number | 是 | 起始寄存器地址 (0-65535) |
| `reg_count` | number | 是 | 读取寄存器数量 (1-125) |
| `interval` | number | 是 | 轮询间隔(毫秒),最小 100ms |
| `enabled` | boolean | 否 | 是否启用轮询,默认 `true` |
### 支持的指令类型
当前支持以下控制指令:
#### 1. MODBUS 轮询控制
```json
{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 0,
"reg_count": 2,
"interval": 1000,
"enabled": true
}
```
#### 2. 设备状态上报间隔控制(可选扩展)
可通过修改 `main.c` 中的参数来调整状态上报间隔:
```c
mqtt_start_device_status_task(10000); // 10000ms = 10秒
```
或运行时调用 API:
```c
mqtt_update_report_interval(5000); // 5秒
```
## 离线数据存储
### 功能特性
设备支持离线数据存储功能,确保网络断开时数据不丢失:
- **自动检测**:实时监测网络状态(通过MQTT连接状态)
- **离线存储**:网络断开时自动存储数据到Flash
- **自动补传**:网络恢复后自动补传离线数据到MQTT服务器
- **LittleFS文件系统**:提供断电保护和磨损均衡
- **8MB存储容量**:可存储约11小时离线数据
### 存储容量
| 数据类型 | 数据量 | 可存储时长 |
|---------|--------|----------|
| MODBUS数据 | 200字节/条(1秒/次) | 约11小时(注意:SPIFFS最小文件4KB) |
### 工作流程
#### 正常模式(网络在线)
```
数据产生 → 直接发布到MQTT → 不存储
```
#### 离线模式(网络断开)
```
数据产生 → 存储到Flash SPIFFS → 等待网络恢复
```
#### 补传模式(网络恢复)
```
检测网络恢复 → 读取最旧数据 → 发布到MQTT → 删除已传数据 → 继续下一条
```
### 存储结构
```
/flash/
├── data/
│ ├── modbus/ # MODBUS采集数据
│ │ ├── 1738452345.json # 时间戳命名
│ │ └── ...
│ └── status/ # 设备状态数据
│ └── ...
└── index.json # 索引文件
```
### 日志示例
#### 存储离线数据
```
I (xxxx) mqtt_esp: Network is offline, stopping offline data upload
I (xxxx) mqtt_esp: Storing offline data (type=1, size=256)
I (xxxx) OFFLINE_STORAGE: Storage usage: 512 / 8388608 bytes (0.0%)
```
#### 补传离线数据
```
I (xxxx) mqtt_esp: Network is online, starting offline data upload
I (xxxx) mqtt_esp: Found 10 offline data files, uploading...
I (xxxx) mqtt_esp: Publishing offline data (type=1, size=256)
I (xxxx) mqtt_esp: Offline data published successfully, msg_id=12345
```
### 存储策略
- **自动循环覆盖**:最多存储10,000个文件,超出自动删除最旧数据
- **时间戳排序**:按时间戳顺序补传,确保数据时间顺序
- **原子写入**:使用临时文件+重命名机制
- **断电保护**:SPIFFS提供基本的断电保护
### 注意事项
1. **首次启动**:会自动初始化并挂载SPIFFS文件系统
2. **空间管理**:存储满后自动循环覆盖最旧数据
3. **补传速率**:每秒处理一条数据,避免阻塞新数据上传
4. **Flash寿命**:SPIFFS无磨损均衡,建议定期格式化
5. **最小文件限制**:SPIFFS最小文件4KB,小文件会浪费空间
6. **格式化**:如需清空所有数据,可调用 `flash_spiffs_format()`
详细文档请参考:[离线数据存储和补传功能说明](docs/OFFLINE_STORAGE.md)
## 注意事项
1. **轮询间隔最小为 100ms**,设置更小的值会被拒绝
2. **寄存器数量最大为 125**,MODBUS RTU 限制
3. **从机地址范围 1-247**,0 为广播地址
4. **通道号只能是 0 或 1**
5. 首次发送指令后会自动启动轮询任务
6. **离线存储容量约11小时**,超出会循环覆盖最旧数据
## 使用 mosquitto_cli 测试
### 发送控制指令
```bash
mosquitto_pub -h -p 1883 -t -m '{
"command": "modbus_poll",
"channel": 0,
"slave_addr": 1,
"start_addr": 0,
"reg_count": 2,
"interval": 1000
}'
```
### 监听数据上报
```bash
mosquitto_sub -h -p 1883 -t
```
## 硬件连接
### RS485 通道 0 (UART0)
| 引脚 | GPIO | 功能 |
|------|------|------|
| RO (RX) | GPIO 41 | RS485 接收器输出 |
| DE/RE | GPIO 42 | 数据使能/接收器使能 |
| DI (TX) | GPIO 44 | RS485 驱动器输入 |
### RS485 通道 1 (UART2)
| 引脚 | GPIO | 功能 |
|------|------|------|
| RO (RX) | GPIO 43 | RS485 接收器输出 |
| DE/RE | GPIO 2 | 数据使能/接收器使能 |
| DI (TX) | GPIO 1 | RS485 驱动器输入 |