ソースを参照

fix: 深度修复Web设备参数设置写入失败bug

- 修复FC 0x10写多寄存器响应帧解析错误(8字节规范,CRC位置)
- 新增sock_mutex串行化轮询与写入线程的Socket收发
- 修复写入失败500响应缺失CORS头
- 快速识别FC 0x10异常响应帧(5字节)
- 更新CHANGELOG v2.3.3、TECHNICAL并发模型与写入规范、FAQ归档
- 打包完整压缩包

Coze-Commit-Type: user
Coze-User-ID: 256649704250362
Coze-Conversation-ID: 11640025
steven_roc 1 ヶ月 前
親
コミット
5445787051

+ 44 - 0
mppt_monitor/CHANGELOG.md

@@ -4,6 +4,50 @@
 
 ---
 
+## [2.3.3] - 2026-09-03
+
+### 关键 Bug 修复 - HTTP Web Modbus 设备参数设置写入失败
+
+**问题现象**: 通过 Web 界面设置设备参数(如 Battery Type 电池类型)时,前端提示 `✗ 写入失败: ...`,且 Modbus 实际写入极不稳定。
+
+**根因分析(深度排查)**:
+
+1. **【致命】FC 0x10 写多寄存器响应帧长度与 CRC 解析错误**
+   - Modbus 功能码 `0x10`(写多个寄存器)的**正常响应帧为 8 字节**:
+     `地址(1) + 功能码(1) + 起始地址(2) + 寄存器数量(2) + CRC(2)`
+   - 但 `modbus_write_register()` 中:
+     - 缓冲区声明为 `uint8_t resp[6]`,只读 6 字节;
+     - CRC 校验从 `resp[4] | resp[5]<<8` 读取,而 `resp[4]/resp[5]` 实际是"寄存器数量(0x00, 0x01)",**并非 CRC**。
+   - 结果:**每次写入都会因 CRC 校验必然失败而返回失败**,这是 Web 写入一直报错的根本原因。
+   - 修复:缓冲区扩大为 8 字节,正确从 `resp[6]/resp[7]` 读取 CRC,并校验起始地址回显与寄存器数量回显(须为 1)。
+
+2. **【并发】写入线程与轮询线程并行访问同一 TCP Socket**
+   - Modbus 轮询线程(`poll_thread`)与 Web 设置写入(`modbus_write_register`)在不同线程中同时对同一个 TCP Socket 收发,没有任何互斥保护。
+   - 结果:写入期间轮询命令/响应相互交错,导致响应数据错乱、地址不匹配、超时等偶发故障。
+   - 修复:引入专用互斥锁 `sock_mutex`,串行化所有 Socket 的 `send/recv` 操作(`modbus_send_recv` 与 `modbus_write_register` 均在持有锁的窗口内完成收发),彻底消除帧交错。
+
+3. **写入失败 500 响应缺少 CORS 头**
+   - `handle_api_settings()` 中写入失败的 `500` 响应缺少 `Access-Control-Allow-Origin: *`,浏览器因 CORS 拦截无法读取后端错误信息,前端只能显示笼统的请求失败提示。
+   - 修复:为 500 错误响应补齐 CORS 头,保证前端可正常展示具体错误原因。
+
+4. **异常响应帧(5 字节)等待超时**
+   - FC 0x10 的异常响应帧为 5 字节(地址+0x90+异常码+CRC2),原逻辑强制等满 6 字节,异常时白白超时。
+   - 修复:检测到 `resp[1] & 0x80`(异常标识)后立即以 5 字节结束读取,快速反馈设备拒绝码。
+
+**影响文件**:
+- `src/modbus_client.c`:重写 `modbus_write_register()` 响应解析;`modbus_send_recv()` 增加 `sock_mutex` 保护;`modbus_init()`/`modbus_stop()` 初始化和销毁互斥锁;`read_input_registers()`/`read_output_registers()`/`poll_device()` 适配传递互斥锁。
+- `src/modbus_client.h`:结构体新增 `pthread_mutex_t sock_mutex` 字段。
+- `src/web_server.c`:写入失败 500 响应补充 CORS 头。
+
+**验证结果**:
+```bash
+$ make clean && make
+Build complete: build/mppt_monitor
+# 零警告零错误
+```
+
+---
+
 ## [2.3.2] - 2026-08-28
 
 ### Bug 修复 - 编译警告和 POSIX 兼容性

+ 18 - 1
mppt_monitor/FAQ.md

