From 286028b6a8b87b0828a1f906d30484f1742ad1c1 Mon Sep 17 00:00:00 2001 From: PlxEV Date: Sat, 24 Jan 2026 16:56:51 +0000 Subject: [PATCH] fix evse_link --- components/evse_link/include/evse_link.h | 3 + .../evse_link/include/evse_link_events.h | 26 +- .../evse_link/include/evse_link_framing.h | 24 +- components/evse_link/src/evse_link.c | 83 +- components/evse_link/src/evse_link_framing.c | 401 ++- components/evse_link/src/evse_link_master.c | 367 ++- components/evse_link/src/evse_link_slave.c | 514 +++- components/loadbalancer/CMakeLists.txt | 2 +- .../loadbalancer/include/grid_limiter.h | 42 + .../loadbalancer/include/loadbalancer.h | 40 +- .../loadbalancer/include/pv_optimizer.h | 40 + components/loadbalancer/src/grid_limiter.c | 123 + components/loadbalancer/src/loadbalancer.c | 1028 ++++--- components/loadbalancer/src/pv_optimizer.c | 165 ++ components/logger/CMakeLists.txt | 7 - components/logger/include/logger.h | 47 - components/logger/include/output_buffer.h | 34 - components/logger/src/logger.c | 192 -- components/logger/src/output_buffer.c | 217 -- components/meter_manager/CMakeLists.txt | 17 +- .../meter_dds661.c | 0 .../meter_dds661.h | 0 .../driver/meter_modbus/meter_dts024m.c | 542 ++++ .../driver/meter_modbus/meter_dts024m.h | 35 + .../meter_dts6619.c | 0 .../meter_dts6619.h | 5 +- .../meter_ea777.c | 13 +- .../meter_ea777.h | 0 .../{meter_orno => meter_modbus}/meter_orno.h | 0 .../meter_orno513.c | 0 .../meter_orno513.h | 0 .../meter_orno516.c | 0 .../meter_orno516.h | 0 .../meter_orno526.c | 0 .../meter_orno526.h | 0 .../modbus_params.c | 0 .../modbus_params.h | 0 .../driver/meter_zigbee/meter_zigbee.c | 237 +- .../meter_manager/include/meter_events.h | 69 +- .../meter_manager/include/meter_manager.h | 3 +- components/meter_manager/src/meter_manager.c | 19 + components/ocpp/src/ocpp.c | 8 +- components/protocols/CMakeLists.txt | 1 - components/protocols/src/mqtt.c | 2 +- .../rest_api/src/loadbalancing_settings_api.c | 222 +- components/rest_api/src/network_api.c | 154 +- .../{index-CH8H7Z_T.js => index-0q0tbwk5.js} | 22 +- .../webfolder/assets/index-BIZ-rt0x.css | 1 + .../webfolder/assets/index-SX00HfRO.css | 1 - components/rest_api/webfolder/index.html | 4 +- dependencies.lock | 2 +- main/main.c | 11 +- projeto_parte1.c | 2359 +++++++++-------- readproject.py | 6 +- 54 files changed, 4456 insertions(+), 2632 deletions(-) create mode 100755 components/loadbalancer/include/grid_limiter.h create mode 100755 components/loadbalancer/include/pv_optimizer.h create mode 100755 components/loadbalancer/src/grid_limiter.c create mode 100755 components/loadbalancer/src/pv_optimizer.c delete mode 100755 components/logger/CMakeLists.txt delete mode 100755 components/logger/include/logger.h delete mode 100755 components/logger/include/output_buffer.h delete mode 100755 components/logger/src/logger.c delete mode 100755 components/logger/src/output_buffer.c rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_dds661.c (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_dds661.h (100%) create mode 100755 components/meter_manager/driver/meter_modbus/meter_dts024m.c create mode 100755 components/meter_manager/driver/meter_modbus/meter_dts024m.h rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_dts6619.c (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_dts6619.h (98%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_ea777.c (97%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_ea777.h (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno.h (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno513.c (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno513.h (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno516.c (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno516.h (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno526.c (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/meter_orno526.h (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/modbus_params.c (100%) rename components/meter_manager/driver/{meter_orno => meter_modbus}/modbus_params.h (100%) rename components/rest_api/webfolder/assets/{index-CH8H7Z_T.js => index-0q0tbwk5.js} (56%) create mode 100644 components/rest_api/webfolder/assets/index-BIZ-rt0x.css delete mode 100644 components/rest_api/webfolder/assets/index-SX00HfRO.css diff --git a/components/evse_link/include/evse_link.h b/components/evse_link/include/evse_link.h index 1bce5ff..871c254 100755 --- a/components/evse_link/include/evse_link.h +++ b/components/evse_link/include/evse_link.h @@ -1,3 +1,4 @@ +// === Início de: components/evse_link/include/evse_link.h === #ifndef EVSE_LINK_H_ #define EVSE_LINK_H_ @@ -43,3 +44,5 @@ void evse_link_set_enabled(bool enabled); bool evse_link_is_enabled(void); #endif // EVSE_LINK_H_ + +// === Fim de: components/evse_link/include/evse_link.h === diff --git a/components/evse_link/include/evse_link_events.h b/components/evse_link/include/evse_link_events.h index e846315..3ae6068 100644 --- a/components/evse_link/include/evse_link_events.h +++ b/components/evse_link/include/evse_link_events.h @@ -1,31 +1,29 @@ -// === Início de: components/evse_link/include/evse_link_events.h === #ifndef EVSE_LINK_EVENTS_H_ #define EVSE_LINK_EVENTS_H_ #include "esp_event.h" +#include -// Base de eventos do EVSE-Link ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS); -// Tamanho máximo de tag propagada via EVSE-Link (inclui NUL) #define EVSE_LINK_TAG_MAX_LEN 32 -// IDs de eventos EVSE-Link typedef enum { - LINK_EVENT_FRAME_RECEIVED, // qualquer frame válido - LINK_EVENT_SLAVE_ONLINE, // heartbeat recebido primeira vez - LINK_EVENT_SLAVE_OFFLINE, // sem heartbeat no timeout - LINK_EVENT_MASTER_POLL_SENT, // opcional: poll enviado pelo master + LINK_EVENT_FRAME_RECEIVED, + LINK_EVENT_SLAVE_ONLINE, // payload: evse_link_slave_presence_event_t + LINK_EVENT_SLAVE_OFFLINE, // payload: evse_link_slave_presence_event_t (master-side) ou NULL (slave-side fallback) + LINK_EVENT_MASTER_POLL_SENT, LINK_EVENT_CURRENT_LIMIT_APPLIED, - LINK_EVENT_SLAVE_CONFIG_UPDATED, // config atualizada pelo master - LINK_EVENT_REMOTE_AUTH_GRANTED // autorização remota (master -> slave) + LINK_EVENT_SLAVE_CONFIG_UPDATED, + LINK_EVENT_REMOTE_AUTH_GRANTED } evse_link_event_t; -// Payload para LINK_EVENT_REMOTE_AUTH_GRANTED typedef struct { - char tag[EVSE_LINK_TAG_MAX_LEN]; // idTag enviada pelo master + char tag[EVSE_LINK_TAG_MAX_LEN]; } evse_link_auth_grant_event_t; -#endif // EVSE_LINK_EVENTS_H_ +typedef struct { + uint8_t slave_id; +} evse_link_slave_presence_event_t; -// === Fim de: components/evse_link/include/evse_link_events.h === +#endif // EVSE_LINK_EVENTS_H_ diff --git a/components/evse_link/include/evse_link_framing.h b/components/evse_link/include/evse_link_framing.h index 700b57d..718159c 100644 --- a/components/evse_link/include/evse_link_framing.h +++ b/components/evse_link/include/evse_link_framing.h @@ -6,23 +6,23 @@ #include "driver/uart.h" // UART instance and configuration -#define UART_PORT UART_NUM_2 // Usa a UART2 -#define UART_BAUDRATE 115200 +#define UART_PORT UART_NUM_2 +#define UART_BAUDRATE 9600 #define UART_RX_BUF_SIZE 256 -// GPIO pin assignments for UART (ajuste conforme o hardware) -#define UART_TXD 17 // TX -> DI do MAX3485 -#define UART_RXD 16 // RX -> RO do MAX3485 -#define UART_RTS 2 // RTS -> DE+RE do MAX3485 +// GPIO pin assignments for RS-485 UART +// Ajuste conforme seu hardware +#define MB_UART_TXD 17 +#define MB_UART_RXD 16 +#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 -// Conveniência: nomes usados no .c -#define TX_PIN UART_TXD -#define RX_PIN UART_RXD -#define RTS_PIN UART_RTS +#define TX_PIN MB_UART_TXD +#define RX_PIN MB_UART_RXD +#define RTS_PIN MB_UART_RTS // Frame delimiters -#define MAGIC_START 0x7E -#define MAGIC_END 0x7F +#define MAGIC_START 0x7E +#define MAGIC_END 0x7F // Maximum payload (excluding sequence byte) #define EVSE_LINK_MAX_PAYLOAD 254 diff --git a/components/evse_link/src/evse_link.c b/components/evse_link/src/evse_link.c index 0976a48..f356912 100755 --- a/components/evse_link/src/evse_link.c +++ b/components/evse_link/src/evse_link.c @@ -1,10 +1,20 @@ // components/evse_link/src/evse_link.c +// +// Camada de transporte EVSE-Link: +// - carrega config (mode/self_id/enabled) +// - init do framing +// - task RX (UART -> framing) +// - entrega frames completos ao callback registado +// +// NOTA: a logica de protocolo (CMD_POLL / ACK / etc.) deve ficar em +// evse_link_master.c / evse_link_slave.c. #include "evse_link.h" #include "evse_link_framing.h" #include "driver/uart.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include @@ -14,104 +24,75 @@ static const char *TAG = "evse_link"; -// Storage keys #define _NVS_NAMESPACE "evse_link" #define _KEY_MODE "mode" #define _KEY_SELF_ID "self_id" #define _KEY_ENABLED "enabled" -// UART parameters -#define UART_PORT UART_NUM_2 -#define UART_RX_BUF_SIZE 256 - -// Runtime config static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER; static uint8_t _self_id = 0x01; static bool _enabled = false; -// Registered Rx callback static evse_link_rx_cb_t _rx_cb = NULL; +static bool s_evse_link_inited = false; -// Forward declarations extern void evse_link_master_init(void); extern void evse_link_slave_init(void); static void framing_rx_cb(uint8_t src, uint8_t dest, const uint8_t *payload, uint8_t len) { - ESP_LOGD(TAG, "framing_rx_cb: src=0x%02X dest=0x%02X len=%u", src, dest, len); if (_rx_cb) _rx_cb(src, dest, payload, len); } -// Register protocol-level Rx callback void evse_link_register_rx_cb(evse_link_rx_cb_t cb) { _rx_cb = cb; } -// Load config from storage_service (NVS-backed) static void load_link_config(void) { uint8_t u8 = 0; - // mode esp_err_t err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_MODE, &u8, pdMS_TO_TICKS(500)); if (err == ESP_OK && (u8 == (uint8_t)EVSE_LINK_MODE_MASTER || u8 == (uint8_t)EVSE_LINK_MODE_SLAVE)) - { _mode = (evse_link_mode_t)u8; - } else { - // default + persist _mode = EVSE_LINK_MODE_MASTER; (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode); - ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER (persisted async)", - esp_err_to_name(err)); + ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER", esp_err_to_name(err)); } - // self_id err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_SELF_ID, &u8, pdMS_TO_TICKS(500)); if (err == ESP_OK) - { _self_id = u8; - } else { _self_id = 0x01; (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id); - ESP_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X (persisted async)", - esp_err_to_name(err), _self_id); + ESP_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X", esp_err_to_name(err), _self_id); } - // enabled err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_ENABLED, &u8, pdMS_TO_TICKS(500)); if (err == ESP_OK && u8 <= 1) - { _enabled = (u8 != 0); - } else { _enabled = false; (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, 0); - ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false (persisted async)", - esp_err_to_name(err)); + ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false", esp_err_to_name(err)); } } -// Save config to storage_service (debounced) static void save_link_config(void) { - // Debounced writes: não bloqueia e reduz desgaste (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode); (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id); (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, _enabled ? 1 : 0); - - // opcional: se quiseres persistência imediata em configurações “críticas” - // (void)storage_flush_async(); } -// Getters/setters void evse_link_set_mode(evse_link_mode_t m) { if (m != EVSE_LINK_MODE_MASTER && m != EVSE_LINK_MODE_SLAVE) @@ -148,10 +129,11 @@ void evse_link_set_enabled(bool en) bool evse_link_is_enabled(void) { return _enabled; } -// RX task: reads bytes from UART and feeds framing static void evse_link_rx_task(void *arg) { (void)arg; + ESP_LOGI(TAG, "evse_link_rx_task started"); + uint8_t buf[UART_RX_BUF_SIZE]; while (true) @@ -159,26 +141,28 @@ static void evse_link_rx_task(void *arg) int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000)); if (len > 0) { + ESP_LOGD(TAG, "UART RX: len=%d first=0x%02X last=0x%02X", len, buf[0], buf[len - 1]); + for (int i = 0; i < len; ++i) evse_link_recv_byte(buf[i]); } } } -// Initialize EVSE-Link component void evse_link_init(void) { - // garante storage disponível + if (s_evse_link_inited) + { + ESP_LOGW(TAG, "evse_link_init called twice; ignoring"); + return; + } + s_evse_link_inited = true; + esp_err_t se = storage_service_init(); - if (se != ESP_OK) - { - ESP_LOGE(TAG, "storage_service_init failed: %s (using defaults in RAM)", esp_err_to_name(se)); - // segue com defaults em RAM - } - else - { + if (se == ESP_OK) load_link_config(); - } + else + ESP_LOGE(TAG, "storage_service_init failed: %s (defaults in RAM)", esp_err_to_name(se)); ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d", _mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S', @@ -187,21 +171,21 @@ void evse_link_init(void) if (!_enabled) return; - // 1) framing layer init (sets up mutex, UART driver, etc.) evse_link_framing_init(); evse_link_framing_register_cb(framing_rx_cb); - // 2) start RX task - xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 3, NULL); + if (xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL) != pdPASS) + { + ESP_LOGE(TAG, "Failed to create evse_link_rx task"); + return; + } - // 3) delegate to master or slave if (_mode == EVSE_LINK_MODE_MASTER) evse_link_master_init(); else evse_link_slave_init(); } -// Send a frame (delegates to framing module) bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len) { if (!evse_link_is_enabled()) @@ -211,7 +195,6 @@ bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len) return evse_link_framing_send(dest, src, payload, len); } -// Receive byte (delegates to framing module) void evse_link_recv_byte(uint8_t byte) { evse_link_framing_recv_byte(byte); diff --git a/components/evse_link/src/evse_link_framing.c b/components/evse_link/src/evse_link_framing.c index 1ce5661..d32eda0 100644 --- a/components/evse_link/src/evse_link_framing.c +++ b/components/evse_link/src/evse_link_framing.c @@ -1,8 +1,29 @@ +// components/evse_link/src/evse_link_framing.c +// +// EVSE-Link framing (RS-485 HALF DUPLEX via UART driver) +// - Usa UART_MODE_RS485_HALF_DUPLEX (driver controla RTS => DE//RE) +// - Configura RX timeout + RX full threshold para evitar “len=120” +// - Remove controlo manual de GPIO do RTS +// +// Requisitos: +// - MAX3485 com DE e /RE juntos ligados ao RTS_PIN +// - RTS_PIN definido em evse_link_framing.h (ex.: GPIO2; recomendado mudar no futuro) +// +// Notas: +// - Se o teu hardware inverter a lógica do RTS (raro), define EVSE_LINK_RTS_INVERT=1 + #include "evse_link_framing.h" + #include "driver/uart.h" #include "freertos/semphr.h" + #include "esp_log.h" +#include "esp_err.h" +#include "esp_timer.h" + #include +#include +#include static const char *TAG = "evse_framing"; @@ -10,118 +31,252 @@ static SemaphoreHandle_t tx_mutex = NULL; static uint8_t seq = 0; static evse_link_frame_cb_t rx_cb = NULL; -// CRC-8 (polynomial 0x07) -static uint8_t crc8(const uint8_t *data, uint8_t len) +static bool s_framing_inited = false; + +// ---- Tunables (fallbacks) ---- +#ifndef EVSE_LINK_INTERBYTE_TIMEOUT_US +// Timeout inter-byte: se um frame morrer a meio, reseta o parser +#define EVSE_LINK_INTERBYTE_TIMEOUT_US 5000 +#endif + +// Rate-limit para warnings (em microsegundos) +#define LOG_RATELIMIT_US 1000000 // 1s + +// RX tuning (evita acumular ~120 bytes antes de "acordar") +#ifndef EVSE_LINK_RX_TIMEOUT +// Timeout do UART TOUT feature (em "character times"). 3..10 funciona bem. +#define EVSE_LINK_RX_TIMEOUT 3 +#endif + +#ifndef EVSE_LINK_RX_FULL_THRESH +// Gera interrupção quando FIFO tem pelo menos N bytes (1..120). 4 é um bom default. +#define EVSE_LINK_RX_FULL_THRESH 1 +#endif + +#ifndef EVSE_LINK_RTS_INVERT +// Se precisares inverter RTS (muito raro), define para 1 no build. +#define EVSE_LINK_RTS_INVERT 0 +#endif + +static inline bool log_ratelimit_ok(int64_t *last_us, int64_t interval_us) { - uint8_t crc = 0; - for (uint8_t i = 0; i < len; ++i) { - crc ^= data[i]; - for (uint8_t b = 0; b < 8; ++b) { - if (crc & 0x80) { - crc = (uint8_t)((crc << 1) ^ 0x07); - } else { - crc <<= 1; - } - } + const int64_t now = esp_timer_get_time(); + if (*last_us == 0 || (now - *last_us) > interval_us) + { + *last_us = now; + return true; + } + return false; +} + +// CRC-8 (poly 0x07), MSB-first, init=0x00 +static uint8_t crc8_update(uint8_t crc, uint8_t data) +{ + crc ^= data; + for (uint8_t b = 0; b < 8; ++b) + { + if (crc & 0x80) + crc = (uint8_t)((crc << 1) ^ 0x07); + else + crc <<= 1; } return crc; } -void evse_link_framing_init(void) +static esp_err_t configure_uart(void) { - // Mutex para proteger TX (framings de várias tasks) - tx_mutex = xSemaphoreCreateMutex(); - - // Instala driver UART - uart_driver_install(UART_PORT, - UART_RX_BUF_SIZE * 2, // RX buffer - 0, // TX buffer (0 = usa buffer interno) - 0, - NULL, - 0); - uart_config_t cfg = { .baud_rate = UART_BAUDRATE, .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, + .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_DEFAULT, }; - uart_param_config(UART_PORT, &cfg); - // Define pinos: TX, RX e RTS (RTS ligado a DE+RE do transceiver RS485) - uart_set_pin(UART_PORT, - TX_PIN, // MB_UART_TXD (ex: GPIO17) - RX_PIN, // MB_UART_RXD (ex: GPIO16) - RTS_PIN, // MB_UART_RTS (ex: GPIO2, DE+RE) - UART_PIN_NO_CHANGE); + esp_err_t err = uart_param_config(UART_PORT, &cfg); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + return err; + } - // Modo RS485 half-duplex: driver controla RTS/DE/RE automaticamente - uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX); + // TX/RX/RTS na UART (RTS controla DE//RE em RS485 half-duplex) + err = uart_set_pin(UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_PIN_NO_CHANGE); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); + return err; + } - ESP_LOGI(TAG, "Framing init: UART%d TX=%d RX=%d RTS(DE/RE)=%d baud=%d", + // RS-485 HALF DUPLEX (driver controla RTS automaticamente) + err = uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "uart_set_mode(RS485_HALF_DUPLEX) failed: %s", esp_err_to_name(err)); + return err; + } + + // Ajustes para RX responsivo (evita "len=120") + (void)uart_set_rx_full_threshold(UART_PORT, EVSE_LINK_RX_FULL_THRESH); + (void)uart_set_rx_timeout(UART_PORT, EVSE_LINK_RX_TIMEOUT); + + // Opcional: inverter RTS se hardware exigir + if (EVSE_LINK_RTS_INVERT) + { + (void)uart_set_line_inverse(UART_PORT, UART_SIGNAL_RTS_INV); + ESP_LOGW(TAG, "RS485 driver: RTS inverted"); + } + + ESP_LOGW(TAG, "RS485 driver enabled: UART%d TX=%d RX=%d RTS=%d baud=%d rx_to=%d rx_thresh=%d", + UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE, + EVSE_LINK_RX_TIMEOUT, EVSE_LINK_RX_FULL_THRESH); + + return ESP_OK; +} + +void evse_link_framing_init(void) +{ + if (s_framing_inited) + { + ESP_LOGI(TAG, "Framing already initialized"); + return; + } + + if (!tx_mutex) + { + tx_mutex = xSemaphoreCreateMutex(); + if (!tx_mutex) + { + ESP_LOGE(TAG, "Failed to create TX mutex"); + return; + } + } + + // Se o driver já estiver instalado, só reconfigura e aplica RX tuning. + if (uart_is_driver_installed(UART_PORT)) + { + esp_err_t err = configure_uart(); + if (err == ESP_OK) + { + s_framing_inited = true; + ESP_LOGW(TAG, "UART%d driver already installed -> configured for RS485 HALF DUPLEX", UART_PORT); + (void)uart_flush_input(UART_PORT); + } + else + { + ESP_LOGE(TAG, "Failed to configure already-installed UART%d", UART_PORT); + } + return; + } + + // Instala driver UART + esp_err_t err = uart_driver_install(UART_PORT, + UART_RX_BUF_SIZE * 2, // RX buffer (ringbuffer) + 0, // TX buffer (não usado) + 0, // event queue size + NULL, // event queue + 0); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + return; + } + + err = configure_uart(); + if (err != ESP_OK) + return; + + (void)uart_flush_input(UART_PORT); + + s_framing_inited = true; + + ESP_LOGI(TAG, "Framing init (RS485 driver): UART%d TX=%d RX=%d RTS=%d baud=%d", UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE); } bool evse_link_framing_send(uint8_t dest, uint8_t src, const uint8_t *payload, uint8_t len) { - if (len > EVSE_LINK_MAX_PAYLOAD) { - ESP_LOGW(TAG, "Payload too large: %u (max=%u)", - len, EVSE_LINK_MAX_PAYLOAD); + if (!s_framing_inited) + { + ESP_LOGE(TAG, "Framing not initialized"); return false; } - if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) { + if (len > EVSE_LINK_MAX_PAYLOAD) + { + ESP_LOGW(TAG, "Payload too large: %u (max=%u)", len, EVSE_LINK_MAX_PAYLOAD); + return false; + } + + if (len > 0 && payload == NULL) + { + ESP_LOGW(TAG, "Invalid send: len=%u but payload=NULL", len); + return false; + } + + if (!tx_mutex) + { + ESP_LOGE(TAG, "TX mutex is NULL (framing_init not called?)"); + return false; + } + + if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) + { ESP_LOGW(TAG, "Failed to take TX mutex"); return false; } // Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END + // LEN on wire = SEQ + PAYLOAD (>=1) uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7]; int idx = 0; frame[idx++] = MAGIC_START; frame[idx++] = dest; frame[idx++] = src; - frame[idx++] = (uint8_t)(len + 1); // LEN = SEQ + payload + frame[idx++] = (uint8_t)(len + 1); frame[idx++] = seq; - if (len > 0 && payload != NULL) { + if (len > 0) + { memcpy(&frame[idx], payload, len); idx += len; } - // CRC cobre: DEST + SRC + LEN + SEQ + PAYLOAD - uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; - memcpy(crc_input, &frame[1], 3 + 1 + len); - uint8_t crc = crc8(crc_input, (uint8_t)(3 + 1 + len)); - frame[idx++] = crc; + // CRC: DEST + SRC + LEN + SEQ + PAYLOAD + uint8_t crc = 0; + crc = crc8_update(crc, dest); + crc = crc8_update(crc, src); + crc = crc8_update(crc, (uint8_t)(len + 1)); + crc = crc8_update(crc, seq); + for (uint8_t i = 0; i < len; ++i) + crc = crc8_update(crc, payload[i]); + frame[idx++] = crc; frame[idx++] = MAGIC_END; - // Envia frame completo int written = uart_write_bytes(UART_PORT, (const char *)frame, idx); - if (written != idx) { + if (written != idx) + { ESP_LOGW(TAG, "uart_write_bytes wrote %d of %d", written, idx); } - // Aguarda TX terminar (o driver controla DE/RE via RTS) - uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(20)); + // Aguarda TX terminar; driver RS485 devolve RTS para RX automaticamente. + (void)uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(700)); xSemaphoreGive(tx_mutex); - ESP_LOGI(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u", + ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u", dest, src, len, seq); - seq++; // incrementa sequência após envio + seq++; return true; } void evse_link_framing_recv_byte(uint8_t b) { - // Máquina de estados para parsing do frame static enum { ST_WAIT_START = 0, ST_WAIT_DEST, @@ -133,17 +288,46 @@ void evse_link_framing_recv_byte(uint8_t b) ST_WAIT_END } rx_state = ST_WAIT_START; - static uint8_t rx_dest; - static uint8_t rx_src; - static uint8_t rx_len; // inclui SEQ + payload - static uint8_t rx_seq; - static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD]; - static uint8_t rx_pos; - static uint8_t rx_crc; + static uint8_t rx_dest; + static uint8_t rx_src; + static uint8_t rx_len; // inclui SEQ + payload (>=1) + static uint8_t rx_seq; + static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD]; + static uint8_t rx_pos; + static uint8_t rx_crc; - switch (rx_state) { + static int64_t s_last_byte_us = 0; + static int64_t s_last_bad_len_log_us = 0; + static int64_t s_last_bad_crc_log_us = 0; + +#define RESET_PARSER() \ + do \ + { \ + rx_state = ST_WAIT_START; \ + rx_dest = 0; \ + rx_src = 0; \ + rx_len = 0; \ + rx_seq = 0; \ + rx_pos = 0; \ + rx_crc = 0; \ + } while (0) + + const int64_t now_us = esp_timer_get_time(); + + // Timeout inter-byte: frame morreu a meio -> reseta + if (rx_state != ST_WAIT_START && s_last_byte_us != 0 && + (now_us - s_last_byte_us) > EVSE_LINK_INTERBYTE_TIMEOUT_US) + { + RESET_PARSER(); + } + s_last_byte_us = now_us; + + switch (rx_state) + { case ST_WAIT_START: - if (b == MAGIC_START) { + if (b == MAGIC_START) + { + rx_pos = 0; rx_state = ST_WAIT_DEST; } break; @@ -159,28 +343,50 @@ void evse_link_framing_recv_byte(uint8_t b) break; case ST_WAIT_LEN: - rx_len = b; // LEN = SEQ + payload + rx_len = b; + + // rx_len = SEQ + payload => >=1 e <= MAX+1 + if (rx_len < 1 || rx_len > (uint8_t)(EVSE_LINK_MAX_PAYLOAD + 1)) + { + if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US)) + { + ESP_LOGW(TAG, "Invalid LEN=%u (max=%u), dropping frame", + rx_len, (unsigned)(EVSE_LINK_MAX_PAYLOAD + 1)); + } + RESET_PARSER(); + break; + } + rx_pos = 0; rx_state = ST_WAIT_SEQ; break; case ST_WAIT_SEQ: rx_seq = b; - if (rx_len > 1) { - rx_state = ST_READING; - } else { - rx_state = ST_WAIT_CRC; - } + rx_state = (rx_len > 1) ? ST_READING : ST_WAIT_CRC; break; case ST_READING: - if (rx_pos < EVSE_LINK_MAX_PAYLOAD) { + { + const uint8_t payload_len = (uint8_t)(rx_len - 1); + + if (payload_len > EVSE_LINK_MAX_PAYLOAD) + { + if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US)) + { + ESP_LOGW(TAG, "Payload len too big: %u", (unsigned)payload_len); + } + RESET_PARSER(); + break; + } + + if (rx_pos < EVSE_LINK_MAX_PAYLOAD) rx_buf[rx_pos++] = b; - } - if (rx_pos >= (uint8_t)(rx_len - 1)) { // payload completo + + if (rx_pos >= payload_len) rx_state = ST_WAIT_CRC; - } break; + } case ST_WAIT_CRC: rx_crc = b; @@ -188,41 +394,44 @@ void evse_link_framing_recv_byte(uint8_t b) break; case ST_WAIT_END: - if (b == MAGIC_END) { - // Monta buffer para verificar CRC: - // DEST + SRC + LEN + SEQ + PAYLOAD - uint8_t temp[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; - int temp_len = 0; - temp[temp_len++] = rx_dest; - temp[temp_len++] = rx_src; - temp[temp_len++] = rx_len; - temp[temp_len++] = rx_seq; - if (rx_len > 1) { - memcpy(&temp[temp_len], rx_buf, rx_len - 1); - temp_len += rx_len - 1; - } + if (b == MAGIC_END) + { + uint8_t expected = 0; + expected = crc8_update(expected, rx_dest); + expected = crc8_update(expected, rx_src); + expected = crc8_update(expected, rx_len); + expected = crc8_update(expected, rx_seq); - uint8_t expected = crc8(temp, (uint8_t)temp_len); - if (expected == rx_crc) { - uint8_t payload_len = (uint8_t)(rx_len - 1); // exclui SEQ - if (rx_cb) { + const uint8_t payload_len = (uint8_t)(rx_len - 1); + for (uint8_t i = 0; i < payload_len; ++i) + expected = crc8_update(expected, rx_buf[i]); + + if (expected == rx_crc) + { + if (rx_cb) rx_cb(rx_src, rx_dest, rx_buf, payload_len); - } + ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u", rx_src, rx_dest, payload_len, rx_seq); - } else { - ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X", - expected, rx_crc); + } + else + { + if (log_ratelimit_ok(&s_last_bad_crc_log_us, LOG_RATELIMIT_US)) + { + ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X", + expected, rx_crc); + } } } - // Em qualquer caso, volta a esperar novo frame - rx_state = ST_WAIT_START; + RESET_PARSER(); break; default: - rx_state = ST_WAIT_START; + RESET_PARSER(); break; } + +#undef RESET_PARSER } void evse_link_framing_register_cb(evse_link_frame_cb_t cb) diff --git a/components/evse_link/src/evse_link_master.c b/components/evse_link/src/evse_link_master.c index 6f50392..d202190 100644 --- a/components/evse_link/src/evse_link_master.c +++ b/components/evse_link/src/evse_link_master.c @@ -1,10 +1,27 @@ +// components/evse_link/src/evse_link_master.c +// +// Correções aplicadas: +// 1) Evitar TX dentro do callback RX (ACK deferido para task -> não bloqueia RX) +// 2) Dedupe de ACK por slave (evita rajadas se RX vier em chunks / queue acumular) +// 3) Log quando ACK queue enche (antes era silencioso) +// 4) Proteção concorrente no s_presence (int64_t não é atómico no ESP32 32-bit) +// 5) Comentário do poll corrigido + opção de jitter no ACK +// +// NOTA: Mantive o teu comportamento (POLL a cada 10s). Se quiseres, muda para 30s. + #include "evse_link.h" #include "evse_link_events.h" + #include "freertos/FreeRTOS.h" -#include "freertos/task.h" #include "freertos/timers.h" +#include "freertos/queue.h" +#include "freertos/task.h" + #include "esp_log.h" #include "esp_event.h" +#include "esp_timer.h" +#include "esp_random.h" + #include #include #include @@ -14,37 +31,180 @@ static const char *TAG = "evse_link_master"; -// Link commands -#define CMD_POLL 0x01 -#define CMD_HEARTBEAT 0x02 -#define CMD_HEARTBEAT_ACK 0x09 +// Link commands (opcode no payload[0]) +#define CMD_POLL 0x01 +#define CMD_HEARTBEAT 0x02 #define CMD_CONFIG_BROADCAST 0x03 -#define CMD_SET_CURRENT 0x08 -#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização a slave +#define CMD_SET_CURRENT 0x08 +#define CMD_HEARTBEAT_ACK 0x09 +#define CMD_AUTH_GRANTED 0x0A -// payload lengths (exclui byte de opcode) -#define LEN_POLL_REQ 1 // [ CMD_POLL ] -#define LEN_POLL_RESP 9 // [ CMD_POLL, float V(4), float I(4) ] -#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ] -#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ] -#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ] -#define LEN_HEARTBEAT_ACK 1 +// payload lengths (INCLUI opcode) +#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ] +#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, ... ] +#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, amps_lo, amps_hi ] + +// Presence monitoring +#define PRESENCE_CHECK_MS 5000 +#define SLAVE_OFFLINE_TIMEOUT_MS 180000 + +// ACK defer +#define ACK_QUEUE_LEN 16 +// backoff para espaçar ACK (evita burst) +// antes era 0..15ms; aqui fica ligeiramente mais suave. +#define ACK_BACKOFF_MIN_MS 5 +#define ACK_BACKOFF_MAX_MS 35 // jitter 5..35ms + +typedef struct +{ + int64_t last_seen_us; + bool online; +} slave_presence_t; + +static slave_presence_t s_presence[256]; +static TimerHandle_t s_presence_timer = NULL; + +// Proteção concorrente (int64_t não é atómico no ESP32) +static portMUX_TYPE s_presence_mux = portMUX_INITIALIZER_UNLOCKED; -// polling / heartbeat timers interval typedef struct { TimerHandle_t timer; TickType_t interval; } timer_def_t; -static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)}; -static timer_def_t hb_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)}; + +// POLL a cada 60s (comentário corrigido) +static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(60000)}; + +static bool s_handlers_registered = false; + +// ACK task/queue +static QueueHandle_t s_ack_q = NULL; +static TaskHandle_t s_ack_task = NULL; +// Dedupe: 1 ACK pendente por slave +static bool s_ack_pending[256] = {0}; + +static void post_presence_event(evse_link_event_t evt, uint8_t slave_id) +{ + evse_link_slave_presence_event_t p = {.slave_id = slave_id}; + (void)esp_event_post(EVSE_LINK_EVENTS, evt, &p, sizeof(p), portMAX_DELAY); +} + +static void mark_slave_seen(uint8_t slave_id) +{ + const int64_t now = esp_timer_get_time(); + + bool was_offline = false; + + portENTER_CRITICAL(&s_presence_mux); + slave_presence_t *p = &s_presence[slave_id]; + p->last_seen_us = now; + + if (!p->online) + { + p->online = true; + was_offline = true; + } + portEXIT_CRITICAL(&s_presence_mux); + + if (was_offline) + { + ESP_LOGI(TAG, "Slave 0x%02X ONLINE", slave_id); + post_presence_event(LINK_EVENT_SLAVE_ONLINE, slave_id); + } +} + +static void presence_timer_cb(TimerHandle_t xTimer) +{ + (void)xTimer; + const int64_t now = esp_timer_get_time(); + const int64_t timeout_us = (int64_t)SLAVE_OFFLINE_TIMEOUT_MS * 1000; + + const uint8_t self = evse_link_get_self_id(); + + for (int i = 0; i < 256; ++i) + { + if ((uint8_t)i == self) + continue; + + bool online; + int64_t last_seen; + + portENTER_CRITICAL(&s_presence_mux); + online = s_presence[i].online; + last_seen = s_presence[i].last_seen_us; + portEXIT_CRITICAL(&s_presence_mux); + + if (!online) + continue; + + if (last_seen > 0 && (now - last_seen) > timeout_us) + { + portENTER_CRITICAL(&s_presence_mux); + s_presence[i].online = false; + portEXIT_CRITICAL(&s_presence_mux); + + ESP_LOGW(TAG, "Slave 0x%02X OFFLINE (no heartbeat for %d ms)", i, SLAVE_OFFLINE_TIMEOUT_MS); + post_presence_event(LINK_EVENT_SLAVE_OFFLINE, (uint8_t)i); + } + } +} + +// Enfileira ACK sem duplicar por slave +static void enqueue_ack(uint8_t slave_id) +{ + if (!s_ack_q) + return; + + // Dedupe: se já existe ACK pendente para este slave, não enfileira outro + if (s_ack_pending[slave_id]) + return; + + s_ack_pending[slave_id] = true; + + if (xQueueSendToBack(s_ack_q, &slave_id, 0) != pdTRUE) + { + s_ack_pending[slave_id] = false; + ESP_LOGW(TAG, "ACK queue full, dropping ACK for 0x%02X", slave_id); + } +} + +// --- ACK task (não bloqueia RX) --- +static void ack_task(void *arg) +{ + (void)arg; + + for (;;) + { + uint8_t slave_id = 0; + if (xQueueReceive(s_ack_q, &slave_id, portMAX_DELAY) != pdTRUE) + continue; + + // libera dedupe + s_ack_pending[slave_id] = false; + + // backoff com jitter + uint32_t backoff = ACK_BACKOFF_MIN_MS + + (esp_random() % (ACK_BACKOFF_MAX_MS - ACK_BACKOFF_MIN_MS + 1)); + vTaskDelay(pdMS_TO_TICKS(backoff)); + + uint8_t ack[] = {CMD_HEARTBEAT_ACK}; + bool ok = evse_link_send(slave_id, ack, sizeof(ack)); + ESP_LOGI(TAG, "CMD_HEARTBEAT_ACK to 0x%02X ok=%d", slave_id, ok); + } +} // --- Send new limit to slave --- static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *data) { - if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT) + (void)arg; + (void)base; + + if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT || data == NULL) return; - const loadbalancer_slave_limit_event_t *evt = data; + + const loadbalancer_slave_limit_event_t *evt = (const loadbalancer_slave_limit_event_t *)data; + uint8_t slave_id = evt->slave_id; uint16_t max_current = evt->max_current; @@ -52,156 +212,179 @@ static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *dat CMD_SET_CURRENT, (uint8_t)(max_current & 0xFF), (uint8_t)(max_current >> 8)}; - evse_link_send(slave_id, buf, sizeof(buf)); - ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, max_current); + + (void)evse_link_send(slave_id, buf, sizeof(buf)); + ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, (unsigned)max_current); } -// --- Bridge AUTH -> EVSE-Link: enviar AUTH_GRANTED para slaves --- +// --- Bridge AUTH -> EVSE-Link --- static void on_auth_result(void *arg, esp_event_base_t base, int32_t id, void *data) { - if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL) { + (void)arg; + + if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL) return; - } const auth_tag_event_data_t *ev = (const auth_tag_event_data_t *)data; - if (!ev->authorized) { + if (!ev->authorized) + { ESP_LOGI(TAG, "Tag %s not authorized, not propagating to slaves", ev->tag); return; } - // Construir payload: [ CMD_AUTH_GRANTED, tag..., '\0' ] uint8_t buf[1 + EVSE_LINK_TAG_MAX_LEN]; buf[0] = CMD_AUTH_GRANTED; - // Copiar tag e garantir NUL + // Copia tag e garante NUL strncpy((char *)&buf[1], ev->tag, EVSE_LINK_TAG_MAX_LEN - 1); ((char *)&buf[1])[EVSE_LINK_TAG_MAX_LEN - 1] = '\0'; - uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1); // opcode + tag + '\0' + // Payload inclui opcode + string + NUL + uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1); - // Neste exemplo: broadcast para todos os slaves (0xFF) - uint8_t dest = 0xFF; - - if (!evse_link_send(dest, buf, payload_len)) { - ESP_LOGW(TAG, "Failed to send CMD_AUTH_GRANTED to dest=0x%02X for tag=%s", - dest, (char *)&buf[1]); - } else { - ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED to dest=0x%02X for tag=%s", - dest, (char *)&buf[1]); - } + (void)evse_link_send(0xFF, buf, payload_len); + ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED (broadcast) tag=%s", (char *)&buf[1]); } // --- Polling broadcast callback --- static void poll_timer_cb(TimerHandle_t xTimer) { - ESP_LOGD(TAG, "Broadcasting CMD_POLL to all slaves"); - ; - // Optionally post event LINK_EVENT_MASTER_POLL_SENT -} + (void)xTimer; -// --- Heartbeat timeout callback --- -static void hb_timer_cb(TimerHandle_t xTimer) -{ - ESP_LOGW(TAG, "Heartbeat timeout: possible slave offline"); - // post event LINK_EVENT_SLAVE_OFFLINE ??? + uint8_t poll[] = {CMD_POLL}; + bool ok = evse_link_send(0xFF, poll, sizeof(poll)); + ESP_LOGI(TAG, "POLL send ok=%d", ok); } static void on_frame_master(uint8_t src, uint8_t dest, const uint8_t *payload, uint8_t len) { - if (len < 1) + const uint8_t self = evse_link_get_self_id(); + + // ignora eco do próprio master e frames que não são para nós nem broadcast + if (src == self) return; + if (dest != self && dest != 0xFF) + return; + + if (payload == NULL || len < 1) + return; + uint8_t cmd = payload[0]; switch (cmd) { case CMD_HEARTBEAT: { + ESP_LOGD(TAG, "HEARTBEAT from 0x%02X: %u bytes", src, len); + if (len != LEN_HEARTBEAT) - { // CMD + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi + { ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len); return; } - bool charging = payload[1] != 0; - uint16_t hw_max = payload[2] | (payload[3] << 8); - uint16_t runtime = payload[4] | (payload[5] << 8); - ESP_LOGI(TAG, "Heartbeat from 0x%02X: charging=%d hw_max=%uA runtime=%uA", - src, charging, hw_max, runtime); + bool charging = payload[1] != 0; + uint16_t hw_max = (uint16_t)(payload[2] | ((uint16_t)payload[3] << 8)); + uint16_t runtime = (uint16_t)(payload[4] | ((uint16_t)payload[5] << 8)); + + mark_slave_seen(src); loadbalancer_slave_status_event_t status = { .slave_id = src, .charging = charging, .hw_max_current = (float)hw_max, - .runtime_current = (float)runtime, // corrente real medida no slave + .runtime_current = (float)runtime, .timestamp_us = esp_timer_get_time()}; - esp_event_post(LOADBALANCER_EVENTS, - LOADBALANCER_EVENT_SLAVE_STATUS, - &status, sizeof(status), portMAX_DELAY); + (void)esp_event_post(LOADBALANCER_EVENTS, + LOADBALANCER_EVENT_SLAVE_STATUS, + &status, sizeof(status), portMAX_DELAY); - // Enviar ACK de volta - uint8_t ack[] = {CMD_HEARTBEAT_ACK}; - evse_link_send(src, ack, sizeof(ack)); - ESP_LOGD(TAG, "Sent HEARTBEAT_ACK to 0x%02X", src); + // ACK deferido e deduplicado + enqueue_ack(src); break; } - case CMD_POLL: - ESP_LOGD(TAG, "Received POLL_RESP from 0x%02X", src); - break; - case CMD_CONFIG_BROADCAST: - ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", - src, payload[1]); + if (len >= LEN_CONFIG_BROADCAST) + ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", src, payload[1]); + else + ESP_LOGW(TAG, "CONFIG_BROADCAST ack short len=%u from 0x%02X", len, src); break; default: - ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src); + ESP_LOGD(TAG, "Cmd 0x%02X from 0x%02X (ignored/unknown)", cmd, src); + break; } } -// --- Master initialization --- void evse_link_master_init(void) { if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled()) - { return; - } + ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id()); - // register frame callback evse_link_register_rx_cb(on_frame_master); - // register loadbalancer event - ESP_ERROR_CHECK( - esp_event_handler_register( + // Cria queue/task de ACK uma vez + if (s_ack_q == NULL) + { + s_ack_q = xQueueCreate(ACK_QUEUE_LEN, sizeof(uint8_t)); + if (!s_ack_q) + { + ESP_LOGE(TAG, "Failed to create ACK queue"); + } + else + { + if (xTaskCreate(ack_task, "evse_ack", 4096, NULL, 4, &s_ack_task) != pdPASS) + { + ESP_LOGE(TAG, "Failed to create ACK task"); + vQueueDelete(s_ack_q); + s_ack_q = NULL; + s_ack_task = NULL; + } + } + } + + if (!s_handlers_registered) + { + s_handlers_registered = true; + + ESP_ERROR_CHECK(esp_event_handler_register( LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, on_new_limit, NULL)); - // escutar resultado do AUTH para propagar autorização aos slaves - ESP_ERROR_CHECK( - esp_event_handler_register( + ESP_ERROR_CHECK(esp_event_handler_register( AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, on_auth_result, NULL)); + } - // create and start poll timer - poll_timer.timer = xTimerCreate("poll_tmr", - poll_timer.interval, - pdTRUE, NULL, - poll_timer_cb); - xTimerStart(poll_timer.timer, 0); + if (poll_timer.timer == NULL) + { + poll_timer.timer = xTimerCreate("poll_tmr", + poll_timer.interval, + pdTRUE, NULL, + poll_timer_cb); + if (poll_timer.timer) + (void)xTimerStart(poll_timer.timer, 0); + } - // create and start heartbeat monitor timer - hb_timer.timer = xTimerCreate("hb_tmr", - hb_timer.interval, - pdFALSE, NULL, - hb_timer_cb); - xTimerStart(hb_timer.timer, 0); + if (s_presence_timer == NULL) + { + s_presence_timer = xTimerCreate("presence_tmr", + pdMS_TO_TICKS(PRESENCE_CHECK_MS), + pdTRUE, NULL, + presence_timer_cb); + if (s_presence_timer) + (void)xTimerStart(s_presence_timer, 0); + else + ESP_LOGE(TAG, "Failed to create presence timer"); + } } - diff --git a/components/evse_link/src/evse_link_slave.c b/components/evse_link/src/evse_link_slave.c index f0f3755..a8b523d 100644 --- a/components/evse_link/src/evse_link_slave.c +++ b/components/evse_link/src/evse_link_slave.c @@ -1,199 +1,477 @@ -// === components/evse_link/src/evse_link_slave.c === +// components/evse_link/src/evse_link_slave.c +// +// Correções aplicadas: +// 1) Evitar TX dentro do callback RX: confirmação (heartbeat) é deferida via queue/task. +// 2) Proteger safe_mode (e flags relacionadas) com mux (evita race entre RX e timer). +// 3) Opção de política no fallback: por default faz PAUSE (mais seguro). Pode ser alterado por macro. +// 4) Reduzir ruído de logs em caminho quente (RX frames em DEBUG). +// 5) Manter semântica: só sai de safe_mode com comando explícito de potência (SET_CURRENT / RESUME). +// 6) Mantém lógica de pause SET_CURRENT=0 e resume quando >0. +// +// Nota: A enum do evse_state_event_data_t que enviaste está correta para o handler. #include "evse_link.h" #include "evse_link_events.h" -#include "loadbalancer_events.h" + #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/timers.h" +#include "freertos/queue.h" +#include "freertos/portmacro.h" + #include "esp_log.h" #include "esp_event.h" +#include "esp_random.h" +#include "esp_err.h" + #include "evse_events.h" #include "evse_state.h" #include "evse_config.h" + #include #include #include static const char *TAG = "evse_link_slave"; -// Link commands -#define CMD_POLL 0x01 -#define CMD_HEARTBEAT 0x02 // not used by slave -#define CMD_CONFIG_BROADCAST 0x03 -#define CMD_SET_CURRENT 0x08 -#define CMD_HEARTBEAT_ACK 0x09 -#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização +#define MASTER_ID 0x01 -// payload lengths (exclui seq byte) -#define LEN_POLL_REQ 1 // [ CMD_POLL ] -#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ] -#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ] -#define LEN_HEARTBEAT_ACK 1 // [ CMD_HEARTBEAT_ACK ] -#define LEN_HEARTBEAT 6 // CMD_HEARTBEAT + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi +// Commands (opcode no payload[0]) +#define CMD_POLL 0x01 +#define CMD_HEARTBEAT 0x02 +#define CMD_CONFIG_BROADCAST 0x03 +#define CMD_SET_CURRENT 0x08 +#define CMD_HEARTBEAT_ACK 0x09 +#define CMD_AUTH_GRANTED 0x0A +// #define CMD_RESUME 0x0B // se existir -// Timing -#define FALLBACK_TIMEOUT_MS 120000 +// lengths (INCLUI opcode) +#define LEN_SET_CURRENT 3 + +#define FALLBACK_TIMEOUT_MS 180000 + +// --- Política de fallback --- +// 1 = mais seguro: PAUSE (revoga autorização) +// 0 = força corrente mínima (pode continuar a carregar dependendo do core) +#ifndef EVSE_LINK_FALLBACK_PAUSE +#define EVSE_LINK_FALLBACK_PAUSE 1 +#endif + +// --- Confirmações via heartbeat (deferidas) --- +#define HB_REQ_QUEUE_LEN 8 + +typedef enum +{ + HB_REQ_SEND = 1, +} hb_req_t; static TimerHandle_t fallback_timer = NULL; -static bool safe_mode = false; +static TaskHandle_t hb_task_handle = NULL; -// --- Helper to send a heartbeat frame --- -static void send_heartbeat_frame(void) { - bool charging = evse_state_is_charging(evse_get_state()); - uint16_t hw_max = evse_get_max_charging_current(); - uint16_t runtime = evse_get_runtime_charging_current(); +// Task para enviar heartbeat sem bloquear RX callback +static TaskHandle_t hb_sender_task_handle = NULL; +static QueueHandle_t hb_req_q = NULL; - ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA", - charging, hw_max, runtime); +// "safe mode" (master offline): aplica fallback local e nao sai com POLL/ACK. +// Só sai com comando explícito de potência (SET_CURRENT / RESUME). +static bool safe_mode = false; +static uint16_t saved_runtime_limit = 0; // informativo + +// "remote pause" (SET_CURRENT=0) +static bool paused_by_master = false; +static bool paused_prev_authorized = false; + +static portMUX_TYPE s_state_mux = portMUX_INITIALIZER_UNLOCKED; + +static bool evse_handler_registered = false; + +static size_t bounded_strlen_u8(const uint8_t *s, size_t max_len) +{ + size_t i = 0; + if (!s) + return 0; + while (i < max_len && s[i] != 0) + i++; + return i; +} + +static void send_heartbeat_frame_now(void) +{ + bool charging = evse_state_is_charging(evse_get_state()); + uint16_t hw_max = evse_get_max_charging_current(); + uint16_t runtime = evse_get_runtime_charging_current(); uint8_t hb[] = { CMD_HEARTBEAT, charging ? 1 : 0, (uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8), - (uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8) - }; - // Broadcast to master (0xFF) - evse_link_send(0xFF, hb, sizeof(hb)); + (uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)}; + + (void)evse_link_send(MASTER_ID, hb, sizeof(hb)); // UNICAST + ESP_LOGI(TAG, "Send Heartbeat Frame"); } - -// --- EVSE state change handler --- -static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { - if (base!=EVSE_EVENTS || id!=EVSE_EVENT_STATE_CHANGED || data==NULL) return; - const evse_state_event_data_t *evt = data; - if (evt->state==EVSE_STATE_EVENT_IDLE || evt->state==EVSE_STATE_EVENT_CHARGING) { - send_heartbeat_frame(); +// pede heartbeat sem bloquear quem chama (RX callback, event handler, etc.) +static void request_heartbeat_send(void) +{ + if (hb_req_q) + { + hb_req_t req = HB_REQ_SEND; + // não bloqueia; se encher, apenas ignora (heartbeat periódico já existe) + (void)xQueueSendToBack(hb_req_q, &req, 0); } + else + { + // fallback: se queue ainda não existe, manda direto + send_heartbeat_frame_now(); + } +} + +static void hb_sender_task(void *arg) +{ + (void)arg; + + hb_req_t req; + for (;;) + { + if (xQueueReceive(hb_req_q, &req, portMAX_DELAY) != pdTRUE) + continue; + + if (req == HB_REQ_SEND) + { + send_heartbeat_frame_now(); + } + } +} + +static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL) + return; + + const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data; + + // Enforce pause: se algo tentar voltar a carregar enquanto paused_by_master, revoga auth. + bool paused; + portENTER_CRITICAL(&s_state_mux); + paused = paused_by_master; + portEXIT_CRITICAL(&s_state_mux); + + if (paused && evt->state == EVSE_STATE_EVENT_CHARGING) + { + // Garante que não continua a carregar por clamp/auto-auth + evse_state_set_authorized(false); + } + + // Envia heartbeat quando entra em IDLE ou CHARGING (estado relevante) + if (evt->state == EVSE_STATE_EVENT_IDLE || evt->state == EVSE_STATE_EVENT_CHARGING) + request_heartbeat_send(); +} + +// Sai de safe-mode APENAS com comando explícito (SET_CURRENT / RESUME). +static void maybe_exit_safe_mode_on_explicit_power_cmd(uint8_t cmd) +{ + bool in_safe; + portENTER_CRITICAL(&s_state_mux); + in_safe = safe_mode; + portEXIT_CRITICAL(&s_state_mux); + + if (!in_safe) + return; + + if (cmd == CMD_SET_CURRENT /*|| cmd == CMD_RESUME*/) + { + portENTER_CRITICAL(&s_state_mux); + safe_mode = false; + portEXIT_CRITICAL(&s_state_mux); + + ESP_LOGI(TAG, "Exiting safe mode due to explicit cmd 0x%02X", cmd); + } +} + +static void apply_pause_by_master(void) +{ + bool prev_auth = evse_state_get_authorized(); + + portENTER_CRITICAL(&s_state_mux); + paused_by_master = true; + paused_prev_authorized = prev_auth; + portEXIT_CRITICAL(&s_state_mux); + + // Revoga autorização para parar contactor/pilot via core + if (prev_auth) + evse_state_set_authorized(false); + + // Mantém runtime num valor seguro (não 0, pois clamp -> 6A). + // O "pause" efetivo é pela autorização=false. + evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT); +} + +static void clear_pause_by_master_if_any(void) +{ + bool was_paused; + bool prev_auth; + + portENTER_CRITICAL(&s_state_mux); + was_paused = paused_by_master; + prev_auth = paused_prev_authorized; + paused_by_master = false; + paused_prev_authorized = false; + portEXIT_CRITICAL(&s_state_mux); + + // Se estava autorizado antes do pause, tenta reautorizar + if (was_paused && prev_auth) + evse_state_set_authorized(true); } static void on_frame_slave(uint8_t src, uint8_t dest, - const uint8_t *payload, uint8_t len) { - if (dest != evse_link_get_self_id() && dest != 0xFF) return; - if (len < 1) return; + const uint8_t *payload, uint8_t len) +{ + const uint8_t self = evse_link_get_self_id(); + + // Muito verboso em caminho quente; deixa em DEBUG + ESP_LOGD(TAG, "RX frames (src=0x%02X dest=0x%02X len=%u self=0x%02X)", src, dest, len, self); + + if (src == self) + return; + + if (dest != self && dest != 0xFF) + return; + + if (payload == NULL || len < 1) + { + ESP_LOGW(TAG, "RX invalid: payload NULL or len<1 (len=%u)", len); + return; + } + + // Só aceitar comandos do master + if (src != MASTER_ID) + { + ESP_LOGW(TAG, "RX ignore: non-master src=0x%02X", src); + return; + } + + // Qualquer frame válido do master => link vivo (reset do fallback) + if (fallback_timer) + (void)xTimerReset(fallback_timer, 0); uint8_t cmd = payload[0]; - switch (cmd) { + + switch (cmd) + { case CMD_POLL: - ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src); + // Liveness only. Não sai de safe-mode e não restaura limites. + ESP_LOGI(TAG, "CMD_POLL from 0x%02X", src); break; case CMD_CONFIG_BROADCAST: - ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src); + ESP_LOGI(TAG, "CMD_CONFIG_BROADCAST from 0x%02X", src); break; - case CMD_SET_CURRENT: { - if (len < LEN_SET_CURRENT) { - ESP_LOGW(TAG, "SET_CURRENT from 0x%02X with invalid length %u", src, len); + case CMD_HEARTBEAT_ACK: + ESP_LOGI(TAG, "HEARTBEAT_ACK from 0x%02X", src); + break; + + case CMD_SET_CURRENT: + { + ESP_LOGI(TAG, "SET_CURRENT from 0x%02X", src); + + if (len < LEN_SET_CURRENT) + { + ESP_LOGW(TAG, "SET_CURRENT invalid len=%u from 0x%02X", len, src); break; } - uint16_t amps = payload[1] | (payload[2] << 8); + uint16_t amps = (uint16_t)(payload[1] | ((uint16_t)payload[2] << 8)); + + // Comando explícito => pode sair de safe-mode (mesmo se for pause) + maybe_exit_safe_mode_on_explicit_power_cmd(cmd); + + if (amps == 0) + { + // PAUSE explícito + ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE (src=0x%02X)", src); + apply_pause_by_master(); + + // Confirma sem bloquear RX + request_heartbeat_send(); + + // Publica evento com 0A (semântica: pause) + (void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED, + &s, sizeof(amps), portMAX_DELAY); + break; + } + + clear_pause_by_master_if_any(); + evse_set_runtime_charging_current(amps); - ESP_LOGI(TAG, "Applied runtime limit: %uA from master 0x%02X", amps, src); - esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED, - &s, sizeof(amps), portMAX_DELAY); + + // confirma sem bloquear RX + request_heartbeat_send(); + + ESP_LOGI(TAG, "Applied runtime limit: %uA from 0x%02X", (unsigned)amps, src); + + (void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED, + &s, sizeof(amps), portMAX_DELAY); break; } - case CMD_HEARTBEAT_ACK: - ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src); - if (fallback_timer) { - xTimerReset(fallback_timer, 0); - if (safe_mode) { - safe_mode = false; - uint16_t current = evse_get_runtime_charging_current(); - evse_set_runtime_charging_current(current); - ESP_LOGI(TAG, "Exiting safe mode, restoring %uA", current); - } - } - break; - - case CMD_AUTH_GRANTED: { - if (len < 2) { - ESP_LOGW(TAG, "CMD_AUTH_GRANTED from 0x%02X with invalid length %u", src, len); + case CMD_AUTH_GRANTED: + { + if (len < 2) + { + ESP_LOGW(TAG, "AUTH_GRANTED invalid len=%u from 0x%02X", len, src); break; } - const char *tag = (const char *)&payload[1]; + const uint8_t *tag_ptr = &payload[1]; + size_t tag_buf_len = (size_t)(len - 1); + size_t tag_len = bounded_strlen_u8(tag_ptr, tag_buf_len); evse_link_auth_grant_event_t ev = {0}; - strncpy(ev.tag, tag, EVSE_LINK_TAG_MAX_LEN - 1); - ev.tag[EVSE_LINK_TAG_MAX_LEN - 1] = '\0'; + size_t copy_len = tag_len; + if (copy_len > (EVSE_LINK_TAG_MAX_LEN - 1)) + copy_len = EVSE_LINK_TAG_MAX_LEN - 1; - ESP_LOGI(TAG, "Received CMD_AUTH_GRANTED from master 0x%02X, tag='%s'", src, ev.tag); + if (copy_len > 0) + memcpy(ev.tag, tag_ptr, copy_len); + ev.tag[copy_len] = '\0'; - esp_err_t err = esp_event_post( - EVSE_LINK_EVENTS, - LINK_EVENT_REMOTE_AUTH_GRANTED, - &ev, - sizeof(ev), - portMAX_DELAY); + // AUTH_GRANTED não deve sair de safe-mode automaticamente + ESP_LOGI(TAG, "AUTH_GRANTED from 0x%02X tag='%s'", src, ev.tag); - if (err != ESP_OK) { + esp_err_t err = esp_event_post(EVSE_LINK_EVENTS, + LINK_EVENT_REMOTE_AUTH_GRANTED, + &ev, sizeof(ev), + portMAX_DELAY); + if (err != ESP_OK) ESP_LOGE(TAG, "Failed to post LINK_EVENT_REMOTE_AUTH_GRANTED: %s", esp_err_to_name(err)); - } break; } default: - ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src); + ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src); + break; } } +static void slave_heartbeat_task(void *arg) +{ + (void)arg; + const uint32_t period_ms = 60000; + uint8_t id = evse_link_get_self_id(); -// --- Periodic heartbeat task --- -static void slave_heartbeat_task(void *arg) { - const TickType_t interval = pdMS_TO_TICKS(10000); - for (;;) { - send_heartbeat_frame(); - vTaskDelay(interval); + // desfasamento por ID: 2s, 4s, 6s... + vTaskDelay(pdMS_TO_TICKS((uint32_t)id * 2000)); + + for (;;) + { + request_heartbeat_send(); + + // jitter opcional + uint32_t jitter_ms = esp_random() % 201; // 0..200ms + vTaskDelay(pdMS_TO_TICKS(period_ms + jitter_ms)); } } -// --- Fallback safe mode callback --- -static void fallback_timer_cb(TimerHandle_t xTimer) { - if (!safe_mode) { +static void fallback_timer_cb(TimerHandle_t xTimer) +{ + (void)xTimer; + + // entra safe_mode uma vez + bool already_safe; + portENTER_CRITICAL(&s_state_mux); + already_safe = safe_mode; + if (!safe_mode) safe_mode = true; - ESP_LOGW(TAG, "Fallback timeout: entering safe mode"); - evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT); - esp_event_post(EVSE_LINK_EVENTS, - LINK_EVENT_SLAVE_OFFLINE, - NULL, 0, portMAX_DELAY); - } + portEXIT_CRITICAL(&s_state_mux); + + if (already_safe) + return; + + saved_runtime_limit = evse_get_runtime_charging_current(); + +#if EVSE_LINK_FALLBACK_PAUSE + ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA). Policy=PAUSE", + (unsigned)saved_runtime_limit); + + // pausar é mais seguro quando o master “morre” + apply_pause_by_master(); +#else + ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA, forcing %uA). Policy=MIN", + (unsigned)saved_runtime_limit, (unsigned)MIN_CHARGING_CURRENT_LIMIT); + + evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT); +#endif + + (void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE, + NULL, 0, portMAX_DELAY); + + // opcional: manda heartbeat para indicar estado atual + request_heartbeat_send(); } -// --- Slave initialization --- -void evse_link_slave_init(void) { - if (evse_link_get_mode()!=EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) return; +void evse_link_slave_init(void) +{ + if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) + return; ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id()); - // register frame callback evse_link_register_rx_cb(on_frame_slave); - // start periodic heartbeat - xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 5, NULL); - - // fallback timer - fallback_timer = xTimerCreate("fallback_tmr", - pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS), - pdFALSE, NULL, - fallback_timer_cb); - if (fallback_timer) { - xTimerStart(fallback_timer, 0); + // cria queue/task do sender (para não mandar UART TX no callback RX) + if (hb_req_q == NULL) + { + hb_req_q = xQueueCreate(HB_REQ_QUEUE_LEN, sizeof(hb_req_t)); + if (!hb_req_q) + { + ESP_LOGE(TAG, "Failed to create HB request queue (fallback to direct send)"); + } + else + { + if (xTaskCreate(hb_sender_task, "hb_sender", 3072, NULL, 3, &hb_sender_task_handle) != pdPASS) + { + ESP_LOGE(TAG, "Failed to create hb_sender task"); + vQueueDelete(hb_req_q); + hb_req_q = NULL; + hb_sender_task_handle = NULL; + } + } } - // react to EVSE state changes - ESP_ERROR_CHECK( - esp_event_handler_register( - EVSE_EVENTS, - EVSE_EVENT_STATE_CHANGED, - evse_event_handler, - NULL - ) - ); -} + if (hb_task_handle == NULL) + { + if (xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 3, &hb_task_handle) != pdPASS) + { + ESP_LOGE(TAG, "Failed to create slave_heartbeat_task"); + hb_task_handle = NULL; + } + } -// === Fim de: components/evse_link/src/evse_link_slave.c === + if (fallback_timer == NULL) + { + fallback_timer = xTimerCreate("fallback_tmr", + pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS), + pdFALSE, NULL, + fallback_timer_cb); + if (fallback_timer) + (void)xTimerStart(fallback_timer, 0); + else + ESP_LOGE(TAG, "Failed to create fallback timer"); + } + else + { + (void)xTimerReset(fallback_timer, 0); + } + + if (!evse_handler_registered) + { + ESP_ERROR_CHECK(esp_event_handler_register( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, + evse_event_handler, NULL)); + evse_handler_registered = true; + } +} diff --git a/components/loadbalancer/CMakeLists.txt b/components/loadbalancer/CMakeLists.txt index e7c2516..240f327 100755 --- a/components/loadbalancer/CMakeLists.txt +++ b/components/loadbalancer/CMakeLists.txt @@ -1,5 +1,5 @@ set(srcs - "src/input_filter.c" "src/loadbalancer.c" "src/loadbalancer_events.c" + "src/input_filter.c" "src/loadbalancer.c" "src/pv_optimizer.c" "src/grid_limiter.c" "src/loadbalancer_events.c" ) idf_component_register(SRCS "${srcs}" diff --git a/components/loadbalancer/include/grid_limiter.h b/components/loadbalancer/include/grid_limiter.h new file mode 100755 index 0000000..c3425c6 --- /dev/null +++ b/components/loadbalancer/include/grid_limiter.h @@ -0,0 +1,42 @@ +#ifndef GRID_LIMITER_H_ +#define GRID_LIMITER_H_ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include +#include "esp_err.h" +#include "meter_events.h" + + void grid_limiter_init(void); + + void grid_limiter_set_enabled(bool en); + bool grid_limiter_is_enabled(void); + + esp_err_t grid_limiter_set_max_import_a(uint8_t a); + uint8_t grid_limiter_get_max_import_a(void); + + /** + * @brief Calcula um novo "total_budget_a" (<= current_total_a) para respeitar max_import_a. + * + * Preferência: + * - Usa watt_total (+import / -export) se existir + * - Caso watt_total==0, usa fallback_grid_current_a (magnitude) + * + * @param grid_evt último evento do GRID + * @param fallback_grid_current_a corrente filtrada (magnitude) como fallback + * @param current_total_a total atual a atribuir aos EVSE (A) + * @return total_budget_a (<= current_total_a) + */ + float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt, + float fallback_grid_current_a, + float current_total_a); + +#ifdef __cplusplus +} +#endif + +#endif /* GRID_LIMITER_H_ */ diff --git a/components/loadbalancer/include/loadbalancer.h b/components/loadbalancer/include/loadbalancer.h index 93ed677..67dfecf 100755 --- a/components/loadbalancer/include/loadbalancer.h +++ b/components/loadbalancer/include/loadbalancer.h @@ -9,36 +9,26 @@ extern "C" { #include #include "esp_err.h" - -/** - * @brief Inicializa o módulo de load balancer - */ void loadbalancer_init(void); -/** - * @brief Task contínua do algoritmo de balanceamento - */ -void loadbalancer_task(void *param); - -/** - * @brief Ativa ou desativa o load balancing - */ -void loadbalancer_set_enabled(bool value); - -/** - * @brief Verifica se o load balancing está ativo - */ +void loadbalancer_set_enabled(bool enabled); bool loadbalancer_is_enabled(void); -/** - * @brief Define a corrente máxima do grid - */ -esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current); +// GRID limit (A) +void loadbalancer_grid_set_enabled(bool en); +bool loadbalancer_grid_is_enabled(void); +esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a); +uint8_t loadbalancer_grid_get_max_import_a(void); -/** - * @brief Obtém a corrente máxima do grid - */ -uint8_t load_balancing_get_max_grid_current(void); +// PV optimizer (W) +void loadbalancer_pv_set_enabled(bool en); +bool loadbalancer_pv_is_enabled(void); +esp_err_t loadbalancer_pv_set_max_import_w(int32_t w); +int32_t loadbalancer_pv_get_max_import_w(void); + +// Aliases legacy (se quiseres manter chamadas antigas) +esp_err_t load_balancing_set_max_grid_current(uint8_t value); +uint8_t load_balancing_get_max_grid_current(void); #ifdef __cplusplus } diff --git a/components/loadbalancer/include/pv_optimizer.h b/components/loadbalancer/include/pv_optimizer.h new file mode 100755 index 0000000..d58e853 --- /dev/null +++ b/components/loadbalancer/include/pv_optimizer.h @@ -0,0 +1,40 @@ +#ifndef PV_OPTIMIZER_H_ +#define PV_OPTIMIZER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include "esp_err.h" +#include "meter_events.h" + +void pv_optimizer_init(void); + +void pv_optimizer_set_enabled(bool en); +bool pv_optimizer_is_enabled(void); + +esp_err_t pv_optimizer_set_max_import_w(int32_t w); +int32_t pv_optimizer_get_max_import_w(void); + +/** + * @brief Calcula o budget TOTAL (A) para todos os EVSEs, para manter importação <= max_import_w. + * + * - max_import_w = 0 => modo "Só PV": tenta manter importação ~0 (só consome quando há exportação). + * - max_import_w > 0 => modo "PV-Grid": permite importar até esse valor. + * + * @param grid_evt Último evento do medidor GRID (watt_total assinado). + * @param last_total_cmd_a Soma da corrente comandada no ciclo anterior (A). + * @param total_hw_max_a Soma dos hw_max_current dos conectores ativos (A). + * @return budget_total_a (0..total_hw_max_a) + */ +float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt, + float last_total_cmd_a, + float total_hw_max_a); + +#ifdef __cplusplus +} +#endif + +#endif /* PV_OPTIMIZER_H_ */ diff --git a/components/loadbalancer/src/grid_limiter.c b/components/loadbalancer/src/grid_limiter.c new file mode 100755 index 0000000..de33664 --- /dev/null +++ b/components/loadbalancer/src/grid_limiter.c @@ -0,0 +1,123 @@ +#include "grid_limiter.h" +#include "esp_log.h" +#include + +static const char *TAG = "grid_limiter"; + +#define DEFAULT_VOLTAGE_V (230.0f) + +typedef struct +{ + bool enabled; + uint8_t max_import_a; +} grid_cfg_t; + +static grid_cfg_t s_cfg = { + .enabled = false, + .max_import_a = 32}; + +static float clamp_pf(float pf) +{ + if (pf < 0.05f || pf > 1.2f) + return 1.0f; + return pf; +} + +static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph) +{ + float sum = 0.0f; + int cnt = 0; + + if (!m) + { + *v_avg = DEFAULT_VOLTAGE_V; + *nph = 1; + return; + } + + for (int i = 0; i < 3; i++) + { + if (m->vrms[i] > 80.0f) + { + sum += m->vrms[i]; + cnt++; + } + } + + if (cnt == 0) + { + *v_avg = DEFAULT_VOLTAGE_V; + *nph = 1; + return; + } + + *v_avg = sum / (float)cnt; + *nph = cnt; +} + +void grid_limiter_init(void) { /* nada */ } + +void grid_limiter_set_enabled(bool en) { s_cfg.enabled = en; } +bool grid_limiter_is_enabled(void) { return s_cfg.enabled; } + +esp_err_t grid_limiter_set_max_import_a(uint8_t a) +{ + if (a < 6 || a > 100) + return ESP_ERR_INVALID_ARG; + s_cfg.max_import_a = a; + return ESP_OK; +} + +uint8_t grid_limiter_get_max_import_a(void) { return s_cfg.max_import_a; } + +float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt, + float fallback_grid_current_a, + float current_total_a) +{ + if (!s_cfg.enabled) + return current_total_a; + if (current_total_a <= 0.0f) + return 0.0f; + + float i_import = 0.0f; + + if (grid_evt && grid_evt->watt_total > 0) + { + float v_avg; + int nph; + estimate_v_and_phases(grid_evt, &v_avg, &nph); + const float pf = clamp_pf(grid_evt->power_factor); + const float denom = v_avg * (float)nph * pf; + + if (denom > 10.0f) + { + i_import = ((float)grid_evt->watt_total) / denom; + } + else + { + i_import = fallback_grid_current_a; + } + } + else + { + // export (<=0) => import=0; ou sem potência => fallback + if (grid_evt && grid_evt->watt_total < 0) + i_import = 0.0f; + else + i_import = fallback_grid_current_a; + } + + if (i_import <= (float)s_cfg.max_import_a + 0.01f) + return current_total_a; + + const float over = i_import - (float)s_cfg.max_import_a; + const float cut_a = ceilf(over); // conservador + float new_total = current_total_a - cut_a; + if (new_total < 0.0f) + new_total = 0.0f; + + ESP_LOGD(TAG, "cap: i_import=%.2fA max=%uA over=%.2fA total=%.1fA -> %.1fA", + i_import, (unsigned)s_cfg.max_import_a, over, current_total_a, new_total); + + return new_total; +} diff --git a/components/loadbalancer/src/loadbalancer.c b/components/loadbalancer/src/loadbalancer.c index 04089f0..4493c89 100755 --- a/components/loadbalancer/src/loadbalancer.c +++ b/components/loadbalancer/src/loadbalancer.c @@ -1,7 +1,11 @@ // components/loadbalancer/src/loadbalancer.c + #include "loadbalancer.h" #include "loadbalancer_events.h" +#include "pv_optimizer.h" +#include "grid_limiter.h" + #include "esp_event.h" #include "esp_log.h" #include "esp_timer.h" @@ -16,6 +20,7 @@ #include "evse_events.h" #include "storage_service.h" +#include "evse_link_events.h" #include #include @@ -37,46 +42,64 @@ static const char *TAG = "loadbalancer"; #define LB_SUSPEND_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT - 1.0f) #define LB_RESUME_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT + 1.0f) -#define GRID_METER_TIMEOUT_US (120LL * 1000000LL) +#define GRID_METER_TIMEOUT_US (10LL * 1000000LL) -static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; static bool loadbalancer_enabled = false; -static float grid_current = 0.0f; -static float evse_current = 0.0f; +// GRID limit +static bool grid_limit_enabled = true; +static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; // mantém nome (já usado em helpers) + +// PV +static bool pv_enabled = false; +static int32_t pv_max_import_w = 0; // 0=só PV + +// métricas do meter +static float grid_current = 0.0f; // fallback magnitude (max irms filtrado) static input_filter_t grid_filter; -static input_filter_t evse_filter; + +static input_filter_t grid_power_filter; // NEW +static int32_t grid_watt_total = 0; // NEW (filtrado) +static meter_event_data_t last_grid_evt; // NEW +static bool have_grid_evt = false; // NEW static int64_t last_grid_timestamp_us = 0; -#define MAX_SLAVES 255 +#define MAX_SLAVES 30 #define CONNECTOR_COUNT (MAX_SLAVES + 1) static SemaphoreHandle_t lb_mutex = NULL; typedef struct { - uint8_t id; // para slaves: 0..254 ; para master não usado externamente + uint8_t id; // slaves: 0..254 bool is_master; + bool charging; + float hw_max_current; float runtime_current; - int64_t timestamp; - bool online; + + int64_t timestamp; // última atualização de métricas + bool online; // fonte: EVSE-Link (ou status recebido) + float assigned; int64_t started_us; uint16_t last_limit; + bool suspended_by_lb; } evse_connector_t; static evse_connector_t connectors[CONNECTOR_COUNT]; -const int64_t METRICS_TIMEOUT_US = 60LL * 1000000LL; +static const int64_t METRICS_TIMEOUT_US = 60LL * 1000000LL; -// Storage namespace/keys (mantém os mesmos nomes para compatibilidade) #define LB_NS "loadbalancing" -#define LB_KEY_MAX_GRID "max_grid_curr" // u8 -#define LB_KEY_ENABLED "enabled" // u8 (0/1) +#define LB_KEY_ENABLED "enabled" +#define LB_KEY_GRID_ON "grid_on" +#define LB_KEY_GRID_MAXA "grid_max_a" +#define LB_KEY_PV_ON "pv_on" +#define LB_KEY_PV_MAXW "pv_max_w" // precisa storage u32/i32 static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); } @@ -106,7 +129,7 @@ static void init_connectors(void) for (int i = 1; i < CONNECTOR_COUNT; i++) { connectors[i] = (evse_connector_t){ - .id = (uint8_t)(i - 1), // slaves 0..254 + .id = (uint8_t)(i - 1), .is_master = false, .charging = false, .hw_max_current = 0.0f, @@ -120,6 +143,48 @@ static void init_connectors(void) } } +// ===== EVSE-Link presence -> connectors[].online ===== +static void on_evse_link_presence(void *handler_arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)handler_arg; + if (base != EVSE_LINK_EVENTS) + return; + if (id != LINK_EVENT_SLAVE_ONLINE && id != LINK_EVENT_SLAVE_OFFLINE) + return; + + uint8_t sid = 0xFF; + if (!data) + return; + + const evse_link_slave_presence_event_t *p = (const evse_link_slave_presence_event_t *)data; + sid = p->slave_id; + if (sid >= MAX_SLAVES) + return; + + const int idx = (int)sid + 1; + + if (lb_mutex) + xSemaphoreTake(lb_mutex, portMAX_DELAY); + + if (id == LINK_EVENT_SLAVE_ONLINE) + { + connectors[idx].online = true; + ESP_LOGI(TAG, "Slave %u marked ONLINE (EVSE-Link)", (unsigned)sid); + } + else + { + connectors[idx].online = false; + connectors[idx].charging = false; + connectors[idx].assigned = 0.0f; + connectors[idx].suspended_by_lb = false; + connectors[idx].last_limit = 0; + ESP_LOGW(TAG, "Slave %u marked OFFLINE (EVSE-Link)", (unsigned)sid); + } + + if (lb_mutex) + xSemaphoreGive(lb_mutex); +} + static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id, void *data) { (void)handler_arg; @@ -134,19 +199,17 @@ static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id return; } - int idx = (int)status->slave_id + 1; - - bool was_charging = false; + const int idx = (int)status->slave_id + 1; if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - was_charging = connectors[idx].charging; + bool was_charging = connectors[idx].charging; connectors[idx].charging = status->charging; connectors[idx].hw_max_current = status->hw_max_current; connectors[idx].runtime_current = status->runtime_current; - connectors[idx].timestamp = esp_timer_get_time(); + connectors[idx].timestamp = (status->timestamp_us > 0) ? status->timestamp_us : esp_timer_get_time(); connectors[idx].online = true; if (status->charging && !was_charging) @@ -158,8 +221,7 @@ static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id if (lb_mutex) xSemaphoreGive(lb_mutex); - ESP_LOGI(TAG, - "Slave %u status: charging=%d hw_max_current=%.1fA runtime_current=%.2fA", + ESP_LOGD(TAG, "Slave %u status: charging=%d hw=%.1fA runtime=%.2fA", (unsigned)status->slave_id, status->charging, status->hw_max_current, status->runtime_current); } @@ -186,9 +248,6 @@ static void on_evse_config_event(void *handler_arg, esp_event_base_t base, int32 if (lb_mutex) xSemaphoreGive(lb_mutex); - - ESP_LOGI(TAG, "EVSE config updated: charging=%d hw_max_current=%.1f runtime_current=%.1f", - evt->charging, evt->hw_max_current, evt->runtime_current); } static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data) @@ -206,28 +265,34 @@ static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t if (evt->irms[i] > max_irms) max_irms = evt->irms[i]; - float max_vrms = evt->vrms[0]; - for (int i = 1; i < 3; ++i) - if (evt->vrms[i] > max_vrms) - max_vrms = evt->vrms[i]; - if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); if (evt->source && strcmp(evt->source, "GRID") == 0) { grid_current = input_filter_update(&grid_filter, max_irms); - last_grid_timestamp_us = esp_timer_get_time(); - ESP_LOGD(TAG, "GRID IRMS (filtered): %.2f A VRMS: %.2f V", grid_current, max_vrms); - } - else if (evt->source && strcmp(evt->source, "EVSE") == 0) - { - evse_current = input_filter_update(&evse_filter, max_irms); - ESP_LOGD(TAG, "EVSE IRMS (filtered): %.2f A", evse_current); + + // NEW: guardar evento completo + potência filtrada + last_grid_evt = *evt; + + // timestamp preferido do evento + const int64_t ts = (evt->timestamp_us > 0) ? evt->timestamp_us : esp_timer_get_time(); + last_grid_timestamp_us = ts; + last_grid_evt.timestamp_us = ts; + + float p_f = input_filter_update(&grid_power_filter, (float)evt->watt_total); + grid_watt_total = (int32_t)lroundf(p_f); + last_grid_evt.watt_total = grid_watt_total; + + have_grid_evt = true; + + ESP_LOGI(TAG, "GRID: I=%.3fA P=%ldW (maxA=%u pv_on=%d pvMaxW=%ld)", + grid_current, (long)grid_watt_total, (unsigned)max_grid_current, + pv_enabled, (long)pv_max_import_w); } else { - ESP_LOGW(TAG, "Unknown meter event source: %s", evt->source ? evt->source : "(null)"); + // EVSE meter ignorado por agora (como combinaste) } if (lb_mutex) @@ -257,9 +322,8 @@ static void loadbalancer_evse_event_handler(void *handler_arg, esp_event_base_t case EVSE_STATE_EVENT_CHARGING: { grid_current = 0.0f; - evse_current = 0.0f; input_filter_reset(&grid_filter); - input_filter_reset(&evse_filter); + input_filter_reset(&grid_power_filter); bool was_charging = connectors[0].charging; @@ -289,106 +353,556 @@ static void loadbalancer_evse_event_handler(void *handler_arg, esp_event_base_t xSemaphoreGive(lb_mutex); } -// --------- Config load/save via storage_service --------- - +// --------- Config load/save --------- static esp_err_t loadbalancer_load_config(void) { - // garante storage iniciado esp_err_t se = storage_service_init(); if (se != ESP_OK) return se; esp_err_t err; bool needs_flush = false; - uint8_t temp_u8 = 0; - // max_grid_curr - err = storage_get_u8_sync(LB_NS, LB_KEY_MAX_GRID, &temp_u8, TO_TICKS_MS(800)); - if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT) - { - max_grid_current = temp_u8; - } - else - { - max_grid_current = MAX_GRID_CURRENT_LIMIT; - (void)storage_set_u8_async(LB_NS, LB_KEY_MAX_GRID, max_grid_current); - needs_flush = true; - ESP_LOGW(TAG, "Invalid/missing max_grid_curr (%s) -> default=%u (persisted)", - esp_err_to_name(err), (unsigned)max_grid_current); - } - // enabled err = storage_get_u8_sync(LB_NS, LB_KEY_ENABLED, &temp_u8, TO_TICKS_MS(800)); if (err == ESP_OK && temp_u8 <= 1) - { loadbalancer_enabled = (temp_u8 != 0); - } else { loadbalancer_enabled = false; (void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0); needs_flush = true; - ESP_LOGW(TAG, "Invalid/missing enabled (%s) -> default=false (persisted)", - esp_err_to_name(err)); + } + + // grid on + err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_ON, &temp_u8, TO_TICKS_MS(800)); + if (err == ESP_OK && temp_u8 <= 1) + grid_limit_enabled = (temp_u8 != 0); + else + { + grid_limit_enabled = true; + (void)storage_set_u8_async(LB_NS, LB_KEY_GRID_ON, 1); + needs_flush = true; + } + + // grid maxA + err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_MAXA, &temp_u8, TO_TICKS_MS(800)); + if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT) + max_grid_current = temp_u8; + else + { + max_grid_current = MAX_GRID_CURRENT_LIMIT; + (void)storage_set_u8_async(LB_NS, LB_KEY_GRID_MAXA, max_grid_current); + needs_flush = true; + } + + // pv on + err = storage_get_u8_sync(LB_NS, LB_KEY_PV_ON, &temp_u8, TO_TICKS_MS(800)); + if (err == ESP_OK && temp_u8 <= 1) + pv_enabled = (temp_u8 != 0); + else + { + pv_enabled = false; + (void)storage_set_u8_async(LB_NS, LB_KEY_PV_ON, 0); + needs_flush = true; + } + + // pv maxW (se não tiveres storage_get_u32_sync, adapta para i32/u32 do teu storage_service) + uint32_t temp_u32 = 0; + err = storage_get_u32_sync(LB_NS, LB_KEY_PV_MAXW, &temp_u32, TO_TICKS_MS(800)); + if (err == ESP_OK) + pv_max_import_w = (int32_t)temp_u32; + else + { + pv_max_import_w = 0; + (void)storage_set_u32_async(LB_NS, LB_KEY_PV_MAXW, 0); + needs_flush = true; } if (needs_flush) - { - // determinístico no boot (void)storage_flush_sync(TO_TICKS_MS(2000)); - } + + // propagar para sub-módulos + pv_optimizer_set_enabled(pv_enabled); + (void)pv_optimizer_set_max_import_w(pv_max_import_w); + + grid_limiter_set_enabled(grid_limit_enabled); + (void)grid_limiter_set_max_import_a(max_grid_current); return ESP_OK; } -static void persist_lb_enabled(bool enabled) -{ - (void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, enabled ? 1 : 0); - // debounced por defeito -} - -static void persist_max_grid_current(uint8_t value) -{ - (void)storage_set_u8_async(LB_NS, LB_KEY_MAX_GRID, value); - // debounced por defeito -} +static void persist_u8(const char *key, uint8_t v) { (void)storage_set_u8_async(LB_NS, key, v); } +static void persist_u32(const char *key, uint32_t v) { (void)storage_set_u32_async(LB_NS, key, v); } void loadbalancer_set_enabled(bool enabled) { - // Só persiste/propaga se mudou if (enabled == loadbalancer_enabled) - { return; - } loadbalancer_enabled = enabled; - persist_lb_enabled(enabled); + persist_u8(LB_KEY_ENABLED, enabled ? 1 : 0); loadbalancer_state_event_t evt = {.enabled = enabled, .timestamp_us = esp_timer_get_time()}; (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_STATE_CHANGED, &evt, sizeof(evt), portMAX_DELAY); } -esp_err_t load_balancing_set_max_grid_current(uint8_t value) +bool loadbalancer_is_enabled(void) { return loadbalancer_enabled; } + +// grid setters/getters +void loadbalancer_grid_set_enabled(bool en) { - if (value < MIN_GRID_CURRENT_LIMIT || value > MAX_GRID_CURRENT_LIMIT) + grid_limit_enabled = en; + persist_u8(LB_KEY_GRID_ON, en ? 1 : 0); + grid_limiter_set_enabled(en); +} + +bool loadbalancer_grid_is_enabled(void) { return grid_limit_enabled; } + +esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a) +{ + if (a < MIN_GRID_CURRENT_LIMIT || a > MAX_GRID_CURRENT_LIMIT) return ESP_ERR_INVALID_ARG; + max_grid_current = a; + persist_u8(LB_KEY_GRID_MAXA, a); + return grid_limiter_set_max_import_a(a); +} - // Só persiste se mudou - if (value == max_grid_current) - { - return ESP_OK; - } +uint8_t loadbalancer_grid_get_max_import_a(void) { return max_grid_current; } - max_grid_current = value; - persist_max_grid_current(value); +// pv setters/getters +void loadbalancer_pv_set_enabled(bool en) +{ + pv_enabled = en; + persist_u8(LB_KEY_PV_ON, en ? 1 : 0); + pv_optimizer_set_enabled(en); +} + +bool loadbalancer_pv_is_enabled(void) { return pv_enabled; } + +esp_err_t loadbalancer_pv_set_max_import_w(int32_t w) +{ + esp_err_t e = pv_optimizer_set_max_import_w(w); + if (e != ESP_OK) + return e; + + pv_max_import_w = w; + persist_u32(LB_KEY_PV_MAXW, (uint32_t)w); return ESP_OK; } -uint8_t load_balancing_get_max_grid_current(void) { return max_grid_current; } -bool loadbalancer_is_enabled(void) { return loadbalancer_enabled; } +int32_t loadbalancer_pv_get_max_import_w(void) { return pv_max_import_w; } +// aliases legacy +esp_err_t load_balancing_set_max_grid_current(uint8_t value) { return loadbalancer_grid_set_max_import_a(value); } +uint8_t load_balancing_get_max_grid_current(void) { return loadbalancer_grid_get_max_import_a(); } + +// ===== util: distribuição “water-fill” segura ===== +static void distribute_extra_evenly(int *idxs, int cnt, float extra) +{ + if (cnt <= 0 || extra <= 0.0f) + return; + + while (extra > AVAILABLE_EPS) + { + int eligible = 0; + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + float headroom = connectors[i].hw_max_current - connectors[i].assigned; + if (headroom > AVAILABLE_EPS && connectors[i].assigned > 0.0f) + eligible++; + } + + if (eligible <= 0) + break; + + float share = extra / (float)eligible; + if (share <= AVAILABLE_EPS) + break; + + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + if (connectors[i].assigned <= 0.0f) + continue; + + float headroom = connectors[i].hw_max_current - connectors[i].assigned; + if (headroom <= AVAILABLE_EPS) + continue; + + float inc = MIN(share, headroom); + connectors[i].assigned += inc; + extra -= inc; + + if (extra <= AVAILABLE_EPS) + break; + } + } +} + +// ============================================================================ +// helpers (iguais aos teus) + NOVO: budget policy +// ============================================================================ + +typedef enum +{ + LB_LIMIT_EVT_MASTER = 0, + LB_LIMIT_EVT_SLAVE = 1 +} lb_limit_evt_type_t; + +typedef struct +{ + lb_limit_evt_type_t type; + uint8_t slave_id; + uint16_t max_current; + int64_t timestamp_us; +} lb_pending_limit_t; + +static int build_active_list_locked(int64_t now, int *idxs, int idxs_max) +{ + int n = 0; + for (int i = 0; i < CONNECTOR_COUNT && n < idxs_max; i++) + { + if (!connectors[i].online) + continue; + if (!connectors[i].charging) + continue; + + if (!connectors[i].is_master) + { + if (connectors[i].timestamp == 0 || (now - connectors[i].timestamp) >= METRICS_TIMEOUT_US) + continue; + } + + idxs[n++] = i; + } + return n; +} + +static void sort_active_by_started_locked(int *idxs, int cnt) +{ + for (int a = 0; a < cnt - 1; a++) + for (int b = 0; b < cnt - 1 - a; b++) + if (connectors[idxs[b]].started_us > connectors[idxs[b + 1]].started_us) + { + int tmp = idxs[b]; + idxs[b] = idxs[b + 1]; + idxs[b + 1] = tmp; + } +} + +static float sum_hw_max_locked(const int *idxs, int cnt) +{ + float s = 0.0f; + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + if (connectors[i].hw_max_current > 0.0f) + s += connectors[i].hw_max_current; + } + return s; +} + +static float sum_last_limit_locked(const int *idxs, int cnt) +{ + float s = 0.0f; + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + s += (float)connectors[i].last_limit; + } + return s; +} + +static float sum_assigned_locked(const int *idxs, int cnt) +{ + float s = 0.0f; + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + if (connectors[i].assigned > 0.0f) + s += connectors[i].assigned; + } + return s; +} + +// NEW: política por budget total (A) +static void apply_budget_policy_locked(int *idxs, int cnt, float total_budget_a) +{ + if (cnt <= 0) + return; + if (total_budget_a < 0.0f) + total_budget_a = 0.0f; + + // baseline: tudo a 0 + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + connectors[i].assigned = 0.0f; + connectors[i].suspended_by_lb = true; + } + + // mínimos por conector (respeita hw_max) + float required = 0.0f; + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + float hw = connectors[i].hw_max_current; + float min_i = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw); + if (min_i > 0.0f) + required += min_i; + } + + float remaining = total_budget_a; + + if (remaining + AVAILABLE_EPS >= required) + { + // dá para dar mínimo a todos + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + float hw = connectors[i].hw_max_current; + float min_i = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw); + if (min_i > 0.0f) + { + connectors[i].assigned = min_i; + connectors[i].suspended_by_lb = false; + remaining -= min_i; + } + } + + if (remaining > AVAILABLE_EPS) + distribute_extra_evenly(idxs, cnt, remaining); + } + else + { + // não dá: mínimos só para os mais antigos até caber + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + float hw = connectors[i].hw_max_current; + float min_i = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw); + + if (min_i > 0.0f && remaining + AVAILABLE_EPS >= min_i) + { + connectors[i].assigned = min_i; + connectors[i].suspended_by_lb = false; + remaining -= min_i; + } + } + } +} + +static void apply_meter_timeout_policy_locked(int *idxs, int cnt) +{ + // igual ao teu: tenta manter MIN se couber no max_grid_current + const float budget = (float)max_grid_current; + float need = (float)MIN_CHARGING_CURRENT_LIMIT * (float)cnt; + + if (need <= budget + AVAILABLE_EPS) + { + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + float hw = connectors[i].hw_max_current; + float target = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw); + connectors[i].assigned = target; + connectors[i].suspended_by_lb = false; + } + } + else + { + int can = (int)floorf(budget / (float)MIN_CHARGING_CURRENT_LIMIT); + if (can < 0) + can = 0; + if (can > cnt) + can = cnt; + + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + if (k < can) + { + float hw = connectors[i].hw_max_current; + connectors[i].assigned = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw); + connectors[i].suspended_by_lb = false; + } + else + { + connectors[i].assigned = 0.0f; + connectors[i].suspended_by_lb = true; + } + } + } +} + +static void apply_normal_policy_locked(int *idxs, int cnt, float grid_snapshot) +{ + // o teu algoritmo atual (grid-limit em A, por corrente medida) + float available = (float)max_grid_current - grid_snapshot; + + if (available < -AVAILABLE_EPS) + { + float factor = 0.0f; + if (grid_snapshot > AVAILABLE_EPS) + factor = ((float)max_grid_current) / grid_snapshot; + + if (factor < 0.0f) + factor = 0.0f; + if (factor > 1.0f) + factor = 1.0f; + + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + // baseline = runtime (como tinhas) + float cur = connectors[i].runtime_current; + float hw = connectors[i].hw_max_current; + if (cur < 0.0f) + cur = 0.0f; + if (hw < 0.0f) + hw = 0.0f; + connectors[i].assigned = MIN(cur, hw) * factor; + } + return; + } + + // baseline = runtime + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + float cur = connectors[i].runtime_current; + float hw = connectors[i].hw_max_current; + if (cur < 0.0f) + cur = 0.0f; + if (hw < 0.0f) + hw = 0.0f; + connectors[i].assigned = MIN(cur, hw); + } + + if (fabsf(available) <= AVAILABLE_EPS) + return; + + float remaining = available; + + // fase 1: subir até MIN para os mais antigos, se possível + for (int k = 0; k < cnt && remaining > 0.0f; k++) + { + int i = idxs[k]; + float hw = connectors[i].hw_max_current; + float target = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw); + + if (connectors[i].assigned >= target) + continue; + + float delta = target - connectors[i].assigned; + float inc = MIN(delta, remaining); + connectors[i].assigned += inc; + remaining -= inc; + } + + // fase 2: suspender os mais recentes que ficaram abaixo do mínimo + for (int k = cnt - 1; k >= 0; k--) + { + int i = idxs[k]; + if (connectors[i].assigned >= (float)MIN_CHARGING_CURRENT_LIMIT) + continue; + + connectors[i].assigned = 0.0f; + connectors[i].suspended_by_lb = true; + } + + // fase 3: distribuir extra pelos que estão ativos + if (remaining > AVAILABLE_EPS) + distribute_extra_evenly(idxs, cnt, remaining); +} + +static int prepare_pending_limits_locked(const int *idxs, + int cnt, + int64_t now, + lb_pending_limit_t *out, + int out_max) +{ + int out_cnt = 0; + + for (int k = 0; k < cnt; k++) + { + int i = idxs[k]; + + float assigned = connectors[i].assigned; + float effective = assigned; + + // histerese suspend/resume (mantida) + if (connectors[i].suspended_by_lb) + { + if (assigned >= LB_RESUME_THRESHOLD) + { + effective = assigned; + connectors[i].suspended_by_lb = false; + } + else + { + effective = 0.0f; + } + } + else + { + if (assigned > 0.0f && assigned < LB_SUSPEND_THRESHOLD) + { + effective = 0.0f; + connectors[i].suspended_by_lb = true; + } + } + + uint16_t max_cur = (effective <= 0.0f) ? 0 : (uint16_t)MIN(effective, (float)MAX_CHARGING_CURRENT_LIMIT); + + if (connectors[i].last_limit == max_cur) + continue; + + connectors[i].last_limit = max_cur; + + if (out_cnt < out_max) + { + lb_pending_limit_t p = { + .type = connectors[i].is_master ? LB_LIMIT_EVT_MASTER : LB_LIMIT_EVT_SLAVE, + .slave_id = connectors[i].is_master ? 0 : connectors[i].id, + .max_current = max_cur, + .timestamp_us = now}; + out[out_cnt++] = p; + } + } + + return out_cnt; +} + +static void post_pending_limits(const lb_pending_limit_t *pending, int pending_cnt) +{ + for (int i = 0; i < pending_cnt; i++) + { + const lb_pending_limit_t *p = &pending[i]; + + if (p->type == LB_LIMIT_EVT_MASTER) + { + loadbalancer_master_limit_event_t m = {.max_current = p->max_current, .timestamp_us = p->timestamp_us}; + (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, + &m, sizeof(m), portMAX_DELAY); + } + else + { + if (p->slave_id < MAX_SLAVES) + { + loadbalancer_slave_limit_event_t s = {.slave_id = p->slave_id, .max_current = p->max_current, .timestamp_us = p->timestamp_us}; + (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + &s, sizeof(s), portMAX_DELAY); + } + } + } +} + +// ===== loadbalancer_task (PV + grid cap, event-driven) ===== void loadbalancer_task(void *param) { (void)param; @@ -401,38 +915,27 @@ void loadbalancer_task(void *param) continue; } + const int64_t now = esp_timer_get_time(); + int idxs[CONNECTOR_COUNT]; int active_cnt = 0; - int64_t now = esp_timer_get_time(); + float grid_snapshot = 0.0f; + int64_t last_grid_ts_snapshot = 0; + bool have_grid_snapshot = false; + meter_event_data_t grid_evt_snapshot; + + // 1) snapshot + lista ativa if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int i = 0; i < CONNECTOR_COUNT; i++) - { - if (connectors[i].is_master) - { - connectors[i].online = true; - if (connectors[i].charging) - idxs[active_cnt++] = i; - continue; - } + grid_snapshot = grid_current; + last_grid_ts_snapshot = last_grid_timestamp_us; - if (!connectors[i].online) - continue; + have_grid_snapshot = have_grid_evt; + grid_evt_snapshot = last_grid_evt; - if ((now - connectors[i].timestamp) >= METRICS_TIMEOUT_US) - { - connectors[i].online = false; - continue; - } - - if (connectors[i].charging) - idxs[active_cnt++] = i; - } - - float grid_snapshot = grid_current; - int64_t last_grid_ts_snapshot = last_grid_timestamp_us; + active_cnt = build_active_list_locked(now, idxs, CONNECTOR_COUNT); if (lb_mutex) xSemaphoreGive(lb_mutex); @@ -446,246 +949,87 @@ void loadbalancer_task(void *param) bool meter_timeout = (last_grid_ts_snapshot == 0 || (now - last_grid_ts_snapshot) > GRID_METER_TIMEOUT_US); - if (meter_timeout) - { - if (lb_mutex) - xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int k = 0; k < active_cnt; k++) - { - int i = idxs[k]; - float cur = connectors[i].runtime_current; - connectors[i].assigned = (cur > MIN_CHARGING_CURRENT_LIMIT) ? (float)MIN_CHARGING_CURRENT_LIMIT : cur; - } - if (lb_mutex) - xSemaphoreGive(lb_mutex); - goto publish_limits; - } + // 2) calcular assigned + preparar eventos + lb_pending_limit_t pending[CONNECTOR_COUNT]; + int pending_cnt = 0; - float available = (float)max_grid_current - grid_snapshot; - - if (available < -AVAILABLE_EPS) - { - float factor = ((float)max_grid_current) / grid_snapshot; - if (factor < 0.0f) - factor = 0.0f; - if (factor > 1.0f) - factor = 1.0f; - - if (lb_mutex) - xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int k = 0; k < active_cnt; k++) - { - int i = idxs[k]; - connectors[i].assigned = connectors[i].runtime_current * factor; - } - if (lb_mutex) - xSemaphoreGive(lb_mutex); - } - else if (fabsf(available) <= AVAILABLE_EPS) - { - if (lb_mutex) - xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int k = 0; k < active_cnt; k++) - { - int i = idxs[k]; - connectors[i].assigned = connectors[i].runtime_current; - } - if (lb_mutex) - xSemaphoreGive(lb_mutex); - } - else - { - if (available > max_grid_current) - available = (float)max_grid_current; - - if (lb_mutex) - xSemaphoreTake(lb_mutex, portMAX_DELAY); - - // ordenar por started_us - for (int a = 0; a < active_cnt - 1; a++) - for (int b = 0; b < active_cnt - 1 - a; b++) - if (connectors[idxs[b]].started_us > connectors[idxs[b + 1]].started_us) - { - int tmp = idxs[b]; - idxs[b] = idxs[b + 1]; - idxs[b + 1] = tmp; - } - - for (int k = 0; k < active_cnt; k++) - connectors[idxs[k]].assigned = connectors[idxs[k]].runtime_current; - - float remaining = available; - - // fase 1 min 6A - for (int k = 0; k < active_cnt && remaining > 0.0f; k++) - { - int i = idxs[k]; - float current = connectors[i].runtime_current; - float hw_max = connectors[i].hw_max_current; - - float target_min = (float)MIN_CHARGING_CURRENT_LIMIT; - if (hw_max < target_min) - target_min = hw_max; - - if (current >= target_min) - continue; - - float delta = target_min - current; - if (delta <= remaining) - { - connectors[i].assigned = current + delta; - remaining -= delta; - } - } - - // fase 2 suspender recentes abaixo min - for (int k = active_cnt - 1; k >= 0; k--) - { - int i = idxs[k]; - if (connectors[i].assigned >= MIN_CHARGING_CURRENT_LIMIT) - continue; - connectors[i].assigned = 0.0f; - connectors[i].suspended_by_lb = true; - } - - // fase 3 distribuir extra - if (remaining > AVAILABLE_EPS) - { - int on_cnt = 0; - for (int k = 0; k < active_cnt; k++) - if (connectors[idxs[k]].assigned > 0.0f) - on_cnt++; - - float extra_remaining = remaining; - int extra_cnt = on_cnt; - - for (int k = 0; k < active_cnt; k++) - { - int i = idxs[k]; - if (connectors[i].assigned <= 0.0f) - continue; - if (extra_cnt <= 0 || extra_remaining <= 0.0f) - break; - - float headroom = connectors[i].hw_max_current - connectors[i].assigned; - if (headroom <= 0.0f) - { - extra_cnt--; - continue; - } - - float share = extra_remaining / (float)extra_cnt; - - if (share >= headroom) - { - connectors[i].assigned += headroom; - extra_remaining -= headroom; - extra_cnt--; - } - else - { - for (int m = k; m < active_cnt; m++) - { - int j = idxs[m]; - if (connectors[j].assigned <= 0.0f) - continue; - - float headroom_j = connectors[j].hw_max_current - connectors[j].assigned; - if (headroom_j <= 0.0f) - continue; - - float inc = MIN(share, headroom_j); - connectors[j].assigned += inc; - } - break; - } - } - } - - if (lb_mutex) - xSemaphoreGive(lb_mutex); - } - - publish_limits: if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int k = 0; k < active_cnt; k++) + sort_active_by_started_locked(idxs, active_cnt); + + const float total_hw_max_a = sum_hw_max_locked(idxs, active_cnt); + const float last_total_cmd_a = sum_last_limit_locked(idxs, active_cnt); + + if (meter_timeout) { - int i = idxs[k]; - float assigned = connectors[i].assigned; - float effective = assigned; - - if (connectors[i].suspended_by_lb) - { - if (assigned >= LB_RESUME_THRESHOLD) - { - effective = assigned; - connectors[i].suspended_by_lb = false; - } - else - { - effective = 0.0f; - } - } + // Sem meter: se grid-limit ON usa política conservadora; senão corta (PV não pode atuar) + if (grid_limit_enabled) + apply_meter_timeout_policy_locked(idxs, active_cnt); else - { - if (assigned > 0.0f && assigned < LB_SUSPEND_THRESHOLD) - { - effective = 0.0f; - connectors[i].suspended_by_lb = true; - } - } - - uint16_t max_cur = (effective <= 0.0f) ? 0 : (uint16_t)MIN(effective, (float)MAX_CHARGING_CURRENT_LIMIT); - - if (connectors[i].last_limit == max_cur) - continue; - connectors[i].last_limit = max_cur; - - // sair do mutex durante o post - if (lb_mutex) - xSemaphoreGive(lb_mutex); - - if (connectors[i].is_master) - { - loadbalancer_master_limit_event_t m = { - .max_current = max_cur, - .timestamp_us = now}; - (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, - &m, sizeof(m), portMAX_DELAY); - } - else - { - uint8_t sid = connectors[i].id; - if (sid < MAX_SLAVES) - { - loadbalancer_slave_limit_event_t s = { - .slave_id = sid, - .max_current = max_cur, - .timestamp_us = now}; - (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, - &s, sizeof(s), portMAX_DELAY); - } - else - { - ESP_LOGW(TAG, "Skipping publish: invalid slave id=%u", (unsigned)sid); - } - } - - if (lb_mutex) - xSemaphoreTake(lb_mutex, portMAX_DELAY); + apply_budget_policy_locked(idxs, active_cnt, 0.0f); } + else + { + if (pv_enabled && have_grid_snapshot) + { + // PV define budget total + float budget_total_a = pv_optimizer_compute_budget_a(&grid_evt_snapshot, + last_total_cmd_a, + total_hw_max_a); + + apply_budget_policy_locked(idxs, active_cnt, budget_total_a); + + // Se também houver grid limit: aplica cap por import (A) usando watt_total (preferido) + if (grid_limit_enabled) + { + const float cur_total = sum_assigned_locked(idxs, active_cnt); + const float limited_total = grid_limiter_limit_total_a(&grid_evt_snapshot, grid_snapshot, cur_total); + + if (limited_total + AVAILABLE_EPS < cur_total && cur_total > AVAILABLE_EPS) + { + float factor = limited_total / cur_total; + if (factor < 0.0f) + factor = 0.0f; + if (factor > 1.0f) + factor = 1.0f; + + for (int k = 0; k < active_cnt; k++) + { + int i = idxs[k]; + connectors[i].assigned *= factor; + } + } + } + } + else + { + // Sem PV: se grid-limit ON usa a tua política atual; senão dá "max" (budget=hwmax) + if (grid_limit_enabled) + apply_normal_policy_locked(idxs, active_cnt, grid_snapshot); + else + apply_budget_policy_locked(idxs, active_cnt, total_hw_max_a); + } + } + + pending_cnt = prepare_pending_limits_locked(idxs, active_cnt, now, pending, CONNECTOR_COUNT); if (lb_mutex) xSemaphoreGive(lb_mutex); + // 3) publicar limites + if (pending_cnt > 0) + post_pending_limits(pending, pending_cnt); + vTaskDelay(pdMS_TO_TICKS(5000)); } } void loadbalancer_init(void) { + pv_optimizer_init(); + grid_limiter_init(); + if (loadbalancer_load_config() != ESP_OK) ESP_LOGW(TAG, "Failed to load/init config. Using defaults."); @@ -694,8 +1038,9 @@ void loadbalancer_init(void) ESP_LOGE(TAG, "Failed to create loadbalancer mutex"); init_connectors(); + input_filter_init(&grid_filter, 0.3f); - input_filter_init(&evse_filter, 0.3f); + input_filter_init(&grid_power_filter, 0.3f); if (xTaskCreate(loadbalancer_task, "loadbalancer", 8192, NULL, 4, NULL) != pdPASS) ESP_LOGE(TAG, "Failed to create loadbalancer task"); @@ -711,4 +1056,9 @@ void loadbalancer_init(void) &on_evse_config_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_STATUS, &on_slave_status, NULL)); + + ESP_ERROR_CHECK(esp_event_handler_register(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_ONLINE, + &on_evse_link_presence, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE, + &on_evse_link_presence, NULL)); } diff --git a/components/loadbalancer/src/pv_optimizer.c b/components/loadbalancer/src/pv_optimizer.c new file mode 100755 index 0000000..295ffb1 --- /dev/null +++ b/components/loadbalancer/src/pv_optimizer.c @@ -0,0 +1,165 @@ +#include "pv_optimizer.h" +#include "esp_log.h" +#include + +static const char *TAG = "pv_optimizer"; + +// internos (fixos, como pediste) +#define PV_MIN_EXPORT_W (50) // deadband export (anti-oscilações) +#define PV_TOTAL_RAMP_STEP_A (2.0f) // step total por ciclo (como tens loop 5s) +#define DEFAULT_VOLTAGE_V (230.0f) + +typedef struct +{ + bool enabled; + int32_t max_import_w; // >=0 +} pv_cfg_t; + +static pv_cfg_t s_cfg = { + .enabled = false, + .max_import_w = 0}; + +static float clamp_pf(float pf) +{ + if (pf < 0.05f || pf > 1.2f) + return 1.0f; + return pf; +} + +static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph) +{ + float sum = 0.0f; + int cnt = 0; + + if (!m) + { + *v_avg = DEFAULT_VOLTAGE_V; + *nph = 1; + return; + } + + for (int i = 0; i < 3; i++) + { + if (m->vrms[i] > 80.0f) + { + sum += m->vrms[i]; + cnt++; + } + } + + if (cnt == 0) + { + *v_avg = DEFAULT_VOLTAGE_V; + *nph = 1; + return; + } + + *v_avg = sum / (float)cnt; + *nph = cnt; +} + +void pv_optimizer_init(void) +{ + // nada a fazer +} + +void pv_optimizer_set_enabled(bool en) { s_cfg.enabled = en; } +bool pv_optimizer_is_enabled(void) { return s_cfg.enabled; } + +esp_err_t pv_optimizer_set_max_import_w(int32_t w) +{ + if (w < 0) + return ESP_ERR_INVALID_ARG; + s_cfg.max_import_w = w; + return ESP_OK; +} + +int32_t pv_optimizer_get_max_import_w(void) { return s_cfg.max_import_w; } + +static float ramp_total(float last_a, float target_a) +{ + if (target_a > last_a + PV_TOTAL_RAMP_STEP_A) + return last_a + PV_TOTAL_RAMP_STEP_A; + if (target_a < last_a - PV_TOTAL_RAMP_STEP_A) + return last_a - PV_TOTAL_RAMP_STEP_A; + return target_a; +} + +float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt, + float last_total_cmd_a, + float total_hw_max_a) +{ + if (!s_cfg.enabled) + return total_hw_max_a; + if (!grid_evt) + return 0.0f; + + // se meter não fornece potência (fica 0) não dá para PV -> conservador: não importa + // (podes mudar para "mantém last" se preferires) + if (grid_evt->watt_total == 0) + { + return ramp_total(last_total_cmd_a, 0.0f); + } + + float v_avg; + int nph; + estimate_v_and_phases(grid_evt, &v_avg, &nph); + + const float pf = clamp_pf(grid_evt->power_factor); + const float w_per_a = v_avg * (float)nph * pf; + if (w_per_a < 10.0f) + { + return ramp_total(last_total_cmd_a, 0.0f); + } + + const int32_t p_grid_w = grid_evt->watt_total; // +import / -export + const int32_t target_import_w = s_cfg.max_import_w; // >=0 + + // deadband só para o "Só PV" + if (target_import_w == 0) + { + if (p_grid_w < 0) + { + int32_t export_w = -p_grid_w; + if (export_w < PV_MIN_EXPORT_W) + { + return ramp_total(last_total_cmd_a, 0.0f); + } + } + else + { + // está a importar + if (p_grid_w < PV_MIN_EXPORT_W) + { + return ramp_total(last_total_cmd_a, 0.0f); + } + } + } + + // estima base-load com o comando anterior + const float p_evse_last_w = last_total_cmd_a * w_per_a; + const float p_base_w = (float)p_grid_w - p_evse_last_w; + + // queremos p_grid -> target_import_w + float p_evse_target_w = (float)target_import_w - p_base_w; + + // clamp [0..max] + if (p_evse_target_w < 0.0f) + p_evse_target_w = 0.0f; + const float p_evse_max_w = total_hw_max_a * w_per_a; + if (p_evse_target_w > p_evse_max_w) + p_evse_target_w = p_evse_max_w; + + float target_total_a = p_evse_target_w / w_per_a; + if (target_total_a < 0.0f) + target_total_a = 0.0f; + if (target_total_a > total_hw_max_a) + target_total_a = total_hw_max_a; + + float ramped = ramp_total(last_total_cmd_a, target_total_a); + + ESP_LOGD(TAG, "pv: p_grid=%ldW target_imp=%ldW base=%.1fW last=%.1fA -> target=%.1fA (v=%.1f nph=%d pf=%.2f)", + (long)p_grid_w, (long)target_import_w, p_base_w, last_total_cmd_a, ramped, v_avg, nph, pf); + + return ramped; +} diff --git a/components/logger/CMakeLists.txt b/components/logger/CMakeLists.txt deleted file mode 100755 index 5579473..0000000 --- a/components/logger/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -set(srcs - "src/logger.c" - "src/output_buffer.c" - ) - -idf_component_register(SRCS "${srcs}" - INCLUDE_DIRS "include") diff --git a/components/logger/include/logger.h b/components/logger/include/logger.h deleted file mode 100755 index 7204011..0000000 --- a/components/logger/include/logger.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef LOGGER_H_ -#define LOGGER_H_ - -#include -#include -#include - -#include "freertos/FreeRTOS.h" -#include "freertos/event_groups.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -#define LOGGER_SERIAL_BIT BIT0 - - extern EventGroupHandle_t logger_event_group; - - void logger_init(void); - - void logger_print(const char *str); - - int logger_vprintf(const char *fmt, va_list args); - - uint16_t logger_count(void); - - // opcional: quantas mensagens foram dropadas por contenção de mutex - uint32_t logger_dropped_count(void); - - /** - * ⚠️ API antiga (não recomendada): devolve ponteiro interno. - * Pode ficar inválido se houver novas escritas/rotação. - */ - bool logger_read(uint16_t *index, char **str, uint16_t *len); - - /** - * ✅ API recomendada: copia a entrada para buffer do caller (safe). - * out é sempre terminado com '\0' (se out_sz > 0). - */ - bool logger_read_copy(uint16_t *index, char *out, uint16_t out_sz, uint16_t *out_len); - -#ifdef __cplusplus -} -#endif - -#endif /* LOGGER_H_ */ diff --git a/components/logger/include/output_buffer.h b/components/logger/include/output_buffer.h deleted file mode 100755 index 29bdca5..0000000 --- a/components/logger/include/output_buffer.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OUTPUT_BUFFER_H_ -#define OUTPUT_BUFFER_H_ - -#include -#include - -#ifdef __cplusplus -extern "C" -{ -#endif - - typedef struct - { - uint16_t size; - uint16_t count; - uint8_t *data; - uint8_t *append; - } output_buffer_t; - - output_buffer_t *output_buffer_create(uint16_t size); - - void output_buffer_delete(output_buffer_t *buffer); - - void output_buffer_append_buf(output_buffer_t *buffer, const char *buf, uint16_t len); - - void output_buffer_append_str(output_buffer_t *buffer, const char *str); - - bool output_buffer_read(output_buffer_t *buffer, uint16_t *index, char **str, uint16_t *len); - -#ifdef __cplusplus -} -#endif - -#endif /* OUTPUT_BUFFER_H_ */ diff --git a/components/logger/src/logger.c b/components/logger/src/logger.c deleted file mode 100755 index 02b22b8..0000000 --- a/components/logger/src/logger.c +++ /dev/null @@ -1,192 +0,0 @@ -#include -#include -#include -#include -#include - -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" - -#include "logger.h" -#include "output_buffer.h" - -#define LOG_BUFFER_SIZE 6096 // tamanho total do buffer circular -#define MAX_LOG_SIZE 256 // ✅ reduzir stack/CPU; era 512 - -static SemaphoreHandle_t mutex = NULL; -static output_buffer_t *buffer = NULL; - -EventGroupHandle_t logger_event_group = NULL; - -// opcional: contador de mensagens dropadas quando mutex está ocupado -static volatile uint32_t s_dropped = 0; - -void logger_init(void) -{ - // Permitir múltiplas chamadas seguras - if (mutex != NULL) - { - return; - } - - mutex = xSemaphoreCreateMutex(); - configASSERT(mutex != NULL); - - logger_event_group = xEventGroupCreate(); - configASSERT(logger_event_group != NULL); - - buffer = output_buffer_create(LOG_BUFFER_SIZE); - configASSERT(buffer != NULL); -} - -uint16_t logger_count(void) -{ - if (!mutex || !buffer) - { - return 0; - } - - // ✅ não bloquear para sempre (mas aqui pode bloquear sem stress) - xSemaphoreTake(mutex, portMAX_DELAY); - uint16_t c = buffer->count; - xSemaphoreGive(mutex); - - return c; -} - -uint32_t logger_dropped_count(void) -{ - return s_dropped; -} - -void logger_print(const char *str) -{ - if (!str || !mutex || !buffer) - { - return; - } - - // Limitar comprimento para evitar entradas enormes - size_t len = strlen(str); - if (len > (MAX_LOG_SIZE - 1)) - { - len = MAX_LOG_SIZE - 1; - } - - // ✅ NÃO bloquear aqui: se o mutex estiver ocupado, dropa - if (xSemaphoreTake(mutex, 0) != pdTRUE) - { - s_dropped++; - return; - } - - output_buffer_append_buf(buffer, str, (uint16_t)len); - xEventGroupSetBits(logger_event_group, LOGGER_SERIAL_BIT); - - xSemaphoreGive(mutex); -} - -int logger_vprintf(const char *fmt, va_list args) -{ - char log_buf[MAX_LOG_SIZE]; - -#ifdef CONFIG_ESP_CONSOLE_UART - // Duplicar va_list para ecoar na UART sem consumir o original - va_list args_copy; - va_copy(args_copy, args); - vprintf(fmt, args_copy); - va_end(args_copy); -#endif - - // Se ainda não inicializado, apenas formatar para devolver comprimento - if (!mutex || !buffer) - { - int len = vsnprintf(log_buf, MAX_LOG_SIZE, fmt, args); - if (len < 0) - { - len = 0; - } - else if (len >= MAX_LOG_SIZE) - { - len = MAX_LOG_SIZE - 1; - } - return len; - } - - int len = vsnprintf(log_buf, MAX_LOG_SIZE, fmt, args); - if (len < 0) - { - len = 0; - } - else if (len >= MAX_LOG_SIZE) - { - len = MAX_LOG_SIZE - 1; - } - - // ✅ NÃO bloquear o sistema (sys_evt/httpd/wifi/etc) por causa de log - if (xSemaphoreTake(mutex, 0) != pdTRUE) - { - s_dropped++; - return len; - } - - output_buffer_append_buf(buffer, log_buf, (uint16_t)len); - xEventGroupSetBits(logger_event_group, LOGGER_SERIAL_BIT); - - xSemaphoreGive(mutex); - - return len; -} - -/** - * ⚠️ API antiga: devolve ponteiro interno do buffer. - * Só é segura se o caller COPIAR imediatamente e garantir que não há novas escritas. - * Recomendo usar logger_read_copy(). - */ -bool logger_read(uint16_t *index, char **str, uint16_t *len) -{ - if (!mutex || !buffer || !index || !str || !len) - { - return false; - } - - xSemaphoreTake(mutex, portMAX_DELAY); - - bool has_next = output_buffer_read(buffer, index, str, len); - - xSemaphoreGive(mutex); - - return has_next; -} - -// ✅ API segura: copia a linha para buffer do caller (evita ponteiro ficar inválido após rotação) -bool logger_read_copy(uint16_t *index, char *out, uint16_t out_sz, uint16_t *out_len) -{ - if (!mutex || !buffer || !index || !out || out_sz == 0) - { - return false; - } - - xSemaphoreTake(mutex, portMAX_DELAY); - - char *ptr = NULL; - uint16_t len = 0; - bool ok = output_buffer_read(buffer, index, &ptr, &len); - if (!ok) - { - xSemaphoreGive(mutex); - return false; - } - - uint16_t n = (len < (out_sz - 1)) ? len : (out_sz - 1); - memcpy(out, ptr, n); - out[n] = '\0'; - - if (out_len) - { - *out_len = n; - } - - xSemaphoreGive(mutex); - return true; -} diff --git a/components/logger/src/output_buffer.c b/components/logger/src/output_buffer.c deleted file mode 100755 index 9a43d71..0000000 --- a/components/logger/src/output_buffer.c +++ /dev/null @@ -1,217 +0,0 @@ -#include -#include - -#include "output_buffer.h" - -output_buffer_t *output_buffer_create(uint16_t size) -{ - if (size == 0) - { - return NULL; - } - - output_buffer_t *buffer = (output_buffer_t *)malloc(sizeof(output_buffer_t)); - if (!buffer) - { - return NULL; - } - - buffer->data = (uint8_t *)malloc((size_t)size); - if (!buffer->data) - { - free(buffer); - return NULL; - } - - buffer->size = size; - buffer->count = 0; - buffer->append = buffer->data; - - return buffer; -} - -void output_buffer_delete(output_buffer_t *buffer) -{ - if (!buffer) - { - return; - } - - if (buffer->data) - { - free((void *)buffer->data); - buffer->data = NULL; - } - - free((void *)buffer); -} - -void output_buffer_append_buf(output_buffer_t *buffer, const char *str, uint16_t len) -{ - if (!buffer || !buffer->data || !str || len == 0) - { - return; - } - - // Garantir que nunca escrevemos entradas absurdamente grandes - if (len > buffer->size / 2) - { - // Tamanho de entrada demasiado grande para a lógica de rotação; - // corta-a para caber de forma segura. - len = buffer->size / 2; - } - - size_t used = (size_t)(buffer->append - buffer->data); - if (used > buffer->size) - { - // Estado incoerente: reset defensivo - buffer->append = buffer->data; - buffer->count = 0; - used = 0; - } - - // Se não couber mais esta entrada, rodar o buffer - if (used + sizeof(uint16_t) + len > buffer->size) - { - uint8_t *pos = buffer->data; - uint16_t rotate_count = 0; - uint8_t *end = buffer->data + buffer->size; - - // Avança entradas até aproximadamente metade do buffer - while ((pos + sizeof(uint16_t)) < end && - (size_t)(pos - buffer->data) < buffer->size / 2) - { - - uint16_t entry_len; - memcpy(&entry_len, pos, sizeof(uint16_t)); - - // Sanitizar entry_len para evitar corrupções - if (entry_len == 0 || entry_len > buffer->size) - { - // Corrompido → reset defensivo - pos = buffer->data; - rotate_count = 0; - buffer->count = 0; - break; - } - - if (pos + sizeof(uint16_t) + entry_len > end) - { - // Entrada incompleta na cauda → para por aqui - break; - } - - pos += sizeof(uint16_t) + entry_len; - rotate_count++; - } - - // Compacta o que sobrou para o início - size_t remaining = (size_t)(end - pos); - memmove(buffer->data, pos, remaining); - - buffer->count = (buffer->count >= rotate_count) ? (buffer->count - rotate_count) : 0; - buffer->append = buffer->data + remaining; - - used = (size_t)(buffer->append - buffer->data); - } - - // Escreve [len][dados] - memcpy(buffer->append, &len, sizeof(uint16_t)); - buffer->append += sizeof(uint16_t); - - memcpy(buffer->append, str, len); - buffer->append += len; - - buffer->count++; -} - -void output_buffer_append_str(output_buffer_t *buffer, const char *str) -{ - if (!buffer || !str) - { - return; - } - - size_t len = strlen(str); - if (len == 0) - { - return; - } - - // A API pública em logger.c já limita o tamanho, mas aqui fazemos - // um clamp defensivo para o caso de uso direto. - if (len > UINT16_MAX) - { - len = UINT16_MAX; - } - - output_buffer_append_buf(buffer, str, (uint16_t)len); -} - -bool output_buffer_read(output_buffer_t *buffer, uint16_t *index, char **str, uint16_t *len) -{ - if (!buffer || !buffer->data || !index || !str || !len) - { - return false; - } - - if (*index > buffer->count) - { - *index = buffer->count; - } - - if (*index >= buffer->count) - { - return false; - } - - uint8_t *pos = buffer->data; - uint8_t *end = buffer->data + buffer->size; - uint16_t current = 0; - - // Avança até à entrada [*index] - while (current < *index) - { - if (pos + sizeof(uint16_t) > end) - { - // Dados corrompidos ou índice fora → fail-safe - return false; - } - - uint16_t entry_len; - memcpy(&entry_len, pos, sizeof(uint16_t)); - - if (entry_len == 0 || entry_len > buffer->size) - { - // Corrompido → aborta - return false; - } - - if (pos + sizeof(uint16_t) + entry_len > end) - { - return false; - } - - pos += sizeof(uint16_t) + entry_len; - current++; - } - - // Agora pos aponta para o len da entrada desejada - if (pos + sizeof(uint16_t) > end) - { - return false; - } - - memcpy(len, pos, sizeof(uint16_t)); - pos += sizeof(uint16_t); - - if (pos + *len > end) - { - return false; - } - - *str = (char *)pos; - (*index)++; - - return true; -} diff --git a/components/meter_manager/CMakeLists.txt b/components/meter_manager/CMakeLists.txt index 5bf131a..0260ea5 100755 --- a/components/meter_manager/CMakeLists.txt +++ b/components/meter_manager/CMakeLists.txt @@ -3,13 +3,14 @@ set(srcs driver/meter_ade7758/meter_ade7758.c driver/meter_ade7758/ade7758.c - driver/meter_orno/meter_orno513.c - driver/meter_orno/meter_orno526.c - driver/meter_orno/meter_orno516.c - driver/meter_orno/meter_dts6619.c - driver/meter_orno/meter_dds661.c - driver/meter_orno/meter_ea777.c - driver/meter_orno/modbus_params.c + driver/meter_modbus/meter_orno513.c + driver/meter_modbus/meter_orno526.c + driver/meter_modbus/meter_orno516.c + driver/meter_modbus/meter_dts6619.c + driver/meter_modbus/meter_dds661.c + driver/meter_modbus/meter_ea777.c + driver/meter_modbus/meter_dts024m.c + driver/meter_modbus/modbus_params.c driver/meter_zigbee/meter_zigbee.c src/meter_manager.c src/meter_events.c @@ -18,7 +19,7 @@ set(srcs set(includes include driver/meter_ade7758 - driver/meter_orno + driver/meter_modbus driver/meter_zigbee ) diff --git a/components/meter_manager/driver/meter_orno/meter_dds661.c b/components/meter_manager/driver/meter_modbus/meter_dds661.c similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_dds661.c rename to components/meter_manager/driver/meter_modbus/meter_dds661.c diff --git a/components/meter_manager/driver/meter_orno/meter_dds661.h b/components/meter_manager/driver/meter_modbus/meter_dds661.h similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_dds661.h rename to components/meter_manager/driver/meter_modbus/meter_dds661.h diff --git a/components/meter_manager/driver/meter_modbus/meter_dts024m.c b/components/meter_manager/driver/meter_modbus/meter_dts024m.c new file mode 100755 index 0000000..97d9fec --- /dev/null +++ b/components/meter_manager/driver/meter_modbus/meter_dts024m.c @@ -0,0 +1,542 @@ +// meter_dts024m.c — Driver Modbus RTU para DTS024M (ESP-IDF / esp-modbus) +// Versão PRODUÇÃO (SEM AUTO-PROBE): parâmetros fixos (baud/parity/id/FC/base). +// Ajusta os #defines DTS024M_PROD_* conforme o teu medidor. + +#include "meter_events.h" +#include "modbus_params.h" +#include "mbcontroller.h" +#include "esp_log.h" +#include "esp_err.h" +#include "driver/uart.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include +#include + +#include "meter_dts024m.h" + +#define TAG "serial_mdb_dts024m" + +// ===== UART / RS-485 ===== +#define MB_PORT_NUM 2 + +// Ajuste os pinos conforme seu hardware +#define MB_UART_TXD 17 +#define MB_UART_RXD 16 +#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 + +// ===== Timings ===== +#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS) +#define POLL_INTERVAL (200 / portTICK_PERIOD_MS) + +// ===== Helpers ===== +#define STR(fieldname) ((const char *)(fieldname)) +#define OPTS(min_val, max_val, step_val) {.opt1 = (min_val), .opt2 = (max_val), .opt3 = (step_val)} +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + +// ===== Config PRODUÇÃO (sem AUTO-PROBE) ===== +// Ajusta estes valores: +#define DTS024M_PROD_BAUD 2400 +#define DTS024M_PROD_PARITY UART_PARITY_DISABLE // 0 = none; UART_PARITY_EVEN se 8E1 +#define DTS024M_PROD_SLAVE_ID 1 // endereço Modbus (1..247) +#define DTS024M_PROD_AREA MB_PARAM_INPUT // MB_PARAM_INPUT (FC04) ou MB_PARAM_HOLDING (FC03) +#define DTS024M_PROD_BASE_OFFSET 0 // 0 ou 1 (depende se o mapa é 0-based ou 1-based) + +// ===== Estado ===== +static bool is_initialized = false; +static bool mb_started = false; +static TaskHandle_t meter_task = NULL; + +// ============================================================================ +// MAPA DE REGISTROS (template) — pode variar conforme firmware. +// Estes endereços são um “perfil” comum. +// ============================================================================ +#define DTS024M_L1_VOLTAGE 0x0000 // U32, 0.01 V (2 regs) +#define DTS024M_L2_VOLTAGE 0x0002 +#define DTS024M_L3_VOLTAGE 0x0004 + +#define DTS024M_L1_CURRENT 0x0006 // U32, 0.001 A (2 regs) +#define DTS024M_L2_CURRENT 0x0008 +#define DTS024M_L3_CURRENT 0x000A + +#define DTS024M_L1_ACTIVE_P 0x000C // I32 (two’s complement), (depende do modelo/escala) +#define DTS024M_L2_ACTIVE_P 0x000E +#define DTS024M_L3_ACTIVE_P 0x0010 + +#define DTS024M_PF_L1 0x001E // I16 (two’s complement), 0.001 +#define DTS024M_PF_L2 0x001F +#define DTS024M_PF_L3 0x0020 + +#define DTS024M_FREQUENCY 0x002A // U16, 0.01 Hz + +#define DTS024M_TOTAL_ACTIVE_E 0x0404 // U32, 0.01 kWh (2 regs) + +// ============================================================================ +// Conversões signed (two’s complement) — porque o projeto não tem PARAM_TYPE_I* +// ============================================================================ +static inline int32_t s32_from_u32(uint32_t x) +{ + return (x & 0x80000000u) ? (int32_t)(x - 0x100000000ULL) : (int32_t)x; +} + +static inline int16_t s16_from_u16(uint16_t x) +{ + return (x & 0x8000u) ? (int16_t)(x - 0x10000u) : (int16_t)x; +} + +// ============================================================================ +// CIDs +// ============================================================================ +enum +{ + CID_DTS024M_L1_VOLTAGE = 0, + CID_DTS024M_L2_VOLTAGE, + CID_DTS024M_L3_VOLTAGE, + + CID_DTS024M_L1_CURRENT, + CID_DTS024M_L2_CURRENT, + CID_DTS024M_L3_CURRENT, + + CID_DTS024M_L1_ACTIVE_P, + CID_DTS024M_L2_ACTIVE_P, + CID_DTS024M_L3_ACTIVE_P, + + CID_DTS024M_PF_L1, + CID_DTS024M_PF_L2, + CID_DTS024M_PF_L3, + + CID_DTS024M_FREQUENCY, + CID_DTS024M_TOTAL_ACTIVE_E, +}; + +// ============================================================================ +// DESCRIPTORS (TEMPLATE) — copiamos para RAM e ajustamos: +// - slave_id +// - base offset (0/1) +// - mb_param_type (HOLDING/INPUT) +// ============================================================================ +static const mb_parameter_descriptor_t device_parameters_dts024m_tmpl[] = { + + // Tensões (U32 / 2 regs) — 0.01 V + {CID_DTS024M_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1, + MB_PARAM_HOLDING, DTS024M_L1_VOLTAGE, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + {CID_DTS024M_L2_VOLTAGE, STR("L2 Voltage"), STR("V"), 1, + MB_PARAM_HOLDING, DTS024M_L2_VOLTAGE, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + {CID_DTS024M_L3_VOLTAGE, STR("L3 Voltage"), STR("V"), 1, + MB_PARAM_HOLDING, DTS024M_L3_VOLTAGE, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + // Correntes (U32 / 2 regs) — 0.001 A + {CID_DTS024M_L1_CURRENT, STR("L1 Current"), STR("A"), 1, + MB_PARAM_HOLDING, DTS024M_L1_CURRENT, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + {CID_DTS024M_L2_CURRENT, STR("L2 Current"), STR("A"), 1, + MB_PARAM_HOLDING, DTS024M_L2_CURRENT, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + {CID_DTS024M_L3_CURRENT, STR("L3 Current"), STR("A"), 1, + MB_PARAM_HOLDING, DTS024M_L3_CURRENT, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + // Potência ativa por fase (U32 / 2 regs no descriptor; interpretamos como signed I32) + {CID_DTS024M_L1_ACTIVE_P, STR("L1 Active Power"), STR("W"), 1, + MB_PARAM_HOLDING, DTS024M_L1_ACTIVE_P, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + {CID_DTS024M_L2_ACTIVE_P, STR("L2 Active Power"), STR("W"), 1, + MB_PARAM_HOLDING, DTS024M_L2_ACTIVE_P, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + {CID_DTS024M_L3_ACTIVE_P, STR("L3 Active Power"), STR("W"), 1, + MB_PARAM_HOLDING, DTS024M_L3_ACTIVE_P, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, + + // PF (U16 / 1 reg; interpretamos como signed I16) — 0.001 + {CID_DTS024M_PF_L1, STR("L1 PF"), STR(""), 1, + MB_PARAM_HOLDING, DTS024M_PF_L1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ}, + + {CID_DTS024M_PF_L2, STR("L2 PF"), STR(""), 1, + MB_PARAM_HOLDING, DTS024M_PF_L2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ}, + + {CID_DTS024M_PF_L3, STR("L3 PF"), STR(""), 1, + MB_PARAM_HOLDING, DTS024M_PF_L3, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ}, + + // Frequência (U16 / 1 reg) — 0.01 Hz + {CID_DTS024M_FREQUENCY, STR("Frequency"), STR("Hz"), 1, + MB_PARAM_HOLDING, DTS024M_FREQUENCY, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ}, + + // Energia ativa total (U32 / 2 regs) — 0.01 kWh + {CID_DTS024M_TOTAL_ACTIVE_E, STR("Total Active Energy"), STR("kWh"), 1, + MB_PARAM_HOLDING, DTS024M_TOTAL_ACTIVE_E, 2, + 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, +}; + +static mb_parameter_descriptor_t device_parameters_dts024m[ARRAY_SIZE(device_parameters_dts024m_tmpl)]; +static const uint16_t num_device_parameters_dts024m = ARRAY_SIZE(device_parameters_dts024m); + +static void dts024m_build_descriptors(uint8_t slave_id, uint16_t base_offset, mb_param_type_t area) +{ + memcpy(device_parameters_dts024m, + device_parameters_dts024m_tmpl, + sizeof(device_parameters_dts024m)); + + for (uint16_t i = 0; i < num_device_parameters_dts024m; ++i) + { + device_parameters_dts024m[i].mb_slave_addr = slave_id; + device_parameters_dts024m[i].mb_reg_start = + (uint16_t)(device_parameters_dts024m[i].mb_reg_start + base_offset); + device_parameters_dts024m[i].mb_param_type = area; // HOLDING (FC03) ou INPUT (FC04) + } +} + +// ============================================================================ +// Modbus master init (fixo) — garante ordem correta (start -> uart_set_mode) +// ============================================================================ +static esp_err_t dts024m_master_reinit(uint32_t baud, uart_parity_t parity) +{ + if (mb_started) + { + (void)mbc_master_destroy(); + mb_started = false; + } + + if (uart_is_driver_installed(MB_PORT_NUM)) + { + uart_driver_delete(MB_PORT_NUM); + } + + mb_communication_info_t comm = { + .port = MB_PORT_NUM, + .mode = MB_MODE_RTU, + .baudrate = baud, + .parity = parity}; + + void *handler = NULL; + + esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler); + if (err != ESP_OK) + return err; + + err = mbc_master_setup(&comm); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + err = uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + // IMPORTANTE: start antes de uart_set_mode (driver UART costuma ser instalado no start) + err = mbc_master_start(); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + mb_started = true; + + err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + mb_started = false; + return err; + } + + vTaskDelay(pdMS_TO_TICKS(40)); + return ESP_OK; +} + +// ============================================================================ +// Post do evento de medição +// ============================================================================ +static void meter_dts024m_post_event(float *voltage, float *current, int *power_w, + float freq_hz, float pf_avg, float total_kwh) +{ + meter_event_data_t evt = { + .source = "GRID", + .frequency = freq_hz, + .power_factor = pf_avg, + .total_energy = total_kwh}; + + memcpy(evt.vrms, voltage, sizeof(evt.vrms)); + memcpy(evt.irms, current, sizeof(evt.irms)); + memcpy(evt.watt, power_w, sizeof(evt.watt)); + + esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, + &evt, sizeof(evt), portMAX_DELAY); + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); + } +} + +// ============================================================================ +// Task de polling +// ============================================================================ +static void serial_mdb_dts024m_task(void *param) +{ + (void)param; + + esp_err_t err; + const mb_parameter_descriptor_t *desc = NULL; + + float v[3] = {0}; + float i[3] = {0}; + float pf[3] = {0}; + float freq = 0.0f; + float total_kwh = 0.0f; + + int p_w[3] = {0}; + + vTaskDelay(pdMS_TO_TICKS(200)); // settle + + while (1) + { + for (uint16_t cid = 0; cid < num_device_parameters_dts024m; cid++) + { + err = mbc_master_get_cid_info(cid, &desc); + if (err != ESP_OK || !desc) + { + continue; + } + + uint8_t type = 0; + uint16_t raw_u16 = 0; + uint32_t raw_u32 = 0; + + void *value_ptr = &raw_u16; + + // U32 + switch (cid) + { + case CID_DTS024M_L1_VOLTAGE: + case CID_DTS024M_L2_VOLTAGE: + case CID_DTS024M_L3_VOLTAGE: + case CID_DTS024M_L1_CURRENT: + case CID_DTS024M_L2_CURRENT: + case CID_DTS024M_L3_CURRENT: + case CID_DTS024M_L1_ACTIVE_P: + case CID_DTS024M_L2_ACTIVE_P: + case CID_DTS024M_L3_ACTIVE_P: + case CID_DTS024M_TOTAL_ACTIVE_E: + value_ptr = &raw_u32; + break; + default: + value_ptr = &raw_u16; + break; + } + + // 1 retry simples em caso de timeout (podes remover se quiseres menos carga) + err = mbc_master_get_parameter(cid, + (char *)desc->param_key, + (uint8_t *)value_ptr, + &type); + if (err == ESP_ERR_TIMEOUT) + { + vTaskDelay(pdMS_TO_TICKS(60)); + err = mbc_master_get_parameter(cid, + (char *)desc->param_key, + (uint8_t *)value_ptr, + &type); + } + + if (err == ESP_OK) + { + switch (cid) + { + // V (0.01V) + case CID_DTS024M_L1_VOLTAGE: + v[0] = ((float)raw_u32) * 0.01f; + break; + case CID_DTS024M_L2_VOLTAGE: + v[1] = ((float)raw_u32) * 0.01f; + break; + case CID_DTS024M_L3_VOLTAGE: + v[2] = ((float)raw_u32) * 0.01f; + break; + + // I (0.001A) + case CID_DTS024M_L1_CURRENT: + i[0] = ((float)raw_u32) * 0.001f; + break; + case CID_DTS024M_L2_CURRENT: + i[1] = ((float)raw_u32) * 0.001f; + break; + case CID_DTS024M_L3_CURRENT: + i[2] = ((float)raw_u32) * 0.001f; + break; + + // P ativa (two’s complement I32) — atenção: escala depende do modelo + case CID_DTS024M_L1_ACTIVE_P: + p_w[0] = (int)s32_from_u32(raw_u32); + break; + case CID_DTS024M_L2_ACTIVE_P: + p_w[1] = (int)s32_from_u32(raw_u32); + break; + case CID_DTS024M_L3_ACTIVE_P: + p_w[2] = (int)s32_from_u32(raw_u32); + break; + + // PF (two’s complement I16; 0.001) + case CID_DTS024M_PF_L1: + pf[0] = ((float)s16_from_u16(raw_u16)) * 0.001f; + break; + case CID_DTS024M_PF_L2: + pf[1] = ((float)s16_from_u16(raw_u16)) * 0.001f; + break; + case CID_DTS024M_PF_L3: + pf[2] = ((float)s16_from_u16(raw_u16)) * 0.001f; + break; + + // Freq (0.01Hz) + case CID_DTS024M_FREQUENCY: + freq = ((float)raw_u16) * 0.01f; + break; + + // Energia (0.01kWh) + case CID_DTS024M_TOTAL_ACTIVE_E: + total_kwh = ((float)raw_u32) * 0.01f; + break; + + default: + break; + } + + ESP_LOGD(TAG, "%s (cid=%u) ok (u16=%u u32=%u)", + desc->param_key, cid, (unsigned)raw_u16, (unsigned)raw_u32); + } + else + { + ESP_LOGE(TAG, "CID %u (%s) read failed: %s", + cid, desc->param_key, esp_err_to_name(err)); + } + + vTaskDelay(POLL_INTERVAL); + } + + // PF médio simples (ignora zeros) + float pf_sum = 0.0f; + int pf_cnt = 0; + for (int k = 0; k < 3; ++k) + { + if (pf[k] != 0.0f) + { + pf_sum += pf[k]; + pf_cnt++; + } + } + float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f); + + meter_dts024m_post_event(v, i, p_w, freq, pf_avg, total_kwh); + vTaskDelay(UPDATE_INTERVAL); + } +} + +// ============================================================================ +// Init / Start / Stop +// ============================================================================ +esp_err_t meter_dts024m_init(void) +{ + if (is_initialized) + { + ESP_LOGW(TAG, "Already initialized"); + return ESP_ERR_INVALID_STATE; + } + + // init fixo (produção) + esp_err_t err = dts024m_master_reinit(DTS024M_PROD_BAUD, DTS024M_PROD_PARITY); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "master_reinit failed: %s", esp_err_to_name(err)); + return err; + } + + // monta descriptors reais com ID/offset/area fixos + dts024m_build_descriptors(DTS024M_PROD_SLAVE_ID, DTS024M_PROD_BASE_OFFSET, DTS024M_PROD_AREA); + + // aplica descriptors reais + esp_err_t derr = mbc_master_set_descriptor(device_parameters_dts024m, + num_device_parameters_dts024m); + if (derr != ESP_OK) + { + ESP_LOGE(TAG, "set_descriptor failed: %s", esp_err_to_name(derr)); + return derr; + } + + is_initialized = true; + ESP_LOGI(TAG, "DTS024M initialized (PROD) baud=%d parity=%d id=%d area=%s base=%d", + DTS024M_PROD_BAUD, + (int)DTS024M_PROD_PARITY, + DTS024M_PROD_SLAVE_ID, + (DTS024M_PROD_AREA == MB_PARAM_HOLDING ? "FC03" : "FC04"), + DTS024M_PROD_BASE_OFFSET); + + return ESP_OK; +} + +esp_err_t meter_dts024m_start(void) +{ + if (!is_initialized) + { + ESP_LOGE(TAG, "Not initialized"); + return ESP_ERR_INVALID_STATE; + } + + if (meter_task == NULL) + { + xTaskCreate(serial_mdb_dts024m_task, + "meter_dts024m_task", + 4096, NULL, 3, &meter_task); + ESP_LOGI(TAG, "DTS024M task started"); + } + + return ESP_OK; +} + +void meter_dts024m_stop(void) +{ + if (!is_initialized) + { + ESP_LOGW(TAG, "Not initialized, skipping stop"); + return; + } + + if (meter_task) + { + vTaskDelete(meter_task); + meter_task = NULL; + ESP_LOGI(TAG, "DTS024M task stopped"); + } + + if (mb_started) + { + (void)mbc_master_destroy(); + mb_started = false; + } + + if (uart_is_driver_installed(MB_PORT_NUM)) + { + uart_driver_delete(MB_PORT_NUM); + ESP_LOGI(TAG, "UART driver deleted"); + } + + is_initialized = false; + ESP_LOGI(TAG, "Meter DTS024M cleaned up"); +} diff --git a/components/meter_manager/driver/meter_modbus/meter_dts024m.h b/components/meter_manager/driver/meter_modbus/meter_dts024m.h new file mode 100755 index 0000000..c1e4c03 --- /dev/null +++ b/components/meter_manager/driver/meter_modbus/meter_dts024m.h @@ -0,0 +1,35 @@ +#ifndef METER_DTS024M_H_ +#define METER_DTS024M_H_ + +#include +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Inicializa o driver do medidor DTS024M (UART RS485, Modbus, registradores). + * + * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. + */ +esp_err_t meter_dts024m_init(void); + +/** + * @brief Inicia a tarefa de leitura de dados do medidor DTS024M. + * + * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. + */ +esp_err_t meter_dts024m_start(void); + +/** + * @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS024M. + */ +void meter_dts024m_stop(void); + +#ifdef __cplusplus +} +#endif + +#endif /* METER_DTS024M_H_ */ diff --git a/components/meter_manager/driver/meter_orno/meter_dts6619.c b/components/meter_manager/driver/meter_modbus/meter_dts6619.c similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_dts6619.c rename to components/meter_manager/driver/meter_modbus/meter_dts6619.c diff --git a/components/meter_manager/driver/meter_orno/meter_dts6619.h b/components/meter_manager/driver/meter_modbus/meter_dts6619.h similarity index 98% rename from components/meter_manager/driver/meter_orno/meter_dts6619.h rename to components/meter_manager/driver/meter_modbus/meter_dts6619.h index a843a1c..70b9075 100755 --- a/components/meter_manager/driver/meter_orno/meter_dts6619.h +++ b/components/meter_manager/driver/meter_modbus/meter_dts6619.h @@ -7,14 +7,14 @@ /** * @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores). - * + * * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. */ esp_err_t meter_dts6619_init(void); /** * @brief Inicia a tarefa de leitura de dados do medidor DTS6619. - * + * * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. */ esp_err_t meter_dts6619_start(void); @@ -24,7 +24,6 @@ esp_err_t meter_dts6619_start(void); */ void meter_dts6619_stop(void); - #ifdef __cplusplus } #endif diff --git a/components/meter_manager/driver/meter_orno/meter_ea777.c b/components/meter_manager/driver/meter_modbus/meter_ea777.c similarity index 97% rename from components/meter_manager/driver/meter_orno/meter_ea777.c rename to components/meter_manager/driver/meter_modbus/meter_ea777.c index e353733..54312b8 100755 --- a/components/meter_manager/driver/meter_orno/meter_ea777.c +++ b/components/meter_manager/driver/meter_modbus/meter_ea777.c @@ -12,13 +12,13 @@ #define TAG "serial_mdb_ea777" // ===== UART / RS-485 ===== -#define MB_PORT_NUM 2 +#define MB_PORT_NUM 1 #define MB_DEV_SPEED 9600 // Ajuste os pinos conforme seu hardware -#define MB_UART_TXD 17 -#define MB_UART_RXD 16 -#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 +#define MB_UART_TXD 21 +#define MB_UART_RXD 22 +#define MB_UART_RTS UART_PIN_NO_CHANGE // sem DE/RE // ===== Timings ===== #define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS) @@ -322,9 +322,10 @@ esp_err_t meter_ea777_init(void) ESP_ERROR_CHECK(mbc_master_setup(&comm)); ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, - MB_UART_RTS, UART_PIN_NO_CHANGE)); + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); ESP_ERROR_CHECK(mbc_master_start()); - ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); + ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART)); + // ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART)); vTaskDelay(pdMS_TO_TICKS(50)); ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777, diff --git a/components/meter_manager/driver/meter_orno/meter_ea777.h b/components/meter_manager/driver/meter_modbus/meter_ea777.h similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_ea777.h rename to components/meter_manager/driver/meter_modbus/meter_ea777.h diff --git a/components/meter_manager/driver/meter_orno/meter_orno.h b/components/meter_manager/driver/meter_modbus/meter_orno.h similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno.h rename to components/meter_manager/driver/meter_modbus/meter_orno.h diff --git a/components/meter_manager/driver/meter_orno/meter_orno513.c b/components/meter_manager/driver/meter_modbus/meter_orno513.c similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno513.c rename to components/meter_manager/driver/meter_modbus/meter_orno513.c diff --git a/components/meter_manager/driver/meter_orno/meter_orno513.h b/components/meter_manager/driver/meter_modbus/meter_orno513.h similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno513.h rename to components/meter_manager/driver/meter_modbus/meter_orno513.h diff --git a/components/meter_manager/driver/meter_orno/meter_orno516.c b/components/meter_manager/driver/meter_modbus/meter_orno516.c similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno516.c rename to components/meter_manager/driver/meter_modbus/meter_orno516.c diff --git a/components/meter_manager/driver/meter_orno/meter_orno516.h b/components/meter_manager/driver/meter_modbus/meter_orno516.h similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno516.h rename to components/meter_manager/driver/meter_modbus/meter_orno516.h diff --git a/components/meter_manager/driver/meter_orno/meter_orno526.c b/components/meter_manager/driver/meter_modbus/meter_orno526.c similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno526.c rename to components/meter_manager/driver/meter_modbus/meter_orno526.c diff --git a/components/meter_manager/driver/meter_orno/meter_orno526.h b/components/meter_manager/driver/meter_modbus/meter_orno526.h similarity index 100% rename from components/meter_manager/driver/meter_orno/meter_orno526.h rename to components/meter_manager/driver/meter_modbus/meter_orno526.h diff --git a/components/meter_manager/driver/meter_orno/modbus_params.c b/components/meter_manager/driver/meter_modbus/modbus_params.c similarity index 100% rename from components/meter_manager/driver/meter_orno/modbus_params.c rename to components/meter_manager/driver/meter_modbus/modbus_params.c diff --git a/components/meter_manager/driver/meter_orno/modbus_params.h b/components/meter_manager/driver/meter_modbus/modbus_params.h similarity index 100% rename from components/meter_manager/driver/meter_orno/modbus_params.h rename to components/meter_manager/driver/meter_modbus/modbus_params.h diff --git a/components/meter_manager/driver/meter_zigbee/meter_zigbee.c b/components/meter_manager/driver/meter_zigbee/meter_zigbee.c index 812dbd2..6e76dc4 100755 --- a/components/meter_manager/driver/meter_zigbee/meter_zigbee.c +++ b/components/meter_manager/driver/meter_zigbee/meter_zigbee.c @@ -12,36 +12,37 @@ #define TAG "meter_zigbee" // UART config -#define UART_PORT UART_NUM_2 -#define TXD_PIN GPIO_NUM_17 -#define RXD_PIN GPIO_NUM_16 -#define UART_BUF_SIZE 128 -#define RX_FRAME_SIZE 14 +#define UART_PORT UART_NUM_2 +#define TXD_PIN GPIO_NUM_17 +#define RXD_PIN GPIO_NUM_16 +#define UART_BUF_SIZE 128 +#define RX_FRAME_SIZE 14 // Zigbee Attribute IDs -#define ATTR_CURRENT_L1 0x0006 -#define ATTR_CURRENT_L2 0x0007 -#define ATTR_CURRENT_L3 0x0008 -#define ATTR_VOLTAGE_L1 0x0266 -#define ATTR_CURRENT_L1_ALT 0x0267 -#define ATTR_POWER_L1 0x0268 -#define ATTR_VOLTAGE_L2 0x0269 -#define ATTR_CURRENT_L2_ALT 0x026A -#define ATTR_POWER_L2 0x026B -#define ATTR_VOLTAGE_L3 0x026C -#define ATTR_CURRENT_L3_ALT 0x026D -#define ATTR_POWER_L3 0x026E -#define ATTR_FREQUENCY 0x0265 -#define ATTR_POWER_FACTOR 0x020F -#define ATTR_TOTAL_ENERGY 0x0201 +#define ATTR_CURRENT_L1 0x0006 +#define ATTR_CURRENT_L2 0x0007 +#define ATTR_CURRENT_L3 0x0008 +#define ATTR_VOLTAGE_L1 0x0266 +#define ATTR_CURRENT_L1_ALT 0x0267 +#define ATTR_POWER_L1 0x0268 +#define ATTR_VOLTAGE_L2 0x0269 +#define ATTR_CURRENT_L2_ALT 0x026A +#define ATTR_POWER_L2 0x026B +#define ATTR_VOLTAGE_L3 0x026C +#define ATTR_CURRENT_L3_ALT 0x026D +#define ATTR_POWER_L3 0x026E +#define ATTR_FREQUENCY 0x0265 +#define ATTR_POWER_FACTOR 0x020F +#define ATTR_TOTAL_ENERGY 0x0201 -#define PHASE_COUNT 3 -#define PHASE_L1 0 -#define PHASE_L2 1 -#define PHASE_L3 2 +#define PHASE_COUNT 3 +#define PHASE_L1 0 +#define PHASE_L2 1 +#define PHASE_L3 2 // Internal meter state -typedef struct { +typedef struct +{ float vrms[PHASE_COUNT]; float irms[PHASE_COUNT]; int watt[PHASE_COUNT]; @@ -58,24 +59,28 @@ static meter_zigbee_data_t meter_data = {0}; static SemaphoreHandle_t meter_mutex = NULL; static TaskHandle_t meter_zigbee_task = NULL; - -bool meter_zigbee_is_running(void) { +bool meter_zigbee_is_running(void) +{ return meter_zigbee_task != NULL; } -void send_stop_command(void) { - //const char *cmd = "stop\n"; // Comando enviado para o outro lado interpretar e dormir - //uart_write_bytes(UART_PORT, cmd, strlen(cmd)); - //uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(100)); // Aguarda envio terminar +static inline int32_t tuya_power16_to_signed(uint16_t p) +{ + // Igual ao quirk multi_dp_to_power() + if (p > 0x7FFF) + { + return (int32_t)((0x999A - p) * -1); + } + return (int32_t)p; } -static void meter_zigbee_post_event(void) { +static void meter_zigbee_post_event(void) +{ meter_event_data_t evt = { .source = "GRID", .frequency = meter_data.frequency, .power_factor = meter_data.power_factor, - .total_energy = meter_data.total_energy - }; + .total_energy = meter_data.total_energy}; memcpy(evt.vrms, meter_data.vrms, sizeof(evt.vrms)); memcpy(evt.irms, meter_data.irms, sizeof(evt.irms)); @@ -87,17 +92,19 @@ static void meter_zigbee_post_event(void) { sizeof(evt), portMAX_DELAY); - if (err != ESP_OK) { + if (err != ESP_OK) + { ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); } } - -static void handle_zigbee_frame(const uint8_t *buf, size_t len) { +static void handle_zigbee_frame(const uint8_t *buf, size_t len) +{ ESP_LOGD(TAG, "Received UART frame (%d bytes):", len); - //ESP_LOG_BUFFER_HEX(TAG, buf, len); + // ESP_LOG_BUFFER_HEX(TAG, buf, len); - if (len < RX_FRAME_SIZE) { + if (len < RX_FRAME_SIZE) + { ESP_LOGW(TAG, "Invalid frame: too short (len = %d)", len); return; } @@ -105,70 +112,85 @@ static void handle_zigbee_frame(const uint8_t *buf, size_t len) { uint16_t attr = buf[2] | (buf[3] << 8); uint8_t size = buf[5]; - if (size != 8) { + if (size != 8) + { ESP_LOGW(TAG, "Unsupported payload size: %d", size); return; } - uint16_t volt_raw = (buf[6] << 8) | buf[7]; - uint32_t current_raw = (buf[8] << 16) | (buf[9] << 8) | buf[10]; - uint32_t power_raw = (buf[11] << 16) | (buf[12] << 8) | buf[13]; + // payload 8 bytes começa em buf[6] + const uint8_t *p = &buf[6]; - float volt = volt_raw / 10.0f; - float current = current_raw / 1000.0f; - float power = power_raw; + uint16_t volt_raw = ((uint16_t)p[0] << 8) | p[1]; - ESP_LOGD(TAG, "Parsed Attr 0x%04X: V=%.1fV I=%.2fA P=%.1fW", attr, volt, current, power); + uint16_t curr_raw_u16 = ((uint16_t)p[3] << 8) | p[4]; // 2 bytes + uint16_t pow_raw_u16 = ((uint16_t)p[6] << 8) | p[7]; // 2 bytes - if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE) { - switch (attr) { - case ATTR_CURRENT_L1: - case ATTR_CURRENT_L1_ALT: - meter_data.irms[PHASE_L1] = current; - meter_data.vrms[PHASE_L1] = volt; - meter_data.watt[PHASE_L1] = (int)power; - phase_updated[PHASE_L1] = true; - break; - case ATTR_CURRENT_L2: - case ATTR_CURRENT_L2_ALT: - meter_data.irms[PHASE_L2] = current; - meter_data.vrms[PHASE_L2] = volt; - meter_data.watt[PHASE_L2] = (int)power; - phase_updated[PHASE_L2] = true; - break; - case ATTR_CURRENT_L3: - case ATTR_CURRENT_L3_ALT: - meter_data.irms[PHASE_L3] = current; - meter_data.vrms[PHASE_L3] = volt; - meter_data.watt[PHASE_L3] = (int)power; - phase_updated[PHASE_L3] = true; - break; - case ATTR_POWER_FACTOR: - meter_data.power_factor = 0; - break; - case ATTR_FREQUENCY: - meter_data.frequency = 0; - break; - case ATTR_TOTAL_ENERGY: - meter_data.total_energy = 0; - break; - default: - ESP_LOGW(TAG, "Unknown attr: 0x%04X", attr); - break; + int32_t power = tuya_power16_to_signed(pow_raw_u16); + + float volt = volt_raw / 10.0f; + float curr = curr_raw_u16 / 1000.0f; + + // Se queres “corrente com sinal”, deriva pelo sinal da potência: + float current = (power < 0) ? -curr : curr; + + ESP_LOGD(TAG, "Attr 0x%04X: V=%.1fV I=%.3fA (signed=%+.3fA) P=%+ldW", + attr, volt, curr, current, (long)power); + + if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE) + { + switch (attr) + { + case ATTR_CURRENT_L1: + case ATTR_CURRENT_L1_ALT: + meter_data.irms[PHASE_L1] = current; + meter_data.vrms[PHASE_L1] = volt; + meter_data.watt[PHASE_L1] = (int)power; + phase_updated[PHASE_L1] = true; + break; + case ATTR_CURRENT_L2: + case ATTR_CURRENT_L2_ALT: + meter_data.irms[PHASE_L2] = current; + meter_data.vrms[PHASE_L2] = volt; + meter_data.watt[PHASE_L2] = (int)power; + phase_updated[PHASE_L2] = true; + break; + case ATTR_CURRENT_L3: + case ATTR_CURRENT_L3_ALT: + meter_data.irms[PHASE_L3] = current; + meter_data.vrms[PHASE_L3] = volt; + meter_data.watt[PHASE_L3] = (int)power; + phase_updated[PHASE_L3] = true; + break; + case ATTR_POWER_FACTOR: + meter_data.power_factor = 0; + break; + case ATTR_FREQUENCY: + meter_data.frequency = 0; + break; + case ATTR_TOTAL_ENERGY: + meter_data.total_energy = 0; + break; + default: + ESP_LOGW(TAG, "Unknown attr: 0x%04X", attr); + break; } xSemaphoreGive(meter_mutex); } // Verifica se todas as 3 fases foram atualizadas - if (phase_updated[PHASE_L1] && phase_updated[PHASE_L2] && phase_updated[PHASE_L3]) { + if (phase_updated[PHASE_L1] && phase_updated[PHASE_L2] && phase_updated[PHASE_L3]) + { meter_zigbee_post_event(); memset(phase_updated, 0, sizeof(phase_updated)); } } -static void meter_zigbee_task_func(void *param) { +static void meter_zigbee_task_func(void *param) +{ uint8_t *buf = malloc(RX_FRAME_SIZE); - if (!buf) { + if (!buf) + { ESP_LOGE(TAG, "Failed to allocate buffer"); vTaskDelete(NULL); return; @@ -176,13 +198,19 @@ static void meter_zigbee_task_func(void *param) { ESP_LOGI(TAG, "Zigbee meter task started"); - while (1) { + while (1) + { int len = uart_read_bytes(UART_PORT, buf, RX_FRAME_SIZE, pdMS_TO_TICKS(5000)); - if (len == RX_FRAME_SIZE) { + if (len == RX_FRAME_SIZE) + { handle_zigbee_frame(buf, len); - } else if (len == 0) { + } + else if (len == 0) + { ESP_LOGD(TAG, "UART timeout with no data"); - } else { + } + else + { ESP_LOGW(TAG, "Incomplete frame received (%d bytes)", len); } } @@ -191,22 +219,24 @@ static void meter_zigbee_task_func(void *param) { vTaskDelete(NULL); } -esp_err_t meter_zigbee_init(void) { +esp_err_t meter_zigbee_init(void) +{ ESP_LOGI(TAG, "Initializing Zigbee meter"); - if (!meter_mutex) { + if (!meter_mutex) + { meter_mutex = xSemaphoreCreateMutex(); - if (!meter_mutex) return ESP_ERR_NO_MEM; + if (!meter_mutex) + return ESP_ERR_NO_MEM; } uart_config_t config = { .baud_rate = 115200, .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, + .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .source_clk = UART_SCLK_DEFAULT - }; + .source_clk = UART_SCLK_DEFAULT}; ESP_ERROR_CHECK(uart_param_config(UART_PORT, &config)); ESP_ERROR_CHECK(uart_set_pin(UART_PORT, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); @@ -215,25 +245,28 @@ esp_err_t meter_zigbee_init(void) { return ESP_OK; } -esp_err_t meter_zigbee_start(void) { - if (meter_zigbee_task) return ESP_ERR_INVALID_STATE; +esp_err_t meter_zigbee_start(void) +{ + if (meter_zigbee_task) + return ESP_ERR_INVALID_STATE; xTaskCreate(meter_zigbee_task_func, "meter_zigbee_task", 4096, NULL, 3, &meter_zigbee_task); return ESP_OK; } +void meter_zigbee_stop(void) +{ - -void meter_zigbee_stop(void) { - - if (meter_zigbee_task) { + if (meter_zigbee_task) + { vTaskDelete(meter_zigbee_task); meter_zigbee_task = NULL; } uart_driver_delete(UART_PORT); - if (meter_mutex) { + if (meter_mutex) + { vSemaphoreDelete(meter_mutex); meter_mutex = NULL; } diff --git a/components/meter_manager/include/meter_events.h b/components/meter_manager/include/meter_events.h index 6cf042a..239b030 100644 --- a/components/meter_manager/include/meter_events.h +++ b/components/meter_manager/include/meter_events.h @@ -2,42 +2,53 @@ #define METER_EVENTS_H #include "esp_event.h" -#include "meter_manager.h" // Para meter_type_t -#include // Para int64_t +#include "meter_manager.h" // meter_type_t +#include // int32_t, int64_t #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -// Base de eventos dos medidores -ESP_EVENT_DECLARE_BASE(METER_EVENT); + // Base de eventos dos medidores + ESP_EVENT_DECLARE_BASE(METER_EVENT); -// IDs de eventos emitidos por medidores -typedef enum { - METER_EVENT_DATA_READY = 0, - METER_EVENT_ERROR, - METER_EVENT_STARTED, - METER_EVENT_STOPPED, - METER_EVENT_CONFIG_UPDATED // Novo: configuração (grid/evse) atualizada -} meter_event_id_t; + // IDs de eventos emitidos por medidores + typedef enum + { + METER_EVENT_DATA_READY = 0, + METER_EVENT_ERROR, + METER_EVENT_STARTED, + METER_EVENT_STOPPED, + METER_EVENT_CONFIG_UPDATED + } meter_event_id_t; -// Estrutura de dados enviados com METER_EVENT_DATA_READY -typedef struct { - const char *source; // "GRID" ou "EVSE" - float vrms[3]; // Tensão por fase - float irms[3]; // Corrente por fase - int watt[3]; // Potência ativa por fase - float frequency; // Frequência da rede (Hz) - float power_factor; // Fator de potência - float total_energy; // Energia acumulada (kWh) -} meter_event_data_t; + // Estrutura de dados enviados com METER_EVENT_DATA_READY + // NOTA: campos não suportados pelo meter devem ficar a 0. + typedef struct + { + const char *source; // "GRID" ou "EVSE" -// Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED -typedef struct { - meter_type_t grid_type; // Tipo de contador configurado para o GRID - meter_type_t evse_type; // Tipo de contador configurado para a EVSE - int64_t timestamp_us; // Momento da atualização (esp_timer_get_time) -} meter_config_event_t; + float vrms[3]; // V por fase (0 se não existir) + float irms[3]; // A por fase (0 se não existir) + + int32_t watt[3]; // W por fase (0 se não existir) + int32_t watt_total; // W total ASSINADO: +import / -export (0 se não existir) + + float frequency; // Hz (0 se não existir) + float power_factor; // (0 se não existir) + float total_energy; // kWh (0 se não existir) + + int64_t timestamp_us; // esp_timer_get_time() (0 => consumidor pode usar "now") + } meter_event_data_t; + + // Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED + typedef struct + { + meter_type_t grid_type; + meter_type_t evse_type; + int64_t timestamp_us; // esp_timer_get_time() + } meter_config_event_t; #ifdef __cplusplus } diff --git a/components/meter_manager/include/meter_manager.h b/components/meter_manager/include/meter_manager.h index d9a1609..ed6458f 100755 --- a/components/meter_manager/include/meter_manager.h +++ b/components/meter_manager/include/meter_manager.h @@ -17,7 +17,8 @@ typedef enum { METER_TYPE_DTS6619, // dts6619 METER_TYPE_MONO_ZIGBEE, // Zigbee single-phase METER_TYPE_TRIF_ZIGBEE, // Zigbee three-phase - METER_TYPE_EA777 // EA777 + METER_TYPE_EA777, // EA777 + METER_TYPE_DTS024M, } meter_type_t; /** diff --git a/components/meter_manager/src/meter_manager.c b/components/meter_manager/src/meter_manager.c index 7d51435..bf8e256 100755 --- a/components/meter_manager/src/meter_manager.c +++ b/components/meter_manager/src/meter_manager.c @@ -9,6 +9,7 @@ #include "meter_dds661.h" #include "meter_zigbee.h" #include "meter_ea777.h" +#include "meter_dts024m.h" #include @@ -239,6 +240,8 @@ esp_err_t meter_manager_evse_init() return meter_dds661_init(); case METER_TYPE_EA777: return meter_ea777_init(); + case METER_TYPE_DTS024M: + return meter_dts024m_init(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_init(); @@ -268,6 +271,8 @@ esp_err_t meter_manager_evse_start() return meter_dds661_start(); case METER_TYPE_EA777: return meter_ea777_start(); + case METER_TYPE_DTS024M: + return meter_dts024m_start(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_start(); @@ -304,6 +309,9 @@ esp_err_t meter_manager_evse_stop(void) case METER_TYPE_EA777: meter_ea777_stop(); break; + case METER_TYPE_DTS024M: + meter_dts024m_stop(); + break; case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: meter_zigbee_stop(); @@ -342,6 +350,8 @@ esp_err_t meter_manager_grid_init() return meter_dds661_init(); case METER_TYPE_EA777: return meter_ea777_init(); + case METER_TYPE_DTS024M: + return meter_dts024m_init(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_init(); @@ -371,6 +381,8 @@ esp_err_t meter_manager_grid_start() return meter_dds661_start(); case METER_TYPE_EA777: return meter_ea777_start(); + case METER_TYPE_DTS024M: + return meter_dts024m_start(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_start(); @@ -407,6 +419,9 @@ esp_err_t meter_manager_grid_stop(void) case METER_TYPE_EA777: meter_ea777_stop(); break; + case METER_TYPE_DTS024M: + meter_dts024m_stop(); + break; case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: meter_zigbee_stop(); @@ -509,6 +524,8 @@ const char *meter_type_to_str(meter_type_t type) return "TRIF-ZIGBEE"; case METER_TYPE_EA777: return "EA-777"; + case METER_TYPE_DTS024M: + return "DTS-024M"; default: return "NENHUM"; } @@ -537,6 +554,8 @@ meter_type_t string_to_meter_type(const char *str) return METER_TYPE_TRIF_ZIGBEE; if (strcmp(str, "EA-777") == 0) return METER_TYPE_EA777; + if (strcmp(str, "DTS-024M") == 0) + return METER_TYPE_DTS024M; return METER_TYPE_NONE; } diff --git a/components/ocpp/src/ocpp.c b/components/ocpp/src/ocpp.c index 6774edb..c9ff189 100755 --- a/components/ocpp/src/ocpp.c +++ b/components/ocpp/src/ocpp.c @@ -170,7 +170,7 @@ static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out, size_t n = strnlen(tmp, out_sz - 1); memcpy(out, tmp, n); out[n] = '\0'; - + return ESP_OK; } @@ -769,6 +769,12 @@ void ocpp_start(void) return; } + + //chargePointModel: "EPower M1" + //chargePointVendor: "Plixin" + //firmwareVersion: "FW-PLXV1.0" + //chargePointSerialNumber: "SN001" + ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false); ocpp_setEvReadyInput(&setEvReadyInput); diff --git a/components/protocols/CMakeLists.txt b/components/protocols/CMakeLists.txt index d4ab773..f2d1bd7 100755 --- a/components/protocols/CMakeLists.txt +++ b/components/protocols/CMakeLists.txt @@ -12,7 +12,6 @@ idf_component_register( vfs spiffs REQUIRES - logger network config evse diff --git a/components/protocols/src/mqtt.c b/components/protocols/src/mqtt.c index eadb935..332c62d 100755 --- a/components/protocols/src/mqtt.c +++ b/components/protocols/src/mqtt.c @@ -317,7 +317,7 @@ static void mqtt_publish_raw(const char *subtopic, const char *payload, bool ret 1, retain ? 1 : 0); - ESP_LOGD(TAG, "MQTT publish [%s] (id=%d): %s", topic, msg_id, payload); + ESP_LOGI(TAG, "MQTT publish [%s] (id=%d): %s", topic, msg_id, payload); } static void mqtt_publish_json(const char *subtopic, cJSON *obj, bool retain) diff --git a/components/rest_api/src/loadbalancing_settings_api.c b/components/rest_api/src/loadbalancing_settings_api.c index 0c4d81b..b0ee0f6 100644 --- a/components/rest_api/src/loadbalancing_settings_api.c +++ b/components/rest_api/src/loadbalancing_settings_api.c @@ -1,108 +1,216 @@ #include "loadbalancing_settings_api.h" #include "loadbalancer.h" + #include "esp_log.h" +#include "esp_err.h" +#include "esp_http_server.h" #include "cJSON.h" +#include +#include + static const char *TAG = "loadbalancing_settings_api"; -// GET Handler: Retorna configurações atuais de load balancing -static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) { - bool enabled = loadbalancer_is_enabled(); - uint8_t currentLimit = load_balancing_get_max_grid_current(); +// limites simples +#define MIN_GRID_A 6 +#define MAX_GRID_A 100 - ESP_LOGD(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit); +#define MIN_PV_W 0 +#define MAX_PV_W 100000 // ajusta se quiseres +static esp_err_t send_json(httpd_req_t *req, cJSON *root) +{ httpd_resp_set_type(req, "application/json"); - cJSON *config = cJSON_CreateObject(); - cJSON_AddBoolToObject(config, "loadBalancingEnabled", enabled); - cJSON_AddNumberToObject(config, "loadBalancingCurrentLimit", currentLimit); + char *json_str = cJSON_PrintUnformatted(root); + if (!json_str) + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON encode failed"); + return ESP_FAIL; + } - const char *json_str = cJSON_Print(config); - httpd_resp_sendstr(req, json_str); + esp_err_t err = httpd_resp_sendstr(req, json_str); + free(json_str); + return err; +} - ESP_LOGD(TAG, "Returned config: %s", json_str); +// GET -> payload novo +static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem"); + return ESP_FAIL; + } - free((void *)json_str); - cJSON_Delete(config); + cJSON_AddBoolToObject(root, "enabled", loadbalancer_is_enabled()); + + cJSON *grid = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "gridLimit", grid); + cJSON_AddBoolToObject(grid, "enabled", loadbalancer_grid_is_enabled()); + cJSON_AddNumberToObject(grid, "maxImportA", loadbalancer_grid_get_max_import_a()); + + cJSON *pv = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "pv", pv); + cJSON_AddBoolToObject(pv, "enabled", loadbalancer_pv_is_enabled()); + cJSON_AddNumberToObject(pv, "maxImportW", loadbalancer_pv_get_max_import_w()); + + esp_err_t err = send_json(req, root); + cJSON_Delete(root); + return err; +} + +// lê body de forma robusta (httpd_req_recv pode devolver parcial) +static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz) +{ + if (req->content_len <= 0) + return ESP_FAIL; + if ((size_t)req->content_len >= buf_sz) + return ESP_ERR_NO_MEM; + + int remaining = req->content_len; + int off = 0; + + while (remaining > 0) + { + int r = httpd_req_recv(req, buf + off, remaining); + if (r <= 0) + return ESP_FAIL; + off += r; + remaining -= r; + } + + buf[off] = '\0'; return ESP_OK; } -// POST Handler: Atualiza configurações de load balancing -static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { - char buf[512]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - - if (len <= 0) { - ESP_LOGE(TAG, "Received empty POST body"); +// POST -> updates parciais aceites +static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) +{ + if (req->content_len <= 0) + { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } - buf[len] = '\0'; - ESP_LOGD(TAG, "Received POST data: %s", buf); + if (req->content_len >= 512) + { + httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large"); + return ESP_FAIL; + } + + char buf[512]; + esp_err_t rb = read_body(req, buf, sizeof(buf)); + if (rb == ESP_ERR_NO_MEM) + { + httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large"); + return ESP_FAIL; + } + if (rb != ESP_OK) + { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read body"); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "POST: %s", buf); cJSON *json = cJSON_Parse(buf); - if (!json) { - ESP_LOGE(TAG, "Invalid JSON"); + if (!json) + { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } - // Atualizar estado habilitado - cJSON *enabled_item = cJSON_GetObjectItem(json, "loadBalancingEnabled"); - if (enabled_item && cJSON_IsBool(enabled_item)) { - bool isEnabled = cJSON_IsTrue(enabled_item); - loadbalancer_set_enabled(isEnabled); - ESP_LOGD(TAG, "Updated loadBalancingEnabled to: %d", isEnabled); + // enabled (top-level) + cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled"); + if (enabled_item && cJSON_IsBool(enabled_item)) + { + loadbalancer_set_enabled(cJSON_IsTrue(enabled_item)); } - // Atualizar limite de corrente - cJSON *limit_item = cJSON_GetObjectItem(json, "loadBalancingCurrentLimit"); - if (limit_item && cJSON_IsNumber(limit_item)) { - uint8_t currentLimit = (uint8_t)limit_item->valuedouble; - - // Validar intervalo - if (currentLimit < 6 || currentLimit > 100) { - ESP_LOGW(TAG, "Rejected invalid currentLimit: %d", currentLimit); - cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid currentLimit (must be 6-100)"); - return ESP_FAIL; + // gridLimit + cJSON *grid = cJSON_GetObjectItem(json, "gridLimit"); + if (grid && cJSON_IsObject(grid)) + { + cJSON *g_en = cJSON_GetObjectItem(grid, "enabled"); + if (g_en && cJSON_IsBool(g_en)) + { + loadbalancer_grid_set_enabled(cJSON_IsTrue(g_en)); } - esp_err_t err = load_balancing_set_max_grid_current(currentLimit); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to save currentLimit: %s", esp_err_to_name(err)); - cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save setting"); - return ESP_FAIL; + cJSON *g_maxA = cJSON_GetObjectItem(grid, "maxImportA"); + if (g_maxA && cJSON_IsNumber(g_maxA)) + { + int maxA = (int)g_maxA->valuedouble; + + if (maxA < MIN_GRID_A || maxA > MAX_GRID_A) + { + cJSON_Delete(json); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "gridLimit.maxImportA must be 6-100"); + return ESP_FAIL; + } + + esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)maxA); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set gridLimit.maxImportA"); + return ESP_FAIL; + } + } + } + + // pv + cJSON *pv = cJSON_GetObjectItem(json, "pv"); + if (pv && cJSON_IsObject(pv)) + { + cJSON *p_en = cJSON_GetObjectItem(pv, "enabled"); + if (p_en && cJSON_IsBool(p_en)) + { + loadbalancer_pv_set_enabled(cJSON_IsTrue(p_en)); } - ESP_LOGD(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit); + cJSON *p_maxW = cJSON_GetObjectItem(pv, "maxImportW"); + if (p_maxW && cJSON_IsNumber(p_maxW)) + { + int32_t maxW = (int32_t)p_maxW->valuedouble; + + if (maxW < MIN_PV_W || maxW > MAX_PV_W) + { + cJSON_Delete(json); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "pv.maxImportW out of range"); + return ESP_FAIL; + } + + esp_err_t e = loadbalancer_pv_set_max_import_w(maxW); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set pv.maxImportW"); + return ESP_FAIL; + } + } } cJSON_Delete(json); - httpd_resp_sendstr(req, "Load balancing settings updated successfully"); + httpd_resp_sendstr(req, "OK"); return ESP_OK; } -// Registro dos handlers na API HTTP -void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) { - // GET +void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) +{ httpd_uri_t get_uri = { .uri = "/api/v1/config/loadbalancing", .method = HTTP_GET, .handler = loadbalancing_config_get_handler, - .user_ctx = ctx - }; + .user_ctx = ctx}; httpd_register_uri_handler(server, &get_uri); - // POST httpd_uri_t post_uri = { .uri = "/api/v1/config/loadbalancing", .method = HTTP_POST, .handler = loadbalancing_config_post_handler, - .user_ctx = ctx - }; + .user_ctx = ctx}; httpd_register_uri_handler(server, &post_uri); } diff --git a/components/rest_api/src/network_api.c b/components/rest_api/src/network_api.c index bda3e68..f63891d 100755 --- a/components/rest_api/src/network_api.c +++ b/components/rest_api/src/network_api.c @@ -8,16 +8,20 @@ #include "network.h" #include "mqtt.h" +#include +#include + static const char *TAG = "network_api"; -typedef struct { +typedef struct +{ bool enabled; char ssid[33]; char password[65]; } wifi_task_data_t; - -static void wifi_apply_config_task(void *param) { +static void wifi_apply_config_task(void *param) +{ wifi_task_data_t *data = (wifi_task_data_t *)param; ESP_LOGD("wifi_task", "Applying Wi-Fi config in background task"); wifi_set_config(data->enabled, data->ssid, data->password); @@ -25,12 +29,12 @@ static void wifi_apply_config_task(void *param) { vTaskDelete(NULL); } -static esp_err_t wifi_get_handler(httpd_req_t *req) { +static esp_err_t wifi_get_handler(httpd_req_t *req) +{ ESP_LOGD(TAG, "Handling GET /api/v1/config/wifi"); httpd_resp_set_type(req, "application/json"); - // Obter dados da NVS via wifi.c bool enabled = wifi_get_enabled(); char ssid[33] = {0}; char password[65] = {0}; @@ -38,78 +42,82 @@ static esp_err_t wifi_get_handler(httpd_req_t *req) { wifi_get_ssid(ssid); wifi_get_password(password); - // Criar JSON cJSON *json = cJSON_CreateObject(); cJSON_AddBoolToObject(json, "enabled", enabled); cJSON_AddStringToObject(json, "ssid", ssid); cJSON_AddStringToObject(json, "password", password); - // Enviar resposta char *response = cJSON_Print(json); httpd_resp_sendstr(req, response); - // Limpeza free(response); cJSON_Delete(json); return ESP_OK; } -static esp_err_t wifi_post_handler(httpd_req_t *req) { +static esp_err_t wifi_post_handler(httpd_req_t *req) +{ ESP_LOGD(TAG, "Handling POST /api/v1/config/wifi"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) return ESP_FAIL; + if (len <= 0) + return ESP_FAIL; buf[len] = '\0'; cJSON *json = cJSON_Parse(buf); - if (!json) return ESP_FAIL; + if (!json) + return ESP_FAIL; - // Valores padrão bool enabled = false; const char *ssid = NULL; const char *password = NULL; cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled"); - if (cJSON_IsBool(j_enabled)) enabled = j_enabled->valueint; + if (cJSON_IsBool(j_enabled)) + enabled = j_enabled->valueint; cJSON *j_ssid = cJSON_GetObjectItem(json, "ssid"); - if (cJSON_IsString(j_ssid)) ssid = j_ssid->valuestring; + if (cJSON_IsString(j_ssid)) + ssid = j_ssid->valuestring; cJSON *j_password = cJSON_GetObjectItem(json, "password"); - if (cJSON_IsString(j_password)) password = j_password->valuestring; + if (cJSON_IsString(j_password)) + password = j_password->valuestring; - // Enviar resposta antes de alterar Wi-Fi + // Resposta imediata httpd_resp_sendstr(req, "Wi-Fi config atualizada com sucesso"); - // Alocar struct para passar para a task - wifi_task_data_t *task_data = malloc(sizeof(wifi_task_data_t)); - if (!task_data) { + wifi_task_data_t *task_data = (wifi_task_data_t *)malloc(sizeof(wifi_task_data_t)); + if (!task_data) + { cJSON_Delete(json); ESP_LOGE(TAG, "Memory allocation failed for Wi-Fi task"); return ESP_ERR_NO_MEM; } task_data->enabled = enabled; - strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid)); - strncpy(task_data->password, password ? password : "", sizeof(task_data->password)); - // Criar task normal com função C + // Copias seguras (garante null-termination) + strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid) - 1); + task_data->ssid[sizeof(task_data->ssid) - 1] = '\0'; + + strncpy(task_data->password, password ? password : "", sizeof(task_data->password) - 1); + task_data->password[sizeof(task_data->password) - 1] = '\0'; + xTaskCreate( wifi_apply_config_task, "wifi_config_task", 4096, task_data, 3, - NULL - ); + NULL); cJSON_Delete(json); return ESP_OK; } - static esp_err_t config_mqtt_get_handler(httpd_req_t *req) { ESP_LOGD(TAG, "Handling GET /api/v1/config/mqtt"); @@ -139,28 +147,28 @@ static esp_err_t config_mqtt_get_handler(httpd_req_t *req) cJSON *config = cJSON_CreateObject(); cJSON_AddBoolToObject(config, "enabled", enabled); cJSON_AddStringToObject(config, "host", server); - cJSON_AddNumberToObject(config, "port", 1883); + cJSON_AddNumberToObject(config, "port", 1883); // fixo (se não usas no mqtt_set_config) cJSON_AddStringToObject(config, "username", username); cJSON_AddStringToObject(config, "password", password); cJSON_AddStringToObject(config, "topic", base_topic); cJSON_AddNumberToObject(config, "periodicity", periodicity); - const char *config_str = cJSON_Print(config); + char *config_str = cJSON_Print(config); httpd_resp_sendstr(req, config_str); - free((void *)config_str); + free(config_str); cJSON_Delete(config); return ESP_OK; } - static esp_err_t config_mqtt_post_handler(httpd_req_t *req) { ESP_LOGD(TAG, "Handling POST /api/v1/config/mqtt"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) { + if (len <= 0) + { ESP_LOGE(TAG, "Failed to read request body"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request body"); return ESP_FAIL; @@ -169,33 +177,75 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req) ESP_LOGD(TAG, "Received JSON: %s", buf); cJSON *json = cJSON_Parse(buf); - if (!json) { + if (!json) + { ESP_LOGE(TAG, "Invalid JSON format"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } - bool enabled = false; - const char *host = NULL, *topic = NULL, *username = NULL, *password = NULL; - int periodicity = 30; + // --- Ler config atual (para permitir "partial update" e evitar strings vazias) + bool current_enabled = mqtt_get_enabled(); + char current_host[64] = {0}; + char current_topic[32] = {0}; + char current_user[32] = {0}; + char current_pass[64] = {0}; + uint16_t current_periodicity = mqtt_get_periodicity(); - if (cJSON_IsBool(cJSON_GetObjectItem(json, "enabled"))) - enabled = cJSON_GetObjectItem(json, "enabled")->valueint; + mqtt_get_server(current_host); + mqtt_get_base_topic(current_topic); + mqtt_get_user(current_user); + mqtt_get_password(current_pass); + + bool enabled = current_enabled; + const char *host_in = NULL, *topic_in = NULL, *user_in = NULL, *pass_in = NULL; + int periodicity = (int)current_periodicity; + + cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled"); + if (cJSON_IsBool(j_enabled)) + enabled = j_enabled->valueint; cJSON *j_host = cJSON_GetObjectItem(json, "host"); - if (cJSON_IsString(j_host)) host = j_host->valuestring; + if (cJSON_IsString(j_host)) + host_in = j_host->valuestring; cJSON *j_topic = cJSON_GetObjectItem(json, "topic"); - if (cJSON_IsString(j_topic)) topic = j_topic->valuestring; + if (cJSON_IsString(j_topic)) + topic_in = j_topic->valuestring; cJSON *j_user = cJSON_GetObjectItem(json, "username"); - if (cJSON_IsString(j_user)) username = j_user->valuestring; + if (cJSON_IsString(j_user)) + user_in = j_user->valuestring; cJSON *j_pass = cJSON_GetObjectItem(json, "password"); - if (cJSON_IsString(j_pass)) password = j_pass->valuestring; + if (cJSON_IsString(j_pass)) + pass_in = j_pass->valuestring; cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity"); - if (cJSON_IsNumber(j_periodicity)) periodicity = j_periodicity->valueint; + if (cJSON_IsNumber(j_periodicity)) + periodicity = j_periodicity->valueint; + + // --- Regras: se vier NULL ou "" mantém o atual; se atual também estiver vazio, usa default + const char *host = + (host_in && host_in[0] != '\0') ? host_in : (current_host[0] != '\0') ? current_host + : "mqtt.plixin.com"; + + const char *topic = + (topic_in && topic_in[0] != '\0') ? topic_in : (current_topic[0] != '\0') ? current_topic + : ""; + + const char *username = + (user_in && user_in[0] != '\0') ? user_in : (current_user[0] != '\0') ? current_user + : ""; + + const char *password = + (pass_in && pass_in[0] != '\0') ? pass_in : (current_pass[0] != '\0') ? current_pass + : ""; + + if (periodicity <= 0) + periodicity = (int)current_periodicity; + if (periodicity <= 0) + periodicity = 30; ESP_LOGD(TAG, "Applying MQTT config:"); ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false"); @@ -206,7 +256,8 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req) ESP_LOGD(TAG, " Periodicity: %d", periodicity); esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity); - if (err != ESP_OK) { + if (err != ESP_OK) + { ESP_LOGE(TAG, "Failed to apply MQTT config (code %d)", err); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to apply config"); cJSON_Delete(json); @@ -218,40 +269,33 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req) return ESP_OK; } - - -void register_network_handlers(httpd_handle_t server, void *ctx) { +void register_network_handlers(httpd_handle_t server, void *ctx) +{ httpd_uri_t wifi_get = { .uri = "/api/v1/config/wifi", .method = HTTP_GET, .handler = wifi_get_handler, - .user_ctx = ctx - }; + .user_ctx = ctx}; httpd_register_uri_handler(server, &wifi_get); httpd_uri_t wifi_post = { .uri = "/api/v1/config/wifi", .method = HTTP_POST, .handler = wifi_post_handler, - .user_ctx = ctx - }; + .user_ctx = ctx}; httpd_register_uri_handler(server, &wifi_post); - // URI handler for getting MQTT config httpd_uri_t config_mqtt_get_uri = { .uri = "/api/v1/config/mqtt", .method = HTTP_GET, .handler = config_mqtt_get_handler, - .user_ctx = ctx - }; + .user_ctx = ctx}; httpd_register_uri_handler(server, &config_mqtt_get_uri); - // URI handler for posting MQTT config httpd_uri_t config_mqtt_post_uri = { .uri = "/api/v1/config/mqtt", .method = HTTP_POST, .handler = config_mqtt_post_handler, - .user_ctx = ctx - }; + .user_ctx = ctx}; httpd_register_uri_handler(server, &config_mqtt_post_uri); } diff --git a/components/rest_api/webfolder/assets/index-CH8H7Z_T.js b/components/rest_api/webfolder/assets/index-0q0tbwk5.js similarity index 56% rename from components/rest_api/webfolder/assets/index-CH8H7Z_T.js rename to components/rest_api/webfolder/assets/index-0q0tbwk5.js index bd6dd7c..55f3ce6 100644 --- a/components/rest_api/webfolder/assets/index-CH8H7Z_T.js +++ b/components/rest_api/webfolder/assets/index-0q0tbwk5.js @@ -1,4 +1,4 @@ -(function(){const d=document.createElement("link").relList;if(d&&d.supports&&d.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))s(h);new MutationObserver(h=>{for(const y of h)if(y.type==="childList")for(const v of y.addedNodes)v.tagName==="LINK"&&v.rel==="modulepreload"&&s(v)}).observe(document,{childList:!0,subtree:!0});function o(h){const y={};return h.integrity&&(y.integrity=h.integrity),h.referrerPolicy&&(y.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?y.credentials="include":h.crossOrigin==="anonymous"?y.credentials="omit":y.credentials="same-origin",y}function s(h){if(h.ep)return;h.ep=!0;const y=o(h);fetch(h.href,y)}})();function ch(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Mr={exports:{}},Hn={};/** +(function(){const d=document.createElement("link").relList;if(d&&d.supports&&d.supports("modulepreload"))return;for(const m of document.querySelectorAll('link[rel="modulepreload"]'))s(m);new MutationObserver(m=>{for(const y of m)if(y.type==="childList")for(const v of y.addedNodes)v.tagName==="LINK"&&v.rel==="modulepreload"&&s(v)}).observe(document,{childList:!0,subtree:!0});function o(m){const y={};return m.integrity&&(y.integrity=m.integrity),m.referrerPolicy&&(y.referrerPolicy=m.referrerPolicy),m.crossOrigin==="use-credentials"?y.credentials="include":m.crossOrigin==="anonymous"?y.credentials="omit":y.credentials="same-origin",y}function s(m){if(m.ep)return;m.ep=!0;const y=o(m);fetch(m.href,y)}})();function sm(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Or={exports:{}},Ln={};/** * @license React * react-jsx-runtime.production.js * @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Gd;function Ky(){if(Gd)return Hn;Gd=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.fragment");function o(s,h,y){var v=null;if(y!==void 0&&(v=""+y),h.key!==void 0&&(v=""+h.key),"key"in h){y={};for(var R in h)R!=="key"&&(y[R]=h[R])}else y=h;return h=y.ref,{$$typeof:i,type:s,key:v,ref:h!==void 0?h:null,props:y}}return Hn.Fragment=d,Hn.jsx=o,Hn.jsxs=o,Hn}var Xd;function Jy(){return Xd||(Xd=1,Mr.exports=Ky()),Mr.exports}var r=Jy(),Dr={exports:{}},ne={};/** + */var Gd;function ky(){if(Gd)return Ln;Gd=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.fragment");function o(s,m,y){var v=null;if(y!==void 0&&(v=""+y),m.key!==void 0&&(v=""+m.key),"key"in m){y={};for(var R in m)R!=="key"&&(y[R]=m[R])}else y=m;return m=y.ref,{$$typeof:i,type:s,key:v,ref:m!==void 0?m:null,props:y}}return Ln.Fragment=d,Ln.jsx=o,Ln.jsxs=o,Ln}var Xd;function $y(){return Xd||(Xd=1,Or.exports=ky()),Or.exports}var r=$y(),Dr={exports:{}},ne={};/** * @license React * react.production.js * @@ -14,7 +14,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Qd;function ky(){if(Qd)return ne;Qd=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),h=Symbol.for("react.profiler"),y=Symbol.for("react.consumer"),v=Symbol.for("react.context"),R=Symbol.for("react.forward_ref"),x=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),N=Symbol.for("react.lazy"),D=Symbol.iterator;function S(p){return p===null||typeof p!="object"?null:(p=D&&p[D]||p["@@iterator"],typeof p=="function"?p:null)}var q={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},U=Object.assign,Y={};function Q(p,L,K){this.props=p,this.context=L,this.refs=Y,this.updater=K||q}Q.prototype.isReactComponent={},Q.prototype.setState=function(p,L){if(typeof p!="object"&&typeof p!="function"&&p!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,p,L,"setState")},Q.prototype.forceUpdate=function(p){this.updater.enqueueForceUpdate(this,p,"forceUpdate")};function H(){}H.prototype=Q.prototype;function G(p,L,K){this.props=p,this.context=L,this.refs=Y,this.updater=K||q}var B=G.prototype=new H;B.constructor=G,U(B,Q.prototype),B.isPureReactComponent=!0;var $=Array.isArray,J={H:null,A:null,T:null,S:null,V:null},me=Object.prototype.hasOwnProperty;function V(p,L,K,X,F,oe){return K=oe.ref,{$$typeof:i,type:p,key:L,ref:K!==void 0?K:null,props:oe}}function fe(p,L){return V(p.type,L,void 0,void 0,void 0,p.props)}function ee(p){return typeof p=="object"&&p!==null&&p.$$typeof===i}function Ce(p){var L={"=":"=0",":":"=2"};return"$"+p.replace(/[=:]/g,function(K){return L[K]})}var yt=/\/+/g;function Je(p,L){return typeof p=="object"&&p!==null&&p.key!=null?Ce(""+p.key):L.toString(36)}function Dl(){}function zl(p){switch(p.status){case"fulfilled":return p.value;case"rejected":throw p.reason;default:switch(typeof p.status=="string"?p.then(Dl,Dl):(p.status="pending",p.then(function(L){p.status==="pending"&&(p.status="fulfilled",p.value=L)},function(L){p.status==="pending"&&(p.status="rejected",p.reason=L)})),p.status){case"fulfilled":return p.value;case"rejected":throw p.reason}}throw p}function ke(p,L,K,X,F){var oe=typeof p;(oe==="undefined"||oe==="boolean")&&(p=null);var ae=!1;if(p===null)ae=!0;else switch(oe){case"bigint":case"string":case"number":ae=!0;break;case"object":switch(p.$$typeof){case i:case d:ae=!0;break;case N:return ae=p._init,ke(ae(p._payload),L,K,X,F)}}if(ae)return F=F(p),ae=X===""?"."+Je(p,0):X,$(F)?(K="",ae!=null&&(K=ae.replace(yt,"$&/")+"/"),ke(F,L,K,"",function(nl){return nl})):F!=null&&(ee(F)&&(F=fe(F,K+(F.key==null||p&&p.key===F.key?"":(""+F.key).replace(yt,"$&/")+"/")+ae)),L.push(F)),1;ae=0;var ut=X===""?".":X+":";if($(p))for(var Te=0;Te>>1,p=C[xe];if(0>>1;xeh(X,te))Fh(oe,X)?(C[xe]=oe,C[F]=te,xe=F):(C[xe]=X,C[K]=te,xe=K);else if(Fh(oe,te))C[xe]=oe,C[F]=te,xe=F;else break e}}return Z}function h(C,Z){var te=C.sortIndex-Z.sortIndex;return te!==0?te:C.id-Z.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var y=performance;i.unstable_now=function(){return y.now()}}else{var v=Date,R=v.now();i.unstable_now=function(){return v.now()-R}}var x=[],m=[],N=1,D=null,S=3,q=!1,U=!1,Y=!1,Q=!1,H=typeof setTimeout=="function"?setTimeout:null,G=typeof clearTimeout=="function"?clearTimeout:null,B=typeof setImmediate<"u"?setImmediate:null;function $(C){for(var Z=o(m);Z!==null;){if(Z.callback===null)s(m);else if(Z.startTime<=C)s(m),Z.sortIndex=Z.expirationTime,d(x,Z);else break;Z=o(m)}}function J(C){if(Y=!1,$(C),!U)if(o(x)!==null)U=!0,me||(me=!0,Je());else{var Z=o(m);Z!==null&&ke(J,Z.startTime-C)}}var me=!1,V=-1,fe=5,ee=-1;function Ce(){return Q?!0:!(i.unstable_now()-eeC&&Ce());){var xe=D.callback;if(typeof xe=="function"){D.callback=null,S=D.priorityLevel;var p=xe(D.expirationTime<=C);if(C=i.unstable_now(),typeof p=="function"){D.callback=p,$(C),Z=!0;break t}D===o(x)&&s(x),$(C)}else s(x);D=o(x)}if(D!==null)Z=!0;else{var L=o(m);L!==null&&ke(J,L.startTime-C),Z=!1}}break e}finally{D=null,S=te,q=!1}Z=void 0}}finally{Z?Je():me=!1}}}var Je;if(typeof B=="function")Je=function(){B(yt)};else if(typeof MessageChannel<"u"){var Dl=new MessageChannel,zl=Dl.port2;Dl.port1.onmessage=yt,Je=function(){zl.postMessage(null)}}else Je=function(){H(yt,0)};function ke(C,Z){V=H(function(){C(i.unstable_now())},Z)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(C){C.callback=null},i.unstable_forceFrameRate=function(C){0>C||125xe?(C.sortIndex=te,d(m,C),o(x)===null&&C===o(m)&&(Y?(G(V),V=-1):Y=!0,ke(J,te-xe))):(C.sortIndex=p,d(x,C),U||q||(U=!0,me||(me=!0,Je()))),C},i.unstable_shouldYield=Ce,i.unstable_wrapCallback=function(C){var Z=S;return function(){var te=S;S=Z;try{return C.apply(this,arguments)}finally{S=te}}}}(_r)),_r}var Kd;function Fy(){return Kd||(Kd=1,Cr.exports=Wy()),Cr.exports}var Ur={exports:{}},Fe={};/** + */var Vd;function Py(){return Vd||(Vd=1,function(i){function d(C,V){var te=C.length;C.push(V);e:for(;0>>1,p=C[xe];if(0>>1;xem(Q,te))Fm(de,Q)?(C[xe]=de,C[F]=te,xe=F):(C[xe]=Q,C[J]=te,xe=J);else if(Fm(de,te))C[xe]=de,C[F]=te,xe=F;else break e}}return V}function m(C,V){var te=C.sortIndex-V.sortIndex;return te!==0?te:C.id-V.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var y=performance;i.unstable_now=function(){return y.now()}}else{var v=Date,R=v.now();i.unstable_now=function(){return v.now()-R}}var x=[],h=[],j=1,D=null,S=3,L=!1,U=!1,G=!1,Z=!1,Y=typeof setTimeout=="function"?setTimeout:null,q=typeof clearTimeout=="function"?clearTimeout:null,B=typeof setImmediate<"u"?setImmediate:null;function k(C){for(var V=o(h);V!==null;){if(V.callback===null)s(h);else if(V.startTime<=C)s(h),V.sortIndex=V.expirationTime,d(x,V);else break;V=o(h)}}function K(C){if(G=!1,k(C),!U)if(o(x)!==null)U=!0,fe||(fe=!0,Je());else{var V=o(h);V!==null&&ke(K,V.startTime-C)}}var fe=!1,X=-1,ie=5,ee=-1;function Ce(){return Z?!0:!(i.unstable_now()-eeC&&Ce());){var xe=D.callback;if(typeof xe=="function"){D.callback=null,S=D.priorityLevel;var p=xe(D.expirationTime<=C);if(C=i.unstable_now(),typeof p=="function"){D.callback=p,k(C),V=!0;break t}D===o(x)&&s(x),k(C)}else s(x);D=o(x)}if(D!==null)V=!0;else{var H=o(h);H!==null&&ke(K,H.startTime-C),V=!1}}break e}finally{D=null,S=te,L=!1}V=void 0}}finally{V?Je():fe=!1}}}var Je;if(typeof B=="function")Je=function(){B(yt)};else if(typeof MessageChannel<"u"){var Dl=new MessageChannel,zl=Dl.port2;Dl.port1.onmessage=yt,Je=function(){zl.postMessage(null)}}else Je=function(){Y(yt,0)};function ke(C,V){X=Y(function(){C(i.unstable_now())},V)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(C){C.callback=null},i.unstable_forceFrameRate=function(C){0>C||125xe?(C.sortIndex=te,d(h,C),o(x)===null&&C===o(h)&&(G?(q(X),X=-1):G=!0,ke(K,te-xe))):(C.sortIndex=p,d(x,C),U||L||(U=!0,fe||(fe=!0,Je()))),C},i.unstable_shouldYield=Ce,i.unstable_wrapCallback=function(C){var V=S;return function(){var te=S;S=V;try{return C.apply(this,arguments)}finally{S=te}}}}(_r)),_r}var Kd;function Iy(){return Kd||(Kd=1,Cr.exports=Py()),Cr.exports}var Ur={exports:{}},Fe={};/** * @license React * react-dom.production.js * @@ -30,7 +30,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Jd;function Py(){if(Jd)return Fe;Jd=1;var i=qr();function d(x){var m="https://react.dev/errors/"+x;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(d){console.error(d)}}return i(),Ur.exports=Py(),Ur.exports}/** + */var Jd;function e0(){if(Jd)return Fe;Jd=1;var i=qr();function d(x){var h="https://react.dev/errors/"+x;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(d){console.error(d)}}return i(),Ur.exports=e0(),Ur.exports}/** * @license React * react-dom-client.production.js * @@ -38,14 +38,14 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var $d;function e0(){if($d)return Ln;$d=1;var i=Fy(),d=qr(),o=Iy();function s(e){var t="https://react.dev/errors/"+e;if(1p||(e.current=xe[p],xe[p]=null,p--)}function X(e,t){p++,xe[p]=e.current,e.current=t}var F=L(null),oe=L(null),ae=L(null),ut=L(null);function Te(e,t){switch(X(ae,t),X(oe,e),X(F,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=yd(t),e=gd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}K(F),X(F,e)}function nl(){K(F),K(oe),K(ae)}function mi(e){e.memoizedState!==null&&X(ut,e);var t=F.current,l=gd(t,e.type);t!==l&&(X(oe,e),X(F,l))}function Zn(e){oe.current===e&&(K(F),K(oe)),ut.current===e&&(K(ut),zn._currentValue=te)}var yi=Object.prototype.hasOwnProperty,gi=i.unstable_scheduleCallback,vi=i.unstable_cancelCallback,jh=i.unstable_shouldYield,Nh=i.unstable_requestPaint,zt=i.unstable_now,Ah=i.unstable_getCurrentPriorityLevel,Jr=i.unstable_ImmediatePriority,kr=i.unstable_UserBlockingPriority,Vn=i.unstable_NormalPriority,Rh=i.unstable_LowPriority,$r=i.unstable_IdlePriority,Oh=i.log,Mh=i.unstable_setDisableYieldValue,Ba=null,it=null;function ul(e){if(typeof Oh=="function"&&Mh(e),it&&typeof it.setStrictMode=="function")try{it.setStrictMode(Ba,e)}catch{}}var ct=Math.clz32?Math.clz32:Ch,Dh=Math.log,zh=Math.LN2;function Ch(e){return e>>>=0,e===0?32:31-(Dh(e)/zh|0)|0}var Kn=256,Jn=4194304;function Cl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function kn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var f=a&134217727;return f!==0?(a=f&~u,a!==0?n=Cl(a):(c&=f,c!==0?n=Cl(c):l||(l=f&~e,l!==0&&(n=Cl(l))))):(f=a&~u,f!==0?n=Cl(f):c!==0?n=Cl(c):l||(l=a&~e,l!==0&&(n=Cl(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function qa(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function _h(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Wr(){var e=Kn;return Kn<<=1,(Kn&4194048)===0&&(Kn=256),e}function Fr(){var e=Jn;return Jn<<=1,(Jn&62914560)===0&&(Jn=4194304),e}function pi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ya(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Uh(e,t,l,a,n,u){var c=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var f=e.entanglements,g=e.expirationTimes,A=e.hiddenUpdates;for(l=c&~l;0p||(e.current=xe[p],xe[p]=null,p--)}function Q(e,t){p++,xe[p]=e.current,e.current=t}var F=H(null),de=H(null),ae=H(null),ut=H(null);function Te(e,t){switch(Q(ae,t),Q(de,e),Q(F,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=yd(t),e=gd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}J(F),Q(F,e)}function nl(){J(F),J(de),J(ae)}function hi(e){e.memoizedState!==null&&Q(ut,e);var t=F.current,l=gd(t,e.type);t!==l&&(Q(de,e),Q(F,l))}function Zn(e){de.current===e&&(J(F),J(de)),ut.current===e&&(J(ut),zn._currentValue=te)}var yi=Object.prototype.hasOwnProperty,gi=i.unstable_scheduleCallback,vi=i.unstable_cancelCallback,Am=i.unstable_shouldYield,Rm=i.unstable_requestPaint,zt=i.unstable_now,Mm=i.unstable_getCurrentPriorityLevel,Jr=i.unstable_ImmediatePriority,kr=i.unstable_UserBlockingPriority,Vn=i.unstable_NormalPriority,Om=i.unstable_LowPriority,$r=i.unstable_IdlePriority,Dm=i.log,zm=i.unstable_setDisableYieldValue,Ba=null,it=null;function ul(e){if(typeof Dm=="function"&&zm(e),it&&typeof it.setStrictMode=="function")try{it.setStrictMode(Ba,e)}catch{}}var ct=Math.clz32?Math.clz32:Um,Cm=Math.log,_m=Math.LN2;function Um(e){return e>>>=0,e===0?32:31-(Cm(e)/_m|0)|0}var Kn=256,Jn=4194304;function Cl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function kn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var f=a&134217727;return f!==0?(a=f&~u,a!==0?n=Cl(a):(c&=f,c!==0?n=Cl(c):l||(l=f&~e,l!==0&&(n=Cl(l))))):(f=a&~u,f!==0?n=Cl(f):c!==0?n=Cl(c):l||(l=a&~e,l!==0&&(n=Cl(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function qa(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function wm(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Wr(){var e=Kn;return Kn<<=1,(Kn&4194048)===0&&(Kn=256),e}function Fr(){var e=Jn;return Jn<<=1,(Jn&62914560)===0&&(Jn=4194304),e}function pi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ya(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Lm(e,t,l,a,n,u){var c=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var f=e.entanglements,g=e.expirationTimes,A=e.hiddenUpdates;for(l=c&~l;0)":-1n||g[a]!==A[n]){var z=` -`+g[a].replace(" at new "," at ");return e.displayName&&z.includes("")&&(z=z.replace("",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{ji=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?ta(l):""}function Yh(e){switch(e.tag){case 26:case 27:case 5:return ta(e.type);case 16:return ta("Lazy");case 13:return ta("Suspense");case 19:return ta("SuspenseList");case 0:case 15:return Ni(e.type,!1);case 11:return Ni(e.type.render,!1);case 1:return Ni(e.type,!0);case 31:return ta("Activity");default:return""}}function cs(e){try{var t="";do t+=Yh(e),e=e.return;while(e);return t}catch(l){return` +`+g[a].replace(" at new "," at ");return e.displayName&&z.includes("")&&(z=z.replace("",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{ji=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?ta(l):""}function Xm(e){switch(e.tag){case 26:case 27:case 5:return ta(e.type);case 16:return ta("Lazy");case 13:return ta("Suspense");case 19:return ta("SuspenseList");case 0:case 15:return Ni(e.type,!1);case 11:return Ni(e.type.render,!1);case 1:return Ni(e.type,!0);case 31:return ta("Activity");default:return""}}function cs(e){try{var t="";do t+=Xm(e),e=e.return;while(e);return t}catch(l){return` Error generating stack: `+l.message+` -`+l.stack}}function gt(e){switch(typeof e){case"bigint":case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function rs(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Gh(e){var t=rs(e)?"checked":"value",l=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),a=""+e[t];if(!e.hasOwnProperty(t)&&typeof l<"u"&&typeof l.get=="function"&&typeof l.set=="function"){var n=l.get,u=l.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return n.call(this)},set:function(c){a=""+c,u.call(this,c)}}),Object.defineProperty(e,t,{enumerable:l.enumerable}),{getValue:function(){return a},setValue:function(c){a=""+c},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Fn(e){e._valueTracker||(e._valueTracker=Gh(e))}function ss(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var l=t.getValue(),a="";return e&&(a=rs(e)?e.checked?"true":"false":e.value),e=a,e!==l?(t.setValue(e),!0):!1}function Pn(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Xh=/[\n"\\]/g;function vt(e){return e.replace(Xh,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Ai(e,t,l,a,n,u,c,f){e.name="",c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"?e.type=c:e.removeAttribute("type"),t!=null?c==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+gt(t)):e.value!==""+gt(t)&&(e.value=""+gt(t)):c!=="submit"&&c!=="reset"||e.removeAttribute("value"),t!=null?Ri(e,c,gt(t)):l!=null?Ri(e,c,gt(l)):a!=null&&e.removeAttribute("value"),n==null&&u!=null&&(e.defaultChecked=!!u),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?e.name=""+gt(f):e.removeAttribute("name")}function fs(e,t,l,a,n,u,c,f){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(e.type=u),t!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||t!=null))return;l=l!=null?""+gt(l):"",t=t!=null?""+gt(t):l,f||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=f?e.checked:!!a,e.defaultChecked=!!a,c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(e.name=c)}function Ri(e,t,l){t==="number"&&Pn(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function la(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ci=!1;if(Gt)try{var Za={};Object.defineProperty(Za,"passive",{get:function(){Ci=!0}}),window.addEventListener("test",Za,Za),window.removeEventListener("test",Za,Za)}catch{Ci=!1}var cl=null,_i=null,eu=null;function vs(){if(eu)return eu;var e,t=_i,l=t.length,a,n="value"in cl?cl.value:cl.textContent,u=n.length;for(e=0;e=Ja),Ts=" ",js=!1;function Ns(e,t){switch(e){case"keyup":return gm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function As(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var ia=!1;function pm(e,t){switch(e){case"compositionend":return As(t);case"keypress":return t.which!==32?null:(js=!0,Ts);case"textInput":return e=t.data,e===Ts&&js?null:e;default:return null}}function bm(e,t){if(ia)return e==="compositionend"||!Bi&&Ns(e,t)?(e=vs(),eu=_i=cl=null,ia=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=Us(l)}}function Hs(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Hs(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ls(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Pn(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=Pn(e.document)}return t}function Gi(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Rm=Gt&&"documentMode"in document&&11>=document.documentMode,ca=null,Xi=null,Fa=null,Qi=!1;function Bs(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Qi||ca==null||ca!==Pn(a)||(a=ca,"selectionStart"in a&&Gi(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Fa&&Wa(Fa,a)||(Fa=a,a=Zu(Xi,"onSelect"),0>=c,n-=c,Qt=1<<32-ct(t)+n|l<u?u:8;var c=C.T,f={};C.T=f,Oc(e,!1,t,l);try{var g=n(),A=C.S;if(A!==null&&A(f,g),g!==null&&typeof g=="object"&&typeof g.then=="function"){var z=Hm(g,a);hn(e,t,z,ht(e))}else hn(e,t,a,ht(e))}catch(w){hn(e,t,{then:function(){},status:"rejected",reason:w},ht())}finally{Z.p=u,C.T=c}}function Gm(){}function Ac(e,t,l,a){if(e.tag!==5)throw Error(s(476));var n=Yf(e).queue;qf(e,n,t,te,l===null?Gm:function(){return Gf(e),l(a)})}function Yf(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:te,baseState:te,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jt,lastRenderedState:te},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jt,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Gf(e){var t=Yf(e).next.queue;hn(e,t,{},ht())}function Rc(){return We(zn)}function Xf(){return Ue().memoizedState}function Qf(){return Ue().memoizedState}function Xm(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=ht();e=fl(l);var a=ol(t,e,l);a!==null&&(mt(a,t,l),cn(a,t,l)),t={cache:ac()},e.payload=t;return}t=t.return}}function Qm(e,t,l){var a=ht();l={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null},ju(e)?Vf(t,l):(l=Ji(e,t,l,a),l!==null&&(mt(l,e,a),Kf(l,t,a)))}function Zf(e,t,l){var a=ht();hn(e,t,l,a)}function hn(e,t,l,a){var n={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null};if(ju(e))Vf(t,n);else{var u=e.alternate;if(e.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var c=t.lastRenderedState,f=u(c,l);if(n.hasEagerState=!0,n.eagerState=f,rt(f,c))return cu(e,t,n,0),Ee===null&&iu(),!1}catch{}finally{}if(l=Ji(e,t,n,a),l!==null)return mt(l,e,a),Kf(l,t,a),!0}return!1}function Oc(e,t,l,a){if(a={lane:2,revertLane:ir(),action:a,hasEagerState:!1,eagerState:null,next:null},ju(e)){if(t)throw Error(s(479))}else t=Ji(e,l,a,2),t!==null&&mt(t,e,2)}function ju(e){var t=e.alternate;return e===ue||t!==null&&t===ue}function Vf(e,t){va=pu=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function Kf(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,Ir(e,l)}}var Nu={readContext:We,use:xu,useCallback:De,useContext:De,useEffect:De,useImperativeHandle:De,useLayoutEffect:De,useInsertionEffect:De,useMemo:De,useReducer:De,useRef:De,useState:De,useDebugValue:De,useDeferredValue:De,useTransition:De,useSyncExternalStore:De,useId:De,useHostTransitionStatus:De,useFormState:De,useActionState:De,useOptimistic:De,useMemoCache:De,useCacheRefresh:De},Jf={readContext:We,use:xu,useCallback:function(e,t){return tt().memoizedState=[e,t===void 0?null:t],e},useContext:We,useEffect:Df,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Tu(4194308,4,Uf.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Tu(4194308,4,e,t)},useInsertionEffect:function(e,t){Tu(4,2,e,t)},useMemo:function(e,t){var l=tt();t=t===void 0?null:t;var a=e();if(Vl){ul(!0);try{e()}finally{ul(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=tt();if(l!==void 0){var n=l(t);if(Vl){ul(!0);try{l(t)}finally{ul(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=Qm.bind(null,ue,e),[a.memoizedState,e]},useRef:function(e){var t=tt();return e={current:e},t.memoizedState=e},useState:function(e){e=Ec(e);var t=e.queue,l=Zf.bind(null,ue,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:jc,useDeferredValue:function(e,t){var l=tt();return Nc(l,e,t)},useTransition:function(){var e=Ec(!1);return e=qf.bind(null,ue,e.queue,!0,!1),tt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ue,n=tt();if(he){if(l===void 0)throw Error(s(407));l=l()}else{if(l=t(),Ee===null)throw Error(s(349));(se&124)!==0||mf(a,t,l)}n.memoizedState=l;var u={value:l,getSnapshot:t};return n.queue=u,Df(gf.bind(null,a,u,e),[e]),a.flags|=2048,ba(9,Eu(),yf.bind(null,a,u,l,t),null),l},useId:function(){var e=tt(),t=Ee.identifierPrefix;if(he){var l=Zt,a=Qt;l=(a&~(1<<32-ct(a)-1)).toString(32)+l,t="«"+t+"R"+l,l=bu++,0I?(Ge=W,W=null):Ge=W.sibling;var de=O(T,W,j[I],_);if(de===null){W===null&&(W=Ge);break}e&&W&&de.alternate===null&&t(T,W),b=u(de,b,I),ie===null?k=de:ie.sibling=de,ie=de,W=Ge}if(I===j.length)return l(T,W),he&&ql(T,I),k;if(W===null){for(;II?(Ge=W,W=null):Ge=W.sibling;var Ol=O(T,W,de.value,_);if(Ol===null){W===null&&(W=Ge);break}e&&W&&Ol.alternate===null&&t(T,W),b=u(Ol,b,I),ie===null?k=Ol:ie.sibling=Ol,ie=Ol,W=Ge}if(de.done)return l(T,W),he&&ql(T,I),k;if(W===null){for(;!de.done;I++,de=j.next())de=w(T,de.value,_),de!==null&&(b=u(de,b,I),ie===null?k=de:ie.sibling=de,ie=de);return he&&ql(T,I),k}for(W=a(W);!de.done;I++,de=j.next())de=M(W,T,I,de.value,_),de!==null&&(e&&de.alternate!==null&&W.delete(de.key===null?I:de.key),b=u(de,b,I),ie===null?k=de:ie.sibling=de,ie=de);return e&&W.forEach(function(Vy){return t(T,Vy)}),he&&ql(T,I),k}function be(T,b,j,_){if(typeof j=="object"&&j!==null&&j.type===U&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case S:e:{for(var k=j.key;b!==null;){if(b.key===k){if(k=j.type,k===U){if(b.tag===7){l(T,b.sibling),_=n(b,j.props.children),_.return=T,T=_;break e}}else if(b.elementType===k||typeof k=="object"&&k!==null&&k.$$typeof===fe&&$f(k)===b.type){l(T,b.sibling),_=n(b,j.props),yn(_,j),_.return=T,T=_;break e}l(T,b);break}else t(T,b);b=b.sibling}j.type===U?(_=Ll(j.props.children,T.mode,_,j.key),_.return=T,T=_):(_=su(j.type,j.key,j.props,null,T.mode,_),yn(_,j),_.return=T,T=_)}return c(T);case q:e:{for(k=j.key;b!==null;){if(b.key===k)if(b.tag===4&&b.stateNode.containerInfo===j.containerInfo&&b.stateNode.implementation===j.implementation){l(T,b.sibling),_=n(b,j.children||[]),_.return=T,T=_;break e}else{l(T,b);break}else t(T,b);b=b.sibling}_=Wi(j,T.mode,_),_.return=T,T=_}return c(T);case fe:return k=j._init,j=k(j._payload),be(T,b,j,_)}if(ke(j))return le(T,b,j,_);if(Je(j)){if(k=Je(j),typeof k!="function")throw Error(s(150));return j=k.call(j),P(T,b,j,_)}if(typeof j.then=="function")return be(T,b,Au(j),_);if(j.$$typeof===B)return be(T,b,hu(T,j),_);Ru(T,j)}return typeof j=="string"&&j!==""||typeof j=="number"||typeof j=="bigint"?(j=""+j,b!==null&&b.tag===6?(l(T,b.sibling),_=n(b,j),_.return=T,T=_):(l(T,b),_=$i(j,T.mode,_),_.return=T,T=_),c(T)):l(T,b)}return function(T,b,j,_){try{mn=0;var k=be(T,b,j,_);return xa=null,k}catch(W){if(W===nn||W===yu)throw W;var ie=st(29,W,null,T.mode);return ie.lanes=_,ie.return=T,ie}finally{}}}var Sa=Wf(!0),Ff=Wf(!1),Et=L(null),_t=null;function hl(e){var t=e.alternate;X(He,He.current&1),X(Et,e),_t===null&&(t===null||ga.current!==null||t.memoizedState!==null)&&(_t=e)}function Pf(e){if(e.tag===22){if(X(He,He.current),X(Et,e),_t===null){var t=e.alternate;t!==null&&t.memoizedState!==null&&(_t=e)}}else ml()}function ml(){X(He,He.current),X(Et,Et.current)}function kt(e){K(Et),_t===e&&(_t=null),K(He)}var He=L(0);function Ou(e){for(var t=e;t!==null;){if(t.tag===13){var l=t.memoizedState;if(l!==null&&(l=l.dehydrated,l===null||l.data==="$?"||pr(l)))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&128)!==0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}function Mc(e,t,l,a){t=e.memoizedState,l=l(a,t),l=l==null?t:N({},t,l),e.memoizedState=l,e.lanes===0&&(e.updateQueue.baseState=l)}var Dc={enqueueSetState:function(e,t,l){e=e._reactInternals;var a=ht(),n=fl(a);n.payload=t,l!=null&&(n.callback=l),t=ol(e,n,a),t!==null&&(mt(t,e,a),cn(t,e,a))},enqueueReplaceState:function(e,t,l){e=e._reactInternals;var a=ht(),n=fl(a);n.tag=1,n.payload=t,l!=null&&(n.callback=l),t=ol(e,n,a),t!==null&&(mt(t,e,a),cn(t,e,a))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var l=ht(),a=fl(l);a.tag=2,t!=null&&(a.callback=t),t=ol(e,a,l),t!==null&&(mt(t,e,l),cn(t,e,l))}};function If(e,t,l,a,n,u,c){return e=e.stateNode,typeof e.shouldComponentUpdate=="function"?e.shouldComponentUpdate(a,u,c):t.prototype&&t.prototype.isPureReactComponent?!Wa(l,a)||!Wa(n,u):!0}function eo(e,t,l,a){e=t.state,typeof t.componentWillReceiveProps=="function"&&t.componentWillReceiveProps(l,a),typeof t.UNSAFE_componentWillReceiveProps=="function"&&t.UNSAFE_componentWillReceiveProps(l,a),t.state!==e&&Dc.enqueueReplaceState(t,t.state,null)}function Kl(e,t){var l=t;if("ref"in t){l={};for(var a in t)a!=="ref"&&(l[a]=t[a])}if(e=e.defaultProps){l===t&&(l=N({},l));for(var n in e)l[n]===void 0&&(l[n]=e[n])}return l}var Mu=typeof reportError=="function"?reportError:function(e){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof e=="object"&&e!==null&&typeof e.message=="string"?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",e);return}console.error(e)};function to(e){Mu(e)}function lo(e){console.error(e)}function ao(e){Mu(e)}function Du(e,t){try{var l=e.onUncaughtError;l(t.value,{componentStack:t.stack})}catch(a){setTimeout(function(){throw a})}}function no(e,t,l){try{var a=e.onCaughtError;a(l.value,{componentStack:l.stack,errorBoundary:t.tag===1?t.stateNode:null})}catch(n){setTimeout(function(){throw n})}}function zc(e,t,l){return l=fl(l),l.tag=3,l.payload={element:null},l.callback=function(){Du(e,t)},l}function uo(e){return e=fl(e),e.tag=3,e}function io(e,t,l,a){var n=l.type.getDerivedStateFromError;if(typeof n=="function"){var u=a.value;e.payload=function(){return n(u)},e.callback=function(){no(t,l,a)}}var c=l.stateNode;c!==null&&typeof c.componentDidCatch=="function"&&(e.callback=function(){no(t,l,a),typeof n!="function"&&(xl===null?xl=new Set([this]):xl.add(this));var f=a.stack;this.componentDidCatch(a.value,{componentStack:f!==null?f:""})})}function Vm(e,t,l,a,n){if(l.flags|=32768,a!==null&&typeof a=="object"&&typeof a.then=="function"){if(t=l.alternate,t!==null&&tn(t,l,n,!0),l=Et.current,l!==null){switch(l.tag){case 13:return _t===null?tr():l.alternate===null&&Oe===0&&(Oe=3),l.flags&=-257,l.flags|=65536,l.lanes=n,a===ic?l.flags|=16384:(t=l.updateQueue,t===null?l.updateQueue=new Set([a]):t.add(a),ar(e,a,n)),!1;case 22:return l.flags|=65536,a===ic?l.flags|=16384:(t=l.updateQueue,t===null?(t={transitions:null,markerInstances:null,retryQueue:new Set([a])},l.updateQueue=t):(l=t.retryQueue,l===null?t.retryQueue=new Set([a]):l.add(a)),ar(e,a,n)),!1}throw Error(s(435,l.tag))}return ar(e,a,n),tr(),!1}if(he)return t=Et.current,t!==null?((t.flags&65536)===0&&(t.flags|=256),t.flags|=65536,t.lanes=n,a!==Ii&&(e=Error(s(422),{cause:a}),en(pt(e,l)))):(a!==Ii&&(t=Error(s(423),{cause:a}),en(pt(t,l))),e=e.current.alternate,e.flags|=65536,n&=-n,e.lanes|=n,a=pt(a,l),n=zc(e.stateNode,a,n),sc(e,n),Oe!==4&&(Oe=2)),!1;var u=Error(s(520),{cause:a});if(u=pt(u,l),En===null?En=[u]:En.push(u),Oe!==4&&(Oe=2),t===null)return!0;a=pt(a,l),l=t;do{switch(l.tag){case 3:return l.flags|=65536,e=n&-n,l.lanes|=e,e=zc(l.stateNode,a,e),sc(l,e),!1;case 1:if(t=l.type,u=l.stateNode,(l.flags&128)===0&&(typeof t.getDerivedStateFromError=="function"||u!==null&&typeof u.componentDidCatch=="function"&&(xl===null||!xl.has(u))))return l.flags|=65536,n&=-n,l.lanes|=n,n=uo(n),io(n,e,l,a),sc(l,n),!1}l=l.return}while(l!==null);return!1}var co=Error(s(461)),qe=!1;function Ze(e,t,l,a){t.child=e===null?Ff(t,null,l,a):Sa(t,e.child,l,a)}function ro(e,t,l,a,n){l=l.render;var u=t.ref;if("ref"in a){var c={};for(var f in a)f!=="ref"&&(c[f]=a[f])}else c=a;return Ql(t),a=mc(e,t,l,c,u,n),f=yc(),e!==null&&!qe?(gc(e,t,n),$t(e,t,n)):(he&&f&&Fi(t),t.flags|=1,Ze(e,t,a,n),t.child)}function so(e,t,l,a,n){if(e===null){var u=l.type;return typeof u=="function"&&!ki(u)&&u.defaultProps===void 0&&l.compare===null?(t.tag=15,t.type=u,fo(e,t,u,a,n)):(e=su(l.type,null,a,t,t.mode,n),e.ref=t.ref,e.return=t,t.child=e)}if(u=e.child,!qc(e,n)){var c=u.memoizedProps;if(l=l.compare,l=l!==null?l:Wa,l(c,a)&&e.ref===t.ref)return $t(e,t,n)}return t.flags|=1,e=Xt(u,a),e.ref=t.ref,e.return=t,t.child=e}function fo(e,t,l,a,n){if(e!==null){var u=e.memoizedProps;if(Wa(u,a)&&e.ref===t.ref)if(qe=!1,t.pendingProps=a=u,qc(e,n))(e.flags&131072)!==0&&(qe=!0);else return t.lanes=e.lanes,$t(e,t,n)}return Cc(e,t,l,a,n)}function oo(e,t,l){var a=t.pendingProps,n=a.children,u=e!==null?e.memoizedState:null;if(a.mode==="hidden"){if((t.flags&128)!==0){if(a=u!==null?u.baseLanes|l:l,e!==null){for(n=t.child=e.child,u=0;n!==null;)u=u|n.lanes|n.childLanes,n=n.sibling;t.childLanes=u&~a}else t.childLanes=0,t.child=null;return ho(e,t,a,l)}if((l&536870912)!==0)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&mu(t,u!==null?u.cachePool:null),u!==null?ff(t,u):oc(),Pf(t);else return t.lanes=t.childLanes=536870912,ho(e,t,u!==null?u.baseLanes|l:l,l)}else u!==null?(mu(t,u.cachePool),ff(t,u),ml(),t.memoizedState=null):(e!==null&&mu(t,null),oc(),ml());return Ze(e,t,n,l),t.child}function ho(e,t,l,a){var n=uc();return n=n===null?null:{parent:we._currentValue,pool:n},t.memoizedState={baseLanes:l,cachePool:n},e!==null&&mu(t,null),oc(),Pf(t),e!==null&&tn(e,t,a,!0),null}function zu(e,t){var l=t.ref;if(l===null)e!==null&&e.ref!==null&&(t.flags|=4194816);else{if(typeof l!="function"&&typeof l!="object")throw Error(s(284));(e===null||e.ref!==l)&&(t.flags|=4194816)}}function Cc(e,t,l,a,n){return Ql(t),l=mc(e,t,l,a,void 0,n),a=yc(),e!==null&&!qe?(gc(e,t,n),$t(e,t,n)):(he&&a&&Fi(t),t.flags|=1,Ze(e,t,l,n),t.child)}function mo(e,t,l,a,n,u){return Ql(t),t.updateQueue=null,l=df(t,a,l,n),of(e),a=yc(),e!==null&&!qe?(gc(e,t,u),$t(e,t,u)):(he&&a&&Fi(t),t.flags|=1,Ze(e,t,l,u),t.child)}function yo(e,t,l,a,n){if(Ql(t),t.stateNode===null){var u=oa,c=l.contextType;typeof c=="object"&&c!==null&&(u=We(c)),u=new l(a,u),t.memoizedState=u.state!==null&&u.state!==void 0?u.state:null,u.updater=Dc,t.stateNode=u,u._reactInternals=t,u=t.stateNode,u.props=a,u.state=t.memoizedState,u.refs={},cc(t),c=l.contextType,u.context=typeof c=="object"&&c!==null?We(c):oa,u.state=t.memoizedState,c=l.getDerivedStateFromProps,typeof c=="function"&&(Mc(t,l,c,a),u.state=t.memoizedState),typeof l.getDerivedStateFromProps=="function"||typeof u.getSnapshotBeforeUpdate=="function"||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(c=u.state,typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount(),c!==u.state&&Dc.enqueueReplaceState(u,u.state,null),sn(t,a,u,n),rn(),u.state=t.memoizedState),typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!0}else if(e===null){u=t.stateNode;var f=t.memoizedProps,g=Kl(l,f);u.props=g;var A=u.context,z=l.contextType;c=oa,typeof z=="object"&&z!==null&&(c=We(z));var w=l.getDerivedStateFromProps;z=typeof w=="function"||typeof u.getSnapshotBeforeUpdate=="function",f=t.pendingProps!==f,z||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(f||A!==c)&&eo(t,u,a,c),sl=!1;var O=t.memoizedState;u.state=O,sn(t,a,u,n),rn(),A=t.memoizedState,f||O!==A||sl?(typeof w=="function"&&(Mc(t,l,w,a),A=t.memoizedState),(g=sl||If(t,l,g,a,O,A,c))?(z||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount()),typeof u.componentDidMount=="function"&&(t.flags|=4194308)):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=a,t.memoizedState=A),u.props=a,u.state=A,u.context=c,a=g):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!1)}else{u=t.stateNode,rc(e,t),c=t.memoizedProps,z=Kl(l,c),u.props=z,w=t.pendingProps,O=u.context,A=l.contextType,g=oa,typeof A=="object"&&A!==null&&(g=We(A)),f=l.getDerivedStateFromProps,(A=typeof f=="function"||typeof u.getSnapshotBeforeUpdate=="function")||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(c!==w||O!==g)&&eo(t,u,a,g),sl=!1,O=t.memoizedState,u.state=O,sn(t,a,u,n),rn();var M=t.memoizedState;c!==w||O!==M||sl||e!==null&&e.dependencies!==null&&du(e.dependencies)?(typeof f=="function"&&(Mc(t,l,f,a),M=t.memoizedState),(z=sl||If(t,l,z,a,O,M,g)||e!==null&&e.dependencies!==null&&du(e.dependencies))?(A||typeof u.UNSAFE_componentWillUpdate!="function"&&typeof u.componentWillUpdate!="function"||(typeof u.componentWillUpdate=="function"&&u.componentWillUpdate(a,M,g),typeof u.UNSAFE_componentWillUpdate=="function"&&u.UNSAFE_componentWillUpdate(a,M,g)),typeof u.componentDidUpdate=="function"&&(t.flags|=4),typeof u.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof u.componentDidUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=1024),t.memoizedProps=a,t.memoizedState=M),u.props=a,u.state=M,u.context=g,a=z):(typeof u.componentDidUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=1024),a=!1)}return u=a,zu(e,t),a=(t.flags&128)!==0,u||a?(u=t.stateNode,l=a&&typeof l.getDerivedStateFromError!="function"?null:u.render(),t.flags|=1,e!==null&&a?(t.child=Sa(t,e.child,null,n),t.child=Sa(t,null,l,n)):Ze(e,t,l,n),t.memoizedState=u.state,e=t.child):e=$t(e,t,n),e}function go(e,t,l,a){return Ia(),t.flags|=256,Ze(e,t,l,a),t.child}var _c={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Uc(e){return{baseLanes:e,cachePool:tf()}}function wc(e,t,l){return e=e!==null?e.childLanes&~l:0,t&&(e|=Tt),e}function vo(e,t,l){var a=t.pendingProps,n=!1,u=(t.flags&128)!==0,c;if((c=u)||(c=e!==null&&e.memoizedState===null?!1:(He.current&2)!==0),c&&(n=!0,t.flags&=-129),c=(t.flags&32)!==0,t.flags&=-33,e===null){if(he){if(n?hl(t):ml(),he){var f=Re,g;if(g=f){e:{for(g=f,f=Ct;g.nodeType!==8;){if(!f){f=null;break e}if(g=Mt(g.nextSibling),g===null){f=null;break e}}f=g}f!==null?(t.memoizedState={dehydrated:f,treeContext:Bl!==null?{id:Qt,overflow:Zt}:null,retryLane:536870912,hydrationErrors:null},g=st(18,null,null,0),g.stateNode=f,g.return=t,t.child=g,Pe=t,Re=null,g=!0):g=!1}g||Gl(t)}if(f=t.memoizedState,f!==null&&(f=f.dehydrated,f!==null))return pr(f)?t.lanes=32:t.lanes=536870912,null;kt(t)}return f=a.children,a=a.fallback,n?(ml(),n=t.mode,f=Cu({mode:"hidden",children:f},n),a=Ll(a,n,l,null),f.return=t,a.return=t,f.sibling=a,t.child=f,n=t.child,n.memoizedState=Uc(l),n.childLanes=wc(e,c,l),t.memoizedState=_c,a):(hl(t),Hc(t,f))}if(g=e.memoizedState,g!==null&&(f=g.dehydrated,f!==null)){if(u)t.flags&256?(hl(t),t.flags&=-257,t=Lc(e,t,l)):t.memoizedState!==null?(ml(),t.child=e.child,t.flags|=128,t=null):(ml(),n=a.fallback,f=t.mode,a=Cu({mode:"visible",children:a.children},f),n=Ll(n,f,l,null),n.flags|=2,a.return=t,n.return=t,a.sibling=n,t.child=a,Sa(t,e.child,null,l),a=t.child,a.memoizedState=Uc(l),a.childLanes=wc(e,c,l),t.memoizedState=_c,t=n);else if(hl(t),pr(f)){if(c=f.nextSibling&&f.nextSibling.dataset,c)var A=c.dgst;c=A,a=Error(s(419)),a.stack="",a.digest=c,en({value:a,source:null,stack:null}),t=Lc(e,t,l)}else if(qe||tn(e,t,l,!1),c=(l&e.childLanes)!==0,qe||c){if(c=Ee,c!==null&&(a=l&-l,a=(a&42)!==0?1:bi(a),a=(a&(c.suspendedLanes|l))!==0?0:a,a!==0&&a!==g.retryLane))throw g.retryLane=a,fa(e,a),mt(c,e,a),co;f.data==="$?"||tr(),t=Lc(e,t,l)}else f.data==="$?"?(t.flags|=192,t.child=e.child,t=null):(e=g.treeContext,Re=Mt(f.nextSibling),Pe=t,he=!0,Yl=null,Ct=!1,e!==null&&(xt[St++]=Qt,xt[St++]=Zt,xt[St++]=Bl,Qt=e.id,Zt=e.overflow,Bl=t),t=Hc(t,a.children),t.flags|=4096);return t}return n?(ml(),n=a.fallback,f=t.mode,g=e.child,A=g.sibling,a=Xt(g,{mode:"hidden",children:a.children}),a.subtreeFlags=g.subtreeFlags&65011712,A!==null?n=Xt(A,n):(n=Ll(n,f,l,null),n.flags|=2),n.return=t,a.return=t,a.sibling=n,t.child=a,a=n,n=t.child,f=e.child.memoizedState,f===null?f=Uc(l):(g=f.cachePool,g!==null?(A=we._currentValue,g=g.parent!==A?{parent:A,pool:A}:g):g=tf(),f={baseLanes:f.baseLanes|l,cachePool:g}),n.memoizedState=f,n.childLanes=wc(e,c,l),t.memoizedState=_c,a):(hl(t),l=e.child,e=l.sibling,l=Xt(l,{mode:"visible",children:a.children}),l.return=t,l.sibling=null,e!==null&&(c=t.deletions,c===null?(t.deletions=[e],t.flags|=16):c.push(e)),t.child=l,t.memoizedState=null,l)}function Hc(e,t){return t=Cu({mode:"visible",children:t},e.mode),t.return=e,e.child=t}function Cu(e,t){return e=st(22,e,null,t),e.lanes=0,e.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},e}function Lc(e,t,l){return Sa(t,e.child,null,l),e=Hc(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function po(e,t,l){e.lanes|=t;var a=e.alternate;a!==null&&(a.lanes|=t),tc(e.return,t,l)}function Bc(e,t,l,a,n){var u=e.memoizedState;u===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:a,tail:l,tailMode:n}:(u.isBackwards=t,u.rendering=null,u.renderingStartTime=0,u.last=a,u.tail=l,u.tailMode=n)}function bo(e,t,l){var a=t.pendingProps,n=a.revealOrder,u=a.tail;if(Ze(e,t,a.children,l),a=He.current,(a&2)!==0)a=a&1|2,t.flags|=128;else{if(e!==null&&(e.flags&128)!==0)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&po(e,l,t);else if(e.tag===19)po(e,l,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}a&=1}switch(X(He,a),n){case"forwards":for(l=t.child,n=null;l!==null;)e=l.alternate,e!==null&&Ou(e)===null&&(n=l),l=l.sibling;l=n,l===null?(n=t.child,t.child=null):(n=l.sibling,l.sibling=null),Bc(t,!1,n,l,u);break;case"backwards":for(l=null,n=t.child,t.child=null;n!==null;){if(e=n.alternate,e!==null&&Ou(e)===null){t.child=n;break}e=n.sibling,n.sibling=l,l=n,n=e}Bc(t,!0,l,null,u);break;case"together":Bc(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function $t(e,t,l){if(e!==null&&(t.dependencies=e.dependencies),bl|=t.lanes,(l&t.childLanes)===0)if(e!==null){if(tn(e,t,l,!1),(l&t.childLanes)===0)return null}else return null;if(e!==null&&t.child!==e.child)throw Error(s(153));if(t.child!==null){for(e=t.child,l=Xt(e,e.pendingProps),t.child=l,l.return=t;e.sibling!==null;)e=e.sibling,l=l.sibling=Xt(e,e.pendingProps),l.return=t;l.sibling=null}return t.child}function qc(e,t){return(e.lanes&t)!==0?!0:(e=e.dependencies,!!(e!==null&&du(e)))}function Km(e,t,l){switch(t.tag){case 3:Te(t,t.stateNode.containerInfo),rl(t,we,e.memoizedState.cache),Ia();break;case 27:case 5:mi(t);break;case 4:Te(t,t.stateNode.containerInfo);break;case 10:rl(t,t.type,t.memoizedProps.value);break;case 13:var a=t.memoizedState;if(a!==null)return a.dehydrated!==null?(hl(t),t.flags|=128,null):(l&t.child.childLanes)!==0?vo(e,t,l):(hl(t),e=$t(e,t,l),e!==null?e.sibling:null);hl(t);break;case 19:var n=(e.flags&128)!==0;if(a=(l&t.childLanes)!==0,a||(tn(e,t,l,!1),a=(l&t.childLanes)!==0),n){if(a)return bo(e,t,l);t.flags|=128}if(n=t.memoizedState,n!==null&&(n.rendering=null,n.tail=null,n.lastEffect=null),X(He,He.current),a)break;return null;case 22:case 23:return t.lanes=0,oo(e,t,l);case 24:rl(t,we,e.memoizedState.cache)}return $t(e,t,l)}function xo(e,t,l){if(e!==null)if(e.memoizedProps!==t.pendingProps)qe=!0;else{if(!qc(e,l)&&(t.flags&128)===0)return qe=!1,Km(e,t,l);qe=(e.flags&131072)!==0}else qe=!1,he&&(t.flags&1048576)!==0&&ks(t,ou,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var a=t.elementType,n=a._init;if(a=n(a._payload),t.type=a,typeof a=="function")ki(a)?(e=Kl(a,e),t.tag=1,t=yo(null,t,a,e,l)):(t.tag=0,t=Cc(null,t,a,e,l));else{if(a!=null){if(n=a.$$typeof,n===$){t.tag=11,t=ro(null,t,a,e,l);break e}else if(n===V){t.tag=14,t=so(null,t,a,e,l);break e}}throw t=zl(a)||a,Error(s(306,t,""))}}return t;case 0:return Cc(e,t,t.type,t.pendingProps,l);case 1:return a=t.type,n=Kl(a,t.pendingProps),yo(e,t,a,n,l);case 3:e:{if(Te(t,t.stateNode.containerInfo),e===null)throw Error(s(387));a=t.pendingProps;var u=t.memoizedState;n=u.element,rc(e,t),sn(t,a,null,l);var c=t.memoizedState;if(a=c.cache,rl(t,we,a),a!==u.cache&&lc(t,[we],l,!0),rn(),a=c.element,u.isDehydrated)if(u={element:a,isDehydrated:!1,cache:c.cache},t.updateQueue.baseState=u,t.memoizedState=u,t.flags&256){t=go(e,t,a,l);break e}else if(a!==n){n=pt(Error(s(424)),t),en(n),t=go(e,t,a,l);break e}else{switch(e=t.stateNode.containerInfo,e.nodeType){case 9:e=e.body;break;default:e=e.nodeName==="HTML"?e.ownerDocument.body:e}for(Re=Mt(e.firstChild),Pe=t,he=!0,Yl=null,Ct=!0,l=Ff(t,null,a,l),t.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling}else{if(Ia(),a===n){t=$t(e,t,l);break e}Ze(e,t,a,l)}t=t.child}return t;case 26:return zu(e,t),e===null?(l=jd(t.type,null,t.pendingProps,null))?t.memoizedState=l:he||(l=t.type,e=t.pendingProps,a=Ku(ae.current).createElement(l),a[$e]=t,a[Ie]=e,Ke(a,l,e),Be(a),t.stateNode=a):t.memoizedState=jd(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return mi(t),e===null&&he&&(a=t.stateNode=Sd(t.type,t.pendingProps,ae.current),Pe=t,Ct=!0,n=Re,Tl(t.type)?(br=n,Re=Mt(a.firstChild)):Re=n),Ze(e,t,t.pendingProps.children,l),zu(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&he&&((n=a=Re)&&(a=xy(a,t.type,t.pendingProps,Ct),a!==null?(t.stateNode=a,Pe=t,Re=Mt(a.firstChild),Ct=!1,n=!0):n=!1),n||Gl(t)),mi(t),n=t.type,u=t.pendingProps,c=e!==null?e.memoizedProps:null,a=u.children,yr(n,u)?a=null:c!==null&&yr(n,c)&&(t.flags|=32),t.memoizedState!==null&&(n=mc(e,t,Bm,null,null,l),zn._currentValue=n),zu(e,t),Ze(e,t,a,l),t.child;case 6:return e===null&&he&&((e=l=Re)&&(l=Sy(l,t.pendingProps,Ct),l!==null?(t.stateNode=l,Pe=t,Re=null,e=!0):e=!1),e||Gl(t)),null;case 13:return vo(e,t,l);case 4:return Te(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=Sa(t,null,a,l):Ze(e,t,a,l),t.child;case 11:return ro(e,t,t.type,t.pendingProps,l);case 7:return Ze(e,t,t.pendingProps,l),t.child;case 8:return Ze(e,t,t.pendingProps.children,l),t.child;case 12:return Ze(e,t,t.pendingProps.children,l),t.child;case 10:return a=t.pendingProps,rl(t,t.type,a.value),Ze(e,t,a.children,l),t.child;case 9:return n=t.type._context,a=t.pendingProps.children,Ql(t),n=We(n),a=a(n),t.flags|=1,Ze(e,t,a,l),t.child;case 14:return so(e,t,t.type,t.pendingProps,l);case 15:return fo(e,t,t.type,t.pendingProps,l);case 19:return bo(e,t,l);case 31:return a=t.pendingProps,l=t.mode,a={mode:a.mode,children:a.children},e===null?(l=Cu(a,l),l.ref=t.ref,t.child=l,l.return=t,t=l):(l=Xt(e.child,a),l.ref=t.ref,t.child=l,l.return=t,t=l),t;case 22:return oo(e,t,l);case 24:return Ql(t),a=We(we),e===null?(n=uc(),n===null&&(n=Ee,u=ac(),n.pooledCache=u,u.refCount++,u!==null&&(n.pooledCacheLanes|=l),n=u),t.memoizedState={parent:a,cache:n},cc(t),rl(t,we,n)):((e.lanes&l)!==0&&(rc(e,t),sn(t,null,null,l),rn()),n=e.memoizedState,u=t.memoizedState,n.parent!==a?(n={parent:a,cache:a},t.memoizedState=n,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=n),rl(t,we,a)):(a=u.cache,rl(t,we,a),a!==n.cache&&lc(t,[we],l,!0))),Ze(e,t,t.pendingProps.children,l),t.child;case 29:throw t.pendingProps}throw Error(s(156,t.tag))}function Wt(e){e.flags|=4}function So(e,t){if(t.type!=="stylesheet"||(t.state.loading&4)!==0)e.flags&=-16777217;else if(e.flags|=16777216,!Md(t)){if(t=Et.current,t!==null&&((se&4194048)===se?_t!==null:(se&62914560)!==se&&(se&536870912)===0||t!==_t))throw un=ic,lf;e.flags|=8192}}function _u(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag!==22?Fr():536870912,e.lanes|=t,Na|=t)}function gn(e,t){if(!he)switch(e.tailMode){case"hidden":t=e.tail;for(var l=null;t!==null;)t.alternate!==null&&(l=t),t=t.sibling;l===null?e.tail=null:l.sibling=null;break;case"collapsed":l=e.tail;for(var a=null;l!==null;)l.alternate!==null&&(a=l),l=l.sibling;a===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:a.sibling=null}}function Ne(e){var t=e.alternate!==null&&e.alternate.child===e.child,l=0,a=0;if(t)for(var n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags&65011712,a|=n.flags&65011712,n.return=e,n=n.sibling;else for(n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags,a|=n.flags,n.return=e,n=n.sibling;return e.subtreeFlags|=a,e.childLanes=l,t}function Jm(e,t,l){var a=t.pendingProps;switch(Pi(t),t.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Ne(t),null;case 1:return Ne(t),null;case 3:return l=t.stateNode,a=null,e!==null&&(a=e.memoizedState.cache),t.memoizedState.cache!==a&&(t.flags|=2048),Kt(we),nl(),l.pendingContext&&(l.context=l.pendingContext,l.pendingContext=null),(e===null||e.child===null)&&(Pa(t)?Wt(t):e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,Fs())),Ne(t),null;case 26:return l=t.memoizedState,e===null?(Wt(t),l!==null?(Ne(t),So(t,l)):(Ne(t),t.flags&=-16777217)):l?l!==e.memoizedState?(Wt(t),Ne(t),So(t,l)):(Ne(t),t.flags&=-16777217):(e.memoizedProps!==a&&Wt(t),Ne(t),t.flags&=-16777217),null;case 27:Zn(t),l=ae.current;var n=t.type;if(e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Wt(t);else{if(!a){if(t.stateNode===null)throw Error(s(166));return Ne(t),null}e=F.current,Pa(t)?$s(t):(e=Sd(n,a,l),t.stateNode=e,Wt(t))}return Ne(t),null;case 5:if(Zn(t),l=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Wt(t);else{if(!a){if(t.stateNode===null)throw Error(s(166));return Ne(t),null}if(e=F.current,Pa(t))$s(t);else{switch(n=Ku(ae.current),e){case 1:e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case 2:e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;default:switch(l){case"svg":e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case"math":e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;case"script":e=n.createElement("div"),e.innerHTML=" - + +
diff --git a/dependencies.lock b/dependencies.lock index c242beb..f9c0a96 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -56,7 +56,7 @@ dependencies: idf: source: type: idf - version: 5.4.3 + version: 5.5.2 direct_dependencies: - espressif/cjson - espressif/esp-modbus diff --git a/main/main.c b/main/main.c index 2786a85..52d65f0 100755 --- a/main/main.c +++ b/main/main.c @@ -18,7 +18,6 @@ #include "network.h" #include "board_config.h" -#include "logger.h" #include "rest_main.h" #include "peripherals.h" @@ -274,21 +273,21 @@ static void init_modules(void) ESP_ERROR_CHECK(storage_service_init()); peripherals_init(); - led_init(); wifi_ini(); // garante wifi_event_group inicializado buzzer_init(); + led_init(); ESP_ERROR_CHECK(rest_server_init("/data")); - + evse_manager_init(); evse_init(); auth_init(); - loadbalancer_init(); meter_manager_init(); meter_manager_start(); - evse_link_init(); ocpp_start(); scheduler_init(); protocols_init(); + loadbalancer_init(); + evse_link_init(); } // @@ -296,8 +295,6 @@ static void init_modules(void) // void app_main(void) { - logger_init(); - esp_log_set_vprintf(logger_vprintf); esp_reset_reason_t reason = esp_reset_reason(); ESP_LOGI(TAG, "Reset reason: %d", reason); diff --git a/projeto_parte1.c b/projeto_parte1.c index ced03e8..9180035 100644 --- a/projeto_parte1.c +++ b/projeto_parte1.c @@ -1,1183 +1,1344 @@ . -// === Início de: components/storage_service/src/storage_service.c === -// ========================= -// storage_service.c (corrigido) -// - Remove ponteiros out_ptr/inout_len_ptr da mensagem -// - GET_STR/GET_BLOB devolvem dados via storage_resp_t (cópia) -// - Evita corrupção se houver timeout -// ========================= - -#include "storage_service.h" - +// === Início de: main/main.c === +// === Início de: main/main.c === #include #include - -#include "esp_log.h" -#include "esp_err.h" -#include "nvs.h" +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "freertos/queue.h" -#include "freertos/semphr.h" +#include "freertos/event_groups.h" -static const char *TAG = "storage_service"; +#include "esp_log.h" +#include "esp_err.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_spiffs.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "driver/gpio.h" -typedef enum +#include "network.h" +#include "board_config.h" +#include "rest_main.h" + +#include "peripherals.h" +#include "protocols.h" +#include "evse_manager.h" +#include "evse_core.h" +#include "auth.h" +#include "loadbalancer.h" +#include "meter_manager.h" +#include "buzzer.h" +#include "evse_link.h" +#include "ocpp.h" +#include "led.h" +#include "scheduler.h" +#include "storage_service.h" + +#define AP_CONNECTION_TIMEOUT 120000 +#define RESET_HOLD_TIME 30000 // ms +#define DEBOUNCE_TIME_MS 50 + +#define PRESS_BIT BIT0 +#define RELEASED_BIT BIT1 + +static const char *TAG = "app_main"; + +static TaskHandle_t user_input_task = NULL; +static TickType_t press_tick = 0; +static volatile TickType_t last_interrupt_tick = 0; +static bool pressed = false; + +// Spinlock para garantir debounce seguro na ISR +static portMUX_TYPE button_spinlock = portMUX_INITIALIZER_UNLOCKED; + +// +// File system (SPIFFS) init and info +// +static void fs_info(esp_vfs_spiffs_conf_t *conf) { - OP_SET_U8, - OP_SET_U16, - OP_SET_U32, - OP_SET_STR, - OP_SET_BLOB, - - OP_ERASE_KEY, - - OP_GET_U8, - OP_GET_U16, - OP_GET_U32, - OP_GET_STR, - OP_GET_BLOB, - - OP_FLUSH, -} storage_op_t; - -typedef enum -{ - T_U8, - T_U16, - T_U32, - T_STR, - T_BLOB, -} storage_type_t; - -typedef struct -{ - esp_err_t err; - uint32_t value; // U8/U16/U32 - size_t len; // STR/BLOB: tamanho (STR sem '\0', BLOB tamanho real) - uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // payload para STR/BLOB (até MAX) -} storage_resp_t; - -typedef struct -{ - storage_op_t op; - char ns[STORAGE_NS_MAX_LEN]; - char key[STORAGE_KEY_MAX_LEN]; - - // SET/GET numéricos - uint32_t value; - - // SET STR/BLOB: bytes e len (copiados na mensagem => seguro async) - uint16_t len; - uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; - - // Apenas para ajudar semântica do GET_BLOB (query vs read) - bool blob_query_only; - - QueueHandle_t resp_q; // opcional (GET/FLUSH sync) -} storage_msg_t; - -typedef struct -{ - bool used; - bool erase; - char ns[STORAGE_NS_MAX_LEN]; - char key[STORAGE_KEY_MAX_LEN]; - storage_type_t type; - - uint32_t value; // U8/U16/U32 - uint16_t len; // STR/BLOB (até MAX) - uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // STR/BLOB -} pending_item_t; - -static bool s_inited = false; - -// Queue principal estática (evita malloc) -static StaticQueue_t s_qbuf; -static uint8_t s_qstorage[STORAGE_QUEUE_LEN * sizeof(storage_msg_t)]; -static QueueHandle_t s_q = NULL; - -// Pending table (evita malloc) -static pending_item_t s_pending[STORAGE_MAX_PENDING]; -static size_t s_pending_count = 0; - -// debounce -static bool s_dirty = false; -static TickType_t s_commit_deadline = 0; - -// Sync: fila global + mutex global -static StaticQueue_t s_sync_qbuf; -static uint8_t s_sync_qstor[sizeof(storage_resp_t)]; -static QueueHandle_t s_sync_q = NULL; - -static StaticSemaphore_t s_sync_mtx_buf; -static SemaphoreHandle_t s_sync_mtx = NULL; - -static inline esp_err_t map_not_found(esp_err_t e) -{ - return (e == ESP_ERR_NVS_NOT_FOUND) ? ESP_ERR_NOT_FOUND : e; -} - -static bool safe_copy_str(char *dst, size_t dst_sz, const char *src) -{ - if (!dst || dst_sz == 0 || !src) - return false; - size_t n = strnlen(src, dst_sz); - if (n >= dst_sz) - return false; - memcpy(dst, src, n); - dst[n] = '\0'; - return true; -} - -static int find_pending(const char *ns, const char *key) -{ - for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + size_t total = 0, used = 0; + esp_err_t ret = esp_spiffs_info(conf->partition_label, &total, &used); + if (ret == ESP_OK) { - if (!s_pending[i].used) - continue; - if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) == 0 && - strncmp(s_pending[i].key, key, STORAGE_KEY_MAX_LEN) == 0) - { - return i; - } + ESP_LOGI(TAG, "Partition %s: total: %d, used: %d", + conf->partition_label, (int)total, (int)used); } - return -1; -} - -static bool pending_is_erased(const char *ns, const char *key) -{ - int idx = find_pending(ns, key); - if (idx < 0) - return false; - return s_pending[idx].used && s_pending[idx].erase; -} - -static int alloc_pending_slot(void) -{ - for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) - { - if (!s_pending[i].used) - return i; - } - return -1; -} - -static void mark_dirty(void) -{ - s_dirty = true; - s_commit_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(STORAGE_COMMIT_DEBOUNCE_MS); -} - -static esp_err_t ensure_pending_entry(int *out_idx, const char *ns, const char *key) -{ - int idx = find_pending(ns, key); - if (idx < 0) - { - idx = alloc_pending_slot(); - if (idx < 0) - return ESP_ERR_NO_MEM; - - memset(&s_pending[idx], 0, sizeof(s_pending[idx])); - s_pending[idx].used = true; - - if (!safe_copy_str(s_pending[idx].ns, sizeof(s_pending[idx].ns), ns)) - { - s_pending[idx].used = false; - return ESP_ERR_INVALID_ARG; - } - if (!safe_copy_str(s_pending[idx].key, sizeof(s_pending[idx].key), key)) - { - s_pending[idx].used = false; - return ESP_ERR_INVALID_ARG; - } - s_pending_count++; - } - if (out_idx) - *out_idx = idx; - return ESP_OK; -} - -static esp_err_t pending_set_num(const char *ns, const char *key, storage_type_t type, uint32_t v) -{ - int idx = -1; - esp_err_t err = ensure_pending_entry(&idx, ns, key); - if (err != ESP_OK) - return err; - - s_pending[idx].erase = false; - s_pending[idx].type = type; - s_pending[idx].value = v; - s_pending[idx].len = 0; - - mark_dirty(); - return ESP_OK; -} - -static esp_err_t pending_set_bytes(const char *ns, const char *key, storage_type_t type, - const uint8_t *bytes, uint16_t len) -{ - if (!bytes && len > 0) - return ESP_ERR_INVALID_ARG; - if (len > STORAGE_MAX_VALUE_BYTES) - return ESP_ERR_INVALID_SIZE; - - int idx = -1; - esp_err_t err = ensure_pending_entry(&idx, ns, key); - if (err != ESP_OK) - return err; - - s_pending[idx].erase = false; - s_pending[idx].type = type; - s_pending[idx].value = 0; - s_pending[idx].len = len; - - if (len > 0) - memcpy(s_pending[idx].bytes, bytes, len); - if (len < STORAGE_MAX_VALUE_BYTES) - memset(&s_pending[idx].bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); - - mark_dirty(); - return ESP_OK; -} - -static esp_err_t pending_erase(const char *ns, const char *key) -{ - int idx = -1; - esp_err_t err = ensure_pending_entry(&idx, ns, key); - if (err != ESP_OK) - return err; - - s_pending[idx].erase = true; - mark_dirty(); - return ESP_OK; -} - -static bool pending_get_num(const char *ns, const char *key, storage_type_t type, uint32_t *out) -{ - int idx = find_pending(ns, key); - if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type) - return false; - - if (out) - *out = s_pending[idx].value; - return true; -} - -static bool pending_get_bytes(const char *ns, const char *key, storage_type_t type, - uint8_t *out, size_t out_sz, uint16_t *out_len) -{ - int idx = find_pending(ns, key); - if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type) - return false; - - uint16_t len = s_pending[idx].len; - - if (out_len) - *out_len = len; - - if (out) - { - size_t n = (len <= out_sz) ? (size_t)len : out_sz; - if (n > 0) - memcpy(out, s_pending[idx].bytes, n); - } - return true; -} - -// Commit: agrupa por namespace e faz 1 commit por namespace -static esp_err_t commit_all_pending(void) -{ - if (s_pending_count == 0) - { - s_dirty = false; - return ESP_OK; - } - - esp_err_t overall = ESP_OK; - - // Lista de namespaces únicos (sem heap) - char ns_list[STORAGE_MAX_PENDING][STORAGE_NS_MAX_LEN]; - int ns_count = 0; - - for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) - { - if (!s_pending[i].used) - continue; - - bool seen = false; - for (int j = 0; j < ns_count; ++j) - { - if (strncmp(ns_list[j], s_pending[i].ns, STORAGE_NS_MAX_LEN) == 0) - { - seen = true; - break; - } - } - if (!seen && ns_count < (int)STORAGE_MAX_PENDING) - { - strncpy(ns_list[ns_count], s_pending[i].ns, STORAGE_NS_MAX_LEN - 1); - ns_list[ns_count][STORAGE_NS_MAX_LEN - 1] = '\0'; - ns_count++; - } - } - - for (int n = 0; n < ns_count; ++n) - { - const char *ns = ns_list[n]; - nvs_handle_t h; - esp_err_t err = nvs_open(ns, NVS_READWRITE, &h); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_open('%s') failed: %s", ns, esp_err_to_name(err)); - overall = err; - continue; - } - - for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) - { - if (!s_pending[i].used) - continue; - if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0) - continue; - - if (s_pending[i].erase) - { - err = nvs_erase_key(h, s_pending[i].key); - if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) - { - ESP_LOGE(TAG, "erase %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(err)); - overall = err; - } - continue; - } - - switch (s_pending[i].type) - { - case T_U8: - err = nvs_set_u8(h, s_pending[i].key, (uint8_t)(s_pending[i].value & 0xFF)); - break; - case T_U16: - err = nvs_set_u16(h, s_pending[i].key, (uint16_t)(s_pending[i].value & 0xFFFF)); - break; - case T_U32: - err = nvs_set_u32(h, s_pending[i].key, s_pending[i].value); - break; - - case T_STR: - { - // garantir null-termination para nvs_set_str - char tmp[STORAGE_MAX_VALUE_BYTES + 1]; - uint16_t len = s_pending[i].len; - if (len > STORAGE_MAX_VALUE_BYTES) - { - err = ESP_ERR_INVALID_SIZE; - break; - } - if (len > 0) - memcpy(tmp, s_pending[i].bytes, len); - tmp[len] = '\0'; - err = nvs_set_str(h, s_pending[i].key, tmp); - break; - } - - case T_BLOB: - { - uint16_t len = s_pending[i].len; - if (len > STORAGE_MAX_VALUE_BYTES) - { - err = ESP_ERR_INVALID_SIZE; - break; - } - err = nvs_set_blob(h, s_pending[i].key, s_pending[i].bytes, (size_t)len); - break; - } - - default: - err = ESP_ERR_INVALID_STATE; - break; - } - - if (err != ESP_OK) - { - ESP_LOGE(TAG, "set %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(err)); - overall = err; - } - } - - err = nvs_commit(h); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "commit('%s') failed: %s", ns, esp_err_to_name(err)); - overall = err; - } - - nvs_close(h); - - if (err == ESP_OK) - { - for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) - { - if (!s_pending[i].used) - continue; - if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0) - continue; - s_pending[i].used = false; - if (s_pending_count > 0) - s_pending_count--; - } - } - } - - if (s_pending_count == 0) - s_dirty = false; else - mark_dirty(); - - return overall; + { + ESP_LOGE(TAG, "Failed to get SPIFFS info (%s): %s", + conf->partition_label, esp_err_to_name(ret)); + } } -// -------- Read helpers: devolvem para buffers internos (cópia na resposta) -------- - -static esp_err_t nvs_read_num(storage_type_t type, const char *ns, const char *key, uint32_t *out) +static void fs_init(void) { - if (pending_is_erased(ns, key)) - return ESP_ERR_NOT_FOUND; + esp_vfs_spiffs_conf_t cfg_conf = { + .base_path = "/cfg", + .partition_label = "cfg", + .max_files = 1, + .format_if_mount_failed = false}; - uint32_t pv = 0; - if (pending_get_num(ns, key, type, &pv)) - { - if (out) - *out = pv; - return ESP_OK; - } + esp_vfs_spiffs_conf_t data_conf = { + .base_path = "/data", + .partition_label = "data", + .max_files = 5, + .format_if_mount_failed = true}; - nvs_handle_t h; - esp_err_t err = nvs_open(ns, NVS_READONLY, &h); - if (err != ESP_OK) - return map_not_found(err); + ESP_ERROR_CHECK(esp_vfs_spiffs_register(&cfg_conf)); + ESP_ERROR_CHECK(esp_vfs_spiffs_register(&data_conf)); - switch (type) - { - case T_U8: - { - uint8_t v8 = 0; - err = nvs_get_u8(h, key, &v8); - if (err == ESP_OK && out) - *out = v8; - break; - } - case T_U16: - { - uint16_t v16 = 0; - err = nvs_get_u16(h, key, &v16); - if (err == ESP_OK && out) - *out = v16; - break; - } - case T_U32: - { - uint32_t v32 = 0; - err = nvs_get_u32(h, key, &v32); - if (err == ESP_OK && out) - *out = v32; - break; - } - default: - err = ESP_ERR_INVALID_ARG; - break; - } - - nvs_close(h); - return map_not_found(err); + fs_info(&cfg_conf); + fs_info(&data_conf); } -static esp_err_t nvs_read_str_to_resp(const char *ns, const char *key, storage_resp_t *resp) +// +// Wi-Fi event monitoring task +// +static void wifi_event_task_func(void *param) { - if (!resp) - return ESP_ERR_INVALID_ARG; - - resp->len = 0; - - if (pending_is_erased(ns, key)) - return ESP_ERR_NOT_FOUND; - - uint16_t plen = 0; - if (pending_get_bytes(ns, key, T_STR, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen)) + (void)param; + EventBits_t mode_bits; + for (;;) { - resp->len = (size_t)plen; - return ESP_OK; - } + mode_bits = xEventGroupWaitBits( + wifi_event_group, + WIFI_AP_MODE_BIT | WIFI_STA_MODE_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); - nvs_handle_t h; - esp_err_t err = nvs_open(ns, NVS_READONLY, &h); - if (err != ESP_OK) - return map_not_found(err); - - // 1) query required size (inclui '\0') - size_t req = 0; - err = nvs_get_str(h, key, NULL, &req); - if (err != ESP_OK) - { - nvs_close(h); - return map_not_found(err); - } - - if (req == 0) - { - nvs_close(h); - resp->len = 0; - return ESP_OK; - } - - size_t str_len = req - 1; // sem '\0' - resp->len = str_len; - - if (str_len > STORAGE_MAX_VALUE_BYTES) - { - nvs_close(h); - return ESP_ERR_NVS_INVALID_LENGTH; - } - - // 2) read - char tmp[STORAGE_MAX_VALUE_BYTES + 1]; - size_t tmp_sz = sizeof(tmp); - err = nvs_get_str(h, key, tmp, &tmp_sz); - nvs_close(h); - - if (err != ESP_OK) - return map_not_found(err); - - size_t n = strnlen(tmp, STORAGE_MAX_VALUE_BYTES); - memcpy(resp->bytes, tmp, n); - resp->len = n; - return ESP_OK; -} - -static esp_err_t nvs_read_blob_to_resp(const char *ns, const char *key, storage_resp_t *resp) -{ - if (!resp) - return ESP_ERR_INVALID_ARG; - - resp->len = 0; - - if (pending_is_erased(ns, key)) - return ESP_ERR_NOT_FOUND; - - uint16_t plen = 0; - if (pending_get_bytes(ns, key, T_BLOB, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen)) - { - resp->len = (size_t)plen; - return ESP_OK; - } - - nvs_handle_t h; - esp_err_t err = nvs_open(ns, NVS_READONLY, &h); - if (err != ESP_OK) - return map_not_found(err); - - // query size - size_t req = 0; - err = nvs_get_blob(h, key, NULL, &req); - if (err != ESP_OK) - { - nvs_close(h); - if (err == ESP_ERR_NVS_NOT_FOUND) - resp->len = 0; - return map_not_found(err); - } - - resp->len = req; - - // Se maior que o máximo, não lemos payload (caller pode só estar a fazer query) - if (req > STORAGE_MAX_VALUE_BYTES) - { - nvs_close(h); - return ESP_OK; // wrapper decide ESP_ERR_NVS_INVALID_LENGTH se tentar ler com buffer - } - - // read payload - size_t tmp = req; - err = nvs_get_blob(h, key, resp->bytes, &tmp); - nvs_close(h); - - if (err != ESP_OK) - return map_not_found(err); - - resp->len = tmp; - return ESP_OK; -} - -// -------- Task -------- - -static void storage_task(void *arg) -{ - (void)arg; - storage_msg_t msg; - - while (true) - { - TickType_t now = xTaskGetTickCount(); - - TickType_t wait = portMAX_DELAY; - if (s_dirty) + if (mode_bits & WIFI_AP_MODE_BIT) { - if (now >= s_commit_deadline) + if (xEventGroupWaitBits( + wifi_event_group, + WIFI_AP_CONNECTED_BIT, + pdFALSE, + pdFALSE, + pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT)) & + WIFI_AP_CONNECTED_BIT) { - (void)commit_all_pending(); - continue; + // Espera sair do AP + xEventGroupWaitBits( + wifi_event_group, + WIFI_AP_DISCONNECTED_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); } else { - wait = s_commit_deadline - now; + if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) + { + // Timeout sem cliente ligado + // wifi_ap_stop(); + ESP_LOGW(TAG, "AP timeout sem conexões"); + } } } - - if (xQueueReceive(s_q, &msg, wait) != pdTRUE) + else if (mode_bits & WIFI_STA_MODE_BIT) { - if (s_dirty && xTaskGetTickCount() >= s_commit_deadline) + // Apenas aguarda desconexão STA + xEventGroupWaitBits( + wifi_event_group, + WIFI_STA_DISCONNECTED_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); + } + } +} + +// +// Button press handler (short press => AP) +// +static void handle_button_press(void) +{ + // Pode ser chamado cedo demais se a ordem de init mudar + if (wifi_event_group == NULL) + { + ESP_LOGW(TAG, "Wi-Fi ainda não inicializado, ignorando botão Wi-Fi"); + return; + } + + if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) + { + ESP_LOGI(TAG, "Starting Wi-Fi AP mode (short press)"); + wifi_ap_start(); + } +} + +// Task para lidar com notificações de botão (PRESS / RELEASE) +static void user_input_task_func(void *param) +{ + (void)param; + uint32_t notification; + + for (;;) + { + if (xTaskNotifyWait( + 0, + UINT32_MAX, + ¬ification, + portMAX_DELAY)) + { + if (notification & PRESS_BIT) { - (void)commit_all_pending(); + press_tick = xTaskGetTickCount(); + pressed = true; + ESP_LOGI(TAG, "Button Pressed"); + // Decisão (short/long) é feita na soltura } + + if ((notification & RELEASED_BIT) && pressed) + { + pressed = false; + TickType_t held_ticks = xTaskGetTickCount() - press_tick; + uint32_t held_ms = pdTICKS_TO_MS(held_ticks); + + ESP_LOGI(TAG, "Button Released (held %u ms)", (unsigned)held_ms); + + if (held_ms >= RESET_HOLD_TIME) + { + ESP_LOGW(TAG, "Long press: erasing NVS + reboot"); + nvs_flash_erase(); + esp_restart(); + } + else + { + // Short press: apenas habilita modo AP + handle_button_press(); + } + } + } + } +} + +// ISR para GPIO do botão (ativo em nível baixo) +static void IRAM_ATTR button_isr_handler(void *arg) +{ + (void)arg; + BaseType_t higher_task_woken = pdFALSE; + TickType_t now = xTaskGetTickCountFromISR(); + + portENTER_CRITICAL_ISR(&button_spinlock); + if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) + { + portEXIT_CRITICAL_ISR(&button_spinlock); + return; + } + last_interrupt_tick = now; + + if (user_input_task == NULL) + { + portEXIT_CRITICAL_ISR(&button_spinlock); + return; + } + + int level = gpio_get_level(board_config.button_wifi_gpio); + uint32_t bits = (level == 0) ? PRESS_BIT : RELEASED_BIT; + + xTaskNotifyFromISR( + user_input_task, + bits, + eSetBits, + &higher_task_woken); + + portEXIT_CRITICAL_ISR(&button_spinlock); + + if (higher_task_woken) + { + portYIELD_FROM_ISR(); + } +} + +static void button_init(void) +{ + gpio_config_t conf = { + .pin_bit_mask = BIT64(board_config.button_wifi_gpio), + .mode = GPIO_MODE_INPUT, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .pull_up_en = GPIO_PULLUP_ENABLE, + .intr_type = GPIO_INTR_ANYEDGE}; + + ESP_ERROR_CHECK(gpio_config(&conf)); + ESP_ERROR_CHECK(gpio_isr_handler_add(board_config.button_wifi_gpio, button_isr_handler, NULL)); +} + +// +// Inicialização dos módulos do sistema (SEM botão) +// +static void init_modules(void) +{ + + ESP_ERROR_CHECK(storage_service_init()); + + peripherals_init(); + wifi_ini(); // garante wifi_event_group inicializado + buzzer_init(); + led_init(); + ESP_ERROR_CHECK(rest_server_init("/data")); + + evse_manager_init(); + evse_init(); + auth_init(); + meter_manager_init(); + meter_manager_start(); + ocpp_start(); + scheduler_init(); + protocols_init(); + loadbalancer_init(); + evse_link_init(); +} + +// +// Função principal do firmware +// +void app_main(void) +{ + + esp_reset_reason_t reason = esp_reset_reason(); + ESP_LOGI(TAG, "Reset reason: %d", reason); + + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_LOGW(TAG, "Erasing NVS flash"); + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + fs_init(); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(gpio_install_isr_service(0)); + + board_config_load(); + + // 1) Inicia todos os módulos (inclui Wi-Fi, EVSE, etc.) + init_modules(); + + // 2) Cria a task que recebe notificações do botão + BaseType_t rc; + rc = xTaskCreate(user_input_task_func, "user_input_task", 4 * 1024, NULL, 3, &user_input_task); + configASSERT(rc == pdPASS); + + // 3) Agora é seguro registrar ISR do botão + button_init(); + + // 4) Task auxiliar para eventos Wi-Fi + rc = xTaskCreate(wifi_event_task_func, "wifi_event_task", 8 * 1024, NULL, 3, NULL); + configASSERT(rc == pdPASS); +} + +// === Fim de: main/main.c === + +// === Fim de: main/main.c === + + +// === Início de: components/ocpp/src/ocpp_events.c === +#include "ocpp_events.h" + +/* Define a base, como em components/auth/src/auth_events.c */ +ESP_EVENT_DEFINE_BASE(OCPP_EVENTS); + +// === Fim de: components/ocpp/src/ocpp_events.c === + + +// === Início de: components/ocpp/src/ocpp.c === +// components/ocpp/src/ocpp.c + +#include +#include +#include +#include +#include + +#include "esp_log.h" +#include "esp_err.h" +#include "esp_timer.h" +#include "esp_event.h" +#include "esp_wifi.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "ocpp.h" +#include "ocpp_events.h" + +#include "evse_error.h" +#include "auth_events.h" +#include "evse_events.h" +#include "evse_state.h" +#include "meter_events.h" + +/* MicroOcpp includes */ +#include +#include // C-facade of MicroOcpp +#include // WebSocket integration for ESP-IDF + +// NEW storage layer +#include "storage_service.h" + +#define NVS_NAMESPACE "ocpp" +#define NVS_OCPP_ENABLED "enabled" +#define NVS_OCPP_SERVER "ocpp_server" +#define NVS_OCPP_CHARGE_ID "charge_id" + +static const char *TAG = "ocpp"; + +static bool enabled = false; + +static TaskHandle_t ocpp_task = NULL; + +static struct mg_mgr mgr; // Event manager +static OCPP_Connection *g_ocpp_conn = NULL; // Para shutdown limpo + +static esp_event_handler_instance_t s_auth_verify_inst = NULL; + +// Flags refletindo o estado do EVSE (atualizadas por eventos) +static volatile bool s_ev_plugged = false; +static volatile bool s_ev_ready = false; + +static esp_event_handler_instance_t s_evse_state_inst = NULL; + +// Flags de config (vindas de EVSE_EVENTS) +static volatile bool s_evse_enabled = true; +static volatile bool s_evse_available = true; + +static esp_event_handler_instance_t s_evse_enable_inst = NULL; +static esp_event_handler_instance_t s_evse_available_inst = NULL; + +// --- cache de medições vindas de METER_EVENT_DATA_READY --- +typedef struct +{ + float vrms[3]; + float irms[3]; + int32_t watt[3]; // ativo por fase (W) + float frequency; + float power_factor; + + float total_energy_Wh; + + int32_t sum_watt; + float avg_voltage; + float sum_current; + + bool have_data; +} ocpp_meter_cache_t; + +static ocpp_meter_cache_t s_meter = {0}; +static portMUX_TYPE s_meter_mux = portMUX_INITIALIZER_UNLOCKED; +static esp_event_handler_instance_t s_meter_inst = NULL; + +// valor de oferta (A por conector) +static float s_current_offered_A = 16.0f; + +static float getCurrentOffered(void) +{ + return s_current_offered_A; +} + +// ----------------------------------------------------------------------------- +// Storage helpers (robustos) +// ----------------------------------------------------------------------------- +#define STORE_TO pdMS_TO_TICKS(800) +#define STORE_FLUSH_TO pdMS_TO_TICKS(2000) + +static void storage_init_best_effort(void) +{ + esp_err_t e = storage_service_init(); + if (e != ESP_OK) + ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(e)); +} + +static esp_err_t store_flush_best_effort(void) +{ + esp_err_t e = storage_flush_sync(STORE_FLUSH_TO); + if (e != ESP_OK) + ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e)); + return e; +} + +static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_u8_async(ns, key, v); + if (e == ESP_OK) + return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(pdMS_TO_TICKS(10)); continue; } - - storage_resp_t resp; - memset(&resp, 0, sizeof(resp)); - resp.err = ESP_OK; - - switch (msg.op) - { - case OP_SET_U8: - resp.err = pending_set_num(msg.ns, msg.key, T_U8, msg.value & 0xFF); - break; - case OP_SET_U16: - resp.err = pending_set_num(msg.ns, msg.key, T_U16, msg.value & 0xFFFF); - break; - case OP_SET_U32: - resp.err = pending_set_num(msg.ns, msg.key, T_U32, msg.value); - break; - - case OP_SET_STR: - resp.err = pending_set_bytes(msg.ns, msg.key, T_STR, msg.bytes, msg.len); - break; - - case OP_SET_BLOB: - resp.err = pending_set_bytes(msg.ns, msg.key, T_BLOB, msg.bytes, msg.len); - break; - - case OP_ERASE_KEY: - resp.err = pending_erase(msg.ns, msg.key); - break; - - case OP_GET_U8: - resp.err = nvs_read_num(T_U8, msg.ns, msg.key, &resp.value); - break; - case OP_GET_U16: - resp.err = nvs_read_num(T_U16, msg.ns, msg.key, &resp.value); - break; - case OP_GET_U32: - resp.err = nvs_read_num(T_U32, msg.ns, msg.key, &resp.value); - break; - - case OP_GET_STR: - resp.err = nvs_read_str_to_resp(msg.ns, msg.key, &resp); - break; - - case OP_GET_BLOB: - // sempre fazemos "query+maybe-read" para preencher resp.len (e bytes se couber) - resp.err = nvs_read_blob_to_resp(msg.ns, msg.key, &resp); - break; - - case OP_FLUSH: - resp.err = commit_all_pending(); - break; - - default: - resp.err = ESP_ERR_INVALID_ARG; - break; - } - - if (msg.resp_q) - { - (void)xQueueSend(msg.resp_q, &resp, 0); - } + return e; } + return ESP_ERR_TIMEOUT; } -// -------- Public API -------- - -esp_err_t storage_service_init(void) +static esp_err_t store_set_str_best_effort(const char *ns, const char *key, const char *s) { - if (s_inited) - return ESP_OK; - - s_q = xQueueCreateStatic(STORAGE_QUEUE_LEN, sizeof(storage_msg_t), s_qstorage, &s_qbuf); - if (!s_q) - return ESP_ERR_NO_MEM; - - s_sync_q = xQueueCreateStatic(1, sizeof(storage_resp_t), s_sync_qstor, &s_sync_qbuf); - if (!s_sync_q) - return ESP_ERR_NO_MEM; - - s_sync_mtx = xSemaphoreCreateMutexStatic(&s_sync_mtx_buf); - if (!s_sync_mtx) - return ESP_ERR_NO_MEM; - - memset(s_pending, 0, sizeof(s_pending)); - s_pending_count = 0; - s_dirty = false; - - BaseType_t ok = xTaskCreate(storage_task, "storage", 8192, NULL, 5, NULL); - if (ok != pdPASS) - return ESP_ERR_NO_MEM; - - s_inited = true; - ESP_LOGI(TAG, "storage_service init OK (queue=%d pending=%d debounce=%dms max_bytes=%d)", - STORAGE_QUEUE_LEN, STORAGE_MAX_PENDING, STORAGE_COMMIT_DEBOUNCE_MS, STORAGE_MAX_VALUE_BYTES); - return ESP_OK; -} - -static esp_err_t send_msg(const storage_msg_t *m, TickType_t to) -{ - if (!s_inited || !s_q) - return ESP_ERR_INVALID_STATE; - return (xQueueSend(s_q, m, to) == pdTRUE) ? ESP_OK : ESP_ERR_TIMEOUT; -} - -static esp_err_t set_async_num(storage_op_t op, const char *ns, const char *key, uint32_t v) -{ - storage_msg_t m; - memset(&m, 0, sizeof(m)); - m.op = op; - if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(m.key, sizeof(m.key), key)) - return ESP_ERR_INVALID_ARG; - m.value = v; - return send_msg(&m, 0); -} - -esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v) -{ - return set_async_num(OP_SET_U8, ns, key, v); -} - -esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v) -{ - return set_async_num(OP_SET_U16, ns, key, v); -} - -esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v) -{ - return set_async_num(OP_SET_U32, ns, key, v); -} - -esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str) -{ - if (!str) - return ESP_ERR_INVALID_ARG; - - // CORRIGIDO: deteta strings > MAX (sem truncar silenciosamente) - size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1); - if (len > STORAGE_MAX_VALUE_BYTES) - return ESP_ERR_INVALID_SIZE; - - storage_msg_t m; - memset(&m, 0, sizeof(m)); - m.op = OP_SET_STR; - if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(m.key, sizeof(m.key), key)) - return ESP_ERR_INVALID_ARG; - - m.len = (uint16_t)len; - if (len > 0) - memcpy(m.bytes, str, len); - if (len < STORAGE_MAX_VALUE_BYTES) - memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); - - return send_msg(&m, 0); -} - -esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len) -{ - if (len > STORAGE_MAX_VALUE_BYTES) - return ESP_ERR_INVALID_SIZE; - if (len > 0 && !data) - return ESP_ERR_INVALID_ARG; - - storage_msg_t m; - memset(&m, 0, sizeof(m)); - m.op = OP_SET_BLOB; - if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(m.key, sizeof(m.key), key)) - return ESP_ERR_INVALID_ARG; - - m.len = (uint16_t)len; - if (len > 0) - memcpy(m.bytes, data, len); - if (len < STORAGE_MAX_VALUE_BYTES) - memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); - - return send_msg(&m, 0); -} - -esp_err_t storage_erase_key_async(const char *ns, const char *key) -{ - storage_msg_t m; - memset(&m, 0, sizeof(m)); - m.op = OP_ERASE_KEY; - if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(m.key, sizeof(m.key), key)) - return ESP_ERR_INVALID_ARG; - return send_msg(&m, 0); -} - -esp_err_t storage_flush_async(void) -{ - storage_msg_t m; - memset(&m, 0, sizeof(m)); - m.op = OP_FLUSH; - return send_msg(&m, 0); -} - -static esp_err_t sync_req(const storage_msg_t *req, storage_resp_t *out_resp, TickType_t to) -{ - if (!s_inited || !s_sync_q || !s_sync_mtx) - return ESP_ERR_INVALID_STATE; - - if (xSemaphoreTake(s_sync_mtx, to) != pdTRUE) - return ESP_ERR_TIMEOUT; - - xQueueReset(s_sync_q); - - storage_msg_t m = *req; - m.resp_q = s_sync_q; - - esp_err_t err = send_msg(&m, to); - if (err != ESP_OK) + for (int attempt = 0; attempt < 3; ++attempt) { - xSemaphoreGive(s_sync_mtx); - return err; + esp_err_t e = storage_set_str_async(ns, key, s ? s : ""); + if (e == ESP_OK) + return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(portMAX_DELAY); + continue; + } + return e; } - - storage_resp_t r; - memset(&r, 0, sizeof(r)); - - if (xQueueReceive(s_sync_q, &r, to) != pdTRUE) - { - xSemaphoreGive(s_sync_mtx); - return ESP_ERR_TIMEOUT; - } - - if (out_resp) - *out_resp = r; - - xSemaphoreGive(s_sync_mtx); - return r.err; + return ESP_ERR_TIMEOUT; } -esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to) -{ - storage_msg_t req; - memset(&req, 0, sizeof(req)); - req.op = OP_GET_U8; - if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(req.key, sizeof(req.key), key)) - return ESP_ERR_INVALID_ARG; - - storage_resp_t r; - esp_err_t err = sync_req(&req, &r, to); - if (err == ESP_OK && out) - *out = (uint8_t)(r.value & 0xFF); - return err; -} - -esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to) -{ - storage_msg_t req; - memset(&req, 0, sizeof(req)); - req.op = OP_GET_U16; - if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(req.key, sizeof(req.key), key)) - return ESP_ERR_INVALID_ARG; - - storage_resp_t r; - esp_err_t err = sync_req(&req, &r, to); - if (err == ESP_OK && out) - *out = (uint16_t)(r.value & 0xFFFF); - return err; -} - -esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to) -{ - storage_msg_t req; - memset(&req, 0, sizeof(req)); - req.op = OP_GET_U32; - if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(req.key, sizeof(req.key), key)) - return ESP_ERR_INVALID_ARG; - - storage_resp_t r; - esp_err_t err = sync_req(&req, &r, to); - if (err == ESP_OK && out) - *out = r.value; - return err; -} - -esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to) +// Lê string de forma segura (buffer grande -> truncagem segura para out) +static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out, size_t out_sz) { if (!out || out_sz == 0) return ESP_ERR_INVALID_ARG; out[0] = '\0'; - storage_msg_t req; - memset(&req, 0, sizeof(req)); - req.op = OP_GET_STR; - if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(req.key, sizeof(req.key), key)) - return ESP_ERR_INVALID_ARG; + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + memset(tmp, 0, sizeof(tmp)); - storage_resp_t r; - esp_err_t err = sync_req(&req, &r, to); - - if (err != ESP_OK) - { - out[out_sz - 1] = '\0'; - return err; - } - - // r.len é o tamanho real (sem '\0') - if (r.len >= out_sz) - { - // espelhar semântica tipo NVS (buffer pequeno) - out[0] = '\0'; - out[out_sz - 1] = '\0'; - return ESP_ERR_NVS_INVALID_LENGTH; - } - - if (r.len > 0) - memcpy(out, r.bytes, r.len); - out[r.len] = '\0'; - return ESP_OK; -} - -esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to) -{ - if (!inout_len) - return ESP_ERR_INVALID_ARG; - - storage_msg_t req; - memset(&req, 0, sizeof(req)); - req.op = OP_GET_BLOB; - req.blob_query_only = (out == NULL); - - if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) - return ESP_ERR_INVALID_ARG; - if (!safe_copy_str(req.key, sizeof(req.key), key)) - return ESP_ERR_INVALID_ARG; - - storage_resp_t r; - esp_err_t err = sync_req(&req, &r, to); - - if (err != ESP_OK) - { - if (out == NULL && err == ESP_ERR_NOT_FOUND) - *inout_len = 0; - return err; - } - - // query mode: só devolver tamanho requerido - if (out == NULL) - { - *inout_len = r.len; + esp_err_t e = storage_get_str_sync(ns, key, tmp, sizeof(tmp), STORE_TO); + if (e == ESP_ERR_NOT_FOUND) return ESP_OK; - } + if (e != ESP_OK) + return e; - // read mode: valida tamanhos - if (r.len > *inout_len) - { - *inout_len = r.len; - return ESP_ERR_NVS_INVALID_LENGTH; - } - if (r.len > STORAGE_MAX_VALUE_BYTES) - { - *inout_len = r.len; - return ESP_ERR_NVS_INVALID_LENGTH; - } + size_t n = strnlen(tmp, out_sz - 1); + memcpy(out, tmp, n); + out[n] = '\0'; - if (r.len > 0) - memcpy(out, r.bytes, r.len); - *inout_len = r.len; return ESP_OK; } -esp_err_t storage_flush_sync(TickType_t to) +// ----------------------------------------------------------------------------- +// Task / Main Loop +// ----------------------------------------------------------------------------- +static void ocpp_task_func(void *param) { - storage_msg_t req; - memset(&req, 0, sizeof(req)); - req.op = OP_FLUSH; - storage_resp_t r; - return sync_req(&req, &r, to); + (void)param; + + while (true) + { + if (enabled) + { + mg_mgr_poll(&mgr, 100); + ocpp_loop(); + + bool operative = ocpp_isOperative(); + if (operative != s_evse_enabled) + { + s_evse_enabled = operative; + + ocpp_operative_event_t ev = { + .operative = operative, + .timestamp_us = esp_timer_get_time()}; + esp_event_post(OCPP_EVENTS, + OCPP_EVENT_OPERATIVE_UPDATED, + &ev, sizeof(ev), + portMAX_DELAY); + + ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d", (int)operative); + } + } + else + { + vTaskDelay(pdMS_TO_TICKS(500)); + } + } } -// === Fim de: components/storage_service/src/storage_service.c === +// ----------------------------------------------------------------------------- +// Storage GETs +// ----------------------------------------------------------------------------- +bool ocpp_get_enabled(void) +{ + storage_init_best_effort(); + + uint8_t value = 0; + esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_OCPP_ENABLED, &value, STORE_TO); + + if (err == ESP_ERR_NOT_FOUND) + return false; + if (err != ESP_OK) + { + ESP_LOGW(TAG, "storage_get_u8_sync(enabled) failed: %s", esp_err_to_name(err)); + return false; + } + return value != 0; +} + +void ocpp_get_server(char *value /* out, size>=64 */) +{ + if (!value) + return; + value[0] = '\0'; + + storage_init_best_effort(); + + esp_err_t e = store_get_str_safe(NVS_NAMESPACE, NVS_OCPP_SERVER, value, 64); + if (e != ESP_OK) + { + ESP_LOGW(TAG, "store_get_str_safe(server) failed: %s", esp_err_to_name(e)); + value[0] = '\0'; + } +} + +void ocpp_get_charge_id(char *value /* out, size>=64 */) +{ + if (!value) + return; + value[0] = '\0'; + + storage_init_best_effort(); + + esp_err_t e = store_get_str_safe(NVS_NAMESPACE, NVS_OCPP_CHARGE_ID, value, 64); + if (e != ESP_OK) + { + ESP_LOGW(TAG, "store_get_str_safe(charge_id) failed: %s", esp_err_to_name(e)); + value[0] = '\0'; + } +} + +// ----------------------------------------------------------------------------- +// Storage SETs +// ----------------------------------------------------------------------------- +void ocpp_set_enabled(bool value) +{ + storage_init_best_effort(); + + ESP_LOGI(TAG, "set enabled %d", value); + + esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_OCPP_ENABLED, value ? 1 : 0); + if (e != ESP_OK) + { + ESP_LOGE(TAG, "store_set_u8_best_effort(enabled) failed: %s", esp_err_to_name(e)); + return; + } + + (void)store_flush_best_effort(); + enabled = value; +} + +void ocpp_set_server(char *value) +{ + storage_init_best_effort(); + + ESP_LOGI(TAG, "set server %s", value ? value : "(null)"); + + esp_err_t e = store_set_str_best_effort(NVS_NAMESPACE, NVS_OCPP_SERVER, value ? value : ""); + if (e != ESP_OK) + { + ESP_LOGE(TAG, "store_set_str_best_effort(server) failed: %s", esp_err_to_name(e)); + return; + } + + (void)store_flush_best_effort(); +} + +void ocpp_set_charge_id(char *value) +{ + storage_init_best_effort(); + + ESP_LOGI(TAG, "set charge_id %s", value ? value : "(null)"); + + esp_err_t e = store_set_str_best_effort(NVS_NAMESPACE, NVS_OCPP_CHARGE_ID, value ? value : ""); + if (e != ESP_OK) + { + ESP_LOGE(TAG, "store_set_str_best_effort(charge_id) failed: %s", esp_err_to_name(e)); + return; + } + + (void)store_flush_best_effort(); +} + +// ----------------------------------------------------------------------------- +// Event handlers (AUTH / EVSE / METER) +// ----------------------------------------------------------------------------- +static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, void *event_data) +{ + (void)arg; + (void)base; + (void)id; + + const auth_tag_verify_event_t *rq = (const auth_tag_verify_event_t *)event_data; + if (!rq) + return; + + char idtag[AUTH_TAG_MAX_LEN]; + if (rq->tag[0] == '\0') + { + strncpy(idtag, "IDTAG", sizeof(idtag)); + idtag[sizeof(idtag) - 1] = '\0'; + } + else + { + strncpy(idtag, rq->tag, sizeof(idtag) - 1); + idtag[sizeof(idtag) - 1] = '\0'; + } + + ESP_LOGI(TAG, "AUTH_EVENT_TAG_VERIFY: tag=%s req_id=%u", idtag, (unsigned)rq->req_id); + + if (!enabled || g_ocpp_conn == NULL) + { + ESP_LOGW(TAG, "OCPP not ready (enabled=%d, conn=%p) – ignoring verify", + enabled, (void *)g_ocpp_conn); + return; + } + + if (ocpp_isTransactionActive()) + { + ESP_LOGI(TAG, "Transaction active -> ocpp_end_transaction(\"%s\")", idtag); + ocpp_endTransaction(idtag, "Local"); + } + else + { + ESP_LOGI(TAG, "No active transaction -> ocpp_begin_transaction(\"%s\")", idtag); + ocpp_beginTransaction(idtag); + } +} + +static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL) + return; + + const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data; + ESP_LOGI(TAG, "EVSE event received: state = %d", (int)evt->state); + + switch (evt->state) + { + case EVSE_STATE_EVENT_IDLE: + s_ev_plugged = false; + s_ev_ready = false; + break; + + case EVSE_STATE_EVENT_WAITING: + s_ev_plugged = true; + s_ev_ready = false; + break; + + case EVSE_STATE_EVENT_CHARGING: + s_ev_plugged = true; + s_ev_ready = true; + break; + + case EVSE_STATE_EVENT_FAULT: + default: + s_ev_ready = false; + break; + } +} + +static void evse_enable_available_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + if (base != EVSE_EVENTS || data == NULL) + return; + + if (id == EVSE_EVENT_ENABLE_UPDATED) + { + const evse_enable_event_data_t *e = (const evse_enable_event_data_t *)data; + s_evse_enabled = e->enabled; + ESP_LOGI(TAG, "[EVSE] ENABLE_UPDATED: enabled=%d (ts=%lld)", + (int)e->enabled, (long long)e->timestamp_us); + return; + } + + if (id == EVSE_EVENT_AVAILABLE_UPDATED) + { + const evse_available_event_data_t *e = (const evse_available_event_data_t *)data; + s_evse_available = e->available; + ESP_LOGI(TAG, "[EVSE] AVAILABLE_UPDATED: available=%d (ts=%lld)", + (int)e->available, (long long)e->timestamp_us); + return; + } +} + +static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + + if (base != METER_EVENT || id != METER_EVENT_DATA_READY || !data) + return; + + const meter_event_data_t *evt = (const meter_event_data_t *)data; + + if (!evt->source || strcmp(evt->source, "EVSE") != 0) + return; + + int32_t sum_w = (int32_t)evt->watt[0] + (int32_t)evt->watt[1] + (int32_t)evt->watt[2]; + float avg_v = (evt->vrms[0] + evt->vrms[1] + evt->vrms[2]) / 3.0f; + float sum_i = evt->irms[0] + evt->irms[1] + evt->irms[2]; + + portENTER_CRITICAL(&s_meter_mux); + memcpy(s_meter.vrms, evt->vrms, sizeof(s_meter.vrms)); + memcpy(s_meter.irms, evt->irms, sizeof(s_meter.irms)); + s_meter.watt[0] = evt->watt[0]; + s_meter.watt[1] = evt->watt[1]; + s_meter.watt[2] = evt->watt[2]; + s_meter.frequency = evt->frequency; + s_meter.power_factor = evt->power_factor; + s_meter.total_energy_Wh = evt->total_energy; + s_meter.sum_watt = sum_w; + s_meter.avg_voltage = avg_v; + s_meter.sum_current = sum_i; + s_meter.have_data = true; + portEXIT_CRITICAL(&s_meter_mux); +} + +// ----------------------------------------------------------------------------- +// MicroOCPP Inputs/CBs +// ----------------------------------------------------------------------------- +bool setConnectorPluggedInput(void) +{ + return s_ev_plugged; +} + +bool setEvReadyInput(void) +{ + return s_ev_ready; +} + +bool setEvseReadyInput(void) +{ + return s_evse_enabled && s_evse_available; +} + +float setPowerMeterInput(void) +{ + int32_t w = 0; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + w = s_meter.sum_watt; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + ESP_LOGW(TAG, "[METER] PowerMeterInput: no data (return 0)"); + else + ESP_LOGD(TAG, "[METER] PowerMeterInput: %" PRId32 " W", w); + + return (float)w; +} + +float setEnergyMeterInput(void) +{ + float wh = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + wh = s_meter.total_energy_Wh; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + ESP_LOGW(TAG, "[METER] EnergyMeterInput: no data (return 0)"); + else + ESP_LOGD(TAG, "[METER] EnergyMeterInput: (%.1f Wh)", wh); + + return wh; +} + +int setEnergyInput(void) +{ + float wh = setEnergyMeterInput(); + int wh_i = (int)lrintf((double)wh); + ESP_LOGD(TAG, "[METER] EnergyInput: %.1f Wh", wh); + return wh_i; +} + +float setCurrentInput(void) +{ + float a = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + a = s_meter.sum_current; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + ESP_LOGW(TAG, "[METER] CurrentInput: no data (return 0)"); + else + ESP_LOGD(TAG, "[METER] CurrentInput: %.2f A (total)", a); + + return a; +} + +float setVoltageInput(void) +{ + float v = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + v = s_meter.avg_voltage; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + ESP_LOGW(TAG, "[METER] VoltageInput: no data (return 0)"); + else + ESP_LOGD(TAG, "[METER] VoltageInput: %.1f V (avg)", v); + + return v; +} + +float setPowerInput(void) +{ + float w = setPowerMeterInput(); + ESP_LOGD(TAG, "[METER] PowerInput: %.1f W", w); + return w; +} + +float setTemperatureInput(void) +{ + ESP_LOGD(TAG, "TemperatureInput"); + return 16.5f; +} + +void setSmartChargingCurrentOutput(float limit) +{ + ESP_LOGI(TAG, "SmartChargingCurrentOutput: %.0f", limit); +} + +void setSmartChargingPowerOutput(float limit) +{ + ESP_LOGI(TAG, "SmartChargingPowerOutput: %.0f", limit); +} + +void setSmartChargingOutput(float power, float current, int nphases) +{ + ESP_LOGI(TAG, "SmartChargingOutput: P=%.0f W, I=%.0f A, phases=%d", power, current, nphases); +} + +void setGetConfiguration(const char *payload, size_t len) +{ + ESP_LOGI(TAG, "GetConfiguration: %.*s (%u)", (int)len, payload, (unsigned)len); +} + +void setStartTransaction(const char *payload, size_t len) +{ + ESP_LOGI(TAG, "StartTransaction: %.*s (%u)", (int)len, payload, (unsigned)len); +} + +void setChangeConfiguration(const char *payload, size_t len) +{ + ESP_LOGI(TAG, "ChangeConfiguration: %.*s (%u)", (int)len, payload, (unsigned)len); +} + +void OnResetExecute(bool state) +{ + ESP_LOGI(TAG, "OnResetExecute (state=%d)", state); + esp_restart(); +} + +bool setOccupiedInput(void) +{ + ESP_LOGD(TAG, "setOccupiedInput"); + return false; +} + +bool setStartTxReadyInput(void) +{ + ESP_LOGD(TAG, "setStartTxReadyInput"); + return true; +} + +bool setStopTxReadyInput(void) +{ + ESP_LOGD(TAG, "setStopTxReadyInput"); + return true; +} + +bool setOnResetNotify(bool value) +{ + ESP_LOGI(TAG, "setOnResetNotify %d", value); + return true; +} + +void notificationOutput(OCPP_Transaction *transaction, enum OCPP_TxNotification txNotification) +{ + (void)transaction; + + ESP_LOGI(TAG, "TxNotification: %d", txNotification); + + switch (txNotification) + { + case Authorized: + ESP_LOGI(TAG, "Authorized"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, NULL, 0, portMAX_DELAY); + break; + + case AuthorizationRejected: + ESP_LOGI(TAG, "AuthorizationRejected"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY); + break; + + case AuthorizationTimeout: + ESP_LOGI(TAG, "AuthorizationTimeout"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_TIMEOUT, NULL, 0, portMAX_DELAY); + break; + + case ReservationConflict: + ESP_LOGI(TAG, "ReservationConflict"); + break; + + case ConnectionTimeout: + ESP_LOGI(TAG, "ConnectionTimeout"); + break; + + case DeAuthorized: + ESP_LOGI(TAG, "DeAuthorized"); + break; + + case RemoteStart: + ESP_LOGI(TAG, "RemoteStart"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, NULL, 0, portMAX_DELAY); + break; + + case RemoteStop: + ESP_LOGI(TAG, "RemoteStop"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_STOP, NULL, 0, portMAX_DELAY); + break; + + case StartTx: + ESP_LOGI(TAG, "StartTx"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, NULL, 0, portMAX_DELAY); + break; + + case StopTx: + ESP_LOGI(TAG, "StopTx"); + esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, NULL, 0, portMAX_DELAY); + break; + } +} + +bool ocpp_is_connected(void) +{ + return g_ocpp_conn != NULL; +} + +const char *addErrorCodeInput(void) +{ + const char *ptr = NULL; + uint32_t error = evse_get_error(); + + if (error & EVSE_ERR_PILOT_FAULT_BIT) + ptr = "InternalError"; + else if (error & EVSE_ERR_DIODE_SHORT_BIT) + ptr = "InternalError"; + else if (error & EVSE_ERR_LOCK_FAULT_BIT) + ptr = "ConnectorLockFailure"; + else if (error & EVSE_ERR_UNLOCK_FAULT_BIT) + ptr = "ConnectorLockFailure"; + else if (error & EVSE_ERR_RCM_TRIGGERED_BIT) + ptr = "OtherError"; + else if (error & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) + ptr = "OtherError"; + else if (error & EVSE_ERR_TEMPERATURE_HIGH_BIT) + ptr = "HighTemperature"; + else if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT) + ptr = "OtherError"; + + return ptr; +} + +// ----------------------------------------------------------------------------- +// Start / Stop OCPP +// ----------------------------------------------------------------------------- +void ocpp_start(void) +{ + ESP_LOGI(TAG, "Starting OCPP"); + + if (ocpp_task != NULL) + { + ESP_LOGW(TAG, "OCPP already running"); + return; + } + + storage_init_best_effort(); + + enabled = ocpp_get_enabled(); + if (!enabled) + { + ESP_LOGW(TAG, "OCPP disabled"); + return; + } + + char serverstr[64] = {0}; + ocpp_get_server(serverstr); + if (serverstr[0] == '\0') + { + ESP_LOGW(TAG, "No OCPP server configured. Skipping connection."); + return; + } + + char charge_id[64] = {0}; + ocpp_get_charge_id(charge_id); + if (charge_id[0] == '\0') + { + ESP_LOGW(TAG, "No OCPP charge_id configured. Skipping connection."); + return; + } + + mg_mgr_init(&mgr); + mg_log_set(MG_LL_ERROR); + + struct OCPP_FilesystemOpt fsopt = {.use = true, .mount = true, .formatFsOnFail = true}; + + g_ocpp_conn = ocpp_makeConnection(&mgr, + serverstr, + charge_id, + "", + "", + fsopt); + if (!g_ocpp_conn) + { + ESP_LOGE(TAG, "ocpp_makeConnection failed"); + mg_mgr_free(&mgr); + return; + } -// === Início de: components/storage_service/include/storage_service.h === + //chargePointModel: "EPower M1" + //chargePointVendor: "Plixin" + //firmwareVersion: "FW-PLXV1.0" + //chargePointSerialNumber: "SN001" + + ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false); + + ocpp_setEvReadyInput(&setEvReadyInput); + ocpp_setEvseReadyInput(&setEvseReadyInput); + ocpp_setConnectorPluggedInput(&setConnectorPluggedInput); + ocpp_setOnResetExecute(&OnResetExecute); + ocpp_setTxNotificationOutput(¬ificationOutput); + ocpp_setStopTxReadyInput(&setStopTxReadyInput); + ocpp_setOnResetNotify(&setOnResetNotify); + + ocpp_setEnergyMeterInput(&setEnergyInput); + + ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL); + ocpp_addMeterValueInputFloat(&getCurrentOffered, "Current.Offered", "A", NULL, NULL); + ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL); + ocpp_addMeterValueInputFloat(&setTemperatureInput, "Temperature", "Celsius", NULL, NULL); + ocpp_addMeterValueInputFloat(&setPowerMeterInput, "Power.Active.Import", "W", NULL, NULL); + ocpp_addMeterValueInputFloat(&setEnergyMeterInput, "Energy.Active.Import.Register", "Wh", NULL, NULL); + + ocpp_addErrorCodeInput(&addErrorCodeInput); + + xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 3, &ocpp_task); + + if (!s_auth_verify_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, + &ocpp_on_auth_verify, NULL, &s_auth_verify_inst)); + ESP_LOGI(TAG, "Registered AUTH_EVENT_TAG_VERIFY listener"); + } + + if (!s_evse_state_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, + &evse_event_handler, NULL, &s_evse_state_inst)); + } + + if (!s_evse_enable_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, + &evse_enable_available_handler, NULL, &s_evse_enable_inst)); + } + if (!s_evse_available_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, + &evse_enable_available_handler, NULL, &s_evse_available_inst)); + } + + if (!s_meter_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + METER_EVENT, METER_EVENT_DATA_READY, + &on_meter_event, NULL, &s_meter_inst)); + ESP_LOGI(TAG, "Registered METER_EVENT_DATA_READY listener (EVSE source)"); + } +} + +void ocpp_stop(void) +{ + ESP_LOGI(TAG, "Stopping OCPP"); + + if (ocpp_task) + { + vTaskDelete(ocpp_task); + ocpp_task = NULL; + } + + ocpp_deinitialize(); + + if (g_ocpp_conn) + { + ocpp_deinitConnection(g_ocpp_conn); + g_ocpp_conn = NULL; + } + + mg_mgr_free(&mgr); + + if (s_auth_verify_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, s_auth_verify_inst)); + s_auth_verify_inst = NULL; + } + + if (s_evse_state_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, s_evse_state_inst)); + s_evse_state_inst = NULL; + } + + if (s_evse_enable_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, s_evse_enable_inst)); + s_evse_enable_inst = NULL; + } + if (s_evse_available_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, s_evse_available_inst)); + s_evse_available_inst = NULL; + } + if (s_meter_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + METER_EVENT, METER_EVENT_DATA_READY, s_meter_inst)); + s_meter_inst = NULL; + } +} + +// === Fim de: components/ocpp/src/ocpp.c === + + +// === Início de: components/ocpp/include/ocpp_events.h === #pragma once +#include "esp_event.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Base de eventos do OCPP (igual ao padrão usado em auth_events.h) */ +ESP_EVENT_DECLARE_BASE(OCPP_EVENTS); + +/* IDs de eventos do OCPP */ +typedef enum { + OCPP_EVENT_CONNECTED = 0, // payload: const char* (server URL) – opcional + OCPP_EVENT_DISCONNECTED, // payload: NULL + OCPP_EVENT_AUTHORIZED, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_AUTH_REJECTED, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_AUTH_TIMEOUT, // payload: NULL + OCPP_EVENT_REMOTE_START, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_REMOTE_STOP, // payload: NULL + OCPP_EVENT_START_TX, // payload: ocpp_tx_event_t (opcional) + OCPP_EVENT_STOP_TX, // payload: ocpp_reason_event_t (opcional) + OCPP_EVENT_RESET, // payload: NULL + OCPP_EVENT_OPERATIVE_UPDATED +} ocpp_event_id_t; + +/* Limites de strings simples (evita dependência de auth.h) */ +#define OCPP_IDTAG_MAX 32 +#define OCPP_REASON_MAX 32 + +/* Payloads opcionais */ +typedef struct { + char idTag[OCPP_IDTAG_MAX]; +} ocpp_idtag_event_t; + +typedef struct { + int tx_id; // se disponível +} ocpp_tx_event_t; + +typedef struct { + char reason[OCPP_REASON_MAX]; +} ocpp_reason_event_t; + +// Payload do novo evento +typedef struct { + bool operative; // true = Operative, false = Inoperative + int64_t timestamp_us; // esp_timer_get_time() +} ocpp_operative_event_t; + +#ifdef __cplusplus +} +#endif + +// === Fim de: components/ocpp/include/ocpp_events.h === + + +// === Início de: components/ocpp/include/ocpp.h === +#ifndef OCPP_H_ +#define OCPP_H_ #include -#include - -#include "esp_err.h" -#include "freertos/FreeRTOS.h" -#include "nvs.h" // para ESP_ERR_NVS_INVALID_LENGTH (documentação/semântica do blob) +#include #ifdef __cplusplus extern "C" { #endif /** - * NVS limita namespace e key a 15 chars (+ '\0') - * (ver docs do NVS: NVS_KEY_NAME_MAX_SIZE / NVS_NS_NAME_MAX_SIZE) + * @brief Start OCPP */ -#define STORAGE_NS_MAX_LEN 16 -#define STORAGE_KEY_MAX_LEN 16 +void ocpp_start(void); /** - * Ajusta conforme o teu sistema. - * Nota: setters async usam xQueueSend(..., 0) -> podem falhar com ESP_ERR_TIMEOUT se a fila estiver cheia. + * @brief Stop OCPP */ -#ifndef STORAGE_QUEUE_LEN -#define STORAGE_QUEUE_LEN 32 -#endif +void ocpp_stop(void); -#ifndef STORAGE_MAX_PENDING -#define STORAGE_MAX_PENDING 48 -#endif +/* Config getters / setters */ +bool ocpp_get_enabled(void); +void ocpp_set_enabled(bool value); -#ifndef STORAGE_COMMIT_DEBOUNCE_MS -#define STORAGE_COMMIT_DEBOUNCE_MS 500 -#endif +void ocpp_get_server(char *value); // buffer >= 64 +void ocpp_set_server(char *value); -/** - * Tamanho máximo (bytes) para STR/BLOB guardado em pending/queue. - * IMPORTANTE: - * - Este limite aplica-se aos SETs (async) e ao cache pending. - * - Leituras (sync) a partir do NVS também ficam, na prática, limitadas por este design - * se esperares que valores maiores existam no NVS (recomenda-se alinhar o teu sistema a este máximo). - */ -#ifndef STORAGE_MAX_VALUE_BYTES -#define STORAGE_MAX_VALUE_BYTES 96 -#endif +void ocpp_get_charge_id(char *value); // buffer >= 64 +void ocpp_set_charge_id(char *value); -/** - * Inicializa o serviço e cria a task interna. - * - * Requisitos: - * - nvs_flash_init() deve ter sido chamado antes (normalmente no arranque da app). - */ -esp_err_t storage_service_init(void); - -// -------------------- Async setters (não bloqueiam; commit é debounced) -------------------- - -/** - * Retorna: - * - ESP_OK - * - ESP_ERR_INVALID_ARG (ns/key inválidos ou > 15 chars) - * - ESP_ERR_TIMEOUT (fila cheia) - * - ESP_ERR_INVALID_STATE (serviço não inicializado) - */ -esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v); -esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v); -esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v); - -/** - * Retorna: - * - ESP_OK - * - ESP_ERR_INVALID_ARG (ns/key/str inválidos) - * - ESP_ERR_INVALID_SIZE (str > STORAGE_MAX_VALUE_BYTES) - * - ESP_ERR_TIMEOUT (fila cheia) - * - ESP_ERR_INVALID_STATE (serviço não inicializado) - */ -esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str); - -/** - * Retorna: - * - ESP_OK - * - ESP_ERR_INVALID_ARG (ns/key inválidos; data NULL com len>0) - * - ESP_ERR_INVALID_SIZE (len > STORAGE_MAX_VALUE_BYTES) - * - ESP_ERR_TIMEOUT (fila cheia) - * - ESP_ERR_INVALID_STATE (serviço não inicializado) - */ -esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len); - -esp_err_t storage_erase_key_async(const char *ns, const char *key); - -/** Força commit imediato (async). Mesmas notas de fila cheia/invalid state. */ -esp_err_t storage_flush_async(void); - -// -------------------- Sync getters (bloqueiam até ler do NVS/pending) -------------------- -/** - * NOTAS IMPORTANTES: - * - Funções sync NÃO devem ser chamadas em ISR (usam mutex/queues e podem bloquear). - * - O timeout `to` aplica-se ao lock + envio/receção da resposta. - * - * Retorna tipicamente: - * - ESP_OK - * - ESP_ERR_NOT_FOUND (chave não existe) - * - ESP_ERR_TIMEOUT - * - outros erros do NVS - */ -esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to); -esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to); -esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to); - -/** - * Lê string para `out` com tamanho `out_sz`. - * Em sucesso, `out` fica sempre null-terminated. - * - * Retorna: - * - ESP_OK - * - ESP_ERR_NOT_FOUND - * - ESP_ERR_INVALID_ARG (out/out_sz inválidos; ns/key inválidos) - * - ESP_ERR_TIMEOUT - * - erros do NVS - */ -esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to); - -/** - * Blob sync (semântica semelhante a nvs_get_blob): - * - Se out == NULL: devolve ESP_OK e coloca em *inout_len o tamanho requerido - * - Se out != NULL e *inout_len < requerido: devolve ESP_ERR_NVS_INVALID_LENGTH e atualiza *inout_len com requerido - * - Se OK: copia e atualiza *inout_len com o tamanho real - * - * Retorna: - * - ESP_OK - * - ESP_ERR_NOT_FOUND - * - ESP_ERR_NVS_INVALID_LENGTH - * - ESP_ERR_INVALID_ARG (inout_len NULL; ns/key inválidos) - * - ESP_ERR_TIMEOUT - * - erros do NVS - */ -esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to); - -/** Força commit imediato (sync). Mesmas notas de timeout/ISR. */ -esp_err_t storage_flush_sync(TickType_t to); +/* Estado de conexão */ +bool ocpp_is_connected(void); #ifdef __cplusplus } #endif -// === Fim de: components/storage_service/include/storage_service.h === +#endif /* OCPP_H_ */ + +// === Fim de: components/ocpp/include/ocpp.h === diff --git a/readproject.py b/readproject.py index 0fbbe12..2e626b5 100644 --- a/readproject.py +++ b/readproject.py @@ -1,6 +1,6 @@ import os -TAMANHO_MAX = 200000 # Limite por arquivo +TAMANHO_MAX = 230000 # Limite por arquivo def coletar_arquivos(diretorios, extensoes=(".c", ".h")): arquivos = [] @@ -51,9 +51,9 @@ def unir_em_partes(arquivos, prefixo="projeto_parte", limite=TAMANHO_MAX): print(f"🔹 Arquivos gerados: {parte}") def main(): - diretorio_main = "" #"main" + diretorio_main = "main" componentes_escolhidos = [ - "storage_service" + "ocpp" ] diretorios_componentes = [os.path.join("components", nome) for nome in componentes_escolhidos]