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

Restored to '340050e5478384d9eb816c362ac4365bda218463'

Coze-Restored-To: '340050e5478384d9eb816c362ac4365bda218463'
Coze-Commit-Type: restore
Coze-User-ID: 256649704250362
steven_roc преди 2 месеца
родител
ревизия
f5f3b93585

+ 0 - 44
mppt_monitor/CHANGELOG.md

@@ -4,50 +4,6 @@
 
 ---
 
-## [1.2.0] - 2026-08-04
-
-### 新增:多设备支持与全量信息实时显示
-
-#### 多设备架构
-- **config.ini 多设备配置**:支持 `[device.N]` 格式定义多台设备(最多 MAX_DEVICES=16)
-  - 每个设备独立配置:name、host、port、slave_addr、enabled
-  - 向后兼容:无 `[device]` 段时自动从 `[modbus]` 段创建默认设备
-- **多线程轮询**:每台设备独立 pthread 线程,独立 Modbus 连接,互不干扰
-- **设备运行时状态**:`device_runtime_t` 结构体管理每设备的数据/锁/连接状态
-
-#### API 新增
-- `GET /api/devices` - 返回所有设备列表及实时数据
-- `GET /api/devices/<name>` - 返回指定设备的完整实时数据
-- `GET /api/history?device_name=xxx` - 按设备名称过滤历史数据
-
-#### 数据库更新
-- `history_data` 表新增 `device_name TEXT` 列
-- 历史查询支持按设备名称过滤
-- 自动迁移:旧数据库自动 ALTER TABLE 添加 device_name 列
-
-#### 前端仪表盘
-- **设备卡片网格**:所有设备以卡片形式展示,显示关键指标(电池电压/SOC/光伏功率/温度/状态)
-- **状态指示灯**:绿色=数据有效,红色=连接异常,灰色=未启用
-- **实时刷新**:2 秒自动轮询 `/api/devices` 更新所有设备数据
-- **设备详情展开**:点击卡片可展开查看完整寄存器数据和计算值
-
-#### 文件变更
-| 文件 | 变更类型 | 说明 |
-|------|----------|------|
-| `src/config.h` | 重写 | 新增 device_config_t、MAX_DEVICES、多设备解析 |
-| `src/config.c` | 重写 | 支持 [device.N] 段解析,向后兼容 |
-| `src/modbus_client.h` | 更新 | mppt_data_t 新增 device_id/device_name/device_type |
-| `src/modbus_client.c` | 更新 | 使用 conn->slave_addr 替代硬编码 |
-| `src/db.h` | 更新 | db_record_t 新增 device_name |
-| `src/db.c` | 更新 | 表结构新增 device_name 列,查询支持设备过滤 |
-| `src/web_server.h` | 重写 | web_ctx 改为设备数组,新增 device_runtime_t |
-| `src/web_server.c` | 重写 | 多设备 API、设备卡片仪表盘 |
-| `src/dashboard_html.h` | 新增 | 独立 HTML 仪表盘(设备网格+详情+历史) |
-| `src/main.c` | 重写 | 多设备线程管理 |
-| `config.ini` | 更新 | 多设备配置示例 |
-
----
-
 ## [1.1.0] - 2026-08-04
 
 ### 新增:历史数据存储与查询

+ 5 - 88
mppt_monitor/TECHNICAL.md

@@ -441,94 +441,11 @@ Modbus 读取成功 → 更新共享数据 → 检查存储间隔
 1. **告警通知**: 温度过高、电压异常时发送邮件/微信通知
 2. **HTTPS 支持**: 已预留 OpenSSL 编译选项
 3. **MQTT 上报**: 将数据发布到 MQTT Broker 供其他系统消费
-4. **Web 认证**: 添加 HTTP Basic Auth 或 Token 认证
-5. **数据导出**: 支持 CSV/Excel 格式导出历史数据
-6. **统计聚合**: 按小时/天/月聚合统计数据(平均、最大、最小)
+4. **多设备支持**: 支持同时监控多个 MPPT 控制器
+5. **Web 认证**: 添加 HTTP Basic Auth 或 Token 认证
+6. **数据导出**: 支持 CSV/Excel 格式导出历史数据
+7. **统计聚合**: 按小时/天/月聚合统计数据(平均、最大、最小)
 
 ---
 
-## 八、多设备架构 (v1.2.0)
-
-### 8.1 架构设计
-
-```
-┌─────────────────────────────────────────────────────┐
-│                    main.c                           │
-│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
-│  │ Thread 1 │  │ Thread 2 │  │ Thread N │  ...      │
-│  │ MPPT-1   │  │ MPPT-2   │  │ MPPT-N   │          │
-│  │ modbus   │  │ modbus   │  │ modbus   │          │
-│  └────┬─────┘  └────┬─────┘  └────┬─────┘          │
-│       │              │              │                │
-│  ┌────▼──────────────▼──────────────▼────┐          │
-│  │     device_runtime_t[MAX_DEVICES]     │          │
-│  │     (共享数据 + pthread_mutex)         │          │
-│  └────┬──────────────┬──────────────┬────┘          │
-│       │              │              │                │
-│  ┌────▼──────────────▼──────────────▼────┐          │
-│  │         web_server (Mongoose)         │          │
-│  │  /api/devices  /api/devices/<name>    │          │
-│  │  /api/data     /api/history           │          │
-│  └───────────────────────────────────────┘          │
-│                                                     │
-│  ┌───────────────────────────────────────┐          │
-│  │     SQLite3 (device_name 过滤)        │          │
-│  └───────────────────────────────────────┘          │
-└─────────────────────────────────────────────────────┘
-```
-
-### 8.2 设备配置格式
-
-```ini
-[device]
-count = 3
-
-[device.1]
-name = MPPT-1
-host = discover.zhonjin.com
-port = 40635
-slave_addr = 1
-enabled = true
-
-[device.2]
-name = MPPT-2
-host = discover.zhonjin.com
-port = 40636
-slave_addr = 1
-enabled = true
-
-[device.3]
-name = MPPT-3
-host = 192.168.1.100
-port = 502
-slave_addr = 2
-enabled = false
-```
-
-### 8.3 多设备 API
-
-#### GET /api/devices
-返回所有设备列表及实时数据:
-```json
-{
-  "device_count": 2,
-  "timestamp": "2026-08-04 15:30:00",
-  "devices": [
-    { "name": "MPPT-1", "data_valid": true, ... },
-    { "name": "MPPT-2", "data_valid": true, ... }
-  ]
-}
-```
-
-#### GET /api/devices/<name>
-返回指定设备的完整实时数据。
-
-### 8.4 数据库设备隔离
-
-- `history_data` 表包含 `device_name` 列
-- 历史查询支持 `?device_name=MPPT-1` 过滤
-- 自动迁移:旧数据库自动添加 `device_name` 列
-
----
-
-*最后更新: 2026-08-04 (v1.2.0)*
+*最后更新: 2026-08-04 (v1.1.0)*

BIN
mppt_monitor/build/mppt_monitor


+ 16 - 33
mppt_monitor/config.ini

@@ -1,46 +1,29 @@
-# MPPT Monitor Configuration
-# Multi-device support: configure multiple MPPT controllers
+# =============================================================================
+# MPPT Solar Charge Controller Monitor - Configuration
+# =============================================================================
+# This file controls runtime behavior. Edit and restart the service to apply.
+# Lines starting with # or ; are comments.
+# =============================================================================
 
-# ── Device Configuration ──────────────────────────────────────────
-# Each device gets its own [device.N] section (N = 1, 2, 3, ...)
-# If no [device.N] sections exist, falls back to [modbus] section below.
-
-[device]
-count = 1
-
-[device.1]
-name = MPPT-1
+[modbus]
+# Modbus TCP device connection settings
 host = discover.zhonjin.com
 port = 40635
-slave_addr = 1
-enabled = 1
-
-# ── Example: Add more devices ─────────────────────────────────────
-# [device.2]
-# name = MPPT-2
-# host = discover.zhonjin.com
-# port = 40636
-# slave_addr = 1
-# enabled = 1
-#
-# [device.3]
-# name = MPPT-3
-# host = 192.168.1.100
-# port = 502
-# slave_addr = 2
-# enabled = 1
-
-# ── Modbus Global Settings ────────────────────────────────────────
-[modbus]
+slave_addr = 0x01
 timeout_sec = 10
+# How often to poll the device for new data (milliseconds)
 poll_ms = 2000
 
-# ── Storage Settings ──────────────────────────────────────────────
 [storage]
+# How often to save data to the database (seconds)
+# Default: 10 seconds. Minimum: 1 second.
 interval_sec = 10
+# SQLite database file path (relative to working directory or absolute)
 db_path = mppt_data.db
 
-# ── Web Server Settings ───────────────────────────────────────────
 [web]
+# HTTP server port (default: 8085)
+# Note: In sandbox environment, DEPLOY_RUN_PORT env var overrides this.
 port = 8085
+# Default page size for history API pagination
 page_size = 50

BIN
mppt_monitor/mppt_data.db


+ 22 - 99
mppt_monitor/src/config.c

@@ -1,9 +1,9 @@
 /**
  * @file config.c
- * @brief INI configuration parser for MPPT Monitor
+ * @brief Simple INI configuration parser for MPPT Monitor
  *
- * Supports multiple MPPT devices via [device.N] sections.
- * Backward compatible: if no [device] section, uses [modbus] for single device.
+ * Supports [section] and key=value pairs.
+ * Lines starting with # or ; are comments.
  */
 
 #include "config.h"
@@ -16,16 +16,9 @@
 /* ── Default Values ────────────────────────────────────────────── */
 void config_set_defaults(app_config_t *cfg)
 {
-    memset(cfg, 0, sizeof(app_config_t));
-
-    /* Default single device */
-    cfg->device_count = 1;
-    strncpy(cfg->devices[0].name, "MPPT-1", MAX_DEVICE_NAME - 1);
-    strncpy(cfg->devices[0].host, "discover.zhonjin.com", sizeof(cfg->devices[0].host) - 1);
-    cfg->devices[0].port = 40635;
-    cfg->devices[0].slave_addr = 0x01;
-    cfg->devices[0].enabled = 1;
-
+    strncpy(cfg->modbus_host, "discover.zhonjin.com", sizeof(cfg->modbus_host) - 1);
+    cfg->modbus_port = 40635;
+    cfg->modbus_slave_addr = 0x01;
     cfg->modbus_timeout_sec = 10;
     cfg->modbus_poll_ms = 2000;
 
@@ -47,59 +40,17 @@ static char *trim(char *str)
     return str;
 }
 
