1331 lines
41 KiB
C
1331 lines
41 KiB
C
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <sys/select.h>
|
|
#include <dirent.h>
|
|
|
|
#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));
|
|
|
|
} |