#include #include #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "config_store.h" #include "wifi_manager.h" #include "esp_log.h" #include "esp_err.h" #include "nvs_flash.h" #include "esp_spiffs.h" #include "esp_heap_caps.h" #include "esp_timer.h" #include "sdkconfig.h" #include "lwip/sockets.h" #include "lwip/netdb.h" #include "lwip/inet.h" #include "lwip/ip_addr.h" #include "lwip/ip4_addr.h" #include "esp_netif_ip_addr.h" #include "modbus_memory.h" #include "modbus_points.h" #include "modbus_tcp.h" #define DEVICE_CONFIG_PATH "/spiffs/device_config.ini" #define TAG "MODBUS_MAIN" #define MODBUS_RX_BUF_SIZE MODBUS_TCP_MAX_ADU_LEN #define MODBUS_TX_BUF_SIZE MODBUS_TCP_MAX_ADU_LEN #define MODBUS_CLIENT_IDLE_TIMEOUT_US (120000000LL) /* 120 seconds */ static device_config_t g_device_cfg; typedef struct { int sock; struct sockaddr_in addr; } modbus_client_ctx_t; static volatile int g_active_modbus_clients = 0; /* * Socket diagnostics are intentionally event-driven so the terminal stays quiet. * A summary is printed only when the server starts, a client connects or * disconnects, or accept() fails. */ static int count_active_client_slots(const int client_socks[], uint8_t max_clients) { int active = 0; if (client_socks == NULL) return 0; for (uint8_t i = 0; i < max_clients; i++) { if (client_socks[i] >= 0) active++; } return active; } static void log_socket_summary(const char *event, int listen_sock, const int client_socks[], uint8_t max_clients, int event_sock, int event_errno) { const int active = count_active_client_slots(client_socks, max_clients); if (event_errno != 0) { ESP_LOGW(TAG, "Socket status [%s]: listen_sock=%d event_sock=%d active=%d/%u " "lwip_max=%d free_heap=%u errno=%d (%s)", event, listen_sock, event_sock, active, (unsigned)max_clients, CONFIG_LWIP_MAX_SOCKETS, (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT), event_errno, strerror(event_errno)); } else { ESP_LOGI(TAG, "Socket status [%s]: listen_sock=%d event_sock=%d active=%d/%u " "lwip_max=%d free_heap=%u", event, listen_sock, event_sock, active, (unsigned)max_clients, CONFIG_LWIP_MAX_SOCKETS, (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT)); } } typedef struct { uint8_t unit_id; char name[DEVICE_NAME_MAX_LEN]; char *csv_path; modbus_db_t db; modbus_memory_t mem; } modbus_device_t; static modbus_device_t **g_devices = NULL; static size_t g_device_count = 0; static size_t g_device_capacity = 0; /* ------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------ */ static modbus_device_t *find_device_by_unit_id(uint8_t unit_id) { size_t i; for (i = 0; i < g_device_count; i++) { if (g_devices[i] != NULL && g_devices[i]->unit_id == unit_id) return g_devices[i]; } return NULL; } static bool file_exists(const char *path) { FILE *fp; if (path == NULL) return false; fp = fopen(path, "r"); if (fp == NULL) return false; fclose(fp); return true; } static char *build_spiffs_path(const char *filename) { const char *prefix = "/spiffs/"; char *path; size_t prefix_len; size_t filename_len; if (filename == NULL || filename[0] == '\0') return NULL; if (filename[0] == '/') { filename_len = strlen(filename); path = heap_caps_malloc(filename_len + 1U, MALLOC_CAP_8BIT); if (path == NULL) return NULL; memcpy(path, filename, filename_len + 1U); return path; } prefix_len = strlen(prefix); filename_len = strlen(filename); path = heap_caps_malloc(prefix_len + filename_len + 1U, MALLOC_CAP_8BIT); if (path == NULL) return NULL; memcpy(path, prefix, prefix_len); memcpy(path + prefix_len, filename, filename_len + 1U); return path; } static void spiffs_list_files(void) { DIR *dir; struct dirent *entry; dir = opendir("/spiffs"); if (dir == NULL) { ESP_LOGE(TAG, "Failed to open /spiffs for listing"); return; } ESP_LOGI(TAG, "----- SPIFFS file list -----"); while ((entry = readdir(dir)) != NULL) { ESP_LOGI(TAG, "SPIFFS file: %s", entry->d_name); } ESP_LOGI(TAG, "----------------------------"); closedir(dir); } /* ------------------------------------------------------------ */ /* SPIFFS */ /* ------------------------------------------------------------ */ static esp_err_t spiffs_init(void) { esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = NULL, .max_files = 8, .format_if_mount_failed = true }; esp_err_t ret = esp_vfs_spiffs_register(&conf); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to mount SPIFFS (%s)", esp_err_to_name(ret)); return ret; } { size_t total = 0; size_t used = 0; ret = esp_spiffs_info(NULL, &total, &used); if (ret == ESP_OK) { ESP_LOGI(TAG, "SPIFFS mounted: total=%u used=%u", (unsigned)total, (unsigned)used); } } return ESP_OK; } /* ------------------------------------------------------------ */ /* Modbus TCP server */ /* ------------------------------------------------------------ */ static void modbus_client_task(void *arg) { modbus_client_ctx_t *ctx = (modbus_client_ctx_t *)arg; uint8_t rx_buf[MODBUS_RX_BUF_SIZE]; uint8_t tx_buf[MODBUS_TX_BUF_SIZE]; if (ctx == NULL) { vTaskDelete(NULL); return; } ESP_LOGI(TAG, "Client connected: %s:%d (active=%d)", inet_ntoa(ctx->addr.sin_addr), ntohs(ctx->addr.sin_port), g_active_modbus_clients); while (1) { int len; size_t resp_len = 0; bool ok; modbus_device_t *dev; uint8_t unit_id = 0; len = recv(ctx->sock, rx_buf, sizeof(rx_buf), 0); if (len < 0) { ESP_LOGW(TAG, "recv failed: errno=%d", errno); break; } else if (len == 0) { ESP_LOGI(TAG, "Client disconnected: %s:%d", inet_ntoa(ctx->addr.sin_addr), ntohs(ctx->addr.sin_port)); break; } if (len < 7) { continue; } unit_id = rx_buf[6]; dev = find_device_by_unit_id(unit_id); if (dev == NULL) { ESP_LOGW(TAG, "Unknown Unit ID %u from %s:%d", (unsigned)unit_id, inet_ntoa(ctx->addr.sin_addr), ntohs(ctx->addr.sin_port)); continue; } ok = modbus_tcp_process_request(&dev->mem, rx_buf, (size_t)len, tx_buf, sizeof(tx_buf), &resp_len); if (!ok) { if (len >= 8 && (rx_buf[6] == 1U || rx_buf[6] == 2U)) { ESP_LOGW(TAG, "Unsupported Modbus request from %s:%d (unit_id=%u fc=0x%02X)", inet_ntoa(ctx->addr.sin_addr), ntohs(ctx->addr.sin_port), (unsigned)rx_buf[6], (unsigned)rx_buf[7]); } continue; } if (resp_len > 0U) { int sent = send(ctx->sock, tx_buf, (int)resp_len, 0); if (sent < 0) { ESP_LOGW(TAG, "send failed: errno=%d", errno); break; } } } shutdown(ctx->sock, 0); close(ctx->sock); if (g_active_modbus_clients > 0) g_active_modbus_clients--; free(ctx); vTaskDelete(NULL); } static void close_client_slot(int listen_sock, int client_socks[], struct sockaddr_in client_addrs[], uint8_t max_clients, int index) { if (client_socks[index] >= 0) { const int closed_sock = client_socks[index]; ESP_LOGI(TAG, "Client disconnected: %s:%d", inet_ntoa(client_addrs[index].sin_addr), ntohs(client_addrs[index].sin_port)); shutdown(closed_sock, 0); close(closed_sock); client_socks[index] = -1; if (g_active_modbus_clients > 0) g_active_modbus_clients--; log_socket_summary("client disconnected", listen_sock, client_socks, max_clients, closed_sock, 0); } } static void modbus_server_task(void *arg) { int listen_sock = -1; struct sockaddr_in server_addr; int opt = 1; int client_socks[16]; struct sockaddr_in client_addrs[16]; int64_t client_last_seen[16]; uint8_t rx_buf[MODBUS_RX_BUF_SIZE]; uint8_t tx_buf[MODBUS_TX_BUF_SIZE]; uint8_t max_clients = g_device_cfg.modbus.max_clients; (void)arg; if (max_clients == 0U) max_clients = 1U; if (max_clients > 16U) max_clients = 16U; for (int i = 0; i < 16; i++) { client_socks[i] = -1; client_last_seen[i] = 0; memset(&client_addrs[i], 0, sizeof(client_addrs[i])); } listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (listen_sock < 0) { ESP_LOGE(TAG, "Unable to create socket: errno=%d", errno); vTaskDelete(NULL); return; } setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(g_device_cfg.modbus.port); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ESP_LOGE(TAG, "Socket bind failed: errno=%d", errno); close(listen_sock); vTaskDelete(NULL); return; } if (listen(listen_sock, max_clients) < 0) { ESP_LOGE(TAG, "Socket listen failed: errno=%d", errno); close(listen_sock); vTaskDelete(NULL); return; } ESP_LOGI(TAG, "Modbus TCP server listening on port %u (max_clients=%u)", (unsigned)g_device_cfg.modbus.port, (unsigned)max_clients); log_socket_summary("server started", listen_sock, client_socks, max_clients, -1, 0); while (1) { fd_set read_fds; int max_fd = listen_sock; int ready; FD_ZERO(&read_fds); FD_SET(listen_sock, &read_fds); for (int i = 0; i < max_clients; i++) { if (client_socks[i] >= 0) { FD_SET(client_socks[i], &read_fds); if (client_socks[i] > max_fd) max_fd = client_socks[i]; } } int64_t now = esp_timer_get_time(); struct timeval timeout; timeout.tv_sec = 1; timeout.tv_usec = 0; ready = select(max_fd + 1, &read_fds, NULL, NULL, &timeout); if (ready < 0) { ESP_LOGW(TAG, "select failed: errno=%d", errno); vTaskDelay(pdMS_TO_TICKS(10)); continue; } if (FD_ISSET(listen_sock, &read_fds)) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_len); int slot = -1; if (client_sock < 0) { const int accept_errno = errno; /* One concise diagnostic line per failed accept. */ log_socket_summary("accept failed", listen_sock, client_socks, max_clients, -1, accept_errno); if (accept_errno == 23 || accept_errno == EMFILE || accept_errno == ENFILE || accept_errno == ENOMEM) { vTaskDelay(pdMS_TO_TICKS(500)); } else { vTaskDelay(pdMS_TO_TICKS(50)); } } else { for (int i = 0; i < max_clients; i++) { if (client_socks[i] < 0) { slot = i; break; } } if (slot < 0) { ESP_LOGW(TAG, "Rejecting client %s:%d (active=%d max=%u)", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), g_active_modbus_clients, (unsigned)max_clients); shutdown(client_sock, 0); close(client_sock); } else { client_socks[slot] = client_sock; client_addrs[slot] = client_addr; client_last_seen[slot] = esp_timer_get_time(); g_active_modbus_clients++; ESP_LOGI(TAG, "Client connected: %s:%d (active=%d)", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), g_active_modbus_clients); log_socket_summary("client connected", listen_sock, client_socks, max_clients, client_sock, 0); } } } for (int i = 0; i < max_clients; i++) { if (client_socks[i] < 0) continue; if (!FD_ISSET(client_socks[i], &read_fds)) continue; int len = recv(client_socks[i], rx_buf, sizeof(rx_buf), 0); if (len < 0) { ESP_LOGW(TAG, "recv failed from %s:%d errno=%d", inet_ntoa(client_addrs[i].sin_addr), ntohs(client_addrs[i].sin_port), errno); close_client_slot(listen_sock, client_socks, client_addrs, max_clients, i); client_last_seen[i] = 0; continue; } if (len == 0) { close_client_slot(listen_sock, client_socks, client_addrs, max_clients, i); client_last_seen[i] = 0; continue; } if (len < 7) continue; client_last_seen[i] = esp_timer_get_time(); uint8_t unit_id = rx_buf[6]; modbus_device_t *dev = find_device_by_unit_id(unit_id); if (dev == NULL) { ESP_LOGW(TAG, "Unknown Unit ID %u from %s:%d", (unsigned)unit_id, inet_ntoa(client_addrs[i].sin_addr), ntohs(client_addrs[i].sin_port)); continue; } size_t resp_len = 0; bool ok = modbus_tcp_process_request(&dev->mem, rx_buf, (size_t)len, tx_buf, sizeof(tx_buf), &resp_len); if (!ok) { if (len >= 8) { ESP_LOGW(TAG, "Unsupported Modbus request from %s:%d (unit_id=%u fc=0x%02X)", inet_ntoa(client_addrs[i].sin_addr), ntohs(client_addrs[i].sin_port), (unsigned)unit_id, (unsigned)rx_buf[7]); } continue; } if (resp_len > 0U) { int sent = send(client_socks[i], tx_buf, (int)resp_len, 0); if (sent < 0) { ESP_LOGW(TAG, "send failed to %s:%d errno=%d", inet_ntoa(client_addrs[i].sin_addr), ntohs(client_addrs[i].sin_port), errno); close_client_slot(listen_sock, client_socks, client_addrs, max_clients, i); continue; } } } for (int i = 0; i < max_clients; i++) { if (client_socks[i] >= 0 && client_last_seen[i] > 0 && (now - client_last_seen[i]) > MODBUS_CLIENT_IDLE_TIMEOUT_US) { ESP_LOGW(TAG, "Closing idle Modbus client %s:%d", inet_ntoa(client_addrs[i].sin_addr), ntohs(client_addrs[i].sin_port)); close_client_slot(listen_sock, client_socks, client_addrs, max_clients, i); client_last_seen[i] = 0; } } } } /* ------------------------------------------------------------ */ /* Override Sync Task */ /* ------------------------------------------------------------ */ static void modbus_override_task(void *arg) { (void)arg; while (1) { size_t d; for (d = 0; d < g_device_count; d++) { modbus_device_t *dev = g_devices[d]; if (dev == NULL) continue; size_t i; for (i = 0; i < dev->db.count; i++) { modbus_point_t *pt = &dev->db.points[i]; if (pt->is_helper) continue; if (!pt->has_control_address) continue; if (pt->data_type == MB_DATA_BOOL) { uint8_t value; if (modbus_memory_read_bit(&dev->mem, modbus_points_type_to_mem(pt->control_type), pt->control_offset, &value)) { modbus_memory_write_bit(&dev->mem, modbus_points_type_to_mem(pt->type), pt->offset, value); } } else if (pt->data_type == MB_DATA_UINT16) { uint16_t value; if (modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), pt->control_offset, &value)) { modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), pt->offset, value); } } else if (pt->data_type == MB_DATA_UINT32 || pt->data_type == MB_DATA_FLOAT) { uint16_t w1, w2; if (modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), pt->control_offset, &w1) && modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), (uint16_t)(pt->control_offset + 1U), &w2)) { modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), pt->offset, w1); modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), (uint16_t)(pt->offset + 1U), w2); } } else if (pt->data_type == MB_DATA_DOUBLE) { uint16_t w1, w2, w3, w4; if (modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), pt->control_offset, &w1) && modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), (uint16_t)(pt->control_offset + 1U), &w2) && modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), (uint16_t)(pt->control_offset + 2U), &w3) && modbus_memory_read_reg(&dev->mem, modbus_points_type_to_mem(pt->control_type), (uint16_t)(pt->control_offset + 3U), &w4)) { modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), pt->offset, w1); modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), (uint16_t)(pt->offset + 1U), w2); modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), (uint16_t)(pt->offset + 2U), w3); modbus_memory_write_reg(&dev->mem, modbus_points_type_to_mem(pt->type), (uint16_t)(pt->offset + 3U), w4); } } } } vTaskDelay(pdMS_TO_TICKS(100)); } } /* ------------------------------------------------------------ */ /* Helpers for app_main */ /* ------------------------------------------------------------ */ static const char *data_type_to_str(modbus_data_type_t type) { switch (type) { case MB_DATA_BOOL: return "bool"; case MB_DATA_UINT16: return "uint16"; case MB_DATA_UINT32: return "uint32"; case MB_DATA_FLOAT: return "float"; case MB_DATA_DOUBLE: return "double"; default: return "invalid"; } } static uint8_t data_type_internal_cost(modbus_data_type_t type) { switch (type) { case MB_DATA_BOOL: case MB_DATA_UINT16: return 1U; case MB_DATA_UINT32: case MB_DATA_FLOAT: return 2U; case MB_DATA_DOUBLE: return 4U; default: return 0U; } } static uint8_t pics_type_cost(pics_data_type_t type) { switch (type) { case PICS_DATA_BOOLEAN: return 1U; case PICS_DATA_INTEGER: return 2U; case PICS_DATA_FLOAT: return 4U; case PICS_DATA_NONE: default: return 0U; } } static void print_device_type_mix(const modbus_device_t *dev) { size_t i; size_t bool_count = 0; size_t uint16_count = 0; size_t uint32_count = 0; size_t float_count = 0; size_t double_count = 0; size_t pics_bool_count = 0; size_t pics_int_count = 0; size_t pics_float_count = 0; size_t pics_none_count = 0; size_t internal_points_from_base_rows = 0; size_t pics_registers_from_base_rows = 0; size_t base_rows = 0; if (dev == NULL) return; for (i = 0; i < dev->db.count; i++) { const modbus_point_t *pt = &dev->db.points[i]; if (pt->is_helper) continue; base_rows++; switch (pt->data_type) { case MB_DATA_BOOL: bool_count++; break; case MB_DATA_UINT16: uint16_count++; break; case MB_DATA_UINT32: uint32_count++; break; case MB_DATA_FLOAT: float_count++; break; case MB_DATA_DOUBLE: double_count++; break; default: break; } switch (pt->pics_data_type) { case PICS_DATA_BOOLEAN: pics_bool_count++; break; case PICS_DATA_INTEGER: pics_int_count++; break; case PICS_DATA_FLOAT: pics_float_count++; break; case PICS_DATA_NONE: default: pics_none_count++; break; } internal_points_from_base_rows += data_type_internal_cost(pt->data_type); pics_registers_from_base_rows += pics_type_cost(pt->pics_data_type); } ESP_LOGI(TAG, "Device %u (%s) type mix: base_rows=%u internal_points=%u pics_regs=%u", (unsigned)dev->unit_id, dev->name, (unsigned)base_rows, (unsigned)internal_points_from_base_rows, (unsigned)pics_registers_from_base_rows); ESP_LOGI(TAG, " data_type counts: bool=%u uint16=%u uint32=%u float=%u double=%u", (unsigned)bool_count, (unsigned)uint16_count, (unsigned)uint32_count, (unsigned)float_count, (unsigned)double_count); ESP_LOGI(TAG, " pics_data_type counts: none=%u Boolean=%u Integer=%u Float=%u", (unsigned)pics_none_count, (unsigned)pics_bool_count, (unsigned)pics_int_count, (unsigned)pics_float_count); } /* ------------------------------------------------------------ */ /* app_main */ /* ------------------------------------------------------------ */ void app_main(void) { esp_err_t ret; size_t i; ESP_LOGI(TAG, "Starting Modbus TCP device"); ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); ESP_ERROR_CHECK(spiffs_init()); spiffs_list_files(); if (!config_store_load(DEVICE_CONFIG_PATH, &g_device_cfg)) { ESP_LOGE(TAG, "Failed to load device config: %s", DEVICE_CONFIG_PATH); return; } ESP_LOGI(TAG, "Modbus Port: %u", (unsigned)g_device_cfg.modbus.port); ESP_LOGI(TAG, "Configured device_count = %u", (unsigned)g_device_cfg.modbus.device_count); ESP_LOGI(TAG, "Configured max_points per device = %u", (unsigned)g_device_cfg.modbus.max_points); ESP_LOGI(TAG, "Socket configuration: configured_modbus_clients=%u lwip_max_sockets=%d", (unsigned)g_device_cfg.modbus.max_clients, CONFIG_LWIP_MAX_SOCKETS); for (i = 0; i < g_device_cfg.modbus.device_count; i++) { ESP_LOGI(TAG, "CFG device[%u]: enabled=%u unit_id=%u name=%s csv=%s", (unsigned)i, g_device_cfg.devices[i].enabled ? 1 : 0, (unsigned)g_device_cfg.devices[i].unit_id, g_device_cfg.devices[i].name, (g_device_cfg.devices[i].csv != NULL) ? g_device_cfg.devices[i].csv : ""); } if (g_device_cfg.modbus.device_count == 0U) { ESP_LOGE(TAG, "No Modbus devices configured"); return; } if (g_device_cfg.modbus.device_count > MAX_VIRTUAL_DEVICES) { ESP_LOGE(TAG, "Too many virtual devices (%u)", (unsigned)g_device_cfg.modbus.device_count); return; } g_device_count = 0U; g_device_capacity = g_device_cfg.modbus.device_count; g_devices = heap_caps_calloc(g_device_capacity, sizeof(modbus_device_t *), MALLOC_CAP_8BIT); if (g_devices == NULL) { ESP_LOGE(TAG, "Failed to allocate Modbus device table: devices=%u total=%u", (unsigned)g_device_capacity, (unsigned)(g_device_capacity * sizeof(modbus_device_t *))); return; } ESP_LOGI(TAG, "Allocated Modbus device table: devices=%u pointer_table_size=%u device_struct_size=%u", (unsigned)g_device_capacity, (unsigned)(g_device_capacity * sizeof(modbus_device_t *)), (unsigned)sizeof(modbus_device_t)); for (i = 0; i < g_device_cfg.modbus.device_count; i++) { modbus_device_t *dev; const virtual_device_settings_t *cfg_dev = &g_device_cfg.devices[i]; if (!cfg_dev->enabled) { ESP_LOGI(TAG, "Skipping disabled device[%u]", (unsigned)i); continue; } if (g_device_count >= g_device_capacity) { ESP_LOGE(TAG, "Device table full: count=%u capacity=%u", (unsigned)g_device_count, (unsigned)g_device_capacity); break; } dev = heap_caps_calloc(1, sizeof(modbus_device_t), MALLOC_CAP_8BIT); if (dev == NULL) { ESP_LOGE(TAG, "Failed to allocate Modbus device %u: size=%u free_heap=%u", (unsigned)(i + 1U), (unsigned)sizeof(modbus_device_t), (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT)); continue; } dev->unit_id = cfg_dev->unit_id; strncpy(dev->name, cfg_dev->name, sizeof(dev->name) - 1U); dev->name[sizeof(dev->name) - 1U] = '\0'; dev->csv_path = build_spiffs_path(cfg_dev->csv); if (dev->csv_path == NULL) { ESP_LOGE(TAG, "Failed to allocate CSV path for device %u", (unsigned)dev->unit_id); heap_caps_free(dev); continue; } ESP_LOGI(TAG, "Preparing device[%u]: unit_id=%u name=%s csv=%s", (unsigned)i, (unsigned)dev->unit_id, dev->name, dev->csv_path); if (!file_exists(dev->csv_path)) { ESP_LOGE(TAG, "CSV file not found for device %u: %s", (unsigned)dev->unit_id, dev->csv_path); if(dev->csv_path != NULL) heap_caps_free(dev->csv_path); heap_caps_free(dev); continue; } ESP_LOGI(TAG, "CSV file found for device %u: %s", (unsigned)dev->unit_id, dev->csv_path); modbus_points_init(&dev->db); if (!modbus_points_load(&dev->db, dev->csv_path, g_device_cfg.modbus.max_points)) { ESP_LOGE(TAG, "Failed to load CSV for device %u: %s", (unsigned)dev->unit_id, dev->csv_path); if(dev->csv_path != NULL) heap_caps_free(dev->csv_path); heap_caps_free(dev); continue; } if (!modbus_memory_init(&dev->mem, &dev->db)) { ESP_LOGE(TAG, "Memory init failed for device %u", (unsigned)dev->unit_id); modbus_points_reset(&dev->db); if(dev->csv_path != NULL) heap_caps_free(dev->csv_path); heap_caps_free(dev); continue; } ESP_LOGI(TAG, "Device %u (%s) loaded %u / %u Modbus point entries from %s", (unsigned)dev->unit_id, dev->name, (unsigned)dev->db.count, (unsigned)dev->db.capacity, dev->csv_path); g_devices[g_device_count] = dev; g_device_count++; } ESP_LOGI(TAG, "----- Runtime Modbus Device Table -----"); for (i = 0; i < g_device_count; i++) { ESP_LOGI(TAG, "RUNTIME device[%u]: unit_id=%u name=%s csv=%s points=%u", (unsigned)i, (unsigned)g_devices[i]->unit_id, g_devices[i]->name, g_devices[i]->csv_path, (unsigned)g_devices[i]->db.count); } ESP_LOGI(TAG, "---------------------------------------"); ESP_LOGI(TAG, "----- Modbus Point Capacity -----"); { size_t total_points_used = 0; size_t total_points_capacity = 0; size_t total_points_remaining = 0; size_t free_heap = heap_caps_get_free_size(MALLOC_CAP_8BIT); size_t point_struct_size = sizeof(modbus_point_t); size_t configured_points_per_device = g_device_cfg.modbus.max_points; size_t configured_db_bytes_per_device = configured_points_per_device * point_struct_size; size_t estimated_additional_devices_free_heap = 0; if (configured_db_bytes_per_device > 0U) { estimated_additional_devices_free_heap = free_heap / configured_db_bytes_per_device; } for (i = 0; i < g_device_count; i++) { const modbus_device_t *dev = g_devices[i]; size_t remaining = 0; float pct = 0.0f; if (dev == NULL) continue; if (dev->db.count < dev->db.capacity) remaining = dev->db.capacity - dev->db.count; if (dev->db.capacity > 0U) pct = (100.0f * (float)dev->db.count) / (float)dev->db.capacity; ESP_LOGI(TAG, "Device %u (%s): used=%u / max=%u, remaining=%u (%.1f%% used)", (unsigned)dev->unit_id, dev->name, (unsigned)dev->db.count, (unsigned)dev->db.capacity, (unsigned)remaining, pct); total_points_used += dev->db.count; total_points_capacity += dev->db.capacity; total_points_remaining += remaining; } ESP_LOGI(TAG, "Configured point capacity: used=%u / max=%u, remaining=%u", (unsigned)total_points_used, (unsigned)total_points_capacity, (unsigned)total_points_remaining); ESP_LOGI(TAG, "Configured max_points per device=%u, point size=%u bytes, DB bytes per device=%u", (unsigned)configured_points_per_device, (unsigned)point_struct_size, (unsigned)configured_db_bytes_per_device); ESP_LOGI(TAG, "Free heap=%u bytes", (unsigned)free_heap); ESP_LOGI(TAG, "Estimated additional devices possible with current max_points: %u", (unsigned)estimated_additional_devices_free_heap); } ESP_LOGI(TAG, "---------------------------------"); ESP_LOGI(TAG, "----- Point Cost Guide -----"); ESP_LOGI(TAG, "Internal point usage (counts against configured max_points):"); ESP_LOGI(TAG, " data_type=bool -> 1 point"); ESP_LOGI(TAG, " data_type=uint16 -> 1 point"); ESP_LOGI(TAG, " data_type=uint32 -> 2 points"); ESP_LOGI(TAG, " data_type=float -> 2 points"); ESP_LOGI(TAG, " data_type=double -> 4 points"); ESP_LOGI(TAG, "PICS staging usage (for pics_address spacing only):"); ESP_LOGI(TAG, " pics_data_type=Boolean -> 1 register"); ESP_LOGI(TAG, " pics_data_type=Integer -> 2 registers"); ESP_LOGI(TAG, " pics_data_type=Float -> 4 registers"); ESP_LOGI(TAG, " pics_data_type=None -> 0 registers"); ESP_LOGI(TAG, "Planning examples:"); ESP_LOGI(TAG, " 100 uint16 CSV rows -> about 100 internal points"); ESP_LOGI(TAG, " 100 float CSV rows -> about 200 internal points"); ESP_LOGI(TAG, " 100 double CSV rows -> about 400 internal points"); ESP_LOGI(TAG, "--------------------------------"); ESP_LOGI(TAG, "----- Device Type Mix -----"); for (i = 0; i < g_device_count; i++) { print_device_type_mix(g_devices[i]); } ESP_LOGI(TAG, "---------------------------"); ESP_LOGI(TAG, "----- Modbus Memory Usage -----"); { size_t total_points = 0; size_t total_capacity = 0; for (size_t d = 0; d < g_device_count; d++) { const modbus_device_t *dev = g_devices[d]; float pct = 0.0f; if (dev == NULL) continue; if (dev->db.capacity > 0U) pct = (100.0f * (float)dev->db.count) / (float)dev->db.capacity; ESP_LOGI(TAG, "Device %u (%s): %u / %u points used (%.1f%%)", (unsigned)dev->unit_id, dev->name, (unsigned)dev->db.count, (unsigned)dev->db.capacity, pct); total_points += dev->db.count; total_capacity += dev->db.capacity; } ESP_LOGI(TAG, "TOTAL POINTS LOADED: %u / %u", (unsigned)total_points, (unsigned)total_capacity); } ESP_LOGI(TAG, "--------------------------------"); if (g_device_count == 0U) { ESP_LOGE(TAG, "No valid Modbus devices loaded, not starting server"); return; } ESP_ERROR_CHECK(wifi_manager_init()); ESP_ERROR_CHECK(wifi_manager_start_sta(&g_device_cfg.wifi)); if (!wifi_manager_wait_connected(pdMS_TO_TICKS(15000))) { ESP_LOGW(TAG, "Wi-Fi not connected within timeout, server will still start"); } ESP_LOGI(TAG, "Heap before starting Modbus tasks: free=%u largest=%u", (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT), (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); BaseType_t server_ok = xTaskCreate(modbus_server_task, "modbus_server_task", 6144, NULL, 5, NULL); if (server_ok != pdPASS) { ESP_LOGE(TAG, "Failed to create modbus_server_task: free_heap=%u", (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT)); return; } BaseType_t override_ok = xTaskCreate(modbus_override_task, "modbus_override_task", 3072, NULL, 5, NULL); if (override_ok != pdPASS) { ESP_LOGE(TAG, "Failed to create modbus_override_task: free_heap=%u", (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT)); return; } ESP_LOGI(TAG, "Modbus tasks started: free_heap=%u", (unsigned)heap_caps_get_free_size(MALLOC_CAP_8BIT)); }