Преглед на файлове

auto saved your changes before restore

Coze-Commit-Type: auto
Coze-User-ID: 256649704250362
steven_roc преди 2 месеца
родител
ревизия
c68925d001
променени са 5 файла, в които са добавени 339 реда и са изтрити 169 реда
  1. BIN
      mppt_monitor/build/mppt_monitor
  2. 5 2
      mppt_monitor/src/main.c
  3. 203 104
      mppt_monitor/src/modbus_client.c
  4. 83 42
      mppt_monitor/src/modbus_client.h
  5. 48 21
      mppt_monitor/src/web_server.c

BIN
mppt_monitor/build/mppt_monitor


+ 5 - 2
mppt_monitor/src/main.c

@@ -89,9 +89,12 @@ static void *modbus_poll_thread(void *arg)
             }
         }
 
-        /* Read registers */
-        ret = modbus_read_registers(&dev->conn, &local_data);
+        /* Read Input Registers (FC=0x04, real-time data) */
+        ret = modbus_read_input_registers(&dev->conn, &local_data);
         if (ret == 0) {
+            /* Also read Output Registers (FC=0x03, parameters) */
+            modbus_read_output_registers(&dev->conn, &local_data);
+
             /* Success - update shared data under lock */
             pthread_mutex_lock(&g_data_mutex);
             memcpy(&dev->data, &local_data, sizeof(mppt_data_t));

+ 203 - 104
mppt_monitor/src/modbus_client.c

@@ -2,8 +2,9 @@
  * @file modbus_client.c
  * @brief Modbus RTU over TCP client implementation
  *
- * Implements CRC-16/Modbus, TCP connection management with timeout,
- * register read/parse, and auto-reconnect logic.
+ * DM Series ModBus 485 Protocol V2.1
+ * - Input Register (FC=0x04): 30001~30011 (11 registers, real-time data)
+ * - Output Register (FC=0x03): 40001~40009 (9 registers, parameters)
  */
 
 #include "modbus_client.h"
@@ -24,11 +25,6 @@
 #include <sys/select.h>
 
 /* ── CRC-16/Modbus ─────────────────────────────────────────────── */
-/*
- * Standard Modbus CRC-16 calculation.
- * Polynomial: 0xA001 (reversed 0x8005)
- * Initial value: 0xFFFF
- */
 uint16_t modbus_crc16(const uint8_t *buf, size_t len)
 {
     uint16_t crc = 0xFFFF;
@@ -74,12 +70,11 @@ static int tcp_connect_with_timeout(const char *host, uint16_t port, int timeout
     char port_str[8];
 
     memset(&hints, 0, sizeof(hints));
-    hints.ai_family = AF_UNSPEC;     /* IPv4 or IPv6 */
-    hints.ai_socktype = SOCK_STREAM; /* TCP */
+    hints.ai_family = AF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
 
     snprintf(port_str, sizeof(port_str), "%u", port);
 
-    /* DNS resolution */
     ret = getaddrinfo(host, port_str, &hints, &res);
     if (ret != 0) {
         fprintf(stderr, "[MODBUS] DNS resolution failed for %s:%u - %s\n",
@@ -87,23 +82,19 @@ static int tcp_connect_with_timeout(const char *host, uint16_t port, int timeout
         return -1;
     }
 
-    /* Try each resolved address */
     for (rp = res; rp != NULL; rp = rp->ai_next) {
         sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
         if (sockfd < 0) continue;
 
-        /* Set non-blocking for connect with timeout */
         int flags = fcntl(sockfd, F_GETFL, 0);
         fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
 
         ret = connect(sockfd, rp->ai_addr, rp->ai_addrlen);
         if (ret == 0) {
-            /* Connected immediately */
             fcntl(sockfd, F_SETFL, flags);
             break;
         }
         if (errno == EINPROGRESS) {
-            /* Wait for connection with timeout using select */
             fd_set wfds;
             struct timeval tv;
 
@@ -114,12 +105,10 @@ static int tcp_connect_with_timeout(const char *host, uint16_t port, int timeout
 
             ret = select(sockfd + 1, NULL, &wfds, NULL, &tv);
             if (ret > 0 && FD_ISSET(sockfd, &wfds)) {
-                /* Check for connection error */
                 int so_error;
                 socklen_t len = sizeof(so_error);
                 getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len);
                 if (so_error == 0) {
-                    /* Connected successfully */
                     fcntl(sockfd, F_SETFL, flags);
                     break;
                 } else {
@@ -145,11 +134,9 @@ static int tcp_connect_with_timeout(const char *host, uint16_t port, int timeout
     freeaddrinfo(res);
 
     if (sockfd >= 0) {
-        /* Set TCP_NODELAY for low latency */
         int flag = 1;
         setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
 
-        /* Set receive timeout */
         struct timeval recv_tv;
         recv_tv.tv_sec = timeout_sec;
         recv_tv.tv_usec = 0;
@@ -163,21 +150,19 @@ static int tcp_connect_with_timeout(const char *host, uint16_t port, int timeout
 int modbus_connect(modbus_conn_t *conn)
 {
     if (conn->state == MODBUS_STATE_CONNECTED && conn->sockfd >= 0) {
-        return 0; /* Already connected */
+        return 0;
     }
 
-    /* Rate limit reconnect attempts */
     time_t now = time(NULL);
     if (conn->last_connect_attempt > 0 &&
         (now - conn->last_connect_attempt) < 2) {
-        return -1; /* Too soon, wait at least 2 seconds between attempts */
+        return -1;
     }
     conn->last_connect_attempt = now;
 
     printf("[MODBUS] Connecting to %s:%u ...\n", conn->host, conn->port);
     conn->state = MODBUS_STATE_CONNECTING;
 
-    /* Close old socket if any */
     if (conn->sockfd >= 0) {
         close(conn->sockfd);
         conn->sockfd = -1;
@@ -211,8 +196,19 @@ void modbus_disconnect(modbus_conn_t *conn)
     printf("[MODBUS] Disconnected\n");
 }
 
-/* ── Read Registers ────────────────────────────────────────────── */
-int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
+/* ── Generic Register Read ─────────────────────────────────────── */
+/**
+ * Generic Modbus RTU register read.
+ * @param conn       Active connection
+ * @param func_code  Function code (0x03 or 0x04)
+ * @param start_reg  Start register address
+ * @param reg_count  Number of registers to read
+ * @param out_regs   Output array for register values (must have reg_count elements)
+ * @return 0 on success, -1 on failure
+ */
+static int modbus_read_generic(modbus_conn_t *conn, uint8_t func_code,
+                                uint16_t start_reg, uint16_t reg_count,
+                                uint16_t *out_regs)
 {
     uint8_t request[8];
     uint8_t response[256];
@@ -224,24 +220,19 @@ int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
         return -1;
     }
 
-    /*
-     * Build Modbus RTU request frame:
-     * [Slave Addr] [Func Code] [Start Reg Hi] [Start Reg Lo]
-     * [Reg Count Hi] [Reg Count Lo] [CRC Lo] [CRC Hi]
-     */
-    request[0] = conn->slave_addr;             /* Slave address */
-    request[1] = MODBUS_FUNC_READ;           /* Function code 04 */
-    request[2] = (MODBUS_REG_START >> 8) & 0xFF; /* Start register high */
-    request[3] = MODBUS_REG_START & 0xFF;         /* Start register low */
-    request[4] = (MODBUS_REG_COUNT >> 8) & 0xFF;  /* Register count high */
-    request[5] = MODBUS_REG_COUNT & 0xFF;          /* Register count low */
-
-    /* Calculate CRC over first 6 bytes */
+    /* Build request: [Addr][FC][StartHi][StartLo][CountHi][CountLo][CRCLo][CRCHi] */
+    request[0] = conn->slave_addr;
+    request[1] = func_code;
+    request[2] = (start_reg >> 8) & 0xFF;
+    request[3] = start_reg & 0xFF;
+    request[4] = (reg_count >> 8) & 0xFF;
+    request[5] = reg_count & 0xFF;
+
     crc_calc = modbus_crc16(request, 6);
-    request[6] = crc_calc & 0xFF;        /* CRC low byte */
-    request[7] = (crc_calc >> 8) & 0xFF; /* CRC high byte */
+    request[6] = crc_calc & 0xFF;
+    request[7] = (crc_calc >> 8) & 0xFF;
 
-    /* Send request */
+    /* Send */
     n = write(conn->sockfd, request, sizeof(request));
     if (n != sizeof(request)) {
         fprintf(stderr, "[MODBUS] Write failed: %s (wrote %zd/%zu)\n",
@@ -250,12 +241,11 @@ int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
         return -1;
     }
 
-    /* Read response with timeout (set via SO_RCVTIMEO) */
+    /* Read response */
     n = read(conn->sockfd, response, sizeof(response));
     if (n < 0) {
         if (errno == EAGAIN || errno == EWOULDBLOCK) {
-            fprintf(stderr, "[MODBUS] Read timeout after %ds\n",
-                    MODBUS_TIMEOUT_SEC);
+            fprintf(stderr, "[MODBUS] Read timeout after %ds\n", MODBUS_TIMEOUT_SEC);
         } else {
             fprintf(stderr, "[MODBUS] Read error: %s\n", strerror(errno));
         }
@@ -268,36 +258,40 @@ int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
         return -1;
     }
 
-    /* Validate minimum response length: addr(1) + func(1) + len(1) + data(20) + crc(2) = 25 */
-    if (n < 25) {
-        fprintf(stderr, "[MODBUS] Response too short (%zd bytes)\n", n);
+    /* Minimum response: addr(1) + func(1) + len(1) + data(N*2) + crc(2) */
+    size_t expected_min = 5 + (size_t)reg_count * 2;
+    if ((size_t)n < expected_min) {
+        fprintf(stderr, "[MODBUS] Response too short (%zd bytes, expected >= %zu)\n",
+                n, expected_min);
         return -1;
     }
 
-    /* Check slave address */
+    /* Validate slave address */
     if (response[0] != conn->slave_addr) {
-        fprintf(stderr, "[MODBUS] Wrong slave address: 0x%02X\n", response[0]);
+        fprintf(stderr, "[MODBUS] Wrong slave address: 0x%02X (expected 0x%02X)\n",
+                response[0], conn->slave_addr);
         return -1;
     }
 
-    /* Check for exception response */
+    /* Check exception */
     if (response[1] & 0x80) {
-        fprintf(stderr, "[MODBUS] Exception response: func=0x%02X code=0x%02X\n",
+        fprintf(stderr, "[MODBUS] Exception: func=0x%02X code=0x%02X\n",
                 response[1], response[2]);
         return -1;
     }
 
-    /* Check function code */
-    if (response[1] != MODBUS_FUNC_READ) {
-        fprintf(stderr, "[MODBUS] Unexpected function code: 0x%02X\n", response[1]);
+    /* Validate function code */
+    if (response[1] != func_code) {
+        fprintf(stderr, "[MODBUS] Unexpected FC: 0x%02X (expected 0x%02X)\n",
+                response[1], func_code);
         return -1;
     }
 
-    /* Check data length */
+    /* Validate data length */
     uint8_t byte_count = response[2];
-    if (byte_count != MODBUS_REG_COUNT * 2) {
-        fprintf(stderr, "[MODBUS] Unexpected data length: %u (expected %u)\n",
-                byte_count, MODBUS_REG_COUNT * 2);
+    if (byte_count != reg_count * 2) {
+        fprintf(stderr, "[MODBUS] Data length mismatch: %u (expected %u)\n",
+                byte_count, reg_count * 2);
         return -1;
     }
 
@@ -311,26 +305,49 @@ int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
     }
 
     /* Parse register values (big-endian) */
-    for (int i = 0; i < MODBUS_REG_COUNT; i++) {
-        data->reg[i] = ((uint16_t)response[3 + i * 2] << 8) |
-                        (uint16_t)response[3 + i * 2 + 1];
+    for (int i = 0; i < reg_count; i++) {
+        out_regs[i] = ((uint16_t)response[3 + i * 2] << 8) |
+                       (uint16_t)response[3 + i * 2 + 1];
     }
 
-    /* Convert to real-world values */
-    data->battery_voltage     = data->reg[0] / 10.0;     /* V */
-    data->pv_voltage          = data->reg[1] / 10.0;     /* V */
-    data->pv_charge_current   = data->reg[2] / 10.0;     /* A */
-    data->cumulative_energy   = data->reg[3] / 10.0;     /* kWh */
-    data->machine_temp        = (double)data->reg[4];     /* deg C */
-    data->fault_code          = data->reg[5];
-    data->battery_soc         = data->reg[6];             /* % */
-    data->battery_string      = data->reg[7];
-    data->charge_state        = data->reg[8];
-    data->device_type         = data->reg[9];
-
-    /* Computed values */
+    return 0;
+}
+
+/* ── Read Input Registers (FC=0x04, 30001~30011) ──────────────── */
+int modbus_read_input_registers(modbus_conn_t *conn, mppt_data_t *data)
+{
+    int ret = modbus_read_generic(conn, MODBUS_FUNC_READ_INPUT,
+                                   MODBUS_INPUT_REG_START,
+                                   MODBUS_INPUT_REG_COUNT,
+                                   data->in_reg);
+    if (ret != 0) {
+        data->data_valid = false;
+        data->consecutive_failures++;
+        return -1;
+    }
+
+    /* Parse Input Registers */
+    data->battery_voltage     = data->in_reg[0] / 10.0;     /* 30001: V */
+    data->pv_voltage          = data->in_reg[1] / 10.0;     /* 30002: V */
+    data->pv_charge_current   = data->in_reg[2] / 10.0;     /* 30003: A */
+    data->cumulative_energy   = data->in_reg[3] / 10.0;     /* 30004: kWh */
+    data->machine_temp        = (double)data->in_reg[4];     /* 30005: deg C */
+    data->fault_code          = data->in_reg[5];             /* 30006 */
+    data->battery_soc         = data->in_reg[6];             /* 30007: % */
+    data->battery_string      = data->in_reg[7];             /* 30008 */
+    data->charge_state        = data->in_reg[8];             /* 30009 */
+    data->device_type         = data->in_reg[9];             /* 30010 */
+    data->software_version    = data->in_reg[10];            /* 30011 */
+
+    /* Computed */
     data->pv_input_power = data->pv_voltage * data->pv_charge_current;
 
+    /* Description strings */
+    snprintf(data->fault_desc, sizeof(data->fault_desc), "%s",
+             modbus_fault_string(data->fault_code));
+    snprintf(data->charge_state_str, sizeof(data->charge_state_str), "%s",
+             modbus_charge_state_string(data->charge_state));
+
     /* Meta */
     data->last_update = time(NULL);
     data->data_valid = true;
@@ -339,33 +356,102 @@ int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
     return 0;
 }
 
-/* ── Fault Code Descriptions ───────────────────────────────────── */
+/* ── Read Output Registers (FC=0x03, 40001~40009) ─────────────── */
+int modbus_read_output_registers(modbus_conn_t *conn, mppt_data_t *data)
+{
+    int ret = modbus_read_generic(conn, MODBUS_FUNC_READ_OUTPUT,
+                                   MODBUS_OUTPUT_REG_START,
+                                   MODBUS_OUTPUT_REG_COUNT,
+                                   data->out_reg);
+    if (ret != 0) {
+        data->params_valid = false;
+        return -1;
+    }
+
+    /* Parse Output Registers */
+    data->battery_type          = data->out_reg[0];  /* 40001 */
+    data->battery_level         = data->out_reg[1];  /* 40002 */
+    data->charge_current_pct    = data->out_reg[2];  /* 40003: % */
+    data->over_discharge_voltage = data->out_reg[3] / 10.0; /* 40004: V */
+    data->boost_voltage         = data->out_reg[4] / 10.0;  /* 40005: V */
+    data->full_charge_voltage   = data->out_reg[5] / 10.0;  /* 40006: V */
+    data->over_discharge_recovery = data->out_reg[6] / 10.0; /* 40007: V */
+    data->rs485_address         = data->out_reg[7];  /* 40008 */
+    data->charge_mode           = data->out_reg[8];  /* 40009 */
+
+    /* Description strings */
+    snprintf(data->battery_type_str, sizeof(data->battery_type_str), "%s",
+             modbus_battery_type_string(data->battery_type));
+    snprintf(data->battery_level_str, sizeof(data->battery_level_str), "%s",
+             modbus_battery_level_string(data->battery_level));
+    snprintf(data->charge_mode_str, sizeof(data->charge_mode_str), "%s",
+             modbus_charge_mode_string(data->charge_mode));
+
+    data->params_valid = true;
+    return 0;
+}
+
+/* ── Fault Code Descriptions (per DM Protocol V2.1) ────────────── */
 const char *modbus_fault_string(uint16_t code)
 {
     switch (code) {
-        case 0:  return "No fault";
-        case 1:  return "Battery over-voltage";
-        case 2:  return "Battery under-voltage";
-        case 3:  return "PV over-voltage";
-        case 4:  return "PV under-voltage";
-        case 5:  return "Over-temperature";
-        case 6:  return "Short circuit";
-        case 7:  return "Over-current";
-        case 8:  return "Communication error";
         case 20: return "Normal";
+        case 30: return "PV overvoltage";
+        case 31: return "PV charge overcurrent";
+        case 32: return "Battery voltage low";
+        case 33: return "Battery overvoltage";
+        case 38: return "Device temperature high";
+        case 50: return "Temperature sensor fault";
+        case 88: return "Not decrypted";
         default: return "Unknown fault";
     }
 }
 
+/* ── Battery Type Descriptions ─────────────────────────────────── */
+const char *modbus_battery_type_string(uint16_t type)
+{
+    switch (type) {
+        case 0: return "Gel";
+        case 1: return "Sealed";
+        case 2: return "LiFePO4";
+        case 3: return "Ternary Lithium";
+        case 4: return "Custom";
+        case 5: return "Flooded";
+        default: return "Unknown";
+    }
+}
+
+/* ── Battery Level Descriptions ────────────────────────────────── */
+const char *modbus_battery_level_string(uint16_t level)
+{
+    switch (level) {
+        case 0: return "Auto";
+        case 1: return "12V";
+        case 2: return "24V";
+        case 3: return "36V";
+        case 4: return "48V";
+        case 5: return "60V";
+        case 6: return "72V";
+        default: return "Unknown";
+    }
+}
+
+/* ── Charge Mode Descriptions ──────────────────────────────────── */
+const char *modbus_charge_mode_string(uint16_t mode)
+{
+    switch (mode) {
+        case 0: return "MPPT";
+        case 1: return "DCDC";
+        default: return "Unknown";
+    }
+}
+
 /* ── Charge State Descriptions ─────────────────────────────────── */
 const char *modbus_charge_state_string(uint16_t state)
 {
     switch (state) {
-        case 0: return "Idle / No charging";
-        case 1: return "Charging (Bulk)";
-        case 2: return "Absorption";
-        case 3: return "Float / Trickle";
-        case 4: return "Equalize";
+        case 0: return "Not charging";
+        case 1: return "Charging";
         default: return "Unknown";
     }
 }
@@ -378,23 +464,36 @@ void modbus_print_data(const mppt_data_t *data)
     strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);
 
     printf("\n");
-    printf("+==========================================+\n");
-    printf("|  MPPT Real-time Data  [%s]  |\n", timebuf);
-    printf("|  Device: %-12s (ID:%d)             |\n",
+    printf("+======================================================+\n");
+    printf("|  MPPT Full Data  [%s]                    |\n", timebuf);
+    printf("|  Device: %-12s (ID:%d)                         |\n",
            data->device_name, data->device_id);
-    printf("+==========================================+\n");
-    printf("| Battery Voltage  : %8.1f V              |\n", data->battery_voltage);
-    printf("| PV Voltage       : %8.1f V              |\n", data->pv_voltage);
-    printf("| Charge Current   : %8.1f A              |\n", data->pv_charge_current);
-    printf("| PV Input Power   : %8.1f W              |\n", data->pv_input_power);
-    printf("| Cumulative Energy: %8.1f kWh            |\n", data->cumulative_energy);
-    printf("| Machine Temp     : %8.1f C              |\n", data->machine_temp);
-    printf("| Battery SOC      : %8u %%               |\n", data->battery_soc);
-    printf("| Battery String   : %8u                  |\n", data->battery_string);
-    printf("| Charge State     : %-20s  |\n", modbus_charge_state_string(data->charge_state));
-    printf("| Fault Code       : %-20s  |\n", modbus_fault_string(data->fault_code));
-    printf("| Device Type      : %8u                  |\n", data->device_type);
-    printf("+==========================================+\n");
+    printf("+======================================================+\n");
+    printf("| --- Input Registers (Real-time) ---                  |\n");
+    printf("| Battery Voltage    : %8.1f V                         |\n", data->battery_voltage);
+    printf("| PV Voltage          : %8.1f V                         |\n", data->pv_voltage);
+    printf("| Charge Current      : %8.1f A                         |\n", data->pv_charge_current);
+    printf("| PV Input Power       : %8.1f W                         |\n", data->pv_input_power);
+    printf("| Cumulative Energy : %8.1f kWh                       |\n", data->cumulative_energy);
+    printf("| Machine Temp        : %8.1f C                         |\n", data->machine_temp);
+    printf("| Battery SOC         : %8u %%                          |\n", data->battery_soc);
+    printf("| Battery String      : %8u                             |\n", data->battery_string);
+    printf("| Charge State        : %-20s               |\n", data->charge_state_str);
+    printf("| Fault Code          : %-20s               |\n", data->fault_desc);
+    printf("| Device Type         : %8u                             |\n", data->device_type);
+    printf("| Software Version    : %8u                             |\n", data->software_version);
+    printf("+------------------------------------------------------+\n");
+    printf("| --- Output Registers (Parameters) ---                |\n");
+    printf("| Battery Type        : %-20s               |\n", data->battery_type_str);
+    printf("| Battery Level       : %-20s               |\n", data->battery_level_str);
+    printf("| Charge Current Pct  : %8u %%                          |\n", data->charge_current_pct);
+    printf("| Over-discharge V    : %8.1f V                         |\n", data->over_discharge_voltage);
+    printf("| Boost Voltage       : %8.1f V                         |\n", data->boost_voltage);
+    printf("| Full Charge V       : %8.1f V                         |\n", data->full_charge_voltage);
+    printf("| Recovery V          : %8.1f V                         |\n", data->over_discharge_recovery);
+    printf("| RS485 Address       : %8u                             |\n", data->rs485_address);
+    printf("| Charge Mode         : %-20s               |\n", data->charge_mode_str);
+    printf("+======================================================+\n");
     printf("\n");
     fflush(stdout);
 }

+ 83 - 42
mppt_monitor/src/modbus_client.h

@@ -3,8 +3,9 @@
  * @brief Modbus RTU over TCP client for MPPT solar charge controller
  *
  * Compatible with: Ubuntu 24.04 / Raspberry Pi 5 8G
- * Protocol: Modbus RTU framing over raw TCP socket
- * Target device: discover.zhonjin.com:40635
+ * Protocol: DM Series ModBus 485 Protocol V2.1
+ * - Input Register (FC=0x04): 30001~30011 (real-time data)
+ * - Output Register (FC=0x03): 40001~40009 (parameters/settings)
  */
 
 #ifndef MODBUS_CLIENT_H
@@ -22,41 +23,71 @@ extern "C" {
 #define MODBUS_HOST         "discover.zhonjin.com"
 #define MODBUS_PORT         40635
 #define MODBUS_SLAVE_ADDR   0x01
-#define MODBUS_FUNC_READ    0x04    /* Read Input Registers */
-#define MODBUS_REG_START    0x0000  /* Start register 30001 -> offset 0 */
-#define MODBUS_REG_COUNT    10      /* 10 registers (30001~30010) */
 #define MODBUS_TIMEOUT_SEC  10      /* Connection/read timeout */
 #define MODBUS_POLL_MS      2000    /* Poll interval in milliseconds */
 #define MODBUS_MAX_RETRIES  3       /* Max retry before giving up */
 
+/* Input Registers (FC=0x04): 30001~30011, 11 registers */
+#define MODBUS_FUNC_READ_INPUT  0x04
+#define MODBUS_INPUT_REG_START  0x0000
+#define MODBUS_INPUT_REG_COUNT  11
+
+/* Output Registers (FC=0x03): 40001~40009, 9 registers */
+#define MODBUS_FUNC_READ_OUTPUT 0x03
+#define MODBUS_OUTPUT_REG_START 0x0000
+#define MODBUS_OUTPUT_REG_COUNT 9
+
 /* ── MPPT Data Structure ───────────────────────────────────────── */
 typedef struct {
     /* Device identification */
     int      device_id;               /* Device index (0-based) */
     char     device_name[64];         /* Device display name */
 
-    /* Raw register values */
-    uint16_t reg[MODBUS_REG_COUNT];
-
-    /* Parsed real-world values */
-    double   battery_voltage;       /* 30001: V (raw / 10) */
-    double   pv_voltage;            /* 30002: V (raw / 10) */
-    double   pv_charge_current;     /* 30003: A (raw / 10) */
-    double   cumulative_energy;     /* 30004: kWh (raw / 10) */
-    double   machine_temp;          /* 30005: deg C */
-    uint16_t fault_code;            /* 30006 */
-    uint16_t battery_soc;           /* 30007: % */
-    uint16_t battery_string;        /* 30008 */
-    uint16_t charge_state;          /* 30009 */
-    uint16_t device_type;           /* 30010 */
-
-    /* Computed */
-    double   pv_input_power;        /* W = pv_voltage * charge_current */
-
-    /* Meta */
-    time_t   last_update;           /* Timestamp of last successful read */
-    bool     data_valid;            /* true if last read succeeded */
-    int      consecutive_failures;  /* Count of consecutive read failures */
+    /* ── Input Registers (30001~30011, FC=0x04) ── */
+    uint16_t in_reg[MODBUS_INPUT_REG_COUNT];
+
+    /* Real-time data (parsed) */
+    double   battery_voltage;         /* 30001: V (raw * 0.1) */
+    double   pv_voltage;              /* 30002: V (raw * 0.1) */
+    double   pv_charge_current;       /* 30003: A (raw * 0.1) */
+    double   cumulative_energy;       /* 30004: kWh (raw * 0.1) */
+    double   machine_temp;            /* 30005: deg C (0xFF=fault) */
+    uint16_t fault_code;              /* 30006 */
+    uint16_t battery_soc;             /* 30007: % */
+    uint16_t battery_string;          /* 30008: reserved */
+    uint16_t charge_state;            /* 30009: 0=not charging, 1=charging */
+    uint16_t device_type;             /* 30010 */
+    uint16_t software_version;        /* 30011 */
+
+    /* ── Output Registers (40001~40009, FC=0x03) ── */
+    uint16_t out_reg[MODBUS_OUTPUT_REG_COUNT];
+
+    /* Parameters/settings (parsed) */
+    uint16_t battery_type;            /* 40001: 0=gel,1=sealed,2=LiFePO4,3=ternary,4=custom,5=flooded */
+    uint16_t battery_level;           /* 40002: 0=auto,1=12V,2=24V,3=36V,4=48V,5=60V,6=72V */
+    uint16_t charge_current_pct;      /* 40003: 5-100% */
+    double   over_discharge_voltage;  /* 40004: V (raw * 0.1) */
+    double   boost_voltage;           /* 40005: V (raw * 0.1) */
+    double   full_charge_voltage;     /* 40006: V (raw * 0.1) */
+    double   over_discharge_recovery; /* 40007: V (raw * 0.1) */
+    uint16_t rs485_address;           /* 40008: 0-247 */
+    uint16_t charge_mode;             /* 40009: 0=MPPT, 1=DCDC */
+
+    /* ── Computed ── */
+    double   pv_input_power;          /* W = pv_voltage * charge_current */
+
+    /* ── Description strings ── */
+    char     fault_desc[64];          /* Human-readable fault description */
+    char     battery_type_str[32];    /* Battery type name */
+    char     battery_level_str[16];   /* Battery level name */
+    char     charge_mode_str[16];     /* Charge mode name */
+    char     charge_state_str[32];    /* Charge state description */
+
+    /* ── Meta ── */
+    time_t   last_update;             /* Timestamp of last successful read */
+    bool     data_valid;              /* true if last read succeeded */
+    bool     params_valid;            /* true if output registers read succeeded */
+    int      consecutive_failures;    /* Count of consecutive read failures */
 } mppt_data_t;
 
 /* ── Connection State ──────────────────────────────────────────── */
@@ -83,19 +114,12 @@ typedef struct {
 
 /**
  * Initialize modbus connection structure.
- * @param conn        Pointer to connection struct
- * @param host        Remote hostname
- * @param port        Remote port
- * @param slave_addr  Modbus slave address (1-247)
- * @param device_id   Device index (0-based)
- * @param device_name Device display name
  */
 void modbus_init(modbus_conn_t *conn, const char *host, uint16_t port,
                  uint8_t slave_addr, int device_id, const char *device_name);
 
 /**
  * Connect to the Modbus TCP device.
- * Handles DNS resolution, socket creation, and connect with timeout.
  * @return 0 on success, -1 on failure
  */
 int modbus_connect(modbus_conn_t *conn);
@@ -106,19 +130,21 @@ int modbus_connect(modbus_conn_t *conn);
 void modbus_disconnect(modbus_conn_t *conn);
 
 /**
- * Send Modbus RTU read command and parse response.
- * Updates mppt_data_t with parsed values.
- * @param conn  Active connection
- * @param data  Pointer to data struct to fill
+ * Read Input Registers (FC=0x04, 30001~30011).
+ * Updates mppt_data_t real-time fields.
+ * @return 0 on success, -1 on failure
+ */
+int modbus_read_input_registers(modbus_conn_t *conn, mppt_data_t *data);
+
+/**
+ * Read Output Registers (FC=0x03, 40001~40009).
+ * Updates mppt_data_t parameter fields.
  * @return 0 on success, -1 on failure
  */
-int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data);
+int modbus_read_output_registers(modbus_conn_t *conn, mppt_data_t *data);
 
 /**
  * Calculate CRC-16/Modbus for a byte buffer.
- * @param buf   Input buffer
- * @param len   Buffer length
- * @return CRC-16 value (low byte first in wire format)
  */
 uint16_t modbus_crc16(const uint8_t *buf, size_t len);
 
@@ -132,6 +158,21 @@ void modbus_print_data(const mppt_data_t *data);
  */
 const char *modbus_fault_string(uint16_t code);
 
+/**
+ * Get human-readable battery type name.
+ */
+const char *modbus_battery_type_string(uint16_t type);
+
+/**
+ * Get human-readable battery level name.
+ */
+const char *modbus_battery_level_string(uint16_t level);
+
+/**
+ * Get human-readable charge mode name.
+ */
+const char *modbus_charge_mode_string(uint16_t mode);
+
 /**
  * Get human-readable charge state description.
  */

+ 48 - 21
mppt_monitor/src/web_server.c

@@ -30,7 +30,7 @@
 #include <time.h>
 #include <pthread.h>
 
-/* ── JSON: Build single device data object ─────────────────────── */
+/* ── JSON: Build single device data object (ALL fields) ────────── */
 static cJSON *build_device_data_obj(const mppt_data_t *data)
 {
     cJSON *obj = cJSON_CreateObject();
@@ -48,37 +48,64 @@ static cJSON *build_device_data_obj(const mppt_data_t *data)
         cJSON_AddStringToObject(obj, "timestamp", "N/A");
     }
     cJSON_AddBoolToObject(obj, "data_valid", data->data_valid ? 1 : 0);
+    cJSON_AddBoolToObject(obj, "params_valid", data->params_valid ? 1 : 0);
+
+    /* ── Input Registers (Real-time data) ── */
+    cJSON *input = cJSON_CreateObject();
 
     cJSON *battery = cJSON_CreateObject();
     cJSON_AddNumberToObject(battery, "voltage", data->battery_voltage);
     cJSON_AddNumberToObject(battery, "soc", data->battery_soc);
     cJSON_AddNumberToObject(battery, "string_count", data->battery_string);
-    cJSON_AddItemToObject(obj, "battery", battery);
+    cJSON_AddItemToObject(input, "battery", battery);
 
     cJSON *pv = cJSON_CreateObject();
     cJSON_AddNumberToObject(pv, "voltage", data->pv_voltage);
     cJSON_AddNumberToObject(pv, "current", data->pv_charge_current);
     cJSON_AddNumberToObject(pv, "power", data->pv_input_power);
-    cJSON_AddItemToObject(obj, "pv", pv);
-
-    cJSON_AddNumberToObject(obj, "cumulative_energy_kwh", data->cumulative_energy);
-
-    cJSON *device = cJSON_CreateObject();
-    cJSON_AddNumberToObject(device, "temperature", data->machine_temp);
-    cJSON_AddNumberToObject(device, "fault_code", data->fault_code);
-    cJSON_AddStringToObject(device, "fault_desc",
-                            modbus_fault_string(data->fault_code));
-    cJSON_AddNumberToObject(device, "charge_state", data->charge_state);
-    cJSON_AddStringToObject(device, "charge_state_desc",
-                            modbus_charge_state_string(data->charge_state));
-    cJSON_AddNumberToObject(device, "device_type", data->device_type);
-    cJSON_AddItemToObject(obj, "device", device);
-
-    cJSON *raw = cJSON_CreateArray();
-    for (int i = 0; i < MODBUS_REG_COUNT; i++) {
-        cJSON_AddItemToArray(raw, cJSON_CreateNumber(data->reg[i]));
+    cJSON_AddNumberToObject(pv, "cumulative_energy_kwh", data->cumulative_energy);
+    cJSON_AddItemToObject(input, "pv", pv);
+
+    cJSON_AddNumberToObject(input, "temperature", data->machine_temp);
+    cJSON_AddNumberToObject(input, "fault_code", data->fault_code);
+    cJSON_AddStringToObject(input, "fault_desc", data->fault_desc);
+    cJSON_AddNumberToObject(input, "charge_state", data->charge_state);
+    cJSON_AddStringToObject(input, "charge_state_desc", data->charge_state_str);
+    cJSON_AddNumberToObject(input, "device_type", data->device_type);
+    cJSON_AddNumberToObject(input, "software_version", data->software_version);
+
+    cJSON_AddItemToObject(obj, "input_registers", input);
+
+    /* ── Output Registers (Parameters) ── */
+    cJSON *output = cJSON_CreateObject();
+
+    cJSON_AddNumberToObject(output, "battery_type", data->battery_type);
+    cJSON_AddStringToObject(output, "battery_type_desc", data->battery_type_str);
+    cJSON_AddNumberToObject(output, "battery_level", data->battery_level);
+    cJSON_AddStringToObject(output, "battery_level_desc", data->battery_level_str);
+    cJSON_AddNumberToObject(output, "charge_current_pct", data->charge_current_pct);
+    cJSON_AddNumberToObject(output, "over_discharge_voltage", data->over_discharge_voltage);
+    cJSON_AddNumberToObject(output, "boost_voltage", data->boost_voltage);
+    cJSON_AddNumberToObject(output, "full_charge_voltage", data->full_charge_voltage);
+    cJSON_AddNumberToObject(output, "over_discharge_recovery_voltage", data->over_discharge_recovery);
+    cJSON_AddNumberToObject(output, "rs485_address", data->rs485_address);
+    cJSON_AddNumberToObject(output, "charge_mode", data->charge_mode);
+    cJSON_AddStringToObject(output, "charge_mode_desc", data->charge_mode_str);
+
+    cJSON_AddItemToObject(obj, "output_registers", output);
+
+    /* ── Raw registers ── */
+    cJSON *raw_in = cJSON_CreateArray();
+    for (int i = 0; i < MODBUS_INPUT_REG_COUNT; i++) {
+        cJSON_AddItemToArray(raw_in, cJSON_CreateNumber(data->in_reg[i]));
+    }
+    cJSON_AddItemToObject(obj, "raw_input_registers", raw_in);
+
+    cJSON *raw_out = cJSON_CreateArray();
+    for (int i = 0; i < MODBUS_OUTPUT_REG_COUNT; i++) {
+        cJSON_AddItemToArray(raw_out, cJSON_CreateNumber(data->out_reg[i]));
     }
-    cJSON_AddItemToObject(obj, "raw_registers", raw);
+    cJSON_AddItemToObject(obj, "raw_output_registers", raw_out);
 
     return obj;
 }