-/* ── Helper: parse device section index from "device.N" ────────── */
-static int parse_device_index(const char *section)
-{
-    /* Match "device.N" where N is 1..MAX_DEVICES */
-    if (strncmp(section, "device.", 7) != 0) return -1;
-    int idx = atoi(section + 7);
-    if (idx < 1 || idx > MAX_DEVICES) return -1;
-    return idx - 1;  /* 0-based index */
-}
-
 /* ── Helper: set a config value by section + key ───────────────── */
 static void config_set_value(app_config_t *cfg, const char *section,
                               const char *key, const char *value)
 {
-    /* Check if this is a device.N section */
-    int dev_idx = parse_device_index(section);
-    if (dev_idx >= 0) {
-        device_config_t *dev = &cfg->devices[dev_idx];
-        if (strcmp(key, "name") == 0) {
-            strncpy(dev->name, value, MAX_DEVICE_NAME - 1);
-        } else if (strcmp(key, "host") == 0) {
-            strncpy(dev->host, value, sizeof(dev->host) - 1);
-        } else if (strcmp(key, "port") == 0) {
-            dev->port = atoi(value);
-        } else if (strcmp(key, "slave_addr") == 0) {
-            dev->slave_addr = (int)strtol(value, NULL, 0);
-        } else if (strcmp(key, "enabled") == 0) {
-            dev->enabled = atoi(value);
-        }
-        /* Track max device index for count */
-        if (dev_idx + 1 > cfg->device_count) {
-            cfg->device_count = dev_idx + 1;
-        }
-        return;
-    }
-
-    /* Global sections */
-    if (strcmp(section, "device") == 0) {
-        if (strcmp(key, "count") == 0) {
-            /* Override count if explicitly set */
-            int c = atoi(value);
-            if (c >= 1 && c <= MAX_DEVICES) {
-                cfg->device_count = c;
-            }
-        }
-    } else if (strcmp(section, "modbus") == 0) {
-        /* Backward compatibility: [modbus] section sets device[0] */
+    if (strcmp(section, "modbus") == 0) {
         if (strcmp(key, "host") == 0) {
-            strncpy(cfg->devices[0].host, value, sizeof(cfg->devices[0].host) - 1);
+            strncpy(cfg->modbus_host, value, sizeof(cfg->modbus_host) - 1);
         } else if (strcmp(key, "port") == 0) {
-            cfg->devices[0].port = atoi(value);
+            cfg->modbus_port = atoi(value);
         } else if (strcmp(key, "slave_addr") == 0) {
-            cfg->devices[0].slave_addr = (int)strtol(value, NULL, 0);
+            cfg->modbus_slave_addr = (int)strtol(value, NULL, 0);
         } else if (strcmp(key, "timeout_sec") == 0) {
             cfg->modbus_timeout_sec = atoi(value);
         } else if (strcmp(key, "poll_ms") == 0) {
@@ -135,7 +86,6 @@ int config_load(app_config_t *cfg, const char *filepath)
 
     char line[1024];
     char section[256] = "";
-    int has_device_sections = 0;
 
     while (fgets(line, sizeof(line), fp)) {
         char *trimmed = trim(line);
@@ -152,9 +102,6 @@ int config_load(app_config_t *cfg, const char *filepath)
                 *end = '\0';
                 strncpy(section, trimmed + 1, sizeof(section) - 1);
                 section[sizeof(section) - 1] = '\0';
-                if (parse_device_index(section) >= 0) {
-                    has_device_sections = 1;
-                }
             }
             continue;
         }
@@ -170,29 +117,6 @@ int config_load(app_config_t *cfg, const char *filepath)
     }
 
     fclose(fp);
-
-    /* If no device.N sections found, reset to single device from [modbus] */
-    if (!has_device_sections) {
-        cfg->device_count = 1;
-        cfg->devices[0].enabled = 1;
-        if (cfg->devices[0].name[0] == '\0') {
-            strncpy(cfg->devices[0].name, "MPPT-1", MAX_DEVICE_NAME - 1);
-        }
-    }
-
-    /* Ensure all devices have names */
-    for (int i = 0; i < cfg->device_count; i++) {
-        if (cfg->devices[i].name[0] == '\0') {
-            snprintf(cfg->devices[i].name, MAX_DEVICE_NAME, "MPPT-%d", i + 1);
-        }
-        if (cfg->devices[i].port == 0) {
-            cfg->devices[i].port = 40635;
-        }
-        if (cfg->devices[i].slave_addr == 0) {
-            cfg->devices[i].slave_addr = 1;
-        }
-    }
-
     printf("[CONFIG] Loaded configuration from '%s'\n", filepath);
     return 0;
 }
@@ -201,18 +125,17 @@ int config_load(app_config_t *cfg, const char *filepath)
 void config_print(const app_config_t *cfg)
 {
     printf("[CONFIG] === Configuration ===\n");
-    printf("[CONFIG] Devices: %d\n", cfg->device_count);
-    for (int i = 0; i < cfg->device_count; i++) {
-        const device_config_t *dev = &cfg->devices[i];
-        printf("[CONFIG]   [%d] %-12s  %s:%d  addr=%d  %s\n",
-               i + 1, dev->name, dev->host, dev->port,
-               dev->slave_addr, dev->enabled ? "ON" : "OFF");
-    }
-    printf("[CONFIG] Modbus: timeout=%ds  poll=%dms\n",
-           cfg->modbus_timeout_sec, cfg->modbus_poll_ms);
-    printf("[CONFIG] Storage: interval=%ds  db=%s\n",
-           cfg->storage_interval_sec, cfg->db_path);
-    printf("[CONFIG] Web: port=%d  page_size=%d\n",
-           cfg->web_port, cfg->web_page_size);
+    printf("[CONFIG] [modbus]\n");
+    printf("[CONFIG]   host         = %s\n", cfg->modbus_host);
+    printf("[CONFIG]   port         = %d\n", cfg->modbus_port);
+    printf("[CONFIG]   slave_addr   = 0x%02X\n", cfg->modbus_slave_addr);
+    printf("[CONFIG]   timeout_sec  = %d\n", cfg->modbus_timeout_sec);
+    printf("[CONFIG]   poll_ms      = %d\n", cfg->modbus_poll_ms);
+    printf("[CONFIG] [storage]\n");
+    printf("[CONFIG]   interval_sec = %d\n", cfg->storage_interval_sec);
+    printf("[CONFIG]   db_path      = %s\n", cfg->db_path);
+    printf("[CONFIG] [web]\n");
+    printf("[CONFIG]   port         = %d\n", cfg->web_port);
+    printf("[CONFIG]   page_size    = %d\n", cfg->web_page_size);
     printf("[CONFIG] =====================\n");
 }

+ 12 - 27
mppt_monitor/src/config.h

@@ -2,8 +2,7 @@
  * @file config.h
  * @brief INI configuration file parser for MPPT Monitor
  *
- * Supports multiple MPPT devices configuration.
- * Backward compatible with single-device config format.
+ * Reads config.ini for runtime settings (storage interval, DB path, etc.)
  */
 
 #ifndef CONFIG_H
@@ -15,43 +14,29 @@
 extern "C" {
 #endif
 
-/* ── Limits ────────────────────────────────────────────────────── */
-#define MAX_DEVICES         16      /* Max number of MPPT devices */
-#define MAX_DEVICE_NAME     64      /* Max device name length */
-
-/* ── Single Device Configuration ───────────────────────────────── */
-typedef struct {
-    char    name[MAX_DEVICE_NAME];  /* Device display name */
-    char    host[256];              /* Modbus TCP host */
-    int     port;                   /* Modbus TCP port */
-    int     slave_addr;             /* Modbus slave address (1-247) */
-    int     enabled;                /* 1=enabled, 0=disabled */
-} device_config_t;
-
-/* ── Application Configuration ─────────────────────────────────── */
+/* ── Configuration Structure ───────────────────────────────────── */
 typedef struct {
-    /* Devices */
-    device_config_t devices[MAX_DEVICES];
-    int             device_count;       /* Number of configured devices */
-
-    /* Modbus global settings */
+    /* Modbus settings */
+    char  modbus_host[256];
+    int   modbus_port;
+    int   modbus_slave_addr;
     int   modbus_timeout_sec;
-    int   modbus_poll_ms;             /* Modbus read interval (ms) */
+    int   modbus_poll_ms;         /* Modbus read interval (ms) */
 
     /* Storage settings */
-    int   storage_interval_sec;       /* Data storage interval (seconds) */
-    char  db_path[512];               /* SQLite database file path */
+    int   storage_interval_sec;   /* Data storage interval (seconds), default 10 */
+    char  db_path[512];           /* SQLite database file path */
 
     /* Web server settings */
-    int   web_port;                   /* HTTP server port */
-    int   web_page_size;              /* Default page size for history API */
+    int   web_port;               /* HTTP server port, default 8085 */
+    int   web_page_size;          /* Default page size for history API */
 } app_config_t;
 
 /* ── Public API ────────────────────────────────────────────────── */
 
 /**
  * Load configuration from INI file.
- * If file doesn't exist, loads defaults (single device from [modbus] section).
+ * If file doesn't exist, loads defaults.
  * @param cfg       Pointer to config struct to fill
  * @param filepath  Path to config.ini
  * @return 0 on success, -1 on error (defaults still loaded)

+ 0 - 356
mppt_monitor/src/dashboard_html.h

@@ -1,356 +0,0 @@
-/**
- * @file dashboard_html.h
- * @brief Embedded HTML dashboard for MPPT Monitor
- *
- * Multi-device real-time monitoring with history charts.
- * Auto-refreshes every 2 seconds.
- */
-
-#ifndef DASHBOARD_HTML_H
-#define DASHBOARD_HTML_H
-
-static inline const char *web_get_dashboard_html(void)
-{
-    return
-"<!DOCTYPE html>\n"
-"<html lang=\"zh-CN\">\n"
-"<head>\n"
-"<meta charset=\"UTF-8\">\n"
-"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
-"<title>MPPT Solar Monitor - Multi-Device</title>\n"
-"<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js\"></script>\n"
-"<style>\n"
-"* { margin: 0; padding: 0; box-sizing: border-box; }\n"
-"body { font-family: -apple-system, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif;\n"
-"  background: #0f172a; color: #e2e8f0; min-height: 100vh; }\n"
-".header { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);\n"
-"  padding: 16px 24px; border-bottom: 1px solid #334155;\n"
-"  display: flex; justify-content: space-between; align-items: center; }\n"
-".header h1 { font-size: 20px; color: #f59e0b; }\n"
-".header .status { font-size: 13px; color: #94a3b8; }\n"
-".tabs { display: flex; gap: 0; background: #1e293b; border-bottom: 1px solid #334155; }\n"
-".tab { padding: 10px 24px; cursor: pointer; color: #94a3b8; font-size: 14px;\n"
-"  border-bottom: 2px solid transparent; transition: all 0.2s; }\n"
-".tab:hover { color: #e2e8f0; }\n"
-".tab.active { color: #f59e0b; border-bottom-color: #f59e0b; }\n"
-".page { display: none; padding: 20px; }\n"
-".page.active { display: block; }\n"
-".devices-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));\n"
-"  gap: 16px; margin-bottom: 20px; }\n"
-".device-card { background: #1e293b; border: 1px solid #334155; border-radius: 12px;\n"
-"  padding: 20px; transition: border-color 0.3s; }\n"
-".device-card.online { border-color: #22c55e; }\n"
-".device-card.offline { border-color: #ef4444; }\n"
-".device-header { display: flex; justify-content: space-between; align-items: center;\n"
-"  margin-bottom: 16px; }\n"
-".device-name { font-size: 18px; font-weight: 600; color: #f8fafc; }\n"
-".device-status { font-size: 12px; padding: 3px 10px; border-radius: 12px;\n"
-"  font-weight: 500; }\n"
-".device-status.online { background: #22c55e20; color: #22c55e; }\n"
-".device-status.offline { background: #ef444420; color: #ef4444; }\n"
-".data-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n"
-".data-item { background: #0f172a; border-radius: 8px; padding: 10px 12px; }\n"
-".data-label { font-size: 11px; color: #64748b; text-transform: uppercase;\n"
-"  letter-spacing: 0.5px; margin-bottom: 4px; }\n"
-".data-value { font-size: 20px; font-weight: 700; color: #f8fafc; }\n"
-".data-value .unit { font-size: 12px; color: #94a3b8; font-weight: 400; }\n"
-".data-item.full { grid-column: 1 / -1; }\n"
-".soc-bar { height: 8px; background: #334155; border-radius: 4px; margin-top: 6px;\n"
-"  overflow: hidden; }\n"
-".soc-fill { height: 100%; border-radius: 4px; transition: width 0.5s;\n"
-"  background: linear-gradient(90deg, #22c55e, #84cc16); }\n"
-".charge-badge { display: inline-block; padding: 2px 8px; border-radius: 4px;\n"
-"  font-size: 12px; font-weight: 500; }\n"
-".charge-badge.charging { background: #f59e0b20; color: #f59e0b; }\n"
-".charge-badge.idle { background: #64748b20; color: #94a3b8; }\n"
-".history-controls { background: #1e293b; border-radius: 12px; padding: 20px;\n"
-"  margin-bottom: 20px; border: 1px solid #334155; }\n"
-".controls-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }\n"
-".control-group { display: flex; flex-direction: column; gap: 4px; }\n"
-".control-group label { font-size: 12px; color: #94a3b8; }\n"
-".control-group input, .control-group select { background: #0f172a;\n"
-"  border: 1px solid #334155; border-radius: 6px; padding: 8px 12px;\n"
-"  color: #e2e8f0; font-size: 13px; }\n"
-".btn { padding: 8px 20px; border-radius: 6px; border: none; cursor: pointer;\n"
-"  font-size: 13px; font-weight: 500; transition: all 0.2s; }\n"
-".btn-primary { background: #f59e0b; color: #0f172a; }\n"
-".btn-primary:hover { background: #d97706; }\n"
-".btn-secondary { background: #334155; color: #e2e8f0; }\n"
-".btn-secondary:hover { background: #475569; }\n"
-".chart-container { background: #1e293b; border-radius: 12px; padding: 20px;\n"
-"  margin-bottom: 20px; border: 1px solid #334155; }\n"
-".chart-container h3 { color: #f59e0b; margin-bottom: 12px; font-size: 15px; }\n"
-".chart-wrapper { position: relative; height: 300px; }\n"
-".table-container { background: #1e293b; border-radius: 12px; padding: 20px;\n"
-"  border: 1px solid #334155; overflow-x: auto; }\n"
-"table { width: 100%; border-collapse: collapse; font-size: 13px; }\n"
-"th { background: #0f172a; color: #94a3b8; padding: 10px 12px; text-align: left;\n"
-"  font-weight: 500; text-transform: uppercase; font-size: 11px;\n"
-"  letter-spacing: 0.5px; }\n"
-"td { padding: 10px 12px; border-bottom: 1px solid #1e293b; color: #e2e8f0; }\n"
-"tr:hover td { background: #0f172a80; }\n"
-".pagination { display: flex; justify-content: center; gap: 8px;\n"
-"  margin-top: 16px; align-items: center; }\n"
-".pagination button { padding: 6px 14px; border-radius: 6px; border: 1px solid #334155;\n"
-"  background: #0f172a; color: #e2e8f0; cursor: pointer; font-size: 13px; }\n"
-".pagination button:hover:not(:disabled) { background: #334155; }\n"
-".pagination button:disabled { opacity: 0.4; cursor: not-allowed; }\n"
-".pagination .page-info { color: #94a3b8; font-size: 13px; }\n"
-".update-time { text-align: center; color: #64748b; font-size: 12px;\n"
-"  margin-top: 12px; }\n"
-"@media (max-width: 768px) {\n"
-"  .devices-grid { grid-template-columns: 1fr; }\n"
-"  .controls-row { flex-direction: column; }\n"
-"  .data-grid { grid-template-columns: 1fr; }\n"
-"}\n"
-"</style>\n"
-"</head>\n"
-"<body>\n"
-"<div class=\"header\">\n"
-"  <h1>MPPT Solar Monitor</h1>\n"
-"  <div class=\"status\" id=\"update-status\">Connecting...</div>\n"
-"</div>\n"
-"<div class=\"tabs\">\n"
-"  <div class=\"tab active\" onclick=\"switchPage('realtime')\">Real-time</div>\n"
-"  <div class=\"tab\" onclick=\"switchPage('history')\">History</div>\n"
-"</div>\n"
-
-"<!-- Real-time Page -->\n"
-"<div class=\"page active\" id=\"page-realtime\">\n"
-"  <div class=\"devices-grid\" id=\"devices-grid\"></div>\n"
-"  <div class=\"update-time\" id=\"rt-update-time\"></div>\n"
-"</div>\n"
-
-"<!-- History Page -->\n"
-"<div class=\"page\" id=\"page-history\">\n"
-"  <div class=\"history-controls\">\n"
-"    <div class=\"controls-row\">\n"
-"      <div class=\"control-group\">\n"
-"        <label>Device</label>\n"
-"        <select id=\"hist-device\"><option value=\"\">All Devices</option></select>\n"
-"      </div>\n"
-"      <div class=\"control-group\">\n"
-"        <label>Start</label>\n"
-"        <input type=\"datetime-local\" id=\"hist-start\">\n"
-"      </div>\n"
-"      <div class=\"control-group\">\n"
-"        <label>End</label>\n"
-"        <input type=\"datetime-local\" id=\"hist-end\">\n"
-"      </div>\n"
-"      <div class=\"control-group\">\n"
-"        <label>Page Size</label>\n"
-"        <select id=\"hist-pagesize\">\n"
-"          <option value=\"20\">20</option>\n"
-"          <option value=\"50\" selected>50</option>\n"
-"          <option value=\"100\">100</option>\n"
-"          <option value=\"200\">200</option>\n"
-"        </select>\n"
-"      </div>\n"
-"      <button class=\"btn btn-primary\" onclick=\"loadHistory()\">Query</button>\n"
-"      <button class=\"btn btn-secondary\" onclick=\"setQuickRange(1)\">1h</button>\n"
-"      <button class=\"btn btn-secondary\" onclick=\"setQuickRange(24)\">24h</button>\n"
-"      <button class=\"btn btn-secondary\" onclick=\"setQuickRange(168)\">7d</button>\n"
-"    </div>\n"
-"  </div>\n"
-"  <div class=\"chart-container\">\n"
-"    <h3>Trend Chart</h3>\n"
-"    <div class=\"chart-wrapper\"><canvas id=\"historyChart\"></canvas></div>\n"
-"  </div>\n"
-"  <div class=\"table-container\">\n"
-"    <table><thead><tr>\n"
-"      <th>Time</th><th>Device</th><th>Battery(V)</th><th>SOC(%)</th>\n"
-"      <th>PV(V)</th><th>Current(A)</th><th>Power(W)</th>\n"
-"      <th>Temp(C)</th><th>Energy(kWh)</th><th>Fault</th><th>State</th>\n"
-"    </tr></thead><tbody id=\"history-tbody\"></tbody></table>\n"
-"    <div class=\"pagination\">\n"
-"      <button id=\"btn-prev\" onclick=\"historyPrev()\" disabled>Prev</button>\n"
-"      <span class=\"page-info\" id=\"page-info\">-</span>\n"
-"      <button id=\"btn-next\" onclick=\"historyNext()\" disabled>Next</button>\n"
-"    </div>\n"
-"  </div>\n"
-"</div>\n"
-
-"<script>\n"
-"let historyChart = null;\n"
-"let currentPage = 1;\n"
-"let totalPages = 1;\n"
-"let deviceNames = [];\n"
-"const CHARGE_STATES = ['Idle','Charging(Bulk)','Charging(Absorb)','Charging(Float)'];\n"
-"\n"
-"function switchPage(name) {\n"
-"  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));\n"
-"  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n"
-"  document.getElementById('page-' + name).classList.add('active');\n"
-"  document.querySelectorAll('.tab')[name === 'realtime' ? 0 : 1].classList.add('active');\n"
-"  if (name === 'history' && !historyChart) initChart();\n"
-"}\n"
-"\n"
-"function renderDevices(data) {\n"
-"  const grid = document.getElementById('devices-grid');\n"
-"  if (!data.devices) return;\n"
-"  deviceNames = data.devices.map(d => d.name);\n"
-"  updateDeviceSelect();\n"
-"  let html = '';\n"
-"  data.devices.forEach(d => {\n"
-"    const online = d.data_valid && d.conn_state === 'connected';\n"
-"    const socColor = d.battery.soc > 80 ? '#22c55e' : d.battery.soc > 40 ? '#f59e0b' : '#ef4444';\n"
-"    const charging = d.device.charge_state > 0;\n"
-"    html += '<div class=\"device-card ' + (online ? 'online' : 'offline') + '\">';\n"
-"    html += '<div class=\"device-header\">';\n"
-"    html += '<span class=\"device-name\">' + d.name + '</span>';\n"
-"    html += '<span class=\"device-status ' + (online ? 'online' : 'offline') + '\">';\n"
-"    html += (online ? 'ONLINE' : 'OFFLINE') + '</span></div>';\n"
-"    html += '<div class=\"data-grid\">';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">Battery</div>';\n"
-"    html += '<div class=\"data-value\">' + d.battery.voltage.toFixed(1) + ' <span class=\"unit\">V</span></div>';\n"
-"    html += '<div class=\"soc-bar\"><div class=\"soc-fill\" style=\"width:' + d.battery.soc + '%;background:' + socColor + '\"></div></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">SOC</div>';\n"
-"    html += '<div class=\"data-value\">' + d.battery.soc + ' <span class=\"unit\">%</span></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">PV Voltage</div>';\n"
-"    html += '<div class=\"data-value\">' + d.pv.voltage.toFixed(1) + ' <span class=\"unit\">V</span></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">PV Current</div>';\n"
-"    html += '<div class=\"data-value\">' + d.pv.current.toFixed(1) + ' <span class=\"unit\">A</span></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">PV Power</div>';\n"
-"    html += '<div class=\"data-value\">' + d.pv.power.toFixed(1) + ' <span class=\"unit\">W</span></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">Temperature</div>';\n"
-"    html += '<div class=\"data-value\">' + d.device.temperature.toFixed(0) + ' <span class=\"unit\">&deg;C</span></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">Energy</div>';\n"
-"    html += '<div class=\"data-value\">' + d.cumulative_energy_kwh.toFixed(1) + ' <span class=\"unit\">kWh</span></div></div>';\n"
-"    html += '<div class=\"data-item\"><div class=\"data-label\">Status</div>';\n"
-"    html += '<span class=\"charge-badge ' + (charging ? 'charging' : 'idle') + '\">';\n"
-"    html += (charging ? d.device.charge_state_desc : 'Idle') + '</span>';\n"
-"    html += ' <span style=\"font-size:11px;color:#64748b\">' + d.device.fault_desc + '</span></div>';\n"
-"    html += '</div></div>';\n"
-"  });\n"
-"  grid.innerHTML = html;\n"
-"  document.getElementById('rt-update-time').textContent = 'Updated: ' + new Date().toLocaleTimeString();\n"
-"  document.getElementById('update-status').textContent = data.device_count + ' device(s) | ' + new Date().toLocaleTimeString();\n"
-"}\n"
-"\n"
-"function updateDeviceSelect() {\n"
-"  const sel = document.getElementById('hist-device');\n"
-"  const current = sel.value;\n"
-"  sel.innerHTML = '<option value=\"\">All Devices</option>';\n"
-"  deviceNames.forEach(n => {\n"
-"    sel.innerHTML += '<option value=\"' + n + '\">' + n + '</option>';\n"
-"  });\n"
-"  sel.value = current;\n"
-"}\n"
-"\n"
-"function fetchDevices() {\n"
-"  fetch('/api/devices').then(r => r.json()).then(data => {\n"
-"    renderDevices(data);\n"
-"  }).catch(e => {\n"
-"    document.getElementById('update-status').textContent = 'Error: ' + e.message;\n"
-"  });\n"
-"}\n"
-"\n"
-"function initChart() {\n"
-"  const ctx = document.getElementById('historyChart').getContext('2d');\n"
-"  historyChart = new Chart(ctx, {\n"
-"    type: 'line',\n"
-"    data: { datasets: [] },\n"
-"    options: {\n"
-"      responsive: true, maintainAspectRatio: false,\n"
-"      interaction: { mode: 'index', intersect: false },\n"
-"      scales: {\n"
-"        x: { type: 'time', ticks: { color: '#94a3b8' }, grid: { color: '#334155' } },\n"
-"        y: { ticks: { color: '#94a3b8' }, grid: { color: '#33415580' } }\n"
-"      },\n"
-"      plugins: { legend: { labels: { color: '#e2e8f0' } } }\n"
-"    }\n"
-"  });\n"
-"}\n"
-"\n"
-"function setQuickRange(hours) {\n"
-"  const end = new Date();\n"
-"  const start = new Date(end - hours * 3600000);\n"
-"  document.getElementById('hist-start').value = start.toISOString().slice(0, 16);\n"
-"  document.getElementById('hist-end').value = end.toISOString().slice(0, 16);\n"
-"  loadHistory();\n"
-"}\n"
-"\n"
-"function loadHistory(page) {\n"
-"  if (page) currentPage = page;\n"
-"  const device = document.getElementById('hist-device').value;\n"
-"  const start = document.getElementById('hist-start').value;\n"
-"  const end = document.getElementById('hist-end').value;\n"
-"  const pageSize = document.getElementById('hist-pagesize').value;\n"
-"  let url = '/api/history?page=' + currentPage + '&page_size=' + pageSize;\n"
-"  if (device) url += '&device=' + encodeURIComponent(device);\n"
-"  if (start) url += '&start_time=' + start.replace('T', ' ');\n"
-"  if (end) url += '&end_time=' + end.replace('T', ' ');\n"
-"  fetch(url).then(r => r.json()).then(data => {\n"
-"    totalPages = data.total_pages || 1;\n"
-"    document.getElementById('page-info').textContent =\n"
-"      'Page ' + data.page + '/' + totalPages + ' (' + data.total + ' records)';\n"
-"    document.getElementById('btn-prev').disabled = data.page <= 1;\n"
-"    document.getElementById('btn-next').disabled = data.page >= totalPages;\n"
-"    renderHistoryTable(data.records);\n"
-"    renderHistoryChart(data.records);\n"
-"  });\n"
-"}\n"
-"\n"
-"function historyPrev() { if (currentPage > 1) loadHistory(currentPage - 1); }\n"
-"function historyNext() { if (currentPage < totalPages) loadHistory(currentPage + 1); }\n"
-"\n"
-"function renderHistoryTable(records) {\n"
-"  const tbody = document.getElementById('history-tbody');\n"
-"  let html = '';\n"
-"  records.forEach(r => {\n"
-"    html += '<tr>';\n"
-"    html += '<td>' + r.timestamp + '</td>';\n"
-"    html += '<td>' + (r.device_name || '-') + '</td>';\n"
-"    html += '<td>' + r.battery_voltage.toFixed(1) + '</td>';\n"
-"    html += '<td>' + r.battery_soc + '</td>';\n"
-"    html += '<td>' + r.pv_voltage.toFixed(1) + '</td>';\n"
-"    html += '<td>' + r.pv_charge_current.toFixed(1) + '</td>';\n"
-"    html += '<td>' + r.pv_input_power.toFixed(1) + '</td>';\n"
-"    html += '<td>' + r.machine_temp.toFixed(0) + '</td>';\n"
-"    html += '<td>' + r.cumulative_energy.toFixed(1) + '</td>';\n"
-"    html += '<td>' + r.fault_desc + '</td>';\n"
-"    html += '<td>' + (r.charge_state_desc || '-') + '</td>';\n"
-"    html += '</tr>';\n"
-"  });\n"
-"  tbody.innerHTML = html || '<tr><td colspan=\"11\" style=\"text-align:center;color:#64748b\">No data</td></tr>';\n"
-"}\n"
-"\n"
-"function renderHistoryChart(records) {\n"
-"  if (!historyChart) return;\n"
-"  const datasets = [];\n"
-"  const deviceGroups = {};\n"
-"  records.forEach(r => {\n"
-"    const name = r.device_name || 'Default';\n"
-"    if (!deviceGroups[name]) deviceGroups[name] = [];\n"
-"    deviceGroups[name].push(r);\n"
-"  });\n"
-"  const colors = ['#f59e0b','#22c55e','#3b82f6','#ef4444','#8b5cf6','#ec4899'];\n"
-"  let ci = 0;\n"
-"  for (const [name, recs] of Object.entries(deviceGroups)) {\n"
-"    const color = colors[ci % colors.length]; ci++;\n"
-"    datasets.push({\n"
-"      label: name + ' Battery(V)',\n"
-"      data: recs.map(r => ({ x: new Date(r.timestamp_unix * 1000), y: r.battery_voltage })),\n"
-"      borderColor: color, backgroundColor: color + '20',\n"
-"      tension: 0.3, pointRadius: 2, borderWidth: 2\n"
-"    });\n"
-"    datasets.push({\n"
-"      label: name + ' Power(W)',\n"
-"      data: recs.map(r => ({ x: new Date(r.timestamp_unix * 1000), y: r.pv_input_power })),\n"
-"      borderColor: color + '80', backgroundColor: 'transparent',\n"
-"      tension: 0.3, pointRadius: 1, borderWidth: 1, borderDash: [5, 3]\n"
-"    });\n"
-"  }\n"
-"  historyChart.data.datasets = datasets;\n"
-"  historyChart.update();\n"
-"}\n"
-"\n"
-"/* Init */\n"
-"fetchDevices();\n"
-"setInterval(fetchDevices, 2000);\n"
-"</script>\n"
-"</body>\n"
-"</html>\n";
-}
-
-#endif /* DASHBOARD_HTML_H */

+ 43 - 70
mppt_monitor/src/db.c

@@ -16,7 +16,6 @@ static int db_create_tables(db_handle_t *handle)
         "CREATE TABLE IF NOT EXISTS mppt_history ("
         "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
         "  timestamp INTEGER NOT NULL,"
-        "  device_name TEXT DEFAULT '',"
         "  battery_voltage REAL,"
         "  pv_voltage REAL,"
         "  pv_charge_current REAL,"
@@ -29,8 +28,7 @@ static int db_create_tables(db_handle_t *handle)
         "  device_type INTEGER,"
         "  pv_input_power REAL"
         ");"
-        "CREATE INDEX IF NOT EXISTS idx_timestamp ON mppt_history(timestamp);"
-        "CREATE INDEX IF NOT EXISTS idx_device ON mppt_history(device_name);";
+        "CREATE INDEX IF NOT EXISTS idx_timestamp ON mppt_history(timestamp);";
 
     char *errmsg = NULL;
     int rc = sqlite3_exec(handle->db, sql, NULL, NULL, &errmsg);
@@ -39,12 +37,6 @@ static int db_create_tables(db_handle_t *handle)
         sqlite3_free(errmsg);
         return -1;
     }
-
-    /* Migration: add device_name column if it doesn't exist (for existing DBs) */
-    sqlite3_exec(handle->db,
-        "ALTER TABLE mppt_history ADD COLUMN device_name TEXT DEFAULT '';",
-        NULL, NULL, NULL);  /* Ignore error if column already exists */
-
     return 0;
 }
 