@@ -250,8 +250,9 @@ sudo iptables -L -n | grep 40635
 ```json
 {"error": "Modbus write failed, CRC mismatch"}
 ```
+或 Web 界面提示 `✗ 写入失败: Modbus写入失败,请检查设备连接`
 
-**原因**:
+**可能原因**:
 1. 设备离线
 2. 寄存器地址不可写
 3. 参数值超出范围
@@ -270,6 +271,21 @@ curl http://localhost:5000/api/settings
 # device_address: 1-247
 ```
 
+**⚠️ v2.3.3 深度修复(查阅 CHANGELOG 2.3.3)**:
+
+> 此前版本 Web 写入基本每次都失败,根因有二,均已升级到 v2.3.3 修复:
+
+1. **FC 0x10 写响应解析错误(最关键)**:
+   - 写多寄存器(FC 0x10)正常响应应为 **8 字节**,但旧代码按 6 字节解析,
+     并把"寄存器数量(0x00, 0x01)"误当成 CRC 校验 → **每次写入 CRC 都必然失败**。
+   - 修复:按 8 字节规范解析,正确从第 6、7 字节读 CRC,并校验起始地址/数量回显。
+
+2. **Socket 并发访问冲突**:
+   - 轮询线程与 Web 写入线程并行读写**同一个 TCP Socket**,无互斥 → 帧交错导致数据错乱。
+   - 修复:新增 `sock_mutex` 串行化所有 Socket 收发。
+
+> 若升级后仍失败,多半是真实业务问题(值超范围 / 设备拒绝 / 离线),按上方步骤排查。
+
 ---
 
 ## 4. HTTP API 问题
@@ -461,6 +477,7 @@ sqlite3 mppt_data.db "VACUUM;"
 
 | 日期 | 版本 | 更新内容 |
 |------|------|---------|
+| 2026-09-03 | 2.3.3 | 归档"设备参数写入失败"深度排查:FC 0x10 响应解析错误 + Socket 并发冲突 |
 | 2026-08-28 | 1.0 | 初始版本,归档所有已知问题 |
 
 ---

+ 39 - 0
mppt_monitor/TECHNICAL.md

@@ -687,6 +687,45 @@ Web 线程读取数据时加锁读取
 确保 HTTP 响应中的数据不会在序列化过程中被修改
 ```
 
