本文档持续更新,记录 MPPT Monitor 的完整技术架构、协议细节、API 设计。
开发一个轻量级 C 语言嵌入式监控程序,通过 Modbus RTU over TCP 协议读取 MPPT 太阳能充电控制器的实时数据,并通过 HTTP REST API 对外提供 JSON 格式的实时数据接口。支持历史数据定时存储到 SQLite3 数据库,并提供带趋势图表的历史数据查询页面。
┌─────────────────────────────────────────────────────────┐
│ MPPT Monitor │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Modbus TCP │ │ Mongoose HTTP│ │
│ │ Client │ │ Server :8085 │ │
│ │ (Thread) │ │ (Main Thread)│ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ Shared Data (mppt_data_t) │ │
│ │ Protected by pthread_mutex │ │
│ └──────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ SQLite3 Database │ │
│ │ (历史数据存储) │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ MPPT Device │ │ Web Browser / │
│ TCP:40635 │ │ REST Client │
└─────────────┘ └─────────────────┘
| 线程 | 职责 | 说明 |
|---|---|---|
| Main Thread | Mongoose HTTP 事件循环 | 处理 HTTP 请求、Web 服务器看门狗 |
| Modbus Thread | Modbus 数据采集 | 每 2 秒读取一次寄存器,自动重连 |
| 参数 | 值 |
|---|---|
| 协议 | Modbus RTU over TCP(原始 TCP Socket) |
| 目标地址 | discover.zhonjin.com |
| 目标端口 | 40635 |
| 从机地址 | 0x01 |
| 功能码 | 0x04(读输入寄存器) |
| 起始寄存器 | 0x0000(对应 30001) |
| 寄存器数量 | 10(30001~30010) |
| 超时时间 | 10 秒 |
| 轮询间隔 | 2000ms |
发→ 01 04 00 00 00 0A 70 0D
字节解析:
01 - 从机地址 (Slave Address)
04 - 功能码: 读输入寄存器 (Read Input Registers)
00 00 - 起始寄存器地址 (0x0000 = 30001)
00 0A - 寄存器数量 (10个)
70 0D - CRC-16 校验 (低字节在前)
收← 01 04 14 01 0C 01 81 00 10 00 07 00 2E 00 14 00 60 00 02 00 01 00 3C E9 F1
字节解析:
01 - 从机地址
04 - 功能码
14 - 数据长度 (20字节 = 10个寄存器 × 2字节)
01 0C - 寄存器1: 电池电压 (268 → 26.8V)
01 81 - 寄存器2: 光伏电压 (385 → 38.5V)
00 10 - 寄存器3: 光伏充电电流 (16 → 1.6A)
00 07 - 寄存器4: 累计发电量 (7 → 0.7kWh)
00 2E - 寄存器5: 机器温度 (46 → 46°C)
00 14 - 寄存器6: 故障代码 (20 → 正常)
00 60 - 寄存器7: 电池电量 (96 → 96%)
00 02 - 寄存器8: 电池串数 (2)
00 01 - 寄存器9: 充电状态 (1 → 充电中)
00 3C - 寄存器10: 设备类型 (60)
E9 F1 - CRC-16 校验
| 寄存器 | 地址 | 参数名 | 原始值 | 实际值 | 单位 | 说明 |
|---|---|---|---|---|---|---|
| 30001 | 0x0000 | 电池电压 | raw/10 | V | 24V系统正常范围 20-30V | |
| 30002 | 0x0001 | 光伏电压 | raw/10 | V | 光伏板输出电压 | |
| 30003 | 0x0002 | 光伏充电电流 | raw/10 | A | 当前充电电流 | |
| 30004 | 0x0003 | 累计发电量 | raw/10 | kWh | 历史总发电量 | |
| 30005 | 0x0004 | 机器温度 | raw | °C | 控制器温度 | |
| 30006 | 0x0005 | 故障代码 | raw | - | 0/20=正常 | |
| 30007 | 0x0006 | 电池电量 | raw | % | SOC 百分比 | |
| 30008 | 0x0007 | 电池串数 | raw | - | 保留字段 | |
| 30009 | 0x0008 | 充电状态 | raw | - | 0=空闲 1=充电 2=吸收 3=浮充 | |
| 30010 | 0x0009 | 设备类型 | raw | - | 设备型号代码 |
多项式: 0xA001 (0x8005 的反转)
初始值: 0xFFFF
字节序: 低字节在前 (Little-Endian on wire)
算法流程:
1. 初始化 CRC = 0xFFFF
2. 对每个字节:
a. CRC ^= byte
b. 循环 8 次:
- 如果 CRC 最低位为 1: CRC = (CRC >> 1) ^ 0xA001
- 否则: CRC >>= 1
3. 返回 CRC 值
| 参数 | 值 |
|---|---|
| 监听地址 | 0.0.0.0 |
| 监听端口 | 8085 |
| HTTP 库 | Mongoose 7.22 |
| JSON 库 | cJSON 1.7.19 |
响应示例:
{
"timestamp": "2026-08-04T15:30:00",
"data_valid": true,
"battery": {
"voltage": 26.8,
"soc": 96,
"string_count": 2
},
"pv": {
"voltage": 38.5,
"current": 1.6,
"power": 61.6
},
"cumulative_energy_kwh": 0.7,
"device": {
"temperature": 46.0,
"fault_code": 20,
"fault_desc": "Normal",
"charge_state": 1,
"charge_state_desc": "Charging (Bulk)",
"device_type": 60
},
"raw_registers": [268, 385, 16, 7, 46, 20, 96, 2, 1, 60]
}
响应示例:
{
"connection": {
"state": "connected",
"host": "discover.zhonjin.com",
"port": 40635,
"reconnect_count": 0
},
"device_online": true,
"consecutive_failures": 0,
"last_update": "2026-08-04T15:30:00"
}
响应:
{"status": "ok"}
返回内嵌的 HTML 仪表盘页面,每 2 秒自动刷新数据。
| 旧 API (7.x 早期) | 新 API (7.22) | 说明 |
|---|---|---|
mg_http_match_uri(hm, "/path") |
mg_match(hm->uri, mg_str("/path"), NULL) |
URI 匹配 |
struct mg_str { const char *ptr; size_t len; } |
struct mg_str { char *buf; size_t len; } |
字符串结构体 |
hm->uri.ptr |
hm->uri.buf |
访问字符串数据 |
URI 匹配: 使用 mg_match() 替代 mg_http_match_uri()
// 旧方式(已废弃)
if (mg_http_match_uri(hm, "/api/data")) { ... }
// 新方式(mongoose-7.22)
if (mg_match(hm->uri, mg_str("/api/data"), NULL)) { ... }
字符串结构体: ptr 改为 buf
// 旧方式
const char *data = hm->uri.ptr;
// 新方式
char *data = hm->uri.buf;
通配符匹配: 支持 ?(单字符)、*(不含/)、#(含/)
// 匹配 /api/ 下所有路径
mg_match(hm->uri, mg_str("/api/#"), NULL)
连接失败 → 等待 2 秒 → 重试
读取超时 → 记录失败计数
连续 3 次失败 → 断开连接 → 重新建立连接
DNS 解析失败 → 等待后重试
主循环每 5 秒检查一次 Web 服务器状态
如果监听连接丢失 → 自动重新启动 HTTP 监听
SIGPIPE 信号被忽略 → 防止客户端断开导致进程退出
Modbus 线程写入数据时使用 pthread_mutex 保护
Web 线程读取数据时加锁读取
确保 HTTP 响应中的数据不会在序列化过程中被修改
| 指标 | 值 | 说明 |
|---|---|---|
| 数据采集间隔 | 2 秒 | 可通过 MODBUS_POLL_MS 调整 |
| HTTP 响应延迟 | < 1ms | 纯内存数据,无磁盘 IO |
| 内存占用 | ~2MB | 含 Mongoose 缓冲区 |
| CPU 占用 | < 1% | 空闲时几乎不消耗 CPU |
| 网络开销 | ~30 bytes/2s | Modbus 请求/响应 |
[modbus]
host = discover.zhonjin.com
port = 40635
slave_addr = 0x01
timeout_sec = 10
poll_ms = 2000
[storage]
interval_sec = 10
db_path = mppt_data.db
[web]
port = 8085
page_size = 50
| 配置段 | 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
| modbus | host | string | discover.zhonjin.com | Modbus 设备域名/IP |
| modbus | port | int | 40635 | Modbus TCP 端口 |
| modbus | slave_addr | hex | 0x01 | Modbus 从机地址 |
| modbus | timeout_sec | int | 10 | 连接超时(秒) |
| modbus | poll_ms | int | 2000 | 轮询间隔(毫秒) |
| storage | interval_sec | int | 10 | 数据存储间隔(秒) |
| storage | db_path | string | mppt_data.db | SQLite 数据库文件路径 |
| web | port | int | 8085 | HTTP 服务端口 |
| web | page_size | int | 50 | 历史数据默认分页大小 |
0x 前缀(如 slave_addr = 0x01)使用 SQLite3 作为轻量级嵌入式数据库,无需额外服务进程。
CREATE TABLE IF NOT EXISTS history_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL, -- Unix 时间戳
battery_voltage REAL NOT NULL, -- 电池电压 (V)
pv_voltage REAL NOT NULL, -- 光伏电压 (V)
pv_charge_current REAL NOT NULL, -- 光伏充电电流 (A)
cumulative_energy REAL NOT NULL, -- 累计发电量 (kWh)
machine_temp REAL NOT NULL, -- 机器温度 (°C)
fault_code INTEGER NOT NULL, -- 故障代码
battery_soc INTEGER NOT NULL, -- 电池电量 (%)
battery_string INTEGER NOT NULL, -- 电池串数
charge_state INTEGER NOT NULL, -- 充电状态
device_type INTEGER NOT NULL, -- 设备类型
pv_input_power REAL NOT NULL, -- 光伏输入功率 (W)
created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history_data(timestamp);
Modbus 读取成功 → 更新共享数据 → 检查存储间隔
│
┌───────────────┴───────────────┐
│ 距上次存储 >= interval_sec? │
│ │
▼ Yes ▼ No
插入 SQLite 跳过
更新 last_store_time
GET /api/history
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| start_time | string | 否 | 起始时间,格式 YYYY-MM-DDTHH:MM:SS 或 YYYY-MM-DD HH:MM:SS |
| end_time | string | 否 | 结束时间,格式同上 |
| page | int | 否 | 页码,默认 1 |
| page_size | int | 否 | 每页条数,默认取 config.web.page_size,最大 500 |
| sort | string | 否 | 排序方式:ASC 或 DESC(默认) |
响应示例:
{
"total": 150,
"page": 1,
"page_size": 50,
"total_pages": 3,
"records": [
{
"id": 150,
"timestamp": "2026-08-04 16:30:00",
"timestamp_unix": 1785832200,
"battery_voltage": 26.8,
"pv_voltage": 37.2,
"pv_charge_current": 2.2,
"pv_input_power": 81.84,
"cumulative_energy": 0.8,
"machine_temp": 45,
"fault_code": 20,
"fault_desc": "Normal",
"battery_soc": 96,
"charge_state": 1,
"charge_state_desc": "Charging (Bulk)",
"device_type": 60
}
]
}
| 方面 | 措施 |
|---|---|
| 网络隔离 | Modbus TCP 仅出站连接,不监听外部端口 |
| HTTP 访问 | 当前无认证,建议通过反向代理添加 Basic Auth |
| 数据完整性 | Modbus CRC-16 校验确保数据正确性 |
| 异常输入 | HTTP 路由严格匹配,未匹配返回 404 |
| 内存安全 | 使用 cJSON 安全 API,无缓冲区溢出风险 |
| SQL 注入 | 使用 SQLite3 参数化查询(? 占位符),杜绝注入风险 |
| 配置安全 | config.ini 解析使用 snprintf 防溢出,非法值回退默认 |
最后更新: 2026-08-04 (v1.1.0)