@@ -91,10 +83,10 @@ int db_insert_record(db_handle_t *handle, const mppt_data_t *data)
 {
     const char *sql =
         "INSERT INTO mppt_history "
-        "(timestamp, device_name, battery_voltage, pv_voltage, pv_charge_current, "
+        "(timestamp, battery_voltage, pv_voltage, pv_charge_current, "
         "cumulative_energy, machine_temp, fault_code, battery_soc, "
         "battery_string, charge_state, device_type, pv_input_power) "
-        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
 
     sqlite3_stmt *stmt = NULL;
     int rc = sqlite3_prepare_v2(handle->db, sql, -1, &stmt, NULL);
@@ -105,18 +97,17 @@ int db_insert_record(db_handle_t *handle, const mppt_data_t *data)
     }
 
     sqlite3_bind_int64(stmt, 1, (int64_t)data->last_update);
-    sqlite3_bind_text(stmt, 2, data->device_name, -1, SQLITE_STATIC);
-    sqlite3_bind_double(stmt, 3, data->battery_voltage);
-    sqlite3_bind_double(stmt, 4, data->pv_voltage);
-    sqlite3_bind_double(stmt, 5, data->pv_charge_current);
-    sqlite3_bind_double(stmt, 6, data->cumulative_energy);
-    sqlite3_bind_double(stmt, 7, data->machine_temp);
-    sqlite3_bind_int(stmt, 8, (int)data->fault_code);
-    sqlite3_bind_int(stmt, 9, (int)data->battery_soc);
-    sqlite3_bind_int(stmt, 10, (int)data->battery_string);
-    sqlite3_bind_int(stmt, 11, (int)data->charge_state);
-    sqlite3_bind_int(stmt, 12, (int)data->device_type);
-    sqlite3_bind_double(stmt, 13, data->pv_input_power);
+    sqlite3_bind_double(stmt, 2, data->battery_voltage);
+    sqlite3_bind_double(stmt, 3, data->pv_voltage);
+    sqlite3_bind_double(stmt, 4, data->pv_charge_current);
+    sqlite3_bind_double(stmt, 5, data->cumulative_energy);
+    sqlite3_bind_double(stmt, 6, data->machine_temp);
+    sqlite3_bind_int(stmt, 7, (int)data->fault_code);
+    sqlite3_bind_int(stmt, 8, (int)data->battery_soc);
+    sqlite3_bind_int(stmt, 9, (int)data->battery_string);
+    sqlite3_bind_int(stmt, 10, (int)data->charge_state);
+    sqlite3_bind_int(stmt, 11, (int)data->device_type);
+    sqlite3_bind_double(stmt, 12, data->pv_input_power);
 
     rc = sqlite3_step(stmt);
     sqlite3_finalize(stmt);
