#include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/uart.h" #include "driver/gpio.h" #include "esp_heap_caps.h" #include "esp_log.h" #include "esp_err.h" #include "esp_rom_sys.h" #include "nvs_flash.h" #include "esp_spiffs.h" #include "config_store.h" #include "modbus_memory.h" #include "modbus_points.h" #include "modbus_rtu.h" #define DEVICE_CONFIG_PATH "/spiffs/device_config.ini" #define TAG "MODBUS_MAIN" #define MODBUS_RX_BUF_SIZE MODBUS_RTU_MAX_ADU_LEN #define MODBUS_TX_BUF_SIZE MODBUS_RTU_MAX_ADU_LEN /* Manual DE polarity: * First try active-high: * idle receive = 0 * transmit = 1 * * If this still does not work, flip these two values: * MANUAL_DE_TX_LEVEL 0 * MANUAL_DE_RX_LEVEL 1 */ #define MANUAL_DE_TX_LEVEL 1 #define MANUAL_DE_RX_LEVEL 0 static device_config_t g_device_cfg; static modbus_rtu_ctx_t g_rtu_ctx; typedef struct { uint8_t unit_id; char name[DEVICE_NAME_MAX_LEN]; char csv_path[128]; modbus_db_t db; modbus_memory_t mem; } modbus_device_t; static modbus_device_t *g_devices = NULL; static size_t g_device_count = 0U; static int g_manual_de_pin = -1; /* ------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------ */ 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 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; } /* ------------------------------------------------------------ */ /* UART / RTU */ /* ------------------------------------------------------------ */ static uart_parity_t cfg_parity_to_uart(modbus_rtu_parity_t parity) { switch (parity) { case MB_RTU_PARITY_EVEN: return UART_PARITY_EVEN; case MB_RTU_PARITY_ODD: return UART_PARITY_ODD; case MB_RTU_PARITY_NONE: default: return UART_PARITY_DISABLE; } } static uart_stop_bits_t cfg_stop_bits_to_uart(uint8_t stop_bits) { if (stop_bits == 2U) return UART_STOP_BITS_2; return UART_STOP_BITS_1; } static esp_err_t modbus_uart_init(const device_config_t *cfg) { uart_config_t uart_cfg; esp_err_t ret; uart_port_t uart_num; if (cfg == NULL) return ESP_ERR_INVALID_ARG; uart_num = (uart_port_t)cfg->rtu.uart_num; memset(&uart_cfg, 0, sizeof(uart_cfg)); uart_cfg.baud_rate = (int)cfg->rtu.baud_rate; uart_cfg.data_bits = UART_DATA_8_BITS; uart_cfg.parity = cfg_parity_to_uart(cfg->rtu.parity); uart_cfg.stop_bits = cfg_stop_bits_to_uart(cfg->rtu.stop_bits); uart_cfg.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_cfg.source_clk = UART_SCLK_DEFAULT; uart_driver_delete(uart_num); ret = uart_driver_install(uart_num, MODBUS_RX_BUF_SIZE * 2, MODBUS_TX_BUF_SIZE * 2, 0, NULL, 0); if (ret != ESP_OK) { ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(ret)); return ret; } ret = uart_param_config(uart_num, &uart_cfg); if (ret != ESP_OK) { ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(ret)); return ret; } /* Manual DE mode: * do NOT hand DE pin to UART RTS here. * TX/RX only. */ ret = uart_set_pin(uart_num, cfg->rtu.tx_pin, cfg->rtu.rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); if (ret != ESP_OK) { ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(ret)); return ret; } ESP_LOGI(TAG, "UART normal mode enabled (manual DE control)"); ESP_LOGI(TAG, "RTU UART configured: uart=%u baud=%u parity=%d stop_bits=%u tx=%d rx=%d de=%d", (unsigned)cfg->rtu.uart_num, (unsigned)cfg->rtu.baud_rate, (int)cfg->rtu.parity, (unsigned)cfg->rtu.stop_bits, cfg->rtu.tx_pin, cfg->rtu.rx_pin, cfg->rtu.de_pin); return ESP_OK; } static void modbus_rtu_task(void *arg) { uint8_t rx_buf[MODBUS_RX_BUF_SIZE]; uint8_t tx_buf[MODBUS_TX_BUF_SIZE]; (void)arg; ESP_LOGI(TAG, "modbus_rtu_task started"); while (1) { int len; size_t resp_len = 0U; bool ok; len = modbus_rtu_read_frame(&g_rtu_ctx, rx_buf, sizeof(rx_buf), pdMS_TO_TICKS(100)); if (len < 0) { ESP_LOGW(TAG, "RTU frame read error"); vTaskDelay(pdMS_TO_TICKS(10)); continue; } if (len == 0) continue; ok = modbus_rtu_process_request(&g_rtu_ctx, rx_buf, (size_t)len, tx_buf, sizeof(tx_buf), &resp_len); if (!ok) continue; if (resp_len > 0U) { int written; if (g_manual_de_pin >= 0) { gpio_set_level((gpio_num_t)g_manual_de_pin, MANUAL_DE_TX_LEVEL); esp_rom_delay_us(200); } written = uart_write_bytes(g_rtu_ctx.uart_num, (const char *)tx_buf, (uint32_t)resp_len); if (written < 0) { ESP_LOGW(TAG, "uart_write_bytes failed"); if (g_manual_de_pin >= 0) gpio_set_level((gpio_num_t)g_manual_de_pin, MANUAL_DE_RX_LEVEL); continue; } ESP_ERROR_CHECK( uart_wait_tx_done(g_rtu_ctx.uart_num, pdMS_TO_TICKS(100))); if (g_manual_de_pin >= 0) { esp_rom_delay_us(200); gpio_set_level((gpio_num_t)g_manual_de_pin, MANUAL_DE_RX_LEVEL); } } } } /* ------------------------------------------------------------ */ /* 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]; 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 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 RTU 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, "RTU config: uart=%u baud=%u parity=%d stop_bits=%u tx=%d rx=%d de=%d", (unsigned)g_device_cfg.rtu.uart_num, (unsigned)g_device_cfg.rtu.baud_rate, (int)g_device_cfg.rtu.parity, (unsigned)g_device_cfg.rtu.stop_bits, g_device_cfg.rtu.tx_pin, g_device_cfg.rtu.rx_pin, g_device_cfg.rtu.de_pin); ESP_LOGI(TAG, "Configured device_count = %u", (unsigned)g_device_cfg.modbus.device_count); 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); } 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; } if (g_devices != NULL) { free(g_devices); g_devices = NULL; } g_devices = calloc(g_device_cfg.modbus.device_count, sizeof(modbus_device_t)); if (g_devices == NULL) { ESP_LOGE(TAG, "Failed to allocate device table for %u devices", (unsigned)g_device_cfg.modbus.device_count); return; } g_device_count = 0U; modbus_rtu_init(&g_rtu_ctx, (uart_port_t)g_device_cfg.rtu.uart_num, g_device_cfg.rtu.baud_rate); ESP_ERROR_CHECK(modbus_uart_init(&g_device_cfg)); g_manual_de_pin = g_device_cfg.rtu.de_pin; if (g_manual_de_pin >= 0) { gpio_reset_pin((gpio_num_t)g_manual_de_pin); gpio_set_direction((gpio_num_t)g_manual_de_pin, GPIO_MODE_OUTPUT); gpio_set_level((gpio_num_t)g_manual_de_pin, MANUAL_DE_RX_LEVEL); ESP_LOGI(TAG, "Manual DE initialized on GPIO%d, idle level=%d", g_manual_de_pin, MANUAL_DE_RX_LEVEL); } ESP_LOGI(TAG, "UART driver installed: uart=%d tx=%d rx=%d de=%d baud=%d", g_device_cfg.rtu.uart_num, g_device_cfg.rtu.tx_pin, g_device_cfg.rtu.rx_pin, g_device_cfg.rtu.de_pin, (int)g_device_cfg.rtu.baud_rate); 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; } dev = &g_devices[g_device_count]; memset(dev, 0, sizeof(*dev)); dev->unit_id = cfg_dev->unit_id; strncpy(dev->name, cfg_dev->name, sizeof(dev->name) - 1U); dev->name[sizeof(dev->name) - 1U] = '\0'; if (strncmp(cfg_dev->csv, "/spiffs/", 8U) == 0) { strncpy(dev->csv_path, cfg_dev->csv, sizeof(dev->csv_path) - 1U); dev->csv_path[sizeof(dev->csv_path) - 1U] = '\0'; } else { snprintf(dev->csv_path, sizeof(dev->csv_path), "/spiffs/%s", cfg_dev->csv); } 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); continue; } modbus_points_init(&dev->db); if (!modbus_points_load(&dev->db, dev->csv_path)) { ESP_LOGE(TAG, "Failed to load CSV for device %u: %s", (unsigned)dev->unit_id, dev->csv_path); continue; } if (dev->db.count > MODBUS_MAX_POINTS) { ESP_LOGE(TAG, "Device %u exceeded MODBUS_MAX_POINTS (%u > %u)", (unsigned)dev->unit_id, (unsigned)dev->db.count, (unsigned)MODBUS_MAX_POINTS); abort(); } if (!modbus_memory_init(&dev->mem, &dev->db)) { ESP_LOGE(TAG, "Memory init failed for device %u", (unsigned)dev->unit_id); continue; } if (!modbus_rtu_add_device(&g_rtu_ctx, dev->unit_id, &dev->mem, dev->name)) { ESP_LOGE(TAG, "Failed to register RTU device unit_id=%u name=%s", (unsigned)dev->unit_id, dev->name); continue; } ESP_LOGI(TAG, "Device %u (%s) loaded %u Modbus points from %s", (unsigned)dev->unit_id, dev->name, (unsigned)dev->db.count, dev->csv_path); 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 = g_device_count * MODBUS_MAX_POINTS; size_t total_points_remaining = 0; size_t free_heap = heap_caps_get_free_size(MALLOC_CAP_8BIT); size_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); size_t point_struct_size = sizeof(modbus_point_t); size_t heap_equivalent_points = 0; if (point_struct_size > 0U) heap_equivalent_points = free_heap / point_struct_size; 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->db.count < MODBUS_MAX_POINTS) remaining = MODBUS_MAX_POINTS - dev->db.count; if (MODBUS_MAX_POINTS > 0U) pct = (100.0f * (float)dev->db.count) / (float)MODBUS_MAX_POINTS; 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)MODBUS_MAX_POINTS, (unsigned)remaining, pct); total_points_used += dev->db.count; total_points_remaining += remaining; } ESP_LOGI(TAG, "Configured capacity: used=%u / max=%u, remaining=%u", (unsigned)total_points_used, (unsigned)total_points_capacity, (unsigned)total_points_remaining); ESP_LOGI(TAG, "Free heap: %u bytes, largest block: %u bytes, point size: %u bytes", (unsigned)free_heap, (unsigned)largest_block, (unsigned)point_struct_size); ESP_LOGI(TAG, "Heap-equivalent points available (informational only): %u", (unsigned)heap_equivalent_points); } ESP_LOGI(TAG, "---------------------------------"); ESP_LOGI(TAG, "----- Point Cost Guide -----"); ESP_LOGI(TAG, "Internal point usage (counts against MODBUS_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 d; for (d = 0; d < g_device_count; d++) { const modbus_device_t *dev = &g_devices[d]; ESP_LOGI(TAG, "Device %u (%s): %u / %u points used (%.1f%%)", (unsigned)dev->unit_id, dev->name, (unsigned)dev->db.count, (unsigned)MODBUS_MAX_POINTS, (100.0f * (float)dev->db.count) / (float)MODBUS_MAX_POINTS); total_points += dev->db.count; } ESP_LOGI(TAG, "TOTAL POINTS LOADED: %u", (unsigned)total_points); } ESP_LOGI(TAG, "--------------------------------"); if (g_device_count == 0U) { ESP_LOGE(TAG, "No valid Modbus devices loaded, not starting RTU"); return; } { BaseType_t rc; ESP_LOGI(TAG, "Creating modbus_rtu_task"); rc = xTaskCreate(modbus_rtu_task, "modbus_rtu_task", 8192, NULL, 5, NULL); ESP_LOGI(TAG, "modbus_rtu_task create rc=%ld", (long)rc); ESP_LOGI(TAG, "Creating modbus_override_task"); rc = xTaskCreate(modbus_override_task, "modbus_override_task", 8192, NULL, 5, NULL); ESP_LOGI(TAG, "modbus_override_task create rc=%ld", (long)rc); } }