diff --git a/cfg/esp32devkitc/board.cfg b/cfg/esp32devkitc/board.cfg index d1014fb..14630af 100755 --- a/cfg/esp32devkitc/board.cfg +++ b/cfg/esp32devkitc/board.cfg @@ -4,7 +4,7 @@ DEVICE_NAME=Plixin Evse LED_CHARGING=y LED_CHARGING_GPIO=14 LED_ERROR=y -LED_ERROR_GPIO=13 +LED_ERROR_GPIO=26 LED_STOP=y LED_STOP_GPIO=12 diff --git a/components/auth/src/wiegand_reader.c b/components/auth/src/wiegand_reader.c index cfc94a9..01ca594 100755 --- a/components/auth/src/wiegand_reader.c +++ b/components/auth/src/wiegand_reader.c @@ -51,6 +51,7 @@ static void wiegand_task(void *arg) { snprintf(tag, sizeof(tag), "%03d%03d%03d%03d", p.data[0], p.data[1], p.data[2], p.data[3]); } else { ESP_LOGW(TAG, "Unsupported bit length: %d", (int)p.bits); + ESP_LOG_BUFFER_HEX(TAG, p.data, sizeof(p.data)); // loga o buffer bruto continue; } diff --git a/components/buzzer/src/buzzer.c b/components/buzzer/src/buzzer.c index baf5982..52e1d84 100755 --- a/components/buzzer/src/buzzer.c +++ b/components/buzzer/src/buzzer.c @@ -16,7 +16,7 @@ typedef struct { } buzzer_step_t; // Padrões de buzzer -static const buzzer_step_t pattern_plugged[] = {{100, 100}, {200, 0}}; +static const buzzer_step_t pattern_plugged[] = {{200, 100}}; static const buzzer_step_t pattern_unplugged[] = {{150, 150}, {150, 150}, {150, 0}}; static const buzzer_step_t pattern_charging[] = {{80, 150}, {100, 120}, {120, 100}, {140, 0}}; static const buzzer_step_t pattern_ap_start[] = {{300, 150}, {300, 0}}; @@ -76,7 +76,7 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi 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_LOGD(TAG, "EVSE event received: state = %d", evt->state); + ESP_LOGI(TAG, "EVSE event received: state = %d", evt->state); buzzer_event_data_t buzzer_evt = {0}; @@ -115,7 +115,7 @@ static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, voi if (id == AUTH_EVENT_TAG_PROCESSED) { const auth_tag_event_data_t *evt = (const auth_tag_event_data_t *)event_data; - ESP_LOGD(TAG, "AUTH processed: tag=%s authorized=%d", evt->tag, evt->authorized); + ESP_LOGI(TAG, "AUTH processed: tag=%s authorized=%d", evt->tag, evt->authorized); buzzer_evt.pattern = evt->authorized ? BUZZER_PATTERN_CARD_READ : BUZZER_PATTERN_CARD_DENIED; } else if (id == AUTH_EVENT_TAG_SAVED) { diff --git a/components/evse/evse_config.c b/components/evse/evse_config.c index 875a3ee..5128ea9 100755 --- a/components/evse/evse_config.c +++ b/components/evse/evse_config.c @@ -4,6 +4,7 @@ #include "evse_limits.h" #include "esp_log.h" #include "nvs.h" +#include "esp_timer.h" static const char *TAG = "evse_config"; @@ -60,7 +61,7 @@ void evse_check_defaults(void) { } // Runtime charging current initialized from persisted default - charging_current_runtime = charging_current; + charging_current_runtime = max_charging_current; ESP_LOGD(TAG, "Runtime charging current initialized to: %d", charging_current_runtime); // Auth required @@ -127,6 +128,7 @@ esp_err_t evse_set_max_charging_current(uint8_t value) { if (value < MIN_CHARGING_CURRENT_LIMIT || value > MAX_CHARGING_CURRENT_LIMIT) return ESP_ERR_INVALID_ARG; max_charging_current = value; + evse_set_runtime_charging_current(value); nvs_set_u8(nvs, "max_chrg_curr", value); return nvs_commit(nvs); } @@ -161,19 +163,35 @@ esp_err_t evse_set_default_charging_current(uint16_t value) { // Runtime current (not saved) // ======================== void evse_set_runtime_charging_current(uint16_t value) { - - if (value > (max_charging_current)) { - value= max_charging_current; - } - if (value < (MIN_CHARGING_CURRENT_LIMIT) ) { - value= MIN_CHARGING_CURRENT_LIMIT; + ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime); + + if (value > max_charging_current) { + value = max_charging_current; + } else if (value < MIN_CHARGING_CURRENT_LIMIT) { + value = MIN_CHARGING_CURRENT_LIMIT; } charging_current_runtime = value; - ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime); + + // --- PUBLICA ALTERAÇÃO DE CONFIG DO EVSE --- + evse_config_event_data_t evt = { + .charging = evse_state_is_charging(evse_get_state()), + .hw_max_current = (float)evse_get_max_charging_current(), + .runtime_current = (float)charging_current_runtime, + .timestamp_us = esp_timer_get_time() + }; + + esp_event_post(EVSE_EVENTS, + EVSE_EVENT_CONFIG_UPDATED, + &evt, + sizeof(evt), + portMAX_DELAY); + + } + uint16_t evse_get_runtime_charging_current(void) { return charging_current_runtime; } diff --git a/components/evse/evse_manager.c b/components/evse/evse_manager.c index 8c921cb..b569c9a 100755 --- a/components/evse/evse_manager.c +++ b/components/evse/evse_manager.c @@ -40,7 +40,7 @@ static void on_auth_event(void* arg, esp_event_base_t base, int32_t id, void* da switch (id) { case AUTH_EVENT_TAG_PROCESSED: { auth_tag_event_data_t *evt = (auth_tag_event_data_t*)data; - ESP_LOGI("EVSE", "Tag: %s | Autorizada: %s", evt->tag, evt->authorized ? "SIM" : "NÃO"); + ESP_LOGI("EVSE", "Tag: %s | Autorized: %s", evt->tag, evt->authorized ? "AUTHORIZED" : "DENIED"); evse_state_set_authorized(evt->authorized); break; } @@ -74,11 +74,11 @@ static void on_loadbalancer_event(void* handler_arg, esp_event_base_t event_base ESP_LOGI(TAG, "Loadbalancer %s (ts: %lld)", evt->enabled ? "ENABLED" : "DISABLED", evt->timestamp_us); // Ações adicionais podem ser adicionadas aqui conforme necessário - } else if (event_id == LOADBALANCER_EVENT_CHARGING_LIMIT_CHANGED) { - const loadbalancer_charging_limit_event_t* evt = (const loadbalancer_charging_limit_event_t*) event_data; - ESP_LOGD(TAG, "Novo limite de corrente: %.1f A (ts: %lld)", evt->limit, evt->timestamp_us); - evse_set_runtime_charging_current((uint16_t)(evt->limit)); - } + } else if (event_id == LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT) { + const loadbalancer_master_limit_event_t* evt = (const loadbalancer_master_limit_event_t*) event_data; + ESP_LOGI(TAG, "Novo limite de corrente (master): %u A (ts: %lld)", evt->max_current, evt->timestamp_us); + evse_set_runtime_charging_current(evt->max_current); + } } // ===== Inicialização ===== diff --git a/components/evse/evse_state.c b/components/evse/evse_state.c index 92e29d8..651c454 100755 --- a/components/evse/evse_state.c +++ b/components/evse/evse_state.c @@ -24,8 +24,8 @@ static const char *TAG = "evse_state"; static evse_state_event_t map_state_to_event(evse_state_t s) { switch (s) { case EVSE_STATE_A: return EVSE_STATE_EVENT_IDLE; - case EVSE_STATE_B1: return EVSE_STATE_EVENT_WAITING; - case EVSE_STATE_B2: + case EVSE_STATE_B1: + case EVSE_STATE_B2: return EVSE_STATE_EVENT_WAITING; case EVSE_STATE_C1: case EVSE_STATE_C2: return EVSE_STATE_EVENT_CHARGING; case EVSE_STATE_E: diff --git a/components/evse/include/evse_config.h b/components/evse/include/evse_config.h index ae5d07c..6ac270f 100755 --- a/components/evse/include/evse_config.h +++ b/components/evse/include/evse_config.h @@ -4,6 +4,8 @@ #include #include #include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "evse_events.h" #ifdef __cplusplus extern "C" { diff --git a/components/evse/include/evse_events.h b/components/evse/include/evse_events.h index 114bf0a..2f68906 100755 --- a/components/evse/include/evse_events.h +++ b/components/evse/include/evse_events.h @@ -9,7 +9,7 @@ ESP_EVENT_DECLARE_BASE(EVSE_EVENTS); typedef enum { EVSE_EVENT_INIT, EVSE_EVENT_STATE_CHANGED, - // Outros eventos possíveis futuramente + EVSE_EVENT_CONFIG_UPDATED, } evse_event_id_t; typedef enum { @@ -23,5 +23,11 @@ typedef struct { evse_state_event_t state; } evse_state_event_data_t; +typedef struct { + bool charging; // Estado de carregamento + float hw_max_current; // Corrente máxima suportada pelo hardware + float runtime_current; // Corrente de carregamento em uso + int64_t timestamp_us; // Momento da atualização +} evse_config_event_data_t; #endif // EVSE_EVENTS_H diff --git a/components/evse_link/CMakeLists.txt b/components/evse_link/CMakeLists.txt new file mode 100755 index 0000000..413b6e8 --- /dev/null +++ b/components/evse_link/CMakeLists.txt @@ -0,0 +1,16 @@ +set(srcs +"src/evse_link_master.c" +"src/evse_link_slave.c" +"src/evse_link_events.c" +"src/evse_link_framing.c" +"src/evse_link.c" + ) + + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "include" + PRIV_REQUIRES driver esp_timer nvs_flash + REQUIRES config evse loadbalancer) + + + diff --git a/components/evse_link/include/evse_link.h b/components/evse_link/include/evse_link.h new file mode 100755 index 0000000..1bce5ff --- /dev/null +++ b/components/evse_link/include/evse_link.h @@ -0,0 +1,45 @@ +#ifndef EVSE_LINK_H_ +#define EVSE_LINK_H_ + +#include +#include + +// Operation mode: slave or master +typedef enum { + EVSE_LINK_MODE_SLAVE = 0, + EVSE_LINK_MODE_MASTER = 1 +} evse_link_mode_t; + +// Callback invoked when a complete frame is received: +// src: device address of sender (0–255) +// dest: device address of receiver (0–255 or 0xFF broadcast) +// payload: pointer to received data buffer (command + data) +// len: length of payload (0–255) +typedef void (*evse_link_rx_cb_t)(uint8_t src, uint8_t dest, + const uint8_t *payload, uint8_t len); + +// Initializes the EVSE-Link component +void evse_link_init(void); + +// Sends a framed payload to `dest` with length `len`. +// The source address is automatically set from configuration. +// Returns true on successful enqueue/transmit. +bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len); + +// Feeds a received byte into the framing parser. +void evse_link_recv_byte(uint8_t byte); + +// Registers a callback to receive complete frames. +void evse_link_register_rx_cb(evse_link_rx_cb_t cb); + +// Runtime configuration getters/setters +void evse_link_set_mode(evse_link_mode_t mode); +evse_link_mode_t evse_link_get_mode(void); + +void evse_link_set_self_id(uint8_t id); +uint8_t evse_link_get_self_id(void); + +void evse_link_set_enabled(bool enabled); +bool evse_link_is_enabled(void); + +#endif // EVSE_LINK_H_ diff --git a/components/evse_link/include/evse_link_events.h b/components/evse_link/include/evse_link_events.h new file mode 100644 index 0000000..9da2283 --- /dev/null +++ b/components/evse_link/include/evse_link_events.h @@ -0,0 +1,16 @@ +#ifndef EVSE_LINK_EVENTS_H_ +#define EVSE_LINK_EVENTS_H_ + +#include "esp_event.h" + +ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS); + +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_CURRENT_LIMIT_APPLIED, + LINK_EVENT_SLAVE_CONFIG_UPDATED // <- NOVO evento +} evse_link_event_t; +#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 new file mode 100644 index 0000000..13c489d --- /dev/null +++ b/components/evse_link/include/evse_link_framing.h @@ -0,0 +1,42 @@ +#ifndef EVSE_LINK_FRAMING_H_ +#define EVSE_LINK_FRAMING_H_ + +#include +#include +#include "driver/uart.h" + +// UART configuration +#define UART_PORT UART_NUM_2 +#define UART_BAUDRATE 115200 +#define UART_RX_BUF_SIZE 256 + +// GPIO pin assignments for UART +#define TX_PIN 21 // GPIO21 -> RX on other board +#define RX_PIN 22 // GPIO22 -> TX on other board + +// Frame delimiters +#define MAGIC_START 0x7E +#define MAGIC_END 0x7F + +// Maximum payload (excluding sequence byte) +#define EVSE_LINK_MAX_PAYLOAD 254 + +// Callback type for when a full frame is received +typedef void (*evse_link_frame_cb_t)(uint8_t src, uint8_t dest, + const uint8_t *payload, uint8_t len); + +// Initialize framing module (mutex, UART driver, etc.) +void evse_link_framing_init(void); + +// Send a framed payload to `dest` with length `len` +// Includes source address in the header +bool evse_link_framing_send(uint8_t dest, uint8_t src, + const uint8_t *payload, uint8_t len); + +// Feed a received byte into the framing parser +void evse_link_framing_recv_byte(uint8_t byte); + +// Register a callback for complete frames +void evse_link_framing_register_cb(evse_link_frame_cb_t cb); + +#endif // EVSE_LINK_FRAMING_H_ diff --git a/components/evse_link/src/evse_link.c b/components/evse_link/src/evse_link.c new file mode 100755 index 0000000..b70d570 --- /dev/null +++ b/components/evse_link/src/evse_link.c @@ -0,0 +1,142 @@ +// components/evse_link/src/evse_link.c + +#include "evse_link.h" +#include "evse_link_framing.h" +#include "driver/uart.h" +#include "nvs.h" +#include "esp_log.h" +#include "freertos/task.h" +#include "freertos/semphr.h" + +static const char *TAG = "evse_link"; + +// NVS keys +#define _NVS_NAMESPACE "evse_link" +#define _NVS_MODE_KEY "mode" +#define _NVS_ID_KEY "self_id" +#define _NVS_ENABLED_KEY "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; + +// 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 NVS +enum { EV_OK = ESP_OK }; +static void load_link_config(void) { + nvs_handle_t handle; + if (nvs_open(_NVS_NAMESPACE, NVS_READONLY, &handle) != EV_OK) { + ESP_LOGW(TAG, "NVS open failed, using defaults"); + return; + } + uint8_t mode, id, en; + if (nvs_get_u8(handle, _NVS_MODE_KEY, &mode) == EV_OK && + (mode == EVSE_LINK_MODE_MASTER || mode == EVSE_LINK_MODE_SLAVE)) { + _mode = (evse_link_mode_t)mode; + } + if (nvs_get_u8(handle, _NVS_ID_KEY, &id) == EV_OK) { + _self_id = id; + } + if (nvs_get_u8(handle, _NVS_ENABLED_KEY, &en) == EV_OK) { + _enabled = (en != 0); + } + nvs_close(handle); +} + +// Save config to NVS +static void save_link_config(void) { + nvs_handle_t handle; + if (nvs_open(_NVS_NAMESPACE, NVS_READWRITE, &handle) == EV_OK) { + nvs_set_u8(handle, _NVS_MODE_KEY, (uint8_t)_mode); + nvs_set_u8(handle, _NVS_ID_KEY, _self_id); + nvs_set_u8(handle, _NVS_ENABLED_KEY, _enabled ? 1 : 0); + nvs_commit(handle); + nvs_close(handle); + } else { + ESP_LOGE(TAG, "Failed to save NVS"); + } +} + +// Getters/setters +void evse_link_set_mode(evse_link_mode_t m) { _mode = m; save_link_config(); } +evse_link_mode_t evse_link_get_mode(void) { return _mode; } +void evse_link_set_self_id(uint8_t id) { _self_id = id; save_link_config(); } +uint8_t evse_link_get_self_id(void) { return _self_id; } +void evse_link_set_enabled(bool en) { _enabled = en; save_link_config(); } +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) { + uint8_t buf[UART_RX_BUF_SIZE]; + while (true) { + int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000)); + if (len > 0) { + for (int i = 0; i < len; ++i) { + evse_link_recv_byte(buf[i]); + } + } + } +} + +// Initialize EVSE-Link component +void evse_link_init(void) { + load_link_config(); + + ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d", + _mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S', + _self_id, _enabled); + 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, 4, NULL); + + // 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()) return false; + uint8_t src = evse_link_get_self_id(); + 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_events.c b/components/evse_link/src/evse_link_events.c new file mode 100644 index 0000000..2c6ea26 --- /dev/null +++ b/components/evse_link/src/evse_link_events.c @@ -0,0 +1,4 @@ +#include "evse_link_events.h" + +// Esta única linha insere o símbolo EVSE_LINK_EVENTS no binário +ESP_EVENT_DEFINE_BASE(EVSE_LINK_EVENTS); diff --git a/components/evse_link/src/evse_link_framing.c b/components/evse_link/src/evse_link_framing.c new file mode 100644 index 0000000..284a580 --- /dev/null +++ b/components/evse_link/src/evse_link_framing.c @@ -0,0 +1,168 @@ +// components/evse_link_framing/src/evse_link_framing.c + +#include "evse_link_framing.h" +#include "driver/uart.h" +#include "freertos/semphr.h" +#include "esp_log.h" +#include + +static const char *TAG = "evse_framing"; + +static SemaphoreHandle_t tx_mutex; +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) { + uint8_t crc = 0; + for (uint8_t i = 0; i < len; ++i) { + crc ^= data[i]; + for (uint8_t b = 0; b < 8; ++b) { + crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1); + } + } + return crc; +} + +void evse_link_framing_init(void) { + // Create mutex for TX + tx_mutex = xSemaphoreCreateMutex(); + // Install UART driver + uart_driver_install(UART_PORT, UART_RX_BUF_SIZE * 2, 0, 0, NULL, 0); + uart_param_config(UART_PORT, &(uart_config_t){ + .baud_rate = UART_BAUDRATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE + }); + uart_set_pin(UART_PORT, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); +} + +bool evse_link_framing_send(uint8_t dest, uint8_t src, + const uint8_t *payload, uint8_t len) { + if (len > EVSE_LINK_MAX_PAYLOAD) return false; + if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) return false; + + // Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END + uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7]; + int idx = 0; + frame[idx++] = MAGIC_START; + frame[idx++] = dest; + frame[idx++] = src; + frame[idx++] = len + 1; // +1 for SEQ + frame[idx++] = seq; + memcpy(&frame[idx], payload, len); + idx += len; + + // CRC covers DEST + SRC + LEN + SEQ + PAYLOAD + uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; + memcpy(crc_input, &frame[1], 3 + 1 + len); + frame[idx++] = crc8(crc_input, 3 + 1 + len); + + frame[idx++] = MAGIC_END; + + uart_write_bytes(UART_PORT, (const char *)frame, idx); + uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(10)); + xSemaphoreGive(tx_mutex); + + ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u", + dest, src, len, seq); + + seq++; // increment sequence after sending + return true; +} + +void evse_link_framing_recv_byte(uint8_t b) { + // State machine for frame parsing + static enum { + ST_WAIT_START, + ST_WAIT_DEST, + ST_WAIT_SRC, + ST_WAIT_LEN, + ST_WAIT_SEQ, + ST_READING, + ST_WAIT_CRC, + ST_WAIT_END + } rx_state = ST_WAIT_START; + + static uint8_t rx_dest; + static uint8_t rx_src; + static uint8_t rx_len; + 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) { + case ST_WAIT_START: + if (b == MAGIC_START) { + rx_state = ST_WAIT_DEST; + } + break; + + case ST_WAIT_DEST: + rx_dest = b; + rx_state = ST_WAIT_SRC; + break; + + case ST_WAIT_SRC: + rx_src = b; + rx_state = ST_WAIT_LEN; + break; + + case ST_WAIT_LEN: + rx_len = b; // includes SEQ + payload + rx_pos = 0; + rx_state = ST_WAIT_SEQ; + break; + + case ST_WAIT_SEQ: + rx_seq = b; + rx_state = (rx_len > 1) ? ST_READING : ST_WAIT_CRC; + break; + + case ST_READING: + rx_buf[rx_pos++] = b; + if (rx_pos >= (rx_len - 1)) { // all payload bytes read + rx_state = ST_WAIT_CRC; + } + break; + + case ST_WAIT_CRC: + rx_crc = b; + rx_state = ST_WAIT_END; + break; + + case ST_WAIT_END: + if (b == MAGIC_END) { + // Build data for CRC calculation + 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; + memcpy(&temp[temp_len], rx_buf, rx_len - 1); + temp_len += rx_len - 1; + + uint8_t expected = crc8(temp, temp_len); + if (expected == rx_crc) { + if (rx_cb) { + rx_cb(rx_src, rx_dest, rx_buf, rx_len - 1); + } + ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u", + rx_src, rx_dest, rx_len - 1, rx_seq); + } else { + ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X", + expected, rx_crc); + } + } + rx_state = ST_WAIT_START; + break; + } +} + +void evse_link_framing_register_cb(evse_link_frame_cb_t cb) { + rx_cb = cb; +} diff --git a/components/evse_link/src/evse_link_master.c b/components/evse_link/src/evse_link_master.c new file mode 100644 index 0000000..4cb9fa8 --- /dev/null +++ b/components/evse_link/src/evse_link_master.c @@ -0,0 +1,152 @@ +// === components/evse_link/src/evse_link_master.c === + +#include "evse_link.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/timers.h" +#include "esp_log.h" +#include "esp_event.h" +#include +#include + +#include "loadbalancer_events.h" + +static const char *TAG = "evse_link_master"; + +// Link commands +#define CMD_POLL 0x01 +#define CMD_HEARTBEAT 0x02 +#define CMD_HEARTBEAT_ACK 0x09 +#define CMD_CONFIG_BROADCAST 0x03 +#define CMD_SET_CURRENT 0x08 + +// 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 + +// 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) }; + +// --- 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) return; + const loadbalancer_slave_limit_event_t *evt = data; + uint8_t slave_id = evt->slave_id; + uint16_t max_current = evt->max_current; + + uint8_t buf[LEN_SET_CURRENT] = { + 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); +} + +// --- 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 +} + +// --- 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 ??? +} + +static void on_frame_master(uint8_t src, uint8_t dest, + const uint8_t *payload, uint8_t len) { + if (len < 1) return; + uint8_t cmd = payload[0]; + + switch (cmd) { + case CMD_HEARTBEAT: { + if (len != 6) { // 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); + + 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 + .timestamp_us = esp_timer_get_time() + }; + + 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); + 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]); + break; + + default: + ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src); + } +} + + +// --- 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( + LOADBALANCER_EVENTS, + LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + on_new_limit, + 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); + + // 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); +} diff --git a/components/evse_link/src/evse_link_slave.c b/components/evse_link/src/evse_link_slave.c new file mode 100644 index 0000000..75639ba --- /dev/null +++ b/components/evse_link/src/evse_link_slave.c @@ -0,0 +1,164 @@ +// === components/evse_link/src/evse_link_slave.c === + +#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 "esp_log.h" +#include "esp_event.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 + +// 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 + +// Timing +#define FALLBACK_TIMEOUT_MS 120000 + +static TimerHandle_t fallback_timer = NULL; +static bool safe_mode = false; + +// --- 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(); + + ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA", + charging, hw_max, runtime); + + 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)); +} + + +// --- 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(); + } +} + +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; + + uint8_t cmd = payload[0]; + switch (cmd) { + case CMD_POLL: + ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src); + break; + + case CMD_CONFIG_BROADCAST: + ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src); + break; + + case CMD_SET_CURRENT: { + uint16_t amps = payload[1] | (payload[2] << 8); + 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); + 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; + + default: + ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src); + } +} + + + +// --- Periodic heartbeat task --- +static void slave_heartbeat_task(void *arg) { + const TickType_t interval = pdMS_TO_TICKS(10000); + for (;;) { + send_heartbeat_frame(); + vTaskDelay(interval); + } +} + +// --- Fallback safe mode callback --- +static void fallback_timer_cb(TimerHandle_t xTimer) { + 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); + } +} + +// --- Slave initialization --- +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); + } + + // react to EVSE state changes + ESP_ERROR_CHECK( + esp_event_handler_register( + EVSE_EVENTS, + EVSE_EVENT_STATE_CHANGED, + evse_event_handler, + NULL + ) + ); +} diff --git a/components/loadbalancer/include/loadbalancer.h b/components/loadbalancer/include/loadbalancer.h index 1b5b654..93ed677 100755 --- a/components/loadbalancer/include/loadbalancer.h +++ b/components/loadbalancer/include/loadbalancer.h @@ -11,67 +11,32 @@ extern "C" { /** - * @brief Initializes the load balancer. - * - * This function configures the load balancer and its resources, including - * any necessary persistence configurations, such as storage in NVS (Non-Volatile Storage). - * This function prepares the system to perform load balancing efficiently. + * @brief Inicializa o módulo de load balancer */ void loadbalancer_init(void); /** - * @brief Continuous task for the load balancer. - * - * This function executes the load balancing logic continuously, typically in a FreeRTOS task. - * It performs balance calculations, checks the grid current and energy conditions, and adjusts - * the outputs as necessary to ensure efficient energy consumption. - * - * @param param Input parameter, usually used to pass additional information or relevant context - * for the task execution. + * @brief Task contínua do algoritmo de balanceamento */ void loadbalancer_task(void *param); /** - * @brief Enables or disables the load balancing system. - * - * This function allows enabling or disabling the load balancing system. When enabled, the load - * balancer starts managing the grid current based on the configured limits. If disabled, the system - * operates without balancing. - * - * The configuration is persisted in NVS, ensuring that the choice is maintained across system restarts. - * - * @param value If true, enables load balancing. If false, disables it. + * @brief Ativa ou desativa o load balancing */ void loadbalancer_set_enabled(bool value); /** - * @brief Checks if load balancing is enabled. - * - * This function returns the current status of the load balancing system. - * - * @return Returns true if load balancing is enabled, otherwise returns false. + * @brief Verifica se o load balancing está ativo */ bool loadbalancer_is_enabled(void); /** - * @brief Sets the maximum grid current. - * - * This function configures the maximum grid current that can be supplied to the load balancing system. - * The value set ensures that the system does not overload the electrical infrastructure and respects - * the safety limits. - * - * @param max_grid_current The maximum allowed current (in amperes) for the load balancing system. - * This value should be appropriate for the grid capacity and the installation. + * @brief Define a corrente máxima do grid */ esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current); - /** - * @brief Gets the maximum grid current. - * - * This function retrieves the current maximum grid current limit. - * - * @return The maximum grid current (in amperes). + * @brief Obtém a corrente máxima do grid */ uint8_t load_balancing_get_max_grid_current(void); diff --git a/components/loadbalancer/include/loadbalancer_events.h b/components/loadbalancer/include/loadbalancer_events.h index 7d8de8f..ffbe8c3 100644 --- a/components/loadbalancer/include/loadbalancer_events.h +++ b/components/loadbalancer/include/loadbalancer_events.h @@ -1,7 +1,7 @@ #pragma once #include "esp_event.h" -#include #include +#include #include "esp_timer.h" ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); @@ -9,16 +9,39 @@ ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); typedef enum { LOADBALANCER_EVENT_INIT, LOADBALANCER_EVENT_STATE_CHANGED, - LOADBALANCER_EVENT_CHARGING_LIMIT_CHANGED + LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT, + LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, + LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + LOADBALANCER_EVENT_SLAVE_STATUS } loadbalancer_event_id_t; typedef struct { - float limit; - int64_t timestamp_us; -} loadbalancer_charging_limit_event_t; - -typedef struct { - bool enabled; - int64_t timestamp_us; + bool enabled; + int64_t timestamp_us; } loadbalancer_state_event_t; +// (opcional) +typedef struct { + float limit; + int64_t timestamp_us; +} loadbalancer_global_limit_event_t; + +typedef struct { + uint8_t slave_id; + uint16_t max_current; + int64_t timestamp_us; +} loadbalancer_master_limit_event_t; + +typedef struct { + uint8_t slave_id; + uint16_t max_current; + int64_t timestamp_us; +} loadbalancer_slave_limit_event_t; + +typedef struct { + uint8_t slave_id; // ID do slave que reportou + bool charging; // Status de carregamento + float hw_max_current; // Limite máximo de corrente do hardware informado + float runtime_current; // Corrente atual de carregamento (A) + int64_t timestamp_us; // Momento em que o status foi coletado +} loadbalancer_slave_status_event_t; \ No newline at end of file diff --git a/components/loadbalancer/src/loadbalancer.c b/components/loadbalancer/src/loadbalancer.c index b9df651..a9b9b99 100755 --- a/components/loadbalancer/src/loadbalancer.c +++ b/components/loadbalancer/src/loadbalancer.c @@ -10,17 +10,22 @@ #include #include "meter_events.h" #include "evse_events.h" -#include +#include "math.h" +#include // Necessário para PRIu64 + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif static const char *TAG = "loadbalancer"; -// Configurable limits -#define MIN_CHARGING_CURRENT_LIMIT 6 // A -#define MAX_CHARGING_CURRENT_LIMIT 32 // A -#define MIN_GRID_CURRENT_LIMIT 6 // A -#define MAX_GRID_CURRENT_LIMIT 100 // A +// Limites configuráveis +#define MIN_CHARGING_CURRENT_LIMIT 6 // A +#define MAX_CHARGING_CURRENT_LIMIT 32 // A +#define MIN_GRID_CURRENT_LIMIT 6 // A +#define MAX_GRID_CURRENT_LIMIT 100 // A -// Parameters +// Parâmetros static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; static bool loadbalancer_enabled = false; @@ -29,16 +34,109 @@ static float evse_current = 0.0f; static input_filter_t grid_filter; static input_filter_t evse_filter; -#define NVS_NAMESPACE "loadbalancing" -#define NVS_MAX_GRID_CURRENT "max_grid_curr" -#define NVS_LOADBALANCER_ENABLED "enabled" +#define MAX_SLAVES 255 +#define CONNECTOR_COUNT (MAX_SLAVES + 1) -// Reset filter helper +// Estrutura unificada para master e slaves +typedef struct { + uint8_t id; // 0xFF = master, 0..MAX_SLAVES-1 = slave + bool is_master; + bool charging; + float hw_max_current; + float runtime_current; + int64_t timestamp; // microssegundos + bool online; + float assigned; +} evse_connector_t; + +static evse_connector_t connectors[CONNECTOR_COUNT]; + +const int64_t METRICS_TIMEOUT_US = 60 * 1000000; // 60 segundos + +// Helper: inicializa array de conectores +static void init_connectors(void) +{ + // master em índice 0 + connectors[0] = (evse_connector_t){ + .id = 0xFF, + .is_master = true, + .charging = false, + .hw_max_current = MAX_CHARGING_CURRENT_LIMIT, + .runtime_current = 0, + .timestamp = 0, + .online = false, + .assigned = 0.0f + }; + // slaves em 1..CONNECTOR_COUNT-1 + for (int i = 1; i < CONNECTOR_COUNT; i++) { + connectors[i] = (evse_connector_t){ + .id = (uint8_t)(i - 1), + .is_master = false, + .charging = false, + .hw_max_current = 0.0f, + .runtime_current = 0.0f, + .timestamp = 0, + .online = false, + .assigned = 0.0f + }; + } +} + +// --- Helpers --- static void input_filter_reset(input_filter_t *filter) { filter->value = 0.0f; } +// Callback de status de slave +static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id, void *data) +{ + const loadbalancer_slave_status_event_t *status = (const loadbalancer_slave_status_event_t *)data; + + if (status->slave_id >= MAX_SLAVES) { + ESP_LOGW(TAG, "Invalid slave_id %d", status->slave_id); + return; + } + + int idx = status->slave_id + 1; // slaves começam no índice 1 + 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].online = true; + + ESP_LOGI(TAG, + "Slave %d status: charging=%d hw_max_current=%.1fA runtime_current=%.2fA", + status->slave_id, status->charging, + status->hw_max_current, status->runtime_current); +} + + + + +static void on_evse_config_event(void* handler_arg, + esp_event_base_t base, + int32_t id, + void* event_data) +{ + const evse_config_event_data_t *evt = (const evse_config_event_data_t*) event_data; + + int idx = 0; // MASTER INDICE 0 + connectors[idx].charging = evt->charging; + connectors[idx].hw_max_current = evt->hw_max_current; + connectors[idx].runtime_current = evt->runtime_current; + connectors[idx].timestamp = esp_timer_get_time(); + connectors[idx].online = true; + + + ESP_LOGI(TAG, "EVSE config updated: charging=%d hw_max_current=%.1f runtime_current=%.1f", + evt->charging, evt->hw_max_current, evt->runtime_current); + +} + + + +// --- Handlers de eventos externos --- static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t base, int32_t id, @@ -46,39 +144,18 @@ static void loadbalancer_meter_event_handler(void *handler_arg, { if (id != METER_EVENT_DATA_READY || event_data == NULL) return; - const meter_event_data_t *evt = (const meter_event_data_t *)event_data; - - ESP_LOGI(TAG, "Received meter event from source: %s", evt->source); - ESP_LOGI(TAG, "IRMS: [%.2f, %.2f, %.2f] A", evt->irms[0], evt->irms[1], evt->irms[2]); - ESP_LOGI(TAG, "VRMS: [%.1f, %.1f, %.1f] V", evt->vrms[0], evt->vrms[1], evt->vrms[2]); - ESP_LOGI(TAG, "Power: [W1=%d, W2=%d, W3=%d]", evt->watt[0], evt->watt[1], evt->watt[2]); - ESP_LOGI(TAG, "Freq: %.2f Hz | PF: %.2f | Energy: %.3f kWh", - evt->frequency, evt->power_factor, evt->total_energy); - float max_irms = evt->irms[0]; for (int i = 1; i < 3; ++i) - { if (evt->irms[i] > max_irms) - { max_irms = evt->irms[i]; - } - } - - ESP_LOGI(TAG, "Max IRMS detected: %.2f A", max_irms); - - if (evt->source && strcmp(evt->source, "GRID") == 0) - { + if (evt->source && strcmp(evt->source, "GRID") == 0) { grid_current = input_filter_update(&grid_filter, max_irms); ESP_LOGI(TAG, "GRID IRMS (filtered): %.2f A", grid_current); - } - else if (evt->source && strcmp(evt->source, "EVSE") == 0) - { + } else if (evt->source && strcmp(evt->source, "EVSE") == 0) { evse_current = input_filter_update(&evse_filter, max_irms); ESP_LOGI(TAG, "EVSE IRMS (filtered): %.2f A", evse_current); - } - else - { + } else { ESP_LOGW(TAG, "Unknown meter event source: %s", evt->source); } } @@ -90,28 +167,32 @@ static void loadbalancer_evse_event_handler(void *handler_arg, { const evse_state_event_data_t *evt = (const evse_state_event_data_t *)event_data; - ESP_LOGI(TAG, "EVSE state changed: %d", evt->state); - switch (evt->state) { case EVSE_STATE_EVENT_IDLE: - ESP_LOGI(TAG, "EVSE is IDLE - vehicle disconnected"); - break; - case EVSE_STATE_EVENT_WAITING: - ESP_LOGI(TAG, "EVSE is WAITING - connected but not charging"); + ESP_LOGI(TAG, "Local EVSE is %s - vehicle %sconnected / not charging", + evt->state == EVSE_STATE_EVENT_IDLE ? "IDLE" : "WAITING", + evt->state == EVSE_STATE_EVENT_IDLE ? "dis" : ""); + connectors[0].charging = false; + connectors[0].online = true; // master está sempre online break; case EVSE_STATE_EVENT_CHARGING: - ESP_LOGI(TAG, "EVSE is CHARGING - resetting filters"); + ESP_LOGI(TAG, "Local EVSE is CHARGING - resetting filters"); grid_current = 0.0f; evse_current = 0.0f; input_filter_reset(&grid_filter); input_filter_reset(&evse_filter); + connectors[0].charging = true; + connectors[0].online = true; + connectors[0].timestamp = esp_timer_get_time(); break; case EVSE_STATE_EVENT_FAULT: - ESP_LOGW(TAG, "EVSE is in FAULT state - consider disabling load balancing"); + ESP_LOGW(TAG, "Local EVSE is in FAULT state - disabling load balancing temporarily"); + connectors[0].charging = false; + connectors[0].online = true; // EVSE está online mas com falha break; default: @@ -120,120 +201,66 @@ static void loadbalancer_evse_event_handler(void *handler_arg, } } + + +// --- Config persistência --- static esp_err_t loadbalancer_load_config() { nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); + esp_err_t err = nvs_open("loadbalancing", NVS_READWRITE, &handle); if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err)); return err; - } - bool needs_commit = false; uint8_t temp_u8; - - err = nvs_get_u8(handle, NVS_MAX_GRID_CURRENT, &temp_u8); + err = nvs_get_u8(handle, "max_grid_curr", &temp_u8); if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT) - { max_grid_current = temp_u8; - } - else - { + else { max_grid_current = MAX_GRID_CURRENT_LIMIT; - nvs_set_u8(handle, NVS_MAX_GRID_CURRENT, max_grid_current); - ESP_LOGW(TAG, "max_grid_current invalid or missing, set to default: %d", max_grid_current); + nvs_set_u8(handle, "max_grid_curr", max_grid_current); needs_commit = true; } - - err = nvs_get_u8(handle, NVS_LOADBALANCER_ENABLED, &temp_u8); + err = nvs_get_u8(handle, "enabled", &temp_u8); if (err == ESP_OK && temp_u8 <= 1) - { loadbalancer_enabled = (temp_u8 != 0); - } - else - { + else { loadbalancer_enabled = false; - nvs_set_u8(handle, NVS_LOADBALANCER_ENABLED, 0); - ESP_LOGW(TAG, "loadbalancer_enabled invalid or missing, set to false"); + nvs_set_u8(handle, "enabled", 0); needs_commit = true; } - if (needs_commit) - { nvs_commit(handle); - } - nvs_close(handle); return ESP_OK; } +// --- API --- void loadbalancer_set_enabled(bool enabled) { - ESP_LOGI(TAG, "Setting load balancing to %d", enabled); nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err)); - return; - } - - err = nvs_set_u8(handle, NVS_LOADBALANCER_ENABLED, enabled ? 1 : 0); - if (err == ESP_OK) - { + if (nvs_open("loadbalancing", NVS_READWRITE, &handle) == ESP_OK) { + nvs_set_u8(handle, "enabled", enabled ? 1 : 0); nvs_commit(handle); - loadbalancer_enabled = enabled; - ESP_LOGI(TAG, "Load balancing state saved"); - - loadbalancer_state_event_t evt = { - .enabled = enabled, - .timestamp_us = esp_timer_get_time()}; - - esp_event_post(LOADBALANCER_EVENTS, - LOADBALANCER_EVENT_STATE_CHANGED, - &evt, - sizeof(evt), - portMAX_DELAY); + nvs_close(handle); } - else - { - ESP_LOGE(TAG, "Failed to save loadbalancer_enabled"); - } - - nvs_close(handle); + loadbalancer_enabled = enabled; + loadbalancer_state_event_t evt = { .enabled = enabled, .timestamp_us = esp_timer_get_time() }; + 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) { if (value < MIN_GRID_CURRENT_LIMIT || value > MAX_GRID_CURRENT_LIMIT) - { - ESP_LOGE(TAG, "Invalid grid current limit: %d", value); return ESP_ERR_INVALID_ARG; - } - nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err)); - return err; - } - - err = nvs_set_u8(handle, NVS_MAX_GRID_CURRENT, value); - if (err == ESP_OK) - { - nvs_commit(handle); - max_grid_current = value; - ESP_LOGI(TAG, "max_grid_current set to: %d", value); - } - else - { - ESP_LOGE(TAG, "Failed to save max_grid_current to NVS"); - } - + if (nvs_open("loadbalancing", NVS_READWRITE, &handle) != ESP_OK) + return ESP_FAIL; + nvs_set_u8(handle, "max_grid_curr", value); + nvs_commit(handle); nvs_close(handle); - return err; + max_grid_current = value; + return ESP_OK; } uint8_t load_balancing_get_max_grid_current(void) @@ -246,79 +273,168 @@ bool loadbalancer_is_enabled(void) return loadbalancer_enabled; } +// --- Task principal --- void loadbalancer_task(void *param) { - while (true) - { - if (!loadbalancer_enabled) - { + while (true) { + if (!loadbalancer_is_enabled()) { vTaskDelay(pdMS_TO_TICKS(5000)); continue; } - float available = max_grid_current - grid_current; + int idxs[CONNECTOR_COUNT]; + int active_cnt = 0; + int64_t now = esp_timer_get_time(); - if (available < 0.0f) - { - available = 0.0f; + // --- Atualiza estado online e conta ativos --- + for (int i = 0; i < CONNECTOR_COUNT; i++) { + + // --- Master nunca pode ficar offline --- + if (connectors[i].is_master) { + connectors[i].online = true; + + ESP_LOGI(TAG, "Connector[%d] ONLINE (MASTER, charging=%d, hw_max_current=%.1f)", + i, connectors[i].charging, connectors[i].hw_max_current); + + if (connectors[i].charging) { + idxs[active_cnt++] = i; + ESP_LOGI(TAG, "Connector[%d] is ACTIVE (charging)", i); + } + continue; + } + + // --- Ignora conectores já marcados como offline --- + if (!connectors[i].online) { + continue; + } + + // --- Timeout de heartbeat para escravos --- + if ((now - connectors[i].timestamp) >= METRICS_TIMEOUT_US) { + connectors[i].online = false; + ESP_LOGW(TAG, "Connector[%d] marked OFFLINE (charging=%d, timestamp_diff=%lld us)", + i, connectors[i].charging, (long long)(now - connectors[i].timestamp)); + continue; + } + + ESP_LOGI(TAG, "Connector[%d] ONLINE (charging=%d, hw_max_current=%.1f, timestamp_diff=%lld us)", + i, connectors[i].charging, connectors[i].hw_max_current, + (long long)(now - connectors[i].timestamp)); + + if (connectors[i].charging) { + idxs[active_cnt++] = i; + ESP_LOGI(TAG, "Connector[%d] is ACTIVE (charging)", i); + } } - else if (available < MIN_CHARGING_CURRENT_LIMIT) - { + + ESP_LOGI(TAG, "Active connectors: %d", active_cnt); + + // --- Calcula corrente disponível --- + float available = max_grid_current - grid_current; + if (available < MIN_CHARGING_CURRENT_LIMIT) { available = MIN_CHARGING_CURRENT_LIMIT; - } - else if (available > max_grid_current) - { + } else if (available > max_grid_current) { available = max_grid_current; } + ESP_LOGI(TAG, "LB Calc: available=%.1fA, active_connectors=%d", available, active_cnt); - ESP_LOGD(TAG, "Calculated available EVSE current: %.1f A", available); + // --- Ordena conectores por hw_max_current (bubble sort simples) --- + for (int a = 0; a < active_cnt - 1; a++) { + for (int b = 0; b < active_cnt - 1 - a; b++) { + if (connectors[idxs[b]].hw_max_current > connectors[idxs[b + 1]].hw_max_current) { + int tmp = idxs[b]; + idxs[b] = idxs[b + 1]; + idxs[b + 1] = tmp; + } + } + } - loadbalancer_charging_limit_event_t evt = { - .limit = available, - .timestamp_us = esp_timer_get_time()}; + // --- Distribui corrente (water-filling) --- + float remaining = available; + int remaining_cnt = active_cnt; + for (int k = 0; k < active_cnt; k++) { + int i = idxs[k]; + float share = remaining / remaining_cnt; + if (share >= connectors[i].hw_max_current) { + connectors[i].assigned = connectors[i].hw_max_current; + remaining -= connectors[i].assigned; + remaining_cnt--; + } else { + for (int m = k; m < active_cnt; m++) { + connectors[idxs[m]].assigned = share; + } + break; + } + } - esp_event_post(LOADBALANCER_EVENTS, - LOADBALANCER_EVENT_CHARGING_LIMIT_CHANGED, - &evt, - sizeof(evt), - portMAX_DELAY); + // --- Aplica piso mínimo --- + for (int k = 0; k < active_cnt; k++) { + int i = idxs[k]; + if (connectors[i].assigned < MIN_CHARGING_CURRENT_LIMIT) { + connectors[i].assigned = MIN_CHARGING_CURRENT_LIMIT; + } + } + + // --- Publica limites de corrente --- + for (int k = 0; k < active_cnt; k++) { + int i = idxs[k]; + uint16_t max_cur = (uint16_t)MIN(connectors[i].assigned, MAX_CHARGING_CURRENT_LIMIT); + uint16_t current_rounded = (uint16_t)roundf(connectors[i].runtime_current); + + if (current_rounded == max_cur) { + continue; // sem alteração + } + + if (connectors[i].is_master) { + loadbalancer_master_limit_event_t master_evt = { + .slave_id = connectors[i].id, + .max_current = max_cur, + .timestamp_us = now + }; + esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, + &master_evt, sizeof(master_evt), portMAX_DELAY); + ESP_LOGI(TAG, "Master limit changed -> %.1f A (runtime=%.1f A)", + (float)max_cur, connectors[i].runtime_current); + } else { + loadbalancer_slave_limit_event_t slave_evt = { + .slave_id = connectors[i].id, + .max_current = max_cur, + .timestamp_us = now + }; + esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + &slave_evt, sizeof(slave_evt), portMAX_DELAY); + ESP_LOGI(TAG, "Slave %d limit changed -> %.1f A (runtime=%.1f A)", + connectors[i].id, (float)max_cur, connectors[i].runtime_current); + } + } vTaskDelay(pdMS_TO_TICKS(5000)); } } +// --- Init --- void loadbalancer_init(void) { - ESP_LOGI(TAG, "Initializing load balancer"); - if (loadbalancer_load_config() != ESP_OK) - { - ESP_LOGW(TAG, "Failed to load/init config. Using in-memory defaults."); - } + ESP_LOGW(TAG, "Failed to load/init config. Using defaults."); + init_connectors(); input_filter_init(&grid_filter, 0.3f); input_filter_init(&evse_filter, 0.3f); if (xTaskCreate(loadbalancer_task, "loadbalancer", 4096, NULL, 4, NULL) != pdPASS) - { ESP_LOGE(TAG, "Failed to create loadbalancer task"); - } - loadbalancer_state_event_t evt = { - .enabled = loadbalancer_enabled, - .timestamp_us = esp_timer_get_time()}; - - esp_event_post(LOADBALANCER_EVENTS, - LOADBALANCER_EVENT_INIT, - &evt, - sizeof(evt), - portMAX_DELAY); + loadbalancer_state_event_t evt = {.enabled = loadbalancer_enabled, .timestamp_us = esp_timer_get_time()}; + esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); ESP_ERROR_CHECK(esp_event_handler_register(METER_EVENT, METER_EVENT_DATA_READY, &loadbalancer_meter_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, + &loadbalancer_evse_event_handler, NULL)); - ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, - EVSE_EVENT_STATE_CHANGED, - &loadbalancer_evse_event_handler, - NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS,EVSE_EVENT_CONFIG_UPDATED, + &on_evse_config_event, NULL)); + + ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS,LOADBALANCER_EVENT_SLAVE_STATUS, + &on_slave_status, NULL)); } diff --git a/components/peripherals/src/led.c b/components/peripherals/src/led.c index 4009fba..df7c51a 100755 --- a/components/peripherals/src/led.c +++ b/components/peripherals/src/led.c @@ -5,10 +5,9 @@ #include "driver/gpio.h" #include "led.h" #include "board_config.h" -#include "evse_error.h" -#include "evse_api.h" +#include "evse_events.h" +#include "evse_state.h" -#define LED_UPDATE_INTERVAL_MS 100 #define BLOCK_TIME pdMS_TO_TICKS(10) static const char *TAG = "led"; @@ -24,32 +23,60 @@ typedef struct { } led_t; static led_t leds[LED_ID_MAX] = {0}; -static TimerHandle_t led_update_timer = NULL; -static evse_state_t led_state = -1; // ---------------------------- // Funções Internas // ---------------------------- -static void led_update_timer_callback(TimerHandle_t xTimer); -static void led_update(void); -static void led_apply_by_state(evse_state_t state); - -static inline void led_gpio_write(gpio_num_t gpio, bool level) { - if (gpio != GPIO_NUM_NC) - gpio_set_level(gpio, level); -} - static void led_timer_callback(TimerHandle_t xTimer) { led_t *led = (led_t *)pvTimerGetTimerID(xTimer); led->on = !led->on; - led_gpio_write(led->gpio, led->on); + gpio_set_level(led->gpio, led->on); uint32_t next_time = led->on ? led->ontime : led->offtime; xTimerChangePeriod(led->timer, pdMS_TO_TICKS(next_time), BLOCK_TIME); } +// ---------------------------- +// Event Handler: EVSE State +// ---------------------------- +static void evse_led_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 = (const evse_state_event_data_t *)data; + + // Log do evento recebido + ESP_LOGI(TAG, "EVSE State Changed: state=%d", evt->state); + + led_apply_pattern(LED_ID_STOP, LED_PATTERN_OFF); + led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_OFF); + led_apply_pattern(LED_ID_ERROR, LED_PATTERN_OFF); + + switch (evt->state) { + case EVSE_STATE_EVENT_IDLE: + ESP_LOGI(TAG, "EVSE_STATE_EVENT_IDLE"); + led_apply_pattern(LED_ID_STOP, LED_PATTERN_ON); + break; + case EVSE_STATE_EVENT_WAITING: + ESP_LOGI(TAG, "EVSE_STATE_EVENT_WAITING"); + led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_ON); + break; + case EVSE_STATE_EVENT_CHARGING: + ESP_LOGI(TAG, "EVSE_STATE_EVENT_CHARGING"); + led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_CHARGING_EFFECT); + break; + case EVSE_STATE_EVENT_FAULT: + ESP_LOGI(TAG, "EVSE_STATE_EVENT_FAULT"); + led_apply_pattern(LED_ID_ERROR, LED_PATTERN_BLINK_FAST); + break; + default: + ESP_LOGW(TAG, "Unknown state: %d", evt->state); + break; + } +} + + // ---------------------------- // Inicialização // ---------------------------- @@ -87,17 +114,20 @@ void led_init(void) ESP_ERROR_CHECK(gpio_config(&io_conf)); } - if (!led_update_timer) { - led_update_timer = xTimerCreate("led_update_timer", - pdMS_TO_TICKS(LED_UPDATE_INTERVAL_MS), - pdTRUE, NULL, - led_update_timer_callback); - if (led_update_timer) { - xTimerStart(led_update_timer, BLOCK_TIME); - } else { - ESP_LOGE(TAG, "Failed to create LED update timer"); - } - } + // Registra handler de evento EVSE + ESP_ERROR_CHECK(esp_event_handler_register( + EVSE_EVENTS, + EVSE_EVENT_STATE_CHANGED, + evse_led_event_handler, + NULL)); + + ESP_LOGI(TAG, "LED system initialized"); + + // Aplica o estado atual do EVSE aos LEDs + evse_state_event_data_t evt = { + .state = EVSE_STATE_EVENT_IDLE + }; + evse_led_event_handler(NULL, EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, &evt); } // ---------------------------- @@ -111,7 +141,6 @@ void led_set_state(led_id_t led_id, uint16_t ontime, uint16_t offtime) led_t *led = &leds[led_id]; if (led->gpio == GPIO_NUM_NC) return; - // Evita reconfiguração idêntica if (led->ontime == ontime && led->offtime == offtime) return; @@ -124,13 +153,13 @@ void led_set_state(led_id_t led_id, uint16_t ontime, uint16_t offtime) if (ontime == 0) { led->on = false; - led_gpio_write(led->gpio, 0); + gpio_set_level(led->gpio, 0); } else if (offtime == 0) { led->on = true; - led_gpio_write(led->gpio, 1); + gpio_set_level(led->gpio, 1); } else { led->on = true; - led_gpio_write(led->gpio, 1); + gpio_set_level(led->gpio, 1); if (!led->timer) { led->timer = xTimerCreate("led_timer", pdMS_TO_TICKS(ontime), @@ -180,66 +209,3 @@ void led_apply_pattern(led_id_t id, led_pattern_t pattern) break; } } - -// ---------------------------- -// Controle por Estado -// ---------------------------- - -static void led_apply_by_state(evse_state_t state) -{ - // Reset todos - led_apply_pattern(LED_ID_STOP, LED_PATTERN_OFF); - led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_OFF); - led_apply_pattern(LED_ID_ERROR, LED_PATTERN_OFF); - - switch (state) { - case EVSE_STATE_A: - led_apply_pattern(LED_ID_STOP, LED_PATTERN_ON); - break; - case EVSE_STATE_B1: - case EVSE_STATE_B2: - case EVSE_STATE_C1: - led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_ON); - break; - case EVSE_STATE_C2: - led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_CHARGING_EFFECT); - break; - case EVSE_STATE_D1: - case EVSE_STATE_D2: - led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_BLINK_FAST); - break; - case EVSE_STATE_E: - case EVSE_STATE_F: - led_apply_pattern(LED_ID_ERROR, LED_PATTERN_BLINK_FAST); - break; - default: - break; - } -} - -// ---------------------------- -// Timer Update -// ---------------------------- - -static void led_update(void) -{ - if (evse_error_is_active()) { - led_apply_pattern(LED_ID_ERROR, LED_PATTERN_BLINK_FAST); - led_apply_pattern(LED_ID_STOP, LED_PATTERN_OFF); - led_apply_pattern(LED_ID_CHARGING, LED_PATTERN_OFF); - return; - } - - evse_state_t current = evse_get_state(); - - if (current != led_state) { - led_state = current; - led_apply_by_state(current); - } -} - -static void led_update_timer_callback(TimerHandle_t xTimer) -{ - (void)xTimer; - led_update(); -} diff --git a/components/protocols/webfolder/assets/index-3W2ZKmTa.js b/components/protocols/webfolder/assets/index-3W2ZKmTa.js deleted file mode 100755 index 2101af7..0000000 --- a/components/protocols/webfolder/assets/index-3W2ZKmTa.js +++ /dev/null @@ -1,51 +0,0 @@ -(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))r(d);new MutationObserver(d=>{for(const v of d)if(v.type==="childList")for(const x of v.addedNodes)x.tagName==="LINK"&&x.rel==="modulepreload"&&r(x)}).observe(document,{childList:!0,subtree:!0});function s(d){const v={};return d.integrity&&(v.integrity=d.integrity),d.referrerPolicy&&(v.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?v.credentials="include":d.crossOrigin==="anonymous"?v.credentials="omit":v.credentials="same-origin",v}function r(d){if(d.ep)return;d.ep=!0;const v=s(d);fetch(d.href,v)}})();function kd(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Tf={exports:{}},Du={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * 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 Hy(){if(jd)return Du;jd=1;var i=Symbol.for("react.transitional.element"),o=Symbol.for("react.fragment");function s(r,d,v){var x=null;if(v!==void 0&&(x=""+v),d.key!==void 0&&(x=""+d.key),"key"in d){v={};for(var z in d)z!=="key"&&(v[z]=d[z])}else v=d;return d=v.ref,{$$typeof:i,type:r,key:x,ref:d!==void 0?d:null,props:v}}return Du.Fragment=o,Du.jsx=s,Du.jsxs=s,Du}var Cd;function By(){return Cd||(Cd=1,Tf.exports=Hy()),Tf.exports}var y=By(),Af={exports:{}},le={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ud;function qy(){if(Ud)return le;Ud=1;var i=Symbol.for("react.transitional.element"),o=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),d=Symbol.for("react.profiler"),v=Symbol.for("react.consumer"),x=Symbol.for("react.context"),z=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),N=Symbol.for("react.lazy"),B=Symbol.iterator;function M(g){return g===null||typeof g!="object"?null:(g=B&&g[B]||g["@@iterator"],typeof g=="function"?g:null)}var L={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},U=Object.assign,q={};function Q(g,H,Z){this.props=g,this.context=H,this.refs=q,this.updater=Z||L}Q.prototype.isReactComponent={},Q.prototype.setState=function(g,H){if(typeof g!="object"&&typeof g!="function"&&g!=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,g,H,"setState")},Q.prototype.forceUpdate=function(g){this.updater.enqueueForceUpdate(this,g,"forceUpdate")};function w(){}w.prototype=Q.prototype;function V(g,H,Z){this.props=g,this.context=H,this.refs=q,this.updater=Z||L}var G=V.prototype=new w;G.constructor=V,U(G,Q.prototype),G.isPureReactComponent=!0;var ee=Array.isArray,J={H:null,A:null,T:null,S:null,V:null},he=Object.prototype.hasOwnProperty;function Te(g,H,Z,Y,$,fe){return Z=fe.ref,{$$typeof:i,type:g,key:H,ref:Z!==void 0?Z:null,props:fe}}function Ne(g,H){return Te(g.type,H,void 0,void 0,void 0,g.props)}function Se(g){return typeof g=="object"&&g!==null&&g.$$typeof===i}function Je(g){var H={"=":"=0",":":"=2"};return"$"+g.replace(/[=:]/g,function(Z){return H[Z]})}var st=/\/+/g;function Xe(g,H){return typeof g=="object"&&g!==null&&g.key!=null?Je(""+g.key):H.toString(36)}function El(){}function Tl(g){switch(g.status){case"fulfilled":return g.value;case"rejected":throw g.reason;default:switch(typeof g.status=="string"?g.then(El,El):(g.status="pending",g.then(function(H){g.status==="pending"&&(g.status="fulfilled",g.value=H)},function(H){g.status==="pending"&&(g.status="rejected",g.reason=H)})),g.status){case"fulfilled":return g.value;case"rejected":throw g.reason}}throw g}function Qe(g,H,Z,Y,$){var fe=typeof g;(fe==="undefined"||fe==="boolean")&&(g=null);var te=!1;if(g===null)te=!0;else switch(fe){case"bigint":case"string":case"number":te=!0;break;case"object":switch(g.$$typeof){case i:case o:te=!0;break;case N:return te=g._init,Qe(te(g._payload),H,Z,Y,$)}}if(te)return $=$(g),te=Y===""?"."+Xe(g,0):Y,ee($)?(Z="",te!=null&&(Z=te.replace(st,"$&/")+"/"),Qe($,H,Z,"",function(Wt){return Wt})):$!=null&&(Se($)&&($=Ne($,Z+($.key==null||g&&g.key===$.key?"":(""+$.key).replace(st,"$&/")+"/")+te)),H.push($)),1;te=0;var et=Y===""?".":Y+":";if(ee(g))for(var xe=0;xe>>1,g=_[ge];if(0>>1;ged(Y,P))$d(fe,Y)?(_[ge]=fe,_[$]=P,ge=$):(_[ge]=Y,_[Z]=P,ge=Z);else if($d(fe,P))_[ge]=fe,_[$]=P,ge=$;else break e}}return X}function d(_,X){var P=_.sortIndex-X.sortIndex;return P!==0?P:_.id-X.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var v=performance;i.unstable_now=function(){return v.now()}}else{var x=Date,z=x.now();i.unstable_now=function(){return x.now()-z}}var p=[],h=[],N=1,B=null,M=3,L=!1,U=!1,q=!1,Q=!1,w=typeof setTimeout=="function"?setTimeout:null,V=typeof clearTimeout=="function"?clearTimeout:null,G=typeof setImmediate<"u"?setImmediate:null;function ee(_){for(var X=s(h);X!==null;){if(X.callback===null)r(h);else if(X.startTime<=_)r(h),X.sortIndex=X.expirationTime,o(p,X);else break;X=s(h)}}function J(_){if(q=!1,ee(_),!U)if(s(p)!==null)U=!0,he||(he=!0,Xe());else{var X=s(h);X!==null&&Qe(J,X.startTime-_)}}var he=!1,Te=-1,Ne=5,Se=-1;function Je(){return Q?!0:!(i.unstable_now()-Se_&&Je());){var ge=B.callback;if(typeof ge=="function"){B.callback=null,M=B.priorityLevel;var g=ge(B.expirationTime<=_);if(_=i.unstable_now(),typeof g=="function"){B.callback=g,ee(_),X=!0;break t}B===s(p)&&r(p),ee(_)}else r(p);B=s(p)}if(B!==null)X=!0;else{var H=s(h);H!==null&&Qe(J,H.startTime-_),X=!1}}break e}finally{B=null,M=P,L=!1}X=void 0}}finally{X?Xe():he=!1}}}var Xe;if(typeof G=="function")Xe=function(){G(st)};else if(typeof MessageChannel<"u"){var El=new MessageChannel,Tl=El.port2;El.port1.onmessage=st,Xe=function(){Tl.postMessage(null)}}else Xe=function(){w(st,0)};function Qe(_,X){Te=w(function(){_(i.unstable_now())},X)}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(_){_.callback=null},i.unstable_forceFrameRate=function(_){0>_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):Ne=0<_?Math.floor(1e3/_):5},i.unstable_getCurrentPriorityLevel=function(){return M},i.unstable_next=function(_){switch(M){case 1:case 2:case 3:var X=3;break;default:X=M}var P=M;M=X;try{return _()}finally{M=P}},i.unstable_requestPaint=function(){Q=!0},i.unstable_runWithPriority=function(_,X){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var P=M;M=_;try{return X()}finally{M=P}},i.unstable_scheduleCallback=function(_,X,P){var ge=i.unstable_now();switch(typeof P=="object"&&P!==null?(P=P.delay,P=typeof P=="number"&&0ge?(_.sortIndex=P,o(h,_),s(p)===null&&_===s(h)&&(q?(V(Te),Te=-1):q=!0,Qe(J,P-ge))):(_.sortIndex=g,o(p,_),U||L||(U=!0,he||(he=!0,Xe()))),_},i.unstable_shouldYield=Je,i.unstable_wrapCallback=function(_){var X=M;return function(){var P=M;M=X;try{return _.apply(this,arguments)}finally{M=P}}}}(Mf)),Mf}var qd;function Yy(){return qd||(qd=1,Of.exports=Ly()),Of.exports}var Nf={exports:{}},Ke={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var wd;function Gy(){if(wd)return Ke;wd=1;var i=Cf();function o(p){var h="https://react.dev/errors/"+p;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(o){console.error(o)}}return i(),Nf.exports=Gy(),Nf.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Yd;function Qy(){if(Yd)return _u;Yd=1;var i=Yy(),o=Cf(),s=Xy();function r(e){var t="https://react.dev/errors/"+e;if(1g||(e.current=ge[g],ge[g]=null,g--)}function Y(e,t){g++,ge[g]=e.current,e.current=t}var $=H(null),fe=H(null),te=H(null),et=H(null);function xe(e,t){switch(Y(te,t),Y(fe,e),Y($,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?id(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=id(t),e=cd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}Z($),Y($,e)}function Wt(){Z($),Z(fe),Z(te)}function ci(e){e.memoizedState!==null&&Y(et,e);var t=$.current,l=cd(t,e.type);t!==l&&(Y(fe,e),Y($,l))}function Lu(e){fe.current===e&&(Z($),Z(fe)),et.current===e&&(Z(et),Ru._currentValue=P)}var fi=Object.prototype.hasOwnProperty,ri=i.unstable_scheduleCallback,si=i.unstable_cancelCallback,hh=i.unstable_shouldYield,mh=i.unstable_requestPaint,Tt=i.unstable_now,yh=i.unstable_getCurrentPriorityLevel,Lf=i.unstable_ImmediatePriority,Yf=i.unstable_UserBlockingPriority,Yu=i.unstable_NormalPriority,vh=i.unstable_LowPriority,Gf=i.unstable_IdlePriority,gh=i.log,bh=i.unstable_setDisableYieldValue,Ca=null,tt=null;function Ft(e){if(typeof gh=="function"&&bh(e),tt&&typeof tt.setStrictMode=="function")try{tt.setStrictMode(Ca,e)}catch{}}var lt=Math.clz32?Math.clz32:xh,ph=Math.log,Sh=Math.LN2;function xh(e){return e>>>=0,e===0?32:31-(ph(e)/Sh|0)|0}var Gu=256,Xu=4194304;function Al(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 Qu(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var u=0,n=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var f=a&134217727;return f!==0?(a=f&~n,a!==0?u=Al(a):(c&=f,c!==0?u=Al(c):l||(l=f&~e,l!==0&&(u=Al(l))))):(f=a&~n,f!==0?u=Al(f):c!==0?u=Al(c):l||(l=a&~e,l!==0&&(u=Al(l)))),u===0?0:t!==0&&t!==u&&(t&n)===0&&(n=u&-u,l=t&-t,n>=l||n===32&&(l&4194048)!==0)?t:u}function Ua(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Eh(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 Xf(){var e=Gu;return Gu<<=1,(Gu&4194048)===0&&(Gu=256),e}function Qf(){var e=Xu;return Xu<<=1,(Xu&62914560)===0&&(Xu=4194304),e}function oi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ha(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Th(e,t,l,a,u,n){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,m=e.expirationTimes,T=e.hiddenUpdates;for(l=c&~l;0)":-1u||m[a]!==T[u]){var D=` -`+m[a].replace(" at new "," at ");return e.displayName&&D.includes("")&&(D=D.replace("",e.displayName)),D}while(1<=a&&0<=u);break}}}finally{gi=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?$l(l):""}function zh(e){switch(e.tag){case 26:case 27:case 5:return $l(e.type);case 16:return $l("Lazy");case 13:return $l("Suspense");case 19:return $l("SuspenseList");case 0:case 15:return bi(e.type,!1);case 11:return bi(e.type.render,!1);case 1:return bi(e.type,!0);case 31:return $l("Activity");default:return""}}function If(e){try{var t="";do t+=zh(e),e=e.return;while(e);return t}catch(l){return` -Error generating stack: `+l.message+` -`+l.stack}}function ot(e){switch(typeof e){case"bigint":case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function er(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Dh(e){var t=er(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 u=l.get,n=l.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return u.call(this)},set:function(c){a=""+c,n.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 Ku(e){e._valueTracker||(e._valueTracker=Dh(e))}function tr(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var l=t.getValue(),a="";return e&&(a=er(e)?e.checked?"true":"false":e.value),e=a,e!==l?(t.setValue(e),!0):!1}function Ju(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 _h=/[\n"\\]/g;function dt(e){return e.replace(_h,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function pi(e,t,l,a,u,n,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=""+ot(t)):e.value!==""+ot(t)&&(e.value=""+ot(t)):c!=="submit"&&c!=="reset"||e.removeAttribute("value"),t!=null?Si(e,c,ot(t)):l!=null?Si(e,c,ot(l)):a!=null&&e.removeAttribute("value"),u==null&&n!=null&&(e.defaultChecked=!!n),u!=null&&(e.checked=u&&typeof u!="function"&&typeof u!="symbol"),f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?e.name=""+ot(f):e.removeAttribute("name")}function lr(e,t,l,a,u,n,c,f){if(n!=null&&typeof n!="function"&&typeof n!="symbol"&&typeof n!="boolean"&&(e.type=n),t!=null||l!=null){if(!(n!=="submit"&&n!=="reset"||t!=null))return;l=l!=null?""+ot(l):"",t=t!=null?""+ot(t):l,f||t===e.value||(e.value=t),e.defaultValue=t}a=a??u,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 Si(e,t,l){t==="number"&&Ju(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function Wl(e,t,l,a){if(e=e.options,t){t={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ri=!1;if(Ct)try{var La={};Object.defineProperty(La,"passive",{get:function(){Ri=!0}}),window.addEventListener("test",La,La),window.removeEventListener("test",La,La)}catch{Ri=!1}var It=null,Oi=null,$u=null;function rr(){if($u)return $u;var e,t=Oi,l=t.length,a,u="value"in It?It.value:It.textContent,n=u.length;for(e=0;e=Xa),yr=" ",vr=!1;function gr(e,t){switch(e){case"keyup":return nm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function br(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var ea=!1;function cm(e,t){switch(e){case"compositionend":return br(t);case"keypress":return t.which!==32?null:(vr=!0,yr);case"textInput":return e=t.data,e===yr&&vr?null:e;default:return null}}function fm(e,t){if(ea)return e==="compositionend"||!_i&&gr(e,t)?(e=rr(),$u=Oi=It=null,ea=!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=Or(l)}}function Nr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Nr(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function zr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ju(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=Ju(e.document)}return t}function Ui(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 vm=Ct&&"documentMode"in document&&11>=document.documentMode,ta=null,Hi=null,Ka=null,Bi=!1;function Dr(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Bi||ta==null||ta!==Ju(a)||(a=ta,"selectionStart"in a&&Ui(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}),Ka&&Va(Ka,a)||(Ka=a,a=Yn(Hi,"onSelect"),0>=c,u-=c,Ht=1<<32-lt(t)+u|l<n?n:8;var c=_.T,f={};_.T=f,xc(e,!1,t,l);try{var m=u(),T=_.S;if(T!==null&&T(f,m),m!==null&&typeof m=="object"&&typeof m.then=="function"){var D=Rm(m,a);cu(e,t,D,ft(e))}else cu(e,t,a,ft(e))}catch(C){cu(e,t,{then:function(){},status:"rejected",reason:C},ft())}finally{X.p=n,_.T=c}}function Dm(){}function pc(e,t,l,a){if(e.tag!==5)throw Error(r(476));var u=_s(e).queue;Ds(e,u,t,P,l===null?Dm:function(){return js(e),l(a)})}function _s(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:P,baseState:P,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Lt,lastRenderedState:P},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Lt,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function js(e){var t=_s(e).next.queue;cu(e,t,{},ft())}function Sc(){return Ve(Ru)}function Cs(){return je().memoizedState}function Us(){return je().memoizedState}function _m(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=ft();e=ll(l);var a=al(t,e,l);a!==null&&(rt(a,t,l),tu(a,t,l)),t={cache:Wi()},e.payload=t;return}t=t.return}}function jm(e,t,l){var a=ft();l={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null},Sn(e)?Bs(t,l):(l=Yi(e,t,l,a),l!==null&&(rt(l,e,a),qs(l,t,a)))}function Hs(e,t,l){var a=ft();cu(e,t,l,a)}function cu(e,t,l,a){var u={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null};if(Sn(e))Bs(t,u);else{var n=e.alternate;if(e.lanes===0&&(n===null||n.lanes===0)&&(n=t.lastRenderedReducer,n!==null))try{var c=t.lastRenderedState,f=n(c,l);if(u.hasEagerState=!0,u.eagerState=f,at(f,c))return ln(e,t,u,0),pe===null&&tn(),!1}catch{}finally{}if(l=Yi(e,t,u,a),l!==null)return rt(l,e,a),qs(l,t,a),!0}return!1}function xc(e,t,l,a){if(a={lane:2,revertLane:Ic(),action:a,hasEagerState:!1,eagerState:null,next:null},Sn(e)){if(t)throw Error(r(479))}else t=Yi(e,l,a,2),t!==null&&rt(t,e,2)}function Sn(e){var t=e.alternate;return e===ae||t!==null&&t===ae}function Bs(e,t){oa=mn=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function qs(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,Vf(e,l)}}var xn={readContext:Ve,use:vn,useCallback:ze,useContext:ze,useEffect:ze,useImperativeHandle:ze,useLayoutEffect:ze,useInsertionEffect:ze,useMemo:ze,useReducer:ze,useRef:ze,useState:ze,useDebugValue:ze,useDeferredValue:ze,useTransition:ze,useSyncExternalStore:ze,useId:ze,useHostTransitionStatus:ze,useFormState:ze,useActionState:ze,useOptimistic:ze,useMemoCache:ze,useCacheRefresh:ze},ws={readContext:Ve,use:vn,useCallback:function(e,t){return Fe().memoizedState=[e,t===void 0?null:t],e},useContext:Ve,useEffect:xs,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,pn(4194308,4,Rs.bind(null,t,e),l)},useLayoutEffect:function(e,t){return pn(4194308,4,e,t)},useInsertionEffect:function(e,t){pn(4,2,e,t)},useMemo:function(e,t){var l=Fe();t=t===void 0?null:t;var a=e();if(ql){Ft(!0);try{e()}finally{Ft(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=Fe();if(l!==void 0){var u=l(t);if(ql){Ft(!0);try{l(t)}finally{Ft(!1)}}}else u=t;return a.memoizedState=a.baseState=u,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:u},a.queue=e,e=e.dispatch=jm.bind(null,ae,e),[a.memoizedState,e]},useRef:function(e){var t=Fe();return e={current:e},t.memoizedState=e},useState:function(e){e=yc(e);var t=e.queue,l=Hs.bind(null,ae,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:gc,useDeferredValue:function(e,t){var l=Fe();return bc(l,e,t)},useTransition:function(){var e=yc(!1);return e=Ds.bind(null,ae,e.queue,!0,!1),Fe().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ae,u=Fe();if(se){if(l===void 0)throw Error(r(407));l=l()}else{if(l=t(),pe===null)throw Error(r(349));(ce&124)!==0||ns(a,t,l)}u.memoizedState=l;var n={value:l,getSnapshot:t};return u.queue=n,xs(cs.bind(null,a,n,e),[e]),a.flags|=2048,ha(9,bn(),is.bind(null,a,n,l,t),null),l},useId:function(){var e=Fe(),t=pe.identifierPrefix;if(se){var l=Bt,a=Ht;l=(a&~(1<<32-lt(a)-1)).toString(32)+l,t="«"+t+"R"+l,l=yn++,0F?(we=k,k=null):we=k.sibling;var re=A(S,k,E[F],j);if(re===null){k===null&&(k=we);break}e&&k&&re.alternate===null&&t(S,k),b=n(re,b,F),ue===null?K=re:ue.sibling=re,ue=re,k=we}if(F===E.length)return l(S,k),se&&_l(S,F),K;if(k===null){for(;FF?(we=k,k=null):we=k.sibling;var Sl=A(S,k,re.value,j);if(Sl===null){k===null&&(k=we);break}e&&k&&Sl.alternate===null&&t(S,k),b=n(Sl,b,F),ue===null?K=Sl:ue.sibling=Sl,ue=Sl,k=we}if(re.done)return l(S,k),se&&_l(S,F),K;if(k===null){for(;!re.done;F++,re=E.next())re=C(S,re.value,j),re!==null&&(b=n(re,b,F),ue===null?K=re:ue.sibling=re,ue=re);return se&&_l(S,F),K}for(k=a(k);!re.done;F++,re=E.next())re=R(k,S,F,re.value,j),re!==null&&(e&&re.alternate!==null&&k.delete(re.key===null?F:re.key),b=n(re,b,F),ue===null?K=re:ue.sibling=re,ue=re);return e&&k.forEach(function(Uy){return t(S,Uy)}),se&&_l(S,F),K}function ve(S,b,E,j){if(typeof E=="object"&&E!==null&&E.type===U&&E.key===null&&(E=E.props.children),typeof E=="object"&&E!==null){switch(E.$$typeof){case M:e:{for(var K=E.key;b!==null;){if(b.key===K){if(K=E.type,K===U){if(b.tag===7){l(S,b.sibling),j=u(b,E.props.children),j.return=S,S=j;break e}}else if(b.elementType===K||typeof K=="object"&&K!==null&&K.$$typeof===Ne&&Ys(K)===b.type){l(S,b.sibling),j=u(b,E.props),ru(j,E),j.return=S,S=j;break e}l(S,b);break}else t(S,b);b=b.sibling}E.type===U?(j=zl(E.props.children,S.mode,j,E.key),j.return=S,S=j):(j=un(E.type,E.key,E.props,null,S.mode,j),ru(j,E),j.return=S,S=j)}return c(S);case L:e:{for(K=E.key;b!==null;){if(b.key===K)if(b.tag===4&&b.stateNode.containerInfo===E.containerInfo&&b.stateNode.implementation===E.implementation){l(S,b.sibling),j=u(b,E.children||[]),j.return=S,S=j;break e}else{l(S,b);break}else t(S,b);b=b.sibling}j=Qi(E,S.mode,j),j.return=S,S=j}return c(S);case Ne:return K=E._init,E=K(E._payload),ve(S,b,E,j)}if(Qe(E))return I(S,b,E,j);if(Xe(E)){if(K=Xe(E),typeof K!="function")throw Error(r(150));return E=K.call(E),W(S,b,E,j)}if(typeof E.then=="function")return ve(S,b,En(E),j);if(E.$$typeof===G)return ve(S,b,rn(S,E),j);Tn(S,E)}return typeof E=="string"&&E!==""||typeof E=="number"||typeof E=="bigint"?(E=""+E,b!==null&&b.tag===6?(l(S,b.sibling),j=u(b,E),j.return=S,S=j):(l(S,b),j=Xi(E,S.mode,j),j.return=S,S=j),c(S)):l(S,b)}return function(S,b,E,j){try{fu=0;var K=ve(S,b,E,j);return ma=null,K}catch(k){if(k===Ia||k===on)throw k;var ue=ut(29,k,null,S.mode);return ue.lanes=j,ue.return=S,ue}finally{}}}var ya=Gs(!0),Xs=Gs(!1),gt=H(null),Rt=null;function nl(e){var t=e.alternate;Y(Ue,Ue.current&1),Y(gt,e),Rt===null&&(t===null||sa.current!==null||t.memoizedState!==null)&&(Rt=e)}function Qs(e){if(e.tag===22){if(Y(Ue,Ue.current),Y(gt,e),Rt===null){var t=e.alternate;t!==null&&t.memoizedState!==null&&(Rt=e)}}else il()}function il(){Y(Ue,Ue.current),Y(gt,gt.current)}function Yt(e){Z(gt),Rt===e&&(Rt=null),Z(Ue)}var Ue=H(0);function An(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==="$?"||df(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 Ec(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 Tc={enqueueSetState:function(e,t,l){e=e._reactInternals;var a=ft(),u=ll(a);u.payload=t,l!=null&&(u.callback=l),t=al(e,u,a),t!==null&&(rt(t,e,a),tu(t,e,a))},enqueueReplaceState:function(e,t,l){e=e._reactInternals;var a=ft(),u=ll(a);u.tag=1,u.payload=t,l!=null&&(u.callback=l),t=al(e,u,a),t!==null&&(rt(t,e,a),tu(t,e,a))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var l=ft(),a=ll(l);a.tag=2,t!=null&&(a.callback=t),t=al(e,a,l),t!==null&&(rt(t,e,l),tu(t,e,l))}};function Zs(e,t,l,a,u,n,c){return e=e.stateNode,typeof e.shouldComponentUpdate=="function"?e.shouldComponentUpdate(a,n,c):t.prototype&&t.prototype.isPureReactComponent?!Va(l,a)||!Va(u,n):!0}function Vs(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&&Tc.enqueueReplaceState(t,t.state,null)}function wl(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 u in e)l[u]===void 0&&(l[u]=e[u])}return l}var Rn=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 Ks(e){Rn(e)}function Js(e){console.error(e)}function ks(e){Rn(e)}function On(e,t){try{var l=e.onUncaughtError;l(t.value,{componentStack:t.stack})}catch(a){setTimeout(function(){throw a})}}function $s(e,t,l){try{var a=e.onCaughtError;a(l.value,{componentStack:l.stack,errorBoundary:t.tag===1?t.stateNode:null})}catch(u){setTimeout(function(){throw u})}}function Ac(e,t,l){return l=ll(l),l.tag=3,l.payload={element:null},l.callback=function(){On(e,t)},l}function Ws(e){return e=ll(e),e.tag=3,e}function Fs(e,t,l,a){var u=l.type.getDerivedStateFromError;if(typeof u=="function"){var n=a.value;e.payload=function(){return u(n)},e.callback=function(){$s(t,l,a)}}var c=l.stateNode;c!==null&&typeof c.componentDidCatch=="function"&&(e.callback=function(){$s(t,l,a),typeof u!="function"&&(dl===null?dl=new Set([this]):dl.add(this));var f=a.stack;this.componentDidCatch(a.value,{componentStack:f!==null?f:""})})}function Um(e,t,l,a,u){if(l.flags|=32768,a!==null&&typeof a=="object"&&typeof a.then=="function"){if(t=l.alternate,t!==null&&Wa(t,l,u,!0),l=gt.current,l!==null){switch(l.tag){case 13:return Rt===null?kc():l.alternate===null&&Me===0&&(Me=3),l.flags&=-257,l.flags|=65536,l.lanes=u,a===Ii?l.flags|=16384:(t=l.updateQueue,t===null?l.updateQueue=new Set([a]):t.add(a),Wc(e,a,u)),!1;case 22:return l.flags|=65536,a===Ii?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)),Wc(e,a,u)),!1}throw Error(r(435,l.tag))}return Wc(e,a,u),kc(),!1}if(se)return t=gt.current,t!==null?((t.flags&65536)===0&&(t.flags|=256),t.flags|=65536,t.lanes=u,a!==Ki&&(e=Error(r(422),{cause:a}),$a(ht(e,l)))):(a!==Ki&&(t=Error(r(423),{cause:a}),$a(ht(t,l))),e=e.current.alternate,e.flags|=65536,u&=-u,e.lanes|=u,a=ht(a,l),u=Ac(e.stateNode,a,u),lc(e,u),Me!==4&&(Me=2)),!1;var n=Error(r(520),{cause:a});if(n=ht(n,l),vu===null?vu=[n]:vu.push(n),Me!==4&&(Me=2),t===null)return!0;a=ht(a,l),l=t;do{switch(l.tag){case 3:return l.flags|=65536,e=u&-u,l.lanes|=e,e=Ac(l.stateNode,a,e),lc(l,e),!1;case 1:if(t=l.type,n=l.stateNode,(l.flags&128)===0&&(typeof t.getDerivedStateFromError=="function"||n!==null&&typeof n.componentDidCatch=="function"&&(dl===null||!dl.has(n))))return l.flags|=65536,u&=-u,l.lanes|=u,u=Ws(u),Fs(u,e,l,a),lc(l,u),!1}l=l.return}while(l!==null);return!1}var Ps=Error(r(461)),Be=!1;function Le(e,t,l,a){t.child=e===null?Xs(t,null,l,a):ya(t,e.child,l,a)}function Is(e,t,l,a,u){l=l.render;var n=t.ref;if("ref"in a){var c={};for(var f in a)f!=="ref"&&(c[f]=a[f])}else c=a;return Hl(t),a=cc(e,t,l,c,n,u),f=fc(),e!==null&&!Be?(rc(e,t,u),Gt(e,t,u)):(se&&f&&Zi(t),t.flags|=1,Le(e,t,a,u),t.child)}function eo(e,t,l,a,u){if(e===null){var n=l.type;return typeof n=="function"&&!Gi(n)&&n.defaultProps===void 0&&l.compare===null?(t.tag=15,t.type=n,to(e,t,n,a,u)):(e=un(l.type,null,a,t,t.mode,u),e.ref=t.ref,e.return=t,t.child=e)}if(n=e.child,!jc(e,u)){var c=n.memoizedProps;if(l=l.compare,l=l!==null?l:Va,l(c,a)&&e.ref===t.ref)return Gt(e,t,u)}return t.flags|=1,e=Ut(n,a),e.ref=t.ref,e.return=t,t.child=e}function to(e,t,l,a,u){if(e!==null){var n=e.memoizedProps;if(Va(n,a)&&e.ref===t.ref)if(Be=!1,t.pendingProps=a=n,jc(e,u))(e.flags&131072)!==0&&(Be=!0);else return t.lanes=e.lanes,Gt(e,t,u)}return Rc(e,t,l,a,u)}function lo(e,t,l){var a=t.pendingProps,u=a.children,n=e!==null?e.memoizedState:null;if(a.mode==="hidden"){if((t.flags&128)!==0){if(a=n!==null?n.baseLanes|l:l,e!==null){for(u=t.child=e.child,n=0;u!==null;)n=n|u.lanes|u.childLanes,u=u.sibling;t.childLanes=n&~a}else t.childLanes=0,t.child=null;return ao(e,t,a,l)}if((l&536870912)!==0)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&sn(t,n!==null?n.cachePool:null),n!==null?ts(t,n):uc(),Qs(t);else return t.lanes=t.childLanes=536870912,ao(e,t,n!==null?n.baseLanes|l:l,l)}else n!==null?(sn(t,n.cachePool),ts(t,n),il(),t.memoizedState=null):(e!==null&&sn(t,null),uc(),il());return Le(e,t,u,l),t.child}function ao(e,t,l,a){var u=Pi();return u=u===null?null:{parent:Ce._currentValue,pool:u},t.memoizedState={baseLanes:l,cachePool:u},e!==null&&sn(t,null),uc(),Qs(t),e!==null&&Wa(e,t,a,!0),null}function Mn(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(r(284));(e===null||e.ref!==l)&&(t.flags|=4194816)}}function Rc(e,t,l,a,u){return Hl(t),l=cc(e,t,l,a,void 0,u),a=fc(),e!==null&&!Be?(rc(e,t,u),Gt(e,t,u)):(se&&a&&Zi(t),t.flags|=1,Le(e,t,l,u),t.child)}function uo(e,t,l,a,u,n){return Hl(t),t.updateQueue=null,l=as(t,a,l,u),ls(e),a=fc(),e!==null&&!Be?(rc(e,t,n),Gt(e,t,n)):(se&&a&&Zi(t),t.flags|=1,Le(e,t,l,n),t.child)}function no(e,t,l,a,u){if(Hl(t),t.stateNode===null){var n=na,c=l.contextType;typeof c=="object"&&c!==null&&(n=Ve(c)),n=new l(a,n),t.memoizedState=n.state!==null&&n.state!==void 0?n.state:null,n.updater=Tc,t.stateNode=n,n._reactInternals=t,n=t.stateNode,n.props=a,n.state=t.memoizedState,n.refs={},ec(t),c=l.contextType,n.context=typeof c=="object"&&c!==null?Ve(c):na,n.state=t.memoizedState,c=l.getDerivedStateFromProps,typeof c=="function"&&(Ec(t,l,c,a),n.state=t.memoizedState),typeof l.getDerivedStateFromProps=="function"||typeof n.getSnapshotBeforeUpdate=="function"||typeof n.UNSAFE_componentWillMount!="function"&&typeof n.componentWillMount!="function"||(c=n.state,typeof n.componentWillMount=="function"&&n.componentWillMount(),typeof n.UNSAFE_componentWillMount=="function"&&n.UNSAFE_componentWillMount(),c!==n.state&&Tc.enqueueReplaceState(n,n.state,null),au(t,a,n,u),lu(),n.state=t.memoizedState),typeof n.componentDidMount=="function"&&(t.flags|=4194308),a=!0}else if(e===null){n=t.stateNode;var f=t.memoizedProps,m=wl(l,f);n.props=m;var T=n.context,D=l.contextType;c=na,typeof D=="object"&&D!==null&&(c=Ve(D));var C=l.getDerivedStateFromProps;D=typeof C=="function"||typeof n.getSnapshotBeforeUpdate=="function",f=t.pendingProps!==f,D||typeof n.UNSAFE_componentWillReceiveProps!="function"&&typeof n.componentWillReceiveProps!="function"||(f||T!==c)&&Vs(t,n,a,c),tl=!1;var A=t.memoizedState;n.state=A,au(t,a,n,u),lu(),T=t.memoizedState,f||A!==T||tl?(typeof C=="function"&&(Ec(t,l,C,a),T=t.memoizedState),(m=tl||Zs(t,l,m,a,A,T,c))?(D||typeof n.UNSAFE_componentWillMount!="function"&&typeof n.componentWillMount!="function"||(typeof n.componentWillMount=="function"&&n.componentWillMount(),typeof n.UNSAFE_componentWillMount=="function"&&n.UNSAFE_componentWillMount()),typeof n.componentDidMount=="function"&&(t.flags|=4194308)):(typeof n.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=a,t.memoizedState=T),n.props=a,n.state=T,n.context=c,a=m):(typeof n.componentDidMount=="function"&&(t.flags|=4194308),a=!1)}else{n=t.stateNode,tc(e,t),c=t.memoizedProps,D=wl(l,c),n.props=D,C=t.pendingProps,A=n.context,T=l.contextType,m=na,typeof T=="object"&&T!==null&&(m=Ve(T)),f=l.getDerivedStateFromProps,(T=typeof f=="function"||typeof n.getSnapshotBeforeUpdate=="function")||typeof n.UNSAFE_componentWillReceiveProps!="function"&&typeof n.componentWillReceiveProps!="function"||(c!==C||A!==m)&&Vs(t,n,a,m),tl=!1,A=t.memoizedState,n.state=A,au(t,a,n,u),lu();var R=t.memoizedState;c!==C||A!==R||tl||e!==null&&e.dependencies!==null&&fn(e.dependencies)?(typeof f=="function"&&(Ec(t,l,f,a),R=t.memoizedState),(D=tl||Zs(t,l,D,a,A,R,m)||e!==null&&e.dependencies!==null&&fn(e.dependencies))?(T||typeof n.UNSAFE_componentWillUpdate!="function"&&typeof n.componentWillUpdate!="function"||(typeof n.componentWillUpdate=="function"&&n.componentWillUpdate(a,R,m),typeof n.UNSAFE_componentWillUpdate=="function"&&n.UNSAFE_componentWillUpdate(a,R,m)),typeof n.componentDidUpdate=="function"&&(t.flags|=4),typeof n.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof n.componentDidUpdate!="function"||c===e.memoizedProps&&A===e.memoizedState||(t.flags|=4),typeof n.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&A===e.memoizedState||(t.flags|=1024),t.memoizedProps=a,t.memoizedState=R),n.props=a,n.state=R,n.context=m,a=D):(typeof n.componentDidUpdate!="function"||c===e.memoizedProps&&A===e.memoizedState||(t.flags|=4),typeof n.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&A===e.memoizedState||(t.flags|=1024),a=!1)}return n=a,Mn(e,t),a=(t.flags&128)!==0,n||a?(n=t.stateNode,l=a&&typeof l.getDerivedStateFromError!="function"?null:n.render(),t.flags|=1,e!==null&&a?(t.child=ya(t,e.child,null,u),t.child=ya(t,null,l,u)):Le(e,t,l,u),t.memoizedState=n.state,e=t.child):e=Gt(e,t,u),e}function io(e,t,l,a){return ka(),t.flags|=256,Le(e,t,l,a),t.child}var Oc={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Mc(e){return{baseLanes:e,cachePool:Jr()}}function Nc(e,t,l){return e=e!==null?e.childLanes&~l:0,t&&(e|=bt),e}function co(e,t,l){var a=t.pendingProps,u=!1,n=(t.flags&128)!==0,c;if((c=n)||(c=e!==null&&e.memoizedState===null?!1:(Ue.current&2)!==0),c&&(u=!0,t.flags&=-129),c=(t.flags&32)!==0,t.flags&=-33,e===null){if(se){if(u?nl(t):il(),se){var f=Oe,m;if(m=f){e:{for(m=f,f=At;m.nodeType!==8;){if(!f){f=null;break e}if(m=Et(m.nextSibling),m===null){f=null;break e}}f=m}f!==null?(t.memoizedState={dehydrated:f,treeContext:Dl!==null?{id:Ht,overflow:Bt}:null,retryLane:536870912,hydrationErrors:null},m=ut(18,null,null,0),m.stateNode=f,m.return=t,t.child=m,ke=t,Oe=null,m=!0):m=!1}m||Cl(t)}if(f=t.memoizedState,f!==null&&(f=f.dehydrated,f!==null))return df(f)?t.lanes=32:t.lanes=536870912,null;Yt(t)}return f=a.children,a=a.fallback,u?(il(),u=t.mode,f=Nn({mode:"hidden",children:f},u),a=zl(a,u,l,null),f.return=t,a.return=t,f.sibling=a,t.child=f,u=t.child,u.memoizedState=Mc(l),u.childLanes=Nc(e,c,l),t.memoizedState=Oc,a):(nl(t),zc(t,f))}if(m=e.memoizedState,m!==null&&(f=m.dehydrated,f!==null)){if(n)t.flags&256?(nl(t),t.flags&=-257,t=Dc(e,t,l)):t.memoizedState!==null?(il(),t.child=e.child,t.flags|=128,t=null):(il(),u=a.fallback,f=t.mode,a=Nn({mode:"visible",children:a.children},f),u=zl(u,f,l,null),u.flags|=2,a.return=t,u.return=t,a.sibling=u,t.child=a,ya(t,e.child,null,l),a=t.child,a.memoizedState=Mc(l),a.childLanes=Nc(e,c,l),t.memoizedState=Oc,t=u);else if(nl(t),df(f)){if(c=f.nextSibling&&f.nextSibling.dataset,c)var T=c.dgst;c=T,a=Error(r(419)),a.stack="",a.digest=c,$a({value:a,source:null,stack:null}),t=Dc(e,t,l)}else if(Be||Wa(e,t,l,!1),c=(l&e.childLanes)!==0,Be||c){if(c=pe,c!==null&&(a=l&-l,a=(a&42)!==0?1:di(a),a=(a&(c.suspendedLanes|l))!==0?0:a,a!==0&&a!==m.retryLane))throw m.retryLane=a,ua(e,a),rt(c,e,a),Ps;f.data==="$?"||kc(),t=Dc(e,t,l)}else f.data==="$?"?(t.flags|=192,t.child=e.child,t=null):(e=m.treeContext,Oe=Et(f.nextSibling),ke=t,se=!0,jl=null,At=!1,e!==null&&(yt[vt++]=Ht,yt[vt++]=Bt,yt[vt++]=Dl,Ht=e.id,Bt=e.overflow,Dl=t),t=zc(t,a.children),t.flags|=4096);return t}return u?(il(),u=a.fallback,f=t.mode,m=e.child,T=m.sibling,a=Ut(m,{mode:"hidden",children:a.children}),a.subtreeFlags=m.subtreeFlags&65011712,T!==null?u=Ut(T,u):(u=zl(u,f,l,null),u.flags|=2),u.return=t,a.return=t,a.sibling=u,t.child=a,a=u,u=t.child,f=e.child.memoizedState,f===null?f=Mc(l):(m=f.cachePool,m!==null?(T=Ce._currentValue,m=m.parent!==T?{parent:T,pool:T}:m):m=Jr(),f={baseLanes:f.baseLanes|l,cachePool:m}),u.memoizedState=f,u.childLanes=Nc(e,c,l),t.memoizedState=Oc,a):(nl(t),l=e.child,e=l.sibling,l=Ut(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 zc(e,t){return t=Nn({mode:"visible",children:t},e.mode),t.return=e,e.child=t}function Nn(e,t){return e=ut(22,e,null,t),e.lanes=0,e.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},e}function Dc(e,t,l){return ya(t,e.child,null,l),e=zc(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function fo(e,t,l){e.lanes|=t;var a=e.alternate;a!==null&&(a.lanes|=t),ki(e.return,t,l)}function _c(e,t,l,a,u){var n=e.memoizedState;n===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:a,tail:l,tailMode:u}:(n.isBackwards=t,n.rendering=null,n.renderingStartTime=0,n.last=a,n.tail=l,n.tailMode=u)}function ro(e,t,l){var a=t.pendingProps,u=a.revealOrder,n=a.tail;if(Le(e,t,a.children,l),a=Ue.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&&fo(e,l,t);else if(e.tag===19)fo(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(Y(Ue,a),u){case"forwards":for(l=t.child,u=null;l!==null;)e=l.alternate,e!==null&&An(e)===null&&(u=l),l=l.sibling;l=u,l===null?(u=t.child,t.child=null):(u=l.sibling,l.sibling=null),_c(t,!1,u,l,n);break;case"backwards":for(l=null,u=t.child,t.child=null;u!==null;){if(e=u.alternate,e!==null&&An(e)===null){t.child=u;break}e=u.sibling,u.sibling=l,l=u,u=e}_c(t,!0,l,null,n);break;case"together":_c(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Gt(e,t,l){if(e!==null&&(t.dependencies=e.dependencies),ol|=t.lanes,(l&t.childLanes)===0)if(e!==null){if(Wa(e,t,l,!1),(l&t.childLanes)===0)return null}else return null;if(e!==null&&t.child!==e.child)throw Error(r(153));if(t.child!==null){for(e=t.child,l=Ut(e,e.pendingProps),t.child=l,l.return=t;e.sibling!==null;)e=e.sibling,l=l.sibling=Ut(e,e.pendingProps),l.return=t;l.sibling=null}return t.child}function jc(e,t){return(e.lanes&t)!==0?!0:(e=e.dependencies,!!(e!==null&&fn(e)))}function Hm(e,t,l){switch(t.tag){case 3:xe(t,t.stateNode.containerInfo),el(t,Ce,e.memoizedState.cache),ka();break;case 27:case 5:ci(t);break;case 4:xe(t,t.stateNode.containerInfo);break;case 10:el(t,t.type,t.memoizedProps.value);break;case 13:var a=t.memoizedState;if(a!==null)return a.dehydrated!==null?(nl(t),t.flags|=128,null):(l&t.child.childLanes)!==0?co(e,t,l):(nl(t),e=Gt(e,t,l),e!==null?e.sibling:null);nl(t);break;case 19:var u=(e.flags&128)!==0;if(a=(l&t.childLanes)!==0,a||(Wa(e,t,l,!1),a=(l&t.childLanes)!==0),u){if(a)return ro(e,t,l);t.flags|=128}if(u=t.memoizedState,u!==null&&(u.rendering=null,u.tail=null,u.lastEffect=null),Y(Ue,Ue.current),a)break;return null;case 22:case 23:return t.lanes=0,lo(e,t,l);case 24:el(t,Ce,e.memoizedState.cache)}return Gt(e,t,l)}function so(e,t,l){if(e!==null)if(e.memoizedProps!==t.pendingProps)Be=!0;else{if(!jc(e,l)&&(t.flags&128)===0)return Be=!1,Hm(e,t,l);Be=(e.flags&131072)!==0}else Be=!1,se&&(t.flags&1048576)!==0&&Yr(t,cn,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var a=t.elementType,u=a._init;if(a=u(a._payload),t.type=a,typeof a=="function")Gi(a)?(e=wl(a,e),t.tag=1,t=no(null,t,a,e,l)):(t.tag=0,t=Rc(null,t,a,e,l));else{if(a!=null){if(u=a.$$typeof,u===ee){t.tag=11,t=Is(null,t,a,e,l);break e}else if(u===Te){t.tag=14,t=eo(null,t,a,e,l);break e}}throw t=Tl(a)||a,Error(r(306,t,""))}}return t;case 0:return Rc(e,t,t.type,t.pendingProps,l);case 1:return a=t.type,u=wl(a,t.pendingProps),no(e,t,a,u,l);case 3:e:{if(xe(t,t.stateNode.containerInfo),e===null)throw Error(r(387));a=t.pendingProps;var n=t.memoizedState;u=n.element,tc(e,t),au(t,a,null,l);var c=t.memoizedState;if(a=c.cache,el(t,Ce,a),a!==n.cache&&$i(t,[Ce],l,!0),lu(),a=c.element,n.isDehydrated)if(n={element:a,isDehydrated:!1,cache:c.cache},t.updateQueue.baseState=n,t.memoizedState=n,t.flags&256){t=io(e,t,a,l);break e}else if(a!==u){u=ht(Error(r(424)),t),$a(u),t=io(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(Oe=Et(e.firstChild),ke=t,se=!0,jl=null,At=!0,l=Xs(t,null,a,l),t.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling}else{if(ka(),a===u){t=Gt(e,t,l);break e}Le(e,t,a,l)}t=t.child}return t;case 26:return Mn(e,t),e===null?(l=yd(t.type,null,t.pendingProps,null))?t.memoizedState=l:se||(l=t.type,e=t.pendingProps,a=Xn(te.current).createElement(l),a[Ze]=t,a[$e]=e,Ge(a,l,e),He(a),t.stateNode=a):t.memoizedState=yd(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return ci(t),e===null&&se&&(a=t.stateNode=dd(t.type,t.pendingProps,te.current),ke=t,At=!0,u=Oe,yl(t.type)?(hf=u,Oe=Et(a.firstChild)):Oe=u),Le(e,t,t.pendingProps.children,l),Mn(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&se&&((u=a=Oe)&&(a=ry(a,t.type,t.pendingProps,At),a!==null?(t.stateNode=a,ke=t,Oe=Et(a.firstChild),At=!1,u=!0):u=!1),u||Cl(t)),ci(t),u=t.type,n=t.pendingProps,c=e!==null?e.memoizedProps:null,a=n.children,rf(u,n)?a=null:c!==null&&rf(u,c)&&(t.flags|=32),t.memoizedState!==null&&(u=cc(e,t,Mm,null,null,l),Ru._currentValue=u),Mn(e,t),Le(e,t,a,l),t.child;case 6:return e===null&&se&&((e=l=Oe)&&(l=sy(l,t.pendingProps,At),l!==null?(t.stateNode=l,ke=t,Oe=null,e=!0):e=!1),e||Cl(t)),null;case 13:return co(e,t,l);case 4:return xe(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=ya(t,null,a,l):Le(e,t,a,l),t.child;case 11:return Is(e,t,t.type,t.pendingProps,l);case 7:return Le(e,t,t.pendingProps,l),t.child;case 8:return Le(e,t,t.pendingProps.children,l),t.child;case 12:return Le(e,t,t.pendingProps.children,l),t.child;case 10:return a=t.pendingProps,el(t,t.type,a.value),Le(e,t,a.children,l),t.child;case 9:return u=t.type._context,a=t.pendingProps.children,Hl(t),u=Ve(u),a=a(u),t.flags|=1,Le(e,t,a,l),t.child;case 14:return eo(e,t,t.type,t.pendingProps,l);case 15:return to(e,t,t.type,t.pendingProps,l);case 19:return ro(e,t,l);case 31:return a=t.pendingProps,l=t.mode,a={mode:a.mode,children:a.children},e===null?(l=Nn(a,l),l.ref=t.ref,t.child=l,l.return=t,t=l):(l=Ut(e.child,a),l.ref=t.ref,t.child=l,l.return=t,t=l),t;case 22:return lo(e,t,l);case 24:return Hl(t),a=Ve(Ce),e===null?(u=Pi(),u===null&&(u=pe,n=Wi(),u.pooledCache=n,n.refCount++,n!==null&&(u.pooledCacheLanes|=l),u=n),t.memoizedState={parent:a,cache:u},ec(t),el(t,Ce,u)):((e.lanes&l)!==0&&(tc(e,t),au(t,null,null,l),lu()),u=e.memoizedState,n=t.memoizedState,u.parent!==a?(u={parent:a,cache:a},t.memoizedState=u,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=u),el(t,Ce,a)):(a=n.cache,el(t,Ce,a),a!==u.cache&&$i(t,[Ce],l,!0))),Le(e,t,t.pendingProps.children,l),t.child;case 29:throw t.pendingProps}throw Error(r(156,t.tag))}function Xt(e){e.flags|=4}function oo(e,t){if(t.type!=="stylesheet"||(t.state.loading&4)!==0)e.flags&=-16777217;else if(e.flags|=16777216,!Sd(t)){if(t=gt.current,t!==null&&((ce&4194048)===ce?Rt!==null:(ce&62914560)!==ce&&(ce&536870912)===0||t!==Rt))throw eu=Ii,kr;e.flags|=8192}}function zn(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag!==22?Qf():536870912,e.lanes|=t,pa|=t)}function su(e,t){if(!se)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 Ae(e){var t=e.alternate!==null&&e.alternate.child===e.child,l=0,a=0;if(t)for(var u=e.child;u!==null;)l|=u.lanes|u.childLanes,a|=u.subtreeFlags&65011712,a|=u.flags&65011712,u.return=e,u=u.sibling;else for(u=e.child;u!==null;)l|=u.lanes|u.childLanes,a|=u.subtreeFlags,a|=u.flags,u.return=e,u=u.sibling;return e.subtreeFlags|=a,e.childLanes=l,t}function Bm(e,t,l){var a=t.pendingProps;switch(Vi(t),t.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Ae(t),null;case 1:return Ae(t),null;case 3:return l=t.stateNode,a=null,e!==null&&(a=e.memoizedState.cache),t.memoizedState.cache!==a&&(t.flags|=2048),wt(Ce),Wt(),l.pendingContext&&(l.context=l.pendingContext,l.pendingContext=null),(e===null||e.child===null)&&(Ja(t)?Xt(t):e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,Qr())),Ae(t),null;case 26:return l=t.memoizedState,e===null?(Xt(t),l!==null?(Ae(t),oo(t,l)):(Ae(t),t.flags&=-16777217)):l?l!==e.memoizedState?(Xt(t),Ae(t),oo(t,l)):(Ae(t),t.flags&=-16777217):(e.memoizedProps!==a&&Xt(t),Ae(t),t.flags&=-16777217),null;case 27:Lu(t),l=te.current;var u=t.type;if(e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Xt(t);else{if(!a){if(t.stateNode===null)throw Error(r(166));return Ae(t),null}e=$.current,Ja(t)?Gr(t):(e=dd(u,a,l),t.stateNode=e,Xt(t))}return Ae(t),null;case 5:if(Lu(t),l=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Xt(t);else{if(!a){if(t.stateNode===null)throw Error(r(166));return Ae(t),null}if(e=$.current,Ja(t))Gr(t);else{switch(u=Xn(te.current),e){case 1:e=u.createElementNS("http://www.w3.org/2000/svg",l);break;case 2:e=u.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;default:switch(l){case"svg":e=u.createElementNS("http://www.w3.org/2000/svg",l);break;case"math":e=u.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;case"script":e=u.createElement("div"),e.innerHTML=" - + +
diff --git a/main/main.c b/main/main.c index 326740c..2c484e6 100755 --- a/main/main.c +++ b/main/main.c @@ -28,6 +28,7 @@ #include "loadbalancer.h" #include "meter_manager.h" #include "buzzer.h" +#include "evse_link.h" #define EVSE_MANAGER_TICK_PERIOD_MS 1000 @@ -235,10 +236,9 @@ static void init_modules(void) { loadbalancer_init(); meter_manager_init(); meter_manager_start(); + evse_link_init(); - - - //wifi_ap_start(); + // wifi_ap_start(); // Outros módulos (descomente conforme necessário) // meter_init(); // ocpp_start(); diff --git a/projeto_parte1.c b/projeto_parte1.c index be71207..5ed30f4 100644 --- a/projeto_parte1.c +++ b/projeto_parte1.c @@ -30,6 +30,8 @@ #include "auth.h" #include "loadbalancer.h" #include "meter_manager.h" +#include "buzzer.h" +#include "evse_link.h" #define EVSE_MANAGER_TICK_PERIOD_MS 1000 @@ -227,6 +229,7 @@ static void button_init(void) { static void init_modules(void) { peripherals_init(); //api_init(); + buzzer_init(); ESP_ERROR_CHECK(rest_server_init("/data")); protocols_init(); evse_manager_init(); @@ -236,7 +239,9 @@ static void init_modules(void) { loadbalancer_init(); meter_manager_init(); meter_manager_start(); + evse_link_init(); + // wifi_ap_start(); // Outros módulos (descomente conforme necessário) // meter_init(); // ocpp_start(); @@ -283,1925 +288,3079 @@ void app_main(void) { // === Fim de: main/main.c === -// === Início de: components/rest_api/src/ocpp_api.c === -// ========================= -// ocpp_api.c -// ========================= -#include "ocpp_api.h" -#include "esp_log.h" -#include "cJSON.h" - -static const char *TAG = "ocpp_api"; - -static struct { - char url[256]; - char chargeBoxId[128]; - char certificate[256]; - char privateKey[256]; -} ocpp_config = {"", "", "", ""}; - -static esp_err_t ocpp_get_status_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); - cJSON *status = cJSON_CreateObject(); - cJSON_AddStringToObject(status, "status", "connected"); - char *str = cJSON_Print(status); - httpd_resp_sendstr(req, str); - free(str); - cJSON_Delete(status); - return ESP_OK; -} - -static esp_err_t ocpp_get_config_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); - cJSON *json = cJSON_CreateObject(); - cJSON_AddStringToObject(json, "url", ocpp_config.url); - cJSON_AddStringToObject(json, "chargeBoxId", ocpp_config.chargeBoxId); - cJSON_AddStringToObject(json, "certificate", ocpp_config.certificate); - cJSON_AddStringToObject(json, "privateKey", ocpp_config.privateKey); - char *str = cJSON_Print(json); - httpd_resp_sendstr(req, str); - free(str); - cJSON_Delete(json); - return ESP_OK; -} - -static esp_err_t ocpp_post_config_handler(httpd_req_t *req) { - char buf[512]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); - return ESP_FAIL; - } - buf[len] = '\0'; - cJSON *json = cJSON_Parse(buf); - if (!json) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); - return ESP_FAIL; - } - cJSON *url = cJSON_GetObjectItem(json, "url"); - if (url) strlcpy(ocpp_config.url, url->valuestring, sizeof(ocpp_config.url)); - cJSON *id = cJSON_GetObjectItem(json, "chargeBoxId"); - if (id) strlcpy(ocpp_config.chargeBoxId, id->valuestring, sizeof(ocpp_config.chargeBoxId)); - cJSON *cert = cJSON_GetObjectItem(json, "certificate"); - if (cert) strlcpy(ocpp_config.certificate, cert->valuestring, sizeof(ocpp_config.certificate)); - cJSON *key = cJSON_GetObjectItem(json, "privateKey"); - if (key) strlcpy(ocpp_config.privateKey, key->valuestring, sizeof(ocpp_config.privateKey)); - cJSON_Delete(json); - httpd_resp_sendstr(req, "OCPP config atualizada com sucesso"); - return ESP_OK; -} - -void register_ocpp_handlers(httpd_handle_t server, void *ctx) { - httpd_uri_t status_uri = { - .uri = "/api/v1/ocpp", - .method = HTTP_GET, - .handler = ocpp_get_status_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &status_uri); - - httpd_uri_t get_uri = { - .uri = "/api/v1/config/ocpp", - .method = HTTP_GET, - .handler = ocpp_get_config_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &get_uri); - - httpd_uri_t post_uri = { - .uri = "/api/v1/config/ocpp", - .method = HTTP_POST, - .handler = ocpp_post_config_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &post_uri); -} - -// === Fim de: components/rest_api/src/ocpp_api.c === - - -// === Início de: components/rest_api/src/static_file_api.c === -#include "static_file_api.h" -#include "esp_log.h" -#include -#include -#include "esp_vfs.h" - -static const char *TAG = "static_file_api"; - -#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128) -#define SCRATCH_BUFSIZE (10240) - -typedef struct rest_server_context { - char base_path[ESP_VFS_PATH_MAX + 1]; - char scratch[SCRATCH_BUFSIZE]; -} rest_server_context_t; - -#define CHECK_FILE_EXTENSION(filename, ext) \ - (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0) - -static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filepath) { - const char *type = "text/plain"; - if (CHECK_FILE_EXTENSION(filepath, ".html")) type = "text/html"; - else if (CHECK_FILE_EXTENSION(filepath, ".js")) type = "application/javascript"; - else if (CHECK_FILE_EXTENSION(filepath, ".css")) type = "text/css"; - else if (CHECK_FILE_EXTENSION(filepath, ".png")) type = "image/png"; - else if (CHECK_FILE_EXTENSION(filepath, ".ico")) type = "image/x-icon"; - else if (CHECK_FILE_EXTENSION(filepath, ".svg")) type = "image/svg+xml"; - return httpd_resp_set_type(req, type); -} - -static esp_err_t static_get_handler(httpd_req_t *req) { - char filepath[FILE_PATH_MAX]; - rest_server_context_t *ctx = (rest_server_context_t *) req->user_ctx; - - strlcpy(filepath, ctx->base_path, sizeof(filepath)); - if (req->uri[strlen(req->uri) - 1] == '/') { - strlcat(filepath, "/index.html", sizeof(filepath)); - } else { - strlcat(filepath, req->uri, sizeof(filepath)); - } - - int fd = open(filepath, O_RDONLY, 0); - if (fd == -1) { - // fallback para /index.html (SPA) - ESP_LOGW(TAG, "Arquivo não encontrado: %s. Tentando index.html", filepath); - strlcpy(filepath, ctx->base_path, sizeof(filepath)); - strlcat(filepath, "/index.html", sizeof(filepath)); - fd = open(filepath, O_RDONLY, 0); - if (fd == -1) { - httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Arquivo não encontrado"); - return ESP_FAIL; - } - } - - set_content_type_from_file(req, filepath); - - char *chunk = ctx->scratch; - ssize_t read_bytes; - do { - read_bytes = read(fd, chunk, SCRATCH_BUFSIZE); - if (read_bytes == -1) { - ESP_LOGE(TAG, "Erro lendo arquivo: %s", filepath); - close(fd); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Erro ao ler arquivo"); - return ESP_FAIL; - } else if (read_bytes > 0) { - if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) { - close(fd); - httpd_resp_sendstr_chunk(req, NULL); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Erro ao enviar arquivo"); - return ESP_FAIL; - } - } - } while (read_bytes > 0); - - close(fd); - httpd_resp_send_chunk(req, NULL, 0); - return ESP_OK; -} - -void register_static_file_handlers(httpd_handle_t server, void *ctx) { - httpd_uri_t uri = { - .uri = "/*", - .method = HTTP_GET, - .handler = static_get_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &uri); -} - -// === Fim de: components/rest_api/src/static_file_api.c === - - -// === Início de: components/rest_api/src/meters_settings_api.c === -#include "meters_settings_api.h" -#include "meter_manager.h" // Atualizado para usar o novo manager -#include "esp_log.h" -#include "cJSON.h" - -static const char *TAG = "meters_settings_api"; - -// Função para recuperar as configurações dos contadores -static esp_err_t meters_config_get_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Received GET request for /api/v1/config/meters"); - - httpd_resp_set_type(req, "application/json"); - - cJSON *config = cJSON_CreateObject(); - - // Recuperando as configurações dos contadores - meter_type_t gridmeterType = meter_manager_grid_get_model(); - meter_type_t evsemeterType = meter_manager_evse_get_model(); - - ESP_LOGI(TAG, "Grid meter type: %s", meter_type_to_str(gridmeterType)); - ESP_LOGI(TAG, "EVSE meter type: %s", meter_type_to_str(evsemeterType)); - - // Adicionando os tipos de contadores ao objeto JSON - cJSON_AddStringToObject(config, "gridmeter", meter_type_to_str(gridmeterType)); - cJSON_AddStringToObject(config, "evsemeter", meter_type_to_str(evsemeterType)); - - // Convertendo o objeto JSON para uma string - const char *json_str = cJSON_Print(config); - ESP_LOGI(TAG, "Returning meters config: %s", json_str); - - httpd_resp_sendstr(req, json_str); - - // Liberação da memória - free((void *)json_str); - cJSON_Delete(config); - - return ESP_OK; -} - -// Função para atualizar as configurações dos contadores -static esp_err_t meters_config_post_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Received POST request for /api/v1/config/meters"); - - char buf[512]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - - if (len <= 0) { - ESP_LOGE(TAG, "Received empty body in POST request"); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); - return ESP_FAIL; - } - - buf[len] = '\0'; // Garantir que a string está terminada - - ESP_LOGI(TAG, "Received POST data: %s", buf); - - cJSON *json = cJSON_Parse(buf); - if (!json) { - ESP_LOGE(TAG, "Failed to parse JSON data"); - // Resposta detalhada de erro - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON format"); - return ESP_FAIL; - } - - // Atualizando os contadores - cJSON *gridmeter = cJSON_GetObjectItem(json, "gridmeter"); - if (gridmeter) { - meter_type_t gridType = string_to_meter_type(gridmeter->valuestring); // Usando a função string_to_meter_type - ESP_LOGI(TAG, "Updating grid meter type to: %s", gridmeter->valuestring); - meter_manager_grid_set_model(gridType); - } - - cJSON *evsemeter = cJSON_GetObjectItem(json, "evsemeter"); - if (evsemeter) { - meter_type_t evseType = string_to_meter_type(evsemeter->valuestring); // Usando a função string_to_meter_type - ESP_LOGI(TAG, "Updating EVSE meter type to: %s", evsemeter->valuestring); - meter_manager_evse_set_model(evseType); - } - - cJSON_Delete(json); - httpd_resp_sendstr(req, "Meters updated successfully"); - - ESP_LOGI(TAG, "Meters configuration updated successfully"); - - return ESP_OK; -} - -// Registrando os manipuladores de URI para os contadores -void register_meters_settings_handlers(httpd_handle_t server, void *ctx) { - ESP_LOGD(TAG, "Registering URI handlers for meters settings"); - - // URI para o método GET - httpd_uri_t meters_get_uri = { - .uri = "/api/v1/config/meters", - .method = HTTP_GET, - .handler = meters_config_get_handler, - .user_ctx = ctx - }; - ESP_LOGD(TAG, "Registering GET handler for /api/v1/config/meters"); - httpd_register_uri_handler(server, &meters_get_uri); - - // URI para o método POST - httpd_uri_t meters_post_uri = { - .uri = "/api/v1/config/meters", - .method = HTTP_POST, - .handler = meters_config_post_handler, - .user_ctx = ctx - }; - ESP_LOGD(TAG, "Registering POST handler for /api/v1/config/meters"); - httpd_register_uri_handler(server, &meters_post_uri); -} - -// === Fim de: components/rest_api/src/meters_settings_api.c === - - -// === Início de: components/rest_api/src/rest_main.c === -#include "rest_main.h" -#include "evse_settings_api.h" -#include "meters_settings_api.h" -#include "loadbalancing_settings_api.h" -#include "network_api.h" -#include "ocpp_api.h" -#include "auth_api.h" -#include "dashboard_api.h" -#include "static_file_api.h" -#include "esp_log.h" - - -static const char *TAG = "rest_main"; - -esp_err_t rest_server_init(const char *base_path) { - ESP_LOGI(TAG, "Initializing REST API with base path: %s", base_path); - - rest_server_context_t *ctx = calloc(1, sizeof(rest_server_context_t)); - if (!ctx) { - ESP_LOGE(TAG, "Failed to allocate memory for REST context"); - return ESP_ERR_NO_MEM; - } - - strlcpy(ctx->base_path, base_path, sizeof(ctx->base_path)); - - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.uri_match_fn = httpd_uri_match_wildcard; - config.max_uri_handlers = 32; - - httpd_handle_t server = NULL; - esp_err_t err = httpd_start(&server, &config); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err)); - free(ctx); - return err; - } - - ESP_LOGI(TAG, "HTTP server started successfully"); - - // Register endpoint groups - register_evse_settings_handlers(server, ctx); // Apenas chamando a função sem comparação - register_network_handlers(server, ctx); // Apenas chamando a função sem comparação - register_ocpp_handlers(server, ctx); // Apenas chamando a função sem comparação - register_auth_handlers(server, ctx); // Apenas chamando a função sem comparação - register_dashboard_handlers(server, ctx); // Apenas chamando a função sem comparação - register_meters_settings_handlers(server, ctx); // Apenas chamando a função sem comparação - register_loadbalancing_settings_handlers(server, ctx); // Apenas chamando a função sem comparação - register_static_file_handlers(server, ctx); // Apenas chamando a função sem comparação - - ESP_LOGI(TAG, "All REST API endpoint groups registered successfully"); - - return ESP_OK; -} - -// === Fim de: components/rest_api/src/rest_main.c === - - -// === Início de: components/rest_api/src/network_api.c === -// ========================= -// network_api.c -// ========================= - -#include "network_api.h" -#include "esp_log.h" -#include "cJSON.h" -#include "network.h" -#include "mqtt.h" - -static const char *TAG = "network_api"; - -typedef struct { - bool enabled; - char ssid[33]; - char password[65]; -} wifi_task_data_t; - - -static void wifi_apply_config_task(void *param) { - wifi_task_data_t *data = (wifi_task_data_t *)param; - ESP_LOGI("wifi_task", "Applying Wi-Fi config in background task"); - wifi_set_config(data->enabled, data->ssid, data->password); - free(data); - vTaskDelete(NULL); -} - -static esp_err_t wifi_get_handler(httpd_req_t *req) { - ESP_LOGI(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}; - - 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) { - ESP_LOGI(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; - buf[len] = '\0'; - - cJSON *json = cJSON_Parse(buf); - 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; - - cJSON *j_ssid = cJSON_GetObjectItem(json, "ssid"); - 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; - - // Enviar resposta antes de alterar Wi-Fi - 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) { - 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 - xTaskCreate( - wifi_apply_config_task, - "wifi_config_task", - 4096, - task_data, - 3, - NULL - ); - - cJSON_Delete(json); - return ESP_OK; -} - - -static esp_err_t config_mqtt_get_handler(httpd_req_t *req) -{ - ESP_LOGI(TAG, "Handling GET /api/v1/config/mqtt"); - - httpd_resp_set_type(req, "application/json"); - - bool enabled = mqtt_get_enabled(); - char server[64] = {0}; - char base_topic[32] = {0}; - char username[32] = {0}; - char password[64] = {0}; - uint16_t periodicity = mqtt_get_periodicity(); - - mqtt_get_server(server); - mqtt_get_base_topic(base_topic); - mqtt_get_user(username); - mqtt_get_password(password); - - ESP_LOGI(TAG, "MQTT Config:"); - ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false"); - ESP_LOGI(TAG, " Server: %s", server); - ESP_LOGI(TAG, " Topic: %s", base_topic); - ESP_LOGI(TAG, " Username: %s", username); - ESP_LOGI(TAG, " Password: %s", password); - ESP_LOGI(TAG, " Periodicity: %d", periodicity); - - cJSON *config = cJSON_CreateObject(); - cJSON_AddBoolToObject(config, "enabled", enabled); - cJSON_AddStringToObject(config, "host", server); - cJSON_AddNumberToObject(config, "port", 1883); - 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); - httpd_resp_sendstr(req, config_str); - - free((void *)config_str); - cJSON_Delete(config); - return ESP_OK; -} - - -static esp_err_t config_mqtt_post_handler(httpd_req_t *req) -{ - ESP_LOGI(TAG, "Handling POST /api/v1/config/mqtt"); - - char buf[512]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - 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; - } - buf[len] = '\0'; - ESP_LOGI(TAG, "Received JSON: %s", buf); - - cJSON *json = cJSON_Parse(buf); - 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; - - if (cJSON_IsBool(cJSON_GetObjectItem(json, "enabled"))) - enabled = cJSON_GetObjectItem(json, "enabled")->valueint; - - cJSON *j_host = cJSON_GetObjectItem(json, "host"); - if (cJSON_IsString(j_host)) host = j_host->valuestring; - - cJSON *j_topic = cJSON_GetObjectItem(json, "topic"); - if (cJSON_IsString(j_topic)) topic = j_topic->valuestring; - - cJSON *j_user = cJSON_GetObjectItem(json, "username"); - if (cJSON_IsString(j_user)) username = j_user->valuestring; - - cJSON *j_pass = cJSON_GetObjectItem(json, "password"); - if (cJSON_IsString(j_pass)) password = j_pass->valuestring; - - cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity"); - if (cJSON_IsNumber(j_periodicity)) periodicity = j_periodicity->valueint; - - ESP_LOGI(TAG, "Applying MQTT config:"); - ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false"); - ESP_LOGI(TAG, " Host: %s", host); - ESP_LOGI(TAG, " Topic: %s", topic); - ESP_LOGI(TAG, " Username: %s", username); - ESP_LOGI(TAG, " Password: %s", password); - ESP_LOGI(TAG, " Periodicity: %d", periodicity); - - esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity); - 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); - return ESP_FAIL; - } - - httpd_resp_sendstr(req, "Configuração MQTT atualizada com sucesso"); - cJSON_Delete(json); - return ESP_OK; -} - - - -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 - }; - 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 - }; - 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 - }; - 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 - }; - httpd_register_uri_handler(server, &config_mqtt_post_uri); -} - -// === Fim de: components/rest_api/src/network_api.c === - - -// === Início de: components/rest_api/src/dashboard_api.c === -#include "dashboard_api.h" -#include "esp_log.h" -#include "cJSON.h" -#include "evse_api.h" -#include "evse_error.h" -#include "evse_config.h" -#include "evse_limits.h" - - -static const char *TAG = "dashboard_api"; - -static esp_err_t dashboard_get_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); - - // Cria o objeto JSON principal do dashboard - cJSON *dashboard = cJSON_CreateObject(); - - // Status do sistema - evse_state_t state = evse_get_state(); - cJSON_AddStringToObject(dashboard, "status", evse_state_to_str(state)); - - // Carregador - informação do carregador 1 (adapte conforme necessário) - cJSON *chargers = cJSON_CreateArray(); - cJSON *charger1 = cJSON_CreateObject(); - cJSON_AddNumberToObject(charger1, "id", 1); - cJSON_AddStringToObject(charger1, "status", evse_state_to_str(state)); - cJSON_AddNumberToObject(charger1, "current", evse_get_charging_current()); - cJSON_AddNumberToObject(charger1, "maxCurrent", evse_get_max_charging_current()); - - // Calcular a potência com base na corrente (considerando 230V) - int power = (evse_get_charging_current()) * 230; - cJSON_AddNumberToObject(charger1, "power", power); - - cJSON_AddItemToArray(chargers, charger1); - cJSON_AddItemToObject(dashboard, "chargers", chargers); - - // Consumo e tempo de carregamento - cJSON_AddNumberToObject(dashboard, "energyConsumed", evse_get_consumption_limit()); - cJSON_AddNumberToObject(dashboard, "chargingTime", evse_get_charging_time_limit()); - - // Alertas - cJSON *alerts = cJSON_CreateArray(); - if (evse_is_limit_reached()) { - cJSON_AddItemToArray(alerts, cJSON_CreateString("Limite de consumo atingido.")); - } - if (!evse_config_is_available()) { - cJSON_AddItemToArray(alerts, cJSON_CreateString("Estação indisponível.")); - } - if (!evse_config_is_enabled()) { - cJSON_AddItemToArray(alerts, cJSON_CreateString("EVSE desativado.")); - } - cJSON_AddItemToObject(dashboard, "alerts", alerts); - - // Erros - uint32_t error_bits = evse_get_error(); - cJSON *errors = cJSON_CreateArray(); - if (error_bits & EVSE_ERR_DIODE_SHORT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Diodo curto-circuitado")); - if (error_bits & EVSE_ERR_LOCK_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no travamento")); - if (error_bits & EVSE_ERR_UNLOCK_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no destravamento")); - if (error_bits & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no autoteste do RCM")); - if (error_bits & EVSE_ERR_RCM_TRIGGERED_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("RCM disparado")); - if (error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Temperatura elevada")); - if (error_bits & EVSE_ERR_PILOT_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Erro no sinal piloto")); - if (error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no sensor de temperatura")); - cJSON_AddItemToObject(dashboard, "errors", errors); - - // Enviar resposta JSON - const char *json_str = cJSON_Print(dashboard); - httpd_resp_sendstr(req, json_str); - - // Liberar memória - free((void *)json_str); - cJSON_Delete(dashboard); - - return ESP_OK; -} - -void register_dashboard_handlers(httpd_handle_t server, void *ctx) { - httpd_uri_t uri = { - .uri = "/api/v1/dashboard", - .method = HTTP_GET, - .handler = dashboard_get_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &uri); -} - -// === Fim de: components/rest_api/src/dashboard_api.c === - - -// === Início de: components/rest_api/src/auth_api.c === -// ========================= -// auth_api.c -// ========================= -#include "auth_api.h" -#include "auth.h" -#include "esp_log.h" -#include "cJSON.h" - -static const char *TAG = "auth_api"; - -static struct { - char username[128]; -} users[10] = { /*{"admin"}, {"user1"}*/ }; -static int num_users = 2; - -static esp_err_t auth_methods_get_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); - cJSON *json = cJSON_CreateObject(); - cJSON_AddBoolToObject(json, "RFID", auth_is_enabled() ); - char *str = cJSON_PrintUnformatted(json); - httpd_resp_sendstr(req, str); - free(str); - cJSON_Delete(json); - return ESP_OK; -} - -static esp_err_t auth_methods_post_handler(httpd_req_t *req) { - char buf[256]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Erro ao receber dados"); - return ESP_FAIL; - } - - buf[len] = '\0'; - cJSON *json = cJSON_Parse(buf); - if (!json) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON inválido"); - return ESP_FAIL; - } - - cJSON *rfid = cJSON_GetObjectItem(json, "RFID"); - if (rfid && cJSON_IsBool(rfid)) { - auth_set_enabled(cJSON_IsTrue(rfid)); - } else { - cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Campo 'RFID' inválido ou ausente"); - return ESP_FAIL; - } - - cJSON_Delete(json); - httpd_resp_sendstr(req, "Métodos de autenticação atualizados"); - return ESP_OK; -} - - -static esp_err_t users_get_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); - cJSON *root = cJSON_CreateObject(); - cJSON *list = cJSON_CreateArray(); - for (int i = 0; i < num_users; ++i) { - cJSON *u = cJSON_CreateObject(); - cJSON_AddStringToObject(u, "username", users[i].username); - cJSON_AddItemToArray(list, u); - } - cJSON_AddItemToObject(root, "users", list); - char *str = cJSON_Print(root); - httpd_resp_sendstr(req, str); - free(str); - cJSON_Delete(root); - return ESP_OK; -} - -static esp_err_t users_post_handler(httpd_req_t *req) { - char buf[128]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) return ESP_FAIL; - buf[len] = '\0'; - if (num_users < 10) { - strlcpy(users[num_users].username, buf, sizeof(users[num_users].username)); - num_users++; - httpd_resp_sendstr(req, "Usuário adicionado com sucesso"); - } else { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Limite de usuários atingido"); - } - return ESP_OK; -} - -static esp_err_t users_delete_handler(httpd_req_t *req) { - char query[128]; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char username[128]; - if (httpd_query_key_value(query, "username", username, sizeof(username)) == ESP_OK) { - for (int i = 0; i < num_users; i++) { - if (strcmp(users[i].username, username) == 0) { - for (int j = i; j < num_users - 1; j++) { - users[j] = users[j + 1]; - } - num_users--; - httpd_resp_sendstr(req, "Usuário removido com sucesso"); - return ESP_OK; - } - } - } - } - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Usuário não encontrado"); - return ESP_FAIL; -} - -void register_auth_handlers(httpd_handle_t server, void *ctx) { - httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/auth-methods", - .method = HTTP_GET, - .handler = auth_methods_get_handler, - .user_ctx = ctx - }); - httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/auth-methods", - .method = HTTP_POST, - .handler = auth_methods_post_handler, - .user_ctx = ctx - }); - httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/users", - .method = HTTP_GET, - .handler = users_get_handler, - .user_ctx = ctx - }); - httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/users", - .method = HTTP_POST, - .handler = users_post_handler, - .user_ctx = ctx - }); - httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/users", - .method = HTTP_DELETE, - .handler = users_delete_handler, - .user_ctx = ctx - }); -} - -// === Fim de: components/rest_api/src/auth_api.c === - - -// === Início de: components/rest_api/src/loadbalancing_settings_api.c === -#include "loadbalancing_settings_api.h" -#include "loadbalancer.h" -#include "esp_log.h" -#include "cJSON.h" - -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(); - - ESP_LOGI(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit); - - httpd_resp_set_type(req, "application/json"); - - cJSON *config = cJSON_CreateObject(); - cJSON_AddBoolToObject(config, "loadBalancingEnabled", enabled); - cJSON_AddNumberToObject(config, "loadBalancingCurrentLimit", currentLimit); - - const char *json_str = cJSON_Print(config); - httpd_resp_sendstr(req, json_str); - - ESP_LOGI(TAG, "Returned config: %s", json_str); - - free((void *)json_str); - cJSON_Delete(config); - 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"); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); - return ESP_FAIL; - } - - buf[len] = '\0'; - ESP_LOGI(TAG, "Received POST data: %s", buf); - - cJSON *json = cJSON_Parse(buf); - if (!json) { - ESP_LOGE(TAG, "Invalid 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_LOGI(TAG, "Updated loadBalancingEnabled to: %d", isEnabled); - } - - // 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; - } - - 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; - } - - ESP_LOGI(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit); - } - - cJSON_Delete(json); - httpd_resp_sendstr(req, "Load balancing settings updated successfully"); - return ESP_OK; -} - -// Registro dos handlers na API HTTP -void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) { - // GET - httpd_uri_t get_uri = { - .uri = "/api/v1/config/loadbalancing", - .method = HTTP_GET, - .handler = loadbalancing_config_get_handler, - .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 - }; - httpd_register_uri_handler(server, &post_uri); -} - -// === Fim de: components/rest_api/src/loadbalancing_settings_api.c === - - -// === Início de: components/rest_api/src/evse_settings_api.c === -// ========================= -// evse_settings_api.c -// ========================= -#include "evse_settings_api.h" -#include "evse_api.h" -#include "evse_config.h" -#include "esp_log.h" -#include "cJSON.h" - -static const char *TAG = "evse_settings_api"; - -static esp_err_t config_settings_get_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); - cJSON *config = cJSON_CreateObject(); - cJSON_AddNumberToObject(config, "currentLimit", evse_get_max_charging_current()); - cJSON_AddNumberToObject(config, "temperatureLimit", evse_get_temp_threshold()); - const char *json_str = cJSON_Print(config); - httpd_resp_sendstr(req, json_str); - free((void *)json_str); - cJSON_Delete(config); - return ESP_OK; -} - -static esp_err_t config_settings_post_handler(httpd_req_t *req) { - char buf[512]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); - return ESP_FAIL; - } - buf[len] = '\0'; - cJSON *json = cJSON_Parse(buf); - if (!json) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); - return ESP_FAIL; - } - - cJSON *current = cJSON_GetObjectItem(json, "currentLimit"); - if (current) evse_set_max_charging_current(current->valueint); - cJSON *temp = cJSON_GetObjectItem(json, "temperatureLimit"); - if (temp) evse_set_temp_threshold(temp->valueint); - - cJSON_Delete(json); - httpd_resp_sendstr(req, "Configurações atualizadas com sucesso"); - return ESP_OK; -} - -void register_evse_settings_handlers(httpd_handle_t server, void *ctx) { - httpd_uri_t get_uri = { - .uri = "/api/v1/config/settings", - .method = HTTP_GET, - .handler = config_settings_get_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &get_uri); - - httpd_uri_t post_uri = { - .uri = "/api/v1/config/settings", - .method = HTTP_POST, - .handler = config_settings_post_handler, - .user_ctx = ctx - }; - httpd_register_uri_handler(server, &post_uri); -} - -// === Fim de: components/rest_api/src/evse_settings_api.c === - - -// === Início de: components/rest_api/include/dashboard_api.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_http_server.h" - -/** - * @brief Registra o handler da dashboard (status geral do sistema) - */ -void register_dashboard_handlers(httpd_handle_t server, void *ctx); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/rest_api/include/dashboard_api.h === - - -// === Início de: components/rest_api/include/static_file_api.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_http_server.h" - -/** - * @brief Registra o handler para servir arquivos estáticos da web (SPA) - */ -void register_static_file_handlers(httpd_handle_t server, void *ctx); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/rest_api/include/static_file_api.h === - - -// === Início de: components/rest_api/include/network_api.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_http_server.h" - -/** - * @brief Registra os handlers de configuração Wi-Fi e MQTT - */ -void register_network_handlers(httpd_handle_t server, void *ctx); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/rest_api/include/network_api.h === - - -// === Início de: components/rest_api/include/auth_api.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_http_server.h" - -/** - * @brief Registra os handlers de autenticação e gerenciamento de usuários - */ -void register_auth_handlers(httpd_handle_t server, void *ctx); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/rest_api/include/auth_api.h === - - -// === Início de: components/rest_api/include/loadbalancing_settings_api.h === -// ========================= -// loadbalancing_settings_api.h -// ========================= - -#ifndef LOADBALANCING_SETTINGS_API_H -#define LOADBALANCING_SETTINGS_API_H - -#include "esp_err.h" -#include "esp_http_server.h" - -// Função para registrar os manipuladores de URI para as configurações de load balancing e solar -void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx); - -#endif // LOADBALANCING_SETTINGS_API_H - -// === Fim de: components/rest_api/include/loadbalancing_settings_api.h === - - -// === Início de: components/rest_api/include/rest_main.h === -#pragma once - -#include -#include - -#define SCRATCH_BUFSIZE (10240) - -typedef struct rest_server_context { - char base_path[ESP_VFS_PATH_MAX + 1]; - char scratch[SCRATCH_BUFSIZE]; -} rest_server_context_t; - -esp_err_t rest_server_init(const char *base_path); - -// === Fim de: components/rest_api/include/rest_main.h === - - -// === Início de: components/rest_api/include/meters_settings_api.h === -// ========================= -// meters_settings_api.h -// ========================= - -#ifndef METERS_SETTINGS_API_H -#define METERS_SETTINGS_API_H - -#include "esp_err.h" -#include "esp_http_server.h" - -// Função para registrar os manipuladores de URI para as configurações dos contadores -void register_meters_settings_handlers(httpd_handle_t server, void *ctx); - -#endif // METERS_SETTINGS_API_H - -// === Fim de: components/rest_api/include/meters_settings_api.h === - - -// === Início de: components/rest_api/include/ocpp_api.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_http_server.h" - -/** - * @brief Registra os handlers da configuração e status do OCPP - */ -void register_ocpp_handlers(httpd_handle_t server, void *ctx); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/rest_api/include/ocpp_api.h === - - -// === Início de: components/rest_api/include/evse_settings_api.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_http_server.h" - -/** - * @brief Registra os handlers de configuração elétrica e limites de carregamento - */ -void register_evse_settings_handlers(httpd_handle_t server, void *ctx); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/rest_api/include/evse_settings_api.h === - - -// === Início de: components/auth/src/auth_events.c === -#include "auth_events.h" - -ESP_EVENT_DEFINE_BASE(AUTH_EVENTS); - -// === Fim de: components/auth/src/auth_events.c === - - -// === Início de: components/auth/src/wiegand.c === -/** - * @file wiegand.c - * - * ESP-IDF Wiegand protocol receiver - */ -#include -#include +// === Início de: components/evse/evse_pilot.c === +#include +#include #include -#include -#include "wiegand.h" - -static const char *TAG = "wiegand"; - -#define TIMER_INTERVAL_US 50000 // 50ms - -#define CHECK(x) \ - do \ - { \ - esp_err_t __; \ - if ((__ = x) != ESP_OK) \ - return __; \ - } while (0) -#define CHECK_ARG(VAL) \ - do \ - { \ - if (!(VAL)) \ - return ESP_ERR_INVALID_ARG; \ - } while (0) - -static void isr_disable(wiegand_reader_t *reader) -{ - gpio_set_intr_type(reader->gpio_d0, GPIO_INTR_DISABLE); - gpio_set_intr_type(reader->gpio_d1, GPIO_INTR_DISABLE); -} - -static void isr_enable(wiegand_reader_t *reader) -{ - gpio_set_intr_type(reader->gpio_d0, GPIO_INTR_NEGEDGE); - gpio_set_intr_type(reader->gpio_d1, GPIO_INTR_NEGEDGE); -} - -#if HELPER_TARGET_IS_ESP32 -static void IRAM_ATTR isr_handler(void *arg) -#else -static void isr_handler(void *arg) -#endif -{ - wiegand_reader_t *reader = (wiegand_reader_t *)arg; - if (!reader->enabled) - return; - - int d0 = gpio_get_level(reader->gpio_d0); - int d1 = gpio_get_level(reader->gpio_d1); - - // ignore equal - if (d0 == d1) - return; - // overflow - if (reader->bits >= reader->size * 8) - return; - - esp_timer_stop(reader->timer); - - uint8_t value; - if (reader->bit_order == WIEGAND_MSB_FIRST) - value = (d0 ? 0x80 : 0) >> (reader->bits % 8); - else - value = (d0 ? 1 : 0) << (reader->bits % 8); - - if (reader->byte_order == WIEGAND_MSB_FIRST) - reader->buf[reader->size - reader->bits / 8 - 1] |= value; - else - reader->buf[reader->bits / 8] |= value; - - reader->bits++; - - esp_timer_start_once(reader->timer, TIMER_INTERVAL_US); -} - -static void timer_handler(void *arg) -{ - wiegand_reader_t *reader = (wiegand_reader_t *)arg; - - ESP_LOGI(TAG, "Got %d bits of data", reader->bits); - - wiegand_reader_disable(reader); - - if (reader->callback) - reader->callback(reader); - - wiegand_reader_enable(reader); - - isr_enable(reader); -} - -//////////////////////////////////////////////////////////////////////////////// - -esp_err_t wiegand_reader_init(wiegand_reader_t *reader, gpio_num_t gpio_d0, gpio_num_t gpio_d1, - bool internal_pullups, size_t buf_size, wiegand_callback_t callback, wiegand_order_t bit_order, - wiegand_order_t byte_order) -{ - CHECK_ARG(reader && buf_size && callback); - - /* - esp_err_t res = gpio_install_isr_service(0); - if (res != ESP_OK && res != ESP_ERR_INVALID_STATE) - return res; - */ - - memset(reader, 0, sizeof(wiegand_reader_t)); - reader->gpio_d0 = gpio_d0; - reader->gpio_d1 = gpio_d1; - reader->size = buf_size; - reader->buf = calloc(buf_size, 1); - reader->bit_order = bit_order; - reader->byte_order = byte_order; - reader->callback = callback; - - esp_timer_create_args_t timer_args = { - .name = TAG, - .arg = reader, - .callback = timer_handler, - .dispatch_method = ESP_TIMER_TASK}; - CHECK(esp_timer_create(&timer_args, &reader->timer)); - - CHECK(gpio_set_direction(gpio_d0, GPIO_MODE_INPUT)); - CHECK(gpio_set_direction(gpio_d1, GPIO_MODE_INPUT)); - CHECK(gpio_set_pull_mode(gpio_d0, internal_pullups ? GPIO_PULLUP_ONLY : GPIO_FLOATING)); - CHECK(gpio_set_pull_mode(gpio_d1, internal_pullups ? GPIO_PULLUP_ONLY : GPIO_FLOATING)); - isr_disable(reader); - CHECK(gpio_isr_handler_add(gpio_d0, isr_handler, reader)); - CHECK(gpio_isr_handler_add(gpio_d1, isr_handler, reader)); - isr_enable(reader); - reader->enabled = true; - - ESP_LOGI(TAG, "Reader initialized on D0=%d, D1=%d", gpio_d0, gpio_d1); - - return ESP_OK; -} - -esp_err_t wiegand_reader_disable(wiegand_reader_t *reader) -{ - CHECK_ARG(reader); - - isr_disable(reader); - esp_timer_stop(reader->timer); - reader->enabled = false; - - ESP_LOGI(TAG, "Reader on D0=%d, D1=%d disabled", reader->gpio_d0, reader->gpio_d1); - - return ESP_OK; -} - -esp_err_t wiegand_reader_enable(wiegand_reader_t *reader) -{ - CHECK_ARG(reader); - - reader->bits = 0; - memset(reader->buf, 0, reader->size); - - isr_enable(reader); - reader->enabled = true; - - ESP_LOGI(TAG, "Reader on D0=%d, D1=%d enabled", reader->gpio_d0, reader->gpio_d1); - - return ESP_OK; -} - -esp_err_t wiegand_reader_done(wiegand_reader_t *reader) -{ - CHECK_ARG(reader && reader->buf); - - isr_disable(reader); - CHECK(gpio_isr_handler_remove(reader->gpio_d0)); - CHECK(gpio_isr_handler_remove(reader->gpio_d1)); - esp_timer_stop(reader->timer); - CHECK(esp_timer_delete(reader->timer)); - free(reader->buf); - - ESP_LOGI(TAG, "Reader removed"); - - return ESP_OK; -} - -// === Fim de: components/auth/src/wiegand.c === - - -// === Início de: components/auth/src/auth.c === -/* - * auth.c - */ - -#include "auth.h" -#include "auth_events.h" -#include "esp_event.h" -#include -#include -#include -#include +#include #include -#include "wiegand_reader.h" -#include "nvs_flash.h" -#include "nvs.h" -#define MAX_TAGS 50 +#include "driver/ledc.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_rom_sys.h" -static const char *TAG = "Auth"; +#include "evse_pilot.h" +#include "adc121s021_dma.h" +#include "board_config.h" -static bool enabled = false; -static char valid_tags[MAX_TAGS][AUTH_TAG_MAX_LEN]; -static int tag_count = 0; +#define PILOT_PWM_TIMER LEDC_TIMER_0 +#define PILOT_PWM_CHANNEL LEDC_CHANNEL_0 +#define PILOT_PWM_SPEED_MODE LEDC_LOW_SPEED_MODE +#define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT +#define PILOT_PWM_MAX_DUTY 1023 -// =========================== -// Persistência em NVS -// =========================== +#define NUM_PILOT_SAMPLES 100 +#define MAX_SAMPLE_ATTEMPTS 1000 +#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior -static void load_auth_config(void) { - nvs_handle_t handle; - esp_err_t err = nvs_open("auth", NVS_READONLY, &handle); - if (err == ESP_OK) { - uint8_t val; - if (nvs_get_u8(handle, "enabled", &val) == ESP_OK) { - enabled = val; - ESP_LOGI(TAG, "Loaded auth enabled = %d", enabled); - } - nvs_close(handle); +// ADC121S021 setup +#define ADC121_VREF_MV 3300 // AJUSTE conforme Vref do seu hardware! +#define ADC121_MAX 4095 // 12 bits + +static const char *TAG = "evse_pilot"; + +// Memoização de estado para evitar comandos/logs desnecessários +static int last_pilot_level = -1; +static uint32_t last_pwm_duty = 0; + +// Função para converter leitura bruta do ADC para mV +static int adc_raw_to_mv(uint16_t raw) { + return (raw * ADC121_VREF_MV) / ADC121_MAX; +} + +void pilot_init(void) +{ + // PWM (LEDC) configuração + ledc_timer_config_t ledc_timer = { + .speed_mode = PILOT_PWM_SPEED_MODE, + .timer_num = PILOT_PWM_TIMER, + .duty_resolution = PILOT_PWM_DUTY_RES, + .freq_hz = 1000, + .clk_cfg = LEDC_AUTO_CLK + }; + ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); + + ledc_channel_config_t ledc_channel = { + .speed_mode = PILOT_PWM_SPEED_MODE, + .channel = PILOT_PWM_CHANNEL, + .timer_sel = PILOT_PWM_TIMER, + .intr_type = LEDC_INTR_DISABLE, + .gpio_num = board_config.pilot_pwm_gpio, + .duty = 0, + .hpoint = 0 + }; + ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); + ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0)); + ESP_ERROR_CHECK(ledc_fade_func_install(0)); + + // Inicializa ADC121S021 externo + adc121s021_dma_init(); +} + +void pilot_set_level(bool level) +{ + if (last_pilot_level == level) return; // só muda se necessário + last_pilot_level = level; + + ESP_LOGI(TAG, "Set level %d", level); + ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, level ? 1 : 0); + last_pwm_duty = 0; // PWM parado +} + +void pilot_set_amps(uint16_t amps) +{ + if (amps < 6 || amps > 80) { + ESP_LOGE(TAG, "Invalid ampere value: %d A (valid: 6–80 A)", amps); + return; + } + + uint32_t duty_percent; + + if (amps <= 51) { + duty_percent = (amps * 10) / 6; // Duty (%) = Amps / 0.6 } else { - ESP_LOGW(TAG, "No stored auth config found. Using default."); + duty_percent = (amps * 10) / 25 + 64; // Duty (%) = (Amps / 2.5) + 64 } + + if (duty_percent > 100) duty_percent = 100; + + uint32_t duty = (PILOT_PWM_MAX_DUTY * duty_percent) / 100; + + if (last_pilot_level == 0 && last_pwm_duty == duty) return; + last_pilot_level = 0; + last_pwm_duty = duty; + + ESP_LOGI(TAG, "Pilot set: %d A → %d/%d (≈ %d%% duty)", + amps, (int)duty, PILOT_PWM_MAX_DUTY, (int)duty_percent); + + ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty); + ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL); } -static void save_auth_config(void) { - nvs_handle_t handle; - if (nvs_open("auth", NVS_READWRITE, &handle) == ESP_OK) { - nvs_set_u8(handle, "enabled", enabled); - nvs_commit(handle); - nvs_close(handle); - ESP_LOGI(TAG, "Auth config saved: enabled = %d", enabled); - } else { - ESP_LOGE(TAG, "Failed to save auth config."); - } + + +static int compare_int(const void *a, const void *b) { + return (*(int *)a - *(int *)b); } -// =========================== -// Internos -// =========================== +static int select_low_median_qsort(int *src, int n, int percent) { + int k = (n * percent) / 100; + if (k == 0) k = 1; + int *copy = alloca(n * sizeof(int)); + memcpy(copy, src, n * sizeof(int)); + qsort(copy, n, sizeof(int), compare_int); + return copy[k / 2]; +} -static bool is_tag_valid(const char *tag) { - for (int i = 0; i < tag_count; i++) { - if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0) { - return true; +static int select_high_median_qsort(int *src, int n, int percent) { + int k = (n * percent) / 100; + if (k == 0) k = 1; + int *copy = alloca(n * sizeof(int)); + memcpy(copy, src, n * sizeof(int)); + qsort(copy, n, sizeof(int), compare_int); + return copy[n - k + (k / 2)]; +} + +void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12) +{ + ESP_LOGD(TAG, "pilot_measure"); + + int samples[NUM_PILOT_SAMPLES]; + int collected = 0, attempts = 0; + uint16_t adc_sample = 0; + + // Lê samples usando ADC121S021 externo + while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS) { + adc_sample = 0; + if (adc121s021_dma_get_sample(&adc_sample)) { + samples[collected++] = adc_sample; + esp_rom_delay_us(10); + } else { + esp_rom_delay_us(100); + attempts++; } } - return true; - //TODO - //return false; -} -// =========================== -// API pública -// =========================== - -void auth_set_enabled(bool value) { - enabled = value; - save_auth_config(); - ESP_LOGI(TAG, "Auth %s", enabled ? "ENABLED" : "DISABLED"); - - auth_enabled_event_data_t event = { .enabled = enabled }; - esp_event_post(AUTH_EVENTS, AUTH_EVENT_ENABLED_CHANGED, &event, sizeof(event), portMAX_DELAY); -} - -bool auth_is_enabled(void) { - return enabled; -} - -bool auth_add_tag(const char *tag) { - if (tag_count >= MAX_TAGS) return false; - if (!tag || strlen(tag) >= AUTH_TAG_MAX_LEN) return false; - if (is_tag_valid(tag)) return true; - - strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1); - valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0'; - tag_count++; - ESP_LOGI(TAG, "Tag added: %s", tag); - return true; -} - -bool auth_remove_tag(const char *tag) { - for (int i = 0; i < tag_count; i++) { - if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0) { - for (int j = i; j < tag_count - 1; j++) { - strncpy(valid_tags[j], valid_tags[j + 1], AUTH_TAG_MAX_LEN); - } - tag_count--; - ESP_LOGI(TAG, "Tag removed: %s", tag); - return true; - } + if (collected < NUM_PILOT_SAMPLES) { + ESP_LOGW(TAG, "Timeout on sample read (%d/%d)", collected, NUM_PILOT_SAMPLES); + *up_voltage = PILOT_VOLTAGE_1; + *down_voltage_n12 = false; + return; } + + int high_raw = select_high_median_qsort(samples, collected, PILOT_EXTREME_PERCENT); + int low_raw = select_low_median_qsort(samples, collected, PILOT_EXTREME_PERCENT); + + int high_mv = adc_raw_to_mv(high_raw); + int low_mv = adc_raw_to_mv(low_raw); + + // Aplica thresholds definidos em board_config (em mV) + if (high_mv >= board_config.pilot_down_threshold_12) + *up_voltage = PILOT_VOLTAGE_12; + else if (high_mv >= board_config.pilot_down_threshold_9) + *up_voltage = PILOT_VOLTAGE_9; + else if (high_mv >= board_config.pilot_down_threshold_6) + *up_voltage = PILOT_VOLTAGE_6; + else if (high_mv >= board_config.pilot_down_threshold_3) + *up_voltage = PILOT_VOLTAGE_3; + else + *up_voltage = PILOT_VOLTAGE_1; + + *down_voltage_n12 = (low_mv <= board_config.pilot_down_threshold_n12); + + ESP_LOGD(TAG, "Final: up_voltage=%d, down_voltage_n12=%d", *up_voltage, *down_voltage_n12); +} + +// === Fim de: components/evse/evse_pilot.c === + + +// === Início de: components/evse/evse_hardware.c === +#include "evse_hardware.h" +#include "evse_pilot.h" +#include "ac_relay.h" +#include "socket_lock.h" +#include "proximity.h" + +static const char *TAG = "evse_hardware"; + +void evse_hardware_init(void) { + pilot_init(); + pilot_set_level(true); // Sinal piloto em 12V (inicial) + ac_relay_set_state(false); // Relé desligado + //socket_lock_set_locked(false); // Destrava o conector +} + +void evse_hardware_tick(void) { + // Tick para atualizações de hardware com polling, se necessário +} + +bool evse_hardware_is_pilot_high(void) { + return pilot_get_state(); // true se sinal piloto estiver em nível alto +} + +bool evse_hardware_is_vehicle_connected(void) { + // Verifica se o veículo está conectado pelo resistor do pino PP + return proximity_get_max_current() > 0; +} + +bool evse_hardware_is_energy_detected(void) { return false; } -bool auth_tag_exists(const char *tag) { - return is_tag_valid(tag); +void evse_hardware_relay_on(void) { + ac_relay_set_state(true); } -void auth_list_tags(void) { - ESP_LOGI(TAG, "Registered Tags (%d):", tag_count); - for (int i = 0; i < tag_count; i++) { - ESP_LOGI(TAG, "- %s", valid_tags[i]); - } +void evse_hardware_relay_off(void) { + ac_relay_set_state(false); } -void auth_init(void) { - load_auth_config(); // carrega estado de ativação - - if (enabled) { - initWiegand(); // só inicia se estiver habilitado - ESP_LOGI(TAG, "Wiegand reader initialized (Auth enabled)"); - } else { - ESP_LOGI(TAG, "Auth disabled, Wiegand reader not started"); - } - - auth_enabled_event_data_t evt = { .enabled = enabled }; - esp_event_post(AUTH_EVENTS, AUTH_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); - - ESP_LOGI(TAG, "Estado inicial AUTH enviado (enabled = %d)", enabled); +bool evse_hardware_relay_status(void) { + return ac_relay_get_state(); } -void auth_process_tag(const char *tag) { - if (!tag || !auth_is_enabled()) { - ESP_LOGW(TAG, "Auth disabled or NULL tag received."); - return; - } - - auth_tag_event_data_t event; - strncpy(event.tag, tag, AUTH_EVENT_TAG_MAX_LEN - 1); - event.tag[AUTH_EVENT_TAG_MAX_LEN - 1] = '\0'; - event.authorized = is_tag_valid(tag); - - ESP_LOGI(TAG, "Tag %s: %s", tag, event.authorized ? "AUTHORIZED" : "DENIED"); - - esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, &event, sizeof(event), portMAX_DELAY); +void evse_hardware_lock(void) { + socket_lock_set_locked(true); } -// === Fim de: components/auth/src/auth.c === +void evse_hardware_unlock(void) { + socket_lock_set_locked(false); +} + +bool evse_hardware_is_locked(void) { + return socket_lock_is_locked_state(); +} + +// === Fim de: components/evse/evse_hardware.c === -// === Início de: components/auth/src/wiegand_reader.c === -#include +// === Início de: components/evse/evse_meter.c === +#include "evse_meter.h" +#include "meter_events.h" +#include "esp_event.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include -#include -#include -#include -#include -#include -#include "auth.h" +#include -#define CONFIG_EXAMPLE_BUF_SIZE 50 - -static const char *TAG = "WiegandReader"; - -static wiegand_reader_t reader; -static QueueHandle_t queue = NULL; +static const char *TAG = "evse_meter"; +static SemaphoreHandle_t meter_mutex; typedef struct { - uint8_t data[CONFIG_EXAMPLE_BUF_SIZE]; - size_t bits; -} data_packet_t; + uint32_t power_watts[EVSE_METER_PHASE_COUNT]; + float voltage[EVSE_METER_PHASE_COUNT]; + float current[EVSE_METER_PHASE_COUNT]; + uint32_t energy_wh; +} evse_meter_data_t; -static void reader_callback(wiegand_reader_t *r) { - data_packet_t p; - p.bits = r->bits; - memcpy(p.data, r->buf, CONFIG_EXAMPLE_BUF_SIZE); - xQueueSendToBack(queue, &p, 0); -} +static evse_meter_data_t meter_data; -static void wiegand_task(void *arg) { - queue = xQueueCreate(5, sizeof(data_packet_t)); - if (!queue) { - ESP_LOGE(TAG, "Failed to create queue"); - vTaskDelete(NULL); - return; - } - - ESP_ERROR_CHECK(wiegand_reader_init(&reader, 21, 22, - true, CONFIG_EXAMPLE_BUF_SIZE, reader_callback, WIEGAND_MSB_FIRST, WIEGAND_LSB_FIRST)); - - data_packet_t p; - while (1) { - ESP_LOGI(TAG, "Waiting for Wiegand data..."); - if (xQueueReceive(queue, &p, portMAX_DELAY) == pdPASS) { - ESP_LOGI(TAG, "Bits received: %d", p.bits); - - char tag[20] = {0}; - - if (p.bits == 26) { - snprintf(tag, sizeof(tag), "%03d%03d%03d", p.data[0], p.data[1], p.data[2]); - } else if (p.bits == 34) { - snprintf(tag, sizeof(tag), "%03d%03d%03d%03d", p.data[0], p.data[1], p.data[2], p.data[3]); - } else { - ESP_LOGW(TAG, "Unsupported bit length: %d", (int)p.bits); - continue; - } - - ESP_LOGI(TAG, "Tag read: %s", tag); - auth_process_tag(tag); // agora delega toda a lógica à auth.c +static void on_meter_event_dispatcher(void* arg, esp_event_base_t base, int32_t id, void* data) { + if (base == METER_EVENT && id == METER_EVENT_DATA_READY && data) { + const meter_event_data_t *evt = (const meter_event_data_t *)data; + if (strcmp(evt->source, "EVSE") == 0) { + evse_meter_on_meter_event(arg, data); } } } -void initWiegand(void) { - ESP_LOGI(TAG, "Initializing Wiegand reader"); - xTaskCreate(wiegand_task, TAG, configMINIMAL_STACK_SIZE * 4, NULL, 4, NULL); +void evse_meter_on_meter_event(void* arg, void* event_data) { + const meter_event_data_t *evt = (const meter_event_data_t *)event_data; + if (!evt) return; + + xSemaphoreTake(meter_mutex, portMAX_DELAY); + for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) { + meter_data.power_watts[i] = evt->watt[i]; + meter_data.voltage[i] = evt->vrms[i]; + meter_data.current[i] = evt->irms[i]; + } + meter_data.energy_wh = (uint32_t)(evt->total_energy * 1000.0f); + xSemaphoreGive(meter_mutex); + + ESP_LOGI(TAG, + "Meter updated: power[W]={%" PRIu32 ",%" PRIu32 ",%" PRIu32 "}, " + "voltage[V]={%.2f,%.2f,%.2f}, " + "current[A]={%.2f,%.2f,%.2f}, " + "total_energy=%" PRIu32 "Wh", + meter_data.power_watts[0], meter_data.power_watts[1], meter_data.power_watts[2], + meter_data.voltage[0], meter_data.voltage[1], meter_data.voltage[2], + meter_data.current[0], meter_data.current[1], meter_data.current[2], + meter_data.energy_wh + ); } -// === Fim de: components/auth/src/wiegand_reader.c === +void evse_meter_init(void) { + meter_mutex = xSemaphoreCreateMutex(); + ESP_ERROR_CHECK(meter_mutex ? ESP_OK : ESP_FAIL); + ESP_ERROR_CHECK(esp_event_handler_register( + METER_EVENT, METER_EVENT_DATA_READY, + on_meter_event_dispatcher, NULL)); + memset(&meter_data, 0, sizeof(meter_data)); + ESP_LOGI(TAG, "EVSE Meter listener registered."); +} + +int evse_meter_get_instant_power(void) { + xSemaphoreTake(meter_mutex, portMAX_DELAY); + int sum = 0; + for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) { + sum += meter_data.power_watts[i]; + } + xSemaphoreGive(meter_mutex); + return sum; +} + +int evse_meter_get_total_energy(void) { + xSemaphoreTake(meter_mutex, portMAX_DELAY); + int val = meter_data.energy_wh; + xSemaphoreGive(meter_mutex); + return val; +} + +void evse_meter_get_power(int power[EVSE_METER_PHASE_COUNT]) { + xSemaphoreTake(meter_mutex, portMAX_DELAY); + for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) { + power[i] = meter_data.power_watts[i]; + } + xSemaphoreGive(meter_mutex); +} + +void evse_meter_get_voltage(float voltage[EVSE_METER_PHASE_COUNT]) { + xSemaphoreTake(meter_mutex, portMAX_DELAY); + for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) { + voltage[i] = meter_data.voltage[i]; + } + xSemaphoreGive(meter_mutex); +} + +void evse_meter_get_current(float current[EVSE_METER_PHASE_COUNT]) { + xSemaphoreTake(meter_mutex, portMAX_DELAY); + for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) { + current[i] = meter_data.current[i]; + } + xSemaphoreGive(meter_mutex); +} + +// === Fim de: components/evse/evse_meter.c === -// === Início de: components/auth/include/auth.h === -#ifndef AUTH_H -#define AUTH_H +// === Início de: components/evse/evse_session.c === +/* + * evse_session.c + * Implementation of evse_session module using instantaneous power accumulation + */ +#include // for PRIu32 +#include "evse_session.h" +#include "evse_meter.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" -#include -#include +static const char *TAG = "evse_session"; + +// Internal state +static TickType_t session_start_tick = 0; +static uint32_t watt_seconds = 0; // accumulator of W·s +static evse_session_t last_session; +static bool last_session_valid = false; + +void evse_session_init(void) { + session_start_tick = 0; + watt_seconds = 0; + last_session_valid = false; +} + +void evse_session_start(void) { + session_start_tick = xTaskGetTickCount(); + watt_seconds = 0; + ESP_LOGI(TAG, "Session started at tick %u", (unsigned)session_start_tick); +} + +void evse_session_end(void) { + if (session_start_tick == 0) { + ESP_LOGW(TAG, "evse_session_end called without active session"); + return; + } + TickType_t now = xTaskGetTickCount(); + uint32_t duration_s = (now - session_start_tick) / configTICK_RATE_HZ; + uint32_t energy_wh = watt_seconds / 3600U; + uint32_t avg_power = duration_s > 0 ? watt_seconds / duration_s : 0; + + last_session.start_tick = session_start_tick; + last_session.duration_s = duration_s; + last_session.energy_wh = energy_wh; + last_session.avg_power_w = avg_power; + last_session.is_current = false; + last_session_valid = true; + + session_start_tick = 0; + ESP_LOGI(TAG, "Session ended: duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg_power=%" PRIu32 " W", + (uint32_t)duration_s, (uint32_t)energy_wh, (uint32_t)avg_power); +} + +void evse_session_tick(void) { + if (session_start_tick == 0) return; + // Should be called every second (or known interval) + uint32_t power_w = evse_meter_get_instant_power(); + watt_seconds += power_w; +} + +bool evse_session_get(evse_session_t *out) { + if (out == NULL) return false; + + if (session_start_tick != 0) { + TickType_t now = xTaskGetTickCount(); + uint32_t duration_s = (now - session_start_tick) / configTICK_RATE_HZ; + uint32_t energy_wh = watt_seconds / 3600U; + uint32_t avg_power = duration_s > 0 ? watt_seconds / duration_s : 0; + + out->start_tick = session_start_tick; + out->duration_s = duration_s; + out->energy_wh = energy_wh; + out->avg_power_w = avg_power; + out->is_current = true; + return true; + } + + if (last_session_valid) { + *out = last_session; + return true; + } + + return false; +} + +// === Fim de: components/evse/evse_session.c === + + +// === Início de: components/evse/evse_state.c === +#include "evse_api.h" +#include "evse_state.h" +#include "evse_session.h" +#include "evse_events.h" +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +#include "esp_log.h" + +// ========================= +// Internal State Variables +// ========================= + +static evse_state_t current_state = EVSE_STATE_A; +static bool is_authorized = false; + +static portMUX_TYPE state_mux = portMUX_INITIALIZER_UNLOCKED; + +static const char *TAG = "evse_state"; + +// ========================= +// Internal Mapping +// ========================= + +static evse_state_event_t map_state_to_event(evse_state_t s) { + switch (s) { + case EVSE_STATE_A: return EVSE_STATE_EVENT_IDLE; + case EVSE_STATE_B1: + case EVSE_STATE_B2: return EVSE_STATE_EVENT_WAITING; + case EVSE_STATE_C1: + case EVSE_STATE_C2: return EVSE_STATE_EVENT_CHARGING; + case EVSE_STATE_E: + case EVSE_STATE_F: return EVSE_STATE_EVENT_FAULT; + default: return EVSE_STATE_EVENT_IDLE; + } +} + +// ========================= +// Public API +// ========================= + +void evse_set_state(evse_state_t new_state) { + bool changed = false; + evse_state_t prev_state; + bool start_session = false; + bool end_session = false; + + // 1) Detecta transição de estado dentro da região crítica + portENTER_CRITICAL(&state_mux); + prev_state = current_state; + if (new_state != current_state) { + // se entrou em charging pela primeira vez + if (evse_state_is_charging(new_state) && !evse_state_is_charging(prev_state)) { + start_session = true; + } + // se saiu de charging para qualquer outro + else if (!evse_state_is_charging(new_state) && evse_state_is_charging(prev_state)) { + end_session = true; + } + current_state = new_state; + changed = true; + } + portEXIT_CRITICAL(&state_mux); + + // 2) Executa start/end de sessão FORA da região crítica, evitando logs/alloc dentro dela + if (start_session) { + evse_session_start(); + } + if (end_session) { + evse_session_end(); + } + + // 3) Se mudou o estado, faz log e dispara evento + if (changed) { + const char *prev_str = evse_state_to_str(prev_state); + const char *curr_str = evse_state_to_str(new_state); + ESP_LOGI(TAG, "State changed: %s → %s", prev_str, curr_str); + + evse_state_event_data_t evt = { + .state = map_state_to_event(new_state) + }; + esp_event_post(EVSE_EVENTS, + EVSE_EVENT_STATE_CHANGED, + &evt, + sizeof(evt), + portMAX_DELAY); + } +} + + + +evse_state_t evse_get_state(void) { + portENTER_CRITICAL(&state_mux); + evse_state_t s = current_state; + portEXIT_CRITICAL(&state_mux); + return s; +} + +const char* evse_state_to_str(evse_state_t state) { + switch (state) { + case EVSE_STATE_A: return "A - EV Not Connected (12V)"; + case EVSE_STATE_B1: return "B1 - EV Connected (9V, Not Authorized)"; + case EVSE_STATE_B2: return "B2 - EV Connected (9V, Authorized and Ready)"; + case EVSE_STATE_C1: return "C1 - Charging Requested (6V, Relay Off)"; + case EVSE_STATE_C2: return "C2 - Charging Active (6V, Relay On)"; + case EVSE_STATE_D1: return "D1 - Ventilation Required (3V, Relay Off)"; + case EVSE_STATE_D2: return "D2 - Ventilation Active (3V, Relay On)"; + case EVSE_STATE_E: return "E - Error: Control Pilot Shorted to Ground (0V)"; + case EVSE_STATE_F: return "F - Fault: EVSE Unavailable or No Pilot Signal"; + default: return "Unknown State"; + } +} + +void evse_state_init(void) { + portENTER_CRITICAL(&state_mux); + current_state = EVSE_STATE_A; + is_authorized = true; + portEXIT_CRITICAL(&state_mux); + + ESP_LOGI("EVSE_STATE", "Initialized in state: %s", evse_state_to_str(current_state)); + + evse_state_event_data_t evt = { + .state = map_state_to_event(current_state) + }; + esp_event_post(EVSE_EVENTS, EVSE_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); +} + +void evse_state_tick(void) { + // Placeholder for future state logic +} + +bool evse_state_is_charging(evse_state_t state) { + return state == EVSE_STATE_C1 || state == EVSE_STATE_C2; +} + +bool evse_state_is_plugged(evse_state_t state) { + return state == EVSE_STATE_B1 || state == EVSE_STATE_B2 || + state == EVSE_STATE_C1 || state == EVSE_STATE_C2 || + state == EVSE_STATE_D1 || state == EVSE_STATE_D2; +} + +bool evse_state_is_session(evse_state_t state) { + return state == EVSE_STATE_B2 || state == EVSE_STATE_C1 || state == EVSE_STATE_C2; +} + +void evse_state_set_authorized(bool authorized) { + portENTER_CRITICAL(&state_mux); + is_authorized = authorized; + portEXIT_CRITICAL(&state_mux); +} + +bool evse_state_get_authorized(void) { + portENTER_CRITICAL(&state_mux); + bool result = is_authorized; + portEXIT_CRITICAL(&state_mux); + return result; +} + +// === Fim de: components/evse/evse_state.c === + + +// === Início de: components/evse/evse_fsm.c === +#include "evse_fsm.h" +#include "evse_api.h" +#include "evse_pilot.h" +#include "evse_config.h" +#include "esp_log.h" +#include "ac_relay.h" +#include "board_config.h" +#include "socket_lock.h" +#include "proximity.h" +#include "rcm.h" +#include "evse_state.h" +#include "evse_error.h" + +static const char *TAG = "evse_fsm"; + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +static bool c1_d1_waiting = false; +static TickType_t c1_d1_relay_to = 0; + +void evse_fsm_reset(void) { + evse_set_state(EVSE_STATE_A); + c1_d1_waiting = false; + c1_d1_relay_to = 0; +} + +// ... includes e defines como já estão + +static void update_outputs(evse_state_t state) { + const uint16_t current = evse_get_runtime_charging_current(); + uint8_t cable_max_current = evse_get_max_charging_current(); + const bool socket_outlet = evse_get_socket_outlet(); + + if (socket_outlet) { + cable_max_current = proximity_get_max_current(); + } + + // Segurança: relé sempre off e outputs seguros em caso de erro + if (evse_get_error() != 0) { + if (ac_relay_get_state()) { + ac_relay_set_state(false); + ESP_LOGW(TAG, "ERRO ativo: relé estava ligado, agora desligado por segurança!"); + } + ac_relay_set_state(false); // redundância tolerável + pilot_set_level(true); // sinal pilot sempre 12V (A) + if (board_config.socket_lock && socket_outlet) { + socket_lock_set_locked(false); + } + return; + } + + // Fluxo normal + switch (state) { + case EVSE_STATE_A: + case EVSE_STATE_E: + case EVSE_STATE_F: + ac_relay_set_state(false); + pilot_set_level(state == EVSE_STATE_A); + if (board_config.socket_lock && socket_outlet) { + socket_lock_set_locked(false); + } + break; + + case EVSE_STATE_B1: + pilot_set_level(true); + ac_relay_set_state(false); + if (board_config.socket_lock && socket_outlet) { + socket_lock_set_locked(true); + } + + if (rcm_test()) { + //ESP_LOGI(TAG, "RCM self test passed"); + } else { + //ESP_LOGW(TAG, "RCM self test failed"); + } + break; + + case EVSE_STATE_B2: + pilot_set_amps(MIN(current, cable_max_current)); + ac_relay_set_state(false); + break; + + case EVSE_STATE_C1: + case EVSE_STATE_D1: + pilot_set_level(true); + c1_d1_waiting = true; + c1_d1_relay_to = xTaskGetTickCount() + pdMS_TO_TICKS(6000); + break; + + case EVSE_STATE_C2: + case EVSE_STATE_D2: + pilot_set_amps(MIN(current, cable_max_current)); + ac_relay_set_state(true); // Só chega aqui se não há erro! + break; + } +} + +// FSM principal - centraliza a lógica de erro e de todos os estados +void evse_fsm_process( + pilot_voltage_t pilot_voltage, + bool authorized, + bool available, + bool enabled +) { + // Proteção total: erro força F sempre! + if (evse_get_error() != 0) { + if (evse_get_state() != EVSE_STATE_F) { + ESP_LOGW(TAG, "Erro ativo detectado: forçando estado FAULT (F)"); + evse_set_state(EVSE_STATE_F); + } + update_outputs(EVSE_STATE_F); + return; + } + + TickType_t now = xTaskGetTickCount(); + evse_state_t prev = evse_get_state(); + evse_state_t curr = prev; + + switch (curr) { + case EVSE_STATE_A: + if (!available) { + evse_set_state(EVSE_STATE_F); + } else if (pilot_voltage == PILOT_VOLTAGE_9) { + evse_set_state(EVSE_STATE_B1); + } + break; + + case EVSE_STATE_B1: + case EVSE_STATE_B2: + if (!available) { + evse_set_state(EVSE_STATE_F); + break; + } + switch (pilot_voltage) { + case PILOT_VOLTAGE_12: + evse_set_state(EVSE_STATE_A); + break; + case PILOT_VOLTAGE_9: + evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1); + break; + case PILOT_VOLTAGE_6: + evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1); + break; + default: + break; + } + break; + + case EVSE_STATE_C1: + case EVSE_STATE_D1: + if (c1_d1_waiting && now >= c1_d1_relay_to) { + ac_relay_set_state(false); + c1_d1_waiting = false; + if (!available) { + evse_set_state(EVSE_STATE_F); + break; + } + } + __attribute__((fallthrough)); + + case EVSE_STATE_C2: + case EVSE_STATE_D2: + if (!enabled || !available) { + evse_set_state((curr == EVSE_STATE_D2 || curr == EVSE_STATE_D1) + ? EVSE_STATE_D1 : EVSE_STATE_C1); + break; + } + switch (pilot_voltage) { + case PILOT_VOLTAGE_6: + evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1); + break; + case PILOT_VOLTAGE_3: + evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1); + break; + case PILOT_VOLTAGE_9: + evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1); + break; + case PILOT_VOLTAGE_12: + evse_set_state(EVSE_STATE_A); + break; + default: + break; + } + break; + + case EVSE_STATE_E: + // Estado elétrico grave: só reset manual + break; + + case EVSE_STATE_F: + // Fault: só sai se disponível e sem erro + if (available && evse_get_error() == 0) { + evse_set_state(EVSE_STATE_A); + } + break; + } + + evse_state_t next = evse_get_state(); + update_outputs(next); + + if (next != prev) { + ESP_LOGI(TAG, "State changed: %s -> %s", + evse_state_to_str(prev), + evse_state_to_str(next)); + } +} + +// === Fim de: components/evse/evse_fsm.c === + + +// === Início de: components/evse/evse_error.c === +#include "evse_error.h" +#include "evse_config.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "ntc_sensor.h" + +static const char *TAG = "evse_error"; + +static uint32_t error_bits = 0; +static TickType_t auto_clear_timeout = 0; +static bool error_cleared = false; + +void evse_error_init(void) { + // Inicialização do sistema de erros +} + +void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v) { + ESP_LOGD(TAG, "Verificando erro: pilot_voltage = %d, is_n12v = %s", + pilot_voltage, is_n12v ? "true" : "false"); + + // Falha elétrica geral no pilot + if (pilot_voltage == PILOT_VOLTAGE_1) { + if (!(error_bits & EVSE_ERR_PILOT_FAULT_BIT)) { // Verifica se o erro já foi registrado + evse_error_set(EVSE_ERR_PILOT_FAULT_BIT); + ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)"); + } + } + + // Falta de -12V durante PWM (C ou D) + if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v) { + if (!(error_bits & EVSE_ERR_DIODE_SHORT_BIT)) { // Verifica se o erro já foi registrado + evse_error_set(EVSE_ERR_DIODE_SHORT_BIT); + ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)"); + ESP_LOGW(TAG, "Verificando erro: pilot_voltage = %d, is_n12v = %s", pilot_voltage, is_n12v ? "true" : "false"); + } + } +} + +void evse_temperature_check(void) { + float temp_c = ntc_temp_sensor(); // leitura atual (última medida válida) + uint8_t threshold = evse_get_temp_threshold(); // padrão 60°C, configurável + + // Log informativo com os valores + ESP_LOGD(TAG, "Verificando temperatura: atual = %.2f °C, limite = %d °C", temp_c, threshold); + + // Se a temperatura parecer inválida, aplica erro de sensor + if (temp_c < -40.0f || temp_c > 150.0f) { + if (!(error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT)) { // Verifica se o erro já foi registrado + evse_error_set(EVSE_ERR_TEMPERATURE_FAULT_BIT); + ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado"); + } + return; + } + + evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT); // leitura válida + + if (temp_c >= threshold) { + if (!(error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT)) { // Verifica se o erro já foi registrado + evse_error_set(EVSE_ERR_TEMPERATURE_HIGH_BIT); + ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C", temp_c, threshold); + } + } else { + evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT); + } +} + +uint32_t evse_get_error(void) { + return error_bits; +} + +bool evse_is_error_cleared(void) { + return error_cleared; +} + +void evse_mark_error_cleared(void) { + error_cleared = false; +} + +// Já existentes +void evse_error_set(uint32_t bitmask) { + error_bits |= bitmask; + + if (bitmask & EVSE_ERR_AUTO_CLEAR_BITS) { + auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); // 60s + } +} + +void evse_error_clear(uint32_t bitmask) { + bool had_error = error_bits != 0; + error_bits &= ~bitmask; + + if (had_error && error_bits == 0) { + error_cleared = true; + } +} + +void evse_error_tick(void) { + if ((error_bits & EVSE_ERR_AUTO_CLEAR_BITS) && xTaskGetTickCount() >= auto_clear_timeout) { + evse_error_clear(EVSE_ERR_AUTO_CLEAR_BITS); + auto_clear_timeout = 0; + } +} + +bool evse_error_is_active(void) { + return error_bits != 0; +} + +uint32_t evse_error_get_bits(void) { + return error_bits; +} + +void evse_error_reset_flag(void) { + error_cleared = false; +} + +bool evse_error_cleared_flag(void) { + return error_cleared; +} + +// === Fim de: components/evse/evse_error.c === + + +// === Início de: components/evse/evse_core.c === +// evse_core.c - Main EVSE control logic + +#include "evse_fsm.h" +#include "evse_error.h" +#include "evse_limits.h" +#include "evse_config.h" +#include "evse_api.h" +#include "evse_session.h" +#include "evse_pilot.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "esp_log.h" + +static const char *TAG = "evse_core"; + +static SemaphoreHandle_t mutex; +static evse_state_t last_state = EVSE_STATE_A; + +static void evse_core_task(void *arg); + +// ================================ +// Initialization +// ================================ + +void evse_init(void) { + ESP_LOGI(TAG, "EVSE Init"); + + mutex = xSemaphoreCreateMutex(); // Optional: use static version for deterministic memory + + evse_check_defaults(); + evse_fsm_reset(); + pilot_set_level(true); // Enable pilot output + + xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 5, NULL); +} + +// ================================ +// Main Processing Logic +// ================================ + +void evse_process(void) { + xSemaphoreTake(mutex, portMAX_DELAY); + + pilot_voltage_t pilot_voltage; + bool is_n12v = false; + + pilot_measure(&pilot_voltage, &is_n12v); + ESP_LOGD(TAG, "Pilot: %d, -12V: %s", pilot_voltage, is_n12v ? "yes" : "no"); + + evse_error_check(pilot_voltage, is_n12v); + + // Só chama FSM, que decide tudo + evse_fsm_process( + pilot_voltage, + evse_state_get_authorized(), + evse_config_is_available(), + evse_config_is_enabled() + ); + + evse_limits_check(); + + evse_state_t current = evse_get_state(); + if (current != last_state) { + ESP_LOGI(TAG, "State changed: %s → %s", + evse_state_to_str(last_state), + evse_state_to_str(current)); + last_state = current; + } + + evse_mark_error_cleared(); + + xSemaphoreGive(mutex); +} + +// ================================ +// Background Task +// ================================ + +static void evse_core_task(void *arg) { + while (true) { + evse_process(); + vTaskDelay(pdMS_TO_TICKS(100)); // 10 Hz cycle + } +} +// === Fim de: components/evse/evse_core.c === + + +// === Início de: components/evse/evse_limits.c === +#include // for PRIu32 +#include "evse_state.h" +#include "evse_api.h" +#include "evse_limits.h" +#include "evse_meter.h" +#include "evse_session.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + + +// ======================== +// External state references +// ======================== + +//extern evse_state_t current_state; // Current EVSE FSM state +//extern TickType_t session_start_tick; // Timestamp of charging session start + +// ======================== +// Concurrency protection +// ======================== + +static portMUX_TYPE evse_mux = portMUX_INITIALIZER_UNLOCKED; + +// ======================== +// Runtime state (volatile) +// ======================== + +static bool limit_reached = false; +static uint32_t consumption_limit = 0; // Energy limit in Wh +static uint32_t charging_time_limit = 0; // Time limit in seconds +static uint16_t under_power_limit = 0; // Minimum acceptable power in W + +// ======================== +// Default (persistent) limits +// ======================== + +static uint32_t default_consumption_limit = 0; +static uint32_t default_charging_time_limit = 0; +static uint16_t default_under_power_limit = 0; + +// ======================== +// Limit status flag +// ======================== + +bool evse_get_limit_reached(void) { + bool val; + portENTER_CRITICAL(&evse_mux); + val = limit_reached; + portEXIT_CRITICAL(&evse_mux); + return val; +} + +void evse_set_limit_reached(bool v) { + portENTER_CRITICAL(&evse_mux); + limit_reached = v; + portEXIT_CRITICAL(&evse_mux); +} + +// ======================== +// Runtime limit accessors +// ======================== + +uint32_t evse_get_consumption_limit(void) { + uint32_t val; + portENTER_CRITICAL(&evse_mux); + val = consumption_limit; + portEXIT_CRITICAL(&evse_mux); + return val; +} + +void evse_set_consumption_limit(uint32_t value) { + portENTER_CRITICAL(&evse_mux); + consumption_limit = value; + portEXIT_CRITICAL(&evse_mux); +} + +uint32_t evse_get_charging_time_limit(void) { + uint32_t val; + portENTER_CRITICAL(&evse_mux); + val = charging_time_limit; + portEXIT_CRITICAL(&evse_mux); + return val; +} + +void evse_set_charging_time_limit(uint32_t value) { + portENTER_CRITICAL(&evse_mux); + charging_time_limit = value; + portEXIT_CRITICAL(&evse_mux); +} + +uint16_t evse_get_under_power_limit(void) { + uint16_t val; + portENTER_CRITICAL(&evse_mux); + val = under_power_limit; + portEXIT_CRITICAL(&evse_mux); + return val; +} + +void evse_set_under_power_limit(uint16_t value) { + portENTER_CRITICAL(&evse_mux); + under_power_limit = value; + portEXIT_CRITICAL(&evse_mux); +} + +// ======================== +// Default (persistent) limit accessors +// These values can be stored/restored via NVS +// ======================== + +uint32_t evse_get_default_consumption_limit(void) { + return default_consumption_limit; +} + +void evse_set_default_consumption_limit(uint32_t value) { + default_consumption_limit = value; +} + +uint32_t evse_get_default_charging_time_limit(void) { + return default_charging_time_limit; +} + +void evse_set_default_charging_time_limit(uint32_t value) { + default_charging_time_limit = value; +} + +uint16_t evse_get_default_under_power_limit(void) { + return default_under_power_limit; +} + +void evse_set_default_under_power_limit(uint16_t value) { + default_under_power_limit = value; +} + +bool evse_is_limit_reached(void) { + return evse_get_limit_reached(); +} + +// ======================== +// Limit checking logic +// This function must be called periodically while charging. +// It will flag the session as "limit reached" when thresholds are violated. +// ======================== +void evse_limits_check(void) { + // Only check during an active charging session + if (!evse_state_is_charging(evse_get_state())) { + return; + } + + evse_session_t sess; + // Retrieve accumulated data for the current session + if (!evse_session_get(&sess) || !sess.is_current) { + // If there's no active session, abort + return; + } + + bool reached = false; + + // 1) Energy consumption limit (Wh) + if (consumption_limit > 0 && sess.energy_wh >= consumption_limit) { + ESP_LOGW("EVSE_LIMITS", + "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh", + sess.energy_wh, consumption_limit); + reached = true; + } + + // 2) Charging time limit (seconds) + if (charging_time_limit > 0 && sess.duration_s >= charging_time_limit) { + ESP_LOGW("EVSE_LIMITS", + "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s", + sess.duration_s, charging_time_limit); + reached = true; + } + + // 3) Under-power limit (instantaneous power) + uint32_t inst_power = evse_meter_get_instant_power(); + if (under_power_limit > 0 && inst_power < under_power_limit) { + ESP_LOGW("EVSE_LIMITS", + "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W", + (uint32_t)inst_power, + (uint32_t)under_power_limit); + reached = true; + } + + if (reached) { + evse_set_limit_reached(true); + } +} +// === Fim de: components/evse/evse_limits.c === + + +// === Início de: components/evse/evse_api.c === +// evse_api.c - Main EVSE control logic + +#include "evse_fsm.h" +#include "evse_error.h" +#include "evse_limits.h" +#include "evse_config.h" +#include "evse_api.h" +#include "evse_session.h" +#include "evse_pilot.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "esp_log.h" + +static const char *TAG = "evse_api"; + + +// ================================ +// Public Configuration Interface +// ================================ + +void evse_set_enabled(bool value) { + ESP_LOGI(TAG, "Set enabled %d", value); + evse_config_set_enabled(value); +} + +bool evse_is_available(void) { + return evse_config_is_available(); +} + +void evse_set_available(bool value) { + ESP_LOGI(TAG, "Set available %d", value); + evse_config_set_available(value); +} + +bool evse_get_session(evse_session_t *out) { + return evse_session_get(out); +} +// === Fim de: components/evse/evse_api.c === + + +// === Início de: components/evse/evse_config.c === +#include // For PRI macros +#include "evse_config.h" +#include "board_config.h" +#include "evse_limits.h" +#include "esp_log.h" +#include "nvs.h" +#include "esp_timer.h" + +static const char *TAG = "evse_config"; + +static nvs_handle_t nvs; + +// ======================== +// Configurable parameters +// ======================== +static uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT; +static uint16_t charging_current; // Persisted (NVS) +static uint16_t charging_current_runtime = 0; // Runtime only +static bool socket_outlet; +static bool rcm; +static uint8_t temp_threshold = 60; +static bool require_auth; + +// ======================== +// Initialization +// ======================== +esp_err_t evse_config_init(void) { + ESP_LOGD(TAG, "Initializing NVS configuration..."); + return nvs_open("evse", NVS_READWRITE, &nvs); +} + +void evse_check_defaults(void) { + esp_err_t err; + uint8_t u8; + uint16_t u16; + uint32_t u32; + bool needs_commit = false; + + ESP_LOGD(TAG, "Checking default parameters..."); + + // Max charging current + err = nvs_get_u8(nvs, "max_chrg_curr", &u8); + if (err != ESP_OK || u8 < MIN_CHARGING_CURRENT_LIMIT || u8 > MAX_CHARGING_CURRENT_LIMIT) { + max_charging_current = MAX_CHARGING_CURRENT_LIMIT; + nvs_set_u8(nvs, "max_chrg_curr", max_charging_current); + needs_commit = true; + ESP_LOGW(TAG, "Invalid or missing max_chrg_curr, resetting to %d", max_charging_current); + } else { + max_charging_current = u8; + } + + // Charging current (default, persisted) + err = nvs_get_u16(nvs, "def_chrg_curr", &u16); + if (err != ESP_OK || u16 < (MIN_CHARGING_CURRENT_LIMIT) || u16 > (max_charging_current)) { + charging_current = max_charging_current; + nvs_set_u16(nvs, "def_chrg_curr", charging_current); + needs_commit = true; + ESP_LOGW(TAG, "Invalid or missing def_chrg_curr, resetting to %d", charging_current); + } else { + charging_current = u16; + } + + // Runtime charging current initialized from persisted default + charging_current_runtime = max_charging_current; + ESP_LOGD(TAG, "Runtime charging current initialized to: %d", charging_current_runtime); + + // Auth required + err = nvs_get_u8(nvs, "require_auth", &u8); + require_auth = (err == ESP_OK && u8 <= 1) ? u8 : false; + if (err != ESP_OK) { + nvs_set_u8(nvs, "require_auth", require_auth); + needs_commit = true; + } + + // Socket outlet + err = nvs_get_u8(nvs, "socket_outlet", &u8); + socket_outlet = (err == ESP_OK && u8) && board_config.proximity; + if (err != ESP_OK) { + nvs_set_u8(nvs, "socket_outlet", socket_outlet); + needs_commit = true; + } + + // RCM + err = nvs_get_u8(nvs, "rcm", &u8); + rcm = (err == ESP_OK && u8) && board_config.rcm; + if (err != ESP_OK) { + nvs_set_u8(nvs, "rcm", rcm); + needs_commit = true; + } + + // Temp threshold + err = nvs_get_u8(nvs, "temp_threshold", &u8); + temp_threshold = (err == ESP_OK && u8 >= 40 && u8 <= 80) ? u8 : 60; + if (err != ESP_OK) { + nvs_set_u8(nvs, "temp_threshold", temp_threshold); + needs_commit = true; + } + + // Optional limits + if (nvs_get_u32(nvs, "def_cons_lim", &u32) == ESP_OK) + evse_set_consumption_limit(u32); + + if (nvs_get_u32(nvs, "def_ch_time_lim", &u32) == ESP_OK) + evse_set_charging_time_limit(u32); + + if (nvs_get_u16(nvs, "def_un_pwr_lim", &u16) == ESP_OK) + evse_set_under_power_limit(u16); + + // Save to NVS if needed + if (needs_commit) { + err = nvs_commit(nvs); + if (err == ESP_OK) { + ESP_LOGD(TAG, "Configuration committed to NVS."); + } else { + ESP_LOGE(TAG, "Failed to commit configuration to NVS: %s", esp_err_to_name(err)); + } + } +} + +// ======================== +// Charging current getters/setters +// ======================== +uint8_t evse_get_max_charging_current(void) { + return max_charging_current; +} + +esp_err_t evse_set_max_charging_current(uint8_t value) { + if (value < MIN_CHARGING_CURRENT_LIMIT || value > MAX_CHARGING_CURRENT_LIMIT) + return ESP_ERR_INVALID_ARG; + max_charging_current = value; + evse_set_runtime_charging_current(value); + nvs_set_u8(nvs, "max_chrg_curr", value); + return nvs_commit(nvs); +} + +uint16_t evse_get_charging_current(void) { + return charging_current; +} + +esp_err_t evse_set_charging_current(uint16_t value) { + if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current)) + return ESP_ERR_INVALID_ARG; + charging_current = value; + nvs_set_u16(nvs, "def_chrg_curr", value); + return nvs_commit(nvs); +} + +uint16_t evse_get_default_charging_current(void) { + uint16_t value; + if (nvs_get_u16(nvs, "def_chrg_curr", &value) == ESP_OK) + return value; + return charging_current; +} + +esp_err_t evse_set_default_charging_current(uint16_t value) { + if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current)) + return ESP_ERR_INVALID_ARG; + nvs_set_u16(nvs, "def_chrg_curr", value); + return nvs_commit(nvs); +} + +// ======================== +// Runtime current (not saved) +// ======================== +void evse_set_runtime_charging_current(uint16_t value) { + + if (value > max_charging_current) { + value = max_charging_current; + } else if (value < MIN_CHARGING_CURRENT_LIMIT) { + value = MIN_CHARGING_CURRENT_LIMIT; + } + + charging_current_runtime = value; + + // --- PUBLICA ALTERAÇÃO DE CONFIG DO EVSE --- + evse_config_event_data_t evt = { + .charging = evse_state_is_charging(evse_get_state()), + .hw_max_current = (float)evse_get_max_charging_current(), + .runtime_current = (float)charging_current_runtime, + .timestamp_us = esp_timer_get_time() + }; + + esp_event_post(EVSE_EVENTS, + EVSE_EVENT_CONFIG_UPDATED, + &evt, + sizeof(evt), + portMAX_DELAY); + + ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime); +} + + +uint16_t evse_get_runtime_charging_current(void) { + return charging_current_runtime; +} + + +// ======================== +// Socket outlet +// ======================== +bool evse_get_socket_outlet(void) { + return socket_outlet; +} + +esp_err_t evse_set_socket_outlet(bool value) { + if (value && !board_config.proximity) + return ESP_ERR_INVALID_ARG; + socket_outlet = value; + nvs_set_u8(nvs, "socket_outlet", value); + return nvs_commit(nvs); +} + +// ======================== +// RCM +// ======================== +bool evse_is_rcm(void) { + return rcm; +} + +esp_err_t evse_set_rcm(bool value) { + if (value && !board_config.rcm) + return ESP_ERR_INVALID_ARG; + rcm = value; + nvs_set_u8(nvs, "rcm", value); + return nvs_commit(nvs); +} + +// ======================== +// Temperature +// ======================== +uint8_t evse_get_temp_threshold(void) { + return temp_threshold; +} + +esp_err_t evse_set_temp_threshold(uint8_t value) { + if (value < 40 || value > 80) + return ESP_ERR_INVALID_ARG; + temp_threshold = value; + nvs_set_u8(nvs, "temp_threshold", value); + return nvs_commit(nvs); +} + + +// ======================== +// Availability +// ======================== +static bool is_available = true; + +bool evse_config_is_available(void) { + return is_available; +} + +void evse_config_set_available(bool available) { + is_available = available; +} + +// ======================== +// Enable/Disable +// ======================== +static bool is_enabled = true; + +bool evse_config_is_enabled(void) { + return is_enabled; +} + +void evse_config_set_enabled(bool enabled) { + is_enabled = enabled; +} + +// === Fim de: components/evse/evse_config.c === + + +// === Início de: components/evse/evse_manager.c === +#include "evse_manager.h" +#include "evse_state.h" +#include "evse_error.h" +#include "evse_hardware.h" +#include "evse_config.h" +#include "evse_api.h" +#include "evse_meter.h" +#include "evse_session.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "esp_log.h" +#include + +#include "auth_events.h" +#include "loadbalancer_events.h" +#include "esp_event.h" + +static const char *TAG = "EVSE_Manager"; + +static SemaphoreHandle_t evse_mutex; +static bool auth_enabled = false; + +#define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo + +// ===== Task de ciclo principal ===== +static void evse_manager_task(void *arg) { + while (true) { + evse_manager_tick(); + vTaskDelay(pdMS_TO_TICKS(EVSE_MANAGER_TICK_PERIOD_MS)); + } +} + +// ===== Tratador de eventos de autenticação ===== +static void on_auth_event(void* arg, esp_event_base_t base, int32_t id, void* data) { + if (base != AUTH_EVENTS || data == NULL) return; + + switch (id) { + case AUTH_EVENT_TAG_PROCESSED: { + auth_tag_event_data_t *evt = (auth_tag_event_data_t*)data; + ESP_LOGI("EVSE", "Tag: %s | Autorized: %s", evt->tag, evt->authorized ? "AUTHORIZED" : "DENIED"); + evse_state_set_authorized(evt->authorized); + break; + } + + case AUTH_EVENT_ENABLED_CHANGED: + case AUTH_EVENT_INIT: { + auth_enabled_event_data_t *evt = (auth_enabled_event_data_t*)data; + auth_enabled = evt->enabled; + + ESP_LOGI("EVSE", "Auth %s (%s)", + id == AUTH_EVENT_ENABLED_CHANGED ? "ficou" : "init", + evt->enabled ? "ATIVO" : "INATIVO"); + + if (!auth_enabled) { + evse_state_set_authorized(true); + ESP_LOGI("EVSE", "Autenticação desativada → autorização forçada."); + } else { + evse_state_set_authorized(false); + ESP_LOGI("EVSE", "Autenticação ativada → aguardando autorização por tag."); + } + break; + } + } +} + +// ===== Tratador de eventos de loadbalancer ===== +static void on_loadbalancer_event(void* handler_arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + if (event_id == LOADBALANCER_EVENT_INIT || event_id == LOADBALANCER_EVENT_STATE_CHANGED) { + const loadbalancer_state_event_t* evt = (const loadbalancer_state_event_t*) event_data; + ESP_LOGI(TAG, "Loadbalancer %s (ts: %lld)", + evt->enabled ? "ENABLED" : "DISABLED", evt->timestamp_us); + // Ações adicionais podem ser adicionadas aqui conforme necessário + } else if (event_id == LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT) { + const loadbalancer_master_limit_event_t* evt = (const loadbalancer_master_limit_event_t*) event_data; + ESP_LOGD(TAG, "Novo limite de corrente (master): %u A (ts: %lld)", evt->max_current, evt->timestamp_us); + evse_set_runtime_charging_current(evt->max_current); + } +} + +// ===== Inicialização ===== +void evse_manager_init(void) { + evse_mutex = xSemaphoreCreateMutex(); + + evse_config_init(); + evse_error_init(); + evse_hardware_init(); + evse_state_init(); + evse_meter_init(); + evse_session_init(); + + ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, ESP_EVENT_ANY_ID, &on_auth_event, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, ESP_EVENT_ANY_ID, &on_loadbalancer_event, NULL)); + + ESP_LOGI(TAG, "EVSE Manager inicializado."); + xTaskCreate(evse_manager_task, "evse_manager_task", 4096, NULL, 5, NULL); +} + +// ===== Main Tick ===== +void evse_manager_tick(void) { + xSemaphoreTake(evse_mutex, portMAX_DELAY); + + evse_hardware_tick(); + evse_error_tick(); + evse_state_tick(); + evse_temperature_check(); + evse_session_tick(); + + if (auth_enabled) { + // If the car is disconnected, revoke authorization + if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A) { + ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization."); + evse_state_set_authorized(false); + } + } else { + // If authentication is disabled, ensure authorization is always granted + if (!evse_state_get_authorized()) { + evse_state_set_authorized(true); + ESP_LOGI(TAG, "Authentication disabled → forced authorization."); + } + } + + xSemaphoreGive(evse_mutex); +} + +// === Fim de: components/evse/evse_manager.c === + + +// === Início de: components/evse/evse_events.c === +#include "evse_events.h" + +ESP_EVENT_DEFINE_BASE(EVSE_EVENTS); + +// === Fim de: components/evse/evse_events.c === + + +// === Início de: components/evse/include/evse_pilot.h === +#ifndef PILOT_H_ +#define PILOT_H_ #ifdef __cplusplus extern "C" { #endif -/// Tamanho máximo de uma tag RFID (incluindo '\0') -#define AUTH_TAG_MAX_LEN 20 +#include +#include -/// Estrutura de evento emitida após leitura de uma tag +/** + * @brief Níveis categóricos de tensão no sinal CP (Control Pilot) + */ +typedef enum +{ + PILOT_VOLTAGE_12, ///< Estado A: +12V + PILOT_VOLTAGE_9, ///< Estado B: +9V + PILOT_VOLTAGE_6, ///< Estado C: +6V + PILOT_VOLTAGE_3, ///< Estado D: +3V + PILOT_VOLTAGE_1 ///< Estado E/F: abaixo de 3V +} pilot_voltage_t; + +/** + * @brief Inicializa o driver do sinal Pilot + */ +void pilot_init(void); + +/** + * @brief Define o nível do Pilot: +12V ou -12V + * + * @param level true = +12V, false = -12V + */ +void pilot_set_level(bool level); + +/** + * @brief Ativa o PWM do Pilot com corrente limitada + * + * @param amps Corrente em décimos de ampère (ex: 160 = 16A) + */ +void pilot_set_amps(uint16_t amps); + +/** + * @brief Mede o nível de tensão do Pilot e detecta -12V + * + * @param up_voltage Valor categórico da tensão positiva + * @param down_voltage_n12 true se o nível negativo atingir -12V + */ +void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12); + +/** + * @brief Retorna o estado lógico atual do Pilot (nível alto = +12V) + * + * @return true se nível atual for +12V, false se for -12V + */ +bool pilot_get_state(void); + +/** + * @brief Cache interno opcional dos níveis de tensão reais do Pilot + */ typedef struct { - char tag[AUTH_TAG_MAX_LEN]; ///< Tag lida - bool authorized; ///< true se a tag for reconhecida como válida -} auth_event_t; - -/** - * @brief Inicializa o sistema de autenticação. - * - * - Carrega a configuração (enabled) da NVS - * - Inicia o leitor Wiegand - * - Emite evento AUTH_EVENT_INIT com estado atual - */ -void auth_init(void); - -/** - * @brief Ativa ou desativa o uso de autenticação via RFID. - * - * Esta configuração é persistida em NVS. Se desativado, o sistema - * considerará todas as autorizações como aceitas. - * - * @param value true para ativar, false para desativar - */ -void auth_set_enabled(bool value); - -/** - * @brief Verifica se o sistema de autenticação está habilitado. - */ -bool auth_is_enabled(void); - -/** - * @brief Adiciona uma nova tag RFID à lista de autorizadas. - * - * @param tag String da tag (máx AUTH_TAG_MAX_LEN-1) - * @return true se a tag foi adicionada, false se já existia ou inválida - */ -bool auth_add_tag(const char *tag); - -/** - * @brief Remove uma tag previamente cadastrada. - * - * @param tag String da tag - * @return true se foi removida, false se não encontrada - */ -bool auth_remove_tag(const char *tag); - -/** - * @brief Verifica se uma tag já está registrada como válida. - */ -bool auth_tag_exists(const char *tag); - -/** - * @brief Lista todas as tags válidas atualmente registradas (via logs). - */ -void auth_list_tags(void); - -/** - * @brief Processa uma tag RFID lida (chamada normalmente pelo leitor). - * - * - Verifica validade - * - Emite evento AUTH_EVENT_TAG_PROCESSED - * - Inicia timer de expiração se autorizada - */ -void auth_process_tag(const char *tag); - + uint16_t high_mv; ///< Pico positivo medido (mV) + uint16_t low_mv; ///< Pico negativo medido (mV) +} pilot_voltage_cache_t; #ifdef __cplusplus } #endif -#endif // AUTH_H +#endif /* PILOT_H_ */ -// === Fim de: components/auth/include/auth.h === +// === Fim de: components/evse/include/evse_pilot.h === -// === Início de: components/auth/include/auth_events.h === +// === Início de: components/evse/include/evse_manager.h === +#ifndef EVSE_MANAGER_H +#define EVSE_MANAGER_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +/** + * @brief Inicializa os módulos internos do EVSE (hardware, estado, erros, etc.) + * e inicia a tarefa de supervisão periódica (tick). + */ +void evse_manager_init(void); + +/** + * @brief Executa uma iteração do ciclo de controle do EVSE. + * + * Esta função é chamada automaticamente pela task periódica, + * mas pode ser chamada manualmente em testes. + */ +void evse_manager_tick(void); + +#ifdef __cplusplus +} +#endif + + +#endif // EVSE_MANAGER_H + +// === Fim de: components/evse/include/evse_manager.h === + + +// === Início de: components/evse/include/evse_fsm.h === +#ifndef EVSE_FSM_H +#define EVSE_FSM_H + +#include +#include +#include "evse_api.h" +#include "evse_pilot.h" +#include "freertos/FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Reinicia a máquina de estados do EVSE para o estado inicial (A). + */ +void evse_fsm_reset(void); + +/** + * @brief Processa uma leitura do sinal de piloto e atualiza a máquina de estados do EVSE. + * + * Esta função deve ser chamada periodicamente pelo núcleo de controle para + * avaliar mudanças no estado do conector, disponibilidade do carregador e + * autorização do usuário. + * + * @param pilot_voltage Leitura atual da tensão do sinal piloto. + * @param authorized Indica se o carregamento foi autorizado. + * @param available Indica se o carregador está disponível (ex: sem falhas). + * @param enabled Indica se o carregador está habilitado via software. + */ +void evse_fsm_process(pilot_voltage_t pilot_voltage, bool authorized, bool available, bool enabled); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_FSM_H + +// === Fim de: components/evse/include/evse_fsm.h === + + +// === Início de: components/evse/include/evse_hardware.h === +#ifndef EVSE_HARDWARE_H +#define EVSE_HARDWARE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * @brief Inicializa todos os periféricos de hardware do EVSE (pilot, relé, trava, etc.) + */ +void evse_hardware_init(void); + +/** + * @brief Executa atualizações periódicas no hardware (tick) + */ +void evse_hardware_tick(void); + +/** + * @brief Verifica se o sinal piloto está em nível alto (12V) + */ +bool evse_hardware_is_pilot_high(void); + +/** + * @brief Verifica se o veículo está fisicamente conectado via Proximity + */ +bool evse_hardware_is_vehicle_connected(void); + +/** + * @brief Verifica se há consumo de energia (corrente detectada) + */ +bool evse_hardware_is_energy_detected(void); + +/** + * @brief Liga o relé de fornecimento de energia + */ +void evse_hardware_relay_on(void); + +/** + * @brief Desliga o relé de fornecimento de energia + */ +void evse_hardware_relay_off(void); + +/** + * @brief Consulta o estado atual do relé + * @return true se ligado, false se desligado + */ +bool evse_hardware_relay_status(void); + +/** + * @brief Aciona a trava física do conector + */ +void evse_hardware_lock(void); + +/** + * @brief Libera a trava física do conector + */ +void evse_hardware_unlock(void); + +/** + * @brief Verifica se o conector está travado + */ +bool evse_hardware_is_locked(void); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_HARDWARE_H + +// === Fim de: components/evse/include/evse_hardware.h === + + +// === Início de: components/evse/include/evse_config.h === +#ifndef EVSE_CONFIG_H +#define EVSE_CONFIG_H + +#include +#include +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "evse_events.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ======================== +// Limites Globais (Defines) +// ======================== + +// Corrente máxima de carregamento (configurável pelo usuário) +#define MIN_CHARGING_CURRENT_LIMIT 6 // A +#define MAX_CHARGING_CURRENT_LIMIT 32 // A + +// Corrente via cabo (proximity) — se configurável +#define MIN_CABLE_CURRENT_LIMIT 6 // A +#define MAX_CABLE_CURRENT_LIMIT 63 // A + +// ======================== +// Funções de Configuração +// ======================== + +// Inicialização +esp_err_t evse_config_init(void); +void evse_check_defaults(void); + +// Corrente de carregamento +uint8_t evse_get_max_charging_current(void); +esp_err_t evse_set_max_charging_current(uint8_t value); + +uint16_t evse_get_charging_current(void); +esp_err_t evse_set_charging_current(uint16_t value); + +uint16_t evse_get_default_charging_current(void); +esp_err_t evse_set_default_charging_current(uint16_t value); + +// Configuração de socket outlet +bool evse_get_socket_outlet(void); +esp_err_t evse_set_socket_outlet(bool socket_outlet); + +void evse_set_runtime_charging_current(uint16_t value); +uint16_t evse_get_runtime_charging_current(void); + + +// RCM +bool evse_is_rcm(void); +esp_err_t evse_set_rcm(bool rcm); + +// Temperatura +uint8_t evse_get_temp_threshold(void); +esp_err_t evse_set_temp_threshold(uint8_t threshold); + +// Disponibilidade +bool evse_config_is_available(void); +void evse_config_set_available(bool available); + +// Ativação/desativação do EVSE +bool evse_config_is_enabled(void); +void evse_config_set_enabled(bool enabled); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_CONFIG_H + +// === Fim de: components/evse/include/evse_config.h === + + +// === Início de: components/evse/include/evse_state.h === +#ifndef EVSE_STATE_H +#define EVSE_STATE_H + +#include +#include "freertos/FreeRTOS.h" +#include "evse_events.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================ +// EVSE Pilot Signal States +// ============================ + +typedef enum { + EVSE_STATE_A, // EV Not Connected (12V) + EVSE_STATE_B1, // EV Connected (9V, Not Authorized) + EVSE_STATE_B2, // EV Connected (9V, Authorized and Ready) + EVSE_STATE_C1, // Charging Requested (6V, Relay Off) + EVSE_STATE_C2, // Charging Active (6V, Relay On) + EVSE_STATE_D1, // Ventilation Required (3V, Relay Off) + EVSE_STATE_D2, // Ventilation Active (3V, Relay On) + EVSE_STATE_E, // Error: Pilot Short to Ground (0V) + EVSE_STATE_F // Fault: No Pilot or EVSE Unavailable +} evse_state_t; + +// ============================ +// Initialization +// ============================ + +/** + * @brief Initializes the EVSE state machine and default state. + */ +void evse_state_init(void); + +/** + * @brief Periodic tick for state handling (optional hook). + */ +void evse_state_tick(void); + +// ============================ +// State Access & Control +// ============================ + +/** + * @brief Returns the current EVSE state. + */ +evse_state_t evse_get_state(void); + +/** + * @brief Sets the current EVSE state and emits a change event if needed. + */ +void evse_set_state(evse_state_t state); + +/** + * @brief Converts the state enum into a human-readable string. + */ +const char* evse_state_to_str(evse_state_t state); + +// ============================ +// State Evaluation Helpers +// ============================ + +/** + * @brief True if EV is in an active session (B2, C1, C2). + */ +bool evse_state_is_session(evse_state_t state); + +/** + * @brief True if EV is actively charging (C1, C2). + */ +bool evse_state_is_charging(evse_state_t state); + +/** + * @brief True if EV is physically plugged in (B1 and beyond). + */ +bool evse_state_is_plugged(evse_state_t state); + +// ============================ +// Authorization Control +// ============================ + +/** + * @brief Sets whether the EV is authorized to charge. + */ +void evse_state_set_authorized(bool authorized); + +/** + * @brief Gets whether the EV is currently authorized. + */ +bool evse_state_get_authorized(void); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_STATE_H + +// === Fim de: components/evse/include/evse_state.h === + + +// === Início de: components/evse/include/evse_error.h === +#ifndef EVSE_ERROR_H +#define EVSE_ERROR_H + +#include +#include +#include "evse_pilot.h" + + +#define EVSE_ERR_AUTO_CLEAR_BITS ( \ + EVSE_ERR_DIODE_SHORT_BIT | \ + EVSE_ERR_TEMPERATURE_HIGH_BIT | \ + EVSE_ERR_RCM_TRIGGERED_BIT ) + +// Error bits +#define EVSE_ERR_DIODE_SHORT_BIT (1 << 0) +#define EVSE_ERR_LOCK_FAULT_BIT (1 << 1) +#define EVSE_ERR_UNLOCK_FAULT_BIT (1 << 2) +#define EVSE_ERR_RCM_SELFTEST_FAULT_BIT (1 << 3) +#define EVSE_ERR_RCM_TRIGGERED_BIT (1 << 4) +#define EVSE_ERR_TEMPERATURE_HIGH_BIT (1 << 5) +#define EVSE_ERR_PILOT_FAULT_BIT (1 << 6) +#define EVSE_ERR_TEMPERATURE_FAULT_BIT (1 << 7) + +// Inicialização do módulo de erros +void evse_error_init(void); + +// Verificações e monitoramento +void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v); + +void evse_temperature_check(void); + +void evse_error_tick(void); + +// Leitura e controle de erros +uint32_t evse_get_error(void); +bool evse_is_error_cleared(void); +void evse_mark_error_cleared(void); +void evse_error_set(uint32_t bitmask); +void evse_error_clear(uint32_t bitmask); +bool evse_error_is_active(void); +uint32_t evse_error_get_bits(void); +void evse_error_reset_flag(void); +bool evse_error_cleared_flag(void); + +#endif // EVSE_ERROR_H + +// === Fim de: components/evse/include/evse_error.h === + + +// === Início de: components/evse/include/evse_session.h === +/* + * evse_session.h + * Module to track and retrieve charging session data (current or last completed), + * accumulating energy via periodic tick of instantaneous power. + */ + +#ifndef EVSE_SESSION_H +#define EVSE_SESSION_H + +#include +#include +#include "freertos/FreeRTOS.h" + +/** + * @brief Charging session statistics + */ +typedef struct { + TickType_t start_tick; ///< tick when session began + uint32_t duration_s; ///< total duration in seconds + uint32_t energy_wh; ///< total energy consumed in Wh + uint32_t avg_power_w; ///< average power in W + bool is_current; ///< true if session still in progress +} evse_session_t; + +/** + * @brief Initialize the session module + */ +void evse_session_init(void); + +/** + * @brief Mark the beginning of a charging session + */ +void evse_session_start(void); + +/** + * @brief Mark the end of the charging session and store it as "last session" + */ +void evse_session_end(void); + +/** + * @brief Periodic tick: must be called (e.g., each 1s) to accumulate energy from instant power + */ +void evse_session_tick(void); + +/** + * @brief Retrieve statistics of either the current ongoing session (if any) or + * the last completed session. + * @param out pointer to evse_session_t to be filled + * @return true if there is a current or last session available, false otherwise + */ +bool evse_session_get(evse_session_t *out); + +#endif // EVSE_SESSION_H + +// === Fim de: components/evse/include/evse_session.h === + + +// === Início de: components/evse/include/evse_meter.h === +#ifndef EVSE_METER_H +#define EVSE_METER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define EVSE_METER_PHASE_COUNT 3 + +/// Inicializa o módulo EVSE Meter e registra os tratadores de eventos +void evse_meter_init(void); + +/// Retorna a potência instantânea (soma das 3 fases, em watts) +int evse_meter_get_instant_power(void); + +/// Retorna a energia total acumulada (em Wh) +int evse_meter_get_total_energy(void); + +/// Retorna as potências instantâneas nas fases L1, L2 e L3 (em watts) +void evse_meter_get_power(int power[EVSE_METER_PHASE_COUNT]); + +/// Retorna as tensões medidas nas fases L1, L2 e L3 (em volts) +void evse_meter_get_voltage(float voltage[EVSE_METER_PHASE_COUNT]); + +/// Retorna as correntes medidas nas fases L1, L2 e L3 (em amperes) +void evse_meter_get_current(float current[EVSE_METER_PHASE_COUNT]); + +/// Handler interno para eventos do medidor (não chamar externamente) +void evse_meter_on_meter_event(void* arg, void* event_data); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_METER_H + +// === Fim de: components/evse/include/evse_meter.h === + + +// === Início de: components/evse/include/evse_core.h === +#ifndef EVSE_CORE_H +#define EVSE_CORE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initializes the EVSE system and starts core task loop. + */ +void evse_init(void); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_CORE_H + +// === Fim de: components/evse/include/evse_core.h === + + +// === Início de: components/evse/include/evse_limits.h === +#ifndef EVSE_LIMITS_H +#define EVSE_LIMITS_H + +#include +#include +#include "evse_state.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================ +// Limit Status & Evaluation +// ============================ + +/** + * @brief Sets the internal 'limit reached' flag. + * Called internally when a limit condition is triggered. + */ +void evse_set_limit_reached(bool value); + +/** + * @brief Returns true if any runtime charging limit has been reached. + */ +bool evse_get_limit_reached(void); + +/** + * @brief Checks if any session limit has been exceeded (energy, time or power). + * Should be called periodically during charging. + */ +void evse_limits_check(void); + +// ============================ +// Runtime Limit Configuration +// ============================ + +/** + * @brief Get/set energy consumption limit (in Wh). + */ +uint32_t evse_get_consumption_limit(void); +void evse_set_consumption_limit(uint32_t value); + +/** + * @brief Get/set maximum charging time (in seconds). + */ +uint32_t evse_get_charging_time_limit(void); +void evse_set_charging_time_limit(uint32_t value); + +/** + * @brief Get/set minimum acceptable power level (in Watts). + * If the power remains below this for a long time, the session may be interrupted. + */ +uint16_t evse_get_under_power_limit(void); +void evse_set_under_power_limit(uint16_t value); + +// ============================ +// Default (Persistent) Limits +// ============================ + +/** + * @brief Default values used after system boot or reset. + * These can be restored from NVS or fallback values. + */ +uint32_t evse_get_default_consumption_limit(void); +void evse_set_default_consumption_limit(uint32_t value); + +uint32_t evse_get_default_charging_time_limit(void); +void evse_set_default_charging_time_limit(uint32_t value); + +uint16_t evse_get_default_under_power_limit(void); +void evse_set_default_under_power_limit(uint16_t value); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_LIMITS_H + +// === Fim de: components/evse/include/evse_limits.h === + + +// === Início de: components/evse/include/evse_events.h === +#ifndef EVSE_EVENTS_H +#define EVSE_EVENTS_H + #pragma once #include "esp_event.h" -#define AUTH_EVENT_TAG_MAX_LEN 32 - -ESP_EVENT_DECLARE_BASE(AUTH_EVENTS); +ESP_EVENT_DECLARE_BASE(EVSE_EVENTS); typedef enum { - AUTH_EVENT_TAG_PROCESSED, - AUTH_EVENT_ENABLED_CHANGED, - AUTH_EVENT_INIT, -} auth_event_id_t; + EVSE_EVENT_INIT, + EVSE_EVENT_STATE_CHANGED, + EVSE_EVENT_CONFIG_UPDATED, +} evse_event_id_t; + +typedef enum { + EVSE_STATE_EVENT_IDLE, + EVSE_STATE_EVENT_WAITING, + EVSE_STATE_EVENT_CHARGING, + EVSE_STATE_EVENT_FAULT +} evse_state_event_t; typedef struct { - char tag[AUTH_EVENT_TAG_MAX_LEN]; - bool authorized; -} auth_tag_event_data_t; + evse_state_event_t state; +} evse_state_event_data_t; typedef struct { - bool enabled; -} auth_enabled_event_data_t; + bool charging; // Estado de carregamento + float hw_max_current; // Corrente máxima suportada pelo hardware + float runtime_current; // Corrente de carregamento em uso + int64_t timestamp_us; // Momento da atualização +} evse_config_event_data_t; -// === Fim de: components/auth/include/auth_events.h === +#endif // EVSE_EVENTS_H + +// === Fim de: components/evse/include/evse_events.h === -// === Início de: components/auth/include/wiegand.h === -/* - * Copyright (c) 2021 Ruslan V. Uss - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of itscontributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ +// === Início de: components/evse/include/evse_api.h === +#ifndef EVSE_API_H +#define EVSE_API_H -/** - * @file wiegand.h - * @defgroup wiegand wiegand - * @{ - * - * ESP-IDF Wiegand protocol receiver - * - * Copyright (c) 2021 Ruslan V. Uss - * - * BSD Licensed as described in the file LICENSE - */ -#ifndef __WIEGAND_H__ -#define __WIEGAND_H__ +#include +#include +#include "evse_state.h" +#include "freertos/FreeRTOS.h" +#include "evse_session.h" -#include -#include -#include #ifdef __cplusplus extern "C" { #endif -typedef struct wiegand_reader wiegand_reader_t; - -typedef void (*wiegand_callback_t)(wiegand_reader_t *reader); +// =============================== +// Core EVSE State +// =============================== /** - * Bit and byte order of data + * @brief Get current EVSE state (e.g., A, B1, C2). */ -typedef enum { - WIEGAND_MSB_FIRST = 0, - WIEGAND_LSB_FIRST -} wiegand_order_t; +evse_state_t evse_get_state(void); /** - * Wiegand reader descriptor + * @brief Set the EVSE state (e.g., called by FSM or hardware layer). */ -struct wiegand_reader +void evse_set_state(evse_state_t state); + +// =============================== +// Charging Session Info +// =============================== + +/** + * @brief Retrieve statistics of either the current ongoing session (if any) + * or the last completed session. + * @param out pointer to evse_session_t to be filled + * @return true if there is a current or last session available + */ +bool evse_get_session(evse_session_t *out); + +// =============================== +// Charging Session Info +// =============================== + +/** + * @brief Returns true if the EV is charging (C1 or C2). + */ +bool evse_state_is_charging(evse_state_t state); + +/** + * @brief Returns true if the EV is connected (plugged). + */ +bool evse_state_is_plugged(evse_state_t state); + +/** + * @brief Returns true if a charging session is active (B2, C1, C2). + */ +bool evse_state_is_session(evse_state_t state); + +// =============================== +// Authorization +// =============================== + +/** + * @brief Set whether the vehicle is authorized to charge. + */ +void evse_state_set_authorized(bool authorized); + +/** + * @brief Get current authorization status. + */ +bool evse_state_get_authorized(void); + + +// =============================== +// Limit Status +// =============================== + +/** + * @brief Returns true if any runtime charging limit has been reached. + */ +bool evse_is_limit_reached(void); + +#ifdef __cplusplus +} +#endif + +#endif // EVSE_API_H + +// === Fim de: components/evse/include/evse_api.h === + + +// === Início de: components/loadbalancer/src/loadbalancer_events.c === +#include "loadbalancer_events.h" + +// Define a base de eventos para o loadbalancer +ESP_EVENT_DEFINE_BASE(LOADBALANCER_EVENTS); + +// === Fim de: components/loadbalancer/src/loadbalancer_events.c === + + +// === Início de: components/loadbalancer/src/loadbalancer.c === +#include "loadbalancer.h" +#include "loadbalancer_events.h" +#include "esp_event.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "input_filter.h" +#include "nvs_flash.h" +#include "nvs.h" +#include +#include "meter_events.h" +#include "evse_events.h" +#include "math.h" + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif + +static const char *TAG = "loadbalancer"; + +// Limites configuráveis +#define MIN_CHARGING_CURRENT_LIMIT 6 // A +#define MAX_CHARGING_CURRENT_LIMIT 32 // A +#define MIN_GRID_CURRENT_LIMIT 6 // A +#define MAX_GRID_CURRENT_LIMIT 100 // A + +// Parâmetros +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; +static input_filter_t grid_filter; +static input_filter_t evse_filter; + +#define MAX_SLAVES 255 +#define CONNECTOR_COUNT (MAX_SLAVES + 1) + +// Estrutura unificada para master e slaves +typedef struct { + uint8_t id; // 0xFF = master, 0..MAX_SLAVES-1 = slave + bool is_master; + bool charging; + float hw_max_current; + float runtime_current; + int64_t timestamp; // microssegundos + bool online; + float assigned; +} evse_connector_t; + +static evse_connector_t connectors[CONNECTOR_COUNT]; + +const int64_t METRICS_TIMEOUT_US = 60 * 1000000; // 60 segundos + +// Helper: inicializa array de conectores +static void init_connectors(void) { - gpio_num_t gpio_d0, gpio_d1; - wiegand_callback_t callback; - wiegand_order_t bit_order; - wiegand_order_t byte_order; - - uint8_t *buf; - size_t size; - size_t bits; - esp_timer_handle_t timer; - bool start_parity; - bool enabled; -}; - -/** - * @brief Create and initialize reader instance. - * - * @param reader Reader descriptor - * @param gpio_d0 GPIO pin for D0 - * @param gpio_d1 GPIO pin for D0 - * @param internal_pullups Enable internal pull-up resistors for D0 and D1 GPIO - * @param buf_size Reader buffer size in bytes, must be large enough to - * contain entire Wiegand key - * @param callback Callback function for processing received codes - * @param bit_order Bit order of data - * @param byte_order Byte order of data - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_init(wiegand_reader_t *reader, gpio_num_t gpio_d0, gpio_num_t gpio_d1, - bool internal_pullups, size_t buf_size, wiegand_callback_t callback, wiegand_order_t bit_order, - wiegand_order_t byte_order); - -/** - * @brief Disable reader - * - * While reader is disabled, it will not receive new data - * - * @param reader Reader descriptor - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_disable(wiegand_reader_t *reader); - -/** - * @brief Enable reader - * - * @param reader Reader descriptor - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_enable(wiegand_reader_t *reader); - -/** - * @brief Delete reader instance. - * - * @param reader Reader descriptor - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_done(wiegand_reader_t *reader); - -#ifdef __cplusplus + // master em índice 0 + connectors[0] = (evse_connector_t){ + .id = 0xFF, + .is_master = true, + .charging = false, + .hw_max_current = MAX_CHARGING_CURRENT_LIMIT, + .runtime_current = 0, + .timestamp = 0, + .online = false, + .assigned = 0.0f + }; + // slaves em 1..CONNECTOR_COUNT-1 + for (int i = 1; i < CONNECTOR_COUNT; i++) { + connectors[i] = (evse_connector_t){ + .id = (uint8_t)(i - 1), + .is_master = false, + .charging = false, + .hw_max_current = 0.0f, + .runtime_current = 0.0f, + .timestamp = 0, + .online = false, + .assigned = 0.0f + }; + } } -#endif -/**@}*/ +// --- Helpers --- +static void input_filter_reset(input_filter_t *filter) +{ + filter->value = 0.0f; +} -#endif /* __WIEGAND_H__ */ +// Callback de status de slave +static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id, void *data) +{ + const loadbalancer_slave_status_event_t *status = (const loadbalancer_slave_status_event_t *)data; -// === Fim de: components/auth/include/wiegand.h === + if (status->slave_id >= MAX_SLAVES) { + ESP_LOGW(TAG, "Invalid slave_id %d", status->slave_id); + return; + } + + int idx = status->slave_id + 1; // slaves começam no índice 1 + 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].online = true; + + ESP_LOGI(TAG, + "Slave %d status: charging=%d hw_max_current=%.1fA runtime_current=%.2fA", + status->slave_id, status->charging, + status->hw_max_current, status->runtime_current); +} -// === Início de: components/auth/include/wiegand_reader.h === -#ifndef WIEGAND_READER_H -#define WIEGAND_READER_H + + +static void on_evse_config_event(void* handler_arg, + esp_event_base_t base, + int32_t id, + void* event_data) +{ + const evse_config_event_data_t *evt = (const evse_config_event_data_t*) event_data; + + int idx = 0; // MASTER INDICE 0 + connectors[idx].charging = evt->charging; + connectors[idx].hw_max_current = evt->hw_max_current; + connectors[idx].runtime_current = evt->runtime_current; + connectors[idx].timestamp = esp_timer_get_time(); + connectors[idx].online = true; + + + ESP_LOGI(TAG, "EVSE config updated: charging=%d hw_max_current=%.1f runtime_current=%.1f", + evt->charging, evt->hw_max_current, evt->runtime_current); + +} + + + +// --- Handlers de eventos externos --- +static void loadbalancer_meter_event_handler(void *handler_arg, + esp_event_base_t base, + int32_t id, + void *event_data) +{ + if (id != METER_EVENT_DATA_READY || event_data == NULL) + return; + const meter_event_data_t *evt = (const meter_event_data_t *)event_data; + float max_irms = evt->irms[0]; + for (int i = 1; i < 3; ++i) + if (evt->irms[i] > max_irms) + max_irms = evt->irms[i]; + if (evt->source && strcmp(evt->source, "GRID") == 0) { + grid_current = input_filter_update(&grid_filter, max_irms); + ESP_LOGI(TAG, "GRID IRMS (filtered): %.2f A", grid_current); + } else if (evt->source && strcmp(evt->source, "EVSE") == 0) { + evse_current = input_filter_update(&evse_filter, max_irms); + ESP_LOGI(TAG, "EVSE IRMS (filtered): %.2f A", evse_current); + } else { + ESP_LOGW(TAG, "Unknown meter event source: %s", evt->source); + } +} + +static void loadbalancer_evse_event_handler(void *handler_arg, + esp_event_base_t base, + int32_t id, + void *event_data) +{ + const evse_state_event_data_t *evt = (const evse_state_event_data_t *)event_data; + switch (evt->state) + { + case EVSE_STATE_EVENT_IDLE: + ESP_LOGI(TAG, "EVSE is IDLE - vehicle disconnected"); + connectors[0].charging = false; + connectors[0].online = false; + break; + case EVSE_STATE_EVENT_WAITING: + ESP_LOGI(TAG, "EVSE is WAITING - connected but not charging"); + connectors[0].charging = false; + connectors[0].online = false; + break; + case EVSE_STATE_EVENT_CHARGING: + ESP_LOGI(TAG, "EVSE is CHARGING - resetting filters"); + grid_current = 0.0f; + evse_current = 0.0f; + input_filter_reset(&grid_filter); + input_filter_reset(&evse_filter); + connectors[0].charging = true; + connectors[0].online = true; + connectors[0].timestamp = esp_timer_get_time(); + break; + case EVSE_STATE_EVENT_FAULT: + ESP_LOGW(TAG, "EVSE is in FAULT state - consider disabling load balancing"); + break; + default: + ESP_LOGW(TAG, "Unknown EVSE state: %d", evt->state); + break; + } +} + + + +// --- Config persistência --- +static esp_err_t loadbalancer_load_config() +{ + nvs_handle_t handle; + esp_err_t err = nvs_open("loadbalancing", NVS_READWRITE, &handle); + if (err != ESP_OK) + return err; + bool needs_commit = false; + uint8_t temp_u8; + err = nvs_get_u8(handle, "max_grid_curr", &temp_u8); + 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; + nvs_set_u8(handle, "max_grid_curr", max_grid_current); + needs_commit = true; + } + err = nvs_get_u8(handle, "enabled", &temp_u8); + if (err == ESP_OK && temp_u8 <= 1) + loadbalancer_enabled = (temp_u8 != 0); + else { + loadbalancer_enabled = false; + nvs_set_u8(handle, "enabled", 0); + needs_commit = true; + } + if (needs_commit) + nvs_commit(handle); + nvs_close(handle); + return ESP_OK; +} + +// --- API --- +void loadbalancer_set_enabled(bool enabled) +{ + nvs_handle_t handle; + if (nvs_open("loadbalancing", NVS_READWRITE, &handle) == ESP_OK) { + nvs_set_u8(handle, "enabled", enabled ? 1 : 0); + nvs_commit(handle); + nvs_close(handle); + } + loadbalancer_enabled = enabled; + loadbalancer_state_event_t evt = { .enabled = enabled, .timestamp_us = esp_timer_get_time() }; + 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) +{ + if (value < MIN_GRID_CURRENT_LIMIT || value > MAX_GRID_CURRENT_LIMIT) + return ESP_ERR_INVALID_ARG; + nvs_handle_t handle; + if (nvs_open("loadbalancing", NVS_READWRITE, &handle) != ESP_OK) + return ESP_FAIL; + nvs_set_u8(handle, "max_grid_curr", value); + nvs_commit(handle); + nvs_close(handle); + max_grid_current = value; + return ESP_OK; +} + +uint8_t load_balancing_get_max_grid_current(void) +{ + return max_grid_current; +} + +bool loadbalancer_is_enabled(void) +{ + return loadbalancer_enabled; +} + +// --- Task principal --- +void loadbalancer_task(void *param) +{ + + + while (true) { + if (!loadbalancer_is_enabled()) { + vTaskDelay(pdMS_TO_TICKS(5000)); + continue; + } + + int64_t now = esp_timer_get_time(); + + // --- Atualiza online e conta ativos --- + int idxs[CONNECTOR_COUNT]; + int active_cnt = 0; + for (int i = 0; i < CONNECTOR_COUNT; i++) { + bool valid = connectors[i].online && connectors[i].charging; + if (!connectors[i].is_master) { + valid &= (now - connectors[i].timestamp) < METRICS_TIMEOUT_US; + } + + if (valid) { + idxs[active_cnt++] = i; + ESP_LOGI(TAG, + "Connector[%d] ONLINE & CHARGING (is_master=%d, hw_max_current=%.1f, timestamp_diff=%lld us)", + i, connectors[i].is_master, connectors[i].hw_max_current, + (long long)(now - connectors[i].timestamp)); + } else if (!connectors[i].is_master) { + if (connectors[i].online) { + ESP_LOGW(TAG, + "Connector[%d] marked OFFLINE (charging=%d, timestamp_diff=%lld us)", + i, connectors[i].charging, + (long long)(now - connectors[i].timestamp)); + } + connectors[i].online = false; + } + } + ESP_LOGI(TAG, "Active connectors: %d", active_cnt); + + // --- Corrente disponível --- + float available = max_grid_current - grid_current; + if (available < MIN_CHARGING_CURRENT_LIMIT) { + available = MIN_CHARGING_CURRENT_LIMIT; + } else if (available > max_grid_current) { + available = max_grid_current; + } + ESP_LOGI(TAG, "LB Calc: available=%.1fA, active_connectors=%d", available, active_cnt); + + // --- Water-filling --- + // Ordena índices por hw_max_current crescente + for (int a = 0; a < active_cnt - 1; a++) { + for (int b = 0; b < active_cnt - 1 - a; b++) { + if (connectors[idxs[b]].hw_max_current > connectors[idxs[b+1]].hw_max_current) { + int tmp = idxs[b]; idxs[b] = idxs[b+1]; idxs[b+1] = tmp; + } + } + } + + // Distribuição de corrente + float remaining = available; + int remaining_cnt = active_cnt; + for (int k = 0; k < active_cnt; k++) { + int i = idxs[k]; + float share = remaining / remaining_cnt; + if (share >= connectors[i].hw_max_current) { + connectors[i].assigned = connectors[i].hw_max_current; + remaining -= connectors[i].assigned; + remaining_cnt--; + } else { + for (int m = k; m < active_cnt; m++) { + connectors[idxs[m]].assigned = share; + } + break; + } + } + + // Assegura o piso mínimo + for (int k = 0; k < active_cnt; k++) { + int i = idxs[k]; + if (connectors[i].assigned < MIN_CHARGING_CURRENT_LIMIT) { + connectors[i].assigned = MIN_CHARGING_CURRENT_LIMIT; + } + } + + // --- Publica limites --- + for (int k = 0; k < active_cnt; k++) { + int i = idxs[k]; + uint16_t max_cur = (uint16_t)MIN(connectors[i].assigned, MAX_CHARGING_CURRENT_LIMIT); + + // só envia evento se a corrente aplicada for diferente do limite calculado + uint16_t current_rounded = (uint16_t)roundf(connectors[i].runtime_current); + if (current_rounded == max_cur) { + continue; // sem alteração -> não faz nada + } + + if (connectors[i].is_master) { + loadbalancer_master_limit_event_t master_evt = { + .slave_id = connectors[i].id, + .max_current = max_cur, + .timestamp_us = now + }; + esp_event_post(LOADBALANCER_EVENTS, + LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, + &master_evt, sizeof(master_evt), portMAX_DELAY); + ESP_LOGI(TAG, "Master limit changed -> %.1f A (runtime=%.1f A)", + (float)max_cur, connectors[i].runtime_current); + } else { + loadbalancer_slave_limit_event_t slave_evt = { + .slave_id = connectors[i].id, + .max_current = max_cur, + .timestamp_us = now + }; + esp_event_post(LOADBALANCER_EVENTS, + LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + &slave_evt, sizeof(slave_evt), portMAX_DELAY); + ESP_LOGI(TAG, "Slave %d limit changed -> %.1f A (runtime=%.1f A)", + connectors[i].id, (float)max_cur, connectors[i].runtime_current); + } + } + + + vTaskDelay(pdMS_TO_TICKS(5000)); + } +} + + +// --- Init --- +void loadbalancer_init(void) +{ + if (loadbalancer_load_config() != ESP_OK) + ESP_LOGW(TAG, "Failed to load/init config. Using defaults."); + + init_connectors(); + input_filter_init(&grid_filter, 0.3f); + input_filter_init(&evse_filter, 0.3f); + + if (xTaskCreate(loadbalancer_task, "loadbalancer", 4096, NULL, 4, NULL) != pdPASS) + ESP_LOGE(TAG, "Failed to create loadbalancer task"); + + loadbalancer_state_event_t evt = {.enabled = loadbalancer_enabled, .timestamp_us = esp_timer_get_time()}; + esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); + + ESP_ERROR_CHECK(esp_event_handler_register(METER_EVENT, METER_EVENT_DATA_READY, + &loadbalancer_meter_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, + &loadbalancer_evse_event_handler, NULL)); + + ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS,EVSE_EVENT_CONFIG_UPDATED, + &on_evse_config_event, NULL)); + + ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS,LOADBALANCER_EVENT_SLAVE_STATUS, + &on_slave_status, NULL)); +} + +// === Fim de: components/loadbalancer/src/loadbalancer.c === + + +// === Início de: components/loadbalancer/src/input_filter.c === +#include "input_filter.h" + +void input_filter_init(input_filter_t *filter, float alpha) { + if (filter) { + filter->alpha = alpha; + filter->value = 0.0f; + filter->initialized = 0; + } +} + +float input_filter_update(input_filter_t *filter, float input) { + if (!filter) return input; + + if (!filter->initialized) { + filter->value = input; + filter->initialized = 1; + } else { + filter->value = filter->alpha * input + (1.0f - filter->alpha) * filter->value; + } + + return filter->value; +} + +// === Fim de: components/loadbalancer/src/input_filter.c === + + +// === Início de: components/loadbalancer/include/loadbalancer_events.h === +#pragma once +#include "esp_event.h" +#include +#include +#include "esp_timer.h" + +ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); + +typedef enum { + LOADBALANCER_EVENT_INIT, + LOADBALANCER_EVENT_STATE_CHANGED, + LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT, + LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, + LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + LOADBALANCER_EVENT_SLAVE_STATUS +} loadbalancer_event_id_t; + +typedef struct { + bool enabled; + int64_t timestamp_us; +} loadbalancer_state_event_t; + +// (opcional) +typedef struct { + float limit; + int64_t timestamp_us; +} loadbalancer_global_limit_event_t; + +typedef struct { + uint8_t slave_id; + uint16_t max_current; + int64_t timestamp_us; +} loadbalancer_master_limit_event_t; + +typedef struct { + uint8_t slave_id; + uint16_t max_current; + int64_t timestamp_us; +} loadbalancer_slave_limit_event_t; + +typedef struct { + uint8_t slave_id; // ID do slave que reportou + bool charging; // Status de carregamento + float hw_max_current; // Limite máximo de corrente do hardware informado + float runtime_current; // Corrente atual de carregamento (A) + int64_t timestamp_us; // Momento em que o status foi coletado +} loadbalancer_slave_status_event_t; +// === Fim de: components/loadbalancer/include/loadbalancer_events.h === + + +// === Início de: components/loadbalancer/include/loadbalancer.h === +#ifndef LOADBALANCER_H_ +#define LOADBALANCER_H_ #ifdef __cplusplus extern "C" { #endif -void initWiegand(void); +#include +#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 + */ +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); + +/** + * @brief Obtém a corrente máxima do grid + */ +uint8_t load_balancing_get_max_grid_current(void); #ifdef __cplusplus } #endif -#endif // WIEGAND_READER_H +#endif /* LOADBALANCER_H_ */ -// === Fim de: components/auth/include/wiegand_reader.h === +// === Fim de: components/loadbalancer/include/loadbalancer.h === + + +// === Início de: components/loadbalancer/include/input_filter.h === +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + float alpha; ///< Fator de suavização (0.0 a 1.0) + float value; ///< Último valor filtrado + int initialized; ///< Flag de inicialização +} input_filter_t; + +/** + * @brief Inicializa o filtro com o fator alpha desejado. + * @param filter Ponteiro para a estrutura do filtro + * @param alpha Valor entre 0.0 (mais lento) e 1.0 (sem filtro) + */ +void input_filter_init(input_filter_t *filter, float alpha); + +/** + * @brief Atualiza o valor filtrado com uma nova entrada. + * @param filter Ponteiro para o filtro + * @param input Valor bruto + * @return Valor suavizado + */ +float input_filter_update(input_filter_t *filter, float input); + +#ifdef __cplusplus +} +#endif + +// === Fim de: components/loadbalancer/include/input_filter.h === diff --git a/readproject.py b/readproject.py index e41be8e..3d2a0bc 100644 --- a/readproject.py +++ b/readproject.py @@ -1,6 +1,6 @@ import os -TAMANHO_MAX = 100000 # Limite por arquivo +TAMANHO_MAX = 200000 # Limite por arquivo def coletar_arquivos(diretorios, extensoes=(".c", ".h")): arquivos = [] @@ -53,7 +53,7 @@ def unir_em_partes(arquivos, prefixo="projeto_parte", limite=TAMANHO_MAX): def main(): diretorio_main = "main" componentes_escolhidos = [ - "rest_api" , "auth" + "evse", "loadbalancer" ] diretorios_componentes = [os.path.join("components", nome) for nome in componentes_escolhidos]