@@ -139,36 +130,23 @@ int db_query_history(db_handle_t *handle, const history_query_t *query,
     result->page_size = query->page_size;
 
     /* Build WHERE clause */
-    char where_clause[768] = "";
-    char conditions[768] = "";
-    int cond_count = 0;
-
-    /* Device name filter */
-    if (query->device_name[0] != '\0') {
-        snprintf(conditions + strlen(conditions),
-                 sizeof(conditions) - strlen(conditions),
-                 "device_name = '%s'", query->device_name);
-        cond_count++;
-    }
-
-    /* Time range filters */
-    if (query->start_time[0] != '\0') {
-        if (cond_count > 0) strncat(conditions, " AND ", sizeof(conditions) - strlen(conditions) - 1);
-        snprintf(conditions + strlen(conditions),
-                 sizeof(conditions) - strlen(conditions),
-                 "timestamp >= strftime('%%s', '%s')", query->start_time);
-        cond_count++;
-    }
-    if (query->end_time[0] != '\0') {
-        if (cond_count > 0) strncat(conditions, " AND ", sizeof(conditions) - strlen(conditions) - 1);
-        snprintf(conditions + strlen(conditions),
-                 sizeof(conditions) - strlen(conditions),
-                 "timestamp <= strftime('%%s', '%s')", query->end_time);
-        cond_count++;
-    }
-
-    if (cond_count > 0) {
-        snprintf(where_clause, sizeof(where_clause), "WHERE %s", conditions);
+    char where_clause[512] = "";
+    int has_start = (query->start_time[0] != '\0');
+    int has_end = (query->end_time[0] != '\0');
+
+    if (has_start && has_end) {
+        snprintf(where_clause, sizeof(where_clause),
+                 "WHERE timestamp >= strftime('%%s', '%s') "
+                 "AND timestamp <= strftime('%%s', '%s')",
+                 query->start_time, query->end_time);
+    } else if (has_start) {
+        snprintf(where_clause, sizeof(where_clause),
+                 "WHERE timestamp >= strftime('%%s', '%s')",
+                 query->start_time);
+    } else if (has_end) {
+        snprintf(where_clause, sizeof(where_clause),
+                 "WHERE timestamp <= strftime('%%s', '%s')",
+                 query->end_time);
     }
 
     /* Count total records */
@@ -205,7 +183,7 @@ int db_query_history(db_handle_t *handle, const history_query_t *query,
     const char *order = (strcmp(query->sort_order, "ASC") == 0) ? "ASC" : "DESC";
     char select_sql[1024];
     snprintf(select_sql, sizeof(select_sql),
-             "SELECT id, timestamp, device_name, battery_voltage, pv_voltage, "
+             "SELECT id, timestamp, battery_voltage, pv_voltage, "
              "pv_charge_current, cumulative_energy, machine_temp, "
              "fault_code, battery_soc, battery_string, charge_state, "
              "device_type, pv_input_power "
@@ -234,22 +212,17 @@ int db_query_history(db_handle_t *handle, const history_query_t *query,
         history_record_t *rec = &result->records[count];
         rec->id = sqlite3_column_int64(stmt, 0);
         rec->timestamp = sqlite3_column_int64(stmt, 1);
-        /* device_name */
-        const unsigned char *dn = sqlite3_column_text(stmt, 2);
-        if (dn) {
-            strncpy(rec->device_name, (const char *)dn, sizeof(rec->device_name) - 1);
-        }
-        rec->battery_voltage = sqlite3_column_double(stmt, 3);
-        rec->pv_voltage = sqlite3_column_double(stmt, 4);
-        rec->pv_charge_current = sqlite3_column_double(stmt, 5);
-        rec->cumulative_energy = sqlite3_column_double(stmt, 6);
-        rec->machine_temp = sqlite3_column_double(stmt, 7);
-        rec->fault_code = (uint16_t)sqlite3_column_int(stmt, 8);
-        rec->battery_soc = (uint16_t)sqlite3_column_int(stmt, 9);
-        rec->battery_string = (uint16_t)sqlite3_column_int(stmt, 10);
-        rec->charge_state = (uint16_t)sqlite3_column_int(stmt, 11);
-        rec->device_type = (uint16_t)sqlite3_column_int(stmt, 12);
-        rec->pv_input_power = sqlite3_column_double(stmt, 13);
+        rec->battery_voltage = sqlite3_column_double(stmt, 2);
+        rec->pv_voltage = sqlite3_column_double(stmt, 3);
+        rec->pv_charge_current = sqlite3_column_double(stmt, 4);
+        rec->cumulative_energy = sqlite3_column_double(stmt, 5);
+        rec->machine_temp = sqlite3_column_double(stmt, 6);
+        rec->fault_code = (uint16_t)sqlite3_column_int(stmt, 7);
+        rec->battery_soc = (uint16_t)sqlite3_column_int(stmt, 8);
+        rec->battery_string = (uint16_t)sqlite3_column_int(stmt, 9);
+        rec->charge_state = (uint16_t)sqlite3_column_int(stmt, 10);
+        rec->device_type = (uint16_t)sqlite3_column_int(stmt, 11);
+        rec->pv_input_power = sqlite3_column_double(stmt, 12);
         count++;
     }
     result->count = count;

+ 0 - 2
mppt_monitor/src/db.h

@@ -19,7 +19,6 @@ extern "C" {
 typedef struct {
     int64_t  id;
     int64_t  timestamp;           /* Unix timestamp */
-    char     device_name[64];     /* Device name */
     double   battery_voltage;
     double   pv_voltage;
     double   pv_charge_current;
@@ -35,7 +34,6 @@ typedef struct {
 
 /* ── Query Parameters ──────────────────────────────────────────── */
 typedef struct {
-    char     device_name[64];     /* Filter by device name (empty = all) */
     char     start_time[32];      /* "YYYY-MM-DD HH:MM:SS" or empty */
     char     end_time[32];        /* "YYYY-MM-DD HH:MM:SS" or empty */
     int      page;                /* 1-based page number */

+ 60 - 94
mppt_monitor/src/main.c

@@ -2,8 +2,8 @@
  * @file main.c
  * @brief MPPT Solar Charge Controller Monitor - Main Entry Point
  *
- * Multi-device architecture:
- *   1. Multiple Modbus RTU over TCP clients (one thread per device)
+ * Architecture:
+ *   1. Modbus RTU over TCP client -> reads MPPT registers every 2s
  *   2. Mongoose HTTP server -> serves JSON API + HTML dashboard
  *   3. SQLite3 historical data storage (configurable interval)
  *   4. Auto-reconnect on TCP failure with 10s timeout
@@ -35,8 +35,8 @@
 
 /* ── Global State ──────────────────────────────────────────────── */
 static volatile sig_atomic_t g_running = 1;
-static device_runtime_t g_devices[MAX_DEVICES];
-static int g_device_count = 0;
+static mppt_data_t  g_mppt_data;
+static modbus_conn_t g_modbus_conn;
 static web_ctx_t    g_web_ctx;
 static db_handle_t  g_db;
 static app_config_t g_config;
@@ -52,34 +52,33 @@ static void signal_handler(int sig)
     printf("\n[MAIN] Shutting down...\n");
 }
 
-/* ── Per-device Modbus Polling Thread ──────────────────────────── */
-typedef struct {
-    int device_idx;
-} thread_arg_t;
-
+/* ── Modbus Polling Thread ─────────────────────────────────────── */
+/*
+ * Runs in a separate thread. Every config.modbus_poll_ms:
+ *   1. Check connection, reconnect if needed (10s timeout)
+ *   2. Send Modbus read command
+ *   3. Parse response and update shared data
+ *   4. Periodically store to database (config.storage_interval_sec)
+ *   5. Print debug output
+ */
 static void *modbus_poll_thread(void *arg)
 {
-    thread_arg_t *targ = (thread_arg_t *)arg;
-    int idx = targ->device_idx;
-    free(targ);
-
-    device_runtime_t *dev = &g_devices[idx];
+    (void)arg;
     mppt_data_t local_data;
     int ret;
     time_t last_store_time = 0;
 
     memset(&local_data, 0, sizeof(local_data));
-    local_data.device_id = idx;
-    snprintf(local_data.device_name, sizeof(local_data.device_name),
-             "%s", dev->conn.device_name);
 
-    printf("[MODBUS-%s] Poll thread started (interval: %dms)\n",
-           dev->conn.device_name, g_config.modbus_poll_ms);
+    printf("[MODBUS] Poll thread started (interval: %dms)\n",
+           g_config.modbus_poll_ms);
+    printf("[STORAGE] Data storage interval: %d seconds\n",
+           g_config.storage_interval_sec);
 
     while (g_running) {
         /* Ensure connection is alive */
-        if (dev->conn.state != MODBUS_STATE_CONNECTED) {
-            ret = modbus_connect(&dev->conn);
+        if (g_modbus_conn.state != MODBUS_STATE_CONNECTED) {
+            ret = modbus_connect(&g_modbus_conn);
             if (ret != 0) {
                 for (int i = 0; i < 10 && g_running; i++) {
                     struct timespec short_ts = { .tv_sec = 0, .tv_nsec = 100000000L };
@@ -89,15 +88,12 @@ static void *modbus_poll_thread(void *arg)
             }
         }
 
-        /* Read Input Registers (FC=0x04, real-time data) */
-        ret = modbus_read_input_registers(&dev->conn, &local_data);
+        /* Read registers */
+        ret = modbus_read_registers(&g_modbus_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));
+            memcpy((mppt_data_t *)&g_mppt_data, &local_data, sizeof(mppt_data_t));
             pthread_mutex_unlock(&g_data_mutex);
 
             /* Print to console */
@@ -107,16 +103,15 @@ static void *modbus_poll_thread(void *arg)
             time_t now = time(NULL);
             if ((now - last_store_time) >= g_config.storage_interval_sec) {
                 pthread_mutex_lock(&g_data_mutex);
-                ret = db_insert_record(&g_db, &dev->data);
+                ret = db_insert_record(&g_db, &g_mppt_data);
                 pthread_mutex_unlock(&g_data_mutex);
 
                 if (ret == 0) {
-                    printf("[STORAGE-%s] Data saved to DB at %s",
-                           dev->conn.device_name, ctime(&now));
+                    printf("[STORAGE] Data saved to DB at %s",
+                           ctime(&now));
                     last_store_time = now;
                 } else {
-                    fprintf(stderr, "[STORAGE-%s] Failed to save data to DB\n",
-                            dev->conn.device_name);
+                    fprintf(stderr, "[STORAGE] Failed to save data to DB\n");
                 }
             }
         } else {
@@ -125,23 +120,23 @@ static void *modbus_poll_thread(void *arg)
             local_data.data_valid = false;
 
             pthread_mutex_lock(&g_data_mutex);
-            dev->data.consecutive_failures = local_data.consecutive_failures;
-            dev->data.data_valid = false;
+            g_mppt_data.consecutive_failures = local_data.consecutive_failures;
+            g_mppt_data.data_valid = false;
             pthread_mutex_unlock(&g_data_mutex);
 
-            fprintf(stderr, "[MODBUS-%s] Read failed (consecutive: %d)\n",
-                    dev->conn.device_name, local_data.consecutive_failures);
+            fprintf(stderr, "[MODBUS] Read failed (consecutive: %d)\n",
+                    local_data.consecutive_failures);
 
             /* If too many failures, disconnect to force reconnect */
             if (local_data.consecutive_failures >= MODBUS_MAX_RETRIES) {
-                fprintf(stderr, "[MODBUS-%s] Too many failures, reconnecting...\n",
-                        dev->conn.device_name);
-                modbus_disconnect(&dev->conn);
+                fprintf(stderr, "[MODBUS] Too many failures, reconnecting...\n");
+                modbus_disconnect(&g_modbus_conn);
                 local_data.consecutive_failures = 0;
             }
         }
 
         /* Wait for next poll interval */
+        /* Sleep in small increments to allow quick shutdown */
         int remaining_ms = g_config.modbus_poll_ms;
         while (remaining_ms > 0 && g_running) {
             int sleep_ms = (remaining_ms > 200) ? 200 : remaining_ms;
@@ -154,8 +149,8 @@ static void *modbus_poll_thread(void *arg)
     }
 
     /* Cleanup */
-    modbus_disconnect(&dev->conn);
-    printf("[MODBUS-%s] Poll thread exiting\n", dev->conn.device_name);
+    modbus_disconnect(&g_modbus_conn);
+    printf("[MODBUS] Poll thread exiting\n");
     return NULL;
 }
 
@@ -184,7 +179,7 @@ static void web_watchdog(struct mg_mgr *mgr)
 int main(int argc, char *argv[])
 {
     struct mg_mgr mgr;
-    pthread_t modbus_tids[MAX_DEVICES];
+    pthread_t modbus_tid;
     int ret;
 
     (void)argc;
@@ -193,7 +188,7 @@ int main(int argc, char *argv[])
     /* Banner */
     printf("============================================\n");
     printf("  MPPT Solar Charge Controller Monitor\n");
-    printf("  Version: 1.2.0 (Multi-Device)\n");
+    printf("  Version: 1.1.0\n");
     printf("  Mongoose: 7.22 | cJSON: 1.7.19 | SQLite3\n");
     printf("============================================\n\n");
 
@@ -206,31 +201,9 @@ int main(int argc, char *argv[])
     config_load(&g_config, "config.ini");
     config_print(&g_config);
 
-    g_device_count = g_config.device_count;
-    printf("[MAIN] Configured devices: %d\n", g_device_count);
-
-    /* Initialize MPPT data for each device */
-    for (int i = 0; i < g_device_count; i++) {
-        device_config_t *dcfg = &g_config.devices[i];
-        if (!dcfg->enabled) {
-            printf("[MAIN] Device [%s] disabled, skipping\n", dcfg->name);
-            continue;
-        }
-
-        memset(&g_devices[i].data, 0, sizeof(mppt_data_t));
-        g_devices[i].data.data_valid = false;
-        g_devices[i].data.device_id = i;
-        snprintf(g_devices[i].data.device_name,
-                 sizeof(g_devices[i].data.device_name), "%s", dcfg->name);
-
-        modbus_init(&g_devices[i].conn, dcfg->host,
-                    (uint16_t)dcfg->port,
-                    (uint8_t)dcfg->slave_addr,
-                    i, dcfg->name);
-
-        printf("[MAIN] Device [%s] -> %s:%d (addr=%d)\n",
-               dcfg->name, dcfg->host, dcfg->port, dcfg->slave_addr);
-    }
+    /* Initialize MPPT data */
+    memset(&g_mppt_data, 0, sizeof(g_mppt_data));
+    g_mppt_data.data_valid = false;
 
     /* Initialize database */
     ret = db_open(&g_db, g_config.db_path);
@@ -241,9 +214,13 @@ int main(int argc, char *argv[])
     }
     printf("[MAIN] Database record count: %d\n", db_get_record_count(&g_db));
 
+    /* Initialize Modbus connection */
+    modbus_init(&g_modbus_conn, g_config.modbus_host,
+                (uint16_t)g_config.modbus_port);
+
     /* Initialize web context */
-    web_server_init(&g_web_ctx, g_devices, g_device_count,
-                    &g_db, &g_config, &g_data_mutex);
+    web_server_init(&g_web_ctx, &g_mppt_data, &g_modbus_conn,
+                    &g_db, &g_config);
 
     /* Initialize Mongoose event manager */
     mg_mgr_init(&mgr);
@@ -256,26 +233,17 @@ int main(int argc, char *argv[])
         return 1;
     }
 
-    /* Start Modbus polling threads (one per enabled device) */
-    int active_devices = 0;
-    for (int i = 0; i < g_device_count; i++) {
-        if (!g_config.devices[i].enabled) continue;
-
-        thread_arg_t *targ = (thread_arg_t *)malloc(sizeof(thread_arg_t));
-        targ->device_idx = i;
-
-        ret = pthread_create(&modbus_tids[i], NULL, modbus_poll_thread, targ);
-        if (ret != 0) {
-            fprintf(stderr, "[MAIN] FATAL: Cannot create Modbus thread for [%s]: %s\n",
-                    g_config.devices[i].name, strerror(ret));
-            free(targ);
-            continue;
-        }
-        active_devices++;
+    /* Start Modbus polling thread */
+    ret = pthread_create(&modbus_tid, NULL, modbus_poll_thread, NULL);
+    if (ret != 0) {
+        fprintf(stderr, "[MAIN] FATAL: Cannot create Modbus thread: %s\n",
+                strerror(ret));
+        mg_mgr_free(&mgr);
+        db_close(&g_db);
+        return 1;
     }
 
-    printf("[MAIN] %d device thread(s) running. Press Ctrl+C to stop.\n\n",
-           active_devices);
+    printf("[MAIN] All systems running. Press Ctrl+C to stop.\n\n");
 
     /* Main event loop */
     while (g_running) {
@@ -284,14 +252,12 @@ int main(int argc, char *argv[])
     }
 
     /* Cleanup */
-    printf("[MAIN] Waiting for threads to exit...\n");
-    for (int i = 0; i < g_device_count; i++) {
-        if (g_config.devices[i].enabled) {
-            pthread_join(modbus_tids[i], NULL);
-        }
-    }
+    printf("[MAIN] Waiting for Modbus thread...\n");
+    pthread_join(modbus_tid, NULL);
 
+    printf("[MAIN] Freeing Mongoose resources...\n");
     mg_mgr_free(&mgr);
+
     db_close(&g_db);
     pthread_mutex_destroy(&g_data_mutex);
 

+ 105 - 211
mppt_monitor/src/modbus_client.c

@@ -2,9 +2,8 @@
  * @file modbus_client.c
  * @brief Modbus RTU over TCP client implementation
  *
- * 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)
+ * Implements CRC-16/Modbus, TCP connection management with timeout,
+ * register read/parse, and auto-reconnect logic.
  */
 
 #include "modbus_client.h"
@@ -25,6 +24,11 @@
 #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;
@@ -45,18 +49,13 @@ uint16_t modbus_crc16(const uint8_t *buf, size_t len)
 }
 
 /* ── Init ──────────────────────────────────────────────────────── */
-void modbus_init(modbus_conn_t *conn, const char *host, uint16_t port,
-                 uint8_t slave_addr, int device_id, const char *device_name)
+void modbus_init(modbus_conn_t *conn, const char *host, uint16_t port)
 {
     memset(conn, 0, sizeof(*conn));
     conn->sockfd = -1;
     conn->state = MODBUS_STATE_DISCONNECTED;
     snprintf(conn->host, sizeof(conn->host), "%s", host);
     conn->port = port;
-    conn->slave_addr = slave_addr;
-    conn->device_id = device_id;
-    snprintf(conn->device_name, sizeof(conn->device_name), "%s",
-             device_name ? device_name : "Unknown");
     conn->last_connect_attempt = 0;
     conn->reconnect_count = 0;
 }
@@ -70,11 +69,12 @@ 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;
-    hints.ai_socktype = SOCK_STREAM;
+    hints.ai_family = AF_UNSPEC;     /* IPv4 or IPv6 */
+    hints.ai_socktype = SOCK_STREAM; /* TCP */
 
     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",
@@ -82,19 +82,23 @@ 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;
 
@@ -105,10 +109,12 @@ 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 {
@@ -134,9 +140,11 @@ 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;
@@ -150,19 +158,21 @@ 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;
+        return 0; /* Already connected */
     }
 
+    /* Rate limit reconnect attempts */
     time_t now = time(NULL);
     if (conn->last_connect_attempt > 0 &&
         (now - conn->last_connect_attempt) < 2) {
-        return -1;
+        return -1; /* Too soon, wait at least 2 seconds between attempts */
     }
     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;
@@ -196,19 +206,8 @@ void modbus_disconnect(modbus_conn_t *conn)
     printf("[MODBUS] Disconnected\n");
 }
 
-/* ── 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)
+/* ── Read Registers ────────────────────────────────────────────── */
+int modbus_read_registers(modbus_conn_t *conn, mppt_data_t *data)
 {
     uint8_t request[8];
     uint8_t response[256];
@@ -220,19 +219,24 @@ static int modbus_read_generic(modbus_conn_t *conn, uint8_t func_code,
         return -1;
     }
 
-    /* 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;
-
+    /*
+     * 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] = MODBUS_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 */
     crc_calc = modbus_crc16(request, 6);
-    request[6] = crc_calc & 0xFF;
-    request[7] = (crc_calc >> 8) & 0xFF;
+    request[6] = crc_calc & 0xFF;        /* CRC low byte */
+    request[7] = (crc_calc >> 8) & 0xFF; /* CRC high byte */
 
-    /* Send */
+    /* Send request */
     n = write(conn->sockfd, request, sizeof(request));
     if (n != sizeof(request)) {
         fprintf(stderr, "[MODBUS] Write failed: %s (wrote %zd/%zu)\n",
@@ -241,11 +245,12 @@ static int modbus_read_generic(modbus_conn_t *conn, uint8_t func_code,
         return -1;
     }
 
-    /* Read response */
+    /* Read response with timeout (set via SO_RCVTIMEO) */
     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));
         }
@@ -258,40 +263,36 @@ static int modbus_read_generic(modbus_conn_t *conn, uint8_t func_code,
         return -1;
     }
 
-    /* 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);
+    /* 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);
         return -1;
     }
 
-    /* Validate slave address */
-    if (response[0] != conn->slave_addr) {
-        fprintf(stderr, "[MODBUS] Wrong slave address: 0x%02X (expected 0x%02X)\n",
-                response[0], conn->slave_addr);
+    /* Check slave address */
+    if (response[0] != MODBUS_SLAVE_ADDR) {
+        fprintf(stderr, "[MODBUS] Wrong slave address: 0x%02X\n", response[0]);
         return -1;
     }
 
-    /* Check exception */
+    /* Check for exception response */
     if (response[1] & 0x80) {
-        fprintf(stderr, "[MODBUS] Exception: func=0x%02X code=0x%02X\n",
+        fprintf(stderr, "[MODBUS] Exception response: func=0x%02X code=0x%02X\n",
                 response[1], response[2]);
         return -1;
     }
 
-    /* Validate function code */
-    if (response[1] != func_code) {
-        fprintf(stderr, "[MODBUS] Unexpected FC: 0x%02X (expected 0x%02X)\n",
-                response[1], func_code);
+    /* Check function code */
+    if (response[1] != MODBUS_FUNC_READ) {
+        fprintf(stderr, "[MODBUS] Unexpected function code: 0x%02X\n", response[1]);
         return -1;
     }
 
-    /* Validate data length */
+    /* Check data length */
     uint8_t byte_count = response[2];
-    if (byte_count != reg_count * 2) {
-        fprintf(stderr, "[MODBUS] Data length mismatch: %u (expected %u)\n",
-                byte_count, reg_count * 2);
+    if (byte_count != MODBUS_REG_COUNT * 2) {
+        fprintf(stderr, "[MODBUS] Unexpected data length: %u (expected %u)\n",
+                byte_count, MODBUS_REG_COUNT * 2);
         return -1;
     }
 
@@ -305,49 +306,26 @@ static int modbus_read_generic(modbus_conn_t *conn, uint8_t func_code,
     }
 
     /* Parse register values (big-endian) */
-    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];
+    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];
     }
 
-    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 */
+    /* 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 */
     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;
@@ -356,102 +334,33 @@ int modbus_read_input_registers(modbus_conn_t *conn, mppt_data_t *data)
     return 0;
 }
 
-/* ── 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) ────────────── */
+/* ── Fault Code Descriptions ───────────────────────────────────── */
 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 "Not charging";
-        case 1: return "Charging";
+        case 0: return "Idle / No charging";
+        case 1: return "Charging (Bulk)";
+        case 2: return "Absorption";
+        case 3: return "Float / Trickle";
+        case 4: return "Equalize";
         default: return "Unknown";
     }
 }
@@ -464,36 +373,21 @@ 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 Full Data  [%s]                    |\n", timebuf);
-    printf("|  Device: %-12s (ID:%d)                         |\n",
-           data->device_name, data->device_id);
-    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");
+    printf("|  MPPT Real-time Data  [%s]  |\n", timebuf);
+    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");
     fflush(stdout);
 }

+ 40 - 92
mppt_monitor/src/modbus_client.h

@@ -3,9 +3,8 @@
  * @brief Modbus RTU over TCP client for MPPT solar charge controller
  *
  * Compatible with: Ubuntu 24.04 / Raspberry Pi 5 8G
- * 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)
+ * Protocol: Modbus RTU framing over raw TCP socket
+ * Target device: discover.zhonjin.com:40635
  */
 
 #ifndef MODBUS_CLIENT_H
@@ -23,71 +22,37 @@ 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 */
-
-    /* ── 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 */
+    /* 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 */
 } mppt_data_t;
 
 /* ── Connection State ──────────────────────────────────────────── */
