diff --git a/components/api/src/json.c b/components/api/src/json.c index e9f4e7c..13f257c 100755 --- a/components/api/src/json.c +++ b/components/api/src/json.c @@ -47,7 +47,7 @@ cJSON *json_get_evse_config(void) cJSON_AddNumberToObject(root, "maxChargingCurrent", evse_get_max_charging_current()); cJSON_AddNumberToObject(root, "chargingCurrent", evse_get_charging_current()); cJSON_AddNumberToObject(root, "defaultChargingCurrent", evse_get_default_charging_current()); - cJSON_AddBoolToObject(root, "requireAuth", auth_is_enabled()); + cJSON_AddBoolToObject(root, "requireAuth", auth_get_mode()); cJSON_AddBoolToObject(root, "socketOutlet", evse_get_socket_outlet()); cJSON_AddBoolToObject(root, "rcm", evse_is_rcm()); cJSON_AddNumberToObject(root, "temperatureThreshold", evse_get_temp_threshold()); @@ -71,8 +71,8 @@ cJSON *json_get_evse_config(void) cJSON_AddBoolToObject(root, "enabledocpp", ocpp_get_enabled()); ocpp_get_server(str); cJSON_AddStringToObject(root, "serverocpp", str); - ocpp_get_rfid(str); - cJSON_AddStringToObject(root, "rfid", str); + //ocpp_get_rfid(str); + //cJSON_AddStringToObject(root, "rfid", str); return root; } @@ -93,7 +93,7 @@ esp_err_t json_set_evse_config(cJSON *root) } if (cJSON_IsBool(cJSON_GetObjectItem(root, "requireAuth"))) { - auth_set_enabled(cJSON_IsTrue(cJSON_GetObjectItem(root, "requireAuth"))); + //auth_set_mode(cJSON_IsTrue(cJSON_GetObjectItem(root, "requireAuth"))); } if (cJSON_IsBool(cJSON_GetObjectItem(root, "socketOutlet"))) { @@ -176,10 +176,10 @@ esp_err_t json_set_evse_config(cJSON *root) ocpp_set_server(cJSON_GetStringValue(cJSON_GetObjectItem(root, "serverocpp"))); } - if (cJSON_IsString(cJSON_GetObjectItem(root, "rfid"))) - { - ocpp_set_rfid(cJSON_GetStringValue(cJSON_GetObjectItem(root, "rfid"))); - } + //if (cJSON_IsString(cJSON_GetObjectItem(root, "rfid"))) + //{ + // ocpp_set_rfid(cJSON_GetStringValue(cJSON_GetObjectItem(root, "rfid"))); + //} return ESP_OK; } @@ -343,7 +343,7 @@ cJSON *json_get_state(void) cJSON_AddStringToObject(root, "state", evse_state_to_str(evse_get_state())); cJSON_AddBoolToObject(root, "available", evse_config_is_available()); cJSON_AddBoolToObject(root, "enabled", evse_config_is_enabled()); - cJSON_AddBoolToObject(root, "pendingAuth", auth_is_enabled()); + cJSON_AddBoolToObject(root, "pendingAuth", auth_get_mode()); cJSON_AddBoolToObject(root, "limitReached", evse_is_limit_reached()); uint32_t error = evse_error_get_bits(); diff --git a/components/auth/CMakeLists.txt b/components/auth/CMakeLists.txt index f989b1c..c7a59d8 100755 --- a/components/auth/CMakeLists.txt +++ b/components/auth/CMakeLists.txt @@ -1,4 +1,4 @@ -set(srcs "src/auth.c" "src/wiegand.c" "src/wiegand_reader.c" "src/auth_events.c") +set(srcs "src/auth_types.c" "src/auth.c" "src/wiegand.c" "src/wiegand_reader.c" "src/auth_events.c") idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" diff --git a/components/auth/include/auth.h b/components/auth/include/auth.h index e35d54c..6af0337 100755 --- a/components/auth/include/auth.h +++ b/components/auth/include/auth.h @@ -1,115 +1,32 @@ -#ifndef AUTH_H -#define AUTH_H - +#pragma once #include -#include +#include "auth_types.h" // enum + MAX LEN para API #ifdef __cplusplus extern "C" { #endif -/// Maximum length of an RFID tag (including null terminator) -#define AUTH_TAG_MAX_LEN 30 - -/// Event structure emitted after a tag is read +/* Evento auxiliar legado/útil (resultado local) */ typedef struct { - char tag[AUTH_TAG_MAX_LEN]; ///< The tag that was read - bool authorized; ///< true if the tag is valid + char tag[AUTH_TAG_MAX_LEN]; + bool authorized; } auth_event_t; -/** - * @brief Initializes the authentication system. - * - * - Loads configuration (enabled/disabled) from NVS - * - Starts the Wiegand reader - * - Emits AUTH_EVENT_INIT with current status - */ -void auth_init(void); +void auth_init(void); +void auth_set_mode(auth_mode_t mode); +auth_mode_t auth_get_mode(void); -/** - * @brief Enables or disables RFID-based authentication. - * - * This setting is persisted in NVS. If disabled, - * all tags will be treated as authorized. - * - * @param value true to enable authentication, false to disable - */ -void auth_set_enabled(bool value); - -/** - * @brief Checks whether authentication is currently enabled. - * - * @return true if enabled, false if disabled - */ -bool auth_is_enabled(void); - -/** - * @brief Adds a new RFID tag to the authorized list. - * - * @param tag The RFID tag (max AUTH_TAG_MAX_LEN-1 characters) - * @return true if the tag was added successfully, - * false if it already exists or is invalid - */ bool auth_add_tag(const char *tag); - -/** - * @brief Removes an existing RFID tag from the authorized list. - * - * @param tag The tag to remove - * @return true if the tag was removed, false if not found - */ bool auth_remove_tag(const char *tag); - -/** - * @brief Checks whether a tag is already registered. - * - * @param tag The tag to check - * @return true if the tag exists, false otherwise - */ bool auth_tag_exists(const char *tag); - -/** - * @brief Logs all currently registered tags via ESP logging. - */ void auth_list_tags(void); -/** - * @brief Processes a read RFID tag. - * - * - Checks whether it's authorized - * - Emits AUTH_EVENT_TAG_PROCESSED event - * - Starts expiration timer if authorized - * - * @param tag The tag that was read - */ void auth_process_tag(const char *tag); - -/** - * @brief Enables registration mode for the next tag read. - * - * When registration mode is active, the next tag read - * will be added to the authorized list automatically. - * Mode is deactivated after one tag is registered. - */ void auth_wait_for_tag_registration(void); -/** - * @brief Returns the total number of registered tags. - * - * @return Number of valid tags - */ -int auth_get_tag_count(void); - -/** - * @brief Returns the tag string at the given index. - * - * @param index The index (0 ≤ index < auth_get_tag_count()) - * @return Pointer to the tag string, or NULL if index is invalid - */ +int auth_get_tag_count(void); const char *auth_get_tag_by_index(int index); #ifdef __cplusplus } #endif - -#endif // AUTH_H diff --git a/components/auth/include/auth_events.h b/components/auth/include/auth_events.h index 43b66c3..76daaa0 100644 --- a/components/auth/include/auth_events.h +++ b/components/auth/include/auth_events.h @@ -1,22 +1,29 @@ #pragma once #include "esp_event.h" - -#define AUTH_EVENT_TAG_MAX_LEN 32 +#include "auth_types.h" // só tipos comuns; evita incluir auth.h ESP_EVENT_DECLARE_BASE(AUTH_EVENTS); +/* IDs de eventos */ typedef enum { - AUTH_EVENT_TAG_PROCESSED, - AUTH_EVENT_TAG_SAVED, - AUTH_EVENT_ENABLED_CHANGED, - AUTH_EVENT_INIT, + AUTH_EVENT_TAG_PROCESSED = 0, // resultado LOCAL -> auth_tag_event_data_t + AUTH_EVENT_TAG_VERIFY, // pedir validação OCPP -> auth_tag_verify_event_t + AUTH_EVENT_TAG_SAVED, // registada (modo registo) -> auth_tag_event_data_t + AUTH_EVENT_MODE_CHANGED, // modo alterado -> auth_mode_event_data_t + AUTH_EVENT_INIT, // estado inicial -> auth_mode_event_data_t } auth_event_id_t; +/* Payloads */ typedef struct { - char tag[AUTH_EVENT_TAG_MAX_LEN]; + char tag[AUTH_TAG_MAX_LEN]; bool authorized; } auth_tag_event_data_t; typedef struct { - bool enabled; -} auth_enabled_event_data_t; + char tag[AUTH_TAG_MAX_LEN]; + uint32_t req_id; // opcional p/ correlacionar +} auth_tag_verify_event_t; + +typedef struct { + auth_mode_t mode; +} auth_mode_event_data_t; diff --git a/components/auth/include/auth_types.h b/components/auth/include/auth_types.h new file mode 100644 index 0000000..2e47b7d --- /dev/null +++ b/components/auth/include/auth_types.h @@ -0,0 +1,26 @@ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Tamanho máx. da tag (inclui NUL) */ +#define AUTH_TAG_MAX_LEN 30 + +/* Modos de autorização */ +typedef enum { + AUTH_MODE_OPEN = 0, // Sem autenticação + AUTH_MODE_LOCAL_RFID, // Lista local (NVS) + AUTH_MODE_OCPP_RFID // Validação via OCPP/CSMS +} auth_mode_t; + +/* Converte enum -> "open"|"local"|"ocpp" (nunca NULL) */ +const char *auth_mode_to_str(auth_mode_t mode); + +/* Converte "open"|"local"|"ocpp" (case-insensitive) -> enum */ +bool auth_mode_from_str(const char *s, auth_mode_t *out); + +#ifdef __cplusplus +} +#endif diff --git a/components/auth/src/auth.c b/components/auth/src/auth.c index 3ca1103..253e28e 100755 --- a/components/auth/src/auth.c +++ b/components/auth/src/auth.c @@ -1,62 +1,40 @@ +// components/auth/src/auth.c + #include "auth.h" #include "auth_events.h" #include "esp_event.h" + #include #include + #include #include +#include // <-- necessário para strcasecmp #include "wiegand_reader.h" #include "nvs_flash.h" #include "nvs.h" #include "esp_random.h" - #define MAX_TAGS 50 static const char *TAG = "Auth"; -static bool enabled = false; +/* ===== Estado ===== */ +static auth_mode_t s_mode = AUTH_MODE_OPEN; static bool waiting_for_registration = false; static char valid_tags[MAX_TAGS][AUTH_TAG_MAX_LEN]; static int tag_count = 0; +static uint32_t s_next_req_id = 1; -// NVS keys +/* ===== NVS keys ===== */ #define NVS_NAMESPACE "auth" #define NVS_TAG_PREFIX "tag_" #define NVS_TAG_COUNT_KEY "count" -#define NVS_ENABLED_KEY "enabled" - -// =========================== -// NVS Persistence -// =========================== - -static void load_auth_config(void) { - nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle); - if (err == ESP_OK) { - uint8_t val; - if (nvs_get_u8(handle, NVS_ENABLED_KEY, &val) == ESP_OK) { - enabled = val; - ESP_LOGI(TAG, "Loaded auth enabled = %d", enabled); - } - nvs_close(handle); - } else { - ESP_LOGW(TAG, "No stored auth config found. Using default."); - } -} - -static void save_auth_config(void) { - nvs_handle_t handle; - if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) == ESP_OK) { - nvs_set_u8(handle, NVS_ENABLED_KEY, 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."); - } -} +#define NVS_MODE_KEY "mode" // uint8_t +/* ========================= + * NVS Persistence (tags) + * ========================= */ static void load_tags_from_nvs(void) { nvs_handle_t handle; if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) { @@ -71,7 +49,6 @@ static void load_tags_from_nvs(void) { } tag_count = 0; - for (int i = 0; i < count && i < MAX_TAGS; i++) { char key[16]; char tag_buf[AUTH_TAG_MAX_LEN]; @@ -109,10 +86,38 @@ static void save_tags_to_nvs(void) { ESP_LOGI(TAG, "Tags saved to NVS (%d tags)", tag_count); } -// =========================== -// Internal Helpers -// =========================== +/* ========================= + * NVS Persistence (mode) + * ========================= */ +static void load_mode_from_nvs(void) { + nvs_handle_t h; + if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &h) == ESP_OK) { + uint8_t u = (uint8_t)AUTH_MODE_OPEN; + if (nvs_get_u8(h, NVS_MODE_KEY, &u) == ESP_OK) { + if (u <= (uint8_t)AUTH_MODE_OCPP_RFID) s_mode = (auth_mode_t)u; + } + nvs_close(h); + } else { + ESP_LOGW(TAG, "No stored auth mode in NVS; default OPEN"); + } + ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode)); +} +static void save_mode_to_nvs(auth_mode_t mode) { + nvs_handle_t h; + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h) == ESP_OK) { + nvs_set_u8(h, NVS_MODE_KEY, (uint8_t)mode); + nvs_commit(h); + nvs_close(h); + ESP_LOGI(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode)); + } else { + ESP_LOGE(TAG, "Failed to save auth mode to NVS"); + } +} + +/* ========================= + * Helpers + * ========================= */ 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) { @@ -122,44 +127,74 @@ static bool is_tag_valid(const char *tag) { return false; } -// =========================== -// Public API -// =========================== - - +/* ========================= + * Public API + * ========================= */ void auth_init(void) { - load_auth_config(); + load_mode_from_nvs(); load_tags_from_nvs(); - if (enabled) { + if (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID) { initWiegand(); - ESP_LOGI(TAG, "Wiegand reader initialized (Auth enabled)"); + ESP_LOGI(TAG, "Wiegand reader initialized (mode=%s)", auth_mode_to_str(s_mode)); } else { - ESP_LOGI(TAG, "Auth disabled, Wiegand reader not started"); + ESP_LOGI(TAG, "Mode OPEN: Wiegand not started"); } - auth_enabled_event_data_t evt = { .enabled = enabled }; + auth_mode_event_data_t evt = { .mode = s_mode }; esp_event_post(AUTH_EVENTS, AUTH_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); - ESP_LOGI(TAG, "Initial AUTH state sent (enabled = %d)", enabled); + ESP_LOGI(TAG, "AUTH INIT sent (mode=%s)", auth_mode_to_str(s_mode)); } -void auth_set_enabled(bool value) { - enabled = value; - save_auth_config(); - ESP_LOGI(TAG, "Auth %s", enabled ? "ENABLED" : "DISABLED"); +void auth_set_mode(auth_mode_t mode) { + if (mode < AUTH_MODE_OPEN || mode > AUTH_MODE_OCPP_RFID) { + ESP_LOGW(TAG, "Invalid mode: %d", (int)mode); + return; + } + if (mode == s_mode) { + ESP_LOGI(TAG, "Mode unchanged: %s", auth_mode_to_str(mode)); + return; + } - auth_enabled_event_data_t event = { .enabled = enabled }; - esp_event_post(AUTH_EVENTS, AUTH_EVENT_ENABLED_CHANGED, &event, sizeof(event), portMAX_DELAY); + s_mode = mode; + save_mode_to_nvs(mode); + + // Nota: se precisares, aqui podes parar/iniciar o Wiegand consoante o modo. + if (s_mode == AUTH_MODE_OPEN) { + ESP_LOGI(TAG, "Mode set to OPEN"); + } else { + ESP_LOGI(TAG, "Mode set to %s; ensure Wiegand reader is running", auth_mode_to_str(s_mode)); + } + + auth_mode_event_data_t evt = { .mode = s_mode }; + esp_event_post(AUTH_EVENTS, AUTH_EVENT_MODE_CHANGED, &evt, sizeof(evt), portMAX_DELAY); } -bool auth_is_enabled(void) { - return enabled; +auth_mode_t auth_get_mode(void) { + return s_mode; +} + +const char *auth_mode_to_str(auth_mode_t mode) { + switch (mode) { + case AUTH_MODE_OPEN: return "open"; + case AUTH_MODE_LOCAL_RFID: return "local"; + case AUTH_MODE_OCPP_RFID: return "ocpp"; + default: return "open"; + } +} + +bool auth_mode_from_str(const char *s, auth_mode_t *out) { + if (!s || !out) return false; + if (!strcasecmp(s, "open")) { *out = AUTH_MODE_OPEN; return true; } + if (!strcasecmp(s, "local")) { *out = AUTH_MODE_LOCAL_RFID; return true; } + if (!strcasecmp(s, "ocpp")) { *out = AUTH_MODE_OCPP_RFID; return true; } + return false; } 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; // Already exists + if (is_tag_valid(tag)) return true; // já existe strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1); valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0'; @@ -198,40 +233,62 @@ void auth_list_tags(void) { } void auth_wait_for_tag_registration(void) { + if (s_mode != AUTH_MODE_LOCAL_RFID) { + ESP_LOGW(TAG, "Registration is only available in LOCAL mode"); + return; + } waiting_for_registration = true; ESP_LOGI(TAG, "Tag registration mode enabled."); } void auth_process_tag(const char *tag) { - if (!tag || !auth_is_enabled()) { - ESP_LOGW(TAG, "Auth disabled or NULL tag received."); + if (!tag || !*tag) { + ESP_LOGW(TAG, "NULL/empty tag received"); return; } - if (waiting_for_registration) { - if (auth_add_tag(tag)) { - 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 = true; - - esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, &event, sizeof(event), portMAX_DELAY); - ESP_LOGI(TAG, "Tag registered: %s", tag); - } else { - ESP_LOGW(TAG, "Failed to register tag: %s", tag); + switch (s_mode) { + case AUTH_MODE_OPEN: { + // Sem verificação; normalmente nem é necessário evento. + ESP_LOGI(TAG, "Mode OPEN: tag=%s (no verification)", tag); + break; + } + + case AUTH_MODE_LOCAL_RFID: { + if (waiting_for_registration) { + waiting_for_registration = false; + + if (auth_add_tag(tag)) { + auth_tag_event_data_t ev = {0}; + strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1); + ev.authorized = true; + esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, &ev, sizeof(ev), portMAX_DELAY); + ESP_LOGI(TAG, "Tag registered: %s", tag); + } else { + ESP_LOGW(TAG, "Failed to register tag: %s", tag); + } + return; + } + + auth_tag_event_data_t ev = {0}; + strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1); + ev.authorized = is_tag_valid(tag); + + ESP_LOGI(TAG, "LOCAL tag %s: %s", tag, ev.authorized ? "AUTHORIZED" : "DENIED"); + esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, &ev, sizeof(ev), portMAX_DELAY); + break; + } + + case AUTH_MODE_OCPP_RFID: { + // Não decide localmente. Pede validação ao OCPP. + auth_tag_verify_event_t rq = {0}; + strncpy(rq.tag, tag, AUTH_TAG_MAX_LEN - 1); + rq.req_id = s_next_req_id++; + ESP_LOGI(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)", rq.tag, (unsigned)rq.req_id); + esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, &rq, sizeof(rq), portMAX_DELAY); + break; } - waiting_for_registration = false; - 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); } int auth_get_tag_count(void) { diff --git a/components/auth/src/auth_types.c b/components/auth/src/auth_types.c new file mode 100755 index 0000000..33430a8 --- /dev/null +++ b/components/auth/src/auth_types.c @@ -0,0 +1,19 @@ +#include "auth_types.h" +#include // strcasecmp + +const char *auth_mode_to_str(auth_mode_t mode) { + switch (mode) { + case AUTH_MODE_OPEN: return "open"; + case AUTH_MODE_LOCAL_RFID: return "local"; + case AUTH_MODE_OCPP_RFID: return "ocpp"; + default: return "open"; + } +} + +bool auth_mode_from_str(const char *s, auth_mode_t *out) { + if (!s || !out) return false; + if (!strcasecmp(s, "open")) { *out = AUTH_MODE_OPEN; return true; } + if (!strcasecmp(s, "local")) { *out = AUTH_MODE_LOCAL_RFID; return true; } + if (!strcasecmp(s, "ocpp")) { *out = AUTH_MODE_OCPP_RFID; return true; } + return false; +} diff --git a/components/buzzer/src/buzzer.c b/components/buzzer/src/buzzer.c index 52e1d84..ebcdc75 100755 --- a/components/buzzer/src/buzzer.c +++ b/components/buzzer/src/buzzer.c @@ -6,10 +6,42 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" +#include "driver/ledc.h" // Para buzzer passivo #define BUZZER_GPIO GPIO_NUM_27 static const char *TAG = "Buzzer"; +// --- Configuração de modo --- +typedef enum { + BUZZER_MODE_ACTIVE = 0, + BUZZER_MODE_PASSIVE +} buzzer_mode_t; + +typedef struct { + buzzer_mode_t mode; + gpio_num_t gpio; + + // Apenas para PASSIVE + uint32_t freq_hz; // ex: 4000 + uint8_t duty_percent; // 0–100 + ledc_mode_t ledc_speed_mode; // LEDC_LOW_SPEED_MODE + ledc_timer_t ledc_timer; // LEDC_TIMER_1 + ledc_channel_t ledc_channel; // LEDC_CHANNEL_1 + ledc_timer_bit_t duty_resolution; // LEDC_TIMER_10_BIT +} buzzer_config_t; + +static buzzer_config_t s_buzzer_cfg = { + .mode = BUZZER_MODE_PASSIVE, // Mude para PASSIVE se for buzzer passivo + .gpio = BUZZER_GPIO, + .freq_hz = 4000, + .duty_percent = 50, + .ledc_speed_mode = LEDC_LOW_SPEED_MODE, + .ledc_timer = LEDC_TIMER_1, + .ledc_channel = LEDC_CHANNEL_1, + .duty_resolution = LEDC_TIMER_10_BIT +}; + +// --- Estrutura de passos --- typedef struct { uint16_t on_ms; uint16_t off_ms; @@ -39,9 +71,37 @@ static const buzzer_pattern_t buzzer_patterns[] = { [BUZZER_PATTERN_CARD_DENIED] = {pattern_card_denied, sizeof(pattern_card_denied) / sizeof(buzzer_step_t)}, }; -static void buzzer_on(void) { gpio_set_level(BUZZER_GPIO, 1); } -static void buzzer_off(void) { gpio_set_level(BUZZER_GPIO, 0); } +// --- Funções de controle --- +static inline uint32_t duty_from_percent(uint8_t pct, ledc_timer_bit_t res) { + if (pct == 0) return 0; + uint32_t max = (1U << res) - 1U; + return (uint32_t)((pct * max) / 100U); +} +static void buzzer_on(void) { + if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE) { + ledc_set_freq(s_buzzer_cfg.ledc_speed_mode, + s_buzzer_cfg.ledc_timer, + s_buzzer_cfg.freq_hz); + ledc_set_duty(s_buzzer_cfg.ledc_speed_mode, + s_buzzer_cfg.ledc_channel, + duty_from_percent(s_buzzer_cfg.duty_percent, s_buzzer_cfg.duty_resolution)); + ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel); + } else { + gpio_set_level(s_buzzer_cfg.gpio, 1); + } +} + +static void buzzer_off(void) { + if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE) { + ledc_set_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel, 0); + ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel); + } else { + gpio_set_level(s_buzzer_cfg.gpio, 0); + } +} + +// --- Execução de padrões --- static void buzzer_execute(buzzer_pattern_id_t pattern_id) { if ((int)pattern_id <= BUZZER_PATTERN_NONE || pattern_id >= BUZZER_PATTERN_MAX) { ESP_LOGW(TAG, "Invalid buzzer pattern id: %d", pattern_id); @@ -60,6 +120,7 @@ static void buzzer_execute(buzzer_pattern_id_t pattern_id) { } } +// --- Event Handlers --- static void buzzer_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { if (base != BUZZER_EVENTS || id != BUZZER_EVENT_PLAY_PATTERN || data == NULL) return; @@ -107,7 +168,6 @@ static void network_event_handler(void *handler_args, esp_event_base_t base, int } } - static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *event_data) { if (base != AUTH_EVENTS || event_data == NULL) return; @@ -117,7 +177,6 @@ static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, voi const auth_tag_event_data_t *evt = (const auth_tag_event_data_t *)event_data; 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) { buzzer_evt.pattern = BUZZER_PATTERN_CARD_ADD; } else { @@ -127,15 +186,41 @@ static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, voi esp_event_post(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, &buzzer_evt, sizeof(buzzer_evt), portMAX_DELAY); } +// --- Inicialização --- void buzzer_init(void) { - gpio_config_t io = { - .pin_bit_mask = BIT64(BUZZER_GPIO), - .mode = GPIO_MODE_OUTPUT, - .pull_down_en = 0, - .pull_up_en = 0, - .intr_type = GPIO_INTR_DISABLE - }; - gpio_config(&io); + if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE) { + // Configura temporizador do PWM + ledc_timer_config_t tcfg = { + .speed_mode = s_buzzer_cfg.ledc_speed_mode, + .duty_resolution = s_buzzer_cfg.duty_resolution, + .timer_num = s_buzzer_cfg.ledc_timer, + .freq_hz = s_buzzer_cfg.freq_hz, + .clk_cfg = LEDC_AUTO_CLK + }; + ESP_ERROR_CHECK(ledc_timer_config(&tcfg)); + + // Configura canal PWM + ledc_channel_config_t ccfg = { + .gpio_num = s_buzzer_cfg.gpio, + .speed_mode = s_buzzer_cfg.ledc_speed_mode, + .channel = s_buzzer_cfg.ledc_channel, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = s_buzzer_cfg.ledc_timer, + .duty = 0, + .hpoint = 0 + }; + ESP_ERROR_CHECK(ledc_channel_config(&ccfg)); + } else { + gpio_config_t io = { + .pin_bit_mask = (1ULL << s_buzzer_cfg.gpio), + .mode = GPIO_MODE_OUTPUT, + .pull_down_en = 0, + .pull_up_en = 0, + .intr_type = GPIO_INTR_DISABLE + }; + gpio_config(&io); + } + buzzer_off(); // Registro de handlers @@ -155,14 +240,12 @@ void buzzer_init(void) { AUTH_EVENT_TAG_SAVED, auth_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_EVENTS, + NETWORK_EVENT_AP_STARTED, + network_event_handler, + NULL)); - ESP_ERROR_CHECK(esp_event_handler_register( - NETWORK_EVENTS, - NETWORK_EVENT_AP_STARTED, - network_event_handler, - NULL - )); - - - ESP_LOGI(TAG, "Buzzer initialized on GPIO %d", BUZZER_GPIO); + ESP_LOGI(TAG, "Buzzer initialized on GPIO %d (%s)", + s_buzzer_cfg.gpio, + s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE ? "passive/PWM" : "active/ON-OFF"); } diff --git a/components/evse/evse_config.c b/components/evse/evse_config.c index 5128ea9..1ae6bae 100755 --- a/components/evse/evse_config.c +++ b/components/evse/evse_config.c @@ -1,7 +1,8 @@ -#include // For PRI macros +#include // For PRI macros #include "evse_config.h" #include "board_config.h" #include "evse_limits.h" +#include "evse_api.h" // <— para evse_get_state / evse_state_is_charging #include "esp_log.h" #include "nvs.h" #include "esp_timer.h" @@ -13,50 +14,63 @@ 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; +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; + +// Availability / Enable flags +static bool is_available = true; +static bool is_enabled = true; // ======================== // Initialization // ======================== -esp_err_t evse_config_init(void) { +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) { +void evse_check_defaults(void) +{ esp_err_t err; uint8_t u8; uint16_t u16; uint32_t u32; bool needs_commit = false; + uint8_t u8_bool; 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) { + 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 { + } + 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)) { + 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 { + } + else + { charging_current = u16; } @@ -67,7 +81,8 @@ void evse_check_defaults(void) { // Auth required err = nvs_get_u8(nvs, "require_auth", &u8); require_auth = (err == ESP_OK && u8 <= 1) ? u8 : false; - if (err != ESP_OK) { + if (err != ESP_OK) + { nvs_set_u8(nvs, "require_auth", require_auth); needs_commit = true; } @@ -75,7 +90,8 @@ void evse_check_defaults(void) { // Socket outlet err = nvs_get_u8(nvs, "socket_outlet", &u8); socket_outlet = (err == ESP_OK && u8) && board_config.proximity; - if (err != ESP_OK) { + if (err != ESP_OK) + { nvs_set_u8(nvs, "socket_outlet", socket_outlet); needs_commit = true; } @@ -83,7 +99,8 @@ void evse_check_defaults(void) { // RCM err = nvs_get_u8(nvs, "rcm", &u8); rcm = (err == ESP_OK && u8) && board_config.rcm; - if (err != ESP_OK) { + if (err != ESP_OK) + { nvs_set_u8(nvs, "rcm", rcm); needs_commit = true; } @@ -91,7 +108,8 @@ void evse_check_defaults(void) { // 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) { + if (err != ESP_OK) + { nvs_set_u8(nvs, "temp_threshold", temp_threshold); needs_commit = true; } @@ -106,12 +124,42 @@ void evse_check_defaults(void) { if (nvs_get_u16(nvs, "def_un_pwr_lim", &u16) == ESP_OK) evse_set_under_power_limit(u16); + // Availability (persist) + if (nvs_get_u8(nvs, "available", &u8_bool) == ESP_OK && u8_bool <= 1) + { + is_available = (u8_bool != 0); + } + else + { + is_available = true; // default + nvs_set_u8(nvs, "available", (uint8_t)is_available); + needs_commit = true; + ESP_LOGW(TAG, "Missing 'available' -> default=true (persisted)."); + } + + // Enabled (persist) + if (nvs_get_u8(nvs, "enabled", &u8_bool) == ESP_OK && u8_bool <= 1) + { + is_enabled = (u8_bool != 0); + } + else + { + is_enabled = true; // default + nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled); + needs_commit = true; + ESP_LOGW(TAG, "Missing 'enabled' -> default=true (persisted)."); + } + // Save to NVS if needed - if (needs_commit) { + if (needs_commit) + { err = nvs_commit(nvs); - if (err == ESP_OK) { + if (err == ESP_OK) + { ESP_LOGD(TAG, "Configuration committed to NVS."); - } else { + } + else + { ESP_LOGE(TAG, "Failed to commit configuration to NVS: %s", esp_err_to_name(err)); } } @@ -120,11 +168,13 @@ void evse_check_defaults(void) { // ======================== // Charging current getters/setters // ======================== -uint8_t evse_get_max_charging_current(void) { +uint8_t evse_get_max_charging_current(void) +{ return max_charging_current; } -esp_err_t evse_set_max_charging_current(uint8_t value) { +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; @@ -133,11 +183,13 @@ esp_err_t evse_set_max_charging_current(uint8_t value) { return nvs_commit(nvs); } -uint16_t evse_get_charging_current(void) { +uint16_t evse_get_charging_current(void) +{ return charging_current; } -esp_err_t evse_set_charging_current(uint16_t value) { +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; @@ -145,14 +197,16 @@ esp_err_t evse_set_charging_current(uint16_t value) { return nvs_commit(nvs); } -uint16_t evse_get_default_charging_current(void) { +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) { +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); @@ -162,49 +216,51 @@ esp_err_t evse_set_default_charging_current(uint16_t value) { // ======================== // Runtime current (not saved) // ======================== -void evse_set_runtime_charging_current(uint16_t value) { +void evse_set_runtime_charging_current(uint16_t value) +{ - ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime); - - if (value > max_charging_current) { + if (value > max_charging_current) + { value = max_charging_current; - } else if (value < MIN_CHARGING_CURRENT_LIMIT) { + } + 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() - }; + .charging = evse_state_is_charging(evse_get_state()), + .hw_max_current = (float)evse_get_max_charging_current(), + .runtime_current = (float)evse_get_runtime_charging_current(), + .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) { +uint16_t evse_get_runtime_charging_current(void) +{ return charging_current_runtime; } - // ======================== // Socket outlet // ======================== -bool evse_get_socket_outlet(void) { +bool evse_get_socket_outlet(void) +{ return socket_outlet; } -esp_err_t evse_set_socket_outlet(bool value) { +esp_err_t evse_set_socket_outlet(bool value) +{ if (value && !board_config.proximity) return ESP_ERR_INVALID_ARG; socket_outlet = value; @@ -215,11 +271,13 @@ esp_err_t evse_set_socket_outlet(bool value) { // ======================== // RCM // ======================== -bool evse_is_rcm(void) { +bool evse_is_rcm(void) +{ return rcm; } -esp_err_t evse_set_rcm(bool value) { +esp_err_t evse_set_rcm(bool value) +{ if (value && !board_config.rcm) return ESP_ERR_INVALID_ARG; rcm = value; @@ -230,11 +288,13 @@ esp_err_t evse_set_rcm(bool value) { // ======================== // Temperature // ======================== -uint8_t evse_get_temp_threshold(void) { +uint8_t evse_get_temp_threshold(void) +{ return temp_threshold; } -esp_err_t evse_set_temp_threshold(uint8_t value) { +esp_err_t evse_set_temp_threshold(uint8_t value) +{ if (value < 40 || value > 80) return ESP_ERR_INVALID_ARG; temp_threshold = value; @@ -242,29 +302,58 @@ esp_err_t evse_set_temp_threshold(uint8_t value) { return nvs_commit(nvs); } - // ======================== // Availability // ======================== -static bool is_available = true; - -bool evse_config_is_available(void) { +bool evse_config_is_available(void) +{ return is_available; } -void evse_config_set_available(bool available) { - is_available = available; +void evse_config_set_available(bool available) +{ + is_available = available ? true : false; + + // Persist + esp_err_t err = nvs_set_u8(nvs, "available", (uint8_t)is_available); + if (err == ESP_OK) + err = nvs_commit(nvs); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist 'available': %s", esp_err_to_name(err)); + } + + // AVAILABLE_UPDATED + evse_available_event_data_t e = { + .available = is_available, + .timestamp_us = esp_timer_get_time()}; + esp_event_post(EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, &e, sizeof(e), portMAX_DELAY); } // ======================== // Enable/Disable // ======================== -static bool is_enabled = true; - -bool evse_config_is_enabled(void) { +bool evse_config_is_enabled(void) +{ return is_enabled; } -void evse_config_set_enabled(bool enabled) { - is_enabled = enabled; +void evse_config_set_enabled(bool enabled) +{ + is_enabled = enabled ? true : false; + + // Persist + esp_err_t err = nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled); + if (err == ESP_OK) + err = nvs_commit(nvs); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist 'enabled': %s", esp_err_to_name(err)); + } + + // ENABLE_UPDATED + evse_enable_event_data_t e = { + .enabled = is_enabled, + .timestamp_us = esp_timer_get_time()}; + esp_event_post(EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, &e, sizeof(e), portMAX_DELAY); } diff --git a/components/evse/evse_core.c b/components/evse/evse_core.c index 53622cb..acef213 100755 --- a/components/evse/evse_core.c +++ b/components/evse/evse_core.c @@ -61,9 +61,7 @@ void evse_process(void) { 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)); + //ESP_LOGI(TAG, "State changed: %s → %s", evse_state_to_str(last_state), evse_state_to_str(current)); last_state = current; } diff --git a/components/evse/evse_fsm.c b/components/evse/evse_fsm.c index ac8bd5c..875a8d1 100755 --- a/components/evse/evse_fsm.c +++ b/components/evse/evse_fsm.c @@ -83,12 +83,13 @@ static void update_outputs(evse_state_t state) { break; case EVSE_STATE_C1: - case EVSE_STATE_D1: - pilot_set_level(true); + case EVSE_STATE_D1: { + pilot_set_amps(MIN(current, cable_max_current)); // mantém PWM + ac_relay_set_state(false); // relé ainda desligado 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)); diff --git a/components/evse/evse_manager.c b/components/evse/evse_manager.c index b569c9a..5aeb10e 100755 --- a/components/evse/evse_manager.c +++ b/components/evse/evse_manager.c @@ -6,6 +6,7 @@ #include "evse_api.h" #include "evse_meter.h" #include "evse_session.h" +#include "evse_config.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -16,6 +17,7 @@ #include "auth_events.h" #include "loadbalancer_events.h" +#include "ocpp_events.h" #include "esp_event.h" static const char *TAG = "EVSE_Manager"; @@ -23,66 +25,142 @@ 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 +#define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo // ===== Task de ciclo principal ===== -static void evse_manager_task(void *arg) { - while (true) { +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; +static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + if (base != AUTH_EVENTS || !data) + 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; + auth_mode_t g_mode = AUTH_MODE_OPEN; + + switch (id) + { + case AUTH_EVENT_TAG_PROCESSED: + { + const auth_tag_event_data_t *evt = (const auth_tag_event_data_t *)data; + ESP_LOGI(TAG, "Tag %s -> %s", evt->tag, evt->authorized ? "AUTHORIZED" : "DENIED"); + evse_state_set_authorized(evt->authorized); + break; + } + + case AUTH_EVENT_MODE_CHANGED: + case AUTH_EVENT_INIT: + { + const auth_mode_event_data_t *evt = (const auth_mode_event_data_t *)data; + g_mode = evt->mode; + ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(g_mode)); + if (g_mode == AUTH_MODE_OPEN) + { + evse_state_set_authorized(true); + auth_enabled = false; } - - 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; + else + { + evse_state_set_authorized(false); + auth_enabled = true; } + 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; +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; + } + 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); - } + } +} + +static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + if (base != OCPP_EVENTS) + return; + + switch (id) + { + case OCPP_EVENT_AUTHORIZED: + ESP_LOGI(TAG, "[OCPP] Authorized"); + evse_state_set_authorized(true); + break; + + case OCPP_EVENT_AUTH_REJECTED: + ESP_LOGW(TAG, "[OCPP] Authorization rejected"); + evse_state_set_authorized(false); + break; + + case OCPP_EVENT_AUTH_TIMEOUT: + ESP_LOGW(TAG, "[OCPP] Authorization timeout"); + evse_state_set_authorized(false); + break; + + case OCPP_EVENT_REMOTE_START: + ESP_LOGI(TAG, "[OCPP] RemoteStart"); + evse_state_set_authorized(true); + break; + + case OCPP_EVENT_REMOTE_STOP: + ESP_LOGI(TAG, "[OCPP] RemoteStop"); + evse_state_set_authorized(false); + break; + + case OCPP_EVENT_START_TX: + ESP_LOGI(TAG, "[OCPP] StartTx"); + break; + + case OCPP_EVENT_STOP_TX: + ESP_LOGI(TAG, "[OCPP] StopTx"); + evse_state_set_authorized(false); + break; + + // hegou ChangeAvailability remoto (operative/inoperative) + case OCPP_EVENT_OPERATIVE_UPDATED: + { + if (!data) + { + ESP_LOGW(TAG, "[OCPP] OperativeUpdated sem payload — ignorado"); + break; + } + const ocpp_operative_event_t *ev = (const ocpp_operative_event_t *)data; + ESP_LOGI(TAG, "[OCPP] OperativeUpdated: operative=%d ts=%lld", + (int)ev->operative, (long long)ev->timestamp_us); + + // Mapear operative → enabled local (persiste e emite EVSE_EVENT_ENABLE_UPDATED) + evse_config_set_enabled(ev->operative); + break; + } + + default: + ESP_LOGD(TAG, "[OCPP] Unhandled event id=%" PRId32, id); + break; + } } // ===== Inicialização ===== -void evse_manager_init(void) { +void evse_manager_init(void) +{ evse_mutex = xSemaphoreCreateMutex(); evse_config_init(); @@ -94,13 +172,15 @@ void evse_manager_init(void) { 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_ERROR_CHECK(esp_event_handler_register(OCPP_EVENTS, ESP_EVENT_ANY_ID, &on_ocpp_event, NULL)); // <— AQUI ESP_LOGI(TAG, "EVSE Manager inicializado."); xTaskCreate(evse_manager_task, "evse_manager_task", 4096, NULL, 5, NULL); } // ===== Main Tick ===== -void evse_manager_tick(void) { +void evse_manager_tick(void) +{ xSemaphoreTake(evse_mutex, portMAX_DELAY); evse_hardware_tick(); @@ -109,15 +189,20 @@ void evse_manager_tick(void) { evse_temperature_check(); evse_session_tick(); - if (auth_enabled) { + if (auth_enabled) + { // If the car is disconnected, revoke authorization - if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A) { + if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A) + { ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization."); evse_state_set_authorized(false); } - } else { + } + else + { // If authentication is disabled, ensure authorization is always granted - if (!evse_state_get_authorized()) { + if (!evse_state_get_authorized()) + { evse_state_set_authorized(true); ESP_LOGI(TAG, "Authentication disabled → forced authorization."); } diff --git a/components/evse/evse_meter.c b/components/evse/evse_meter.c index 2adb91f..8764695 100644 --- a/components/evse/evse_meter.c +++ b/components/evse/evse_meter.c @@ -10,50 +10,57 @@ static const char *TAG = "evse_meter"; static SemaphoreHandle_t meter_mutex; -typedef struct { +typedef struct +{ uint32_t power_watts[EVSE_METER_PHASE_COUNT]; - float voltage[EVSE_METER_PHASE_COUNT]; - float current[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 evse_meter_data_t meter_data; -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) { +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) { + if (strcmp(evt->source, "EVSE") == 0) + { evse_meter_on_meter_event(arg, data); } } } -void evse_meter_on_meter_event(void* arg, void* event_data) { +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; + if (!evt) + return; xSemaphoreTake(meter_mutex, portMAX_DELAY); - for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) { + 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.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 - ); + "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); } -void evse_meter_init(void) { +void evse_meter_init(void) +{ meter_mutex = xSemaphoreCreateMutex(); ESP_ERROR_CHECK(meter_mutex ? ESP_OK : ESP_FAIL); ESP_ERROR_CHECK(esp_event_handler_register( @@ -63,42 +70,51 @@ void evse_meter_init(void) { ESP_LOGI(TAG, "EVSE Meter listener registered."); } -int evse_meter_get_instant_power(void) { +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) { + 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) { +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]) { +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) { + 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]) { +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) { + 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]) { +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) { + for (int i = 0; i < EVSE_METER_PHASE_COUNT; ++i) + { current[i] = meter_data.current[i]; } xSemaphoreGive(meter_mutex); diff --git a/components/evse/evse_pilot.c b/components/evse/evse_pilot.c index 63bd7d4..525a6ff 100755 --- a/components/evse/evse_pilot.c +++ b/components/evse/evse_pilot.c @@ -61,7 +61,7 @@ void pilot_init(void) }; 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)); + //ESP_ERROR_CHECK(ledc_fade_func_install(0)); // Inicializa ADC121S021 externo adc121s021_dma_init(); @@ -107,6 +107,12 @@ void pilot_set_amps(uint16_t amps) ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL); } +bool pilot_get_state(void) { + // true se estamos em 12V fixo; false se PWM ou -12V + // Se quiser diferenciar PWM, guarde um flag quando chamar set_amps. + return (last_pilot_level == 1) && (last_pwm_duty == 0); +} + static int compare_int(const void *a, const void *b) { diff --git a/components/evse/include/evse_events.h b/components/evse/include/evse_events.h index 2f68906..85e8823 100755 --- a/components/evse/include/evse_events.h +++ b/components/evse/include/evse_events.h @@ -10,6 +10,8 @@ typedef enum { EVSE_EVENT_INIT, EVSE_EVENT_STATE_CHANGED, EVSE_EVENT_CONFIG_UPDATED, + EVSE_EVENT_ENABLE_UPDATED, + EVSE_EVENT_AVAILABLE_UPDATED, } evse_event_id_t; typedef enum { @@ -30,4 +32,15 @@ typedef struct { int64_t timestamp_us; // Momento da atualização } evse_config_event_data_t; +// Eventos simples e específicos +typedef struct { + bool enabled; // novo estado de enabled + int64_t timestamp_us; // epoch micros +} evse_enable_event_data_t; + +typedef struct { + bool available; // novo estado de available + int64_t timestamp_us; // epoch micros +} evse_available_event_data_t; + #endif // EVSE_EVENTS_H diff --git a/components/evse/include/evse_pilot.h b/components/evse/include/evse_pilot.h index 5c6bc2c..d541183 100755 --- a/components/evse/include/evse_pilot.h +++ b/components/evse/include/evse_pilot.h @@ -35,7 +35,7 @@ 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) + * @param amps Corrente em ampères (ex: 16 = 16A) */ void pilot_set_amps(uint16_t amps); diff --git a/components/evse_link/src/evse_link.c b/components/evse_link/src/evse_link.c index b70d570..c67d6aa 100755 --- a/components/evse_link/src/evse_link.c +++ b/components/evse_link/src/evse_link.c @@ -11,19 +11,19 @@ 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_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_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; +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; @@ -33,71 +33,100 @@ 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) { + 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) { + 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) { +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) { +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) { + 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_MASTER || mode == EVSE_LINK_MODE_SLAVE)) + { _mode = (evse_link_mode_t)mode; } - if (nvs_get_u8(handle, _NVS_ID_KEY, &id) == EV_OK) { + 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) { + 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) { +static void save_link_config(void) +{ nvs_handle_t handle; - if (nvs_open(_NVS_NAMESPACE, NVS_READWRITE, &handle) == EV_OK) { + 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 { + } + 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; } +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) { +static void evse_link_rx_task(void *arg) +{ uint8_t buf[UART_RX_BUF_SIZE]; - while (true) { + 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) { + if (len > 0) + { + for (int i = 0; i < len; ++i) + { evse_link_recv_byte(buf[i]); } } @@ -105,13 +134,15 @@ static void evse_link_rx_task(void *arg) { } // Initialize EVSE-Link component -void evse_link_init(void) { +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; + if (!_enabled) + return; // 1) framing layer init (sets up mutex, UART driver, etc.) evse_link_framing_init(); @@ -121,22 +152,27 @@ void evse_link_init(void) { xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL); // 3) delegate to master or slave - if (_mode == EVSE_LINK_MODE_MASTER) { + if (_mode == EVSE_LINK_MODE_MASTER) + { evse_link_master_init(); - } else { + } + 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; +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) { +void evse_link_recv_byte(uint8_t byte) +{ evse_link_framing_recv_byte(byte); } diff --git a/components/loadbalancer/src/loadbalancer.c b/components/loadbalancer/src/loadbalancer.c index a9b9b99..44f663b 100755 --- a/components/loadbalancer/src/loadbalancer.c +++ b/components/loadbalancer/src/loadbalancer.c @@ -11,7 +11,7 @@ #include "meter_events.h" #include "evse_events.h" #include "math.h" -#include // Necessário para PRIu64 +#include // Necessário para PRIu64 #ifndef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) @@ -20,10 +20,10 @@ 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 +#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; @@ -38,15 +38,16 @@ static input_filter_t evse_filter; #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; +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]; @@ -58,27 +59,26 @@ static void init_connectors(void) { // master em índice 0 connectors[0] = (evse_connector_t){ - .id = 0xFF, - .is_master = true, - .charging = false, + .id = 0xFF, + .is_master = true, + .charging = false, .hw_max_current = MAX_CHARGING_CURRENT_LIMIT, .runtime_current = 0, - .timestamp = 0, - .online = false, - .assigned = 0.0f - }; + .timestamp = 0, + .online = false, + .assigned = 0.0f}; // slaves em 1..CONNECTOR_COUNT-1 - for (int i = 1; i < CONNECTOR_COUNT; i++) { + for (int i = 1; i < CONNECTOR_COUNT; i++) + { connectors[i] = (evse_connector_t){ - .id = (uint8_t)(i - 1), - .is_master = false, - .charging = false, + .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 - }; + .timestamp = 0, + .online = false, + .assigned = 0.0f}; } } @@ -93,17 +93,18 @@ static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id { const loadbalancer_slave_status_event_t *status = (const loadbalancer_slave_status_event_t *)data; - if (status->slave_id >= MAX_SLAVES) { + 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].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; + 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", @@ -111,31 +112,24 @@ static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id status->hw_max_current, status->runtime_current); } - - - -static void on_evse_config_event(void* handler_arg, +static void on_evse_config_event(void *handler_arg, esp_event_base_t base, int32_t id, - void* event_data) + void *event_data) { - const evse_config_event_data_t *evt = (const evse_config_event_data_t*) 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].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; - + 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, @@ -149,13 +143,18 @@ static void loadbalancer_meter_event_handler(void *handler_arg, 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) { + 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); } } @@ -175,7 +174,7 @@ static void loadbalancer_evse_event_handler(void *handler_arg, 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 + connectors[0].online = true; // master está sempre online break; case EVSE_STATE_EVENT_CHARGING: @@ -184,15 +183,15 @@ static void loadbalancer_evse_event_handler(void *handler_arg, evse_current = 0.0f; input_filter_reset(&grid_filter); input_filter_reset(&evse_filter); - connectors[0].charging = true; - connectors[0].online = true; + connectors[0].charging = true; + connectors[0].online = true; connectors[0].timestamp = esp_timer_get_time(); break; case EVSE_STATE_EVENT_FAULT: 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 + connectors[0].online = true; // EVSE está online mas com falha break; default: @@ -201,8 +200,6 @@ static void loadbalancer_evse_event_handler(void *handler_arg, } } - - // --- Config persistência --- static esp_err_t loadbalancer_load_config() { @@ -215,7 +212,8 @@ static esp_err_t loadbalancer_load_config() 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, "max_grid_curr", max_grid_current); needs_commit = true; @@ -223,7 +221,8 @@ static esp_err_t loadbalancer_load_config() 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, "enabled", 0); needs_commit = true; @@ -238,13 +237,14 @@ static esp_err_t loadbalancer_load_config() void loadbalancer_set_enabled(bool enabled) { nvs_handle_t handle; - if (nvs_open("loadbalancing", NVS_READWRITE, &handle) == ESP_OK) { + 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() }; + 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); } @@ -276,8 +276,10 @@ bool loadbalancer_is_enabled(void) // --- Task principal --- void loadbalancer_task(void *param) { - while (true) { - if (!loadbalancer_is_enabled()) { + while (true) + { + if (!loadbalancer_is_enabled()) + { vTaskDelay(pdMS_TO_TICKS(5000)); continue; } @@ -287,16 +289,19 @@ void loadbalancer_task(void *param) int64_t now = esp_timer_get_time(); // --- Atualiza estado online e conta ativos --- - for (int i = 0; i < CONNECTOR_COUNT; i++) { + for (int i = 0; i < CONNECTOR_COUNT; i++) + { // --- Master nunca pode ficar offline --- - if (connectors[i].is_master) { + 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) { + if (connectors[i].charging) + { idxs[active_cnt++] = i; ESP_LOGI(TAG, "Connector[%d] is ACTIVE (charging)", i); } @@ -304,12 +309,14 @@ void loadbalancer_task(void *param) } // --- Ignora conectores já marcados como offline --- - if (!connectors[i].online) { + if (!connectors[i].online) + { continue; } // --- Timeout de heartbeat para escravos --- - if ((now - connectors[i].timestamp) >= METRICS_TIMEOUT_US) { + 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)); @@ -320,7 +327,8 @@ void loadbalancer_task(void *param) i, connectors[i].charging, connectors[i].hw_max_current, (long long)(now - connectors[i].timestamp)); - if (connectors[i].charging) { + if (connectors[i].charging) + { idxs[active_cnt++] = i; ESP_LOGI(TAG, "Connector[%d] is ACTIVE (charging)", i); } @@ -330,17 +338,23 @@ void loadbalancer_task(void *param) // --- Calcula corrente disponível --- float available = max_grid_current - grid_current; - if (available < MIN_CHARGING_CURRENT_LIMIT) { + 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); // --- 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) { + 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; @@ -351,15 +365,20 @@ void loadbalancer_task(void *param) // --- Distribui corrente (water-filling) --- float remaining = available; int remaining_cnt = active_cnt; - for (int k = 0; k < active_cnt; k++) { + for (int k = 0; k < active_cnt; k++) + { int i = idxs[k]; float share = remaining / remaining_cnt; - if (share >= connectors[i].hw_max_current) { + 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++) { + } + else + { + for (int m = k; m < active_cnt; m++) + { connectors[idxs[m]].assigned = share; } break; @@ -367,39 +386,44 @@ void loadbalancer_task(void *param) } // --- Aplica piso mínimo --- - for (int k = 0; k < active_cnt; k++) { + for (int k = 0; k < active_cnt; k++) + { int i = idxs[k]; - if (connectors[i].assigned < MIN_CHARGING_CURRENT_LIMIT) { + 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++) { + 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) { + if (current_rounded == max_cur) + { continue; // sem alteração } - if (connectors[i].is_master) { + if (connectors[i].is_master) + { loadbalancer_master_limit_event_t master_evt = { - .slave_id = connectors[i].id, - .max_current = max_cur, - .timestamp_us = now - }; + .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 { + } + else + { loadbalancer_slave_limit_event_t slave_evt = { - .slave_id = connectors[i].id, - .max_current = max_cur, - .timestamp_us = now - }; + .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)", @@ -432,9 +456,9 @@ void loadbalancer_init(void) 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(EVSE_EVENTS, EVSE_EVENT_CONFIG_UPDATED, + &on_evse_config_event, NULL)); - ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS,LOADBALANCER_EVENT_SLAVE_STATUS, + ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_STATUS, &on_slave_status, NULL)); } diff --git a/components/ocpp/CMakeLists.txt b/components/ocpp/CMakeLists.txt index be0cd8f..a60a825 100755 --- a/components/ocpp/CMakeLists.txt +++ b/components/ocpp/CMakeLists.txt @@ -1,8 +1,9 @@ set(srcs + "src/ocpp_events.c" "src/ocpp.c" ) idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" PRIV_REQUIRES nvs_flash - REQUIRES config esp_wifi evse mongoose MicroOcpp MicroOcppMongoose) + REQUIRES esp_event config esp_wifi evse mongoose MicroOcpp MicroOcppMongoose) diff --git a/components/ocpp/include/ocpp.h b/components/ocpp/include/ocpp.h index 6d81319..f9fc0b8 100755 --- a/components/ocpp/include/ocpp.h +++ b/components/ocpp/include/ocpp.h @@ -4,41 +4,35 @@ #include #include -/** - * @brief Start ocpp - */ -void ocpp_start(); +#ifdef __cplusplus +extern "C" { +#endif /** - * @brief Stop ocpp - * + * @brief Start OCPP + */ +void ocpp_start(void); + +/** + * @brief Stop OCPP */ void ocpp_stop(void); +/* Config getters / setters */ bool ocpp_get_enabled(void); - -void ocpp_get_server(char *value); - -void ocpp_get_rfid(char *value); - void ocpp_set_enabled(bool value); +void ocpp_get_server(char *value); // buffer >= 64 void ocpp_set_server(char *value); -void ocpp_set_rfid(char *value); +void ocpp_get_charge_id(char *value); // buffer >= 64 +void ocpp_set_charge_id(char *value); -void ocpp_begin_transaction(char *value); +/* Estado de conexão */ +bool ocpp_is_connected(void); -void ocpp_end_transaction(char *value); - -void ocpp_begin_transaction_authorized(char *value); - -void ocpp_end_transaction_authorized(char *value); - -bool ocpp_is_TransactionActive(); - -void ocpp_set_plugged(bool value); - -void ocpp_set_charging(bool value); +#ifdef __cplusplus +} +#endif #endif /* OCPP_H_ */ diff --git a/components/ocpp/include/ocpp_events.h b/components/ocpp/include/ocpp_events.h new file mode 100644 index 0000000..10df544 --- /dev/null +++ b/components/ocpp/include/ocpp_events.h @@ -0,0 +1,53 @@ +#pragma once +#include "esp_event.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Base de eventos do OCPP (igual ao padrão usado em auth_events.h) */ +ESP_EVENT_DECLARE_BASE(OCPP_EVENTS); + +/* IDs de eventos do OCPP */ +typedef enum { + OCPP_EVENT_CONNECTED = 0, // payload: const char* (server URL) – opcional + OCPP_EVENT_DISCONNECTED, // payload: NULL + OCPP_EVENT_AUTHORIZED, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_AUTH_REJECTED, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_AUTH_TIMEOUT, // payload: NULL + OCPP_EVENT_REMOTE_START, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_REMOTE_STOP, // payload: NULL + OCPP_EVENT_START_TX, // payload: ocpp_tx_event_t (opcional) + OCPP_EVENT_STOP_TX, // payload: ocpp_reason_event_t (opcional) + OCPP_EVENT_RESET, // payload: NULL + OCPP_EVENT_OPERATIVE_UPDATED +} ocpp_event_id_t; + +/* Limites de strings simples (evita dependência de auth.h) */ +#define OCPP_IDTAG_MAX 32 +#define OCPP_REASON_MAX 32 + +/* Payloads opcionais */ +typedef struct { + char idTag[OCPP_IDTAG_MAX]; +} ocpp_idtag_event_t; + +typedef struct { + int tx_id; // se disponível +} ocpp_tx_event_t; + +typedef struct { + char reason[OCPP_REASON_MAX]; +} ocpp_reason_event_t; + +// Payload do novo evento +typedef struct { + bool operative; // true = Operative, false = Inoperative + int64_t timestamp_us; // esp_timer_get_time() +} ocpp_operative_event_t; + +#ifdef __cplusplus +} +#endif diff --git a/components/ocpp/src/ocpp.c b/components/ocpp/src/ocpp.c index bda2723..7e6fac2 100755 --- a/components/ocpp/src/ocpp.c +++ b/components/ocpp/src/ocpp.c @@ -1,543 +1,853 @@ +// components/ocpp/src/ocpp.c +#include +#include +#include +#include + #include "esp_log.h" +#include "esp_err.h" #include "ocpp.h" -#include "evse_api.h" -#include "evse_error.h" -#include "evse_state.h" -#include "evse_config.h" +#include "ocpp_events.h" #include "esp_wifi.h" #include "nvs.h" +#include "nvs_flash.h" + +#include "evse_error.h" +#include "auth_events.h" +#include "evse_events.h" +#include "evse_state.h" +#include "meter_events.h" +#include "esp_timer.h" +#include /* MicroOcpp includes */ #include -#include //C-facade of MicroOcpp -#include //WebSocket integration for ESP-IDF +#include // C-facade of MicroOcpp +#include // WebSocket integration for ESP-IDF #define NVS_NAMESPACE "ocpp" - -#define NVS_OCPP_AUTHORIZATION "authorization" #define NVS_OCPP_ENABLED "enabled" #define NVS_OCPP_SERVER "ocpp_server" -#define NVS_OCPP_RFID "ocpp_rfid" - -#define AP_SSID "PLX-%02x%02x%02x" - -static nvs_handle nvs; +#define NVS_OCPP_CHARGE_ID "charge_id" static const char *TAG = "ocpp"; -static bool enabled = true; - -static bool authorization = false; - -// static bool connector_plugged = false; - -// static bool is_charging = false; +static bool enabled = false; static TaskHandle_t ocpp_task = NULL; -static struct mg_mgr mgr; // Event manager +static struct mg_mgr mgr; // Event manager +static OCPP_Connection *g_ocpp_conn = NULL; // Para shutdown limpo +static esp_event_handler_instance_t s_auth_verify_inst = NULL; + +// Flags refletindo o estado do EVSE (atualizadas por eventos) +static volatile bool s_ev_plugged = false; +static volatile bool s_ev_ready = false; + +static esp_event_handler_instance_t s_evse_state_inst = NULL; + +// Flags de config (vindas de EVSE_EVENTS) +static volatile bool s_evse_enabled = true; +static volatile bool s_evse_available = true; + +static esp_event_handler_instance_t s_evse_enable_inst = NULL; +static esp_event_handler_instance_t s_evse_available_inst = NULL; + +// --- cache de medições vindas de METER_EVENT_DATA_READY --- +typedef struct +{ + // dados por fase + float vrms[3]; + float irms[3]; + int32_t watt[3]; // ativo por fase (W) + float frequency; + float power_factor; + // acumulados + float total_energy_kWh; + // derivados práticos + int32_t sum_watt; // soma das 3 fases + float avg_voltage; // média das 3 fases + float sum_current; // soma das 3 fases + // flag de validade + bool have_data; +} ocpp_meter_cache_t; + +static ocpp_meter_cache_t s_meter = {0}; +static portMUX_TYPE s_meter_mux = portMUX_INITIALIZER_UNLOCKED; +static esp_event_handler_instance_t s_meter_inst = NULL; + +/* ========================= + * Task / Main Loop + * ========================= * + */ static void ocpp_task_func(void *param) { - while (true) { - // if (enabled) + if (enabled) { - mg_mgr_poll(&mgr, 10); + mg_mgr_poll(&mgr, 100); ocpp_loop(); - if (evse_config_is_enabled() != ocpp_isOperative()) + bool operative = ocpp_isOperative(); + if (operative != s_evse_enabled) { - printf("ocpp_isOperative()"); - evse_config_set_enabled(ocpp_isOperative()); + s_evse_enabled = operative; + + // >>> enviar OCPP_EVENT (remoto → local) + ocpp_operative_event_t ev = { + .operative = operative, + .timestamp_us = esp_timer_get_time()}; + esp_event_post(OCPP_EVENTS, + OCPP_EVENT_OPERATIVE_UPDATED, + &ev, sizeof(ev), + portMAX_DELAY); + + ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d", + (int)operative); } } - vTaskDelay(pdMS_TO_TICKS(500)); + else + { + vTaskDelay(pdMS_TO_TICKS(500)); + } } } +/* ========================= + * NVS GETs + * ========================= */ bool ocpp_get_enabled(void) { - uint8_t value = false; - nvs_get_u8(nvs, NVS_OCPP_ENABLED, &value); - ESP_LOGI(TAG, "Ocpp get enabled %d", value); - return value; + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + // namespace ainda não existe -> default: disabled + return false; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return false; + } + + uint8_t value = 0; + err = nvs_get_u8(h, NVS_OCPP_ENABLED, &value); + nvs_close(h); + + if (err == ESP_ERR_NVS_NOT_FOUND) + return false; // default + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_get_u8(enabled) failed: %s", esp_err_to_name(err)); + return false; + } + return value != 0; } -bool ocpp_get_authorization(void) +void ocpp_get_server(char *value /* out, size>=64 */) { - uint8_t value = false; - nvs_get_u8(nvs, NVS_OCPP_AUTHORIZATION, &value); - ESP_LOGI(TAG, "Ocpp get authorization %d", value); - return value; -} - -void ocpp_get_server(char *value) -{ - size_t len = 64; + if (!value) + return; value[0] = '\0'; - nvs_get_str(nvs, NVS_OCPP_SERVER, value, &len); - ESP_LOGI(TAG, "Ocpp get server %s", value); -} + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + // namespace ainda não existe -> default: "" + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } -void ocpp_get_rfid(char *value) -{ size_t len = 64; - value[0] = '\0'; - nvs_get_str(nvs, NVS_OCPP_RFID, value, &len); + err = nvs_get_str(h, NVS_OCPP_SERVER, value, &len); + nvs_close(h); - ESP_LOGI(TAG, "Ocpp get rfid %s", value); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + value[0] = '\0'; + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_get_str(server) failed: %s", esp_err_to_name(err)); + value[0] = '\0'; + } } -void ocpp_set_authorization(bool value) +void ocpp_get_charge_id(char *value /* out, size>=64 */) { - ESP_LOGI(TAG, "Ocpp set authorization %d", value); - nvs_set_u8(nvs, NVS_OCPP_ENABLED, value); - nvs_commit(nvs); - authorization = value; + if (!value) + return; + value[0] = '\0'; + + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + // namespace ainda não existe -> default: "" + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + + size_t len = 64; + err = nvs_get_str(h, NVS_OCPP_CHARGE_ID, value, &len); + nvs_close(h); + + if (err == ESP_ERR_NVS_NOT_FOUND) + { + value[0] = '\0'; + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_get_str(charge_id) failed: %s", esp_err_to_name(err)); + value[0] = '\0'; + } } +/* ========================= + * NVS SETs + * ========================= */ +// --- SETTERS: RW (cria o namespace na 1ª escrita), commit e fecha --- void ocpp_set_enabled(bool value) { - ESP_LOGI(TAG, "Ocpp set enabled %d", value); - nvs_set_u8(nvs, NVS_OCPP_ENABLED, value); - nvs_commit(nvs); + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "set enabled %d", value); + ESP_ERROR_CHECK(nvs_set_u8(h, NVS_OCPP_ENABLED, value ? 1 : 0)); + ESP_ERROR_CHECK(nvs_commit(h)); + nvs_close(h); enabled = value; } void ocpp_set_server(char *value) { - ESP_LOGI(TAG, "Ocpp set server %s", value); - - nvs_set_str(nvs, NVS_OCPP_SERVER, value); - - nvs_commit(nvs); + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "set server %s", value ? value : "(null)"); + ESP_ERROR_CHECK(nvs_set_str(h, NVS_OCPP_SERVER, value ? value : "")); + ESP_ERROR_CHECK(nvs_commit(h)); + nvs_close(h); } -void ocpp_set_rfid(char *value) +void ocpp_set_charge_id(char *value) { - ESP_LOGI(TAG, "Ocpp set rfid %s", value); - - nvs_set_str(nvs, NVS_OCPP_RFID, value); - - nvs_commit(nvs); + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "set charge_id %s", value ? value : "(null)"); + ESP_ERROR_CHECK(nvs_set_str(h, NVS_OCPP_CHARGE_ID, value ? value : "")); + ESP_ERROR_CHECK(nvs_commit(h)); + nvs_close(h); } -bool setConnectorPluggedInput() +static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, void *event_data) { - // ESP_LOGI(TAG, "setConnectorPluggedInput"); - return evse_state_is_plugged(evse_get_state()); - // return true; -} + const auth_tag_verify_event_t *rq = (const auth_tag_verify_event_t *)event_data; + if (!rq) + return; -bool setEvReadyInput() -{ - // ESP_LOGI(TAG, "EvReadyInput"); - return evse_state_is_charging(evse_get_state()); - // return is_charging; -} - -bool setEvseReadyInput() -{ - // ESP_LOGI(TAG, "EvseReadyInput"); - return evse_config_is_enabled(); - //return false; -} - -float setPowerMeterInput() -{ - ESP_LOGI(TAG, "PowerMeterInput"); - //MeterData data = meter_getData(); - //return data.wattA + data.wattB + data.wattC; - return 0.0f; -} - -float setEnergyMeterInput() -{ - ESP_LOGI(TAG, "EnergyMeterInput"); - - // Exemplo com valor fixo (pode ser substituído depois) - return 3600.0f; -} - -int setEnergyInput() -{ - ESP_LOGI(TAG, "EnergyInput"); - - // Exemplo com valor fixo - return 3600; -} - -float setCurrentInput() -{ - ESP_LOGI(TAG, "CurrentInput"); - - /* - if (!meter_is_running()) { - ESP_LOGW(TAG, "Meter not running, returning fallback."); - return 0.0f; + // Sanitizar/copiar a idTag + char idtag[AUTH_TAG_MAX_LEN]; + if (rq->tag[0] == '\0') + { + strncpy(idtag, "IDTAG", sizeof(idtag)); + idtag[sizeof(idtag) - 1] = '\0'; + } + else + { + strncpy(idtag, rq->tag, sizeof(idtag) - 1); + idtag[sizeof(idtag) - 1] = '\0'; } - MeterData data = meter_getData(); - return data.irmsA; - */ - return 0; -} + ESP_LOGI(TAG, "AUTH_EVENT_TAG_VERIFY: tag=%s req_id=%u", idtag, (unsigned)rq->req_id); -float setVoltageInput() -{ - ESP_LOGI(TAG, "VoltageInput"); - - /* - if (!meter_is_running()) { - ESP_LOGW(TAG, "Meter not running, returning fallback."); - return 0.0f; + // Se não está pronto, apenas regista e sai (podes adaptar conforme política) + if (!enabled || g_ocpp_conn == NULL) + { + ESP_LOGW(TAG, "OCPP not ready (enabled=%d, conn=%p) – ignoring verify", enabled, (void *)g_ocpp_conn); + return; } - MeterData data = meter_getData(); - return data.vrmsA; - */ - return 0.0f; + // Regra pedida: + // - se já existe transação/charge em andamento -> terminar + // - senão -> iniciar com a IDTAG recebida + if (ocpp_isTransactionActive()) + { + ESP_LOGI(TAG, "Transaction active -> ocpp_end_transaction(\"%s\")", idtag); + ocpp_endTransaction(idtag, "Local"); + } + else + { + ESP_LOGI(TAG, "No active transaction -> ocpp_begin_transaction(\"%s\")", idtag); + ocpp_beginTransaction(idtag); + } } -float setTemperatureInput() +static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { - ESP_LOGI(TAG, "TemperatureInput"); + if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL) + return; + + const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data; + ESP_LOGI(TAG, "EVSE event received: state = %d", (int)evt->state); + + switch (evt->state) + { + case EVSE_STATE_EVENT_IDLE: + s_ev_plugged = false; + s_ev_ready = false; + break; + + case EVSE_STATE_EVENT_WAITING: + s_ev_plugged = true; + s_ev_ready = false; + break; + + case EVSE_STATE_EVENT_CHARGING: + s_ev_plugged = true; + s_ev_ready = true; // EV está a pedir/receber energia + break; + + case EVSE_STATE_EVENT_FAULT: + default: + // em falha, considera não pronto (mantém plugged se quiseres) + s_ev_ready = false; + break; + } +} + +static void evse_enable_available_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + if (base != EVSE_EVENTS || data == NULL) + return; + + if (id == EVSE_EVENT_ENABLE_UPDATED) + { + const evse_enable_event_data_t *e = (const evse_enable_event_data_t *)data; + s_evse_enabled = e->enabled; + ESP_LOGI(TAG, "[EVSE] ENABLE_UPDATED: enabled=%d (ts=%lld)", (int)e->enabled, (long long)e->timestamp_us); + return; + } + + if (id == EVSE_EVENT_AVAILABLE_UPDATED) + { + const evse_available_event_data_t *e = (const evse_available_event_data_t *)data; + s_evse_available = e->available; + ESP_LOGI(TAG, "[EVSE] AVAILABLE_UPDATED: available=%d (ts=%lld)", (int)e->available, (long long)e->timestamp_us); + return; + } +} + +static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + if (base != METER_EVENT || id != METER_EVENT_DATA_READY || !data) + return; + + const meter_event_data_t *evt = (const meter_event_data_t *)data; + + // Só queremos o medidor do EVSE (não o GRID) + if (!evt->source || strcmp(evt->source, "EVSE") != 0) + return; + + // Derivados simples + int32_t sum_w = (int32_t)evt->watt[0] + (int32_t)evt->watt[1] + (int32_t)evt->watt[2]; + float avg_v = (evt->vrms[0] + evt->vrms[1] + evt->vrms[2]) / 3.0f; + float sum_i = evt->irms[0] + evt->irms[1] + evt->irms[2]; + + portENTER_CRITICAL(&s_meter_mux); + memcpy(s_meter.vrms, evt->vrms, sizeof(s_meter.vrms)); + memcpy(s_meter.irms, evt->irms, sizeof(s_meter.irms)); + s_meter.watt[0] = evt->watt[0]; + s_meter.watt[1] = evt->watt[1]; + s_meter.watt[2] = evt->watt[2]; + s_meter.frequency = evt->frequency; + s_meter.power_factor = evt->power_factor; + s_meter.total_energy_kWh = evt->total_energy; // já vem em kWh segundo o teu .h + s_meter.sum_watt = sum_w; + s_meter.avg_voltage = avg_v; + s_meter.sum_current = sum_i; + s_meter.have_data = true; + portEXIT_CRITICAL(&s_meter_mux); +} + +/* ========================= + * MicroOCPP Inputs/CBs + * ========================= */ +bool setConnectorPluggedInput(void) +{ + return s_ev_plugged; // EV fisicamente ligado +} + +bool setEvReadyInput(void) +{ + return s_ev_ready; // EV pede / pronto a carregar +} + +bool setEvseReadyInput(void) +{ + // EVSE autorizado / operacional + return s_evse_enabled && s_evse_available; +} + +float setPowerMeterInput(void) +{ + int32_t w = 0; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + w = s_meter.sum_watt; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + { + ESP_LOGW(TAG, "[METER] PowerMeterInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] PowerMeterInput: %" PRId32 " W", w); + } + return (float)w; +} + +float setEnergyMeterInput(void) +{ + float kwh = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + kwh = s_meter.total_energy_kWh; + portEXIT_CRITICAL(&s_meter_mux); + + float wh = kwh * 1000.0f; + + if (!have) + { + ESP_LOGW(TAG, "[METER] EnergyMeterInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] EnergyMeterInput: %.3f kWh (%.1f Wh)", kwh, wh); + } + return wh; // agora devolve Wh +} + +int setEnergyInput(void) +{ + float energy_kWh = setEnergyMeterInput(); // kWh + int wh = (int)lrintf((double)energy_kWh * 1000.0); // Wh arredondado + ESP_LOGD(TAG, "[METER] EnergyInput: %.3f kWh -> %d Wh", energy_kWh, wh); + return wh; +} + +float setCurrentInput(void) +{ + float a = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + a = s_meter.sum_current; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + { + ESP_LOGW(TAG, "[METER] CurrentInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] CurrentInput: %.2f A (total)", a); + } + return a; +} + +float setVoltageInput(void) +{ + float v = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + v = s_meter.avg_voltage; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + { + ESP_LOGW(TAG, "[METER] VoltageInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] VoltageInput: %.1f V (avg)", v); + } + return v; +} + +float setPowerInput(void) +{ + float w = setPowerMeterInput(); // alias + ESP_LOGD(TAG, "[METER] PowerInput: %.1f W", w); + return w; +} + +float setTemperatureInput(void) +{ + ESP_LOGD(TAG, "TemperatureInput"); return 16.5f; } -float setPowerInput() -{ - ESP_LOGI(TAG, "PowerInput"); - // return (float)orno_modbus_get_meter_state().activepower; - //return meter_getData().wattA + meter_getData().wattB + meter_getData().wattC; - return 0; -} - void setSmartChargingCurrentOutput(float limit) { - ESP_LOGI(TAG, "SmartChargingCurrentOutput: %.0f\n", limit); -}; + ESP_LOGI(TAG, "SmartChargingCurrentOutput: %.0f", limit); +} void setSmartChargingPowerOutput(float limit) { - ESP_LOGI(TAG, "SmartChargingPowerOutput: %.0f\n", limit); -}; + ESP_LOGI(TAG, "SmartChargingPowerOutput: %.0f", limit); +} void setSmartChargingOutput(float power, float current, int nphases) { - ESP_LOGI(TAG, " SmartChargingOutput: %.0f %.0f \n", power, current); -}; + ESP_LOGI(TAG, "SmartChargingOutput: P=%.0f W, I=%.0f A, phases=%d", power, current, nphases); +} void setGetConfiguration(const char *payload, size_t len) { - ESP_LOGI(TAG, " setGetConfiguration: %s %d \n", payload, len); + ESP_LOGI(TAG, "GetConfiguration: %.*s (%u)", (int)len, payload, (unsigned)len); } void setStartTransaction(const char *payload, size_t len) { - ESP_LOGI(TAG, " setStartTransaction: %s %d \n", payload, len); + ESP_LOGI(TAG, "StartTransaction: %.*s (%u)", (int)len, payload, (unsigned)len); } void setChangeConfiguration(const char *payload, size_t len) { - ESP_LOGI(TAG, " setChangeConfiguration: %s %d \n", payload, len); + ESP_LOGI(TAG, "ChangeConfiguration: %.*s (%u)", (int)len, payload, (unsigned)len); } void OnResetExecute(bool state) { - ESP_LOGI(TAG, "#### OnResetExecute"); - //esp_restart(); + ESP_LOGI(TAG, "OnResetExecute (state=%d)", state); + esp_restart(); } -bool setOccupiedInput() +bool setOccupiedInput(void) { - ESP_LOGI(TAG, "setOccupiedInput"); + ESP_LOGD(TAG, "setOccupiedInput"); return false; } -bool setStartTxReadyInput() +bool setStartTxReadyInput(void) { - ESP_LOGI(TAG, "!!!!!! StartTxReadyInput"); - return false; -} - -bool setStopTxReadyInput() -{ - ESP_LOGI(TAG, "===== StopTxReadyInput"); + ESP_LOGD(TAG, "setStartTxReadyInput"); return true; } -/* -enum OptionalBool setOnUnlockConnectorInOut() +bool setStopTxReadyInput(void) { - ESP_LOGI(TAG, "***** OnUnlockConnectorInOut"); - return OptionalTrue; -}*/ + ESP_LOGD(TAG, "setStopTxReadyInput"); + return true; +} bool setOnResetNotify(bool value) { - ESP_LOGI(TAG, "!!!!!! setOnResetNotify %d", value); + ESP_LOGI(TAG, "setOnResetNotify %d", value); return true; } void notificationOutput(OCPP_Transaction *transaction, enum OCPP_TxNotification txNotification) { - ESP_LOGI(TAG, "Set notificationOutput ----> %d", txNotification); + ESP_LOGI(TAG, "TxNotification: %d", txNotification); switch (txNotification) { - // Authorization events case Authorized: - ESP_LOGI(TAG, "<----------- Authorized ---------->"); - //evse_authorize(); - // is_charging = true; - break; // success - case AuthorizationRejected: - ESP_LOGI(TAG, "AuthorizationRejected ---->"); - break; // IdTag not authorized - case AuthorizationTimeout: - ESP_LOGI(TAG, "AuthorizationTimeout ---->"); - break; // authorization failed - offline - case ReservationConflict: - ESP_LOGI(TAG, "ReservationConflict ---->"); - break; // connector reserved for other IdTag - case ConnectionTimeout: - ESP_LOGI(TAG, "ConnectionTimeout ---->"); - break; // user took to long to plug vehicle after the authorization - case DeAuthorized: - ESP_LOGI(TAG, "DeAuthorized ---->"); - //evse_set_authorized(false); - //evse_set_limit_reached(2); - // ocpp_set_charging(false); - break; // server rejected StartTx - case RemoteStart: - ESP_LOGI(TAG, "RemoteStart ---->"); - break; // authorized via RemoteStartTransaction - case RemoteStop: - ESP_LOGI(TAG, "RemoteStop ---->"); - break; // stopped via RemoteStopTransaction + ESP_LOGI(TAG, "Authorized"); + // TODO: send event ocpp Authorized + // evse_authorize(); + + // Opcional: enviar idTag no payload (se tiveres como obter do transaction) + // ocpp_idtag_event_t ev = {0}; + // strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, &ev, sizeof(ev), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, NULL, 0, portMAX_DELAY); + break; + + case AuthorizationRejected: + ESP_LOGI(TAG, "AuthorizationRejected"); + // TODO: send event ocpp AuthorizationRejected + + // ocpp_idtag_event_t ev = {0}; + // strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, &ev, sizeof(ev), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY); + break; + + case AuthorizationTimeout: + ESP_LOGI(TAG, "AuthorizationTimeout"); + // TODO: send event ocpp AuthorizationTimeout + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_TIMEOUT, NULL, 0, portMAX_DELAY); + break; + + case ReservationConflict: + ESP_LOGI(TAG, "ReservationConflict"); + // TODO: send event ocpp ReservationConflict + // (Se quiseres, cria um ID específico no enum e publica aqui) + break; + + case ConnectionTimeout: + ESP_LOGI(TAG, "ConnectionTimeout"); + // TODO: send event ocpp ConnectionTimeout + // (Se quiseres, cria um ID específico no enum e publica aqui) + break; + + case DeAuthorized: + ESP_LOGI(TAG, "DeAuthorized"); + // TODO: send event ocpp DeAuthorized + // TODO: adapt to the new interface + // evse_set_authorized(false); + // evse_set_limit_reached(2); + + // Poderias mapear para AUTH_REJECTED ou STOP_TX por política da aplicação: + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY); + break; + + case RemoteStart: + ESP_LOGI(TAG, "RemoteStart"); + // TODO: send event ocpp RemoteStart + + // ocpp_idtag_event_t ev = {0}; + // strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, &ev, sizeof(ev), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, NULL, 0, portMAX_DELAY); + break; + + case RemoteStop: + ESP_LOGI(TAG, "RemoteStop"); + // TODO: send event ocpp RemoteStop + esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_STOP, NULL, 0, portMAX_DELAY); + break; - // Tx lifecycle events case StartTx: - ESP_LOGI(TAG, "StartTx ---->"); + ESP_LOGI(TAG, "StartTx"); + // TODO: send event ocpp StartTx + + // ocpp_tx_event_t tx = { .tx_id = ocpp_tx_get_id(transaction) }; + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, &tx, sizeof(tx), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, NULL, 0, portMAX_DELAY); break; + case StopTx: - // is_charging = false; - ESP_LOGI(TAG, "StopTx ---->"); - //evse_set_authorized(false); - //evse_set_limit_reached(2); + ESP_LOGI(TAG, "StopTx"); + // TODO: send event ocpp StopTx + // TODO: adapt to the new interface + // evse_set_authorized(false); + // evse_set_limit_reached(2); + + // ocpp_reason_event_t rs = {0}; + // strlcpy(rs.reason, "Local", sizeof(rs.reason)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, &rs, sizeof(rs), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, NULL, 0, portMAX_DELAY); break; - }; + } } -const char *addErrorCodeInput() +// Estado de conexão simples do OCPP +bool ocpp_is_connected(void) { - // ESP_LOGI(TAG, "AddErrorCodeInput"); + // Se quiser algo mais preciso (WS aberto), substitui por uma chamada da MicroOcpp, + // mas como fallback isto já evita o undefined symbol. + return g_ocpp_conn != NULL; +} - char *ptr = NULL; - - uint32_t error = 0; // evse_get_error(); - - // ESP_LOGI(TAG, "AddErrorCodeInput %" PRIu32 "", error); +const char *addErrorCodeInput(void) +{ + const char *ptr = NULL; + uint32_t error = evse_get_error(); if (error & EVSE_ERR_PILOT_FAULT_BIT) - { ptr = "InternalError"; - } else if (error & EVSE_ERR_DIODE_SHORT_BIT) - { ptr = "InternalError"; - } else if (error & EVSE_ERR_LOCK_FAULT_BIT) - { ptr = "ConnectorLockFailure"; - } else if (error & EVSE_ERR_UNLOCK_FAULT_BIT) - { ptr = "ConnectorLockFailure"; - } else if (error & EVSE_ERR_RCM_TRIGGERED_BIT) - { ptr = "OtherError"; - } else if (error & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) - { ptr = "OtherError"; - } else if (error & EVSE_ERR_TEMPERATURE_HIGH_BIT) - { ptr = "HighTemperature"; - } else if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT) - { ptr = "OtherError"; + + return ptr; // NULL => sem erro +} + +/* ========================= + * Start / Stop OCPP + * ========================= */ +void ocpp_start(void) +{ + ESP_LOGI(TAG, "Starting OCPP"); + + if (ocpp_task != NULL) + { + ESP_LOGW(TAG, "OCPP already running"); + return; } - return ptr; -} + enabled = ocpp_get_enabled(); + if (!enabled) + { + ESP_LOGW(TAG, "OCPP disabled"); + return; + } -void ocpp_begin_transaction(char *value) -{ - ocpp_beginTransaction(value); -} - -void ocpp_end_transaction(char *value) -{ - ocpp_endTransaction(value, "Local"); -} - -void ocpp_begin_transaction_authorized(char *value) -{ - ocpp_beginTransaction_authorized(value, NULL); -} - -void ocpp_end_transaction_authorized(char *value) -{ - ocpp_endTransaction_authorized(value, "Local"); -} - -bool ocpp_is_TransactionActive() -{ - return ocpp_isTransactionActive(); -} - -void ocpp_start() -{ - ESP_LOGI(TAG, "Starting ocpp"); - - ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs)); - - // enabled = ocpp_get_enabled(); - authorization = ocpp_get_authorization(); - - char serverstr[64]; + char serverstr[64] = {0}; ocpp_get_server(serverstr); - char rfidstr[64]; - ocpp_get_rfid(rfidstr); + if (serverstr[0] == '\0') + { + ESP_LOGW(TAG, "No OCPP server configured. Skipping connection."); + return; + } - /* Initialize Mongoose (necessary for MicroOcpp)*/ - // struct mg_mgr mgr; // Event manager - mg_mgr_init(&mgr); // Initialise event manager - mg_log_set(MG_LL_ERROR); // Set log level + char charge_id[64] = {0}; + ocpp_get_charge_id(charge_id); + if (charge_id[0] == '\0') + { + ESP_LOGW(TAG, "No OCPP charge_id configured. Skipping connection."); + return; + } + + /* Inicializar Mongoose + MicroOcpp */ + mg_mgr_init(&mgr); + mg_log_set(MG_LL_ERROR); - /* Initialize MicroOcpp */ struct OCPP_FilesystemOpt fsopt = {.use = true, .mount = true, .formatFsOnFail = true}; - char chargeid[12]; - uint8_t mac[6]; - esp_wifi_get_mac(ESP_IF_WIFI_AP, mac); - sprintf((char *)chargeid, AP_SSID, mac[3], mac[4], mac[5]); + g_ocpp_conn = ocpp_makeConnection(&mgr, + serverstr, /* ex: ws://host:port/OCPP16/... */ + charge_id, /* ChargeBoxId / identity */ + "", + "", + fsopt); + if (!g_ocpp_conn) + { + ESP_LOGE(TAG, "ocpp_makeConnection failed"); + mg_mgr_free(&mgr); + return; + } - char *urlid = (char *)malloc(62 * sizeof(char)); - sprintf(urlid, "%s", strupr(chargeid)); - - //"ws://192.168.1.164:3000", - - // ws://192.168.1.115:8010/OCPP16/5c866e81a2d9593de43efdb4/6750f0235b1b277aa16caf19 - - OCPP_Connection *osock = ocpp_makeConnection(&mgr, - "ws://192.168.2.217:8010/OCPP16/5c866e81a2d9593de43efdb4/6750f0235b1b277aa16caf19/", - "Plx_00AA1", - "", - "", fsopt); - - ocpp_initialize(osock, "EPower M1", "Plixin", fsopt, false); - - // ocpp_authorize() - - // ocpp_authorize(rfidstr,NULL,NULL,NULL,NULL,NULL); - - // ocpp_authorize("2D3CF08E", NULL, NULL, NULL, NULL, NULL); - - // 2D3CF08E - - /* - * Load OCPP configs. Default values will be overwritten by OpenEVSE configs. Mark configs - * to require reboot if changed via OCPP server - - freevendActive = ArduinoOcpp::declareConfiguration("AO_FreeVendActive", true, CONFIGURATION_FN, true, true, true, true); - freevendIdTag = ArduinoOcpp::declareConfiguration("AO_FreeVendIdTag", "DefaultIdTag", CONFIGURATION_FN, true, true, true, true); - */ - // allowOfflineTxForUnknownId = ArduinoOcpp::declareConfiguration("AllowOfflineTxForUnknownId", true, CONFIGURATION_FN, true, true, true, true); - // silentOfflineTx = ArduinoOcpp::declareConfiguration("AO_SilentOfflineTransactions", true, CONFIGURATION_FN, true, true, true, true); - - /* - //when the OCPP server updates the configs, the following callback will apply them to the OpenEVSE configs - setOnReceiveRequest("ChangeConfiguration", [this] (JsonObject) { - - config_set("ocpp_auth_auto", (uint32_t) (*freevendActive ? 1 : 0)); - config_set("ocpp_idtag", String((const char*) *freevendIdTag)); - config_set("ocpp_auth_offline", (uint32_t) (*allowOfflineTxForUnknownId ? 1 : 0)); - config_commit(); - }); - */ - - // ocpp_set_authorization - - // ocpp_setEnergyMeterInput(&setEnergyInput); - - // ocpp_setPowerMeterInput(&setPowerMeterInput); + ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false); + /* Inputs/outputs e callbacks */ ocpp_setEvReadyInput(&setEvReadyInput); - ocpp_setEvseReadyInput(&setEvseReadyInput); - - ocpp_setSmartChargingCurrentOutput(&setSmartChargingCurrentOutput); - - ocpp_setSmartChargingPowerOutput(&setSmartChargingPowerOutput); - - ocpp_setSmartChargingOutput(&setSmartChargingOutput); - ocpp_setConnectorPluggedInput(&setConnectorPluggedInput); - ocpp_setOnResetExecute(&OnResetExecute); - ocpp_setTxNotificationOutput(¬ificationOutput); - // ocpp_setStartTxReadyInput(&setStartTxReadyInput); - ocpp_setStopTxReadyInput(&setStopTxReadyInput); + ocpp_setOnResetNotify(&setOnResetNotify); - // ocpp_setOnUnlockConnectorInOut(&setOnUnlockConnectorInOut); - - // ocpp_setOccupiedInput(&setOccupiedInput); + ocpp_setEnergyMeterInput(&setEnergyInput); // inteiro Wh + /* Metering */ ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL); ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Offered", "A", NULL, NULL); ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL); ocpp_addMeterValueInputFloat(&setTemperatureInput, "Temperature", "Celsius", NULL, NULL); - ocpp_addMeterValueInputFloat(&setPowerMeterInput, "Power.Active.Import", "W", NULL, NULL); - ocpp_addMeterValueInputFloat(&setEnergyMeterInput, "Energy.Active.Import.Register", "W", NULL, NULL); + ocpp_addMeterValueInputFloat(&setEnergyMeterInput, "Energy.Active.Import.Register", "Wh", NULL, NULL); ocpp_addErrorCodeInput(&addErrorCodeInput); - ocpp_setOnResetNotify(&setOnResetNotify); + /* Task */ + xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 5, &ocpp_task); - // ocpp_setOnResetExecute(void (*onResetExecute)(bool)); + if (!s_auth_verify_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, + &ocpp_on_auth_verify, NULL, &s_auth_verify_inst)); + ESP_LOGI(TAG, "Registered AUTH_EVENT_TAG_VERIFY listener"); + } - // ocpp_setOnReceiveRequest(const char *operationType, OnMessage onRequest); + // ouvir mudanças de estado do EVSE + if (!s_evse_state_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, + &evse_event_handler, NULL, &s_evse_state_inst)); + } - // ocpp_setOnSendConf(const char *operationType, OnMessage onConfirmation); + // ouvir mudanças de ENABLE / AVAILABLE do EVSE (Local → OCPP) + if (!s_evse_enable_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, + &evse_enable_available_handler, NULL, &s_evse_enable_inst)); + } + if (!s_evse_available_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, + &evse_enable_available_handler, NULL, &s_evse_available_inst)); + } - // when the OCPP server updates the configs, the following callback will apply them to the OpenEVSE configs - // ocpp_setOnReceiveRequest("ChangeConfiguration", &setChangeConfiguration); - - // ocpp_setOnReceiveRequest("GetConfiguration", &setGetConfiguration); - - // ocpp_setOnReceiveRequest("StartTransaction", &setStartTransaction); - - xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 4, &ocpp_task); + if (!s_meter_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + METER_EVENT, METER_EVENT_DATA_READY, + &on_meter_event, NULL, &s_meter_inst)); + ESP_LOGI(TAG, "Registered METER_EVENT_DATA_READY listener (EVSE source)"); + } } void ocpp_stop(void) { - ESP_LOGI(TAG, "Stopping"); + ESP_LOGI(TAG, "Stopping OCPP"); if (ocpp_task) { @@ -545,8 +855,46 @@ void ocpp_stop(void) ocpp_task = NULL; } - /* Deallocate ressources */ ocpp_deinitialize(); - // ocpp_deinitConnection(osock); + + if (g_ocpp_conn) + { + ocpp_deinitConnection(g_ocpp_conn); + g_ocpp_conn = NULL; + } + mg_mgr_free(&mgr); -} \ No newline at end of file + + if (s_auth_verify_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, s_auth_verify_inst)); + s_auth_verify_inst = NULL; + } + + if (s_evse_state_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, s_evse_state_inst)); + s_evse_state_inst = NULL; + } + + if (s_evse_enable_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, s_evse_enable_inst)); + s_evse_enable_inst = NULL; + } + if (s_evse_available_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, s_evse_available_inst)); + s_evse_available_inst = NULL; + } + if (s_meter_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + METER_EVENT, METER_EVENT_DATA_READY, s_meter_inst)); + s_meter_inst = NULL; + } +} diff --git a/components/ocpp/src/ocpp_events.c b/components/ocpp/src/ocpp_events.c new file mode 100755 index 0000000..160232d --- /dev/null +++ b/components/ocpp/src/ocpp_events.c @@ -0,0 +1,4 @@ +#include "ocpp_events.h" + +/* Define a base, como em components/auth/src/auth_events.c */ +ESP_EVENT_DEFINE_BASE(OCPP_EVENTS); diff --git a/components/rest_api/src/auth_api.c b/components/rest_api/src/auth_api.c index 81a046a..b33e6d4 100755 --- a/components/rest_api/src/auth_api.c +++ b/components/rest_api/src/auth_api.c @@ -1,5 +1,5 @@ // ========================= -// auth_api.c +// auth_api.c (nova interface por modo) // ========================= #include "auth_api.h" @@ -16,60 +16,101 @@ static const char *TAG = "auth_api"; static struct { char username[128]; -} users[10] = { /* {"admin"}, {"user1"} */ }; +} users[10] = { {"admin"}, {"user1"} }; static int num_users = 2; // ================================= -// Handlers for Auth Methods (RFID) +// Helpers // ================================= -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()); +static void send_json(httpd_req_t *req, cJSON *json) { char *str = cJSON_PrintUnformatted(json); - httpd_resp_sendstr(req, str); - free(str); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, str ? str : "{}"); + if (str) free(str); cJSON_Delete(json); +} + +static esp_err_t recv_body(httpd_req_t *req, char *buf, size_t buf_sz, int *out_len) { + int remaining = req->content_len; + int received = 0; + + if (remaining <= 0) { + *out_len = 0; + return ESP_OK; + } + + while (remaining > 0 && received < (int)(buf_sz - 1)) { + int chunk = remaining; + int space = (int)(buf_sz - 1 - received); + if (chunk > space) chunk = space; + + int ret = httpd_req_recv(req, buf + received, chunk); + if (ret <= 0) return ESP_FAIL; + + received += ret; + remaining -= ret; + } + buf[received] = '\0'; + *out_len = received; return ESP_OK; } -static esp_err_t auth_methods_post_handler(httpd_req_t *req) { +// ================================= +// Auth Mode (NEW API) +// ================================= + +static esp_err_t auth_mode_get_handler(httpd_req_t *req) { + auth_mode_t mode = auth_get_mode(); + cJSON *json = cJSON_CreateObject(); + cJSON_AddStringToObject(json, "mode", auth_mode_to_str(mode)); + send_json(req, json); + return ESP_OK; +} + +static esp_err_t auth_mode_post_handler(httpd_req_t *req) { char buf[256]; - int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) { + int len = 0; + if (recv_body(req, buf, sizeof(buf), &len) != ESP_OK) { 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); + cJSON *json = cJSON_ParseWithLength(buf, len); 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 *mode_js = cJSON_GetObjectItem(json, "mode"); + if (!cJSON_IsString(mode_js) || mode_js->valuestring == NULL) { cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Campo 'RFID' inválido ou ausente"); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Campo 'mode' inválido ou ausente"); return ESP_FAIL; } + auth_mode_t mode; + if (!auth_mode_from_str(mode_js->valuestring, &mode)) { + cJSON_Delete(json); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Valor de 'mode' inválido (use: open|local|ocpp)"); + return ESP_FAIL; + } + + auth_set_mode(mode); cJSON_Delete(json); - httpd_resp_sendstr(req, "Métodos de autenticação atualizados"); + + cJSON *resp = cJSON_CreateObject(); + cJSON_AddStringToObject(resp, "mode", auth_mode_to_str(mode)); + send_json(req, resp); return ESP_OK; } // ================================= -// User Management Handlers +/* Users (mock) */ // ================================= 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) { @@ -78,28 +119,27 @@ static esp_err_t users_get_handler(httpd_req_t *req) { cJSON_AddItemToArray(list, u); } cJSON_AddItemToObject(root, "users", list); - char *str = cJSON_PrintUnformatted(root); - httpd_resp_sendstr(req, str); - free(str); - cJSON_Delete(root); + send_json(req, 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'; + int len = 0; + if (recv_body(req, buf, sizeof(buf), &len) != ESP_OK || len <= 0) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Body vazio"); + return ESP_FAIL; + } 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"); + return ESP_OK; } else { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Limite de usuários atingido"); + return ESP_FAIL; } - return ESP_OK; } static esp_err_t users_delete_handler(httpd_req_t *req) { @@ -124,27 +164,21 @@ static esp_err_t users_delete_handler(httpd_req_t *req) { } // ================================= -// Tag Management Handlers +// Tags (apenas úteis no modo LOCAL) // ================================= static esp_err_t tags_get_handler(httpd_req_t *req) { - httpd_resp_set_type(req, "application/json"); cJSON *root = cJSON_CreateObject(); cJSON *list = cJSON_CreateArray(); int count = auth_get_tag_count(); for (int i = 0; i < count; i++) { const char *tag = auth_get_tag_by_index(i); - if (tag) { - cJSON_AddItemToArray(list, cJSON_CreateString(tag)); - } + if (tag) cJSON_AddItemToArray(list, cJSON_CreateString(tag)); } cJSON_AddItemToObject(root, "tags", list); - char *str = cJSON_PrintUnformatted(root); - httpd_resp_sendstr(req, str); - free(str); - cJSON_Delete(root); + send_json(req, root); return ESP_OK; } @@ -164,6 +198,10 @@ static esp_err_t tags_delete_handler(httpd_req_t *req) { } static esp_err_t tags_register_handler(httpd_req_t *req) { + if (auth_get_mode() != AUTH_MODE_LOCAL_RFID) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Registo de tags disponível apenas no modo LOCAL"); + return ESP_FAIL; + } auth_wait_for_tag_registration(); httpd_resp_sendstr(req, "Modo de registro de tag ativado"); return ESP_OK; @@ -174,21 +212,21 @@ static esp_err_t tags_register_handler(httpd_req_t *req) { // ================================= void register_auth_handlers(httpd_handle_t server, void *ctx) { - // Auth methods + // Auth mode httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/auth-methods", + .uri = "/api/v1/config/auth-mode", .method = HTTP_GET, - .handler = auth_methods_get_handler, + .handler = auth_mode_get_handler, .user_ctx = ctx }); httpd_register_uri_handler(server, &(httpd_uri_t){ - .uri = "/api/v1/config/auth-methods", + .uri = "/api/v1/config/auth-mode", .method = HTTP_POST, - .handler = auth_methods_post_handler, + .handler = auth_mode_post_handler, .user_ctx = ctx }); - // Users + // Users (mock) httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/users", .method = HTTP_GET, diff --git a/components/rest_api/src/ocpp_api.c b/components/rest_api/src/ocpp_api.c index a70b40f..f31b846 100755 --- a/components/rest_api/src/ocpp_api.c +++ b/components/rest_api/src/ocpp_api.c @@ -1,45 +1,63 @@ // ========================= // ocpp_api.c // ========================= -#include "ocpp_api.h" +#include "ocpp.h" #include "esp_log.h" +#include "esp_err.h" +#include "esp_http_server.h" #include "cJSON.h" +#include 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) { + ESP_LOGD(TAG, "GET /api/v1/ocpp"); httpd_resp_set_type(req, "application/json"); + + char server[64] = {0}; + char charge_id[64] = {0}; + ocpp_get_server(server); + ocpp_get_charge_id(charge_id); + cJSON *status = cJSON_CreateObject(); - cJSON_AddStringToObject(status, "status", "connected"); - char *str = cJSON_Print(status); + cJSON_AddBoolToObject(status, "connected", ocpp_is_connected()); + cJSON_AddStringToObject(status, "server", server); + cJSON_AddStringToObject(status, "charge_id", charge_id); + + char *str = cJSON_PrintUnformatted(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) { + ESP_LOGD(TAG, "GET /api/v1/config/ocpp"); httpd_resp_set_type(req, "application/json"); + + char server[64] = {0}; + char charge_id[64] = {0}; + bool enabled = ocpp_get_enabled(); + ocpp_get_server(server); + ocpp_get_charge_id(charge_id); + 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); + cJSON_AddBoolToObject(json, "enabled", enabled); + cJSON_AddStringToObject(json, "url", server); + cJSON_AddStringToObject(json, "chargeBoxId", charge_id); + + char *str = cJSON_PrintUnformatted(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) { + ESP_LOGD(TAG, "POST /api/v1/config/ocpp"); + char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { @@ -47,19 +65,28 @@ static esp_err_t ocpp_post_config_handler(httpd_req_t *req) { 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 *enabled = cJSON_GetObjectItem(json, "enabled"); + if (cJSON_IsBool(enabled)) { + ocpp_set_enabled(cJSON_IsTrue(enabled)); + } + cJSON *url = cJSON_GetObjectItem(json, "url"); - if (url) strlcpy(ocpp_config.url, url->valuestring, sizeof(ocpp_config.url)); + if (cJSON_IsString(url)) { + ocpp_set_server(url->valuestring); + } + 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)); + if (cJSON_IsString(id)) { + ocpp_set_charge_id(id->valuestring); + } + cJSON_Delete(json); httpd_resp_sendstr(req, "OCPP config atualizada com sucesso"); return ESP_OK; diff --git a/components/rest_api/webfolder/assets/index-AuNGQ-2m.css b/components/rest_api/webfolder/assets/index-Bc9ibDeR.css similarity index 53% rename from components/rest_api/webfolder/assets/index-AuNGQ-2m.css rename to components/rest_api/webfolder/assets/index-Bc9ibDeR.css index 7d56c6a..8517d65 100644 --- a/components/rest_api/webfolder/assets/index-AuNGQ-2m.css +++ b/components/rest_api/webfolder/assets/index-Bc9ibDeR.css @@ -1 +1 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.table{display:table}.hidden{display:none}.max-h-96{max-height:24rem}.w-20{width:5rem}.w-full{width:100%}.min-w-\[150px\]{min-width:150px}.min-w-full{min-width:100%}.max-w-3xl{max-width:48rem}.flex-1{flex:1 1 0%}.table-auto{table-layout:auto}.cursor-not-allowed{cursor:not-allowed}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-green-700{--tw-gradient-from: #15803d var(--tw-gradient-from-position);--tw-gradient-to: rgb(21 128 61 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-green-600{--tw-gradient-to: #16a34a var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-left{text-align:left}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}@media (min-width: 768px){.md\:\!flex{display:flex!important}.md\:flex{display:flex}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}} +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.table{display:table}.hidden{display:none}.max-h-96{max-height:24rem}.w-20{width:5rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.min-w-\[150px\]{min-width:150px}.min-w-full{min-width:100%}.max-w-3xl{max-width:48rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.table-auto{table-layout:auto}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-green-700{--tw-gradient-from: #15803d var(--tw-gradient-from-position);--tw-gradient-to: rgb(21 128 61 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-green-600{--tw-gradient-to: #16a34a var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-left{text-align:left}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-50{opacity:.5}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}@media (min-width: 768px){.md\:\!flex{display:flex!important}.md\:flex{display:flex}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}} diff --git a/components/rest_api/webfolder/assets/index-ClgQvp_F.js b/components/rest_api/webfolder/assets/index-CmjuW5AW.js similarity index 56% rename from components/rest_api/webfolder/assets/index-ClgQvp_F.js rename to components/rest_api/webfolder/assets/index-CmjuW5AW.js index b803edc..76bf816 100644 --- a/components/rest_api/webfolder/assets/index-ClgQvp_F.js +++ b/components/rest_api/webfolder/assets/index-CmjuW5AW.js @@ -1,4 +1,4 @@ -(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))f(h);new MutationObserver(h=>{for(const v of h)if(v.type==="childList")for(const S of v.addedNodes)S.tagName==="LINK"&&S.rel==="modulepreload"&&f(S)}).observe(document,{childList:!0,subtree:!0});function s(h){const v={};return h.integrity&&(v.integrity=h.integrity),h.referrerPolicy&&(v.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?v.credentials="include":h.crossOrigin==="anonymous"?v.credentials="omit":v.credentials="same-origin",v}function f(h){if(h.ep)return;h.ep=!0;const v=s(h);fetch(h.href,v)}})();function $d(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Tf={exports:{}},Cn={};/** +(function(){const d=document.createElement("link").relList;if(d&&d.supports&&d.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))f(h);new MutationObserver(h=>{for(const v of h)if(v.type==="childList")for(const S of v.addedNodes)S.tagName==="LINK"&&S.rel==="modulepreload"&&f(S)}).observe(document,{childList:!0,subtree:!0});function o(h){const v={};return h.integrity&&(v.integrity=h.integrity),h.referrerPolicy&&(v.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?v.credentials="include":h.crossOrigin==="anonymous"?v.credentials="omit":v.credentials="same-origin",v}function f(h){if(h.ep)return;h.ep=!0;const v=o(h);fetch(h.href,v)}})();function $d(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Tf={exports:{}},_n={};/** * @license React * react-jsx-runtime.production.js * @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Cd;function By(){if(Cd)return Cn;Cd=1;var i=Symbol.for("react.transitional.element"),o=Symbol.for("react.fragment");function s(f,h,v){var S=null;if(v!==void 0&&(S=""+v),h.key!==void 0&&(S=""+h.key),"key"in h){v={};for(var j in h)j!=="key"&&(v[j]=h[j])}else v=h;return h=v.ref,{$$typeof:i,type:f,key:S,ref:h!==void 0?h:null,props:v}}return Cn.Fragment=o,Cn.jsx=s,Cn.jsxs=s,Cn}var _d;function wy(){return _d||(_d=1,Tf.exports=By()),Tf.exports}var d=wy(),Af={exports:{}},te={};/** + */var _d;function By(){if(_d)return _n;_d=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.fragment");function o(f,h,v){var S=null;if(v!==void 0&&(S=""+v),h.key!==void 0&&(S=""+h.key),"key"in h){v={};for(var j in h)j!=="key"&&(v[j]=h[j])}else v=h;return h=v.ref,{$$typeof:i,type:f,key:S,ref:h!==void 0?h:null,props:v}}return _n.Fragment=d,_n.jsx=o,_n.jsxs=o,_n}var Cd;function qy(){return Cd||(Cd=1,Tf.exports=By()),Tf.exports}var s=qy(),Af={exports:{}},te={};/** * @license React * react.production.js * @@ -14,7 +14,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Ud;function qy(){if(Ud)return te;Ud=1;var i=Symbol.for("react.transitional.element"),o=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),h=Symbol.for("react.profiler"),v=Symbol.for("react.consumer"),S=Symbol.for("react.context"),j=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),M=Symbol.for("react.lazy"),U=Symbol.iterator;function O(g){return g===null||typeof g!="object"?null:(g=U&&g[U]||g["@@iterator"],typeof g=="function"?g:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},H=Object.assign,G={};function V(g,B,Q){this.props=g,this.context=B,this.refs=G,this.updater=Q||w}V.prototype.isReactComponent={},V.prototype.setState=function(g,B){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,B,"setState")},V.prototype.forceUpdate=function(g){this.updater.enqueueForceUpdate(this,g,"forceUpdate")};function q(){}q.prototype=V.prototype;function Z(g,B,Q){this.props=g,this.context=B,this.refs=G,this.updater=Q||w}var k=Z.prototype=new q;k.constructor=Z,H(k,V.prototype),k.isPureReactComponent=!0;var ce=Array.isArray,L={H:null,A:null,T:null,S:null,V:null},fe=Object.prototype.hasOwnProperty;function ne(g,B,Q,Y,$,oe){return Q=oe.ref,{$$typeof:i,type:g,key:B,ref:Q!==void 0?Q:null,props:oe}}function se(g,B){return ne(g.type,B,void 0,void 0,void 0,g.props)}function Ee(g){return typeof g=="object"&&g!==null&&g.$$typeof===i}function Je(g){var B={"=":"=0",":":"=2"};return"$"+g.replace(/[=:]/g,function(Q){return B[Q]})}var ot=/\/+/g;function Xe(g,B){return typeof g=="object"&&g!==null&&g.key!=null?Je(""+g.key):B.toString(36)}function Nl(){}function Ol(g){switch(g.status){case"fulfilled":return g.value;case"rejected":throw g.reason;default:switch(typeof g.status=="string"?g.then(Nl,Nl):(g.status="pending",g.then(function(B){g.status==="pending"&&(g.status="fulfilled",g.value=B)},function(B){g.status==="pending"&&(g.status="rejected",g.reason=B)})),g.status){case"fulfilled":return g.value;case"rejected":throw g.reason}}throw g}function Qe(g,B,Q,Y,$){var oe=typeof g;(oe==="undefined"||oe==="boolean")&&(g=null);var ee=!1;if(g===null)ee=!0;else switch(oe){case"bigint":case"string":case"number":ee=!0;break;case"object":switch(g.$$typeof){case i:case o:ee=!0;break;case M:return ee=g._init,Qe(ee(g._payload),B,Q,Y,$)}}if(ee)return $=$(g),ee=Y===""?"."+Xe(g,0):Y,ce($)?(Q="",ee!=null&&(Q=ee.replace(ot,"$&/")+"/"),Qe($,B,Q,"",function(tl){return tl})):$!=null&&(Ee($)&&($=se($,Q+($.key==null||g&&g.key===$.key?"":(""+$.key).replace(ot,"$&/")+"/")+ee)),B.push($)),1;ee=0;var tt=Y===""?".":Y+":";if(ce(g))for(var Te=0;Te>>1,g=z[pe];if(0>>1;peh(Y,P))$h(oe,Y)?(z[pe]=oe,z[$]=P,pe=$):(z[pe]=Y,z[Q]=P,pe=Q);else if($h(oe,P))z[pe]=oe,z[$]=P,pe=$;else break e}}return X}function h(z,X){var P=z.sortIndex-X.sortIndex;return P!==0?P:z.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 S=Date,j=S.now();i.unstable_now=function(){return S.now()-j}}var p=[],m=[],M=1,U=null,O=3,w=!1,H=!1,G=!1,V=!1,q=typeof setTimeout=="function"?setTimeout:null,Z=typeof clearTimeout=="function"?clearTimeout:null,k=typeof setImmediate<"u"?setImmediate:null;function ce(z){for(var X=s(m);X!==null;){if(X.callback===null)f(m);else if(X.startTime<=z)f(m),X.sortIndex=X.expirationTime,o(p,X);else break;X=s(m)}}function L(z){if(G=!1,ce(z),!H)if(s(p)!==null)H=!0,fe||(fe=!0,Xe());else{var X=s(m);X!==null&&Qe(L,X.startTime-z)}}var fe=!1,ne=-1,se=5,Ee=-1;function Je(){return V?!0:!(i.unstable_now()-Eez&&Je());){var pe=U.callback;if(typeof pe=="function"){U.callback=null,O=U.priorityLevel;var g=pe(U.expirationTime<=z);if(z=i.unstable_now(),typeof g=="function"){U.callback=g,ce(z),X=!0;break t}U===s(p)&&f(p),ce(z)}else f(p);U=s(p)}if(U!==null)X=!0;else{var B=s(m);B!==null&&Qe(L,B.startTime-z),X=!1}}break e}finally{U=null,O=P,w=!1}X=void 0}}finally{X?Xe():fe=!1}}}var Xe;if(typeof k=="function")Xe=function(){k(ot)};else if(typeof MessageChannel<"u"){var Nl=new MessageChannel,Ol=Nl.port2;Nl.port1.onmessage=ot,Xe=function(){Ol.postMessage(null)}}else Xe=function(){q(ot,0)};function Qe(z,X){ne=q(function(){z(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(z){z.callback=null},i.unstable_forceFrameRate=function(z){0>z||125pe?(z.sortIndex=P,o(m,z),s(p)===null&&z===s(m)&&(G?(Z(ne),ne=-1):G=!0,Qe(L,P-pe))):(z.sortIndex=g,o(p,z),H||w||(H=!0,fe||(fe=!0,Xe()))),z},i.unstable_shouldYield=Je,i.unstable_wrapCallback=function(z){var X=O;return function(){var P=O;O=X;try{return z.apply(this,arguments)}finally{O=P}}}}(Of)),Of}var wd;function Gy(){return wd||(wd=1,Nf.exports=Yy()),Nf.exports}var Mf={exports:{}},Ke={};/** + */var Bd;function Yy(){return Bd||(Bd=1,function(i){function d(D,X){var P=D.length;D.push(X);e:for(;0>>1,g=D[pe];if(0>>1;peh(G,P))$h(oe,G)?(D[pe]=oe,D[$]=P,pe=$):(D[pe]=G,D[Q]=P,pe=Q);else if($h(oe,P))D[pe]=oe,D[$]=P,pe=$;else break e}}return X}function h(D,X){var P=D.sortIndex-X.sortIndex;return P!==0?P:D.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 S=Date,j=S.now();i.unstable_now=function(){return S.now()-j}}var p=[],m=[],M=1,H=null,O=3,q=!1,C=!1,Y=!1,V=!1,L=typeof setTimeout=="function"?setTimeout:null,Z=typeof clearTimeout=="function"?clearTimeout:null,k=typeof setImmediate<"u"?setImmediate:null;function le(D){for(var X=o(m);X!==null;){if(X.callback===null)f(m);else if(X.startTime<=D)f(m),X.sortIndex=X.expirationTime,d(p,X);else break;X=o(m)}}function w(D){if(Y=!1,le(D),!C)if(o(p)!==null)C=!0,se||(se=!0,Xe());else{var X=o(m);X!==null&&Qe(w,X.startTime-D)}}var se=!1,ue=-1,re=5,Ee=-1;function Je(){return V?!0:!(i.unstable_now()-EeD&&Je());){var pe=H.callback;if(typeof pe=="function"){H.callback=null,O=H.priorityLevel;var g=pe(H.expirationTime<=D);if(D=i.unstable_now(),typeof g=="function"){H.callback=g,le(D),X=!0;break t}H===o(p)&&f(p),le(D)}else f(p);H=o(p)}if(H!==null)X=!0;else{var B=o(m);B!==null&&Qe(w,B.startTime-D),X=!1}}break e}finally{H=null,O=P,q=!1}X=void 0}}finally{X?Xe():se=!1}}}var Xe;if(typeof k=="function")Xe=function(){k(ot)};else if(typeof MessageChannel<"u"){var Rl=new MessageChannel,Ol=Rl.port2;Rl.port1.onmessage=ot,Xe=function(){Ol.postMessage(null)}}else Xe=function(){L(ot,0)};function Qe(D,X){ue=L(function(){D(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(D){D.callback=null},i.unstable_forceFrameRate=function(D){0>D||125pe?(D.sortIndex=P,d(m,D),o(p)===null&&D===o(m)&&(Y?(Z(ue),ue=-1):Y=!0,Qe(w,P-pe))):(D.sortIndex=g,d(p,D),C||q||(C=!0,se||(se=!0,Xe()))),D},i.unstable_shouldYield=Je,i.unstable_wrapCallback=function(D){var X=O;return function(){var P=O;O=X;try{return D.apply(this,arguments)}finally{O=P}}}}(Of)),Of}var qd;function Gy(){return qd||(qd=1,Rf.exports=Yy()),Rf.exports}var Mf={exports:{}},Ke={};/** * @license React * react-dom.production.js * @@ -30,7 +30,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var qd;function Xy(){if(qd)return Ke;qd=1;var i=_f();function o(p){var m="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(),Mf.exports=Xy(),Mf.exports}/** + */var wd;function Xy(){if(wd)return Ke;wd=1;var i=Cf();function d(p){var m="https://react.dev/errors/"+p;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(d){console.error(d)}}return i(),Mf.exports=Xy(),Mf.exports}/** * @license React * react-dom-client.production.js * @@ -38,14 +38,14 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Yd;function Zy(){if(Yd)return _n;Yd=1;var i=Gy(),o=_f(),s=Qy();function f(e){var t="https://react.dev/errors/"+e;if(1g||(e.current=pe[g],pe[g]=null,g--)}function Y(e,t){g++,pe[g]=e.current,e.current=t}var $=B(null),oe=B(null),ee=B(null),tt=B(null);function Te(e,t){switch(Y(ee,t),Y(oe,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}}Q($),Y($,e)}function tl(){Q($),Q(oe),Q(ee)}function ci(e){e.memoizedState!==null&&Y(tt,e);var t=$.current,l=cd(t,e.type);t!==l&&(Y(oe,e),Y($,l))}function Yn(e){oe.current===e&&(Q($),Q(oe)),tt.current===e&&(Q(tt),On._currentValue=P)}var fi=Object.prototype.hasOwnProperty,ri=i.unstable_scheduleCallback,si=i.unstable_cancelCallback,mh=i.unstable_shouldYield,yh=i.unstable_requestPaint,Rt=i.unstable_now,vh=i.unstable_getCurrentPriorityLevel,Lf=i.unstable_ImmediatePriority,Yf=i.unstable_UserBlockingPriority,Gn=i.unstable_NormalPriority,gh=i.unstable_LowPriority,Gf=i.unstable_IdlePriority,bh=i.log,ph=i.unstable_setDisableYieldValue,Ua=null,lt=null;function ll(e){if(typeof bh=="function"&&ph(e),lt&&typeof lt.setStrictMode=="function")try{lt.setStrictMode(Ua,e)}catch{}}var at=Math.clz32?Math.clz32:Eh,Sh=Math.log,xh=Math.LN2;function Eh(e){return e>>>=0,e===0?32:31-(Sh(e)/xh|0)|0}var Xn=256,Qn=4194304;function Ml(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 Zn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var r=a&134217727;return r!==0?(a=r&~u,a!==0?n=Ml(a):(c&=r,c!==0?n=Ml(c):l||(l=r&~e,l!==0&&(n=Ml(l))))):(r=a&~u,r!==0?n=Ml(r):c!==0?n=Ml(c):l||(l=a&~e,l!==0&&(n=Ml(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function Ha(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Th(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=Xn;return Xn<<=1,(Xn&4194048)===0&&(Xn=256),e}function Qf(){var e=Qn;return Qn<<=1,(Qn&62914560)===0&&(Qn=4194304),e}function oi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ba(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ah(e,t,l,a,n,u){var c=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var r=e.entanglements,y=e.expirationTimes,T=e.hiddenUpdates;for(l=c&~l;0g||(e.current=pe[g],pe[g]=null,g--)}function G(e,t){g++,pe[g]=e.current,e.current=t}var $=B(null),oe=B(null),ee=B(null),tt=B(null);function Te(e,t){switch(G(ee,t),G(oe,e),G($,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}}Q($),G($,e)}function tl(){Q($),Q(oe),Q(ee)}function ci(e){e.memoizedState!==null&&G(tt,e);var t=$.current,l=cd(t,e.type);t!==l&&(G(oe,e),G($,l))}function Yn(e){oe.current===e&&(Q($),Q(oe)),tt.current===e&&(Q(tt),On._currentValue=P)}var fi=Object.prototype.hasOwnProperty,ri=i.unstable_scheduleCallback,si=i.unstable_cancelCallback,mh=i.unstable_shouldYield,yh=i.unstable_requestPaint,Nt=i.unstable_now,vh=i.unstable_getCurrentPriorityLevel,Lf=i.unstable_ImmediatePriority,Yf=i.unstable_UserBlockingPriority,Gn=i.unstable_NormalPriority,gh=i.unstable_LowPriority,Gf=i.unstable_IdlePriority,bh=i.log,ph=i.unstable_setDisableYieldValue,Ua=null,lt=null;function ll(e){if(typeof bh=="function"&&ph(e),lt&&typeof lt.setStrictMode=="function")try{lt.setStrictMode(Ua,e)}catch{}}var at=Math.clz32?Math.clz32:Eh,Sh=Math.log,xh=Math.LN2;function Eh(e){return e>>>=0,e===0?32:31-(Sh(e)/xh|0)|0}var Xn=256,Qn=4194304;function Ml(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 Zn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var r=a&134217727;return r!==0?(a=r&~u,a!==0?n=Ml(a):(c&=r,c!==0?n=Ml(c):l||(l=r&~e,l!==0&&(n=Ml(l))))):(r=a&~u,r!==0?n=Ml(r):c!==0?n=Ml(c):l||(l=a&~e,l!==0&&(n=Ml(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function Ha(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Th(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=Xn;return Xn<<=1,(Xn&4194048)===0&&(Xn=256),e}function Qf(){var e=Qn;return Qn<<=1,(Qn&62914560)===0&&(Qn=4194304),e}function oi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ba(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ah(e,t,l,a,n,u){var c=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var r=e.entanglements,y=e.expirationTimes,T=e.hiddenUpdates;for(l=c&~l;0)":-1n||y[a]!==T[n]){var D=` -`+y[a].replace(" at new "," at ");return e.displayName&&D.includes("")&&(D=D.replace("",e.displayName)),D}while(1<=a&&0<=n);break}}}finally{gi=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?Pl(l):""}function Dh(e){switch(e.tag){case 26:case 27:case 5:return Pl(e.type);case 16:return Pl("Lazy");case 13:return Pl("Suspense");case 19:return Pl("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 Pl("Activity");default:return""}}function If(e){try{var t="";do t+=Dh(e),e=e.return;while(e);return t}catch(l){return` +`);for(n=a=0;an||y[a]!==T[n]){var z=` +`+y[a].replace(" at new "," at ");return e.displayName&&z.includes("")&&(z=z.replace("",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{gi=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?Pl(l):""}function zh(e){switch(e.tag){case 26:case 27:case 5:return Pl(e.type);case 16:return Pl("Lazy");case 13:return Pl("Suspense");case 19:return Pl("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 Pl("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 dt(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 zh(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 n=l.get,u=l.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return n.call(this)},set:function(c){a=""+c,u.call(this,c)}}),Object.defineProperty(e,t,{enumerable:l.enumerable}),{getValue:function(){return a},setValue:function(c){a=""+c},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Jn(e){e._valueTracker||(e._valueTracker=zh(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 kn(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 Ch=/[\n"\\]/g;function ht(e){return e.replace(Ch,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function pi(e,t,l,a,n,u,c,r){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=""+dt(t)):e.value!==""+dt(t)&&(e.value=""+dt(t)):c!=="submit"&&c!=="reset"||e.removeAttribute("value"),t!=null?Si(e,c,dt(t)):l!=null?Si(e,c,dt(l)):a!=null&&e.removeAttribute("value"),n==null&&u!=null&&(e.defaultChecked=!!u),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"?e.name=""+dt(r):e.removeAttribute("name")}function lr(e,t,l,a,n,u,c,r){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(e.type=u),t!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||t!=null))return;l=l!=null?""+dt(l):"",t=t!=null?""+dt(t):l,r||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=r?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"&&kn(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function Il(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ri=!1;if(wt)try{var Ya={};Object.defineProperty(Ya,"passive",{get:function(){Ri=!0}}),window.addEventListener("test",Ya,Ya),window.removeEventListener("test",Ya,Ya)}catch{Ri=!1}var nl=null,Ni=null,Wn=null;function rr(){if(Wn)return Wn;var e,t=Ni,l=t.length,a,n="value"in nl?nl.value:nl.textContent,u=n.length;for(e=0;e=Qa),yr=" ",vr=!1;function gr(e,t){switch(e){case"keyup":return im.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 aa=!1;function fm(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 rm(e,t){if(aa)return e==="compositionend"||!zi&&gr(e,t)?(e=rr(),Wn=Ni=nl=null,aa=!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=Nr(l)}}function Mr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Mr(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function jr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=kn(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=kn(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 gm=wt&&"documentMode"in document&&11>=document.documentMode,na=null,Hi=null,Ja=null,Bi=!1;function Dr(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Bi||na==null||na!==kn(a)||(a=na,"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}),Ja&&Ka(Ja,a)||(Ja=a,a=Yu(Hi,"onSelect"),0>=c,n-=c,Lt=1<<32-at(t)+n|l<u?u:8;var c=z.T,r={};z.T=r,xc(e,!1,t,l);try{var y=n(),T=z.S;if(T!==null&&T(r,y),y!==null&&typeof y=="object"&&typeof y.then=="function"){var D=Nm(y,a);rn(e,t,D,rt(e))}else rn(e,t,a,rt(e))}catch(_){rn(e,t,{then:function(){},status:"rejected",reason:_},rt())}finally{X.p=u,z.T=c}}function zm(){}function pc(e,t,l,a){if(e.tag!==5)throw Error(f(476));var n=zs(e).queue;Ds(e,n,t,P,l===null?zm:function(){return Cs(e),l(a)})}function zs(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:Qt,lastRenderedState:P},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Qt,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Cs(e){var t=zs(e).next.queue;rn(e,t,{},rt())}function Sc(){return Ve(On)}function _s(){return Ce().memoizedState}function Us(){return Ce().memoizedState}function Cm(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=rt();e=cl(l);var a=fl(t,e,l);a!==null&&(st(a,t,l),ln(a,t,l)),t={cache:Wi()},e.payload=t;return}t=t.return}}function _m(e,t,l){var a=rt();l={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null},Su(e)?Bs(t,l):(l=Yi(e,t,l,a),l!==null&&(st(l,e,a),ws(l,t,a)))}function Hs(e,t,l){var a=rt();rn(e,t,l,a)}function rn(e,t,l,a){var n={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null};if(Su(e))Bs(t,n);else{var u=e.alternate;if(e.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var c=t.lastRenderedState,r=u(c,l);if(n.hasEagerState=!0,n.eagerState=r,nt(r,c))return au(e,t,n,0),xe===null&&lu(),!1}catch{}finally{}if(l=Yi(e,t,n,a),l!==null)return st(l,e,a),ws(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},Su(e)){if(t)throw Error(f(479))}else t=Yi(e,l,a,2),t!==null&&st(t,e,2)}function Su(e){var t=e.alternate;return e===le||t!==null&&t===le}function Bs(e,t){ma=mu=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function ws(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,Vf(e,l)}}var xu={readContext:Ve,use:vu,useCallback:je,useContext:je,useEffect:je,useImperativeHandle:je,useLayoutEffect:je,useInsertionEffect:je,useMemo:je,useReducer:je,useRef:je,useState:je,useDebugValue:je,useDeferredValue:je,useTransition:je,useSyncExternalStore:je,useId:je,useHostTransitionStatus:je,useFormState:je,useActionState:je,useOptimistic:je,useMemoCache:je,useCacheRefresh:je},qs={readContext:Ve,use:vu,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,pu(4194308,4,Rs.bind(null,t,e),l)},useLayoutEffect:function(e,t){return pu(4194308,4,e,t)},useInsertionEffect:function(e,t){pu(4,2,e,t)},useMemo:function(e,t){var l=Fe();t=t===void 0?null:t;var a=e();if(Gl){ll(!0);try{e()}finally{ll(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=Fe();if(l!==void 0){var n=l(t);if(Gl){ll(!0);try{l(t)}finally{ll(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=_m.bind(null,le,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,le,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,le,e.queue,!0,!1),Fe().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=le,n=Fe();if(he){if(l===void 0)throw Error(f(407));l=l()}else{if(l=t(),xe===null)throw Error(f(349));(re&124)!==0||us(a,t,l)}n.memoizedState=l;var u={value:l,getSnapshot:t};return n.queue=u,xs(cs.bind(null,a,u,e),[e]),a.flags|=2048,va(9,bu(),is.bind(null,a,u,l,t),null),l},useId:function(){var e=Fe(),t=xe.identifierPrefix;if(he){var l=Yt,a=Lt;l=(a&~(1<<32-at(a)-1)).toString(32)+l,t="«"+t+"R"+l,l=yu++,0F?(qe=J,J=null):qe=J.sibling;var de=R(x,J,E[F],C);if(de===null){J===null&&(J=qe);break}e&&J&&de.alternate===null&&t(x,J),b=u(de,b,F),ae===null?K=de:ae.sibling=de,ae=de,J=qe}if(F===E.length)return l(x,J),he&&Hl(x,F),K;if(J===null){for(;FF?(qe=J,J=null):qe=J.sibling;var Rl=R(x,J,de.value,C);if(Rl===null){J===null&&(J=qe);break}e&&J&&Rl.alternate===null&&t(x,J),b=u(Rl,b,F),ae===null?K=Rl:ae.sibling=Rl,ae=Rl,J=qe}if(de.done)return l(x,J),he&&Hl(x,F),K;if(J===null){for(;!de.done;F++,de=E.next())de=_(x,de.value,C),de!==null&&(b=u(de,b,F),ae===null?K=de:ae.sibling=de,ae=de);return he&&Hl(x,F),K}for(J=a(J);!de.done;F++,de=E.next())de=N(J,x,F,de.value,C),de!==null&&(e&&de.alternate!==null&&J.delete(de.key===null?F:de.key),b=u(de,b,F),ae===null?K=de:ae.sibling=de,ae=de);return e&&J.forEach(function(Hy){return t(x,Hy)}),he&&Hl(x,F),K}function be(x,b,E,C){if(typeof E=="object"&&E!==null&&E.type===H&&E.key===null&&(E=E.props.children),typeof E=="object"&&E!==null){switch(E.$$typeof){case O:e:{for(var K=E.key;b!==null;){if(b.key===K){if(K=E.type,K===H){if(b.tag===7){l(x,b.sibling),C=n(b,E.props.children),C.return=x,x=C;break e}}else if(b.elementType===K||typeof K=="object"&&K!==null&&K.$$typeof===se&&Ys(K)===b.type){l(x,b.sibling),C=n(b,E.props),on(C,E),C.return=x,x=C;break e}l(x,b);break}else t(x,b);b=b.sibling}E.type===H?(C=_l(E.props.children,x.mode,C,E.key),C.return=x,x=C):(C=uu(E.type,E.key,E.props,null,x.mode,C),on(C,E),C.return=x,x=C)}return c(x);case w: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(x,b.sibling),C=n(b,E.children||[]),C.return=x,x=C;break e}else{l(x,b);break}else t(x,b);b=b.sibling}C=Qi(E,x.mode,C),C.return=x,x=C}return c(x);case se:return K=E._init,E=K(E._payload),be(x,b,E,C)}if(Qe(E))return I(x,b,E,C);if(Xe(E)){if(K=Xe(E),typeof K!="function")throw Error(f(150));return E=K.call(E),W(x,b,E,C)}if(typeof E.then=="function")return be(x,b,Eu(E),C);if(E.$$typeof===k)return be(x,b,ru(x,E),C);Tu(x,E)}return typeof E=="string"&&E!==""||typeof E=="number"||typeof E=="bigint"?(E=""+E,b!==null&&b.tag===6?(l(x,b.sibling),C=n(b,E),C.return=x,x=C):(l(x,b),C=Xi(E,x.mode,C),C.return=x,x=C),c(x)):l(x,b)}return function(x,b,E,C){try{sn=0;var K=be(x,b,E,C);return ga=null,K}catch(J){if(J===en||J===ou)throw J;var ae=ut(29,J,null,x.mode);return ae.lanes=C,ae.return=x,ae}finally{}}}var ba=Gs(!0),Xs=Gs(!1),bt=B(null),Ot=null;function sl(e){var t=e.alternate;Y(Ue,Ue.current&1),Y(bt,e),Ot===null&&(t===null||ha.current!==null||t.memoizedState!==null)&&(Ot=e)}function Qs(e){if(e.tag===22){if(Y(Ue,Ue.current),Y(bt,e),Ot===null){var t=e.alternate;t!==null&&t.memoizedState!==null&&(Ot=e)}}else ol()}function ol(){Y(Ue,Ue.current),Y(bt,bt.current)}function Zt(e){Q(bt),Ot===e&&(Ot=null),Q(Ue)}var Ue=B(0);function Au(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:M({},t,l),e.memoizedState=l,e.lanes===0&&(e.updateQueue.baseState=l)}var Tc={enqueueSetState:function(e,t,l){e=e._reactInternals;var a=rt(),n=cl(a);n.payload=t,l!=null&&(n.callback=l),t=fl(e,n,a),t!==null&&(st(t,e,a),ln(t,e,a))},enqueueReplaceState:function(e,t,l){e=e._reactInternals;var a=rt(),n=cl(a);n.tag=1,n.payload=t,l!=null&&(n.callback=l),t=fl(e,n,a),t!==null&&(st(t,e,a),ln(t,e,a))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var l=rt(),a=cl(l);a.tag=2,t!=null&&(a.callback=t),t=fl(e,a,l),t!==null&&(st(t,e,l),ln(t,e,l))}};function Zs(e,t,l,a,n,u,c){return e=e.stateNode,typeof e.shouldComponentUpdate=="function"?e.shouldComponentUpdate(a,u,c):t.prototype&&t.prototype.isPureReactComponent?!Ka(l,a)||!Ka(n,u):!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 Xl(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=M({},l));for(var n in e)l[n]===void 0&&(l[n]=e[n])}return l}var Ru=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){Ru(e)}function Js(e){console.error(e)}function ks(e){Ru(e)}function Nu(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(n){setTimeout(function(){throw n})}}function Ac(e,t,l){return l=cl(l),l.tag=3,l.payload={element:null},l.callback=function(){Nu(e,t)},l}function Ws(e){return e=cl(e),e.tag=3,e}function Fs(e,t,l,a){var n=l.type.getDerivedStateFromError;if(typeof n=="function"){var u=a.value;e.payload=function(){return n(u)},e.callback=function(){$s(t,l,a)}}var c=l.stateNode;c!==null&&typeof c.componentDidCatch=="function"&&(e.callback=function(){$s(t,l,a),typeof n!="function"&&(gl===null?gl=new Set([this]):gl.add(this));var r=a.stack;this.componentDidCatch(a.value,{componentStack:r!==null?r:""})})}function Hm(e,t,l,a,n){if(l.flags|=32768,a!==null&&typeof a=="object"&&typeof a.then=="function"){if(t=l.alternate,t!==null&&Fa(t,l,n,!0),l=bt.current,l!==null){switch(l.tag){case 13:return Ot===null?kc():l.alternate===null&&Me===0&&(Me=3),l.flags&=-257,l.flags|=65536,l.lanes=n,a===Ii?l.flags|=16384:(t=l.updateQueue,t===null?l.updateQueue=new Set([a]):t.add(a),Wc(e,a,n)),!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,n)),!1}throw Error(f(435,l.tag))}return Wc(e,a,n),kc(),!1}if(he)return t=bt.current,t!==null?((t.flags&65536)===0&&(t.flags|=256),t.flags|=65536,t.lanes=n,a!==Ki&&(e=Error(f(422),{cause:a}),Wa(mt(e,l)))):(a!==Ki&&(t=Error(f(423),{cause:a}),Wa(mt(t,l))),e=e.current.alternate,e.flags|=65536,n&=-n,e.lanes|=n,a=mt(a,l),n=Ac(e.stateNode,a,n),lc(e,n),Me!==4&&(Me=2)),!1;var u=Error(f(520),{cause:a});if(u=mt(u,l),bn===null?bn=[u]:bn.push(u),Me!==4&&(Me=2),t===null)return!0;a=mt(a,l),l=t;do{switch(l.tag){case 3:return l.flags|=65536,e=n&-n,l.lanes|=e,e=Ac(l.stateNode,a,e),lc(l,e),!1;case 1:if(t=l.type,u=l.stateNode,(l.flags&128)===0&&(typeof t.getDerivedStateFromError=="function"||u!==null&&typeof u.componentDidCatch=="function"&&(gl===null||!gl.has(u))))return l.flags|=65536,n&=-n,l.lanes|=n,n=Ws(n),Fs(n,e,l,a),lc(l,n),!1}l=l.return}while(l!==null);return!1}var Ps=Error(f(461)),Be=!1;function Le(e,t,l,a){t.child=e===null?Xs(t,null,l,a):ba(t,e.child,l,a)}function Is(e,t,l,a,n){l=l.render;var u=t.ref;if("ref"in a){var c={};for(var r in a)r!=="ref"&&(c[r]=a[r])}else c=a;return Ll(t),a=cc(e,t,l,c,u,n),r=fc(),e!==null&&!Be?(rc(e,t,n),Vt(e,t,n)):(he&&r&&Zi(t),t.flags|=1,Le(e,t,a,n),t.child)}function eo(e,t,l,a,n){if(e===null){var u=l.type;return typeof u=="function"&&!Gi(u)&&u.defaultProps===void 0&&l.compare===null?(t.tag=15,t.type=u,to(e,t,u,a,n)):(e=uu(l.type,null,a,t,t.mode,n),e.ref=t.ref,e.return=t,t.child=e)}if(u=e.child,!Cc(e,n)){var c=u.memoizedProps;if(l=l.compare,l=l!==null?l:Ka,l(c,a)&&e.ref===t.ref)return Vt(e,t,n)}return t.flags|=1,e=qt(u,a),e.ref=t.ref,e.return=t,t.child=e}function to(e,t,l,a,n){if(e!==null){var u=e.memoizedProps;if(Ka(u,a)&&e.ref===t.ref)if(Be=!1,t.pendingProps=a=u,Cc(e,n))(e.flags&131072)!==0&&(Be=!0);else return t.lanes=e.lanes,Vt(e,t,n)}return Rc(e,t,l,a,n)}function lo(e,t,l){var a=t.pendingProps,n=a.children,u=e!==null?e.memoizedState:null;if(a.mode==="hidden"){if((t.flags&128)!==0){if(a=u!==null?u.baseLanes|l:l,e!==null){for(n=t.child=e.child,u=0;n!==null;)u=u|n.lanes|n.childLanes,n=n.sibling;t.childLanes=u&~a}else t.childLanes=0,t.child=null;return ao(e,t,a,l)}if((l&536870912)!==0)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&su(t,u!==null?u.cachePool:null),u!==null?ts(t,u):nc(),Qs(t);else return t.lanes=t.childLanes=536870912,ao(e,t,u!==null?u.baseLanes|l:l,l)}else u!==null?(su(t,u.cachePool),ts(t,u),ol(),t.memoizedState=null):(e!==null&&su(t,null),nc(),ol());return Le(e,t,n,l),t.child}function ao(e,t,l,a){var n=Pi();return n=n===null?null:{parent:_e._currentValue,pool:n},t.memoizedState={baseLanes:l,cachePool:n},e!==null&&su(t,null),nc(),Qs(t),e!==null&&Fa(e,t,a,!0),null}function Ou(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(f(284));(e===null||e.ref!==l)&&(t.flags|=4194816)}}function Rc(e,t,l,a,n){return Ll(t),l=cc(e,t,l,a,void 0,n),a=fc(),e!==null&&!Be?(rc(e,t,n),Vt(e,t,n)):(he&&a&&Zi(t),t.flags|=1,Le(e,t,l,n),t.child)}function no(e,t,l,a,n,u){return Ll(t),t.updateQueue=null,l=as(t,a,l,n),ls(e),a=fc(),e!==null&&!Be?(rc(e,t,u),Vt(e,t,u)):(he&&a&&Zi(t),t.flags|=1,Le(e,t,l,u),t.child)}function uo(e,t,l,a,n){if(Ll(t),t.stateNode===null){var u=fa,c=l.contextType;typeof c=="object"&&c!==null&&(u=Ve(c)),u=new l(a,u),t.memoizedState=u.state!==null&&u.state!==void 0?u.state:null,u.updater=Tc,t.stateNode=u,u._reactInternals=t,u=t.stateNode,u.props=a,u.state=t.memoizedState,u.refs={},ec(t),c=l.contextType,u.context=typeof c=="object"&&c!==null?Ve(c):fa,u.state=t.memoizedState,c=l.getDerivedStateFromProps,typeof c=="function"&&(Ec(t,l,c,a),u.state=t.memoizedState),typeof l.getDerivedStateFromProps=="function"||typeof u.getSnapshotBeforeUpdate=="function"||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(c=u.state,typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount(),c!==u.state&&Tc.enqueueReplaceState(u,u.state,null),nn(t,a,u,n),an(),u.state=t.memoizedState),typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!0}else if(e===null){u=t.stateNode;var r=t.memoizedProps,y=Xl(l,r);u.props=y;var T=u.context,D=l.contextType;c=fa,typeof D=="object"&&D!==null&&(c=Ve(D));var _=l.getDerivedStateFromProps;D=typeof _=="function"||typeof u.getSnapshotBeforeUpdate=="function",r=t.pendingProps!==r,D||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(r||T!==c)&&Vs(t,u,a,c),il=!1;var R=t.memoizedState;u.state=R,nn(t,a,u,n),an(),T=t.memoizedState,r||R!==T||il?(typeof _=="function"&&(Ec(t,l,_,a),T=t.memoizedState),(y=il||Zs(t,l,y,a,R,T,c))?(D||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount()),typeof u.componentDidMount=="function"&&(t.flags|=4194308)):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=a,t.memoizedState=T),u.props=a,u.state=T,u.context=c,a=y):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!1)}else{u=t.stateNode,tc(e,t),c=t.memoizedProps,D=Xl(l,c),u.props=D,_=t.pendingProps,R=u.context,T=l.contextType,y=fa,typeof T=="object"&&T!==null&&(y=Ve(T)),r=l.getDerivedStateFromProps,(T=typeof r=="function"||typeof u.getSnapshotBeforeUpdate=="function")||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(c!==_||R!==y)&&Vs(t,u,a,y),il=!1,R=t.memoizedState,u.state=R,nn(t,a,u,n),an();var N=t.memoizedState;c!==_||R!==N||il||e!==null&&e.dependencies!==null&&fu(e.dependencies)?(typeof r=="function"&&(Ec(t,l,r,a),N=t.memoizedState),(D=il||Zs(t,l,D,a,R,N,y)||e!==null&&e.dependencies!==null&&fu(e.dependencies))?(T||typeof u.UNSAFE_componentWillUpdate!="function"&&typeof u.componentWillUpdate!="function"||(typeof u.componentWillUpdate=="function"&&u.componentWillUpdate(a,N,y),typeof u.UNSAFE_componentWillUpdate=="function"&&u.UNSAFE_componentWillUpdate(a,N,y)),typeof u.componentDidUpdate=="function"&&(t.flags|=4),typeof u.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof u.componentDidUpdate!="function"||c===e.memoizedProps&&R===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&R===e.memoizedState||(t.flags|=1024),t.memoizedProps=a,t.memoizedState=N),u.props=a,u.state=N,u.context=y,a=D):(typeof u.componentDidUpdate!="function"||c===e.memoizedProps&&R===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&R===e.memoizedState||(t.flags|=1024),a=!1)}return u=a,Ou(e,t),a=(t.flags&128)!==0,u||a?(u=t.stateNode,l=a&&typeof l.getDerivedStateFromError!="function"?null:u.render(),t.flags|=1,e!==null&&a?(t.child=ba(t,e.child,null,n),t.child=ba(t,null,l,n)):Le(e,t,l,n),t.memoizedState=u.state,e=t.child):e=Vt(e,t,n),e}function io(e,t,l,a){return $a(),t.flags|=256,Le(e,t,l,a),t.child}var Nc={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Oc(e){return{baseLanes:e,cachePool:Jr()}}function Mc(e,t,l){return e=e!==null?e.childLanes&~l:0,t&&(e|=pt),e}function co(e,t,l){var a=t.pendingProps,n=!1,u=(t.flags&128)!==0,c;if((c=u)||(c=e!==null&&e.memoizedState===null?!1:(Ue.current&2)!==0),c&&(n=!0,t.flags&=-129),c=(t.flags&32)!==0,t.flags&=-33,e===null){if(he){if(n?sl(t):ol(),he){var r=Oe,y;if(y=r){e:{for(y=r,r=Nt;y.nodeType!==8;){if(!r){r=null;break e}if(y=Tt(y.nextSibling),y===null){r=null;break e}}r=y}r!==null?(t.memoizedState={dehydrated:r,treeContext:Ul!==null?{id:Lt,overflow:Yt}:null,retryLane:536870912,hydrationErrors:null},y=ut(18,null,null,0),y.stateNode=r,y.return=t,t.child=y,ke=t,Oe=null,y=!0):y=!1}y||wl(t)}if(r=t.memoizedState,r!==null&&(r=r.dehydrated,r!==null))return df(r)?t.lanes=32:t.lanes=536870912,null;Zt(t)}return r=a.children,a=a.fallback,n?(ol(),n=t.mode,r=Mu({mode:"hidden",children:r},n),a=_l(a,n,l,null),r.return=t,a.return=t,r.sibling=a,t.child=r,n=t.child,n.memoizedState=Oc(l),n.childLanes=Mc(e,c,l),t.memoizedState=Nc,a):(sl(t),jc(t,r))}if(y=e.memoizedState,y!==null&&(r=y.dehydrated,r!==null)){if(u)t.flags&256?(sl(t),t.flags&=-257,t=Dc(e,t,l)):t.memoizedState!==null?(ol(),t.child=e.child,t.flags|=128,t=null):(ol(),n=a.fallback,r=t.mode,a=Mu({mode:"visible",children:a.children},r),n=_l(n,r,l,null),n.flags|=2,a.return=t,n.return=t,a.sibling=n,t.child=a,ba(t,e.child,null,l),a=t.child,a.memoizedState=Oc(l),a.childLanes=Mc(e,c,l),t.memoizedState=Nc,t=n);else if(sl(t),df(r)){if(c=r.nextSibling&&r.nextSibling.dataset,c)var T=c.dgst;c=T,a=Error(f(419)),a.stack="",a.digest=c,Wa({value:a,source:null,stack:null}),t=Dc(e,t,l)}else if(Be||Fa(e,t,l,!1),c=(l&e.childLanes)!==0,Be||c){if(c=xe,c!==null&&(a=l&-l,a=(a&42)!==0?1:di(a),a=(a&(c.suspendedLanes|l))!==0?0:a,a!==0&&a!==y.retryLane))throw y.retryLane=a,ca(e,a),st(c,e,a),Ps;r.data==="$?"||kc(),t=Dc(e,t,l)}else r.data==="$?"?(t.flags|=192,t.child=e.child,t=null):(e=y.treeContext,Oe=Tt(r.nextSibling),ke=t,he=!0,Bl=null,Nt=!1,e!==null&&(vt[gt++]=Lt,vt[gt++]=Yt,vt[gt++]=Ul,Lt=e.id,Yt=e.overflow,Ul=t),t=jc(t,a.children),t.flags|=4096);return t}return n?(ol(),n=a.fallback,r=t.mode,y=e.child,T=y.sibling,a=qt(y,{mode:"hidden",children:a.children}),a.subtreeFlags=y.subtreeFlags&65011712,T!==null?n=qt(T,n):(n=_l(n,r,l,null),n.flags|=2),n.return=t,a.return=t,a.sibling=n,t.child=a,a=n,n=t.child,r=e.child.memoizedState,r===null?r=Oc(l):(y=r.cachePool,y!==null?(T=_e._currentValue,y=y.parent!==T?{parent:T,pool:T}:y):y=Jr(),r={baseLanes:r.baseLanes|l,cachePool:y}),n.memoizedState=r,n.childLanes=Mc(e,c,l),t.memoizedState=Nc,a):(sl(t),l=e.child,e=l.sibling,l=qt(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 jc(e,t){return t=Mu({mode:"visible",children:t},e.mode),t.return=e,e.child=t}function Mu(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 ba(t,e.child,null,l),e=jc(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 zc(e,t,l,a,n){var u=e.memoizedState;u===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:a,tail:l,tailMode:n}:(u.isBackwards=t,u.rendering=null,u.renderingStartTime=0,u.last=a,u.tail=l,u.tailMode=n)}function ro(e,t,l){var a=t.pendingProps,n=a.revealOrder,u=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),n){case"forwards":for(l=t.child,n=null;l!==null;)e=l.alternate,e!==null&&Au(e)===null&&(n=l),l=l.sibling;l=n,l===null?(n=t.child,t.child=null):(n=l.sibling,l.sibling=null),zc(t,!1,n,l,u);break;case"backwards":for(l=null,n=t.child,t.child=null;n!==null;){if(e=n.alternate,e!==null&&Au(e)===null){t.child=n;break}e=n.sibling,n.sibling=l,l=n,n=e}zc(t,!0,l,null,u);break;case"together":zc(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Vt(e,t,l){if(e!==null&&(t.dependencies=e.dependencies),vl|=t.lanes,(l&t.childLanes)===0)if(e!==null){if(Fa(e,t,l,!1),(l&t.childLanes)===0)return null}else return null;if(e!==null&&t.child!==e.child)throw Error(f(153));if(t.child!==null){for(e=t.child,l=qt(e,e.pendingProps),t.child=l,l.return=t;e.sibling!==null;)e=e.sibling,l=l.sibling=qt(e,e.pendingProps),l.return=t;l.sibling=null}return t.child}function Cc(e,t){return(e.lanes&t)!==0?!0:(e=e.dependencies,!!(e!==null&&fu(e)))}function Bm(e,t,l){switch(t.tag){case 3:Te(t,t.stateNode.containerInfo),ul(t,_e,e.memoizedState.cache),$a();break;case 27:case 5:ci(t);break;case 4:Te(t,t.stateNode.containerInfo);break;case 10:ul(t,t.type,t.memoizedProps.value);break;case 13:var a=t.memoizedState;if(a!==null)return a.dehydrated!==null?(sl(t),t.flags|=128,null):(l&t.child.childLanes)!==0?co(e,t,l):(sl(t),e=Vt(e,t,l),e!==null?e.sibling:null);sl(t);break;case 19:var n=(e.flags&128)!==0;if(a=(l&t.childLanes)!==0,a||(Fa(e,t,l,!1),a=(l&t.childLanes)!==0),n){if(a)return ro(e,t,l);t.flags|=128}if(n=t.memoizedState,n!==null&&(n.rendering=null,n.tail=null,n.lastEffect=null),Y(Ue,Ue.current),a)break;return null;case 22:case 23:return t.lanes=0,lo(e,t,l);case 24:ul(t,_e,e.memoizedState.cache)}return Vt(e,t,l)}function so(e,t,l){if(e!==null)if(e.memoizedProps!==t.pendingProps)Be=!0;else{if(!Cc(e,l)&&(t.flags&128)===0)return Be=!1,Bm(e,t,l);Be=(e.flags&131072)!==0}else Be=!1,he&&(t.flags&1048576)!==0&&Yr(t,cu,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var a=t.elementType,n=a._init;if(a=n(a._payload),t.type=a,typeof a=="function")Gi(a)?(e=Xl(a,e),t.tag=1,t=uo(null,t,a,e,l)):(t.tag=0,t=Rc(null,t,a,e,l));else{if(a!=null){if(n=a.$$typeof,n===ce){t.tag=11,t=Is(null,t,a,e,l);break e}else if(n===ne){t.tag=14,t=eo(null,t,a,e,l);break e}}throw t=Ol(a)||a,Error(f(306,t,""))}}return t;case 0:return Rc(e,t,t.type,t.pendingProps,l);case 1:return a=t.type,n=Xl(a,t.pendingProps),uo(e,t,a,n,l);case 3:e:{if(Te(t,t.stateNode.containerInfo),e===null)throw Error(f(387));a=t.pendingProps;var u=t.memoizedState;n=u.element,tc(e,t),nn(t,a,null,l);var c=t.memoizedState;if(a=c.cache,ul(t,_e,a),a!==u.cache&&$i(t,[_e],l,!0),an(),a=c.element,u.isDehydrated)if(u={element:a,isDehydrated:!1,cache:c.cache},t.updateQueue.baseState=u,t.memoizedState=u,t.flags&256){t=io(e,t,a,l);break e}else if(a!==n){n=mt(Error(f(424)),t),Wa(n),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=Tt(e.firstChild),ke=t,he=!0,Bl=null,Nt=!0,l=Xs(t,null,a,l),t.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling}else{if($a(),a===n){t=Vt(e,t,l);break e}Le(e,t,a,l)}t=t.child}return t;case 26:return Ou(e,t),e===null?(l=yd(t.type,null,t.pendingProps,null))?t.memoizedState=l:he||(l=t.type,e=t.pendingProps,a=Xu(ee.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&&he&&(a=t.stateNode=dd(t.type,t.pendingProps,ee.current),ke=t,Nt=!0,n=Oe,Sl(t.type)?(hf=n,Oe=Tt(a.firstChild)):Oe=n),Le(e,t,t.pendingProps.children,l),Ou(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&he&&((n=a=Oe)&&(a=sy(a,t.type,t.pendingProps,Nt),a!==null?(t.stateNode=a,ke=t,Oe=Tt(a.firstChild),Nt=!1,n=!0):n=!1),n||wl(t)),ci(t),n=t.type,u=t.pendingProps,c=e!==null?e.memoizedProps:null,a=u.children,rf(n,u)?a=null:c!==null&&rf(n,c)&&(t.flags|=32),t.memoizedState!==null&&(n=cc(e,t,Mm,null,null,l),On._currentValue=n),Ou(e,t),Le(e,t,a,l),t.child;case 6:return e===null&&he&&((e=l=Oe)&&(l=oy(l,t.pendingProps,Nt),l!==null?(t.stateNode=l,ke=t,Oe=null,e=!0):e=!1),e||wl(t)),null;case 13:return co(e,t,l);case 4:return Te(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=ba(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,ul(t,t.type,a.value),Le(e,t,a.children,l),t.child;case 9:return n=t.type._context,a=t.pendingProps.children,Ll(t),n=Ve(n),a=a(n),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=Mu(a,l),l.ref=t.ref,t.child=l,l.return=t,t=l):(l=qt(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 Ll(t),a=Ve(_e),e===null?(n=Pi(),n===null&&(n=xe,u=Wi(),n.pooledCache=u,u.refCount++,u!==null&&(n.pooledCacheLanes|=l),n=u),t.memoizedState={parent:a,cache:n},ec(t),ul(t,_e,n)):((e.lanes&l)!==0&&(tc(e,t),nn(t,null,null,l),an()),n=e.memoizedState,u=t.memoizedState,n.parent!==a?(n={parent:a,cache:a},t.memoizedState=n,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=n),ul(t,_e,a)):(a=u.cache,ul(t,_e,a),a!==n.cache&&$i(t,[_e],l,!0))),Le(e,t,t.pendingProps.children,l),t.child;case 29:throw t.pendingProps}throw Error(f(156,t.tag))}function Kt(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=bt.current,t!==null&&((re&4194048)===re?Ot!==null:(re&62914560)!==re&&(re&536870912)===0||t!==Ot))throw tn=Ii,kr;e.flags|=8192}}function ju(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag!==22?Qf():536870912,e.lanes|=t,Ea|=t)}function dn(e,t){if(!he)switch(e.tailMode){case"hidden":t=e.tail;for(var l=null;t!==null;)t.alternate!==null&&(l=t),t=t.sibling;l===null?e.tail=null:l.sibling=null;break;case"collapsed":l=e.tail;for(var a=null;l!==null;)l.alternate!==null&&(a=l),l=l.sibling;a===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:a.sibling=null}}function Re(e){var t=e.alternate!==null&&e.alternate.child===e.child,l=0,a=0;if(t)for(var n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags&65011712,a|=n.flags&65011712,n.return=e,n=n.sibling;else for(n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags,a|=n.flags,n.return=e,n=n.sibling;return e.subtreeFlags|=a,e.childLanes=l,t}function wm(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 Re(t),null;case 1:return Re(t),null;case 3:return l=t.stateNode,a=null,e!==null&&(a=e.memoizedState.cache),t.memoizedState.cache!==a&&(t.flags|=2048),Xt(_e),tl(),l.pendingContext&&(l.context=l.pendingContext,l.pendingContext=null),(e===null||e.child===null)&&(ka(t)?Kt(t):e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,Qr())),Re(t),null;case 26:return l=t.memoizedState,e===null?(Kt(t),l!==null?(Re(t),oo(t,l)):(Re(t),t.flags&=-16777217)):l?l!==e.memoizedState?(Kt(t),Re(t),oo(t,l)):(Re(t),t.flags&=-16777217):(e.memoizedProps!==a&&Kt(t),Re(t),t.flags&=-16777217),null;case 27:Yn(t),l=ee.current;var n=t.type;if(e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Kt(t);else{if(!a){if(t.stateNode===null)throw Error(f(166));return Re(t),null}e=$.current,ka(t)?Gr(t):(e=dd(n,a,l),t.stateNode=e,Kt(t))}return Re(t),null;case 5:if(Yn(t),l=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Kt(t);else{if(!a){if(t.stateNode===null)throw Error(f(166));return Re(t),null}if(e=$.current,ka(t))Gr(t);else{switch(n=Xu(ee.current),e){case 1:e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case 2:e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;default:switch(l){case"svg":e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case"math":e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;case"script":e=n.createElement("div"),e.innerHTML=" - + +
diff --git a/main/main.c b/main/main.c index 2c484e6..7c96879 100755 --- a/main/main.c +++ b/main/main.c @@ -18,7 +18,7 @@ #include "network.h" #include "board_config.h" #include "logger.h" -#include "rest_main.h" +#include "rest_main.h" #include "peripherals.h" #include "protocols.h" @@ -29,15 +29,15 @@ #include "meter_manager.h" #include "buzzer.h" #include "evse_link.h" - +#include "ocpp.h" #define EVSE_MANAGER_TICK_PERIOD_MS 1000 -#define AP_CONNECTION_TIMEOUT 120000 -#define RESET_HOLD_TIME 10000 -#define DEBOUNCE_TIME_MS 50 +#define AP_CONNECTION_TIMEOUT 120000 +#define RESET_HOLD_TIME 30000 +#define DEBOUNCE_TIME_MS 50 -#define PRESS_BIT BIT0 -#define RELEASED_BIT BIT1 +#define PRESS_BIT BIT0 +#define RELEASED_BIT BIT1 static const char *TAG = "app_main"; @@ -46,11 +46,11 @@ static TickType_t press_tick = 0; static TickType_t last_interrupt_tick = 0; static bool pressed = false; - // // File system (SPIFFS) init and info // -static void fs_info(esp_vfs_spiffs_conf_t *conf) { +static void fs_info(esp_vfs_spiffs_conf_t *conf) +{ size_t total = 0, used = 0; esp_err_t ret = esp_spiffs_info(conf->partition_label, &total, &used); if (ret == ESP_OK) @@ -59,20 +59,19 @@ static void fs_info(esp_vfs_spiffs_conf_t *conf) { ESP_LOGE(TAG, "Failed to get SPIFFS info: %s", esp_err_to_name(ret)); } -static void fs_init(void) { +static void fs_init(void) +{ esp_vfs_spiffs_conf_t cfg_conf = { .base_path = "/cfg", .partition_label = "cfg", .max_files = 1, - .format_if_mount_failed = false - }; + .format_if_mount_failed = false}; esp_vfs_spiffs_conf_t data_conf = { .base_path = "/data", .partition_label = "data", .max_files = 5, - .format_if_mount_failed = true - }; + .format_if_mount_failed = true}; ESP_ERROR_CHECK(esp_vfs_spiffs_register(&cfg_conf)); ESP_ERROR_CHECK(esp_vfs_spiffs_register(&data_conf)); @@ -83,116 +82,139 @@ static void fs_init(void) { // // Wi-Fi event monitoring task // -static void wifi_event_task_func(void *param) { +static void wifi_event_task_func(void *param) +{ EventBits_t mode_bits; - for (;;) { + for (;;) + { // Wait indefinitely until either AP or STA mode is entered mode_bits = xEventGroupWaitBits( wifi_event_group, WIFI_AP_MODE_BIT | WIFI_STA_MODE_BIT, - pdFALSE, // do not clear bits on exit - pdFALSE, // wait for any bit - portMAX_DELAY - ); + pdFALSE, // do not clear bits on exit + pdFALSE, // wait for any bit + portMAX_DELAY); - if (mode_bits & WIFI_AP_MODE_BIT) { + if (mode_bits & WIFI_AP_MODE_BIT) + { // We're in AP mode: wait for a client to connect within the timeout if (xEventGroupWaitBits( wifi_event_group, WIFI_AP_CONNECTED_BIT, pdFALSE, pdFALSE, - pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT) - ) & WIFI_AP_CONNECTED_BIT) { + pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT)) & + WIFI_AP_CONNECTED_BIT) + { // Once connected, block until the client disconnects xEventGroupWaitBits( wifi_event_group, WIFI_AP_DISCONNECTED_BIT, pdFALSE, pdFALSE, - portMAX_DELAY - ); - } else { + portMAX_DELAY); + } + else + { // Timeout expired with no client—optionally stop the AP - if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) { + if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) + { // wifi_ap_stop(); } } - } else if (mode_bits & WIFI_STA_MODE_BIT) { + } + else if (mode_bits & WIFI_STA_MODE_BIT) + { // We're in STA mode: block until disconnected from the AP xEventGroupWaitBits( wifi_event_group, WIFI_STA_DISCONNECTED_BIT, pdFALSE, pdFALSE, - portMAX_DELAY - ); + portMAX_DELAY); } // Prevent this task from hogging the CPU when idle - //vTaskDelay(pdMS_TO_TICKS(10)); + // vTaskDelay(pdMS_TO_TICKS(10)); } } // // Button press handler // -static void handle_button_press(void) { +static void handle_button_press(void) +{ // If not already in AP mode, start it - if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) { + if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) + { ESP_LOGI(TAG, "Starting Wi-Fi AP mode"); wifi_ap_start(); } } // Task to handle button press/release notifications -static void user_input_task_func(void *param) { +static void user_input_task_func(void *param) +{ uint32_t notification; - for (;;) { + for (;;) + { // Wait for notification bits from ISR if (xTaskNotifyWait( - 0, // do not clear any bits on entry - UINT32_MAX, // clear all bits on exit + 0, // do not clear any bits on entry + UINT32_MAX, // clear all bits on exit ¬ification, - portMAX_DELAY)) { + portMAX_DELAY)) + { // Handle button press event - if (notification & PRESS_BIT) { + if (notification & PRESS_BIT) + { press_tick = xTaskGetTickCount(); pressed = true; ESP_LOGI(TAG, "Button Pressed"); - handle_button_press(); + handle_button_press(); // só aqui } - // Handle button release event (only if previously pressed) - if ((notification & RELEASED_BIT) && pressed) { + if ((notification & RELEASED_BIT) && pressed) + { pressed = false; - ESP_LOGI(TAG, "Button Released"); - handle_button_press(); + TickType_t held = xTaskGetTickCount() - press_tick; + ESP_LOGI(TAG, "Button Released (held %u ms)", (unsigned)pdTICKS_TO_MS(held)); + if (held >= pdMS_TO_TICKS(RESET_HOLD_TIME)) + { + ESP_LOGW(TAG, "Long press: erasing NVS + reboot"); + nvs_flash_erase(); + esp_restart(); + } } } } } // ISR for button GPIO interrupt (active-low) -static void IRAM_ATTR button_isr_handler(void *arg) { +static void IRAM_ATTR button_isr_handler(void *arg) +{ BaseType_t higher_task_woken = pdFALSE; TickType_t now = xTaskGetTickCountFromISR(); // Debounce: ignore interrupts occurring too close together - if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) { + if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) + { return; } last_interrupt_tick = now; // Read GPIO level: 0 = button pressed, 1 = button released int level = gpio_get_level(board_config.button_wifi_gpio); - if (level == 0) { + if (level == 0) + { // Notify task: button pressed xTaskNotifyFromISR( user_input_task, PRESS_BIT, eSetBits, &higher_task_woken); - } else { + } + else + { // Notify task: button released xTaskNotifyFromISR( user_input_task, @@ -202,20 +224,20 @@ static void IRAM_ATTR button_isr_handler(void *arg) { } // Yield to higher priority task if unblocked - if (higher_task_woken) { + if (higher_task_woken) + { portYIELD_FROM_ISR(); } } - -static void button_init(void) { +static void button_init(void) +{ gpio_config_t conf = { .pin_bit_mask = BIT64(board_config.button_wifi_gpio), .mode = GPIO_MODE_INPUT, .pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_up_en = GPIO_PULLUP_ENABLE, - .intr_type = GPIO_INTR_ANYEDGE - }; + .intr_type = GPIO_INTR_ANYEDGE}; ESP_ERROR_CHECK(gpio_config(&conf)); ESP_ERROR_CHECK(gpio_isr_handler_add(board_config.button_wifi_gpio, button_isr_handler, NULL)); } @@ -223,20 +245,23 @@ static void button_init(void) { // // Inicialização dos módulos do sistema // -static void init_modules(void) { +static void init_modules(void) +{ peripherals_init(); - //api_init(); + wifi_ini(); + // api_init(); buzzer_init(); ESP_ERROR_CHECK(rest_server_init("/data")); protocols_init(); evse_manager_init(); - evse_init(); // Cria a task para FSM + evse_init(); // Cria a task para FSM button_init(); auth_init(); loadbalancer_init(); meter_manager_init(); - meter_manager_start(); + meter_manager_start(); evse_link_init(); + ocpp_start(); // wifi_ap_start(); // Outros módulos (descomente conforme necessário) @@ -253,7 +278,8 @@ static void init_modules(void) { // // Função principal do firmware // -void app_main(void) { +void app_main(void) +{ logger_init(); esp_log_set_vprintf(logger_vprintf); @@ -261,7 +287,8 @@ void app_main(void) { ESP_LOGI(TAG, "Reset reason: %d", reason); esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { ESP_LOGW(TAG, "Erasing NVS flash"); ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); @@ -274,8 +301,6 @@ void app_main(void) { ESP_ERROR_CHECK(gpio_install_isr_service(0)); board_config_load(); - wifi_ini(); - //wifi_ap_start(); init_modules(); xTaskCreate(wifi_event_task_func, "wifi_event_task", 8 * 1024, NULL, 3, NULL); diff --git a/projeto_parte1.c b/projeto_parte1.c index 5ed30f4..6c77a03 100644 --- a/projeto_parte1.c +++ b/projeto_parte1.c @@ -21,7 +21,7 @@ #include "network.h" #include "board_config.h" #include "logger.h" -#include "rest_main.h" +#include "rest_main.h" #include "peripherals.h" #include "protocols.h" @@ -32,15 +32,15 @@ #include "meter_manager.h" #include "buzzer.h" #include "evse_link.h" - +#include "ocpp.h" #define EVSE_MANAGER_TICK_PERIOD_MS 1000 -#define AP_CONNECTION_TIMEOUT 120000 -#define RESET_HOLD_TIME 10000 -#define DEBOUNCE_TIME_MS 50 +#define AP_CONNECTION_TIMEOUT 120000 +#define RESET_HOLD_TIME 30000 +#define DEBOUNCE_TIME_MS 50 -#define PRESS_BIT BIT0 -#define RELEASED_BIT BIT1 +#define PRESS_BIT BIT0 +#define RELEASED_BIT BIT1 static const char *TAG = "app_main"; @@ -49,11 +49,11 @@ static TickType_t press_tick = 0; static TickType_t last_interrupt_tick = 0; static bool pressed = false; - // // File system (SPIFFS) init and info // -static void fs_info(esp_vfs_spiffs_conf_t *conf) { +static void fs_info(esp_vfs_spiffs_conf_t *conf) +{ size_t total = 0, used = 0; esp_err_t ret = esp_spiffs_info(conf->partition_label, &total, &used); if (ret == ESP_OK) @@ -62,20 +62,19 @@ static void fs_info(esp_vfs_spiffs_conf_t *conf) { ESP_LOGE(TAG, "Failed to get SPIFFS info: %s", esp_err_to_name(ret)); } -static void fs_init(void) { +static void fs_init(void) +{ esp_vfs_spiffs_conf_t cfg_conf = { .base_path = "/cfg", .partition_label = "cfg", .max_files = 1, - .format_if_mount_failed = false - }; + .format_if_mount_failed = false}; esp_vfs_spiffs_conf_t data_conf = { .base_path = "/data", .partition_label = "data", .max_files = 5, - .format_if_mount_failed = true - }; + .format_if_mount_failed = true}; ESP_ERROR_CHECK(esp_vfs_spiffs_register(&cfg_conf)); ESP_ERROR_CHECK(esp_vfs_spiffs_register(&data_conf)); @@ -86,116 +85,139 @@ static void fs_init(void) { // // Wi-Fi event monitoring task // -static void wifi_event_task_func(void *param) { +static void wifi_event_task_func(void *param) +{ EventBits_t mode_bits; - for (;;) { + for (;;) + { // Wait indefinitely until either AP or STA mode is entered mode_bits = xEventGroupWaitBits( wifi_event_group, WIFI_AP_MODE_BIT | WIFI_STA_MODE_BIT, - pdFALSE, // do not clear bits on exit - pdFALSE, // wait for any bit - portMAX_DELAY - ); + pdFALSE, // do not clear bits on exit + pdFALSE, // wait for any bit + portMAX_DELAY); - if (mode_bits & WIFI_AP_MODE_BIT) { + if (mode_bits & WIFI_AP_MODE_BIT) + { // We're in AP mode: wait for a client to connect within the timeout if (xEventGroupWaitBits( wifi_event_group, WIFI_AP_CONNECTED_BIT, pdFALSE, pdFALSE, - pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT) - ) & WIFI_AP_CONNECTED_BIT) { + pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT)) & + WIFI_AP_CONNECTED_BIT) + { // Once connected, block until the client disconnects xEventGroupWaitBits( wifi_event_group, WIFI_AP_DISCONNECTED_BIT, pdFALSE, pdFALSE, - portMAX_DELAY - ); - } else { + portMAX_DELAY); + } + else + { // Timeout expired with no client—optionally stop the AP - if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) { + if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) + { // wifi_ap_stop(); } } - } else if (mode_bits & WIFI_STA_MODE_BIT) { + } + else if (mode_bits & WIFI_STA_MODE_BIT) + { // We're in STA mode: block until disconnected from the AP xEventGroupWaitBits( wifi_event_group, WIFI_STA_DISCONNECTED_BIT, pdFALSE, pdFALSE, - portMAX_DELAY - ); + portMAX_DELAY); } // Prevent this task from hogging the CPU when idle - //vTaskDelay(pdMS_TO_TICKS(10)); + // vTaskDelay(pdMS_TO_TICKS(10)); } } // // Button press handler // -static void handle_button_press(void) { +static void handle_button_press(void) +{ // If not already in AP mode, start it - if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) { + if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) + { ESP_LOGI(TAG, "Starting Wi-Fi AP mode"); wifi_ap_start(); } } // Task to handle button press/release notifications -static void user_input_task_func(void *param) { +static void user_input_task_func(void *param) +{ uint32_t notification; - for (;;) { + for (;;) + { // Wait for notification bits from ISR if (xTaskNotifyWait( - 0, // do not clear any bits on entry - UINT32_MAX, // clear all bits on exit + 0, // do not clear any bits on entry + UINT32_MAX, // clear all bits on exit ¬ification, - portMAX_DELAY)) { + portMAX_DELAY)) + { // Handle button press event - if (notification & PRESS_BIT) { + if (notification & PRESS_BIT) + { press_tick = xTaskGetTickCount(); pressed = true; ESP_LOGI(TAG, "Button Pressed"); - handle_button_press(); + handle_button_press(); // só aqui } - // Handle button release event (only if previously pressed) - if ((notification & RELEASED_BIT) && pressed) { + if ((notification & RELEASED_BIT) && pressed) + { pressed = false; - ESP_LOGI(TAG, "Button Released"); - handle_button_press(); + TickType_t held = xTaskGetTickCount() - press_tick; + ESP_LOGI(TAG, "Button Released (held %u ms)", (unsigned)pdTICKS_TO_MS(held)); + if (held >= pdMS_TO_TICKS(RESET_HOLD_TIME)) + { + ESP_LOGW(TAG, "Long press: erasing NVS + reboot"); + nvs_flash_erase(); + esp_restart(); + } } } } } // ISR for button GPIO interrupt (active-low) -static void IRAM_ATTR button_isr_handler(void *arg) { +static void IRAM_ATTR button_isr_handler(void *arg) +{ BaseType_t higher_task_woken = pdFALSE; TickType_t now = xTaskGetTickCountFromISR(); // Debounce: ignore interrupts occurring too close together - if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) { + if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) + { return; } last_interrupt_tick = now; // Read GPIO level: 0 = button pressed, 1 = button released int level = gpio_get_level(board_config.button_wifi_gpio); - if (level == 0) { + if (level == 0) + { // Notify task: button pressed xTaskNotifyFromISR( user_input_task, PRESS_BIT, eSetBits, &higher_task_woken); - } else { + } + else + { // Notify task: button released xTaskNotifyFromISR( user_input_task, @@ -205,20 +227,20 @@ static void IRAM_ATTR button_isr_handler(void *arg) { } // Yield to higher priority task if unblocked - if (higher_task_woken) { + if (higher_task_woken) + { portYIELD_FROM_ISR(); } } - -static void button_init(void) { +static void button_init(void) +{ gpio_config_t conf = { .pin_bit_mask = BIT64(board_config.button_wifi_gpio), .mode = GPIO_MODE_INPUT, .pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_up_en = GPIO_PULLUP_ENABLE, - .intr_type = GPIO_INTR_ANYEDGE - }; + .intr_type = GPIO_INTR_ANYEDGE}; ESP_ERROR_CHECK(gpio_config(&conf)); ESP_ERROR_CHECK(gpio_isr_handler_add(board_config.button_wifi_gpio, button_isr_handler, NULL)); } @@ -226,20 +248,23 @@ static void button_init(void) { // // Inicialização dos módulos do sistema // -static void init_modules(void) { +static void init_modules(void) +{ peripherals_init(); - //api_init(); + wifi_ini(); + // api_init(); buzzer_init(); ESP_ERROR_CHECK(rest_server_init("/data")); protocols_init(); evse_manager_init(); - evse_init(); // Cria a task para FSM + evse_init(); // Cria a task para FSM button_init(); auth_init(); loadbalancer_init(); meter_manager_init(); - meter_manager_start(); + meter_manager_start(); evse_link_init(); + ocpp_start(); // wifi_ap_start(); // Outros módulos (descomente conforme necessário) @@ -256,7 +281,8 @@ static void init_modules(void) { // // Função principal do firmware // -void app_main(void) { +void app_main(void) +{ logger_init(); esp_log_set_vprintf(logger_vprintf); @@ -264,7 +290,8 @@ void app_main(void) { ESP_LOGI(TAG, "Reset reason: %d", reason); esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { ESP_LOGW(TAG, "Erasing NVS flash"); ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); @@ -277,8 +304,6 @@ void app_main(void) { ESP_ERROR_CHECK(gpio_install_isr_service(0)); board_config_load(); - wifi_ini(); - //wifi_ap_start(); init_modules(); xTaskCreate(wifi_event_task_func, "wifi_event_task", 8 * 1024, NULL, 3, NULL); @@ -288,3079 +313,1016 @@ void app_main(void) { // === Fim de: main/main.c === -// === Início de: components/evse/evse_pilot.c === -#include -#include +// === Início de: components/ocpp/src/ocpp_events.c === +#include "ocpp_events.h" + +/* Define a base, como em components/auth/src/auth_events.c */ +ESP_EVENT_DEFINE_BASE(OCPP_EVENTS); + +// === Fim de: components/ocpp/src/ocpp_events.c === + + +// === Início de: components/ocpp/src/ocpp.c === +// components/ocpp/src/ocpp.c +#include #include -#include -#include - -#include "driver/ledc.h" -#include "esp_err.h" -#include "esp_log.h" -#include "esp_rom_sys.h" - -#include "evse_pilot.h" -#include "adc121s021_dma.h" -#include "board_config.h" - -#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 - -#define NUM_PILOT_SAMPLES 100 -#define MAX_SAMPLE_ATTEMPTS 1000 -#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior - -// 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 { - 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 int compare_int(const void *a, const void *b) { - return (*(int *)a - *(int *)b); -} - -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 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++; - } - } - - 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; -} - -void evse_hardware_relay_on(void) { - ac_relay_set_state(true); -} - -void evse_hardware_relay_off(void) { - ac_relay_set_state(false); -} - -bool evse_hardware_relay_status(void) { - return ac_relay_get_state(); -} - -void evse_hardware_lock(void) { - socket_lock_set_locked(true); -} - -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/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 -static const char *TAG = "evse_meter"; -static SemaphoreHandle_t meter_mutex; - -typedef struct { - 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 evse_meter_data_t meter_data; - -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 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 - ); -} - -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/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" - -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 - -#include -#include - -/** - * @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 { - uint16_t high_mv; ///< Pico positivo medido (mV) - uint16_t low_mv; ///< Pico negativo medido (mV) -} pilot_voltage_cache_t; - -#ifdef __cplusplus -} -#endif - -#endif /* PILOT_H_ */ - -// === Fim de: components/evse/include/evse_pilot.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 +#include "ocpp.h" +#include "ocpp_events.h" -// ======================== -// 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" - -ESP_EVENT_DECLARE_BASE(EVSE_EVENTS); - -typedef enum { - 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 { - 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 - -// === Fim de: components/evse/include/evse_events.h === - - -// === Início de: components/evse/include/evse_api.h === -#ifndef EVSE_API_H -#define EVSE_API_H - -#include -#include -#include "evse_state.h" -#include "freertos/FreeRTOS.h" -#include "evse_session.h" - - -#ifdef __cplusplus -extern "C" { -#endif - -// =============================== -// Core EVSE State -// =============================== - -/** - * @brief Get current EVSE state (e.g., A, B1, C2). - */ -evse_state_t evse_get_state(void); - -/** - * @brief Set the EVSE state (e.g., called by FSM or hardware layer). - */ -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 "esp_wifi.h" #include "nvs.h" -#include -#include "meter_events.h" +#include "nvs_flash.h" + +#include "evse_error.h" +#include "auth_events.h" #include "evse_events.h" -#include "math.h" +#include "evse_state.h" +#include "meter_events.h" +#include "esp_timer.h" +#include -#ifndef MIN -#define MIN(a, b) ((a) < (b) ? (a) : (b)) -#endif +/* MicroOcpp includes */ +#include +#include // C-facade of MicroOcpp +#include // WebSocket integration for ESP-IDF -static const char *TAG = "loadbalancer"; +#define NVS_NAMESPACE "ocpp" +#define NVS_OCPP_ENABLED "enabled" +#define NVS_OCPP_SERVER "ocpp_server" +#define NVS_OCPP_CHARGE_ID "charge_id" -// 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 +static const char *TAG = "ocpp"; -// Parâmetros -static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; -static bool loadbalancer_enabled = false; +static bool 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; +static TaskHandle_t ocpp_task = NULL; -#define MAX_SLAVES 255 -#define CONNECTOR_COUNT (MAX_SLAVES + 1) +static struct mg_mgr mgr; // Event manager +static OCPP_Connection *g_ocpp_conn = NULL; // Para shutdown limpo -// 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 esp_event_handler_instance_t s_auth_verify_inst = NULL; -static evse_connector_t connectors[CONNECTOR_COUNT]; +// Flags refletindo o estado do EVSE (atualizadas por eventos) +static volatile bool s_ev_plugged = false; +static volatile bool s_ev_ready = false; -const int64_t METRICS_TIMEOUT_US = 60 * 1000000; // 60 segundos +static esp_event_handler_instance_t s_evse_state_inst = NULL; -// Helper: inicializa array de conectores -static void init_connectors(void) +// Flags de config (vindas de EVSE_EVENTS) +static volatile bool s_evse_enabled = true; +static volatile bool s_evse_available = true; + +static esp_event_handler_instance_t s_evse_enable_inst = NULL; +static esp_event_handler_instance_t s_evse_available_inst = NULL; + +// --- cache de medições vindas de METER_EVENT_DATA_READY --- +typedef struct { - // 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 - }; + // dados por fase + float vrms[3]; + float irms[3]; + int32_t watt[3]; // ativo por fase (W) + float frequency; + float power_factor; + // acumulados + float total_energy_kWh; + // derivados práticos + int32_t sum_watt; // soma das 3 fases + float avg_voltage; // média das 3 fases + float sum_current; // soma das 3 fases + // flag de validade + bool have_data; +} ocpp_meter_cache_t; + +static ocpp_meter_cache_t s_meter = {0}; +static portMUX_TYPE s_meter_mux = portMUX_INITIALIZER_UNLOCKED; +static esp_event_handler_instance_t s_meter_inst = NULL; + +/* ========================= + * Task / Main Loop + * ========================= * + */ +static void ocpp_task_func(void *param) +{ + while (true) + { + if (enabled) + { + mg_mgr_poll(&mgr, 100); + ocpp_loop(); + + bool operative = ocpp_isOperative(); + if (operative != s_evse_enabled) + { + s_evse_enabled = operative; + + // >>> enviar OCPP_EVENT (remoto → local) + ocpp_operative_event_t ev = { + .operative = operative, + .timestamp_us = esp_timer_get_time()}; + esp_event_post(OCPP_EVENTS, + OCPP_EVENT_OPERATIVE_UPDATED, + &ev, sizeof(ev), + portMAX_DELAY); + + ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d", + (int)operative); + } + } + else + { + vTaskDelay(pdMS_TO_TICKS(500)); + } } } -// --- Helpers --- -static void input_filter_reset(input_filter_t *filter) +/* ========================= + * NVS GETs + * ========================= */ +bool ocpp_get_enabled(void) { - filter->value = 0.0f; + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + // namespace ainda não existe -> default: disabled + return false; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return false; + } + + uint8_t value = 0; + err = nvs_get_u8(h, NVS_OCPP_ENABLED, &value); + nvs_close(h); + + if (err == ESP_ERR_NVS_NOT_FOUND) + return false; // default + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_get_u8(enabled) failed: %s", esp_err_to_name(err)); + return false; + } + return value != 0; } -// Callback de status de slave -static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id, void *data) +void ocpp_get_server(char *value /* out, size>=64 */) { - const loadbalancer_slave_status_event_t *status = (const loadbalancer_slave_status_event_t *)data; + if (!value) + return; + value[0] = '\0'; - if (status->slave_id >= MAX_SLAVES) { - ESP_LOGW(TAG, "Invalid slave_id %d", status->slave_id); + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + // namespace ainda não existe -> default: "" + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); 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; + size_t len = 64; + err = nvs_get_str(h, NVS_OCPP_SERVER, value, &len); + nvs_close(h); - 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, - void *event_data) -{ - if (id != METER_EVENT_DATA_READY || event_data == NULL) + if (err == ESP_ERR_NVS_NOT_FOUND) + { + value[0] = '\0'; 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); + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_get_str(server) failed: %s", esp_err_to_name(err)); + value[0] = '\0'; } } -static void loadbalancer_evse_event_handler(void *handler_arg, - esp_event_base_t base, - int32_t id, - void *event_data) +void ocpp_get_charge_id(char *value /* out, size>=64 */) { - const evse_state_event_data_t *evt = (const evse_state_event_data_t *)event_data; + if (!value) + return; + value[0] = '\0'; + + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); + if (err == ESP_ERR_NVS_NOT_FOUND) + { + // namespace ainda não existe -> default: "" + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + + size_t len = 64; + err = nvs_get_str(h, NVS_OCPP_CHARGE_ID, value, &len); + nvs_close(h); + + if (err == ESP_ERR_NVS_NOT_FOUND) + { + value[0] = '\0'; + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "nvs_get_str(charge_id) failed: %s", esp_err_to_name(err)); + value[0] = '\0'; + } +} + +/* ========================= + * NVS SETs + * ========================= */ +// --- SETTERS: RW (cria o namespace na 1ª escrita), commit e fecha --- +void ocpp_set_enabled(bool value) +{ + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "set enabled %d", value); + ESP_ERROR_CHECK(nvs_set_u8(h, NVS_OCPP_ENABLED, value ? 1 : 0)); + ESP_ERROR_CHECK(nvs_commit(h)); + nvs_close(h); + enabled = value; +} + +void ocpp_set_server(char *value) +{ + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "set server %s", value ? value : "(null)"); + ESP_ERROR_CHECK(nvs_set_str(h, NVS_OCPP_SERVER, value ? value : "")); + ESP_ERROR_CHECK(nvs_commit(h)); + nvs_close(h); +} + +void ocpp_set_charge_id(char *value) +{ + nvs_handle_t h; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + return; + } + ESP_LOGI(TAG, "set charge_id %s", value ? value : "(null)"); + ESP_ERROR_CHECK(nvs_set_str(h, NVS_OCPP_CHARGE_ID, value ? value : "")); + ESP_ERROR_CHECK(nvs_commit(h)); + nvs_close(h); +} + +static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, void *event_data) +{ + const auth_tag_verify_event_t *rq = (const auth_tag_verify_event_t *)event_data; + if (!rq) + return; + + // Sanitizar/copiar a idTag + char idtag[AUTH_TAG_MAX_LEN]; + if (rq->tag[0] == '\0') + { + strncpy(idtag, "IDTAG", sizeof(idtag)); + idtag[sizeof(idtag) - 1] = '\0'; + } + else + { + strncpy(idtag, rq->tag, sizeof(idtag) - 1); + idtag[sizeof(idtag) - 1] = '\0'; + } + + ESP_LOGI(TAG, "AUTH_EVENT_TAG_VERIFY: tag=%s req_id=%u", idtag, (unsigned)rq->req_id); + + // Se não está pronto, apenas regista e sai (podes adaptar conforme política) + if (!enabled || g_ocpp_conn == NULL) + { + ESP_LOGW(TAG, "OCPP not ready (enabled=%d, conn=%p) – ignoring verify", enabled, (void *)g_ocpp_conn); + return; + } + + // Regra pedida: + // - se já existe transação/charge em andamento -> terminar + // - senão -> iniciar com a IDTAG recebida + if (ocpp_isTransactionActive()) + { + ESP_LOGI(TAG, "Transaction active -> ocpp_end_transaction(\"%s\")", idtag); + ocpp_endTransaction(idtag, "Local"); + } + else + { + ESP_LOGI(TAG, "No active transaction -> ocpp_begin_transaction(\"%s\")", idtag); + ocpp_beginTransaction(idtag); + } +} + +static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL) + return; + + const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data; + ESP_LOGI(TAG, "EVSE event received: state = %d", (int)evt->state); + switch (evt->state) { case EVSE_STATE_EVENT_IDLE: - ESP_LOGI(TAG, "EVSE is IDLE - vehicle disconnected"); - connectors[0].charging = false; - connectors[0].online = false; + s_ev_plugged = false; + s_ev_ready = 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; + s_ev_plugged = true; + s_ev_ready = 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(); + s_ev_plugged = true; + s_ev_ready = true; // EV está a pedir/receber energia 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); + // em falha, considera não pronto (mantém plugged se quiseres) + s_ev_ready = false; break; } } - - -// --- Config persistência --- -static esp_err_t loadbalancer_load_config() +static void evse_enable_available_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { - 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; + if (base != EVSE_EVENTS || data == NULL) + return; + + if (id == EVSE_EVENT_ENABLE_UPDATED) + { + const evse_enable_event_data_t *e = (const evse_enable_event_data_t *)data; + s_evse_enabled = e->enabled; + ESP_LOGI(TAG, "[EVSE] ENABLE_UPDATED: enabled=%d (ts=%lld)", (int)e->enabled, (long long)e->timestamp_us); + return; } - 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)); + if (id == EVSE_EVENT_AVAILABLE_UPDATED) + { + const evse_available_event_data_t *e = (const evse_available_event_data_t *)data; + s_evse_available = e->available; + ESP_LOGI(TAG, "[EVSE] AVAILABLE_UPDATED: available=%d (ts=%lld)", (int)e->available, (long long)e->timestamp_us); + return; } } - -// --- Init --- -void loadbalancer_init(void) +static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *data) { - if (loadbalancer_load_config() != ESP_OK) - ESP_LOGW(TAG, "Failed to load/init config. Using defaults."); + if (base != METER_EVENT || id != METER_EVENT_DATA_READY || !data) + return; - init_connectors(); - input_filter_init(&grid_filter, 0.3f); - input_filter_init(&evse_filter, 0.3f); + const meter_event_data_t *evt = (const meter_event_data_t *)data; - if (xTaskCreate(loadbalancer_task, "loadbalancer", 4096, NULL, 4, NULL) != pdPASS) - ESP_LOGE(TAG, "Failed to create loadbalancer task"); + // Só queremos o medidor do EVSE (não o GRID) + if (!evt->source || strcmp(evt->source, "EVSE") != 0) + return; - 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); + // Derivados simples + int32_t sum_w = (int32_t)evt->watt[0] + (int32_t)evt->watt[1] + (int32_t)evt->watt[2]; + float avg_v = (evt->vrms[0] + evt->vrms[1] + evt->vrms[2]) / 3.0f; + float sum_i = evt->irms[0] + evt->irms[1] + evt->irms[2]; - 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)); + portENTER_CRITICAL(&s_meter_mux); + memcpy(s_meter.vrms, evt->vrms, sizeof(s_meter.vrms)); + memcpy(s_meter.irms, evt->irms, sizeof(s_meter.irms)); + s_meter.watt[0] = evt->watt[0]; + s_meter.watt[1] = evt->watt[1]; + s_meter.watt[2] = evt->watt[2]; + s_meter.frequency = evt->frequency; + s_meter.power_factor = evt->power_factor; + s_meter.total_energy_kWh = evt->total_energy; // já vem em kWh segundo o teu .h + s_meter.sum_watt = sum_w; + s_meter.avg_voltage = avg_v; + s_meter.sum_current = sum_i; + s_meter.have_data = true; + portEXIT_CRITICAL(&s_meter_mux); } -// === Fim de: components/loadbalancer/src/loadbalancer.c === +/* ========================= + * MicroOCPP Inputs/CBs + * ========================= */ +bool setConnectorPluggedInput(void) +{ + return s_ev_plugged; // EV fisicamente ligado +} +bool setEvReadyInput(void) +{ + return s_ev_ready; // EV pede / pronto a carregar +} -// === Início de: components/loadbalancer/src/input_filter.c === -#include "input_filter.h" +bool setEvseReadyInput(void) +{ + // EVSE autorizado / operacional + return s_evse_enabled && s_evse_available; +} -void input_filter_init(input_filter_t *filter, float alpha) { - if (filter) { - filter->alpha = alpha; - filter->value = 0.0f; - filter->initialized = 0; +float setPowerMeterInput(void) +{ + int32_t w = 0; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + w = s_meter.sum_watt; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + { + ESP_LOGW(TAG, "[METER] PowerMeterInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] PowerMeterInput: %" PRId32 " W", w); + } + return (float)w; +} + +float setEnergyMeterInput(void) +{ + float kwh = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + kwh = s_meter.total_energy_kWh; + portEXIT_CRITICAL(&s_meter_mux); + + float wh = kwh * 1000.0f; + + if (!have) + { + ESP_LOGW(TAG, "[METER] EnergyMeterInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] EnergyMeterInput: %.3f kWh (%.1f Wh)", kwh, wh); + } + return wh; // agora devolve Wh +} + +int setEnergyInput(void) +{ + float energy_kWh = setEnergyMeterInput(); // kWh + int wh = (int)lrintf((double)energy_kWh * 1000.0); // Wh arredondado + ESP_LOGD(TAG, "[METER] EnergyInput: %.3f kWh -> %d Wh", energy_kWh, wh); + return wh; +} + +float setCurrentInput(void) +{ + float a = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + a = s_meter.sum_current; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + { + ESP_LOGW(TAG, "[METER] CurrentInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] CurrentInput: %.2f A (total)", a); + } + return a; +} + +float setVoltageInput(void) +{ + float v = 0.0f; + bool have = false; + + portENTER_CRITICAL(&s_meter_mux); + have = s_meter.have_data; + if (have) + v = s_meter.avg_voltage; + portEXIT_CRITICAL(&s_meter_mux); + + if (!have) + { + ESP_LOGW(TAG, "[METER] VoltageInput: no data (return 0)"); + } + else + { + ESP_LOGD(TAG, "[METER] VoltageInput: %.1f V (avg)", v); + } + return v; +} + +float setPowerInput(void) +{ + float w = setPowerMeterInput(); // alias + ESP_LOGD(TAG, "[METER] PowerInput: %.1f W", w); + return w; +} + +float setTemperatureInput(void) +{ + ESP_LOGD(TAG, "TemperatureInput"); + return 16.5f; +} + +void setSmartChargingCurrentOutput(float limit) +{ + ESP_LOGI(TAG, "SmartChargingCurrentOutput: %.0f", limit); +} + +void setSmartChargingPowerOutput(float limit) +{ + ESP_LOGI(TAG, "SmartChargingPowerOutput: %.0f", limit); +} + +void setSmartChargingOutput(float power, float current, int nphases) +{ + ESP_LOGI(TAG, "SmartChargingOutput: P=%.0f W, I=%.0f A, phases=%d", power, current, nphases); +} + +void setGetConfiguration(const char *payload, size_t len) +{ + ESP_LOGI(TAG, "GetConfiguration: %.*s (%u)", (int)len, payload, (unsigned)len); +} + +void setStartTransaction(const char *payload, size_t len) +{ + ESP_LOGI(TAG, "StartTransaction: %.*s (%u)", (int)len, payload, (unsigned)len); +} + +void setChangeConfiguration(const char *payload, size_t len) +{ + ESP_LOGI(TAG, "ChangeConfiguration: %.*s (%u)", (int)len, payload, (unsigned)len); +} + +void OnResetExecute(bool state) +{ + ESP_LOGI(TAG, "OnResetExecute (state=%d)", state); + esp_restart(); +} + +bool setOccupiedInput(void) +{ + ESP_LOGD(TAG, "setOccupiedInput"); + return false; +} + +bool setStartTxReadyInput(void) +{ + ESP_LOGD(TAG, "setStartTxReadyInput"); + return true; +} + +bool setStopTxReadyInput(void) +{ + ESP_LOGD(TAG, "setStopTxReadyInput"); + return true; +} + +bool setOnResetNotify(bool value) +{ + ESP_LOGI(TAG, "setOnResetNotify %d", value); + return true; +} + +void notificationOutput(OCPP_Transaction *transaction, enum OCPP_TxNotification txNotification) +{ + ESP_LOGI(TAG, "TxNotification: %d", txNotification); + + switch (txNotification) + { + case Authorized: + ESP_LOGI(TAG, "Authorized"); + // TODO: send event ocpp Authorized + // evse_authorize(); + + // Opcional: enviar idTag no payload (se tiveres como obter do transaction) + // ocpp_idtag_event_t ev = {0}; + // strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, &ev, sizeof(ev), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, NULL, 0, portMAX_DELAY); + break; + + case AuthorizationRejected: + ESP_LOGI(TAG, "AuthorizationRejected"); + // TODO: send event ocpp AuthorizationRejected + + // ocpp_idtag_event_t ev = {0}; + // strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, &ev, sizeof(ev), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY); + break; + + case AuthorizationTimeout: + ESP_LOGI(TAG, "AuthorizationTimeout"); + // TODO: send event ocpp AuthorizationTimeout + esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_TIMEOUT, NULL, 0, portMAX_DELAY); + break; + + case ReservationConflict: + ESP_LOGI(TAG, "ReservationConflict"); + // TODO: send event ocpp ReservationConflict + // (Se quiseres, cria um ID específico no enum e publica aqui) + break; + + case ConnectionTimeout: + ESP_LOGI(TAG, "ConnectionTimeout"); + // TODO: send event ocpp ConnectionTimeout + // (Se quiseres, cria um ID específico no enum e publica aqui) + break; + + case DeAuthorized: + ESP_LOGI(TAG, "DeAuthorized"); + // TODO: send event ocpp DeAuthorized + // TODO: adapt to the new interface + // evse_set_authorized(false); + // evse_set_limit_reached(2); + + // Poderias mapear para AUTH_REJECTED ou STOP_TX por política da aplicação: + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY); + break; + + case RemoteStart: + ESP_LOGI(TAG, "RemoteStart"); + // TODO: send event ocpp RemoteStart + + // ocpp_idtag_event_t ev = {0}; + // strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, &ev, sizeof(ev), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, NULL, 0, portMAX_DELAY); + break; + + case RemoteStop: + ESP_LOGI(TAG, "RemoteStop"); + // TODO: send event ocpp RemoteStop + esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_STOP, NULL, 0, portMAX_DELAY); + break; + + case StartTx: + ESP_LOGI(TAG, "StartTx"); + // TODO: send event ocpp StartTx + + // ocpp_tx_event_t tx = { .tx_id = ocpp_tx_get_id(transaction) }; + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, &tx, sizeof(tx), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, NULL, 0, portMAX_DELAY); + break; + + case StopTx: + ESP_LOGI(TAG, "StopTx"); + // TODO: send event ocpp StopTx + // TODO: adapt to the new interface + // evse_set_authorized(false); + // evse_set_limit_reached(2); + + // ocpp_reason_event_t rs = {0}; + // strlcpy(rs.reason, "Local", sizeof(rs.reason)); + // esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, &rs, sizeof(rs), portMAX_DELAY); + + esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, NULL, 0, portMAX_DELAY); + break; } } -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; +// Estado de conexão simples do OCPP +bool ocpp_is_connected(void) +{ + // Se quiser algo mais preciso (WS aberto), substitui por uma chamada da MicroOcpp, + // mas como fallback isto já evita o undefined symbol. + return g_ocpp_conn != NULL; } -// === Fim de: components/loadbalancer/src/input_filter.c === +const char *addErrorCodeInput(void) +{ + const char *ptr = NULL; + uint32_t error = evse_get_error(); + + if (error & EVSE_ERR_PILOT_FAULT_BIT) + ptr = "InternalError"; + else if (error & EVSE_ERR_DIODE_SHORT_BIT) + ptr = "InternalError"; + else if (error & EVSE_ERR_LOCK_FAULT_BIT) + ptr = "ConnectorLockFailure"; + else if (error & EVSE_ERR_UNLOCK_FAULT_BIT) + ptr = "ConnectorLockFailure"; + else if (error & EVSE_ERR_RCM_TRIGGERED_BIT) + ptr = "OtherError"; + else if (error & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) + ptr = "OtherError"; + else if (error & EVSE_ERR_TEMPERATURE_HIGH_BIT) + ptr = "HighTemperature"; + else if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT) + ptr = "OtherError"; + + return ptr; // NULL => sem erro +} + +/* ========================= + * Start / Stop OCPP + * ========================= */ +void ocpp_start(void) +{ + ESP_LOGI(TAG, "Starting OCPP"); + + if (ocpp_task != NULL) + { + ESP_LOGW(TAG, "OCPP already running"); + return; + } + + enabled = ocpp_get_enabled(); + if (!enabled) + { + ESP_LOGW(TAG, "OCPP disabled"); + return; + } + + char serverstr[64] = {0}; + ocpp_get_server(serverstr); + if (serverstr[0] == '\0') + { + ESP_LOGW(TAG, "No OCPP server configured. Skipping connection."); + return; + } + + char charge_id[64] = {0}; + ocpp_get_charge_id(charge_id); + if (charge_id[0] == '\0') + { + ESP_LOGW(TAG, "No OCPP charge_id configured. Skipping connection."); + return; + } + + /* Inicializar Mongoose + MicroOcpp */ + mg_mgr_init(&mgr); + mg_log_set(MG_LL_ERROR); + + struct OCPP_FilesystemOpt fsopt = {.use = true, .mount = true, .formatFsOnFail = true}; + + g_ocpp_conn = ocpp_makeConnection(&mgr, + serverstr, /* ex: ws://host:port/OCPP16/... */ + charge_id, /* ChargeBoxId / identity */ + "", + "", + fsopt); + if (!g_ocpp_conn) + { + ESP_LOGE(TAG, "ocpp_makeConnection failed"); + mg_mgr_free(&mgr); + return; + } + + ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false); + + /* Inputs/outputs e callbacks */ + ocpp_setEvReadyInput(&setEvReadyInput); + ocpp_setEvseReadyInput(&setEvseReadyInput); + ocpp_setConnectorPluggedInput(&setConnectorPluggedInput); + ocpp_setOnResetExecute(&OnResetExecute); + ocpp_setTxNotificationOutput(¬ificationOutput); + // ocpp_setStartTxReadyInput(&setStartTxReadyInput); + ocpp_setStopTxReadyInput(&setStopTxReadyInput); + ocpp_setOnResetNotify(&setOnResetNotify); + + ocpp_setEnergyMeterInput(&setEnergyInput); // inteiro Wh + + /* Metering */ + ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL); + ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Offered", "A", NULL, NULL); + ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL); + ocpp_addMeterValueInputFloat(&setTemperatureInput, "Temperature", "Celsius", NULL, NULL); + ocpp_addMeterValueInputFloat(&setPowerMeterInput, "Power.Active.Import", "W", NULL, NULL); + ocpp_addMeterValueInputFloat(&setEnergyMeterInput, "Energy.Active.Import.Register", "Wh", NULL, NULL); + + ocpp_addErrorCodeInput(&addErrorCodeInput); + + /* Task */ + xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 5, &ocpp_task); + + if (!s_auth_verify_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, + &ocpp_on_auth_verify, NULL, &s_auth_verify_inst)); + ESP_LOGI(TAG, "Registered AUTH_EVENT_TAG_VERIFY listener"); + } + + // ouvir mudanças de estado do EVSE + if (!s_evse_state_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, + &evse_event_handler, NULL, &s_evse_state_inst)); + } + + // ouvir mudanças de ENABLE / AVAILABLE do EVSE (Local → OCPP) + if (!s_evse_enable_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, + &evse_enable_available_handler, NULL, &s_evse_enable_inst)); + } + if (!s_evse_available_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, + &evse_enable_available_handler, NULL, &s_evse_available_inst)); + } + + if (!s_meter_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_register( + METER_EVENT, METER_EVENT_DATA_READY, + &on_meter_event, NULL, &s_meter_inst)); + ESP_LOGI(TAG, "Registered METER_EVENT_DATA_READY listener (EVSE source)"); + } +} + +void ocpp_stop(void) +{ + ESP_LOGI(TAG, "Stopping OCPP"); + + if (ocpp_task) + { + vTaskDelete(ocpp_task); + ocpp_task = NULL; + } + + ocpp_deinitialize(); + + if (g_ocpp_conn) + { + ocpp_deinitConnection(g_ocpp_conn); + g_ocpp_conn = NULL; + } + + mg_mgr_free(&mgr); + + if (s_auth_verify_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, s_auth_verify_inst)); + s_auth_verify_inst = NULL; + } + + if (s_evse_state_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, s_evse_state_inst)); + s_evse_state_inst = NULL; + } + + if (s_evse_enable_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, s_evse_enable_inst)); + s_evse_enable_inst = NULL; + } + if (s_evse_available_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, s_evse_available_inst)); + s_evse_available_inst = NULL; + } + if (s_meter_inst) + { + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + METER_EVENT, METER_EVENT_DATA_READY, s_meter_inst)); + s_meter_inst = NULL; + } +} + +// === Fim de: components/ocpp/src/ocpp.c === -// === Início de: components/loadbalancer/include/loadbalancer_events.h === +// === Início de: components/ocpp/include/ocpp_events.h === #pragma once #include "esp_event.h" -#include #include -#include "esp_timer.h" +#include -ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); +#ifdef __cplusplus +extern "C" { +#endif +/* Base de eventos do OCPP (igual ao padrão usado em auth_events.h) */ +ESP_EVENT_DECLARE_BASE(OCPP_EVENTS); + +/* IDs de eventos do OCPP */ typedef enum { - 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; + OCPP_EVENT_CONNECTED = 0, // payload: const char* (server URL) – opcional + OCPP_EVENT_DISCONNECTED, // payload: NULL + OCPP_EVENT_AUTHORIZED, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_AUTH_REJECTED, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_AUTH_TIMEOUT, // payload: NULL + OCPP_EVENT_REMOTE_START, // payload: ocpp_idtag_event_t (opcional) + OCPP_EVENT_REMOTE_STOP, // payload: NULL + OCPP_EVENT_START_TX, // payload: ocpp_tx_event_t (opcional) + OCPP_EVENT_STOP_TX, // payload: ocpp_reason_event_t (opcional) + OCPP_EVENT_RESET, // payload: NULL + OCPP_EVENT_OPERATIVE_UPDATED +} ocpp_event_id_t; + +/* Limites de strings simples (evita dependência de auth.h) */ +#define OCPP_IDTAG_MAX 32 +#define OCPP_REASON_MAX 32 + +/* Payloads opcionais */ +typedef struct { + char idTag[OCPP_IDTAG_MAX]; +} ocpp_idtag_event_t; typedef struct { - bool enabled; - int64_t timestamp_us; -} loadbalancer_state_event_t; - -// (opcional) -typedef struct { - float limit; - int64_t timestamp_us; -} loadbalancer_global_limit_event_t; + int tx_id; // se disponível +} ocpp_tx_event_t; typedef struct { - uint8_t slave_id; - uint16_t max_current; - int64_t timestamp_us; -} loadbalancer_master_limit_event_t; + char reason[OCPP_REASON_MAX]; +} ocpp_reason_event_t; +// Payload do novo evento 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_ + bool operative; // true = Operative, false = Inoperative + int64_t timestamp_us; // esp_timer_get_time() +} ocpp_operative_event_t; #ifdef __cplusplus -extern "C" { +} #endif -#include +// === Fim de: components/ocpp/include/ocpp_events.h === + + +// === Início de: components/ocpp/include/ocpp.h === +#ifndef OCPP_H_ +#define OCPP_H_ + #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 /* LOADBALANCER_H_ */ - -// === Fim de: components/loadbalancer/include/loadbalancer.h === - - -// === Início de: components/loadbalancer/include/input_filter.h === -#pragma once +#include #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 Start OCPP + */ +void ocpp_start(void); /** - * @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) + * @brief Stop OCPP */ -void input_filter_init(input_filter_t *filter, float alpha); +void ocpp_stop(void); -/** - * @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); +/* Config getters / setters */ +bool ocpp_get_enabled(void); +void ocpp_set_enabled(bool value); + +void ocpp_get_server(char *value); // buffer >= 64 +void ocpp_set_server(char *value); + +void ocpp_get_charge_id(char *value); // buffer >= 64 +void ocpp_set_charge_id(char *value); + +/* Estado de conexão */ +bool ocpp_is_connected(void); #ifdef __cplusplus } #endif -// === Fim de: components/loadbalancer/include/input_filter.h === +#endif /* OCPP_H_ */ + +// === Fim de: components/ocpp/include/ocpp.h === diff --git a/readproject.py b/readproject.py index 3d2a0bc..de4b371 100644 --- a/readproject.py +++ b/readproject.py @@ -1,6 +1,6 @@ import os -TAMANHO_MAX = 200000 # Limite por arquivo +TAMANHO_MAX = 150000 # 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 = [ - "evse", "loadbalancer" + "ocpp" ] diretorios_componentes = [os.path.join("components", nome) for nome in componentes_escolhidos]