+### 5.4 设备参数写入(Web → Modbus)与并发保护(v2.3.3)
+
+Web 设备参数设置(`POST /api/settings` → `modbus_write_register()`)与
+Modbus 轮询线程共用**同一个 TCP Socket**。为避免帧交错导致的写入失败/数据错乱,
+采用如下并发模型:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│  sock_mutex (pthread_mutex_t)  串行化所有 Socket 收发操作      │
+├─────────────────────────────────────────────────────────────┤
+│  轮询线程: read_input/read_output → modbus_send_recv()        │
+│           (内部加锁: drain → send → recv → 解锁)              │
+│  写入线程: modbus_write_register()                            │
+│           (加锁: drain → send(frame) → recv(resp) → 解锁)     │
+└─────────────────────────────────────────────────────────────┘
+```
+
+**关键点**:
+- 每条 Modbus 命令(发送 + 接收完整响应)都在一次性持锁窗口内完成,杜绝并发帧交错。
+- `data_mutex` 负责设备数据快照的读写一致性,与 `sock_mutex` 职责分离,避免死锁。
+
+### 5.5 写多寄存器(FC 0x10)响应帧规范(v2.3.3 修复)
+
+Modbus 功能码 `0x10`(写多个寄存器)的**正常响应帧为 8 字节**:
+
+| 字节 | 内容 | 说明 |
+|------|------|------|
+| 0 | 设备地址 | 回显请求地址 |
+| 1 | 功能码 `0x10` | 写多个寄存器 |
+| 2-3 | 起始地址 | 回显写入的起始寄存器地址 |
+| 4-5 | 寄存器数量 | 回显写入的数量(本系统为 1) |
+| 6-7 | CRC-16 | 前 6 字节 CRC |
+
+异常响应帧为 5 字节(地址 + `0x90` + 异常码 + CRC-2)。
+
+> ⚠️ **Bug 历史**:v2.3.2 及以前版本将正常响应错误地按 6 字节解析,
+> 并把"寄存器数量(0x00,0x01)"误当成 CRC 校验,导致**每次写入都必然 CRC 失败**
+> (Web 设置恒报"写入失败")。v2.3.3 已修复为按 8 字节规范解析。
+
 ---
 
 ## 六、性能参数

BIN
mppt_monitor/build/mppt_monitor


+ 85 - 25
mppt_monitor/src/modbus_client.c

@@ -206,15 +206,20 @@ static void drain_socket(int sockfd)
 }
 
 /* ---- Send Modbus RTU frame and receive complete response ---- */
-static int modbus_send_recv(int sockfd, const uint8_t *tx_frame, int tx_len,
+static int modbus_send_recv(int sockfd, pthread_mutex_t *sock_mutex,
+                            const uint8_t *tx_frame, int tx_len,
                             uint8_t *rx_buf, int rx_buf_size, int timeout_sec)
 {
+    /* Serialize with write operations (both share the same TCP socket) */
+    pthread_mutex_lock(sock_mutex);
+
     /* Drain any leftover data before sending */
     drain_socket(sockfd);
     
     /* Send */
     ssize_t sent = send(sockfd, tx_frame, tx_len, 0);
     if (sent != tx_len) {
+        pthread_mutex_unlock(sock_mutex);
         return -1;
     }
 
@@ -231,10 +236,11 @@ static int modbus_send_recv(int sockfd, const uint8_t *tx_frame, int tx_len,
         tv.tv_usec = 0;
 
         int ret = select(sockfd + 1, &rfds, NULL, NULL, &tv);
-        if (ret < 0) return -1;  /* Error */
+        if (ret < 0) { pthread_mutex_unlock(sock_mutex); return -1; }  /* Error */
         if (ret == 0) {
             /* Timeout */
             if (total_received > 0) break;  /* Got some data, use it */
+            pthread_mutex_unlock(sock_mutex);
             return -1;  /* No data at all */
         }
 
@@ -247,6 +253,7 @@ static int modbus_send_recv(int sockfd, const uint8_t *tx_frame, int tx_len,
                 usleep(10000);  /* 10ms */
                 continue;
             }
+            pthread_mutex_unlock(sock_mutex);
             return -1;
         }
         if (received == 0) break;  /* Connection closed */
@@ -267,11 +274,12 @@ static int modbus_send_recv(int sockfd, const uint8_t *tx_frame, int tx_len,
         }
     }
 
+    pthread_mutex_unlock(sock_mutex);
     return total_received;
 }
 
 /* ---- Build and send read input registers command (0x04) ---- */
-static int read_input_registers(int sockfd, uint8_t addr,
+static int read_input_registers(modbus_client_t *client, uint8_t addr,
                                 uint16_t start_reg, uint16_t count,
                                 uint16_t *regs, int timeout_sec)
 {
@@ -287,7 +295,8 @@ static int read_input_registers(int sockfd, uint8_t addr,
     tx[7] = (crc >> 8) & 0xFF; /* CRC high */
 
     uint8_t rx[512];
-    int rx_len = modbus_send_recv(sockfd, tx, 8, rx, sizeof(rx), timeout_sec);
+    int rx_len = modbus_send_recv(client->sockfd, &client->sock_mutex,
+                                  tx, 8, rx, sizeof(rx), timeout_sec);
     if (rx_len < 0) {
         return -1;
     }
@@ -339,7 +348,7 @@ static int read_input_registers(int sockfd, uint8_t addr,
 }
 
 /* ---- Build and send read output registers command (0x03) ---- */
-static int read_output_registers(int sockfd, uint8_t addr,
+static int read_output_registers(modbus_client_t *client, uint8_t addr,
                                  uint16_t start_reg, uint16_t count,
                                  uint16_t *regs, int timeout_sec)
 {
@@ -355,7 +364,8 @@ static int read_output_registers(int sockfd, uint8_t addr,
     tx[7] = (crc >> 8) & 0xFF;
 
     uint8_t rx[512];
-    int rx_len = modbus_send_recv(sockfd, tx, 8, rx, sizeof(rx), timeout_sec);
+    int rx_len = modbus_send_recv(client->sockfd, &client->sock_mutex,
+                                  tx, 8, rx, sizeof(rx), timeout_sec);
     if (rx_len < 0) {
         return -1;
     }
@@ -403,7 +413,7 @@ static int poll_device(modbus_client_t *client, mppt_device_data_t *dev)
     int ret;
 
     /* Read Input Registers (0x04): 11 registers starting at 0x0000 */
-    ret = read_input_registers(client->sockfd, dev->device_addr,
+    ret = read_input_registers(client, dev->device_addr,
                                0x0000, MODBUS_INPUT_REG_COUNT,
                                regs, client->config.connect_timeout_sec);
     if (ret < 0) {
@@ -427,7 +437,7 @@ static int poll_device(modbus_client_t *client, mppt_device_data_t *dev)
     /* Protocol spec: two commands must have 1 second interval */
     usleep(1000000);  /* 1 second delay per protocol specification */
 
-    ret = read_output_registers(client->sockfd, dev->device_addr,
+    ret = read_output_registers(client, dev->device_addr,
                                 0x0000, MODBUS_OUTPUT_REG_COUNT,
                                 regs, client->config.connect_timeout_sec);
     if (ret >= 0) {
@@ -576,6 +586,7 @@ void modbus_init(modbus_client_t *client, const modbus_config_t *config)
     client->running = false;
     client->device_count = config->device_count;
     pthread_mutex_init(&client->data_mutex, NULL);
+    pthread_mutex_init(&client->sock_mutex, NULL);
 
     /* Initialize device entries */
     for (int i = 0; i < config->device_count && i < MODBUS_MAX_DEVICES; i++) {
@@ -608,6 +619,7 @@ void modbus_stop(modbus_client_t *client)
     }
     client->connected = false;
     pthread_mutex_destroy(&client->data_mutex);
+    pthread_mutex_destroy(&client->sock_mutex);
 }
 
 void modbus_get_all_data(modbus_client_t *client,
@@ -671,7 +683,16 @@ int modbus_validate_setting(uint16_t reg_addr, uint16_t value, uint16_t battery_
 int modbus_write_register(modbus_client_t *ctx, uint8_t dev_addr,
                           uint16_t reg_addr, uint16_t value)
 {
-    if (!ctx || !ctx->connected) return -1;
+    if (!ctx) return -1;
+
+    /* Serialize socket access with the polling thread (both share the same TCP socket) */
+    pthread_mutex_lock(&ctx->sock_mutex);
+
+    if (!ctx->connected || ctx->sockfd < 0) {
+        pthread_mutex_unlock(&ctx->sock_mutex);
+        fprintf(stderr, "[MODBUS-WRITE] Not connected\n");
+        return -1;
+    }
 
     /* Build Write Multiple Registers frame (0x10) */
     uint8_t frame[11];
@@ -696,60 +717,99 @@ int modbus_write_register(modbus_client_t *ctx, uint8_t dev_addr,
     ssize_t sent = send(ctx->sockfd, frame, 11, 0);
     if (sent != 11) {
         fprintf(stderr, "[MODBUS-WRITE] Send failed: %zd/11\n", sent);
+        pthread_mutex_unlock(&ctx->sock_mutex);
         return -1;
     }
 
     printf("[MODBUS-WRITE] Sent to dev %d reg 0x%04X value %u\n",
            dev_addr, reg_addr, value);
 
-    /* Read response (should be 6 bytes: addr + 0x10 + start_addr(2) + count(2) + crc(2)) */
-    uint8_t resp[6];
+    /*
+     * Read response.
+     * Normal FC 0x10 response is 8 bytes:
+     *   addr(1) + func(1) + start_addr(2) + quantity(2) + crc(2)
+     * Exception response is 5 bytes:
+     *   addr(1) + func|0x80(1) + exception_code(1) + crc(2)
+     */
+    uint8_t resp[8];
     ssize_t total = 0;
     int retries = 0;
-    while (total < 6 && retries < 20) {
+    while (total < (ssize_t)sizeof(resp) && retries < 60) {
         struct timeval tv = { .tv_sec = 0, .tv_usec = 100000 }; /* 100ms */
         fd_set readfds;
         FD_ZERO(&readfds);
         FD_SET(ctx->sockfd, &readfds);
         int ret = select(ctx->sockfd + 1, &readfds, NULL, NULL, &tv);
         if (ret > 0) {
-            ssize_t n = recv(ctx->sockfd, resp + total, 6 - total, 0);
+            ssize_t n = recv(ctx->sockfd, resp + total,
+                             (ssize_t)sizeof(resp) - total, 0);
             if (n <= 0) break;
             total += n;
+            /* Exception response: 5 bytes is enough */
+            if (total >= 5 && (resp[1] & 0x80)) break;
+            if (total >= 8) break;  /* Normal response complete */
         } else {
             retries++;
         }
     }
 
-    if (total < 6) {
-        fprintf(stderr, "[MODBUS-WRITE] Response too short: %zd/6 bytes\n", total);
+    if (total < 5) {
+        fprintf(stderr, "[MODBUS-WRITE] Response too short: %zd bytes (< 5)\n", total);
+        pthread_mutex_unlock(&ctx->sock_mutex);
         return -1;
     }
 
-    /* Verify response */
+    /* Verify address */
     if (resp[0] != dev_addr) {
         fprintf(stderr, "[MODBUS-WRITE] Address mismatch: expected %d, got %d\n",
                 dev_addr, resp[0]);
+        pthread_mutex_unlock(&ctx->sock_mutex);
+        return -1;
+    }
+    /* Verify function code byte (check exception flag) */
+    if (resp[1] != 0x10) {
+        if (resp[1] & 0x80) {
+            fprintf(stderr, "[MODBUS-WRITE] Error response: code 0x%02X\n", resp[2]);
+        } else {
+            fprintf(stderr, "[MODBUS-WRITE] Unexpected function code: 0x%02X\n", resp[1]);
+        }
+        pthread_mutex_unlock(&ctx->sock_mutex);
         return -1;
     }
-    if (resp[1] == 0x90) {
-        /* Error response: 0x10 + 0x80 = 0x90 */
-        fprintf(stderr, "[MODBUS-WRITE] Error response: code %d\n", resp[2]);
+    /* Normal response must be 8 bytes */
+    if (total < 8) {
+        fprintf(stderr, "[MODBUS-WRITE] Response too short: %zd/8 bytes\n", total);
+        pthread_mutex_unlock(&ctx->sock_mutex);
         return -1;
     }
-    if (resp[1] != 0x10) {
-        fprintf(stderr, "[MODBUS-WRITE] Unexpected function code: 0x%02X\n", resp[1]);
+
+    /* Verify start address echoed */
+    uint16_t resp_reg = (uint16_t)((resp[2] << 8) | resp[3]);
+    if (resp_reg != reg_addr) {
+        fprintf(stderr, "[MODBUS-WRITE] Start address mismatch: expected 0x%04X, got 0x%04X\n",
+                reg_addr, resp_reg);
+        pthread_mutex_unlock(&ctx->sock_mutex);
+        return -1;
+    }
+    /* Verify register quantity echoed (must be 1) */
+    uint16_t resp_qty = (uint16_t)((resp[4] << 8) | resp[5]);
+    if (resp_qty != 1) {
+        fprintf(stderr, "[MODBUS-WRITE] Quantity mismatch: expected 1, got %u\n", resp_qty);
+        pthread_mutex_unlock(&ctx->sock_mutex);
         return -1;
     }
 
-    /* Verify CRC */
-    uint16_t recv_crc = resp[4] | (resp[5] << 8);
-    uint16_t calc_crc = crc16_modbus(resp, 4);
+    /* Verify CRC over first 6 bytes */
+    uint16_t recv_crc = (uint16_t)(resp[6] | (resp[7] << 8));
+    uint16_t calc_crc = crc16_modbus(resp, 6);
     if (recv_crc != calc_crc) {
-        fprintf(stderr, "[MODBUS-WRITE] CRC mismatch\n");
+        fprintf(stderr, "[MODBUS-WRITE] CRC mismatch: recv=0x%04X calc=0x%04X\n",
+                recv_crc, calc_crc);
+        pthread_mutex_unlock(&ctx->sock_mutex);
         return -1;
     }
 
+    pthread_mutex_unlock(&ctx->sock_mutex);
     printf("[MODBUS-WRITE] Success: dev %d reg 0x%04X = %u\n",
            dev_addr, reg_addr, value);
     return 0;

+ 2 - 1
mppt_monitor/src/modbus_client.h

@@ -154,7 +154,8 @@ typedef struct {
     modbus_config_t      config;
     mppt_device_data_t   devices[MODBUS_MAX_DEVICES];
     int                  device_count;
-    pthread_mutex_t      data_mutex;
+    pthread_mutex_t      data_mutex;    /* Protects devices[] data */
+    pthread_mutex_t      sock_mutex;    /* Serializes socket send/recv (poll + write) */
     pthread_t            poll_thread;
     bool                 running;
 } modbus_client_t;

+ 1 - 1
mppt_monitor/src/web_server.c

@@ -326,7 +326,7 @@ static void handle_api_settings(struct mg_connection *c, struct mg_http_message
                  "{\"success\":false,\"error\":\"Modbus写入失败,请检查设备连接\","
                  "\"device_addr\":%d,\"param\":\"%s\"}",
                  dev_addr, param_copy);
-        mg_http_reply(c, 500, "Content-Type: application/json\r\n", "%s", resp);
+        mg_http_reply(c, 500, "Content-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n", "%s", resp);
     }
 }