@@ -103,9 +68,6 @@ typedef struct {
     modbus_state_t  state;
     char            host[256];
     uint16_t        port;
-    uint8_t         slave_addr;         /* Modbus slave address */
-    int             device_id;          /* Device index (0-based) */
-    char            device_name[64];    /* Device display name */
     time_t          last_connect_attempt;
     int             reconnect_count;
 } modbus_conn_t;
@@ -114,12 +76,15 @@ typedef struct {
 
 /**
  * Initialize modbus connection structure.
+ * @param conn  Pointer to connection struct
+ * @param host  Remote hostname
+ * @param port  Remote port
  */
-void modbus_init(modbus_conn_t *conn, const char *host, uint16_t port,
-                 uint8_t slave_addr, int device_id, const char *device_name);
+void modbus_init(modbus_conn_t *conn, const char *host, uint16_t port);
 
 /**
  * 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);
@@ -130,21 +95,19 @@ int modbus_connect(modbus_conn_t *conn);
 void modbus_disconnect(modbus_conn_t *conn);
 
 /**
- * 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.
+ * 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
  * @return 0 on success, -1 on failure
  */
-int modbus_read_output_registers(modbus_conn_t *conn, mppt_data_t *data);
+int modbus_read_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);
 
@@ -158,21 +121,6 @@ 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.
  */

+ 503 - 310
mppt_monitor/src/web_server.c

@@ -2,8 +2,6 @@
  * @file web_server.c
  * @brief Mongoose HTTP server implementation for MPPT REST API
  *
- * Multi-device support: serves data for all configured MPPT devices.
- *
  * Uses mongoose-7.22 API:
  *   - mg_match() instead of deprecated mg_http_match_uri()
  *   - struct mg_str with char *buf (not const char *ptr)
@@ -11,14 +9,11 @@
  *   - cJSON for JSON serialization
  *
  * API Endpoints:
- *   GET /api/devices           - All devices real-time data
- *   GET /api/devices/<name>    - Single device real-time data
- *   GET /api/data              - Alias for /api/devices (backward compat)
- *   GET /api/status            - All devices connection status
- *   GET /api/health            - Health check
- *   GET /api/history           - Historical data with pagination
- *   GET /api/history/stats     - History statistics
- *   GET /                      - HTML dashboard
+ *   GET /api/data       - Full MPPT data as JSON
+ *   GET /api/status     - Connection & device status
+ *   GET /api/health     - Health check (simple)
+ *   GET /api/history    - Historical data with pagination & time range
+ *   GET /               - HTML dashboard page
  */
 
 #include "web_server.h"
@@ -28,224 +23,91 @@
 #include <stdlib.h>
 #include <string.h>
 #include <time.h>
-#include <pthread.h>
 
-/* ── JSON: Build single device data object (ALL fields) ────────── */
-static cJSON *build_device_data_obj(const mppt_data_t *data)
-{
-    cJSON *obj = cJSON_CreateObject();
-    if (!obj) return NULL;
+/* ── JSON Serialization ────────────────────────────────────────── */
 
-    cJSON_AddStringToObject(obj, "name", data->device_name);
-    cJSON_AddNumberToObject(obj, "id", data->device_id);
+static char *build_data_json(const mppt_data_t *data)
+{
+    cJSON *root = cJSON_CreateObject();
+    if (!root) return NULL;
 
     char timebuf[64];
     if (data->data_valid && data->last_update > 0) {
         struct tm *tm_info = localtime(&data->last_update);
         strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%S", tm_info);
-        cJSON_AddStringToObject(obj, "timestamp", timebuf);
+        cJSON_AddStringToObject(root, "timestamp", timebuf);
     } else {
-        cJSON_AddStringToObject(obj, "timestamp", "N/A");
+        cJSON_AddStringToObject(root, "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_AddBoolToObject(root, "data_valid", data->data_valid ? 1 : 0);
 
     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(input, "battery", battery);
+    cJSON_AddItemToObject(root, "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_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_output_registers", raw_out);
-
-    return obj;
-}
-
-/* ── JSON: All devices data ────────────────────────────────────── */
-char *web_build_devices_json(web_ctx_t *ctx)
-{
-    cJSON *root = cJSON_CreateObject();
-    if (!root) return NULL;
-
-    cJSON_AddNumberToObject(root, "device_count", ctx->device_count);
-
-    cJSON *devices = cJSON_CreateArray();
-    pthread_mutex_lock(ctx->mutex);
-    for (int i = 0; i < ctx->device_count; i++) {
-        cJSON *dev_obj = build_device_data_obj(&ctx->devices[i].data);
-        if (dev_obj) {
-            /* Add connection state */
-            const modbus_conn_t *conn = &ctx->devices[i].conn;
-            const char *state_str;
-            switch (conn->state) {
-                case MODBUS_STATE_CONNECTED:  state_str = "connected"; break;
-                case MODBUS_STATE_CONNECTING: state_str = "connecting"; break;
-                case MODBUS_STATE_ERROR:      state_str = "error"; break;
-                default:                      state_str = "disconnected"; break;
-            }
-            cJSON_AddStringToObject(dev_obj, "conn_state", state_str);
-            cJSON_AddStringToObject(dev_obj, "host", conn->host);
-            cJSON_AddNumberToObject(dev_obj, "port", conn->port);
-            cJSON_AddItemToArray(devices, dev_obj);
-        }
+    cJSON_AddItemToObject(root, "pv", pv);
+
+    cJSON_AddNumberToObject(root, "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(root, "device", device);
+
+    cJSON *raw = cJSON_CreateArray();
+    for (int i = 0; i < MODBUS_REG_COUNT; i++) {
+        cJSON_AddItemToArray(raw, cJSON_CreateNumber(data->reg[i]));
     }
-    pthread_mutex_unlock(ctx->mutex);
-
-    cJSON_AddItemToObject(root, "devices", devices);
+    cJSON_AddItemToObject(root, "raw_registers", raw);
 
     char *json_str = cJSON_PrintUnformatted(root);
     cJSON_Delete(root);
     return json_str;
 }
 
-/* ── JSON: Single device data ──────────────────────────────────── */
-char *web_build_device_json(web_ctx_t *ctx, int device_idx)
+static char *build_status_json(const mppt_data_t *data,
+                               const modbus_conn_t *conn)
 {
-    if (device_idx < 0 || device_idx >= ctx->device_count) return NULL;
-
     cJSON *root = cJSON_CreateObject();
     if (!root) return NULL;
 
-    pthread_mutex_lock(ctx->mutex);
-    cJSON *dev_obj = build_device_data_obj(&ctx->devices[device_idx].data);
-    const modbus_conn_t *conn = &ctx->devices[device_idx].conn;
-    pthread_mutex_unlock(ctx->mutex);
-
-    if (!dev_obj) {
-        cJSON_Delete(root);
-        return NULL;
-    }
-
-    /* Add connection info */
+    cJSON *connection = cJSON_CreateObject();
     const char *state_str;
     switch (conn->state) {
-        case MODBUS_STATE_CONNECTED:  state_str = "connected"; break;
-        case MODBUS_STATE_CONNECTING: state_str = "connecting"; break;
-        case MODBUS_STATE_ERROR:      state_str = "error"; break;
-        default:                      state_str = "disconnected"; break;
+        case MODBUS_STATE_CONNECTED:    state_str = "connected"; break;
+        case MODBUS_STATE_CONNECTING:   state_str = "connecting"; break;
+        case MODBUS_STATE_ERROR:        state_str = "error"; break;
+        default:                        state_str = "disconnected"; break;
     }
-    cJSON_AddStringToObject(dev_obj, "conn_state", state_str);
-    cJSON_AddStringToObject(dev_obj, "host", conn->host);
-    cJSON_AddNumberToObject(dev_obj, "port", conn->port);
-    cJSON_AddNumberToObject(dev_obj, "reconnect_count", conn->reconnect_count);
-    cJSON_AddNumberToObject(dev_obj, "consecutive_failures",
-                            ctx->devices[device_idx].data.consecutive_failures);
-
-    cJSON_AddItemToObject(root, "device", dev_obj);
+    cJSON_AddStringToObject(connection, "state", state_str);
+    cJSON_AddStringToObject(connection, "host", conn->host);
+    cJSON_AddNumberToObject(connection, "port", conn->port);
+    cJSON_AddNumberToObject(connection, "reconnect_count", conn->reconnect_count);
+    cJSON_AddItemToObject(root, "connection", connection);
 
-    char *json_str = cJSON_PrintUnformatted(root);
-    cJSON_Delete(root);
-    return json_str;
-}
-
-/* ── JSON: History query result ────────────────────────────────── */
-char *web_build_history_json(history_result_t *result)
-{
-    cJSON *root = cJSON_CreateObject();
-    if (!root) return NULL;
-
-    cJSON_AddNumberToObject(root, "total", result->total);
-    cJSON_AddNumberToObject(root, "page", result->page);
-    cJSON_AddNumberToObject(root, "page_size", result->page_size);
-    cJSON_AddNumberToObject(root, "total_pages", result->total_pages);
-
-    cJSON *records = cJSON_CreateArray();
-    for (int i = 0; i < result->count; i++) {
-        history_record_t *rec = &result->records[i];
-        cJSON *item = cJSON_CreateObject();
-        cJSON_AddNumberToObject(item, "id", rec->id);
+    cJSON_AddBoolToObject(root, "device_online", data->data_valid ? 1 : 0);
+    cJSON_AddNumberToObject(root, "consecutive_failures",
+                            data->consecutive_failures);
 
+    if (data->data_valid && data->last_update > 0) {
         char timebuf[64];
-        time_t ts = (time_t)rec->timestamp;
-        struct tm *tm_info = localtime(&ts);
-        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);
-        cJSON_AddStringToObject(item, "timestamp", timebuf);
-        cJSON_AddNumberToObject(item, "timestamp_unix", rec->timestamp);
-        cJSON_AddStringToObject(item, "device_name", rec->device_name);
-
-        cJSON_AddNumberToObject(item, "battery_voltage", rec->battery_voltage);
-        cJSON_AddNumberToObject(item, "pv_voltage", rec->pv_voltage);
-        cJSON_AddNumberToObject(item, "pv_charge_current", rec->pv_charge_current);
-        cJSON_AddNumberToObject(item, "pv_input_power", rec->pv_input_power);
-        cJSON_AddNumberToObject(item, "cumulative_energy", rec->cumulative_energy);
-        cJSON_AddNumberToObject(item, "machine_temp", rec->machine_temp);
-        cJSON_AddNumberToObject(item, "fault_code", rec->fault_code);
-        cJSON_AddStringToObject(item, "fault_desc",
-                                modbus_fault_string(rec->fault_code));
-        cJSON_AddNumberToObject(item, "battery_soc", rec->battery_soc);
-        cJSON_AddNumberToObject(item, "charge_state", rec->charge_state);
-        cJSON_AddStringToObject(item, "charge_state_desc",
-                                modbus_charge_state_string(rec->charge_state));
-        cJSON_AddNumberToObject(item, "device_type", rec->device_type);
-
-        cJSON_AddItemToArray(records, item);
+        struct tm *tm_info = localtime(&data->last_update);
+        strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%S", tm_info);
+        cJSON_AddStringToObject(root, "last_update", timebuf);
     }
-    cJSON_AddItemToObject(root, "records", records);
-
-    char *json_str = cJSON_PrintUnformatted(root);
-    cJSON_Delete(root);
-    return json_str;
-}
-
-/* ── JSON: History statistics ──────────────────────────────────── */
-char *web_build_stats_json(db_handle_t *db, const char *device_name)
-{
-    (void)db;
-    (void)device_name;
-    /* Placeholder - stats can be extended later */
-    cJSON *root = cJSON_CreateObject();
-    if (!root) return NULL;
-
-    cJSON_AddNumberToObject(root, "total_records", db_get_record_count(db));
 
     char *json_str = cJSON_PrintUnformatted(root);
     cJSON_Delete(root);
@@ -259,6 +121,7 @@ static void get_query_param(struct mg_http_message *hm, const char *key,
     int n = mg_http_get_var(&hm->query, key, buf, (size_t)buf_size);
     if (n > 0) {
         buf[n] = '\0';
+        /* URL decode + to space */
         for (int i = 0; i < n; i++) {
             if (buf[i] == '+') buf[i] = ' ';
         }
@@ -267,40 +130,29 @@ static void get_query_param(struct mg_http_message *hm, const char *key,
     }
 }
 
-/* ── Find device index by name ─────────────────────────────────── */
-static int find_device_by_name(web_ctx_t *ctx, const char *name)
-{
-    for (int i = 0; i < ctx->device_count; i++) {
-        if (strcmp(ctx->devices[i].data.device_name, name) == 0) {
-            return i;
-        }
-    }
-    return -1;
-}
-
 /* ── History API Handler ───────────────────────────────────────── */
 static void handle_history(struct mg_connection *c,
                            struct mg_http_message *hm,
                            web_ctx_t *ctx)
 {
-    char param_buf[128];
+    char param_buf[64];
     history_query_t query;
     memset(&query, 0, sizeof(query));
 
-    /* Device name filter */
-    get_query_param(hm, "device", param_buf, sizeof(param_buf));
-    snprintf(query.device_name, sizeof(query.device_name), "%s", param_buf);
-
-    /* Time range */
+    /* Parse start_time: "YYYY-MM-DD HH:MM:SS" or "YYYY-MM-DDTHH:MM:SS" */
     get_query_param(hm, "start_time", param_buf, sizeof(param_buf));
-    for (char *p = param_buf; *p; p++) { if (*p == 'T') *p = ' '; }
+    /* Convert T separator to space for SQLite strftime */
+    for (char *p = param_buf; *p; p++) {
+        if (*p == 'T') *p = ' ';
+    }
     snprintf(query.start_time, sizeof(query.start_time), "%s", param_buf);
 
     get_query_param(hm, "end_time", param_buf, sizeof(param_buf));
-    for (char *p = param_buf; *p; p++) { if (*p == 'T') *p = ' '; }
+    for (char *p = param_buf; *p; p++) {
+        if (*p == 'T') *p = ' ';
+    }
     snprintf(query.end_time, sizeof(query.end_time), "%s", param_buf);
 
-    /* Pagination */
     get_query_param(hm, "page", param_buf, sizeof(param_buf));
     query.page = param_buf[0] ? atoi(param_buf) : 1;
     if (query.page < 1) query.page = 1;
@@ -325,7 +177,47 @@ static void handle_history(struct mg_connection *c,
         return;
     }
 
-    char *json_str = web_build_history_json(&result);
+    /* Build JSON response */
+    cJSON *root = cJSON_CreateObject();
+    cJSON_AddNumberToObject(root, "total", result.total);
+    cJSON_AddNumberToObject(root, "page", result.page);
+    cJSON_AddNumberToObject(root, "page_size", result.page_size);
+    cJSON_AddNumberToObject(root, "total_pages", result.total_pages);
+
+    cJSON *records = cJSON_CreateArray();
+    for (int i = 0; i < result.count; i++) {
+        history_record_t *rec = &result.records[i];
+        cJSON *item = cJSON_CreateObject();
+        cJSON_AddNumberToObject(item, "id", rec->id);
+
+        char timebuf[64];
+        time_t ts = (time_t)rec->timestamp;
+        struct tm *tm_info = localtime(&ts);
+        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);
+        cJSON_AddStringToObject(item, "timestamp", timebuf);
+        cJSON_AddNumberToObject(item, "timestamp_unix", rec->timestamp);
+
+        cJSON_AddNumberToObject(item, "battery_voltage", rec->battery_voltage);
+        cJSON_AddNumberToObject(item, "pv_voltage", rec->pv_voltage);
+        cJSON_AddNumberToObject(item, "pv_charge_current", rec->pv_charge_current);
+        cJSON_AddNumberToObject(item, "pv_input_power", rec->pv_input_power);
+        cJSON_AddNumberToObject(item, "cumulative_energy", rec->cumulative_energy);
+        cJSON_AddNumberToObject(item, "machine_temp", rec->machine_temp);
+        cJSON_AddNumberToObject(item, "fault_code", rec->fault_code);
+        cJSON_AddStringToObject(item, "fault_desc",
+                                modbus_fault_string(rec->fault_code));
+        cJSON_AddNumberToObject(item, "battery_soc", rec->battery_soc);
+        cJSON_AddNumberToObject(item, "charge_state", rec->charge_state);
+        cJSON_AddStringToObject(item, "charge_state_desc",
+                                modbus_charge_state_string(rec->charge_state));
+        cJSON_AddNumberToObject(item, "device_type", rec->device_type);
+
+        cJSON_AddItemToArray(records, item);
+    }
+    cJSON_AddItemToObject(root, "records", records);
+
+    char *json_str = cJSON_PrintUnformatted(root);
+    cJSON_Delete(root);
     db_free_result(&result);
 
     if (json_str) {
@@ -341,69 +233,399 @@ static void handle_history(struct mg_connection *c,
     }
 }
 
-/* ── Event Handler ─────────────────────────────────────────────── */
-static void web_event_handler(struct mg_connection *c, int ev,
-                               void *ev_data)
+/* ── HTML Dashboard ────────────────────────────────────────────── */
+
+static const char *DASHBOARD_HTML =
+"<!DOCTYPE html>\n"
+"<html lang=\"zh-CN\">\n"
+"<head>\n"
+"<meta charset=\"UTF-8\">\n"
+"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
+"<title>MPPT Solar Monitor</title>\n"
+"<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js\"></script>\n"
+"<script src=\"https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js\"></script>\n"
+"<style>\n"
+"* { margin: 0; padding: 0; box-sizing: border-box; }\n"
+"body { font-family: -apple-system, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif;\n"
+"       background: #0f172a; color: #e2e8f0; min-height: 100vh; }\n"
+".nav { background: #1e293b; border-bottom: 1px solid #334155; padding: 0 24px;\n"
+"       display: flex; align-items: center; height: 56px; }\n"
+".nav h1 { font-size: 1.1em; color: #38bdf8; margin-right: 32px; }\n"
+".nav-tab { padding: 16px 20px; cursor: pointer; color: #94a3b8;\n"
+"           border-bottom: 2px solid transparent; font-size: 0.9em;\n"
+"           transition: all 0.2s; }\n"
+".nav-tab:hover { color: #e2e8f0; }\n"
+".nav-tab.active { color: #38bdf8; border-bottom-color: #38bdf8; }\n"
+".container { max-width: 1200px; margin: 0 auto; padding: 24px; }\n"
+".page { display: none; }\n"
+".page.active { display: block; }\n"
+".grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n"
+"        gap: 16px; margin-bottom: 24px; }\n"
+".card { background: #1e293b; border-radius: 12px; padding: 20px;\n"
+"        border: 1px solid #334155; }\n"
+".card h2 { color: #38bdf8; font-size: 0.95em; margin-bottom: 12px;\n"
+"           border-bottom: 1px solid #334155; padding-bottom: 8px; }\n"
+".metric { display: flex; justify-content: space-between; align-items: center;\n"
+"          padding: 6px 0; }\n"
+".metric .label { color: #94a3b8; font-size: 0.85em; }\n"
+".metric .value { font-size: 1.15em; font-weight: 600; color: #f1f5f9; }\n"
+".metric .unit { color: #64748b; font-size: 0.8em; margin-left: 3px; }\n"
+".status-badge { display: inline-block; padding: 3px 10px; border-radius: 20px;\n"
+"                font-size: 0.78em; font-weight: 600; }\n"
+".status-ok { background: #064e3b; color: #34d399; }\n"
+".status-warn { background: #78350f; color: #fbbf24; }\n"
+".status-err { background: #7f1d1d; color: #f87171; }\n"
+".power-hl { font-size: 1.6em; color: #fbbf24; text-align: center; padding: 8px 0; }\n"
+".update-time { text-align: center; color: #64748b; font-size: 0.8em; margin-top: 16px; }\n"
+/* History page */
+".filter-bar { background: #1e293b; border-radius: 12px; padding: 20px;\n"
+"              border: 1px solid #334155; margin-bottom: 20px;\n"
+"              display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }\n"
+".filter-group { display: flex; flex-direction: column; gap: 4px; }\n"
+".filter-group label { color: #94a3b8; font-size: 0.8em; }\n"
+".filter-group input, .filter-group select {\n"
+"  background: #0f172a; border: 1px solid #334155; color: #e2e8f0;\n"
+"  padding: 8px 12px; border-radius: 8px; font-size: 0.9em; }\n"
+".filter-group input:focus, .filter-group select:focus {\n"
+"  outline: none; border-color: #38bdf8; }\n"
+".btn { padding: 8px 20px; border-radius: 8px; border: none;\n"
+"       cursor: pointer; font-size: 0.9em; font-weight: 500; transition: all 0.2s; }\n"
+".btn-primary { background: #38bdf8; color: #0f172a; }\n"
+".btn-primary:hover { background: #7dd3fc; }\n"
+".btn-secondary { background: #334155; color: #e2e8f0; }\n"
+".btn-secondary:hover { background: #475569; }\n"
+".chart-container { background: #1e293b; border-radius: 12px; padding: 20px;\n"
+"                   border: 1px solid #334155; margin-bottom: 20px; }\n"
+".chart-container h3 { color: #94a3b8; font-size: 0.9em; margin-bottom: 12px; }\n"
+".chart-metrics { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }\n"
+".chart-metric-btn { padding: 4px 12px; border-radius: 16px; border: 1px solid #334155;\n"
+"                    background: transparent; color: #94a3b8; cursor: pointer;\n"
+"                    font-size: 0.8em; transition: all 0.2s; }\n"
+".chart-metric-btn.active { background: #38bdf8; color: #0f172a; border-color: #38bdf8; }\n"
+".chart-metric-btn:hover { border-color: #38bdf8; }\n"
+".table-container { background: #1e293b; border-radius: 12px; padding: 20px;\n"
+"                   border: 1px solid #334155; overflow-x: auto; }\n"
+".data-table { width: 100%; border-collapse: collapse; font-size: 0.85em; }\n"
+".data-table th { text-align: left; padding: 10px 12px; color: #94a3b8;\n"
+"                 border-bottom: 1px solid #334155; font-weight: 500;\n"
+"                 white-space: nowrap; }\n"
+".data-table td { padding: 8px 12px; border-bottom: 1px solid #1e293b;\n"
+"                 color: #e2e8f0; white-space: nowrap; }\n"
+".data-table tr:hover td { background: rgba(56,189,248,0.05); }\n"
+".pagination { display: flex; justify-content: center; align-items: center;\n"
+"              gap: 8px; margin-top: 16px; }\n"
+".pagination button { padding: 6px 14px; border-radius: 6px; border: 1px solid #334155;\n"
+"                     background: transparent; color: #94a3b8; cursor: pointer;\n"
+"                     font-size: 0.85em; }\n"
+".pagination button:hover:not(:disabled) { border-color: #38bdf8; color: #38bdf8; }\n"
+".pagination button:disabled { opacity: 0.3; cursor: default; }\n"
+".pagination .page-info { color: #94a3b8; font-size: 0.85em; }\n"
+".no-data { text-align: center; color: #64748b; padding: 40px; }\n"
+".loading { text-align: center; color: #94a3b8; padding: 20px; }\n"
+"</style>\n"
+"</head>\n"
+"<body>\n"
+"<div class=\"nav\">\n"
+"  <h1>MPPT Monitor</h1>\n"
+"  <div class=\"nav-tab active\" onclick=\"switchPage('realtime')\">Real-time</div>\n"
+"  <div class=\"nav-tab\" onclick=\"switchPage('history')\">History</div>\n"
+"</div>\n"
+"<div class=\"container\">\n"
+"  <!-- Real-time Page -->\n"
+"  <div id=\"page-realtime\" class=\"page active\">\n"
+"    <div class=\"grid\">\n"
+"      <div class=\"card\"><h2>Battery</h2>\n"
+"        <div class=\"metric\"><span class=\"label\">Voltage</span>\n"
+"          <span class=\"value\" id=\"bat-v\">--<span class=\"unit\">V</span></span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">SOC</span>\n"
+"          <span class=\"value\" id=\"bat-soc\">--<span class=\"unit\">%</span></span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">String</span>\n"
+"          <span class=\"value\" id=\"bat-str\">--</span></div>\n"
+"      </div>\n"
+"      <div class=\"card\"><h2>Solar PV</h2>\n"
+"        <div class=\"metric\"><span class=\"label\">Voltage</span>\n"
+"          <span class=\"value\" id=\"pv-v\">--<span class=\"unit\">V</span></span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">Current</span>\n"
+"          <span class=\"value\" id=\"pv-a\">--<span class=\"unit\">A</span></span></div>\n"
+"        <div class=\"power-hl\" id=\"pv-w\">-- W</div>\n"
+"      </div>\n"
+"      <div class=\"card\"><h2>Device</h2>\n"
+"        <div class=\"metric\"><span class=\"label\">Temperature</span>\n"
+"          <span class=\"value\" id=\"dev-temp\">--<span class=\"unit\">C</span></span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">Charge State</span>\n"
+"          <span class=\"value\" id=\"dev-state\">--</span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">Fault</span>\n"
+"          <span class=\"value\" id=\"dev-fault\">--</span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">Type</span>\n"
+"          <span class=\"value\" id=\"dev-type\">--</span></div>\n"
+"      </div>\n"
+"      <div class=\"card\"><h2>Energy</h2>\n"
+"        <div class=\"metric\"><span class=\"label\">Cumulative</span>\n"
+"          <span class=\"value\" id=\"energy\">--<span class=\"unit\">kWh</span></span></div>\n"
+"        <div class=\"metric\"><span class=\"label\">Status</span>\n"
+"          <span class=\"value\" id=\"conn-status\">--</span></div>\n"
+"      </div>\n"
+"    </div>\n"
+"    <div class=\"update-time\" id=\"update-time\">Waiting for data...</div>\n"
+"  </div>\n"
+"  <!-- History Page -->\n"
+"  <div id=\"page-history\" class=\"page\">\n"
+"    <div class=\"filter-bar\">\n"
+"      <div class=\"filter-group\">\n"
+"        <label>Start Time</label>\n"
+"        <input type=\"datetime-local\" id=\"hist-start\">\n"
+"      </div>\n"
+"      <div class=\"filter-group\">\n"
+"        <label>End Time</label>\n"
+"        <input type=\"datetime-local\" id=\"hist-end\">\n"
+"      </div>\n"
+"      <div class=\"filter-group\">\n"
+"        <label>Page Size</label>\n"
+"        <select id=\"hist-page-size\">\n"
+"          <option value=\"20\">20</option>\n"
+"          <option value=\"50\" selected>50</option>\n"
+"          <option value=\"100\">100</option>\n"
+"          <option value=\"200\">200</option>\n"
+"        </select>\n"
+"      </div>\n"
+"      <button class=\"btn btn-primary\" onclick=\"queryHistory(1)\">Query</button>\n"
+"      <button class=\"btn btn-secondary\" onclick=\"resetFilter()\">Reset</button>\n"
+"    </div>\n"
+"    <div class=\"chart-container\">\n"
+"      <h3>Trend Chart</h3>\n"
+"      <div class=\"chart-metrics\">\n"
+"        <button class=\"chart-metric-btn active\" data-metric=\"battery_voltage\" onclick=\"toggleMetric(this)\">Battery V</button>\n"
+"        <button class=\"chart-metric-btn\" data-metric=\"pv_voltage\" onclick=\"toggleMetric(this)\">PV Voltage</button>\n"
+"        <button class=\"chart-metric-btn\" data-metric=\"pv_charge_current\" onclick=\"toggleMetric(this)\">Current</button>\n"
+"        <button class=\"chart-metric-btn\" data-metric=\"pv_input_power\" onclick=\"toggleMetric(this)\">Power</button>\n"
+"        <button class=\"chart-metric-btn\" data-metric=\"machine_temp\" onclick=\"toggleMetric(this)\">Temp</button>\n"
+"        <button class=\"chart-metric-btn\" data-metric=\"battery_soc\" onclick=\"toggleMetric(this)\">SOC %</button>\n"
+"      </div>\n"
+"      <canvas id=\"historyChart\" height=\"260\"></canvas>\n"
+"    </div>\n"
+"    <div class=\"table-container\">\n"
+"      <div id=\"hist-loading\" class=\"loading\" style=\"display:none\">Loading...</div>\n"
+"      <div id=\"hist-nodata\" class=\"no-data\" style=\"display:none\">No data. Click Query to load history.</div>\n"
+"      <table class=\"data-table\" id=\"hist-table\" style=\"display:none\">\n"
+"        <thead><tr>\n"
+"          <th>Time</th><th>Bat V</th><th>PV V</th><th>Current A</th>\n"
+"          <th>Power W</th><th>Energy kWh</th><th>Temp C</th>\n"
+"          <th>SOC %</th><th>State</th><th>Fault</th>\n"
+"        </tr></thead>\n"
+"        <tbody id=\"hist-tbody\"></tbody>\n"
+"      </table>\n"
+"      <div class=\"pagination\" id=\"hist-pagination\"></div>\n"
+"    </div>\n"
+"  </div>\n"
+"</div>\n"
+"<script>\n"
+"/* ── Page Switching ─────────────────────────────────── */\n"
+"function switchPage(name) {\n"
+"  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));\n"
+"  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));\n"
+"  document.getElementById('page-'+name).classList.add('active');\n"
+"  document.querySelectorAll('.nav-tab')[name==='realtime'?0:1].classList.add('active');\n"
+"  if (name === 'history' && !histChart) initChart();\n"
+"}\n"
+"\n"
+"/* ── Real-time Data ─────────────────────────────────── */\n"
+"async function fetchRealtime() {\n"
+"  try {\n"
+"    const res = await fetch('/api/data');\n"
+"    const d = await res.json();\n"
+"    if (!d.data_valid) return;\n"
+"    document.getElementById('bat-v').innerHTML = d.battery.voltage.toFixed(1)+'<span class=\"unit\">V</span>';\n"
+"    document.getElementById('bat-soc').innerHTML = d.battery.soc+'<span class=\"unit\">%</span>';\n"
+"    document.getElementById('bat-str').textContent = d.battery.string_count;\n"
+"    document.getElementById('pv-v').innerHTML = d.pv.voltage.toFixed(1)+'<span class=\"unit\">V</span>';\n"
+"    document.getElementById('pv-a').innerHTML = d.pv.current.toFixed(1)+'<span class=\"unit\">A</span>';\n"
+"    document.getElementById('pv-w').textContent = d.pv.power.toFixed(1)+' W';\n"
+"    document.getElementById('dev-temp').innerHTML = d.device.temperature.toFixed(1)+'<span class=\"unit\">C</span>';\n"
+"    document.getElementById('dev-state').textContent = d.device.charge_state_desc;\n"
+"    var fe = document.getElementById('dev-fault');\n"
+"    fe.textContent = d.device.fault_desc;\n"
+"    fe.className = 'value ' + (d.device.fault_code===0||d.device.fault_code===20?'status-ok':'status-err');\n"
+"    document.getElementById('dev-type').textContent = d.device.device_type;\n"
+"    document.getElementById('energy').innerHTML = d.cumulative_energy_kwh.toFixed(1)+'<span class=\"unit\">kWh</span>';\n"
+"    document.getElementById('update-time').textContent = 'Updated: '+d.timestamp;\n"
+"    var cs = document.getElementById('conn-status');\n"
+"    cs.textContent = d.data_valid ? 'Online' : 'Offline';\n"
+"    cs.className = 'value ' + (d.data_valid ? 'status-ok' : 'status-err');\n"
+"  } catch(e) { console.error(e); }\n"
+"}\n"
+"fetchRealtime();\n"
+"setInterval(fetchRealtime, 2000);\n"
+"\n"
+"/* ── History Chart ──────────────────────────────────── */\n"
+"let histChart = null;\n"
+"let histData = [];\n"
+"const metricColors = {\n"
+"  battery_voltage: '#38bdf8',\n"
+"  pv_voltage: '#fbbf24',\n"
+"  pv_charge_current: '#34d399',\n"
+"  pv_input_power: '#f87171',\n"
+"  machine_temp: '#a78bfa',\n"
+"  battery_soc: '#fb923c'\n"
+"};\n"
+"const metricLabels = {\n"
+"  battery_voltage: 'Battery V',\n"
+"  pv_voltage: 'PV Voltage',\n"
+"  pv_charge_current: 'Current A',\n"
+"  pv_input_power: 'Power W',\n"
+"  machine_temp: 'Temp C',\n"
+"  battery_soc: 'SOC %'\n"
+"};\n"
+"let activeMetrics = new Set(['battery_voltage']);\n"
+"\n"
+"function initChart() {\n"
+"  const ctx = document.getElementById('historyChart').getContext('2d');\n"
+"  histChart = new Chart(ctx, {\n"
+"    type: 'line',\n"
+"    data: { datasets: [] },\n"
+"    options: {\n"
+"      responsive: true,\n"
+"      maintainAspectRatio: false,\n"
+"      interaction: { mode: 'index', intersect: false },\n"
+"      plugins: { legend: { display: true, labels: { color: '#94a3b8' } } },\n"
+"      scales: {\n"
+"        x: { type: 'time', ticks: { color: '#64748b' }, grid: { color: '#1e293b' } },\n"
+"        y: { ticks: { color: '#64748b' }, grid: { color: '#334155' } }\n"
+"      }\n"
+"    }\n"
+"  });\n"
+"}\n"
+"\n"
+"function toggleMetric(btn) {\n"
+"  const m = btn.dataset.metric;\n"
+"  if (activeMetrics.has(m)) { activeMetrics.delete(m); btn.classList.remove('active'); }\n"
+"  else { activeMetrics.add(m); btn.classList.add('active'); }\n"
+"  updateChart();\n"
+"}\n"
+"\n"
+"function updateChart() {\n"
+"  if (!histChart || histData.length === 0) return;\n"
+"  const datasets = [];\n"
+"  activeMetrics.forEach(m => {\n"
+"    datasets.push({\n"
+"      label: metricLabels[m] || m,\n"
+"      data: histData.map(r => ({ x: r.timestamp_unix * 1000, y: r[m] })),\n"
+"      borderColor: metricColors[m] || '#fff',\n"
+"      backgroundColor: (metricColors[m] || '#fff') + '20',\n"
+"      borderWidth: 2, pointRadius: 1, tension: 0.3, fill: false\n"
+"    });\n"
+"  });\n"
+"  histChart.data.datasets = datasets;\n"
+"  histChart.update();\n"
+"}\n"
+"\n"
+"/* ── History Query ──────────────────────────────────── */\n"
+"let currentPage = 1;\n"
+"let currentTotalPages = 1;\n"
+"\n"
+"async function queryHistory(page) {\n"
+"  page = page || 1;\n"
+"  currentPage = page;\n"
+"  const startEl = document.getElementById('hist-start');\n"
+"  const endEl = document.getElementById('hist-end');\n"
+"  const psEl = document.getElementById('hist-page-size');\n"
+"  let url = '/api/history?page=' + page + '&page_size=' + psEl.value + '&sort=DESC';\n"
+"  if (startEl.value) url += '&start_time=' + encodeURIComponent(startEl.value.replace('T',' '));\n"
+"  if (endEl.value) url += '&end_time=' + encodeURIComponent(endEl.value.replace('T',' '));\n"
+"\n"
+"  document.getElementById('hist-loading').style.display = 'block';\n"
+"  document.getElementById('hist-nodata').style.display = 'none';\n"
+"  document.getElementById('hist-table').style.display = 'none';\n"
+"\n"
+"  try {\n"
+"    const res = await fetch(url);\n"
+"    const d = await res.json();\n"
+"    document.getElementById('hist-loading').style.display = 'none';\n"
+"\n"
+"    if (!d.records || d.records.length === 0) {\n"
+"      document.getElementById('hist-nodata').style.display = 'block';\n"
+"      histData = [];\n"
+"      updateChart();\n"
+"      return;\n"
+"    }\n"
+"\n"
+"    histData = d.records;\n"
+"    currentTotalPages = d.total_pages;\n"
+"\n"
+"    /* Populate table */\n"
+"    const tbody = document.getElementById('hist-tbody');\n"
+"    tbody.innerHTML = '';\n"
+"    d.records.forEach(r => {\n"
+"      const tr = document.createElement('tr');\n"
+"      tr.innerHTML = '<td>'+r.timestamp+'</td>'\n"
+"        + '<td>'+r.battery_voltage.toFixed(1)+'</td>'\n"
+"        + '<td>'+r.pv_voltage.toFixed(1)+'</td>'\n"
+"        + '<td>'+r.pv_charge_current.toFixed(1)+'</td>'\n"
+"        + '<td>'+r.pv_input_power.toFixed(1)+'</td>'\n"
+"        + '<td>'+r.cumulative_energy.toFixed(1)+'</td>'\n"
+"        + '<td>'+r.machine_temp.toFixed(0)+'</td>'\n"
+"        + '<td>'+r.battery_soc+'</td>'\n"
+"        + '<td>'+r.charge_state_desc+'</td>'\n"
+"        + '<td><span class=\"status-badge '+(r.fault_code===0||r.fault_code===20?'status-ok':'status-err')+'\">'+r.fault_desc+'</span></td>';\n"
+"      tbody.appendChild(tr);\n"
+"    });\n"
+"    document.getElementById('hist-table').style.display = 'table';\n"
+"\n"
+"    /* Pagination */\n"
+"    const pg = document.getElementById('hist-pagination');\n"
+"    pg.innerHTML = '<button '+(d.page<=1?'disabled':'')+' onclick=\"queryHistory('+(d.page-1)+')\">Prev</button>'\n"
+"      + '<span class=\"page-info\">Page '+d.page+' / '+d.total_pages+' (Total: '+d.total+')</span>'\n"
+"      + '<button '+(d.page>=d.total_pages?'disabled':'')+' onclick=\"queryHistory('+(d.page+1)+')\">Next</button>';\n"
+"\n"
+"    updateChart();\n"
+"  } catch(e) {\n"
+"    console.error(e);\n"
+"    document.getElementById('hist-loading').style.display = 'none';\n"
+"    document.getElementById('hist-nodata').style.display = 'block';\n"
+"    document.getElementById('hist-nodata').textContent = 'Query failed: ' + e.message;\n"
+"  }\n"
+"}\n"
+"\n"
+"function resetFilter() {\n"
+"  document.getElementById('hist-start').value = '';\n"
+"  document.getElementById('hist-end').value = '';\n"
+"  document.getElementById('hist-page-size').value = '50';\n"
+"  queryHistory(1);\n"
+"}\n"
+"\n"
+"/* Set default date range: last 24 hours */\n"
+"(function() {\n"
+"  const now = new Date();\n"
+"  const ago = new Date(now.getTime() - 24*60*60*1000);\n"
+"  const fmt = d => d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0')+'T'+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');\n"
+"  document.getElementById('hist-start').value = fmt(ago);\n"
+"  document.getElementById('hist-end').value = fmt(now);\n"
+"})();\n"
+"</script>\n"
+"</body>\n"
+"</html>\n";
+
+/* ── Init ──────────────────────────────────────────────────────── */
+void web_server_init(web_ctx_t *ctx, mppt_data_t *mppt_data,
+                     modbus_conn_t *conn, db_handle_t *db,
+                     const app_config_t *config)
+{
+    ctx->mppt_data = mppt_data;
+    ctx->modbus_conn = conn;
+    ctx->db = db;
+    ctx->config = config;
+}
+
+/* ── Event Handler (mongoose-7.22 API) ─────────────────────────── */
+void web_event_handler(struct mg_connection *c, int ev, void *ev_data)
 {
     if (ev != MG_EV_HTTP_MSG) return;
 
     struct mg_http_message *hm = (struct mg_http_message *)ev_data;
     web_ctx_t *ctx = (web_ctx_t *)c->fn_data;
 
-    /* GET /api/devices - all devices data */
-    if (mg_match(hm->uri, mg_str("/api/devices"), NULL)) {
-        char *json = web_build_devices_json(ctx);
-        if (json) {
-            mg_http_reply(c, 200,
-                          "Content-Type: application/json\r\n"
-                          "Access-Control-Allow-Origin: *\r\n",
-                          "%s", json);
-            free(json);
-        } else {
-            mg_http_reply(c, 500, "Content-Type: application/json\r\n",
-                          "{\"error\":\"Failed to build JSON\"}");
-        }
-        return;
-    }
-
-    /* GET /api/devices/<name> - single device */
-    if (hm->uri.len > 13 && strncmp(hm->uri.buf, "/api/devices/", 13) == 0) {
-        char dev_name[128] = {0};
-        size_t name_len = hm->uri.len - 13;
-        if (name_len >= sizeof(dev_name)) name_len = sizeof(dev_name) - 1;
-        memcpy(dev_name, hm->uri.buf + 13, name_len);
-        dev_name[name_len] = '\0';
-
-        /* URL decode */
-        for (char *p = dev_name; *p; p++) {
-            if (*p == '+' || *p == '%') {
-                if (*p == '+') *p = ' ';
-            }
-        }
-
-        int idx = find_device_by_name(ctx, dev_name);
-        if (idx >= 0) {
-            char *json = web_build_device_json(ctx, idx);
-            if (json) {
-                mg_http_reply(c, 200,
-                              "Content-Type: application/json\r\n"
-                              "Access-Control-Allow-Origin: *\r\n",
-                              "%s", json);
-                free(json);
-            } else {
-                mg_http_reply(c, 500, "Content-Type: application/json\r\n",
-                              "{\"error\":\"JSON build failed\"}");
-            }
-        } else {
-            mg_http_reply(c, 404, "Content-Type: application/json\r\n",
-                          "{\"error\":\"Device not found: %s\"}", dev_name);
-        }
-        return;
-    }
-
-    /* GET /api/data - backward compat (returns all devices) */
     if (mg_match(hm->uri, mg_str("/api/data"), NULL)) {
-        char *json = web_build_devices_json(ctx);
+        const mppt_data_t *data = (const mppt_data_t *)ctx->mppt_data;
+        char *json = build_data_json(data);
         if (json) {
             mg_http_reply(c, 200,
                           "Content-Type: application/json\r\n"
@@ -411,15 +633,17 @@ static void web_event_handler(struct mg_connection *c, int ev,
                           "%s", json);
             free(json);
         } else {
-            mg_http_reply(c, 500, "Content-Type: application/json\r\n",
+            mg_http_reply(c, 500,
+                          "Content-Type: application/json\r\n",
                           "{\"error\":\"Failed to build JSON\"}");
         }
         return;
     }
 
-    /* GET /api/status - all devices status */
     if (mg_match(hm->uri, mg_str("/api/status"), NULL)) {
-        char *json = web_build_devices_json(ctx);
+        const mppt_data_t *data = (const mppt_data_t *)ctx->mppt_data;
+        const modbus_conn_t *conn = (const modbus_conn_t *)ctx->modbus_conn;
+        char *json = build_status_json(data, conn);
         if (json) {
             mg_http_reply(c, 200,
                           "Content-Type: application/json\r\n"
@@ -427,13 +651,13 @@ static void web_event_handler(struct mg_connection *c, int ev,
                           "%s", json);
             free(json);
         } else {
-            mg_http_reply(c, 500, "Content-Type: application/json\r\n",
+            mg_http_reply(c, 500,
+                          "Content-Type: application/json\r\n",
                           "{\"error\":\"Failed to build JSON\"}");
         }
         return;
     }
 
-    /* GET /api/health */
     if (mg_match(hm->uri, mg_str("/api/health"), NULL)) {
         mg_http_reply(c, 200,
                       "Content-Type: application/json\r\n",
@@ -441,35 +665,15 @@ static void web_event_handler(struct mg_connection *c, int ev,
         return;
     }
 
-    /* GET /api/history/stats - must be before /api/history */
-    if (mg_match(hm->uri, mg_str("/api/history/stats"), NULL)) {
-        char param_buf[128];
-        get_query_param(hm, "device", param_buf, sizeof(param_buf));
-        char *json = web_build_stats_json(ctx->db, param_buf);
-        if (json) {
-            mg_http_reply(c, 200,
-                          "Content-Type: application/json\r\n"
-                          "Access-Control-Allow-Origin: *\r\n",
-                          "%s", json);
-            free(json);
-        } else {
-            mg_http_reply(c, 500, "Content-Type: application/json\r\n",
-                          "{\"error\":\"JSON build failed\"}");
-        }
-        return;
-    }
-
-    /* GET /api/history */
     if (mg_match(hm->uri, mg_str("/api/history"), NULL)) {
         handle_history(c, hm, ctx);
         return;
     }
 
-    /* GET / - HTML dashboard */
     if (mg_match(hm->uri, mg_str("/"), NULL)) {
         mg_http_reply(c, 200,
                       "Content-Type: text/html; charset=utf-8\r\n",
-                      "%s", web_get_dashboard_html());
+                      "%s", DASHBOARD_HTML);
         return;
     }
 
@@ -478,29 +682,18 @@ static void web_event_handler(struct mg_connection *c, int ev,
                   "{\"error\":\"Not Found\"}");
 }
 
-/* ── Init & Start ──────────────────────────────────────────────── */
-void web_server_init(web_ctx_t *ctx, device_runtime_t *devices,
-                     int device_count, db_handle_t *db,
-                     app_config_t *config, pthread_mutex_t *mutex)
-{
-    memset(ctx, 0, sizeof(*ctx));
-    ctx->devices = devices;
-    ctx->device_count = device_count;
-    ctx->db = db;
-    ctx->config = config;
-    ctx->mutex = mutex;
-}
-
+/* ── Start Listener ────────────────────────────────────────────── */
 struct mg_connection *web_server_start(struct mg_mgr *mgr, web_ctx_t *ctx)
 {
     struct mg_connection *c;
     char listen_url[128];
 
+    /*
+     * Read port from DEPLOY_RUN_PORT environment variable (sandbox compat).
+     * Falls back to WEB_SERVER_PORT (8085) if not set.
+     */
     const char *env_port = getenv("DEPLOY_RUN_PORT");
-    int port = 8085;
-    if (ctx->config && ctx->config->web_port > 0) {
-        port = ctx->config->web_port;
-    }
+    int port = WEB_SERVER_PORT;
     if (env_port != NULL && env_port[0] != '\0') {
         int parsed = atoi(env_port);
         if (parsed > 0 && parsed < 65536) {

+ 19 - 53
mppt_monitor/src/web_server.h

@@ -1,87 +1,53 @@
 /**
  * @file web_server.h
- * @brief Mongoose HTTP server for MPPT Monitor
+ * @brief Mongoose HTTP server for MPPT data REST API
  *
- * Provides REST API for real-time data, device list, and history queries.
- * Serves embedded HTML dashboard with real-time updates.
- *
- * Uses mongoose-7.22 API (mg_match, mg_http_reply, etc.)
+ * Uses mongoose-7.22 API (mg_match, struct mg_str with char *buf).
+ * Provides JSON REST endpoints for real-time and historical MPPT data.
  */
 
 #ifndef WEB_SERVER_H
 #define WEB_SERVER_H
 
-#include "mongoose.h"
 #include "modbus_client.h"
 #include "db.h"
 #include "config.h"
+#include "mongoose.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-/* ── Limits ────────────────────────────────────────────────────── */
-#define MAX_WEB_DEVICES  MAX_DEVICES   /* Max devices for web context */
-
-/* ── Per-device runtime state ──────────────────────────────────── */
-typedef struct {
-    mppt_data_t   data;               /* Real-time data (protected by mutex) */
-    modbus_conn_t conn;               /* Modbus connection */
-} device_runtime_t;
+/* ── Configuration ─────────────────────────────────────────────── */
+#define WEB_SERVER_PORT       8085
+#define WEB_SERVER_LISTEN_URL "http://0.0.0.0:8085"
 
-/* ── Web Server Context ────────────────────────────────────────── */
+/* ── Shared Data Context ───────────────────────────────────────── */
 typedef struct {
-    device_runtime_t *devices;        /* Pointer to devices array */
-    int              device_count;
-    db_handle_t     *db;
-    app_config_t    *config;
-    pthread_mutex_t *mutex;           /* Shared data mutex */
+    volatile mppt_data_t   *mppt_data;    /* Pointer to shared MPPT data */
+    volatile modbus_conn_t *modbus_conn;  /* Pointer to Modbus connection */
+    db_handle_t            *db;           /* Pointer to database handle */
+    const app_config_t     *config;       /* Pointer to app config */
 } web_ctx_t;
 
 /* ── Public API ────────────────────────────────────────────────── */
 
 /**
- * Initialize web server context.
+ * Initialize the web server context.
  */
-void web_server_init(web_ctx_t *ctx, device_runtime_t *devices,
-                     int device_count, db_handle_t *db,
-                     app_config_t *config, pthread_mutex_t *mutex);
+void web_server_init(web_ctx_t *ctx, mppt_data_t *mppt_data,
+                     modbus_conn_t *conn, db_handle_t *db,
+                     const app_config_t *config);
 
 /**
- * Start HTTP server on configured port.
- * @return Connection pointer, or NULL on failure
+ * Start the Mongoose HTTP listener.
  */
 struct mg_connection *web_server_start(struct mg_mgr *mgr, web_ctx_t *ctx);
 
 /**
- * Generate JSON string for all devices real-time data.
- * Caller must free the returned string with free().
- */
-char *web_build_devices_json(web_ctx_t *ctx);
-
-/**
- * Generate JSON string for a single device's real-time data.
- * Caller must free the returned string with free().
- */
-char *web_build_device_json(web_ctx_t *ctx, int device_idx);
-
-/**
- * Generate JSON string for history query result.
- * Caller must free the returned string with free().
- */
-char *web_build_history_json(history_result_t *result);
-
-/**
- * Generate JSON string for history statistics.
- * Caller must free the returned string with free().
- */
-char *web_build_stats_json(db_handle_t *db, const char *device_name);
-
-/**
- * Get the embedded HTML dashboard page.
- * Defined in dashboard_html.h as static inline.
+ * Mongoose event handler callback.
  */
-#include "dashboard_html.h"
+void web_event_handler(struct mg_connection *c, int ev, void *ev_data);
 
 #ifdef __cplusplus
 }