diff --git a/README.md b/README.md index 87e700e..86adb0a 100755 --- a/README.md +++ b/README.md @@ -1,85 +1,180 @@ -![ESP32 EVSE](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/logo-full.svg) +# ChargeFlow EVSE Firmware (ESP32, ESP-IDF 5.x) -J1772 EVSE firmware for ESP32 based devices. +Firmware for an AC EVSE (EV charger) based on ESP32 and ESP-IDF 5.x, with: -![Build with ESP-IDF](https://github.com/dzurikmiroslav/esp32-evse/workflows/Build%20with%20ESP-IDF/badge.svg) -[![License](https://img.shields.io/github/license/dzurikmiroslav/esp32-evse.svg)](LICENSE.md) +- IEC-style EVSE state machine (Control Pilot A/B/C/D) +- Wi-Fi (STA + AP for local configuration) +- REST API served from SPIFFS +- Local authentication and OCPP integration +- Load balancing (master + slaves) +- Scheduler (time windows) +- Audible feedback (buzzer) and RGB LED status +- On-device ring-buffer logger -## Key features - - Hardware abstraction for device design - - Responsive web-interface -- OTA update -- Integrated energy meter -- Energy detection for relay control -- [REST](https://github.com/dzurikmiroslav/esp32-evse/wiki/Rest) API - - MQTT API - - [Modbus](https://github.com/dzurikmiroslav/esp32-evse/wiki/Modbus) (RS485, TCP) - - [Scripting](https://github.com/dzurikmiroslav/esp32-evse/wiki/Script) - - [Nextion HMI](https://github.com/dzurikmiroslav/esp32-evse/wiki/Nextion) +--- -### Device definition method +## Features -_One firmware to rule them all._ Not really :-) one per device platform (ESP32, ESP32-S2...). +### Core EVSE -There is no need to compile the firmware for your EVSE design. -Source code ist not hardcoded to GPIOs or other hardware design features. -All code is written in ESP-IDF without additional mapping layer like Arduino. +- EVSE manager (`evse_manager`) coordinating: + - Hardware layer (`evse_hardware`) + - State machine (`evse_state`) + - Error handling (`evse_error`) + - Energy metering (`evse_meter` / `meter_manager`) + - Session tracking (`evse_session`) +- Runs a periodic tick (`evse_manager_tick()`) in its own FreeRTOS task. +- Supports multiple auth modes (OPEN / RFID / OCPP), with scheduling and load-balancer aware logic. -All configuration is written outside firmware in configuration file named _board.cfg_ on dedicated partition. -For example, on following scheme is minimal EVSE circuit with ESP32 devkit. +### Networking & REST -![Minimal circuit](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/minimal-circuit.png) +- Wi-Fi: + - Station mode for normal operation. + - Access point mode for local configuration, enabled by a physical button. +- REST server (`rest_main`) serving from `/data` SPIFFS mount: + - For configuration, status, logs, etc. (exact endpoints depend on your REST implementation). -For this circuit there is _board.cfg_, for more information's see [Wiki](https://github.com/dzurikmiroslav/esp32-evse/wiki/Board-config). +### Button & User Input +- One physical button (configured via `board_config`): + - **Short press** → Starts Wi-Fi AP mode for configuration. + - **Long press (~30s)** → Erases NVS and reboots (factory-like reset). +- Robust handling: + - ISR with software debounce and spinlock. + - Dedicated `user_input_task` that receives button press/release notifications via `xTaskNotify`. -```bash -#Device name -DEVICE_NAME=ESP32 minimal EVSE -#Button -BUTTON_WIFI_GPIO=0 -#Pilot -PILOT_PWM_GPIO=33 -PILOT_ADC_CHANNEL=7 -PILOT_DOWN_THRESHOLD_12=2410 -PILOT_DOWN_THRESHOLD_9=2104 -PILOT_DOWN_THRESHOLD_6=1797 -PILOT_DOWN_THRESHOLD_3=1491 -PILOT_DOWN_THRESHOLD_N12=265 -#AC relay -AC_RELAY_GPIO=32 -``` +### Storage -### Web interface +- SPIFFS used for: + - `/cfg` partition: persistent configuration. + - `/data` partition: web assets, runtime data, logs, etc. +- Two separate mounts: + - `cfg_conf` → `/cfg` (label: `cfg`) + - `data_conf` → `/data` (label: `data`) -Fully responsive web interface is accessible local network IP address on port 80. +### LED Subsystem -Dashboard page +- RGB LED driven by LEDC: + - `ledc_driver` abstracts LEDC timer + channels. + - `led` module maps EVSE state & sessions to colors/patterns. +- LED patterns per EVSE state: + - **IDLE** → Green solid. + - **WAITING** (vehicle plugged, not charging) → Blue slow blink. + - **CHARGING** → Blue “breathing” effect. + - **FAULT** → Red fast blink. +- Session effects: + - Distinct visual patterns when a session starts/finishes. + - Uses a one-shot timer and a dedicated effect state machine. -![Dashboard](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-dashboard.png) +### Buzzer -Settings page +- Buzzer with multiple patterns (`buzzer` + `buzzer_events`): + - Plugged/unplugged, card read/denied, AP start, charging, fault, etc. +- Supported modes: + - Active buzzer (ON/OFF). + - Passive buzzer with LEDC PWM (frequency & duty configurable). +- Features: + - Central queue + dedicated `buzzer_task`. + - Quiet hours support (optionally suppress non-critical sounds at night). + - Anti-spam mechanism to avoid excessively frequent beeps. + - Integrated with: + - EVSE events (state changes & faults) + - Auth events (RFID card success/denied/added) + - Network events (AP/STA up) -![Settings](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-settings.png) +### Load Balancer -Mobile dashboard page +- `loadbalancer` component: + - Monitors GRID meter and EVSE meter via `meter_events`. + - Supports one master + up to 255 slaves (connectors array). + - Fair distribution of current with: + - Headroom calculation based on grid limit and measured current. + - Min current guarantees (e.g. 6 A) using a “water-filling” algorithm. + - Session-age based priority (oldest sessions first). + - Per-connector hysteresis and LB suspension/resume flags. + - Publishes limits via `LOADBALANCER_EVENTS`: + - `LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT` + - `LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT` +- Fail-safe behavior: + - If GRID meter data times out, clamps connectors to minimum safe current instead of ramping up. -![Dashboard mobile](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-dashboard-mobile.png) +### Scheduler -## Hardware +- Scheduler component (`scheduler`) emits `SCHED_EVENTS` with `allowed_now` flag: + - EVSE manager revokes authorization when the window closes. + - In OPEN mode, automatic re-authorization only happens when scheduler allows. -### ESP32DevkitC +### OCPP -Dev board with basic functionality, single phase energy meter, RS485. One side pcb, for DIY makers easy to make at home conditions ;-) +- `ocpp` module integration: + - Listens to OCPP events (`OCPP_EVENTS`). + - Handles: + - RemoteStart/Stop + - Authorization results + - ChangeAvailability (operative/inoperative) → mapped into local `enabled` config. + - EVSE manager mediates OCPP decisions with scheduler + load balancer. -[EasyEDA project](https://oshwlab.com/dzurik.miroslav/esp32-devkit-evse) +### Logger -![ESP32DevkitC](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/esp32devkitc.jpg) +- `logger` + `output_buffer` components: + - Central log sink with ring buffer in RAM. + - Thread-safe via FreeRTOS mutex. + - Integrated with ESP log system via `esp_log_set_vprintf(logger_vprintf);` + - Optionally mirrors to UART (controlled via `CONFIG_ESP_CONSOLE_UART`). + - Simple reader API: + - Iterate entries using an index. + - Handy for exposing logs over REST/Web UI. -### ESP32-S2 DIY ALPHA +--- -ESP32-S2 based EVSE with advanced functionality, three phase energy meter, RS485, UART, 1WIRE, RCM, socket lock. +## Project Structure (Relevant Parts) -[EasyEDA project](https://oshwlab.com/dzurik.miroslav/esp32s2-diy-evse) +Approximate layout (names may vary slightly in your repo): -![ESP32-S2-DA](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/esp32s2da.jpg) +```text +main/ + main.c # System entrypoint, button setup, module init + +components/ + evse/ + evse_manager.c/.h # High-level EVSE orchestration + evse_state.c/.h # State machine & events + evse_error.c/.h # Error handling + evse_hardware.c/.h # Hardware abstraction + evse_session.c/.h # Session metrics + + loadbalancer/ + src/ + loadbalancer.c + loadbalancer_events.c + input_filter.c + include/ + loadbalancer.h + loadbalancer_events.h + input_filter.h + + buzzer/ + src/ + buzzer.c + buzzer_events.c + include/ + buzzer.h + buzzer_events.h + + led/ + src/ + led.c + ledc_driver.c + include/ + led.h + ledc_driver.h + + logger/ + src/ + logger.c + output_buffer.c + include/ + logger.h + output_buffer.h + + # ... other modules: auth, ocpp, scheduler, meter_manager, evse_link, etc. diff --git a/components/auth/CMakeLists.txt b/components/auth/CMakeLists.txt index 1c68e6d..2c164e8 100755 --- a/components/auth/CMakeLists.txt +++ b/components/auth/CMakeLists.txt @@ -3,5 +3,5 @@ set(srcs "src/auth_types.c" "src/auth.c" "src/wiegand.c" "src/wiegand_reader.c" idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" PRIV_INCLUDE_DIRS "src" - PRIV_REQUIRES nvs_flash driver esp_timer - REQUIRES esp_event evse ocpp evse_link) \ No newline at end of file + PRIV_REQUIRES driver esp_timer + REQUIRES esp_event evse ocpp evse_link storage_service) \ No newline at end of file diff --git a/components/auth/src/auth.c b/components/auth/src/auth.c index 05fd50d..e954fc9 100755 --- a/components/auth/src/auth.c +++ b/components/auth/src/auth.c @@ -4,19 +4,22 @@ #include #include +#include #include +#include #include -#include // strcasecmp +#include +#include + #include "wiegand_reader.h" -#include "nvs_flash.h" -#include "nvs.h" + +#include "storage_service.h" #include "evse_link.h" #include "evse_link_events.h" #define MAX_TAGS 50 - static const char *TAG = "Auth"; /* ===== Estado ===== */ @@ -25,162 +28,16 @@ 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; -static bool s_wiegand_started = false; // controla se o Wiegand já foi iniciado +static bool s_wiegand_started = false; -/* ===== NVS keys ===== */ +/* ===== Storage keys ===== */ #define NVS_NAMESPACE "auth" #define NVS_TAG_PREFIX "tag_" #define NVS_TAG_COUNT_KEY "count" #define NVS_MODE_KEY "mode" // uint8_t -/* ========================= - * NVS Persistence (tags) - * ========================= */ -static void load_tags_from_nvs(void) -{ - nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "No stored tags in NVS (nvs_open: %s)", esp_err_to_name(err)); - return; - } - - uint8_t count = 0; - err = nvs_get_u8(handle, NVS_TAG_COUNT_KEY, &count); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "No tag count key in NVS (nvs_get_u8: %s)", esp_err_to_name(err)); - nvs_close(handle); - return; - } - - tag_count = 0; - for (int i = 0; i < count && i < MAX_TAGS; i++) - { - char key[16]; - char tag_buf[AUTH_TAG_MAX_LEN]; - size_t len = sizeof(tag_buf); - - snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i); - err = nvs_get_str(handle, key, tag_buf, &len); - if (err == ESP_OK) - { - strncpy(valid_tags[tag_count], tag_buf, AUTH_TAG_MAX_LEN - 1); - valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0'; - tag_count++; - } - else - { - ESP_LOGW(TAG, "Failed to load tag %d from NVS (%s)", i, esp_err_to_name(err)); - } - } - - nvs_close(handle); - ESP_LOGI(TAG, "Loaded %d tags from NVS", tag_count); -} - -static void save_tags_to_nvs(void) -{ - nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open NVS to save tags: %s", esp_err_to_name(err)); - return; - } - - err = nvs_set_u8(handle, NVS_TAG_COUNT_KEY, tag_count); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_u8(count) failed: %s", esp_err_to_name(err)); - nvs_close(handle); - return; - } - - for (int i = 0; i < tag_count; i++) - { - char key[16]; - snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i); - err = nvs_set_str(handle, key, valid_tags[i]); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_str(%s) failed: %s", key, esp_err_to_name(err)); - nvs_close(handle); - return; - } - } - - err = nvs_commit(handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_commit failed when saving tags: %s", esp_err_to_name(err)); - nvs_close(handle); - return; - } - - nvs_close(handle); - ESP_LOGI(TAG, "Tags saved to NVS (%d tags)", tag_count); -} - -/* ========================= - * NVS Persistence (mode) - * ========================= */ -static void load_mode_from_nvs(void) -{ - nvs_handle_t h; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); - if (err == ESP_OK) - { - uint8_t u = (uint8_t)AUTH_MODE_OPEN; - err = nvs_get_u8(h, NVS_MODE_KEY, &u); - if (err == ESP_OK) - { - if (u <= (uint8_t)AUTH_MODE_OCPP_RFID) - s_mode = (auth_mode_t)u; - } - else - { - ESP_LOGW(TAG, "No stored auth mode in NVS (nvs_get_u8: %s). Default OPEN", esp_err_to_name(err)); - } - nvs_close(h); - } - else - { - ESP_LOGW(TAG, "No stored auth mode in NVS (nvs_open: %s). Default OPEN", esp_err_to_name(err)); - } - 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; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open NVS to save auth mode: %s", esp_err_to_name(err)); - return; - } - - err = nvs_set_u8(h, NVS_MODE_KEY, (uint8_t)mode); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_u8(mode) failed: %s", esp_err_to_name(err)); - nvs_close(h); - return; - } - - err = nvs_commit(h); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_commit failed when saving mode: %s", esp_err_to_name(err)); - nvs_close(h); - return; - } - - nvs_close(h); - ESP_LOGI(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode)); -} +// timeout para operações sync do storage +#define STORAGE_TO pdMS_TO_TICKS(2000) /* ========================= * Helpers @@ -190,37 +47,155 @@ static bool is_tag_valid(const char *tag) for (int i = 0; i < tag_count; i++) { if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0) - { return true; - } } return false; } +/* ========================= + * Storage Persistence (tags) + * ========================= */ +static void load_tags_from_storage(void) +{ + uint8_t count = 0; + esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, &count, STORAGE_TO); + if (err == ESP_ERR_NOT_FOUND) + { + ESP_LOGD(TAG, "No stored tags (count not found)"); + tag_count = 0; + return; + } + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Failed to read tag count (%s)", esp_err_to_name(err)); + tag_count = 0; + return; + } + + tag_count = 0; + for (int i = 0; i < (int)count && i < MAX_TAGS; i++) + { + char key[16]; + char tag_buf[AUTH_TAG_MAX_LEN] = {0}; + + snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i); + + err = storage_get_str_sync(NVS_NAMESPACE, key, tag_buf, sizeof(tag_buf), STORAGE_TO); + if (err == ESP_OK) + { + if (tag_buf[0] != '\0') + { + strncpy(valid_tags[tag_count], tag_buf, AUTH_TAG_MAX_LEN - 1); + valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0'; + tag_count++; + } + } + else if (err == ESP_ERR_NOT_FOUND) + { + // pode acontecer se count estiver desfasado; ignora + continue; + } + else + { + ESP_LOGW(TAG, "Failed to load tag %d (%s)", i, esp_err_to_name(err)); + } + } + + ESP_LOGI(TAG, "Loaded %d tags from storage", tag_count); +} + +static void save_tags_to_storage(void) +{ + // ler count antigo (para apagar keys antigas se removemos tags) + uint8_t old_count = 0; + esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, &old_count, STORAGE_TO); + if (err == ESP_ERR_NOT_FOUND) + old_count = 0; + + // grava count + tags + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, (uint8_t)tag_count); + + for (int i = 0; i < tag_count; i++) + { + char key[16]; + snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i); + (void)storage_set_str_async(NVS_NAMESPACE, key, valid_tags[i]); + } + + // se removemos tags: apagar chaves antigas + if (old_count > (uint8_t)tag_count) + { + for (int i = tag_count; i < (int)old_count && i < MAX_TAGS; i++) + { + char key[16]; + snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i); + (void)storage_erase_key_async(NVS_NAMESPACE, key); + } + } + + // opcional: forçar commit “já” + (void)storage_flush_async(); + + ESP_LOGD(TAG, "Tags saved to storage (%d tags)", tag_count); +} + +/* ========================= + * Storage Persistence (mode) + * ========================= */ +static void load_mode_from_storage(void) +{ + uint8_t u = (uint8_t)AUTH_MODE_OPEN; + esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_MODE_KEY, &u, STORAGE_TO); + + if (err == ESP_OK) + { + if (u <= (uint8_t)AUTH_MODE_OCPP_RFID) + s_mode = (auth_mode_t)u; + else + s_mode = AUTH_MODE_OPEN; + } + else if (err == ESP_ERR_NOT_FOUND) + { + s_mode = AUTH_MODE_OPEN; + ESP_LOGD(TAG, "No stored mode -> default OPEN"); + } + else + { + s_mode = AUTH_MODE_OPEN; + ESP_LOGW(TAG, "Failed to read mode (%s) -> default OPEN", esp_err_to_name(err)); + } + + ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode)); +} + +static void save_mode_to_storage(auth_mode_t mode) +{ + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_MODE_KEY, (uint8_t)mode); + (void)storage_flush_async(); // opcional: commit mais rápido + ESP_LOGD(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode)); +} + /* ========================= * Bridge: EVSE-Link -> AUTH (remote AUTH_GRANTED no slave) * ========================= */ static void on_remote_auth_grant(void *arg, esp_event_base_t base, int32_t id, void *data) { + (void)arg; + if (base != EVSE_LINK_EVENTS || id != LINK_EVENT_REMOTE_AUTH_GRANTED || data == NULL) - { return; - } const evse_link_auth_grant_event_t *src = (const evse_link_auth_grant_event_t *)data; - // Só faz sentido em SLAVE; em MASTER este evento não deve aparecer if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE) - { return; - } auth_tag_event_data_t ev = {0}; strncpy(ev.tag, src->tag, AUTH_TAG_MAX_LEN - 1); ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0'; ev.authorized = true; - ESP_LOGI(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag); + ESP_LOGD(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag); esp_err_t err = esp_event_post( AUTH_EVENTS, @@ -241,8 +216,11 @@ static void on_remote_auth_grant(void *arg, esp_event_base_t base, int32_t id, v * ========================= */ void auth_init(void) { - load_mode_from_nvs(); - load_tags_from_nvs(); + // garantir que o storage service está pronto + ESP_ERROR_CHECK(storage_service_init()); + + load_mode_from_storage(); + load_tags_from_storage(); bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID); if (need_wiegand) @@ -256,7 +234,7 @@ void auth_init(void) ESP_LOGI(TAG, "Mode OPEN: Wiegand not started"); } - // Registar bridge para autorizações remotas vindas do EVSE-Link + // bridge EVSE-Link -> AUTH { esp_err_t err = esp_event_handler_register( EVSE_LINK_EVENTS, @@ -285,44 +263,34 @@ void auth_set_mode(auth_mode_t mode) if (mode == s_mode) { - ESP_LOGI(TAG, "Mode unchanged: %s", auth_mode_to_str(mode)); + ESP_LOGD(TAG, "Mode unchanged: %s", auth_mode_to_str(mode)); return; } auth_mode_t old = s_mode; s_mode = mode; - save_mode_to_nvs(mode); + save_mode_to_storage(mode); bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID); if (need_wiegand && !s_wiegand_started) { - ESP_LOGI(TAG, "Mode changed %s -> %s, starting Wiegand", + ESP_LOGD(TAG, "Mode changed %s -> %s, starting Wiegand", auth_mode_to_str(old), auth_mode_to_str(s_mode)); initWiegand(); s_wiegand_started = true; } else if (!need_wiegand && s_wiegand_started) { - // Aqui poderias implementar um wiegand_deinit() se o driver o expuser. - ESP_LOGI(TAG, "Mode changed %s -> %s, Wiegand remains started (no deinit implemented)", + ESP_LOGD(TAG, "Mode changed %s -> %s, Wiegand remains started (no deinit implemented)", auth_mode_to_str(old), auth_mode_to_str(s_mode)); } else { - ESP_LOGI(TAG, "Mode changed %s -> %s, no change in Wiegand state", + ESP_LOGD(TAG, "Mode changed %s -> %s, no change in Wiegand state", auth_mode_to_str(old), auth_mode_to_str(s_mode)); } - if (s_mode == AUTH_MODE_OPEN) - { - ESP_LOGI(TAG, "Mode set to OPEN"); - } - else - { - ESP_LOGI(TAG, "Mode set to %s", 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); } @@ -338,15 +306,16 @@ bool auth_add_tag(const char *tag) return false; if (tag_count >= MAX_TAGS) return false; + if (is_tag_valid(tag)) - return true; // já existe + return true; strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1); valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0'; tag_count++; - save_tags_to_nvs(); - ESP_LOGI(TAG, "Tag added: %s", tag); + save_tags_to_storage(); + ESP_LOGD(TAG, "Tag added: %s", tag); return true; } @@ -366,8 +335,8 @@ bool auth_remove_tag(const char *tag) } tag_count--; - save_tags_to_nvs(); - ESP_LOGI(TAG, "Tag removed: %s", tag); + save_tags_to_storage(); + ESP_LOGD(TAG, "Tag removed: %s", tag); return true; } } @@ -383,11 +352,9 @@ bool auth_tag_exists(const char *tag) void auth_list_tags(void) { - ESP_LOGI(TAG, "Registered Tags (%d):", tag_count); + ESP_LOGD(TAG, "Registered Tags (%d):", tag_count); for (int i = 0; i < tag_count; i++) - { - ESP_LOGI(TAG, "- %s", valid_tags[i]); - } + ESP_LOGD(TAG, "- %s", valid_tags[i]); } void auth_wait_for_tag_registration(void) @@ -398,7 +365,7 @@ void auth_wait_for_tag_registration(void) return; } waiting_for_registration = true; - ESP_LOGI(TAG, "Tag registration mode enabled."); + ESP_LOGD(TAG, "Tag registration mode enabled."); } void auth_process_tag(const char *tag) @@ -412,11 +379,8 @@ void auth_process_tag(const char *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); + ESP_LOGD(TAG, "Mode OPEN: tag=%s (no verification)", tag); break; - } case AUTH_MODE_LOCAL_RFID: { @@ -430,9 +394,10 @@ void auth_process_tag(const char *tag) strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1); ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0'; ev.authorized = true; + esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, &ev, sizeof(ev), portMAX_DELAY); - ESP_LOGI(TAG, "Tag registered: %s", tag); + ESP_LOGD(TAG, "Tag registered: %s", tag); } else { @@ -446,8 +411,9 @@ void auth_process_tag(const char *tag) ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0'; ev.authorized = is_tag_valid(tag); - ESP_LOGI(TAG, "LOCAL tag %s: %s", tag, + ESP_LOGD(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; @@ -455,13 +421,14 @@ void auth_process_tag(const char *tag) 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.tag[AUTH_TAG_MAX_LEN - 1] = '\0'; rq.req_id = s_next_req_id++; - ESP_LOGI(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)", + + ESP_LOGD(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; diff --git a/components/auth/src/wiegand_reader.c b/components/auth/src/wiegand_reader.c index 66e1362..8387fd2 100755 --- a/components/auth/src/wiegand_reader.c +++ b/components/auth/src/wiegand_reader.c @@ -301,7 +301,7 @@ void initWiegand(void) xTaskCreate(wiegand_task, TAG, configMINIMAL_STACK_SIZE * 4, NULL, 4, NULL); // Para testes, podes ativar o simulador: - //ESP_LOGI(TAG, "Inicializando Wiegand simulado"); - //xTaskCreate(wiegand_sim_task, "WiegandSim", + // ESP_LOGI(TAG, "Inicializando Wiegand simulado"); + // xTaskCreate(wiegand_sim_task, "WiegandSim", // configMINIMAL_STACK_SIZE * 3, NULL, 3, NULL); } diff --git a/components/buzzer/CMakeLists.txt b/components/buzzer/CMakeLists.txt index 021b5f9..7ed46ad 100755 --- a/components/buzzer/CMakeLists.txt +++ b/components/buzzer/CMakeLists.txt @@ -8,5 +8,5 @@ idf_component_register( INCLUDE_DIRS "include" PRIV_INCLUDE_DIRS "src" REQUIRES esp_event - PRIV_REQUIRES driver nvs_flash esp_timer evse + PRIV_REQUIRES driver esp_timer evse network ) diff --git a/components/buzzer/src/buzzer.c b/components/buzzer/src/buzzer.c index 300808d..0b04098 100755 --- a/components/buzzer/src/buzzer.c +++ b/components/buzzer/src/buzzer.c @@ -398,7 +398,7 @@ static void network_event_handler(void *handler_args, esp_event_base_t base, int if (base != NETWORK_EVENTS) return; - ESP_LOGI(TAG, "Network event id=%d", (int)id); + ESP_LOGD(TAG, "Network event id=%d", (int)id); buzzer_event_data_t evt = {0}; diff --git a/components/config/CMakeLists.txt b/components/config/CMakeLists.txt index f6d2900..0e2fb12 100755 --- a/components/config/CMakeLists.txt +++ b/components/config/CMakeLists.txt @@ -2,5 +2,5 @@ set(srcs "board_config.c") idf_component_register(SRCS "${srcs}" - PRIV_REQUIRES nvs_flash + PRIV_REQUIRES INCLUDE_DIRS "include") \ No newline at end of file diff --git a/components/evse/CMakeLists.txt b/components/evse/CMakeLists.txt index 831e236..1a4453b 100755 --- a/components/evse/CMakeLists.txt +++ b/components/evse/CMakeLists.txt @@ -17,6 +17,6 @@ set(srcs idf_component_register( SRCS ${srcs} INCLUDE_DIRS "include" - PRIV_REQUIRES nvs_flash driver - REQUIRES peripherals auth loadbalancer scheduler + PRIV_REQUIRES driver + REQUIRES peripherals auth loadbalancer scheduler storage_service ) \ No newline at end of file diff --git a/components/evse/evse_config.c b/components/evse/evse_config.c index 7b01aa7..438e0ec 100755 --- a/components/evse/evse_config.c +++ b/components/evse/evse_config.c @@ -1,215 +1,289 @@ -#include // For PRI macros +#include +#include + +#include "freertos/FreeRTOS.h" + #include "evse_config.h" #include "board_config.h" #include "evse_limits.h" #include "evse_api.h" +#include "evse_state.h" + #include "esp_log.h" -#include "nvs.h" -#include "esp_timer.h" +#include "esp_err.h" +#include "esp_check.h" + +#include "storage_service.h" static const char *TAG = "evse_config"; - -static nvs_handle_t nvs; +#define NVS_NAMESPACE "evse_config" // ======================== -// Configurable parameters +// 3 variáveis (semântica simples) // ======================== -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; + +// 1) Hardware (FIXO) +static const uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT; + +// 2) Configurável (persistido) +static uint16_t charging_current = MAX_CHARGING_CURRENT_LIMIT; + +// 3) Runtime (RAM) +static uint16_t charging_current_runtime = 0; + +// Outros parâmetros (persistidos) +static bool socket_outlet = false; +static bool rcm = false; static uint8_t temp_threshold = 60; -static bool require_auth; -// Availability / Enable flags +// Availability / Enable flags (persistidos) static bool is_available = true; static bool is_enabled = true; +static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); } + +// Ajusta conforme o teu boot: +// 1000ms pode ser curto com Wi-Fi/FS/tasks; 2000ms é mais robusto em produto. +static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(2000); } + // ======================== // Initialization // ======================== esp_err_t evse_config_init(void) { - ESP_LOGD(TAG, "Initializing NVS configuration..."); - return nvs_open("evse", NVS_READWRITE, &nvs); + // garante storage iniciado + ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed"); + ESP_LOGI(TAG, "EVSE config init OK (storage-backed)"); + return ESP_OK; } 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; + uint8_t u8 = 0; + uint16_t u16 = 0; - ESP_LOGD(TAG, "Checking default parameters..."); + // Timeouts: leitura e escrita no boot + const TickType_t rd_to = BOOT_TO(); + const TickType_t wr_to = TO_TICKS_MS(2000); - // 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; - } + ESP_LOGD(TAG, "Checking default parameters (sync persistence)..."); + // ----------------------------------------- // 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)) + // ----------------------------------------- + err = storage_get_u16_sync(NVS_NAMESPACE, "def_chrg_curr", &u16, rd_to); + 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); + + esp_err_t se = storage_set_u16_sync(NVS_NAMESPACE, "def_chrg_curr", charging_current, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist def_chrg_curr=%u: %s", + (unsigned)charging_current, esp_err_to_name(se)); + // seguimos com RAM correta; persist pode falhar por flash/partição + } + + ESP_LOGW(TAG, "Invalid/missing def_chrg_curr (%s) -> reset to %u (sync persisted)", + esp_err_to_name(err), (unsigned)charging_current); } else { charging_current = u16; } - // Runtime charging current inicializado a partir do default persistido + // runtime inicializa a partir do default charging_current_runtime = charging_current; - ESP_LOGD(TAG, "Runtime charging current initialized to: %d", charging_current_runtime); + ESP_LOGD(TAG, "Runtime charging current initialized from default: %u", + (unsigned)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) + // ----------------------------------------- + // Socket outlet (persisted) + capability gate + // ----------------------------------------- + err = storage_get_u8_sync(NVS_NAMESPACE, "socket_outlet", &u8, rd_to); + if (err == ESP_OK && u8 <= 1) { - nvs_set_u8(nvs, "require_auth", require_auth); - needs_commit = true; - } + bool wanted = (u8 != 0); - // 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); - - // 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)."); - } - - if (needs_commit) - { - err = nvs_commit(nvs); - if (err == ESP_OK) + if (wanted && !board_config.proximity) { - ESP_LOGD(TAG, "Configuration committed to NVS."); + // NVS dizia 1, mas HW não suporta -> runtime false e persistir 0 + socket_outlet = false; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "socket_outlet", 0, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist socket_outlet=0 (capability mismatch): %s", + esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "socket_outlet requested but HW has no proximity -> forcing false (sync persisted)"); } else { - ESP_LOGE(TAG, "Failed to commit configuration to NVS: %s", esp_err_to_name(err)); + socket_outlet = wanted; } } + else + { + socket_outlet = false; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "socket_outlet", 0, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist socket_outlet default=0: %s", esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "Missing/invalid socket_outlet (%s) -> default=false (sync persisted).", + esp_err_to_name(err)); + } + + // ----------------------------------------- + // RCM (persisted) + capability gate + // ----------------------------------------- + err = storage_get_u8_sync(NVS_NAMESPACE, "rcm", &u8, rd_to); + if (err == ESP_OK && u8 <= 1) + { + bool wanted = (u8 != 0); + + if (wanted && !board_config.rcm) + { + rcm = false; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist rcm=0 (capability mismatch): %s", + esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "rcm requested but HW has no RCM -> forcing false (sync persisted)"); + } + else + { + rcm = wanted; + } + } + else + { + rcm = false; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist rcm default=0: %s", esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "Missing/invalid rcm (%s) -> default=false (sync persisted).", + esp_err_to_name(err)); + } + + // ----------------------------------------- + // Temp threshold (persisted) + // ----------------------------------------- + err = storage_get_u8_sync(NVS_NAMESPACE, "temp_threshold", &u8, rd_to); + if (err == ESP_OK && u8 >= 40 && u8 <= 80) + { + temp_threshold = u8; + } + else + { + temp_threshold = 60; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "temp_threshold", temp_threshold, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist temp_threshold=%u: %s", + (unsigned)temp_threshold, esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "Invalid/missing temp_threshold (%s) -> default=60 (sync persisted).", + esp_err_to_name(err)); + } + + // ----------------------------------------- + // Availability (persisted) [0/1] + // ----------------------------------------- + err = storage_get_u8_sync(NVS_NAMESPACE, "available", &u8, rd_to); + if (err == ESP_OK && u8 <= 1) + { + is_available = (u8 != 0); + } + else + { + is_available = true; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "available", 1, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist available=1: %s", esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "Missing/invalid 'available' (%s) -> default=true (sync persisted).", + esp_err_to_name(err)); + } + + // ----------------------------------------- + // Enabled (persisted) [0/1] + // ----------------------------------------- + err = storage_get_u8_sync(NVS_NAMESPACE, "enabled", &u8, rd_to); + if (err == ESP_OK && u8 <= 1) + { + is_enabled = (u8 != 0); + } + else + { + is_enabled = true; + + esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "enabled", 1, wr_to); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist enabled=1: %s", esp_err_to_name(se)); + } + + ESP_LOGW(TAG, "Missing/invalid 'enabled' (%s) -> default=true (sync persisted).", + esp_err_to_name(err)); + } + + // Flush explícito no boot: + // - ajuda a garantir commit determinístico antes do resto do sistema avançar + // - mantém-se útil mesmo com setters sync se o teu storage ainda estiver com debounce interno + esp_err_t fe = storage_flush_sync(wr_to); + if (fe != ESP_OK) + ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe)); } // ======================== // Charging current getters/setters // ======================== -uint8_t evse_get_max_charging_current(void) -{ - return max_charging_current; -} +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; -} +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)) + if (value < MIN_CHARGING_CURRENT_LIMIT || value > max_charging_current) return ESP_ERR_INVALID_ARG; + + if (value == charging_current) + { + evse_set_runtime_charging_current(value); + return ESP_OK; + } + 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 err = storage_set_u16_async(NVS_NAMESPACE, "def_chrg_curr", value); + if (err != ESP_OK) + { + // Em runtime, isto pode falhar por fila cheia. RAM fica correta; persistência é best-effort. + ESP_LOGE(TAG, "Failed to persist def_chrg_curr async=%u: %s", (unsigned)value, esp_err_to_name(err)); + return err; + } -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); + evse_set_runtime_charging_current(value); + return ESP_OK; } // ======================== @@ -218,135 +292,119 @@ esp_err_t evse_set_default_charging_current(uint16_t value) 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; - - ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime); - - 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)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) -{ - return 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; -} +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; + if (value == socket_outlet) + return ESP_OK; + socket_outlet = value; - nvs_set_u8(nvs, "socket_outlet", value); - return nvs_commit(nvs); + + esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "socket_outlet", (uint8_t)value); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist socket_outlet async=%u: %s", (unsigned)value, esp_err_to_name(err)); + return err; + } + + return ESP_OK; } // ======================== // RCM // ======================== -bool evse_is_rcm(void) -{ - return 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; + if (value == rcm) + return ESP_OK; + rcm = value; - nvs_set_u8(nvs, "rcm", value); - return nvs_commit(nvs); + + esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "rcm", (uint8_t)value); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist rcm async=%u: %s", (unsigned)value, esp_err_to_name(err)); + return err; + } + + return ESP_OK; } // ======================== // Temperature // ======================== -uint8_t evse_get_temp_threshold(void) -{ - return temp_threshold; -} +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; + if (value == temp_threshold) + return ESP_OK; + temp_threshold = value; - nvs_set_u8(nvs, "temp_threshold", value); - return nvs_commit(nvs); + + esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "temp_threshold", value); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to persist temp_threshold async=%u: %s", (unsigned)value, esp_err_to_name(err)); + return err; + } + + return ESP_OK; } // ======================== // Availability // ======================== -bool evse_config_is_available(void) -{ - return is_available; -} +bool evse_config_is_available(void) { return is_available; } void evse_config_set_available(bool available) { - is_available = available ? true : false; + bool newv = available; + if (newv == is_available) + return; - esp_err_t err = nvs_set_u8(nvs, "available", (uint8_t)is_available); - if (err == ESP_OK) - err = nvs_commit(nvs); + is_available = newv; + + esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "available", (uint8_t)is_available); if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to persist 'available': %s", esp_err_to_name(err)); - } - - 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); + ESP_LOGE(TAG, "Failed to persist 'available' async=%u: %s", (unsigned)is_available, esp_err_to_name(err)); } // ======================== // Enable/Disable // ======================== -bool evse_config_is_enabled(void) -{ - return is_enabled; -} +bool evse_config_is_enabled(void) { return is_enabled; } void evse_config_set_enabled(bool enabled) { - is_enabled = enabled ? true : false; + bool newv = enabled; + if (newv == is_enabled) + return; - esp_err_t err = nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled); - if (err == ESP_OK) - err = nvs_commit(nvs); + is_enabled = newv; + + esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "enabled", (uint8_t)is_enabled); if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to persist 'enabled': %s", esp_err_to_name(err)); - } - - 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); + ESP_LOGE(TAG, "Failed to persist 'enabled' async=%u: %s", (unsigned)is_enabled, esp_err_to_name(err)); } diff --git a/components/evse/evse_core.c b/components/evse/evse_core.c index 857d820..578dbf8 100755 --- a/components/evse/evse_core.c +++ b/components/evse/evse_core.c @@ -1,3 +1,4 @@ +// components/evse/evse_core.c #include "evse_fsm.h" #include "evse_error.h" #include "evse_limits.h" @@ -14,6 +15,36 @@ static const char *TAG = "evse_core"; static SemaphoreHandle_t mutex; static evse_state_t last_state = EVSE_STATE_A; +// Filtro simples de histerese no pilot +#define PILOT_STABLE_SAMPLES 2 + +static pilot_voltage_t s_last_raw = PILOT_VOLTAGE_12; +static pilot_voltage_t s_filtered = PILOT_VOLTAGE_12; +static int s_stable_count = 0; + +static pilot_voltage_t filter_pilot_voltage(pilot_voltage_t raw) +{ + if (raw == s_last_raw) + { + if (s_stable_count < PILOT_STABLE_SAMPLES) + { + s_stable_count++; + } + } + else + { + s_last_raw = raw; + s_stable_count = 1; + } + + if (s_stable_count >= PILOT_STABLE_SAMPLES && raw != s_filtered) + { + s_filtered = raw; + } + + return s_filtered; +} + static void evse_process(void); static void evse_core_task(void *arg); @@ -32,7 +63,8 @@ void evse_init(void) evse_fsm_reset(); pilot_set_level(true); - xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 5, NULL); + BaseType_t rc = xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 6, NULL); + configASSERT(rc == pdPASS); } static void evse_process(void) @@ -44,19 +76,27 @@ static void evse_process(void) xSemaphoreTake(mutex, portMAX_DELAY); - pilot_voltage_t pilot_voltage; + pilot_voltage_t pilot_raw; bool is_n12v = false; - pilot_measure(&pilot_voltage, &is_n12v); - ESP_LOGD(TAG, "Pilot: %d, -12V: %s", pilot_voltage, is_n12v ? "yes" : "no"); + pilot_measure(&pilot_raw, &is_n12v); + pilot_voltage_t pilot_voltage = filter_pilot_voltage(pilot_raw); + ESP_LOGD(TAG, "Pilot(raw=%d, filt=%d), -12V: %s", + pilot_raw, pilot_voltage, is_n12v ? "yes" : "no"); + + // raw set/clear; erro visível mantém holdoff interno (60s após sumir) evse_error_check(pilot_voltage, is_n12v); + // ✅ Sem cooldown externo: disponibilidade depende só do erro "visível" + bool available = evse_config_is_available() && (evse_get_error() == 0); + bool enabled = evse_config_is_enabled(); + evse_fsm_process( pilot_voltage, evse_state_get_authorized(), - evse_config_is_available(), - evse_config_is_enabled()); + available, + enabled); evse_limits_check(); diff --git a/components/evse/evse_error.c b/components/evse/evse_error.c index eca88f1..3a9c554 100755 --- a/components/evse/evse_error.c +++ b/components/evse/evse_error.c @@ -1,5 +1,4 @@ #include "evse_error.h" -#include "evse_config.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -7,151 +6,147 @@ #include "esp_log.h" #include "ntc_sensor.h" +#include "esp_event.h" +#include "esp_timer.h" +#include "evse_events.h" +#include "evse_config.h" + static const char *TAG = "evse_error"; -// Estado global de erros -static uint32_t error_bits = 0; -static TickType_t auto_clear_timeout = 0; +// ---------------------------------------------------- +// Estado interno +// ---------------------------------------------------- +// raw_bits = erros “instantâneos” conforme checks (set/clear) +// visible_bits = erros expostos ao resto do sistema (com holdoff) +// clear_deadline = quando pode finalmente limpar visible_bits para 0 +static uint32_t raw_bits = 0; +static uint32_t visible_bits = 0; +static TickType_t clear_deadline = 0; -// Sticky flag: "todos erros foram limpos" +// Sticky flag: "todos erros visíveis foram limpos" static bool error_cleared = false; // Proteção contra concorrência static portMUX_TYPE error_mux = portMUX_INITIALIZER_UNLOCKED; -void evse_error_init(void) +// ---------------------------------------------------- +// Helper: publicar evento de alteração de erro (visible_bits) +// ---------------------------------------------------- +static void evse_error_post_event(uint32_t new_bits, uint32_t changed_mask) { + evse_error_event_data_t ev = { + .error_bits = new_bits, + .changed_mask = changed_mask, + .timestamp_us = esp_timer_get_time(), + }; + + esp_err_t err = esp_event_post( + EVSE_EVENTS, + EVSE_EVENT_ERROR_CHANGED, + &ev, + sizeof(ev), + portMAX_DELAY); + + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Falha ao publicar EVSE_EVENT_ERROR_CHANGED: %s", + esp_err_to_name(err)); + } +} + +// ---------------------------------------------------- +// Helpers internos +// ---------------------------------------------------- +static bool raw_has_bit(uint32_t bit) +{ + bool v; portENTER_CRITICAL(&error_mux); - error_bits = 0; - auto_clear_timeout = 0; - error_cleared = false; + v = ((raw_bits & bit) != 0); portEXIT_CRITICAL(&error_mux); + return v; } -void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v) +static void reconcile_visible_locked(TickType_t now) { - ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s", - pilot_voltage, is_n12v ? "true" : "false"); - - // 1) Falha elétrica geral no pilot - if (pilot_voltage == PILOT_VOLTAGE_1) + // Se existem erros reais, o visível segue imediatamente + if (raw_bits != 0) { - bool first_time = false; - - portENTER_CRITICAL(&error_mux); - if (!(error_bits & EVSE_ERR_PILOT_FAULT_BIT)) - { - error_cleared = false; - error_bits |= EVSE_ERR_PILOT_FAULT_BIT; - first_time = true; - } - portEXIT_CRITICAL(&error_mux); - - if (first_time) - { - ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)"); - } - } - else - { - // Pilot voltou a nível válido → limpa erro de pilot fault - evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT); - } - - // 2) Falta de -12V durante PWM (C ou D) - if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v) - { - bool first_time = false; - - portENTER_CRITICAL(&error_mux); - if (!(error_bits & EVSE_ERR_DIODE_SHORT_BIT)) - { - error_cleared = false; - error_bits |= EVSE_ERR_DIODE_SHORT_BIT; - auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); - first_time = true; - } - portEXIT_CRITICAL(&error_mux); - - if (first_time) - { - ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)"); - } - } - else - { - // Se já não estamos em C/D sem -12V, limpa o erro de diodo curto - evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT); - } -} - -void evse_temperature_check(void) -{ - float temp_c = ntc_temp_sensor(); - uint8_t threshold = evse_get_temp_threshold(); - - ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C", - temp_c, threshold); - - // Temperatura inválida -> erro de sensor - if (temp_c < -40.0f || temp_c > 150.0f) - { - bool first_time = false; - - portENTER_CRITICAL(&error_mux); - if (!(error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT)) - { - error_cleared = false; - error_bits |= EVSE_ERR_TEMPERATURE_FAULT_BIT; - first_time = true; - } - portEXIT_CRITICAL(&error_mux); - - if (first_time) - { - ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado"); - } + visible_bits = raw_bits; + clear_deadline = 0; + error_cleared = false; return; } - // Leitura válida -> limpa erro de sensor - evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT); - - // Temperatura máxima - if (temp_c >= threshold) + // raw_bits == 0 + if (visible_bits == 0) { - bool first_time = false; - - portENTER_CRITICAL(&error_mux); - if (!(error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT)) - { - error_cleared = false; - error_bits |= EVSE_ERR_TEMPERATURE_HIGH_BIT; - auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); - first_time = true; - } - portEXIT_CRITICAL(&error_mux); - - if (first_time) - { - ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C", - temp_c, threshold); - } + clear_deadline = 0; + return; } - else + + // Ainda há erro visível (holdoff). Arma deadline 1x. + if (clear_deadline == 0) { - evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT); + clear_deadline = now + pdMS_TO_TICKS(EVSE_ERROR_COOLDOWN_MS); + return; + } + + // Expirou -> limpar finalmente + if ((int32_t)(now - clear_deadline) >= 0) + { + visible_bits = 0; + clear_deadline = 0; + error_cleared = true; + } +} + +// ---------------------------------------------------- +// API pública +// ---------------------------------------------------- +void evse_error_init(void) +{ + uint32_t old_vis, new_vis, changed; + bool post = false; + + portENTER_CRITICAL(&error_mux); + + old_vis = visible_bits; + + raw_bits = 0; + visible_bits = 0; + clear_deadline = 0; + error_cleared = false; + + new_vis = visible_bits; + changed = old_vis ^ new_vis; + post = (changed != 0); + + portEXIT_CRITICAL(&error_mux); + + if (post) + { + evse_error_post_event(new_vis, changed); } } uint32_t evse_get_error(void) { portENTER_CRITICAL(&error_mux); - uint32_t val = error_bits; + uint32_t val = visible_bits; portEXIT_CRITICAL(&error_mux); return val; } +bool evse_error_is_active(void) +{ + return evse_get_error() != 0; +} + +uint32_t evse_error_get_bits(void) +{ + return evse_get_error(); +} + bool evse_error_cleared_flag(void) { portENTER_CRITICAL(&error_mux); @@ -169,61 +164,147 @@ void evse_error_reset_flag(void) void evse_error_set(uint32_t bitmask) { + uint32_t old_vis, new_vis, changed; + TickType_t now = xTaskGetTickCount(); + portENTER_CRITICAL(&error_mux); - error_cleared = false; - error_bits |= bitmask; + old_vis = visible_bits; - if (bitmask & EVSE_ERR_AUTO_CLEAR_BITS) - { - auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); // 60s - } + raw_bits |= bitmask; + // se aparece qualquer erro, o "cleared" deixa de ser verdade + error_cleared = false; + + reconcile_visible_locked(now); + + new_vis = visible_bits; + changed = old_vis ^ new_vis; portEXIT_CRITICAL(&error_mux); + + if (changed != 0) + { + evse_error_post_event(new_vis, changed); + } } void evse_error_clear(uint32_t bitmask) { + uint32_t old_vis, new_vis, changed; + TickType_t now = xTaskGetTickCount(); + portENTER_CRITICAL(&error_mux); - bool had_error = (error_bits != 0); - error_bits &= ~bitmask; + old_vis = visible_bits; - if (had_error && error_bits == 0) - { - error_cleared = true; - } + raw_bits &= ~bitmask; + + // ✅ Aqui é onde o “60s depois do erro desaparecer” é armado: + // quando raw_bits chega a 0, reconcile arma clear_deadline (uma vez) + reconcile_visible_locked(now); + + new_vis = visible_bits; + changed = old_vis ^ new_vis; portEXIT_CRITICAL(&error_mux); + + if (changed != 0) + { + evse_error_post_event(new_vis, changed); + } } void evse_error_tick(void) { + uint32_t old_vis, new_vis, changed; + TickType_t now = xTaskGetTickCount(); + portENTER_CRITICAL(&error_mux); - if ((error_bits & EVSE_ERR_AUTO_CLEAR_BITS) && - auto_clear_timeout != 0 && - xTaskGetTickCount() >= auto_clear_timeout) - { - error_bits &= ~EVSE_ERR_AUTO_CLEAR_BITS; - - if (error_bits == 0) - { - error_cleared = true; - } - - auto_clear_timeout = 0; - } + old_vis = visible_bits; + reconcile_visible_locked(now); + new_vis = visible_bits; + changed = old_vis ^ new_vis; portEXIT_CRITICAL(&error_mux); + + if (changed != 0) + { + evse_error_post_event(new_vis, changed); + } } -bool evse_error_is_active(void) +// ---------------------------------------------------- +// Checks (raw -> set/clear) +// ---------------------------------------------------- +void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v) { - return evse_get_error() != 0; + ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s", + pilot_voltage, is_n12v ? "true" : "false"); + + // 1) Falha elétrica geral no pilot + if (pilot_voltage == PILOT_VOLTAGE_1) + { + if (!raw_has_bit(EVSE_ERR_PILOT_FAULT_BIT)) + { + ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)"); + } + evse_error_set(EVSE_ERR_PILOT_FAULT_BIT); + } + else + { + evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT); + } + + // 2) Falta de -12V durante PWM (C ou D) + if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v) + { + if (!raw_has_bit(EVSE_ERR_DIODE_SHORT_BIT)) + { + ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)"); + } + evse_error_set(EVSE_ERR_DIODE_SHORT_BIT); + } + else + { + evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT); + } } -uint32_t evse_error_get_bits(void) +void evse_temperature_check(void) { - return evse_get_error(); + float temp_c = ntc_temp_sensor(); + uint8_t threshold = evse_get_temp_threshold(); + + ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C", + temp_c, threshold); + + // Temperatura inválida -> erro de sensor + if (temp_c < -40.0f || temp_c > 150.0f) + { + if (!raw_has_bit(EVSE_ERR_TEMPERATURE_FAULT_BIT)) + { + ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado"); + } + evse_error_set(EVSE_ERR_TEMPERATURE_FAULT_BIT); + return; + } + + // Leitura válida -> limpa erro de sensor + evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT); + + // Temperatura máxima + if (temp_c >= threshold) + { + if (!raw_has_bit(EVSE_ERR_TEMPERATURE_HIGH_BIT)) + { + ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C", + temp_c, threshold); + } + evse_error_set(EVSE_ERR_TEMPERATURE_HIGH_BIT); + } + else + { + evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT); + } } diff --git a/components/evse/evse_fsm.c b/components/evse/evse_fsm.c index 547c669..2f6cbee 100755 --- a/components/evse/evse_fsm.c +++ b/components/evse/evse_fsm.c @@ -1,3 +1,4 @@ +// components/evse/evse_fsm.c #include "evse_fsm.h" #include "evse_api.h" #include "evse_pilot.h" @@ -17,16 +18,14 @@ static const char *TAG = "evse_fsm"; #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; } +/** + * @brief Atualiza saídas de hardware (pilot, relé, trava) em função do estado lógico. + */ static void update_outputs(evse_state_t state) { const uint16_t current = evse_get_runtime_charging_current(); @@ -38,7 +37,7 @@ static void update_outputs(evse_state_t state) cable_max_current = proximity_get_max_current(); } - // Segurança: relé sempre off e outputs seguros em caso de erro + // Segurança total: qualquer erro ativo força saída segura if (evse_get_error() != 0) { if (ac_relay_get_state()) @@ -46,8 +45,14 @@ static void update_outputs(evse_state_t state) ac_relay_set_state(false); ESP_LOGW(TAG, "ERRO ativo: relé estava ligado, agora desligado por segurança!"); } - ac_relay_set_state(false); + else + { + ac_relay_set_state(false); + } + + // Em erro, garantir pilot OFF (não PWM / não +12V) pilot_set_level(true); + if (board_config.socket_lock && socket_outlet) { socket_lock_set_locked(false); @@ -55,14 +60,16 @@ static void update_outputs(evse_state_t state) return; } - // Fluxo normal switch (state) { case EVSE_STATE_A: case EVSE_STATE_E: case EVSE_STATE_F: ac_relay_set_state(false); + + // A → pilot alto (+12V), E/F → pilot OFF pilot_set_level(state == EVSE_STATE_A); + if (board_config.socket_lock && socket_outlet) { socket_lock_set_locked(false); @@ -72,66 +79,77 @@ static void update_outputs(evse_state_t state) 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"); - } + (void)rcm_test(); break; case EVSE_STATE_B2: pilot_set_amps(MIN(current, cable_max_current)); ac_relay_set_state(false); + + if (board_config.socket_lock && socket_outlet) + { + socket_lock_set_locked(true); + } break; case EVSE_STATE_C1: case EVSE_STATE_D1: - { pilot_set_amps(MIN(current, cable_max_current)); ac_relay_set_state(false); - c1_d1_waiting = true; - c1_d1_relay_to = xTaskGetTickCount() + pdMS_TO_TICKS(6000); + + if (board_config.socket_lock && socket_outlet) + { + socket_lock_set_locked(true); + } break; - } case EVSE_STATE_C2: case EVSE_STATE_D2: pilot_set_amps(MIN(current, cable_max_current)); ac_relay_set_state(true); + + if (board_config.socket_lock && socket_outlet) + { + socket_lock_set_locked(true); + } break; } } -// FSM principal +/** + * @brief Máquina de estados principal do EVSE (IEC 61851). + */ 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) + // 1) Erros globais: dominam qualquer outra lógica + uint32_t err_bits = evse_get_error(); + if (err_bits != 0) { - if (evse_get_state() != EVSE_STATE_F) + evse_state_t forced_state = + (err_bits & EVSE_ERR_PILOT_FAULT_BIT) ? EVSE_STATE_E : EVSE_STATE_F; + + if (evse_get_state() != forced_state) { - ESP_LOGW(TAG, "Erro ativo detectado: forçando estado FAULT (F)"); - evse_set_state(EVSE_STATE_F); + ESP_LOGW(TAG, "Erro ativo detectado: forçando estado %s", + evse_state_to_str(forced_state)); + evse_set_state(forced_state); } - update_outputs(EVSE_STATE_F); + + update_outputs(forced_state); return; } - TickType_t now = xTaskGetTickCount(); - evse_state_t prev = evse_get_state(); - evse_state_t curr = prev; + evse_state_t curr = evse_get_state(); switch (curr) { @@ -153,17 +171,25 @@ void evse_fsm_process( 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; + + case PILOT_VOLTAGE_3: + evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1); + break; + default: break; } @@ -171,52 +197,59 @@ void evse_fsm_process( 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) + if (!available) { - evse_set_state((curr == EVSE_STATE_D2 || curr == EVSE_STATE_D1) - ? EVSE_STATE_D1 - : EVSE_STATE_C1); + evse_set_state(EVSE_STATE_F); break; } + + if (!enabled) + { + if (curr == EVSE_STATE_C2) + { + evse_set_state(EVSE_STATE_C1); + } + else if (curr == EVSE_STATE_D2) + { + evse_set_state(EVSE_STATE_D1); + } + 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 + // ✅ Agora recupera como F: se disponível e sem erro -> volta a A + if (available && evse_get_error() == 0) + { + evse_set_state(EVSE_STATE_A); + } 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); diff --git a/components/evse/evse_limits.c b/components/evse/evse_limits.c index 11ad5ab..32b0f8d 100755 --- a/components/evse/evse_limits.c +++ b/components/evse/evse_limits.c @@ -1,34 +1,123 @@ -#include // for PRIu32 +#include +#include + #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" #include "esp_err.h" -#include "nvs.h" +#include "esp_check.h" -// ======================== -// Concurrency protection -// ======================== +#include "storage_service.h" + +#define NVS_NAMESPACE "evse_limits" +static const char *TAG = "evse_limits"; static portMUX_TYPE evse_mux = portMUX_INITIALIZER_UNLOCKED; -// ======================== -// Runtime state (volatile) -// ======================== +static bool limit_reached = false; +static uint32_t consumption_limit = 0; // Wh +static uint32_t charging_time_limit = 0; // seconds +static uint16_t under_power_limit = 0; // W -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 +static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); } +static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(1000); } -// ======================== -// Limit status flag -// ======================== +// --------------------------------- +// Init + defaults +// --------------------------------- +esp_err_t evse_limits_init(void) +{ + ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed"); + ESP_LOGI(TAG, "EVSE limits init OK (storage-backed)"); + return ESP_OK; +} +void evse_limits_check_defaults(void) +{ + esp_err_t err; + bool needs_flush = false; + + uint32_t u32 = 0; + uint16_t u16 = 0; + + ESP_LOGD(TAG, "Checking default limits..."); + + // Consumption limit (Wh) default = 0 (disabled) + err = storage_get_u32_sync(NVS_NAMESPACE, "def_cons_lim", &u32, BOOT_TO()); + if (err == ESP_OK) + { + portENTER_CRITICAL(&evse_mux); + consumption_limit = u32; + portEXIT_CRITICAL(&evse_mux); + } + else + { + portENTER_CRITICAL(&evse_mux); + consumption_limit = 0; + portEXIT_CRITICAL(&evse_mux); + + (void)storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", 0); + needs_flush = true; + ESP_LOGW(TAG, "Missing def_cons_lim (%s) -> default=0 (persisted).", esp_err_to_name(err)); + } + + // Charging time limit (s) default = 0 (disabled) + err = storage_get_u32_sync(NVS_NAMESPACE, "def_ch_time_lim", &u32, BOOT_TO()); + if (err == ESP_OK) + { + portENTER_CRITICAL(&evse_mux); + charging_time_limit = u32; + portEXIT_CRITICAL(&evse_mux); + } + else + { + portENTER_CRITICAL(&evse_mux); + charging_time_limit = 0; + portEXIT_CRITICAL(&evse_mux); + + (void)storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", 0); + needs_flush = true; + ESP_LOGW(TAG, "Missing def_ch_time_lim (%s) -> default=0 (persisted).", esp_err_to_name(err)); + } + + // Under-power limit (W) default = 0 (disabled) + err = storage_get_u16_sync(NVS_NAMESPACE, "def_un_pwr_lim", &u16, BOOT_TO()); + if (err == ESP_OK) + { + portENTER_CRITICAL(&evse_mux); + under_power_limit = u16; + portEXIT_CRITICAL(&evse_mux); + } + else + { + portENTER_CRITICAL(&evse_mux); + under_power_limit = 0; + portEXIT_CRITICAL(&evse_mux); + + (void)storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", 0); + needs_flush = true; + ESP_LOGW(TAG, "Missing def_un_pwr_lim (%s) -> default=0 (persisted).", esp_err_to_name(err)); + } + + if (needs_flush) + { + esp_err_t fe = storage_flush_sync(TO_TICKS_MS(2000)); + if (fe != ESP_OK) + ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe)); + else + ESP_LOGD(TAG, "Defaults committed (flush)."); + } +} + +// --------------------------------- +// Limit reached flag +// --------------------------------- bool evse_get_limit_reached(void) { bool val; @@ -50,10 +139,9 @@ bool evse_is_limit_reached(void) return evse_get_limit_reached(); } -// ======================== -// Runtime limit accessors -// ======================== - +// --------------------------------- +// Consumption limit +// --------------------------------- uint32_t evse_get_consumption_limit(void) { uint32_t val; @@ -78,30 +166,18 @@ void evse_set_consumption_limit(uint32_t value) if (!changed) return; - nvs_handle_t h; - esp_err_t err = nvs_open("evse", NVS_READWRITE, &h); - if (err == ESP_OK) + esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", value); + if (err != ESP_OK) { - err = nvs_set_u32(h, "def_cons_lim", value); - if (err == ESP_OK) - err = nvs_commit(h); - nvs_close(h); - - if (err != ESP_OK) - { - ESP_LOGE("EVSE_LIMITS", - "Failed to persist consumption limit (%" PRIu32 " Wh): %s", - value, esp_err_to_name(err)); - } - } - else - { - ESP_LOGE("EVSE_LIMITS", - "Failed to open NVS for consumption limit: %s", - esp_err_to_name(err)); + ESP_LOGE(TAG, + "Failed to persist consumption limit (%" PRIu32 " Wh): %s", + value, esp_err_to_name(err)); } } +// --------------------------------- +// Charging time limit +// --------------------------------- uint32_t evse_get_charging_time_limit(void) { uint32_t val; @@ -126,30 +202,18 @@ void evse_set_charging_time_limit(uint32_t value) if (!changed) return; - nvs_handle_t h; - esp_err_t err = nvs_open("evse", NVS_READWRITE, &h); - if (err == ESP_OK) + esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", value); + if (err != ESP_OK) { - err = nvs_set_u32(h, "def_ch_time_lim", value); - if (err == ESP_OK) - err = nvs_commit(h); - nvs_close(h); - - if (err != ESP_OK) - { - ESP_LOGE("EVSE_LIMITS", - "Failed to persist charging time limit (%" PRIu32 " s): %s", - value, esp_err_to_name(err)); - } - } - else - { - ESP_LOGE("EVSE_LIMITS", - "Failed to open NVS for charging time limit: %s", - esp_err_to_name(err)); + ESP_LOGE(TAG, + "Failed to persist charging time limit (%" PRIu32 " s): %s", + value, esp_err_to_name(err)); } } +// --------------------------------- +// Under-power limit +// --------------------------------- uint16_t evse_get_under_power_limit(void) { uint16_t val; @@ -174,82 +238,64 @@ void evse_set_under_power_limit(uint16_t value) if (!changed) return; - nvs_handle_t h; - esp_err_t err = nvs_open("evse", NVS_READWRITE, &h); - if (err == ESP_OK) + esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", value); + if (err != ESP_OK) { - err = nvs_set_u16(h, "def_un_pwr_lim", value); - if (err == ESP_OK) - err = nvs_commit(h); - nvs_close(h); - - if (err != ESP_OK) - { - ESP_LOGE("EVSE_LIMITS", - "Failed to persist under-power limit (%" PRIu32 " W): %s", - (uint32_t)value, esp_err_to_name(err)); - } - } - else - { - ESP_LOGE("EVSE_LIMITS", - "Failed to open NVS for under-power limit: %s", - esp_err_to_name(err)); + ESP_LOGE(TAG, + "Failed to persist under-power limit (%" PRIu32 " W): %s", + (uint32_t)value, esp_err_to_name(err)); } } -// ======================== -// Limit checking logic -// ======================== - +// --------------------------------- +// Runtime check +// --------------------------------- void evse_limits_check(void) { - // Só faz sentido durante carregamento + // Só faz sentido quando há energia ativa (C2/D2) if (!evse_state_is_charging(evse_get_state())) - { return; - } evse_session_t sess; if (!evse_session_get(&sess) || !sess.is_current) - { - // Sem sessão ativa → nada a fazer return; - } + + uint32_t cons_lim; + uint32_t time_lim; + uint16_t unp_lim; + + portENTER_CRITICAL(&evse_mux); + cons_lim = consumption_limit; + time_lim = charging_time_limit; + unp_lim = under_power_limit; + portEXIT_CRITICAL(&evse_mux); bool reached = false; - // 1) Limite de energia (Wh) - if (consumption_limit > 0 && sess.energy_wh >= consumption_limit) + if (cons_lim > 0 && sess.energy_wh >= cons_lim) { - ESP_LOGW("EVSE_LIMITS", - "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh", - sess.energy_wh, consumption_limit); + ESP_LOGW(TAG, "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh", + sess.energy_wh, cons_lim); reached = true; } - // 2) Limite de tempo (s) - if (charging_time_limit > 0 && sess.duration_s >= charging_time_limit) + if (time_lim > 0 && sess.duration_s >= time_lim) { - ESP_LOGW("EVSE_LIMITS", - "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s", - sess.duration_s, charging_time_limit); + ESP_LOGW(TAG, "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s", + sess.duration_s, time_lim); reached = true; } - // 3) Under-power (potência instantânea) - uint32_t inst_power = evse_meter_get_instant_power(); - if (under_power_limit > 0 && inst_power < under_power_limit) + int32_t p = evse_meter_get_instant_power(); + uint32_t inst_power = (p > 0) ? (uint32_t)p : 0; + + if (unp_lim > 0 && inst_power < (uint32_t)unp_lim) { - ESP_LOGW("EVSE_LIMITS", - "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W", - (uint32_t)inst_power, - (uint32_t)under_power_limit); + ESP_LOGW(TAG, "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W", + inst_power, (uint32_t)unp_lim); reached = true; } if (reached) - { evse_set_limit_reached(true); - } } diff --git a/components/evse/evse_manager.c b/components/evse/evse_manager.c index 3d2f49f..aa74a05 100755 --- a/components/evse/evse_manager.c +++ b/components/evse/evse_manager.c @@ -1,4 +1,3 @@ -// === Início de: components/evse/evse_manager.c === #include "evse_manager.h" #include "evse_state.h" #include "evse_error.h" @@ -11,10 +10,10 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" -#include "freertos/queue.h" #include "esp_log.h" #include "esp_event.h" +#include "esp_err.h" #include #include @@ -27,27 +26,29 @@ static const char *TAG = "EVSE_Manager"; static SemaphoreHandle_t evse_mutex; -static volatile bool auth_enabled = false; + +// ✅ Proteção para flags partilhadas (event handlers vs task) +static portMUX_TYPE s_mgr_mux = portMUX_INITIALIZER_UNLOCKED; +static bool auth_enabled = false; // Estado de pausa controlado pelo Load Balancer -static volatile bool lb_paused = false; -static volatile bool lb_prev_authorized = false; +static bool lb_paused = false; +static bool lb_prev_authorized = false; // Estado de janela do scheduler -static volatile bool s_sched_allowed = true; +static bool s_sched_allowed = true; static portMUX_TYPE s_sched_mux = portMUX_INITIALIZER_UNLOCKED; #define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo -// ================= Helpers internos ================= - static void lb_clear_pause_state(void) { + portENTER_CRITICAL(&s_mgr_mux); lb_paused = false; lb_prev_authorized = false; + portEXIT_CRITICAL(&s_mgr_mux); } -// Exposto para outros módulos (se quiserem saber se o scheduler permite) bool evse_sched_is_allowed(void) { bool v; @@ -60,19 +61,35 @@ bool evse_sched_is_allowed(void) static void evse_manager_handle_auth_on_tick(void) { bool sched_allowed = evse_sched_is_allowed(); + uint32_t err_bits = evse_get_error(); // inclui holdoff interno + bool has_error = (err_bits != 0); - if (auth_enabled) + bool local_auth_enabled; + bool local_lb_paused; + + portENTER_CRITICAL(&s_mgr_mux); + local_auth_enabled = auth_enabled; + local_lb_paused = lb_paused; + portEXIT_CRITICAL(&s_mgr_mux); + + if (local_auth_enabled) { - // Se o carro foi desconectado, revoga autorização if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A) { ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization."); evse_state_set_authorized(false); - // Desconexão física invalida qualquer pausa pendente do LB lb_clear_pause_state(); } - // Em modos RFID/OCPP, o scheduler pode também forçar paragem + if (has_error && evse_state_get_authorized()) + { + ESP_LOGI(TAG, + "[AUTH] error active (err=0x%08" PRIx32 ") → revoking authorization.", + err_bits); + evse_state_set_authorized(false); + lb_clear_pause_state(); + } + if (!sched_allowed && evse_state_get_authorized()) { ESP_LOGI(TAG, "[SCHED] window closed (auth mode) → revoking authorization."); @@ -81,28 +98,32 @@ static void evse_manager_handle_auth_on_tick(void) } else { - // Modo OPEN: só autoriza se LB e Scheduler permitirem - if (!lb_paused && sched_allowed && !evse_state_get_authorized()) + bool limit_hit = evse_is_limit_reached(); + bool can_operate = evse_config_is_available() && evse_config_is_enabled(); + + if ((has_error || limit_hit || !sched_allowed || !can_operate || local_lb_paused) && + evse_state_get_authorized()) { - evse_state_set_authorized(true); - ESP_LOGI(TAG, "Authentication disabled → forced authorization (within schedule)."); - lb_clear_pause_state(); + ESP_LOGI(TAG, + "[OPEN] blocking (err=%d limit=%d sched=%d operate=%d lb_paused=%d) → revoking authorization.", + (int)has_error, (int)limit_hit, (int)sched_allowed, (int)can_operate, (int)local_lb_paused); + evse_state_set_authorized(false); } - // Fora da janela, garantir que não fica autorizado - if (!sched_allowed && evse_state_get_authorized()) + if (!local_lb_paused && sched_allowed && can_operate && + !has_error && !limit_hit && + !evse_state_get_authorized()) { - ESP_LOGI(TAG, "[SCHED] window closed (OPEN mode) → revoking authorization."); - evse_state_set_authorized(false); + evse_state_set_authorized(true); + ESP_LOGI(TAG, "Authentication disabled → forced authorization (schedule ok, no error/limits)."); + lb_clear_pause_state(); } } } -// ===== Task de ciclo principal ===== static void evse_manager_task(void *arg) { (void)arg; - while (true) { evse_manager_tick(); @@ -110,15 +131,10 @@ static void evse_manager_task(void *arg) } } -// ===== Tratador de eventos de AUTH ===== static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *data) { (void)arg; - - if (base != AUTH_EVENTS || !data) - return; - - auth_mode_t g_mode = AUTH_MODE_OPEN; + if (base != AUTH_EVENTS || !data) return; switch (id) { @@ -127,8 +143,6 @@ static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *da 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); - - // Qualquer alteração explícita de auth invalida pausa do LB lb_clear_pause_state(); break; } @@ -137,37 +151,26 @@ static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *da 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) - { - // Em OPEN, a autorização passa a ser gerida por evse_manager_handle_auth_on_tick(), - // que também respeita o scheduler. - evse_state_set_authorized(false); // vai ser forçado no próximo tick se permitido - auth_enabled = false; - } - else - { - evse_state_set_authorized(false); - auth_enabled = true; - } + ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(evt->mode)); - // Modo mudou -> qualquer pausa antiga deixa de fazer sentido + portENTER_CRITICAL(&s_mgr_mux); + auth_enabled = (evt->mode != AUTH_MODE_OPEN); + portEXIT_CRITICAL(&s_mgr_mux); + + evse_state_set_authorized(false); lb_clear_pause_state(); break; } } } -// ===== Tratador de eventos de Load Balancer ===== static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { (void)handler_arg; (void)event_base; - if (!event_data) - return; + if (!event_data) return; if (event_id == LOADBALANCER_EVENT_INIT || event_id == LOADBALANCER_EVENT_STATE_CHANGED) { @@ -181,98 +184,86 @@ static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base 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)", + ESP_LOGI(TAG, "Novo limite de corrente (master): %u A (ts: %lld)", evt->max_current, (long long)evt->timestamp_us); if (evt->max_current == 0) { - // Suspensão por LB (não interessa se é OPEN ou RFID/OCPP) - lb_paused = true; - lb_prev_authorized = evse_state_get_authorized(); + bool prev_auth = evse_state_get_authorized(); - if (lb_prev_authorized) + portENTER_CRITICAL(&s_mgr_mux); + lb_paused = true; + lb_prev_authorized = prev_auth; + portEXIT_CRITICAL(&s_mgr_mux); + + if (prev_auth) { ESP_LOGI(TAG, "[LB] limit=0A → pausando sessão (authorized=false)"); evse_state_set_authorized(false); } - else - { - ESP_LOGD(TAG, "[LB] limit=0A → já não estava autorizado"); - } } else { - // Ajusta corrente em runtime evse_set_runtime_charging_current(evt->max_current); - if (lb_paused) - { - lb_paused = false; + bool was_paused; + bool prev_auth; - // Só retomamos se EVSE estiver operacional e scheduler permitir + portENTER_CRITICAL(&s_mgr_mux); + was_paused = lb_paused; + prev_auth = lb_prev_authorized; + portEXIT_CRITICAL(&s_mgr_mux); + + if (was_paused) + { bool can_resume = (evse_get_error() == 0) && evse_config_is_available() && evse_config_is_enabled() && - evse_sched_is_allowed(); + evse_sched_is_allowed() && + !evse_is_limit_reached(); if (!can_resume) { ESP_LOGW(TAG, - "[LB] limit=%uA → não retoma automaticamente (erro/indisponível/desabilitado/fora de horário)", + "[LB] limit=%uA → não retoma automaticamente (erro/indisp/desab/fora de horário/limite)", evt->max_current); lb_clear_pause_state(); return; } - if (!auth_enabled) + bool local_auth_enabled; + portENTER_CRITICAL(&s_mgr_mux); + local_auth_enabled = auth_enabled; + lb_paused = false; // já vai tentar retomar + portEXIT_CRITICAL(&s_mgr_mux); + + if (!local_auth_enabled) { - // Modo OPEN: retoma sempre (se dentro da janela do scheduler) - ESP_LOGI(TAG, - "[LB] limit=%uA → modo OPEN, reautorizando (authorized=true)", - evt->max_current); + ESP_LOGI(TAG, "[LB] limit=%uA → modo OPEN, reautorizando", evt->max_current); evse_state_set_authorized(true); } else { - // RFID/OCPP: só retoma se havia autorização antes da pausa - if (lb_prev_authorized) + if (prev_auth) { - ESP_LOGI(TAG, - "[LB] limit=%uA → RFID/OCPP, retomando autorização anterior (auto-resume)", - evt->max_current); + ESP_LOGI(TAG, "[LB] limit=%uA → RFID/OCPP, retomando autorização anterior", evt->max_current); evse_state_set_authorized(true); } - else - { - ESP_LOGI(TAG, - "[LB] limit=%uA → RFID/OCPP, sem autorização prévia, mantendo estado atual", - evt->max_current); - } } - // Limpa estado prévio (não reaplicar em pausas futuras) + portENTER_CRITICAL(&s_mgr_mux); lb_prev_authorized = false; - } - else - { - // Caso normal: apenas ajuste de corrente, sem mexer em auth - ESP_LOGD(TAG, - "[LB] limit=%uA → ajustando corrente runtime (sem mudança de autorização)", - evt->max_current); + portEXIT_CRITICAL(&s_mgr_mux); } } } } -// ===== Tratador de eventos de OCPP ===== static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *data) { (void)arg; - - if (base != OCPP_EVENTS) - return; + if (base != OCPP_EVENTS) return; switch (id) { @@ -283,13 +274,10 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da break; case OCPP_EVENT_AUTH_REJECTED: - ESP_LOGW(TAG, "[OCPP] Authorization rejected"); - evse_state_set_authorized(false); - lb_clear_pause_state(); - break; - case OCPP_EVENT_AUTH_TIMEOUT: - ESP_LOGW(TAG, "[OCPP] Authorization timeout"); + case OCPP_EVENT_REMOTE_STOP: + case OCPP_EVENT_STOP_TX: + ESP_LOGW(TAG, "[OCPP] Authorization/Stop"); evse_state_set_authorized(false); lb_clear_pause_state(); break; @@ -300,24 +288,11 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da lb_clear_pause_state(); break; - case OCPP_EVENT_REMOTE_STOP: - ESP_LOGI(TAG, "[OCPP] RemoteStop"); - evse_state_set_authorized(false); - lb_clear_pause_state(); - break; - case OCPP_EVENT_START_TX: ESP_LOGI(TAG, "[OCPP] StartTx"); lb_clear_pause_state(); break; - case OCPP_EVENT_STOP_TX: - ESP_LOGI(TAG, "[OCPP] StopTx"); - evse_state_set_authorized(false); - lb_clear_pause_state(); - break; - - // ChangeAvailability remoto (operative/inoperative) case OCPP_EVENT_OPERATIVE_UPDATED: { if (!data) @@ -329,7 +304,6 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da 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; } @@ -340,16 +314,10 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da } } -// ===== Tratador de eventos de Scheduler ===== -static void on_sched_event(void *arg, - esp_event_base_t base, - int32_t id, - void *data) +static void on_sched_event(void *arg, esp_event_base_t base, int32_t id, void *data) { (void)arg; - - if (base != SCHED_EVENTS || data == NULL) - return; + if (base != SCHED_EVENTS || data == NULL) return; const sched_event_state_t *ev = (const sched_event_state_t *)data; @@ -357,29 +325,26 @@ static void on_sched_event(void *arg, s_sched_allowed = ev->allowed_now; portEXIT_CRITICAL(&s_sched_mux); - ESP_LOGI(TAG, - "[SCHED] event id=%" PRIi32 " allowed_now=%d", - id, (int)ev->allowed_now); + ESP_LOGI(TAG, "[SCHED] allowed_now=%d", (int)ev->allowed_now); - // Se a janela fechou, parar sessão (revogar autorização) if (!ev->allowed_now && evse_state_get_authorized()) { ESP_LOGI(TAG, "[SCHED] window closed → stopping session (authorized=false)"); evse_state_set_authorized(false); } - - // Se a janela abriu de novo, não auto-reautorizamos aqui. - // Deixamos que o utilizador / OCPP decida iniciar nova sessão. - // (Em modo OPEN, o tick trata disso respeitando o scheduler.) } -// ===== Inicialização ===== void evse_manager_init(void) { evse_mutex = xSemaphoreCreateMutex(); configASSERT(evse_mutex != NULL); - evse_config_init(); + esp_err_t err = evse_config_init(); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Failed to init EVSE config NVS: %s", esp_err_to_name(err)); + } + evse_error_init(); evse_hardware_init(); evse_state_init(); @@ -393,11 +358,10 @@ void evse_manager_init(void) ESP_LOGI(TAG, "EVSE Manager inicializado."); - BaseType_t rc = xTaskCreate(evse_manager_task, "evse_manager_task", 4096, NULL, 5, NULL); + BaseType_t rc = xTaskCreate(evse_manager_task, "evse_manager_task", 8192, NULL, 4, NULL); configASSERT(rc == pdPASS); } -// ===== Main Tick ===== void evse_manager_tick(void) { xSemaphoreTake(evse_mutex, portMAX_DELAY); @@ -412,5 +376,3 @@ void evse_manager_tick(void) xSemaphoreGive(evse_mutex); } - -// === Fim de: components/evse/evse_manager.c === diff --git a/components/evse/evse_pilot.c b/components/evse/evse_pilot.c index 9c89635..d70fcac 100755 --- a/components/evse/evse_pilot.c +++ b/components/evse/evse_pilot.c @@ -1,8 +1,7 @@ +// components/evse/evse_pilot.c #include #include #include -#include -#include #include "driver/ledc.h" #include "esp_err.h" @@ -13,153 +12,232 @@ #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 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 +// --- Configuração de amostragem do Pilot --- +#define NUM_PILOT_SAMPLES 100 +#define MAX_SAMPLE_ATTEMPTS 1000 -#define ADC121_VREF_MV 3300 -#define ADC121_MAX 4095 +#define PILOT_SAMPLE_DELAY_US 10 + +// Percentagem para descartar extremos superior/inferior (ruído) +#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior + +// ADC referência +#define ADC121_VREF_MV 3300 +#define ADC121_MAX 4095 static const char *TAG = "evse_pilot"; -static int last_pilot_level = -1; -static uint32_t last_pwm_duty = 0; +typedef enum { + PILOT_MODE_DC_HIGH = 0, // +12V (nível alto) + PILOT_MODE_DC_LOW, // nível baixo / pilot desligado (dependente do hardware) + PILOT_MODE_PWM // PWM ativo +} pilot_mode_t; -static int adc_raw_to_mv(uint16_t raw) { - return (raw * ADC121_VREF_MV) / ADC121_MAX; +static pilot_mode_t s_mode = PILOT_MODE_DC_LOW; +static uint32_t last_pwm_duty = 0; + +// --------------------- +// Helpers internos +// --------------------- +static int adc_raw_to_mv(uint16_t raw) +{ + return (int)((raw * ADC121_VREF_MV) / ADC121_MAX); } +static int compare_uint16(const void *a, const void *b) +{ + uint16_t va = *(const uint16_t *)a; + uint16_t vb = *(const uint16_t *)b; + if (va < vb) return -1; + if (va > vb) return 1; + return 0; +} + +// --------------------- +// Inicialização PWM + ADC +// --------------------- void pilot_init(void) { + // Configura timer do PWM do Pilot (1 kHz) 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 + .speed_mode = PILOT_PWM_SPEED_MODE, + .timer_num = PILOT_PWM_TIMER, + .duty_resolution = PILOT_PWM_DUTY_RES, + .freq_hz = 1000, // 1 kHz (IEC 61851) + .clk_cfg = LEDC_AUTO_CLK }; ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); + // Canal do PWM no pino configurado em board_config 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 + .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)); + // Garante que começa parado e em idle baixo (pilot off) + ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0)); + s_mode = PILOT_MODE_DC_LOW; + last_pwm_duty = 0; + + // Inicializa driver do ADC121S021 adc121s021_dma_init(); } -void pilot_set_level(bool level) +// --------------------- +// Controlo do modo do Pilot +// --------------------- +void pilot_set_level(bool high) { - if (last_pilot_level == level) return; - last_pilot_level = level; + pilot_mode_t target = high ? PILOT_MODE_DC_HIGH : PILOT_MODE_DC_LOW; - ESP_LOGI(TAG, "Set level %d", level); - ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, level ? 1 : 0); + // Se já estiver no modo DC desejado e sem PWM ativo, ignora + if (s_mode == target && last_pwm_duty == 0) { + return; + } + + ESP_LOGI(TAG, "Pilot set DC level: %s", high ? "HIGH(+12V)" : "LOW/OFF"); + + // Para PWM e fixa o nível idle do GPIO + ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, high ? 1 : 0)); + + s_mode = target; last_pwm_duty = 0; } void pilot_set_amps(uint16_t amps) { - if (amps < 6 || amps > 80) { + 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) { + if (amps <= 51) + { duty_percent = (amps * 10) / 6; - } else { + } + else + { duty_percent = (amps * 10) / 25 + 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; + // Se já estiver em PWM com o mesmo duty, ignora + if (s_mode == PILOT_MODE_PWM && last_pwm_duty == duty) { + return; + } + + s_mode = PILOT_MODE_PWM; last_pwm_duty = duty; - ESP_LOGI(TAG, "Pilot set: %d A → %d/%d (≈ %d%% duty)", + ESP_LOGI(TAG, "Pilot set PWM: %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); + ESP_ERROR_CHECK(ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty)); + ESP_ERROR_CHECK(ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL)); } -bool pilot_get_state(void) { - return (last_pilot_level == 1) && (last_pwm_duty == 0); -} - -static int compare_int(const void *a, const void *b) { - return (*(const int *)a - *(const int *)b); +bool pilot_get_state(void) +{ + // "Alto" significa DC +12V (estado A). PWM não conta como DC high. + return (s_mode == PILOT_MODE_DC_HIGH); } +// --------------------- +// Medição do sinal de Pilot (PWM 1 kHz J1772) +// --------------------- 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; + uint16_t samples[NUM_PILOT_SAMPLES]; + int collected = 0; + int attempts = 0; - while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS) { - adc_sample = 0; - if (adc121s021_dma_get_sample(&adc_sample)) { + while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS) + { + uint16_t adc_sample; + + if (adc121s021_dma_get_sample(&adc_sample)) + { samples[collected++] = adc_sample; - esp_rom_delay_us(10); - } else { + esp_rom_delay_us(PILOT_SAMPLE_DELAY_US); + } + else + { esp_rom_delay_us(100); attempts++; } } - if (collected < NUM_PILOT_SAMPLES) { + 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; } - qsort(samples, collected, sizeof(int), compare_int); + // Ordena as amostras para eliminar extremos (ruído/espúrios) + qsort(samples, collected, sizeof(uint16_t), compare_uint16); int k = (collected * PILOT_EXTREME_PERCENT) / 100; - if (k == 0) k = 1; + if (k < 2) k = 2; // garante margem mínima + // descarta k/2 em cada lado (aprox. 10% total, mantendo simetria) int low_index = k / 2; - int high_index = collected - k + (k / 2); - if (high_index >= collected) high_index = collected - 1; + int high_index = collected - 1 - (k / 2); - int low_raw = samples[low_index]; - int high_raw = samples[high_index]; + if (low_index < 0) low_index = 0; + if (high_index >= collected) high_index = collected - 1; + if (high_index <= low_index) high_index = low_index; + + uint16_t low_raw = samples[low_index]; + uint16_t high_raw = samples[high_index]; int high_mv = adc_raw_to_mv(high_raw); int low_mv = adc_raw_to_mv(low_raw); + // Determina o nível positivo (+12, +9, +6, +3 ou <3 V) 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; + } + // Verifica se o nível negativo atinge -12 V (diodo presente, C/D válidos) *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); + ESP_LOGD(TAG, "Final: up_voltage=%d, down_voltage_n12=%d (high=%d mV, low=%d mV)", + *up_voltage, *down_voltage_n12, high_mv, low_mv); } diff --git a/components/evse/evse_session.c b/components/evse/evse_session.c index 7e80d12..6999554 100644 --- a/components/evse/evse_session.c +++ b/components/evse/evse_session.c @@ -7,22 +7,49 @@ #include "evse_events.h" #include "esp_event.h" #include "evse_limits.h" +#include "esp_timer.h" + +#define EVSE_EVENT_POST_TIMEOUT_MS 50 static const char *TAG = "evse_session"; static TickType_t session_start_tick = 0; -static uint32_t watt_seconds = 0; + +// Tempo real (microsegundos) +static int64_t session_start_us = 0; +static int64_t last_tick_us = 0; + +// Energia integrada com tempo real: soma de (W * us) +static uint64_t watt_microseconds = 0; + static evse_session_t last_session; static bool last_session_valid = false; static uint32_t session_counter = 0; static portMUX_TYPE session_mux = portMUX_INITIALIZER_UNLOCKED; +static void post_session_event(const evse_session_event_data_t *evt) +{ + esp_err_t err = esp_event_post( + EVSE_EVENTS, + EVSE_EVENT_SESSION, + evt, + sizeof(*evt), + portMAX_DELAY); + + if (err != ESP_OK) + { + ESP_LOGW(TAG, "esp_event_post(EVSE_EVENT_SESSION) failed: %s", esp_err_to_name(err)); + } +} + void evse_session_init(void) { portENTER_CRITICAL(&session_mux); session_start_tick = 0; - watt_seconds = 0; + session_start_us = 0; + last_tick_us = 0; + watt_microseconds = 0; last_session_valid = false; session_counter = 0; portEXIT_CRITICAL(&session_mux); @@ -31,55 +58,65 @@ void evse_session_init(void) void evse_session_start(void) { TickType_t tick = xTaskGetTickCount(); + int64_t now_us = esp_timer_get_time(); portENTER_CRITICAL(&session_mux); session_start_tick = tick; - watt_seconds = 0; + session_start_us = now_us; + last_tick_us = now_us; + watt_microseconds = 0; session_counter++; + uint32_t id = session_counter; portEXIT_CRITICAL(&session_mux); evse_set_limit_reached(false); - ESP_LOGI(TAG, "Session started at tick %u", (unsigned)tick); + ESP_LOGI(TAG, "Session started (id=%" PRIu32 ") tick=%u us=%" PRId64, + id, (unsigned)tick, now_us); evse_session_event_data_t evt = { .type = EVSE_SESSION_EVENT_STARTED, - .session_id = session_counter, + .session_id = id, .duration_s = 0, .energy_wh = 0, .avg_power_w = 0, .is_current = true, }; - - esp_event_post(EVSE_EVENTS, - EVSE_EVENT_SESSION, - &evt, - sizeof(evt), - portMAX_DELAY); + post_session_event(&evt); } void evse_session_end(void) { TickType_t start_tick; - uint32_t ws; + int64_t start_us; + uint64_t w_us; uint32_t id; + int64_t end_us = esp_timer_get_time(); + portENTER_CRITICAL(&session_mux); - if (session_start_tick == 0) { + if (session_start_tick == 0) + { portEXIT_CRITICAL(&session_mux); ESP_LOGW(TAG, "evse_session_end called without active session"); return; } + start_tick = session_start_tick; - ws = watt_seconds; + start_us = session_start_us; + w_us = watt_microseconds; id = session_counter; + session_start_tick = 0; + session_start_us = 0; + last_tick_us = 0; + watt_microseconds = 0; portEXIT_CRITICAL(&session_mux); - TickType_t now = xTaskGetTickCount(); - uint32_t duration_s = (now - start_tick) / configTICK_RATE_HZ; - uint32_t energy_wh = ws / 3600U; - uint32_t avg_power = duration_s > 0 ? ws / duration_s : 0; + uint32_t duration_s = (end_us > start_us) ? (uint32_t)((end_us - start_us) / 1000000LL) : 0; + uint32_t energy_wh = (uint32_t)(w_us / (3600ULL * 1000000ULL)); + uint64_t watt_seconds = (uint64_t)(w_us / 1000000ULL); + uint32_t avg_power = (duration_s > 0) ? (uint32_t)(watt_seconds / duration_s) : 0; portENTER_CRITICAL(&session_mux); last_session.start_tick = start_tick; @@ -91,9 +128,9 @@ void evse_session_end(void) portEXIT_CRITICAL(&session_mux); ESP_LOGI(TAG, - "Session ended: duration=%" PRIu32 " s, energy=%" PRIu32 + "Session ended (id=%" PRIu32 "): duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg_power=%" PRIu32 " W", - duration_s, energy_wh, avg_power); + id, duration_s, energy_wh, avg_power); evse_session_event_data_t evt = { .type = EVSE_SESSION_EVENT_FINISHED, @@ -103,21 +140,36 @@ void evse_session_end(void) .avg_power_w = avg_power, .is_current = false, }; - - esp_event_post(EVSE_EVENTS, - EVSE_EVENT_SESSION, - &evt, - sizeof(evt), - portMAX_DELAY); + post_session_event(&evt); } void evse_session_tick(void) { - uint32_t power_w = evse_meter_get_instant_power(); + // Potência instantânea pode ser negativa (ruído/overflow de sensor) -> clamp + int p = evse_meter_get_instant_power(); + uint32_t power_w = (p > 0) ? (uint32_t)p : 0; + + int64_t now_us = esp_timer_get_time(); portENTER_CRITICAL(&session_mux); - if (session_start_tick != 0) { - watt_seconds += power_w; + if (session_start_tick != 0) + { + if (last_tick_us == 0) + { + last_tick_us = now_us; + } + int64_t dt_us = now_us - last_tick_us; + if (dt_us > 0) + { + // Energia incremental: W * us (64-bit) + watt_microseconds += ((uint64_t)power_w * (uint64_t)dt_us); + last_tick_us = now_us; + } + else + { + // relógio não devia andar para trás; ignora + last_tick_us = now_us; + } } portEXIT_CRITICAL(&session_mux); } @@ -127,15 +179,19 @@ bool evse_session_get(evse_session_t *out) if (out == NULL) return false; - TickType_t start; - uint32_t ws; + TickType_t start_tick; + int64_t start_us; + uint64_t w_us; bool has_current; evse_session_t last_copy; bool last_valid; + int64_t now_us = esp_timer_get_time(); + portENTER_CRITICAL(&session_mux); - start = session_start_tick; - ws = watt_seconds; + start_tick = session_start_tick; + start_us = session_start_us; + w_us = watt_microseconds; has_current = (session_start_tick != 0); last_copy = last_session; last_valid = last_session_valid; @@ -143,12 +199,12 @@ bool evse_session_get(evse_session_t *out) if (has_current) { - TickType_t now = xTaskGetTickCount(); - uint32_t duration_s = (now - start) / configTICK_RATE_HZ; - uint32_t energy_wh = ws / 3600U; - uint32_t avg_power = duration_s > 0 ? ws / duration_s : 0; + uint32_t duration_s = (now_us > start_us) ? (uint32_t)((now_us - start_us) / 1000000LL) : 0; + uint32_t energy_wh = (uint32_t)(w_us / (3600ULL * 1000000ULL)); + uint64_t watt_seconds = (uint64_t)(w_us / 1000000ULL); + uint32_t avg_power = (duration_s > 0) ? (uint32_t)(watt_seconds / duration_s) : 0; - out->start_tick = start; + out->start_tick = start_tick; out->duration_s = duration_s; out->energy_wh = energy_wh; out->avg_power_w = avg_power; diff --git a/components/evse/evse_state.c b/components/evse/evse_state.c index 651c454..beab922 100755 --- a/components/evse/evse_state.c +++ b/components/evse/evse_state.c @@ -5,150 +5,203 @@ #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" #include "esp_log.h" +#include "esp_event.h" -// ========================= -// Internal State Variables -// ========================= +#define EVSE_EVENT_POST_TIMEOUT_MS 50 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; -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; + case EVSE_STATE_B1: + case EVSE_STATE_B2: + return EVSE_STATE_EVENT_WAITING; + + case EVSE_STATE_C1: + case EVSE_STATE_C2: + case EVSE_STATE_D1: + case EVSE_STATE_D2: + 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 -// ========================= +static void post_evse_event(evse_event_id_t id, const void *data, size_t len) +{ + esp_err_t err = esp_event_post( + EVSE_EVENTS, + id, + data, + len, + portMAX_DELAY); -void evse_set_state(evse_state_t new_state) { + if (err != ESP_OK) + { + ESP_LOGW(TAG, "esp_event_post(id=%d) failed: %s", (int)id, esp_err_to_name(err)); + } +} + +bool evse_state_is_charging(evse_state_t state) +{ + // “charging” == energia efetiva (relé ON) + return (state == EVSE_STATE_C2 || state == EVSE_STATE_D2); +} + +bool evse_state_is_power_flowing(evse_state_t state) +{ + return evse_state_is_charging(state); +} + +bool evse_state_is_requesting(evse_state_t state) +{ + // EV pediu carga mas o relé ainda está OFF + return (state == EVSE_STATE_C1 || state == EVSE_STATE_D1); +} + +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) +{ + // Sessão lógica: “autorizado/pronto” ou “a pedir/a fornecer energia” + return (state == EVSE_STATE_B2 || + state == EVSE_STATE_C1 || state == EVSE_STATE_C2 || + state == EVSE_STATE_D1 || state == EVSE_STATE_D2); +} + +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; + 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; - } + prev_state = current_state; + + if (new_state != current_state) + { + + // Sessão começa quando entra em energia (relé ON) + if (evse_state_is_power_flowing(new_state) && !evse_state_is_power_flowing(prev_state)) + { + start_session = true; + } + // Sessão termina quando sai de energia + else if (!evse_state_is_power_flowing(new_state) && evse_state_is_power_flowing(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) { + // Fora da região crítica + if (start_session) + { evse_session_start(); } - if (end_session) { + 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); + if (changed) + { + ESP_LOGI(TAG, "State changed: %s → %s", + evse_state_to_str(prev_state), + evse_state_to_str(new_state)); 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); + .state = map_state_to_event(new_state)}; + post_evse_event(EVSE_EVENT_STATE_CHANGED, &evt, sizeof(evt)); } } - - -evse_state_t evse_get_state(void) { +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"; +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) { +void evse_state_init(void) +{ portENTER_CRITICAL(&state_mux); current_state = EVSE_STATE_A; - is_authorized = true; + is_authorized = false; portEXIT_CRITICAL(&state_mux); - ESP_LOGI("EVSE_STATE", "Initialized in state: %s", evse_state_to_str(current_state)); + ESP_LOGI(TAG, "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); + .state = map_state_to_event(current_state)}; + post_evse_event(EVSE_EVENT_INIT, &evt, sizeof(evt)); } -void evse_state_tick(void) { - // Placeholder for future state logic +void evse_state_tick(void) +{ + // placeholder } -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) { +void evse_state_set_authorized(bool authorized) +{ portENTER_CRITICAL(&state_mux); is_authorized = authorized; portEXIT_CRITICAL(&state_mux); } -bool evse_state_get_authorized(void) { +bool evse_state_get_authorized(void) +{ portENTER_CRITICAL(&state_mux); bool result = is_authorized; portEXIT_CRITICAL(&state_mux); diff --git a/components/evse/include/evse_config.h b/components/evse/include/evse_config.h index 6ac270f..fe91b40 100755 --- a/components/evse/include/evse_config.h +++ b/components/evse/include/evse_config.h @@ -15,11 +15,9 @@ extern "C" { // 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 @@ -31,23 +29,20 @@ extern "C" { esp_err_t evse_config_init(void); void evse_check_defaults(void); -// Corrente de carregamento +// Corrente máxima de hardware (fixa) uint8_t evse_get_max_charging_current(void); -esp_err_t evse_set_max_charging_current(uint8_t value); +// Corrente configurável (persistida) <= max hardware 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); - +// Corrente runtime (RAM) <= max hardware (load balancer pode alterar) void evse_set_runtime_charging_current(uint16_t value); uint16_t evse_get_runtime_charging_current(void); +// Socket outlet +bool evse_get_socket_outlet(void); +esp_err_t evse_set_socket_outlet(bool socket_outlet); // RCM bool evse_is_rcm(void); diff --git a/components/evse/include/evse_error.h b/components/evse/include/evse_error.h index a81d8e2..b97f147 100755 --- a/components/evse/include/evse_error.h +++ b/components/evse/include/evse_error.h @@ -1,4 +1,3 @@ -// === Início de: components/evse/include/evse_error.h === #ifndef EVSE_ERROR_H #define EVSE_ERROR_H @@ -6,21 +5,23 @@ #include #include "evse_pilot.h" -// Bits que auto-limpam passado um timeout -#define EVSE_ERR_AUTO_CLEAR_BITS ( \ - EVSE_ERR_DIODE_SHORT_BIT | \ - EVSE_ERR_TEMPERATURE_HIGH_BIT | \ - EVSE_ERR_RCM_TRIGGERED_BIT) +// ---------------------------------------------------- +// Holdoff interno pós-erro (sem expor "cooldown" ao resto) +// ---------------------------------------------------- +// Após TODOS os erros reais desaparecerem (raw_bits == 0), +// o módulo mantém o erro "visível" durante este tempo. +// Durante este período, evse_get_error() continua != 0. +#define EVSE_ERROR_COOLDOWN_MS 60000 // 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) +#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); @@ -30,7 +31,7 @@ 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 +// Leitura e controle de erros (estado "visível" com holdoff) uint32_t evse_get_error(void); void evse_error_set(uint32_t bitmask); void evse_error_clear(uint32_t bitmask); @@ -40,12 +41,9 @@ uint32_t evse_error_get_bits(void); // ---------------------------------------------------- // Semântica sticky: flag "todos erros limpos" +// (fica true quando o erro visível chega a 0; pode ser útil para UI/logs) // ---------------------------------------------------- -// Fica true quando TODOS os erros são limpos. -// Volta a false assim que qualquer erro novo aparece. -// Permanece true até o consumidor limpar explicitamente. bool evse_error_cleared_flag(void); void evse_error_reset_flag(void); #endif // EVSE_ERROR_H -// === Fim de: components/evse/include/evse_error.h === diff --git a/components/evse/include/evse_events.h b/components/evse/include/evse_events.h index 8d9f907..3bb590f 100755 --- a/components/evse/include/evse_events.h +++ b/components/evse/include/evse_events.h @@ -16,6 +16,7 @@ typedef enum { EVSE_EVENT_ENABLE_UPDATED, EVSE_EVENT_AVAILABLE_UPDATED, EVSE_EVENT_SESSION, + EVSE_EVENT_ERROR_CHANGED, } evse_event_id_t; // ----------------- @@ -43,35 +44,41 @@ typedef enum { typedef struct { evse_session_event_type_t type; ///< STARTED / FINISHED - // campos básicos da sessão, em tipos simples: - uint32_t session_id; ///< opcional, se tiveres um ID - uint32_t duration_s; ///< duração em segundos (0 no STARTED) - uint32_t energy_wh; ///< energia em Wh (0 no STARTED) - uint32_t avg_power_w; ///< potência média em W (0 no STARTED) + uint32_t session_id; + uint32_t duration_s; + uint32_t energy_wh; + uint32_t avg_power_w; - bool is_current; ///< true se ainda estiver em curso + bool is_current; } evse_session_event_data_t; - // ----------------- // Eventos de CONFIG // ----------------- 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 + bool charging; + float hw_max_current; + float runtime_current; + int64_t timestamp_us; } evse_config_event_data_t; -// Eventos simples e específicos typedef struct { - bool enabled; // novo estado de enabled - int64_t timestamp_us; // epoch micros + bool enabled; + int64_t timestamp_us; } evse_enable_event_data_t; typedef struct { - bool available; // novo estado de available - int64_t timestamp_us; // epoch micros + bool available; + int64_t timestamp_us; } evse_available_event_data_t; +// ----------------- +// Eventos de ERRO +// ----------------- +typedef struct { + uint32_t error_bits; ///< estado atual (todos os bits de erro) + uint32_t changed_mask; ///< bits que mudaram nesta notificação + int64_t timestamp_us; ///< esp_timer_get_time() +} evse_error_event_data_t; + #endif // EVSE_EVENTS_H diff --git a/components/evse/include/evse_pilot.h b/components/evse/include/evse_pilot.h index d541183..ebdb2e3 100755 --- a/components/evse/include/evse_pilot.h +++ b/components/evse/include/evse_pilot.h @@ -2,65 +2,43 @@ #define PILOT_H_ #ifdef __cplusplus -extern "C" { +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; + typedef enum + { + PILOT_VOLTAGE_12, + PILOT_VOLTAGE_9, + PILOT_VOLTAGE_6, + PILOT_VOLTAGE_3, + PILOT_VOLTAGE_1 + } pilot_voltage_t; -/** - * @brief Inicializa o driver do sinal Pilot - */ -void pilot_init(void); + 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 Define o pilot em modo DC. + * + * @param high true = nível alto (+12V) + * false = nível baixo (-12V) + */ + void pilot_set_level(bool high); -/** - * @brief Ativa o PWM do Pilot com corrente limitada - * - * @param amps Corrente em ampères (ex: 16 = 16A) - */ -void pilot_set_amps(uint16_t amps); + 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); + 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); + 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; + typedef struct + { + uint16_t high_mv; + uint16_t low_mv; + } pilot_voltage_cache_t; #ifdef __cplusplus } diff --git a/components/evse/include/evse_session.h b/components/evse/include/evse_session.h index 0d84b20..a8bb27e 100644 --- a/components/evse/include/evse_session.h +++ b/components/evse/include/evse_session.h @@ -1,9 +1,3 @@ -/* - * 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 @@ -15,39 +9,23 @@ * @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 + TickType_t start_tick; ///< tick when session began (debug/trace) + uint32_t duration_s; ///< total duration in seconds (tempo real) + uint32_t energy_wh; ///< total energy consumed in Wh (tempo real) 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 + * @brief Periodic tick: called (e.g., each 1s) to accumulate energy from instant power. + * Implementação usa esp_timer (não assume 1s exato). */ 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 diff --git a/components/evse/include/evse_state.h b/components/evse/include/evse_state.h index 3c8455f..f336ce7 100755 --- a/components/evse/include/evse_state.h +++ b/components/evse/include/evse_state.h @@ -6,90 +6,68 @@ #include "evse_events.h" #ifdef __cplusplus -extern "C" { +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; -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 + void evse_state_init(void); + void evse_state_tick(void); -// ============================ -// Initialization -// ============================ + // State Access & Control + evse_state_t evse_get_state(void); + void evse_set_state(evse_state_t state); + const char *evse_state_to_str(evse_state_t state); -/** - * @brief Initializes the EVSE state machine and default state. - */ -void evse_state_init(void); + // --------------------------- + // State Evaluation Helpers + // --------------------------- -/** - * @brief Periodic tick for state handling (optional hook). - */ -void evse_state_tick(void); + /** + * @brief True se existe uma sessão "lógica" ativa (carro ligado e autorizado/pronto ou a carregar). + * Inclui B2, C1/C2, D1/D2. + */ + bool evse_state_is_session(evse_state_t state); -// ============================ -// State Access & Control -// ============================ + /** + * @brief True se o EVSE está a fornecer energia (relé ON). + * Estados com energia: C2 e D2. + * + * Nota: isto substitui a antiga interpretação “C1/C2”. + */ + bool evse_state_is_charging(evse_state_t state); -/** - * @brief Returns the current EVSE state. - */ -evse_state_t evse_get_state(void); + /** + * @brief True se o EV pediu carga mas o relé ainda está OFF (C1/D1). + */ + bool evse_state_is_requesting(evse_state_t state); -/** - * @brief Sets the current EVSE state and emits a change event if needed. - */ -void evse_set_state(evse_state_t state); + /** + * @brief True se há fluxo de energia (alias explícito para charging). + */ + bool evse_state_is_power_flowing(evse_state_t state); -/** - * @brief Converts the state enum into a human-readable string. - */ -const char* evse_state_to_str(evse_state_t state); + /** + * @brief True se o EV está fisicamente ligado (B1 e além). + */ + bool evse_state_is_plugged(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); + // Authorization Control + void evse_state_set_authorized(bool authorized); + bool evse_state_get_authorized(void); #ifdef __cplusplus } diff --git a/components/evse_link/CMakeLists.txt b/components/evse_link/CMakeLists.txt index 413b6e8..bd5dd50 100755 --- a/components/evse_link/CMakeLists.txt +++ b/components/evse_link/CMakeLists.txt @@ -9,8 +9,8 @@ set(srcs idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" - PRIV_REQUIRES driver esp_timer nvs_flash - REQUIRES config evse loadbalancer) + PRIV_REQUIRES driver esp_timer + REQUIRES config evse loadbalancer storage_service) diff --git a/components/evse_link/src/evse_link.c b/components/evse_link/src/evse_link.c index c67d6aa..0976a48 100755 --- a/components/evse_link/src/evse_link.c +++ b/components/evse_link/src/evse_link.c @@ -2,19 +2,23 @@ #include "evse_link.h" #include "evse_link_framing.h" + #include "driver/uart.h" -#include "nvs.h" #include "esp_log.h" #include "freertos/task.h" -#include "freertos/semphr.h" + +#include +#include + +#include "storage_service.h" static const char *TAG = "evse_link"; -// NVS keys +// Storage keys #define _NVS_NAMESPACE "evse_link" -#define _NVS_MODE_KEY "mode" -#define _NVS_ID_KEY "self_id" -#define _NVS_ENABLED_KEY "enabled" +#define _KEY_MODE "mode" +#define _KEY_SELF_ID "self_id" +#define _KEY_ENABLED "enabled" // UART parameters #define UART_PORT UART_NUM_2 @@ -37,9 +41,7 @@ static void framing_rx_cb(uint8_t src, uint8_t dest, { ESP_LOGD(TAG, "framing_rx_cb: src=0x%02X dest=0x%02X len=%u", src, dest, len); if (_rx_cb) - { _rx_cb(src, dest, payload, len); - } } // Register protocol-level Rx callback @@ -48,87 +50,117 @@ void evse_link_register_rx_cb(evse_link_rx_cb_t cb) _rx_cb = cb; } -// Load config from NVS -enum -{ - EV_OK = ESP_OK -}; +// Load config from storage_service (NVS-backed) static void load_link_config(void) { - nvs_handle_t handle; - if (nvs_open(_NVS_NAMESPACE, NVS_READONLY, &handle) != EV_OK) - { - ESP_LOGW(TAG, "NVS open failed, using defaults"); - return; - } - uint8_t mode, id, en; - if (nvs_get_u8(handle, _NVS_MODE_KEY, &mode) == EV_OK && - (mode == EVSE_LINK_MODE_MASTER || mode == EVSE_LINK_MODE_SLAVE)) - { - _mode = (evse_link_mode_t)mode; - } - if (nvs_get_u8(handle, _NVS_ID_KEY, &id) == EV_OK) - { - _self_id = id; - } - if (nvs_get_u8(handle, _NVS_ENABLED_KEY, &en) == EV_OK) - { - _enabled = (en != 0); - } - nvs_close(handle); -} + uint8_t u8 = 0; -// Save config to NVS -static void save_link_config(void) -{ - nvs_handle_t handle; - if (nvs_open(_NVS_NAMESPACE, NVS_READWRITE, &handle) == EV_OK) + // mode + esp_err_t err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_MODE, &u8, pdMS_TO_TICKS(500)); + if (err == ESP_OK && (u8 == (uint8_t)EVSE_LINK_MODE_MASTER || u8 == (uint8_t)EVSE_LINK_MODE_SLAVE)) { - 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); + _mode = (evse_link_mode_t)u8; } else { - ESP_LOGE(TAG, "Failed to save NVS"); + // default + persist + _mode = EVSE_LINK_MODE_MASTER; + (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode); + ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER (persisted async)", + esp_err_to_name(err)); } + + // self_id + err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_SELF_ID, &u8, pdMS_TO_TICKS(500)); + if (err == ESP_OK) + { + _self_id = u8; + } + else + { + _self_id = 0x01; + (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id); + ESP_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X (persisted async)", + esp_err_to_name(err), _self_id); + } + + // enabled + err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_ENABLED, &u8, pdMS_TO_TICKS(500)); + if (err == ESP_OK && u8 <= 1) + { + _enabled = (u8 != 0); + } + else + { + _enabled = false; + (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, 0); + ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false (persisted async)", + esp_err_to_name(err)); + } +} + +// Save config to storage_service (debounced) +static void save_link_config(void) +{ + // Debounced writes: não bloqueia e reduz desgaste + (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode); + (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id); + (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, _enabled ? 1 : 0); + + // opcional: se quiseres persistência imediata em configurações “críticas” + // (void)storage_flush_async(); } // Getters/setters void evse_link_set_mode(evse_link_mode_t m) { + if (m != EVSE_LINK_MODE_MASTER && m != EVSE_LINK_MODE_SLAVE) + { + ESP_LOGW(TAG, "Invalid link mode: %d", (int)m); + return; + } + if (_mode == m) + return; + _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) { + if (_self_id == id) + return; _self_id = id; save_link_config(); } + uint8_t evse_link_get_self_id(void) { return _self_id; } + void evse_link_set_enabled(bool en) { + if (_enabled == en) + return; _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) { + (void)arg; uint8_t buf[UART_RX_BUF_SIZE]; + while (true) { int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000)); if (len > 0) { for (int i = 0; i < len; ++i) - { evse_link_recv_byte(buf[i]); - } } } } @@ -136,11 +168,22 @@ static void evse_link_rx_task(void *arg) // Initialize EVSE-Link component void evse_link_init(void) { - load_link_config(); + // garante storage disponível + esp_err_t se = storage_service_init(); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "storage_service_init failed: %s (using defaults in RAM)", esp_err_to_name(se)); + // segue com defaults em RAM + } + else + { + 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; @@ -149,17 +192,13 @@ void evse_link_init(void) evse_link_framing_register_cb(framing_rx_cb); // 2) start RX task - xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL); + xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 3, NULL); // 3) delegate to master or slave if (_mode == EVSE_LINK_MODE_MASTER) - { evse_link_master_init(); - } else - { evse_link_slave_init(); - } } // Send a frame (delegates to framing module) @@ -167,6 +206,7 @@ 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); } diff --git a/components/led/src/led.c b/components/led/src/led.c index bede749..57ca936 100755 --- a/components/led/src/led.c +++ b/components/led/src/led.c @@ -13,7 +13,7 @@ #include "evse_state.h" #include "ledc_driver.h" -#define BLOCK_TIME pdMS_TO_TICKS(10) +#define BLOCK_TIME portMAX_DELAY static const char *TAG = "led"; @@ -350,7 +350,7 @@ static void evse_led_event_handler(void *arg, esp_event_base_t base, int32_t id, const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data; - ESP_LOGI(TAG, "EVSE State Changed: state=%d", evt->state); + ESP_LOGD(TAG, "EVSE State Changed: state=%d", evt->state); // Atualiza o estado base current_state_mode = evt->state; @@ -378,7 +378,7 @@ static void evse_session_led_event_handler(void *arg, esp_event_base_t base, int const evse_session_event_data_t *evt = (const evse_session_event_data_t *)data; - ESP_LOGI(TAG, + ESP_LOGD(TAG, "EVSE Session Event: type=%d, id=%" PRIu32 ", duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg=%" PRIu32 " W, current=%d", (int)evt->type, diff --git a/components/loadbalancer/CMakeLists.txt b/components/loadbalancer/CMakeLists.txt index 90de35a..e7c2516 100755 --- a/components/loadbalancer/CMakeLists.txt +++ b/components/loadbalancer/CMakeLists.txt @@ -4,5 +4,4 @@ set(srcs idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" - PRIV_REQUIRES nvs_flash REQUIRES esp_event esp_timer meter_manager evse) diff --git a/components/loadbalancer/include/loadbalancer_events.h b/components/loadbalancer/include/loadbalancer_events.h index ffbe8c3..913a097 100644 --- a/components/loadbalancer/include/loadbalancer_events.h +++ b/components/loadbalancer/include/loadbalancer_events.h @@ -1,17 +1,26 @@ +// components/loadbalancer/include/loadbalancer_events.h #pragma once + #include "esp_event.h" #include #include #include "esp_timer.h" +#ifdef __cplusplus +extern "C" { +#endif + ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); typedef enum { - LOADBALANCER_EVENT_INIT, + LOADBALANCER_EVENT_INIT = 0, LOADBALANCER_EVENT_STATE_CHANGED, LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT, + + // IMPORTANT: eventos separados e payloads diferentes LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + LOADBALANCER_EVENT_SLAVE_STATUS } loadbalancer_event_id_t; @@ -20,18 +29,18 @@ typedef struct { int64_t timestamp_us; } loadbalancer_state_event_t; -// (opcional) typedef struct { float limit; int64_t timestamp_us; } loadbalancer_global_limit_event_t; +// MASTER: NÃO tem slave_id typedef struct { - uint8_t slave_id; uint16_t max_current; int64_t timestamp_us; } loadbalancer_master_limit_event_t; +// SLAVE: tem slave_id typedef struct { uint8_t slave_id; uint16_t max_current; @@ -39,9 +48,13 @@ typedef struct { } loadbalancer_slave_limit_event_t; typedef struct { - uint8_t slave_id; // ID do slave que reportou - bool charging; // Status de carregamento - float hw_max_current; // Limite máximo de corrente do hardware informado - float runtime_current; // Corrente atual de carregamento (A) - int64_t timestamp_us; // Momento em que o status foi coletado -} loadbalancer_slave_status_event_t; \ No newline at end of file + uint8_t slave_id; + bool charging; + float hw_max_current; + float runtime_current; + int64_t timestamp_us; +} loadbalancer_slave_status_event_t; + +#ifdef __cplusplus +} +#endif diff --git a/components/loadbalancer/src/loadbalancer.c b/components/loadbalancer/src/loadbalancer.c index 4be381f..04089f0 100755 --- a/components/loadbalancer/src/loadbalancer.c +++ b/components/loadbalancer/src/loadbalancer.c @@ -1,18 +1,25 @@ +// components/loadbalancer/src/loadbalancer.c #include "loadbalancer.h" #include "loadbalancer_events.h" + #include "esp_event.h" #include "esp_log.h" +#include "esp_timer.h" +#include "esp_err.h" + #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" + #include "input_filter.h" -#include "nvs_flash.h" -#include "nvs.h" -#include #include "meter_events.h" #include "evse_events.h" -#include "math.h" -#include // Necessário para PRIu64 + +#include "storage_service.h" + +#include +#include +#include #ifndef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) @@ -20,23 +27,18 @@ 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 +#define MAX_CHARGING_CURRENT_LIMIT 32 + +#define MIN_GRID_CURRENT_LIMIT 6 +#define MAX_GRID_CURRENT_LIMIT 100 -// Pequena tolerância para considerar "sem margem" #define AVAILABLE_EPS 1.0f +#define LB_SUSPEND_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT - 1.0f) +#define LB_RESUME_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT + 1.0f) -// Histerese de suspensão / retoma (em torno dos 6A) -#define LB_SUSPEND_THRESHOLD 5.0f // abaixo disto -> suspende -#define LB_RESUME_THRESHOLD 7.0f // acima disto -> pode retomar +#define GRID_METER_TIMEOUT_US (120LL * 1000000LL) -// Timeout para perda de medição de GRID (fail-safe) -#define GRID_METER_TIMEOUT_US (10LL * 1000000LL) // 30 segundos - -// Parâmetros static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; static bool loadbalancer_enabled = false; @@ -45,56 +47,66 @@ static float evse_current = 0.0f; static input_filter_t grid_filter; static input_filter_t evse_filter; -static int64_t last_grid_timestamp_us = 0; // última atualização de medição GRID +static int64_t last_grid_timestamp_us = 0; #define MAX_SLAVES 255 #define CONNECTOR_COUNT (MAX_SLAVES + 1) -// Proteção simples de concorrência static SemaphoreHandle_t lb_mutex = NULL; -// Estrutura unificada para master e slaves typedef struct { - uint8_t id; // 0xFF = master, 0..MAX_SLAVES-1 = slave + uint8_t id; // para slaves: 0..254 ; para master não usado externamente bool is_master; bool charging; float hw_max_current; float runtime_current; - int64_t timestamp; // microssegundos (última métrica EVSE/slave) + int64_t timestamp; bool online; - float assigned; // limite calculado pelo LB - int64_t started_us; // início da sessão de carregamento (para prioridade) - uint16_t last_limit; // último max_current enviado - bool suspended_by_lb; // flag de suspensão por LB (para histerese) + float assigned; + int64_t started_us; + uint16_t last_limit; + bool suspended_by_lb; } evse_connector_t; static evse_connector_t connectors[CONNECTOR_COUNT]; -const int64_t METRICS_TIMEOUT_US = 60 * 1000000; // 60 segundos +const int64_t METRICS_TIMEOUT_US = 60LL * 1000000LL; + +// Storage namespace/keys (mantém os mesmos nomes para compatibilidade) +#define LB_NS "loadbalancing" +#define LB_KEY_MAX_GRID "max_grid_curr" // u8 +#define LB_KEY_ENABLED "enabled" // u8 (0/1) + +static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); } + +static void input_filter_reset(input_filter_t *filter) +{ + if (!filter) + return; + filter->value = 0.0f; + filter->initialized = 0; +} -// Helper: inicializa array de conectores static void init_connectors(void) { - // master em índice 0 connectors[0] = (evse_connector_t){ - .id = 0xFF, + .id = 0, .is_master = true, .charging = false, .hw_max_current = MAX_CHARGING_CURRENT_LIMIT, .runtime_current = 0, .timestamp = 0, - .online = false, + .online = true, .assigned = 0.0f, .started_us = 0, .last_limit = 0, .suspended_by_lb = false}; - // slaves em 1..CONNECTOR_COUNT-1 for (int i = 1; i < CONNECTOR_COUNT; i++) { connectors[i] = (evse_connector_t){ - .id = (uint8_t)(i - 1), + .id = (uint8_t)(i - 1), // slaves 0..254 .is_master = false, .charging = false, .hw_max_current = 0.0f, @@ -108,26 +120,23 @@ static void init_connectors(void) } } -// --- Helpers --- -static void input_filter_reset(input_filter_t *filter) -{ - filter->value = 0.0f; -} - -// Callback de status de slave static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id, void *data) { + (void)handler_arg; + if (base != LOADBALANCER_EVENTS || id != LOADBALANCER_EVENT_SLAVE_STATUS || !data) + return; + const loadbalancer_slave_status_event_t *status = (const loadbalancer_slave_status_event_t *)data; if (status->slave_id >= MAX_SLAVES) { - ESP_LOGW(TAG, "Invalid slave_id %d", status->slave_id); + ESP_LOGW(TAG, "Invalid slave_id %u", (unsigned)status->slave_id); return; } - int idx = status->slave_id + 1; // slaves começam no índice 1 + int idx = (int)status->slave_id + 1; - bool was_charging; + bool was_charging = false; if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); @@ -140,44 +149,40 @@ static void on_slave_status(void *handler_arg, esp_event_base_t base, int32_t id connectors[idx].timestamp = esp_timer_get_time(); connectors[idx].online = true; - // Se começou agora a carregar, marca início da sessão if (status->charging && !was_charging) { connectors[idx].started_us = connectors[idx].timestamp; - connectors[idx].suspended_by_lb = false; // reset + connectors[idx].suspended_by_lb = false; } if (lb_mutex) xSemaphoreGive(lb_mutex); ESP_LOGI(TAG, - "Slave %d status: charging=%d hw_max_current=%.1fA runtime_current=%.2fA", - status->slave_id, status->charging, + "Slave %u status: charging=%d hw_max_current=%.1fA runtime_current=%.2fA", + (unsigned)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) +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; + (void)handler_arg; + if (base != EVSE_EVENTS || id != EVSE_EVENT_CONFIG_UPDATED || !event_data) + return; - int idx = 0; // MASTER INDICE 0 + const evse_config_event_data_t *evt = (const evse_config_event_data_t *)event_data; if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - 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[0].charging = evt->charging; + connectors[0].hw_max_current = evt->hw_max_current; + connectors[0].runtime_current = evt->runtime_current; + connectors[0].timestamp = esp_timer_get_time(); + connectors[0].online = true; if (!evt->charging) - { - connectors[idx].suspended_by_lb = false; - } + connectors[0].suspended_by_lb = false; if (lb_mutex) xSemaphoreGive(lb_mutex); @@ -186,30 +191,25 @@ static void on_evse_config_event(void *handler_arg, 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) + (void)handler_arg; + (void)base; + + if (id != METER_EVENT_DATA_READY || !event_data) 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]; - } - } float max_vrms = evt->vrms[0]; for (int i = 1; i < 3; ++i) - { if (evt->vrms[i] > max_vrms) - { max_vrms = evt->vrms[i]; - } - } if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); @@ -218,28 +218,28 @@ static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t { grid_current = input_filter_update(&grid_filter, max_irms); last_grid_timestamp_us = esp_timer_get_time(); - ESP_LOGI(TAG, "GRID IRMS (filtered): %.2f A", grid_current); - ESP_LOGI(TAG, "GRID VRMS: %.2f V", max_vrms); + ESP_LOGD(TAG, "GRID IRMS (filtered): %.2f A VRMS: %.2f V", grid_current, max_vrms); } else if (evt->source && strcmp(evt->source, "EVSE") == 0) { evse_current = input_filter_update(&evse_filter, max_irms); - ESP_LOGI(TAG, "EVSE IRMS (filtered): %.2f A", evse_current); + ESP_LOGD(TAG, "EVSE IRMS (filtered): %.2f A", evse_current); } else { - ESP_LOGW(TAG, "Unknown meter event source: %s", evt->source); + ESP_LOGW(TAG, "Unknown meter event source: %s", evt->source ? evt->source : "(null)"); } if (lb_mutex) xSemaphoreGive(lb_mutex); } -static void loadbalancer_evse_event_handler(void *handler_arg, - esp_event_base_t base, - int32_t id, - void *event_data) +static void loadbalancer_evse_event_handler(void *handler_arg, esp_event_base_t base, int32_t id, void *event_data) { + (void)handler_arg; + if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || !event_data) + return; + const evse_state_event_data_t *evt = (const evse_state_event_data_t *)event_data; if (lb_mutex) @@ -249,17 +249,13 @@ static void loadbalancer_evse_event_handler(void *handler_arg, { case EVSE_STATE_EVENT_IDLE: case EVSE_STATE_EVENT_WAITING: - ESP_LOGI(TAG, "Local EVSE is %s - vehicle %sconnected / not charging", - evt->state == EVSE_STATE_EVENT_IDLE ? "IDLE" : "WAITING", - evt->state == EVSE_STATE_EVENT_IDLE ? "dis" : ""); connectors[0].charging = false; - connectors[0].online = true; // master está sempre online + connectors[0].online = true; connectors[0].suspended_by_lb = false; break; case EVSE_STATE_EVENT_CHARGING: { - ESP_LOGI(TAG, "Local EVSE is CHARGING - resetting filters"); grid_current = 0.0f; evse_current = 0.0f; input_filter_reset(&grid_filter); @@ -280,14 +276,12 @@ static void loadbalancer_evse_event_handler(void *handler_arg, } 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; connectors[0].suspended_by_lb = false; break; default: - ESP_LOGW(TAG, "Unknown EVSE state: %d", evt->state); break; } @@ -295,86 +289,110 @@ static void loadbalancer_evse_event_handler(void *handler_arg, xSemaphoreGive(lb_mutex); } -// --- Config persistência --- -static esp_err_t loadbalancer_load_config() +// --------- Config load/save via storage_service --------- + +static esp_err_t loadbalancer_load_config(void) { - nvs_handle_t handle; - esp_err_t err = nvs_open("loadbalancing", NVS_READWRITE, &handle); - if (err != ESP_OK) - return err; + // garante storage iniciado + esp_err_t se = storage_service_init(); + if (se != ESP_OK) + return se; - bool needs_commit = false; - uint8_t temp_u8; + esp_err_t err; + bool needs_flush = false; - err = nvs_get_u8(handle, "max_grid_curr", &temp_u8); + uint8_t temp_u8 = 0; + + // max_grid_curr + err = storage_get_u8_sync(LB_NS, LB_KEY_MAX_GRID, &temp_u8, TO_TICKS_MS(800)); if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT) + { max_grid_current = temp_u8; + } else { max_grid_current = MAX_GRID_CURRENT_LIMIT; - nvs_set_u8(handle, "max_grid_curr", max_grid_current); - needs_commit = true; + (void)storage_set_u8_async(LB_NS, LB_KEY_MAX_GRID, max_grid_current); + needs_flush = true; + ESP_LOGW(TAG, "Invalid/missing max_grid_curr (%s) -> default=%u (persisted)", + esp_err_to_name(err), (unsigned)max_grid_current); } - err = nvs_get_u8(handle, "enabled", &temp_u8); + // enabled + err = storage_get_u8_sync(LB_NS, LB_KEY_ENABLED, &temp_u8, TO_TICKS_MS(800)); if (err == ESP_OK && temp_u8 <= 1) + { loadbalancer_enabled = (temp_u8 != 0); + } else { loadbalancer_enabled = false; - nvs_set_u8(handle, "enabled", 0); - needs_commit = true; + (void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0); + needs_flush = true; + ESP_LOGW(TAG, "Invalid/missing enabled (%s) -> default=false (persisted)", + esp_err_to_name(err)); + } + + if (needs_flush) + { + // determinístico no boot + (void)storage_flush_sync(TO_TICKS_MS(2000)); } - if (needs_commit) - nvs_commit(handle); - nvs_close(handle); return ESP_OK; } -// --- API --- +static void persist_lb_enabled(bool enabled) +{ + (void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, enabled ? 1 : 0); + // debounced por defeito +} + +static void persist_max_grid_current(uint8_t value) +{ + (void)storage_set_u8_async(LB_NS, LB_KEY_MAX_GRID, value); + // debounced por defeito +} + void loadbalancer_set_enabled(bool enabled) { - nvs_handle_t handle; - if (nvs_open("loadbalancing", NVS_READWRITE, &handle) == ESP_OK) + // Só persiste/propaga se mudou + if (enabled == loadbalancer_enabled) { - nvs_set_u8(handle, "enabled", enabled ? 1 : 0); - nvs_commit(handle); - nvs_close(handle); + return; } + loadbalancer_enabled = enabled; + persist_lb_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); + (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_STATE_CHANGED, + &evt, sizeof(evt), portMAX_DELAY); } esp_err_t load_balancing_set_max_grid_current(uint8_t value) { 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); + + // Só persiste se mudou + if (value == max_grid_current) + { + return ESP_OK; + } + max_grid_current = value; + persist_max_grid_current(value); return ESP_OK; } -uint8_t load_balancing_get_max_grid_current(void) -{ - return max_grid_current; -} +uint8_t load_balancing_get_max_grid_current(void) { return max_grid_current; } +bool loadbalancer_is_enabled(void) { return loadbalancer_enabled; } -bool loadbalancer_is_enabled(void) -{ - return loadbalancer_enabled; -} - -// --- Task principal --- void loadbalancer_task(void *param) { + (void)param; + while (true) { if (!loadbalancer_is_enabled()) @@ -387,110 +405,66 @@ void loadbalancer_task(void *param) int active_cnt = 0; int64_t now = esp_timer_get_time(); - // --- Atualiza estado online e conta ativos --- if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); for (int i = 0; i < CONNECTOR_COUNT; i++) { - // --- Master nunca pode ficar offline --- if (connectors[i].is_master) { connectors[i].online = true; - - ESP_LOGI(TAG, "Connector[%d] ONLINE (MASTER, charging=%d, hw_max_current=%.1f)", - i, connectors[i].charging, connectors[i].hw_max_current); - if (connectors[i].charging) - { idxs[active_cnt++] = i; - ESP_LOGI(TAG, "Connector[%d] is ACTIVE (charging)", i); - } continue; } - // --- Ignora conectores já marcados como offline --- if (!connectors[i].online) - { continue; - } - // --- Timeout de heartbeat para escravos --- if ((now - connectors[i].timestamp) >= METRICS_TIMEOUT_US) { connectors[i].online = false; - ESP_LOGW(TAG, "Connector[%d] marked OFFLINE (charging=%d, timestamp_diff=%lld us)", - i, connectors[i].charging, (long long)(now - connectors[i].timestamp)); continue; } - ESP_LOGI(TAG, "Connector[%d] ONLINE (charging=%d, hw_max_current=%.1f, timestamp_diff=%lld us)", - i, connectors[i].charging, connectors[i].hw_max_current, - (long long)(now - connectors[i].timestamp)); - if (connectors[i].charging) - { idxs[active_cnt++] = i; - ESP_LOGI(TAG, "Connector[%d] is ACTIVE (charging)", i); - } } - // snapshot de grid_current e last_grid_timestamp sob mutex float grid_snapshot = grid_current; int64_t last_grid_ts_snapshot = last_grid_timestamp_us; if (lb_mutex) xSemaphoreGive(lb_mutex); - ESP_LOGI(TAG, "Active connectors: %d", active_cnt); - if (active_cnt == 0) { vTaskDelay(pdMS_TO_TICKS(5000)); continue; } - // --- Verifica timeout de medição de GRID (fail-safe) --- bool meter_timeout = (last_grid_ts_snapshot == 0 || (now - last_grid_ts_snapshot) > GRID_METER_TIMEOUT_US); if (meter_timeout) { - ESP_LOGW(TAG, - "GRID meter timeout (last update=%lld us ago). Applying fail-safe limits (<=%dA).", - (long long)(now - last_grid_ts_snapshot), MIN_CHARGING_CURRENT_LIMIT); - if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - - // Fail-safe: limitar cada EV ao mínimo permitido (6A) ou menos, nunca aumentar for (int k = 0; k < active_cnt; k++) { int i = idxs[k]; float cur = connectors[i].runtime_current; - if (cur > MIN_CHARGING_CURRENT_LIMIT) - connectors[i].assigned = (float)MIN_CHARGING_CURRENT_LIMIT; - else - connectors[i].assigned = cur; + connectors[i].assigned = (cur > MIN_CHARGING_CURRENT_LIMIT) ? (float)MIN_CHARGING_CURRENT_LIMIT : cur; } - if (lb_mutex) xSemaphoreGive(lb_mutex); + goto publish_limits; } - // --- Calcula corrente disponível (headroom global) --- float available = (float)max_grid_current - grid_snapshot; - ESP_LOGI(TAG, "LB raw headroom: max_grid=%uA, grid_current=%.1fA, available=%.2fA", - max_grid_current, grid_snapshot, available); - // ========================== - // ZONA A: overload significativo -> reduzir correntes (throttling) - // ========================== if (available < -AVAILABLE_EPS) { - ESP_LOGW(TAG, "Overload: grid=%.1fA, max=%.1fA (available=%.2fA) -> throttling", - grid_snapshot, (float)max_grid_current, available); - float factor = ((float)max_grid_current) / grid_snapshot; if (factor < 0.0f) factor = 0.0f; @@ -499,89 +473,53 @@ void loadbalancer_task(void *param) if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int k = 0; k < active_cnt; k++) { int i = idxs[k]; connectors[i].assigned = connectors[i].runtime_current * factor; - ESP_LOGI(TAG, - "Connector[%d] overload throttling: runtime=%.1fA -> assigned=%.1fA", - i, connectors[i].runtime_current, connectors[i].assigned); } - if (lb_mutex) xSemaphoreGive(lb_mutex); } - // ========================== - // ZONA B: sem margem prática -> manter correntes atuais como limites - // ========================== else if (fabsf(available) <= AVAILABLE_EPS) { - ESP_LOGI(TAG, - "No effective headroom: grid=%.1fA, max=%.1fA (available=%.2fA). Keeping current as limit.", - grid_snapshot, (float)max_grid_current, available); - if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - for (int k = 0; k < active_cnt; k++) { int i = idxs[k]; connectors[i].assigned = connectors[i].runtime_current; - ESP_LOGI(TAG, - "Connector[%d] keep runtime as limit: assigned=%.1fA", - i, connectors[i].assigned); } - if (lb_mutex) xSemaphoreGive(lb_mutex); } - // ========================== - // ZONA C: há margem positiva -> garantir mínimo e depois water-filling SOBRE assigned - // ========================== - else // available > AVAILABLE_EPS + else { if (available > max_grid_current) - { available = (float)max_grid_current; - } - - ESP_LOGI(TAG, "LB Calc (zone C): available=%.1fA, active_connectors=%d", - available, active_cnt); if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); - // 1) Ordenar conectores ativos por started_us (mais antigos primeiro) + // ordenar por started_us for (int a = 0; a < active_cnt - 1; a++) - { for (int b = 0; b < active_cnt - 1 - a; b++) - { - int i1 = idxs[b]; - int i2 = idxs[b + 1]; - if (connectors[i1].started_us > connectors[i2].started_us) + if (connectors[idxs[b]].started_us > connectors[idxs[b + 1]].started_us) { int tmp = idxs[b]; idxs[b] = idxs[b + 1]; idxs[b + 1] = tmp; } - } - } - // Inicialmente: assigned = runtime_current for (int k = 0; k < active_cnt; k++) - { - int i = idxs[k]; - connectors[i].assigned = connectors[i].runtime_current; - } + connectors[idxs[k]].assigned = connectors[idxs[k]].runtime_current; - float remaining = available; // margem extra total + float remaining = available; - // 2) FASE 1: tentar garantir pelo menos 6A (ou hw_max, se menor) aos mais antigos + // fase 1 min 6A for (int k = 0; k < active_cnt && remaining > 0.0f; k++) { int i = idxs[k]; - float current = connectors[i].runtime_current; float hw_max = connectors[i].hw_max_current; @@ -590,10 +528,7 @@ void loadbalancer_task(void *param) target_min = hw_max; if (current >= target_min) - { - connectors[i].assigned = current; continue; - } float delta = target_min - current; if (delta <= remaining) @@ -601,37 +536,25 @@ void loadbalancer_task(void *param) connectors[i].assigned = current + delta; remaining -= delta; } - else - { - connectors[i].assigned = current; - } } - // 3) FASE 2: "last in, first cut" -> cortar quem ficou abaixo do mínimo começando pelos mais recentes + // fase 2 suspender recentes abaixo min for (int k = active_cnt - 1; k >= 0; k--) { int i = idxs[k]; if (connectors[i].assigned >= MIN_CHARGING_CURRENT_LIMIT) - { continue; - } - - ESP_LOGI(TAG, "Connector[%d] below min after phase1 (assigned=%.1fA) -> suspending (0A)", - i, connectors[i].assigned); connectors[i].assigned = 0.0f; connectors[i].suspended_by_lb = true; } - // 4) FASE 3: se ainda sobrar margem, distribuir extra por cima dos que ficaram ON (assigned > 0) + // fase 3 distribuir extra if (remaining > AVAILABLE_EPS) { int on_cnt = 0; for (int k = 0; k < active_cnt; k++) - { - int i = idxs[k]; - if (connectors[i].assigned > 0.0f) + if (connectors[idxs[k]].assigned > 0.0f) on_cnt++; - } float extra_remaining = remaining; int extra_cnt = on_cnt; @@ -639,10 +562,8 @@ void loadbalancer_task(void *param) for (int k = 0; k < active_cnt; k++) { int i = idxs[k]; - if (connectors[i].assigned <= 0.0f) continue; - if (extra_cnt <= 0 || extra_remaining <= 0.0f) break; @@ -666,7 +587,6 @@ void loadbalancer_task(void *param) for (int m = k; m < active_cnt; m++) { int j = idxs[m]; - if (connectors[j].assigned <= 0.0f) continue; @@ -686,7 +606,7 @@ void loadbalancer_task(void *param) xSemaphoreGive(lb_mutex); } - // --- Publicação de limites / suspensão com histerese --- + publish_limits: if (lb_mutex) xSemaphoreTake(lb_mutex, portMAX_DELAY); @@ -696,10 +616,8 @@ void loadbalancer_task(void *param) float assigned = connectors[i].assigned; float effective = assigned; - // Histerese de suspensão / retoma if (connectors[i].suspended_by_lb) { - // Está suspenso: só retoma se limite calculado for >= limiar de retoma if (assigned >= LB_RESUME_THRESHOLD) { effective = assigned; @@ -712,7 +630,6 @@ void loadbalancer_task(void *param) } else { - // Ainda não está suspenso: só suspende se ficar claramente abaixo do limiar if (assigned > 0.0f && assigned < LB_SUSPEND_THRESHOLD) { effective = 0.0f; @@ -720,48 +637,40 @@ void loadbalancer_task(void *param) } } - uint16_t max_cur; - if (effective <= 0.0f) - { - max_cur = 0; - } - else - { - max_cur = (uint16_t)MIN(effective, (float)MAX_CHARGING_CURRENT_LIMIT); - } + uint16_t max_cur = (effective <= 0.0f) ? 0 : (uint16_t)MIN(effective, (float)MAX_CHARGING_CURRENT_LIMIT); - // Evita flapping de comandos: só envia se o limite mudou if (connectors[i].last_limit == max_cur) - { - continue; // sem alteração - } - + continue; connectors[i].last_limit = max_cur; + // sair do mutex durante o post if (lb_mutex) xSemaphoreGive(lb_mutex); if (connectors[i].is_master) { - loadbalancer_master_limit_event_t master_evt = { - .slave_id = connectors[i].id, + loadbalancer_master_limit_event_t m = { .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 (assigned=%.2f A)", - (float)max_cur, assigned); + (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, + &m, sizeof(m), portMAX_DELAY); } 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 (assigned=%.2f A)", - connectors[i].id, (float)max_cur, assigned); + uint8_t sid = connectors[i].id; + if (sid < MAX_SLAVES) + { + loadbalancer_slave_limit_event_t s = { + .slave_id = sid, + .max_current = max_cur, + .timestamp_us = now}; + (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + &s, sizeof(s), portMAX_DELAY); + } + else + { + ESP_LOGW(TAG, "Skipping publish: invalid slave id=%u", (unsigned)sid); + } } if (lb_mutex) @@ -775,36 +684,31 @@ void loadbalancer_task(void *param) } } -// --- Init --- void loadbalancer_init(void) { if (loadbalancer_load_config() != ESP_OK) ESP_LOGW(TAG, "Failed to load/init config. Using defaults."); lb_mutex = xSemaphoreCreateMutex(); - if (lb_mutex == NULL) - { + if (!lb_mutex) ESP_LOGE(TAG, "Failed to create loadbalancer mutex"); - } init_connectors(); input_filter_init(&grid_filter, 0.3f); input_filter_init(&evse_filter, 0.3f); - if (xTaskCreate(loadbalancer_task, "loadbalancer", 4096, NULL, 4, NULL) != pdPASS) + if (xTaskCreate(loadbalancer_task, "loadbalancer", 8192, NULL, 4, NULL) != pdPASS) ESP_LOGE(TAG, "Failed to create loadbalancer task"); loadbalancer_state_event_t evt = {.enabled = loadbalancer_enabled, .timestamp_us = esp_timer_get_time()}; - esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); + (void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); ESP_ERROR_CHECK(esp_event_handler_register(METER_EVENT, METER_EVENT_DATA_READY, &loadbalancer_meter_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, &loadbalancer_evse_event_handler, NULL)); - ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_CONFIG_UPDATED, &on_evse_config_event, NULL)); - ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_SLAVE_STATUS, &on_slave_status, NULL)); -} \ No newline at end of file +} diff --git a/components/logger/include/logger.h b/components/logger/include/logger.h index dfb8eb4..7204011 100755 --- a/components/logger/include/logger.h +++ b/components/logger/include/logger.h @@ -3,56 +3,45 @@ #include #include +#include + #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" -#define LOGGER_SERIAL_BIT BIT0 +#ifdef __cplusplus +extern "C" +{ +#endif -/** - * @brief Logger event group LOGGER_SERIAL_BIT - * - */ -extern EventGroupHandle_t logger_event_group; +#define LOGGER_SERIAL_BIT BIT0 -/** - * @brief Initialize logger - * - */ -void logger_init(void); + extern EventGroupHandle_t logger_event_group; -/** - * @brief Print - * - * @param str - */ -void logger_print(const char* str); + void logger_init(void); -/** - * @brief Print va - * - * @param str - * @param l - * @return int - */ -int logger_vprintf(const char* str, va_list l); + void logger_print(const char *str); -/** - * @brief Get entries count - * - * @return uint16_t - */ -uint16_t logger_count(void); + int logger_vprintf(const char *fmt, va_list args); -/** - * @brief Read line from index, set index for reading next entry - * - * @param index - * @param str - * @param v - * @return true When has next entry - * @return false When no entry left - */ -bool logger_read(uint16_t *index, char **str, uint16_t* len); + uint16_t logger_count(void); + // opcional: quantas mensagens foram dropadas por contenção de mutex + uint32_t logger_dropped_count(void); -#endif /* LOGGER_H_ */ \ No newline at end of file + /** + * ⚠️ API antiga (não recomendada): devolve ponteiro interno. + * Pode ficar inválido se houver novas escritas/rotação. + */ + bool logger_read(uint16_t *index, char **str, uint16_t *len); + + /** + * ✅ API recomendada: copia a entrada para buffer do caller (safe). + * out é sempre terminado com '\0' (se out_sz > 0). + */ + bool logger_read_copy(uint16_t *index, char *out, uint16_t out_sz, uint16_t *out_len); + +#ifdef __cplusplus +} +#endif + +#endif /* LOGGER_H_ */ diff --git a/components/logger/include/output_buffer.h b/components/logger/include/output_buffer.h index b25038e..29bdca5 100755 --- a/components/logger/include/output_buffer.h +++ b/components/logger/include/output_buffer.h @@ -4,21 +4,31 @@ #include #include -typedef struct { - uint16_t size; - uint16_t count; - uint8_t* data; - uint8_t* append; -} output_buffer_t; +#ifdef __cplusplus +extern "C" +{ +#endif -output_buffer_t* output_buffer_create(uint16_t size); + typedef struct + { + uint16_t size; + uint16_t count; + uint8_t *data; + uint8_t *append; + } output_buffer_t; -void output_buffer_delete(output_buffer_t* buffer); + output_buffer_t *output_buffer_create(uint16_t size); -void output_buffer_append_buf(output_buffer_t* buffer, const char* buf, uint16_t len); + void output_buffer_delete(output_buffer_t *buffer); -void output_buffer_append_str(output_buffer_t* buffer, const char* str); + void output_buffer_append_buf(output_buffer_t *buffer, const char *buf, uint16_t len); -bool output_buffer_read(output_buffer_t* buffer, uint16_t *index, char **str, uint16_t* len); + void output_buffer_append_str(output_buffer_t *buffer, const char *str); -#endif /* OUTPUT_BUFFER_H_ */ \ No newline at end of file + bool output_buffer_read(output_buffer_t *buffer, uint16_t *index, char **str, uint16_t *len); + +#ifdef __cplusplus +} +#endif + +#endif /* OUTPUT_BUFFER_H_ */ diff --git a/components/logger/src/logger.c b/components/logger/src/logger.c index f82b5f3..02b22b8 100755 --- a/components/logger/src/logger.c +++ b/components/logger/src/logger.c @@ -1,66 +1,155 @@ #include -#include +#include +#include +#include #include + #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "logger.h" #include "output_buffer.h" -#define LOG_BUFFER_SIZE 6096 //4096 -#define MAX_LOG_SIZE 512 +#define LOG_BUFFER_SIZE 6096 // tamanho total do buffer circular +#define MAX_LOG_SIZE 256 // ✅ reduzir stack/CPU; era 512 - -static SemaphoreHandle_t mutex; - -static output_buffer_t * buffer = NULL; +static SemaphoreHandle_t mutex = NULL; +static output_buffer_t *buffer = NULL; EventGroupHandle_t logger_event_group = NULL; +// opcional: contador de mensagens dropadas quando mutex está ocupado +static volatile uint32_t s_dropped = 0; + void logger_init(void) { + // Permitir múltiplas chamadas seguras + if (mutex != NULL) + { + return; + } + mutex = xSemaphoreCreateMutex(); + configASSERT(mutex != NULL); + logger_event_group = xEventGroupCreate(); + configASSERT(logger_event_group != NULL); buffer = output_buffer_create(LOG_BUFFER_SIZE); + configASSERT(buffer != NULL); } uint16_t logger_count(void) { - return buffer->count; + if (!mutex || !buffer) + { + return 0; + } + + // ✅ não bloquear para sempre (mas aqui pode bloquear sem stress) + xSemaphoreTake(mutex, portMAX_DELAY); + uint16_t c = buffer->count; + xSemaphoreGive(mutex); + + return c; } -void logger_print(const char* str) +uint32_t logger_dropped_count(void) { - xSemaphoreTake(mutex, portMAX_DELAY); + return s_dropped; +} + +void logger_print(const char *str) +{ + if (!str || !mutex || !buffer) + { + return; + } + + // Limitar comprimento para evitar entradas enormes + size_t len = strlen(str); + if (len > (MAX_LOG_SIZE - 1)) + { + len = MAX_LOG_SIZE - 1; + } + + // ✅ NÃO bloquear aqui: se o mutex estiver ocupado, dropa + if (xSemaphoreTake(mutex, 0) != pdTRUE) + { + s_dropped++; + return; + } + + output_buffer_append_buf(buffer, str, (uint16_t)len); + xEventGroupSetBits(logger_event_group, LOGGER_SERIAL_BIT); - output_buffer_append_str(buffer, str); - xEventGroupSetBits(logger_event_group, 0xFF); - xSemaphoreGive(mutex); } -int logger_vprintf(const char* str, va_list l) +int logger_vprintf(const char *fmt, va_list args) { + char log_buf[MAX_LOG_SIZE]; + #ifdef CONFIG_ESP_CONSOLE_UART - vprintf(str, l); + // Duplicar va_list para ecoar na UART sem consumir o original + va_list args_copy; + va_copy(args_copy, args); + vprintf(fmt, args_copy); + va_end(args_copy); #endif - xSemaphoreTake(mutex, portMAX_DELAY); + // Se ainda não inicializado, apenas formatar para devolver comprimento + if (!mutex || !buffer) + { + int len = vsnprintf(log_buf, MAX_LOG_SIZE, fmt, args); + if (len < 0) + { + len = 0; + } + else if (len >= MAX_LOG_SIZE) + { + len = MAX_LOG_SIZE - 1; + } + return len; + } - static char log[MAX_LOG_SIZE]; - int len = vsnprintf(log, MAX_LOG_SIZE, str, l); + int len = vsnprintf(log_buf, MAX_LOG_SIZE, fmt, args); + if (len < 0) + { + len = 0; + } + else if (len >= MAX_LOG_SIZE) + { + len = MAX_LOG_SIZE - 1; + } - output_buffer_append_buf(buffer, log, len); - xEventGroupSetBits(logger_event_group, 0xFF); + // ✅ NÃO bloquear o sistema (sys_evt/httpd/wifi/etc) por causa de log + if (xSemaphoreTake(mutex, 0) != pdTRUE) + { + s_dropped++; + return len; + } + + output_buffer_append_buf(buffer, log_buf, (uint16_t)len); + xEventGroupSetBits(logger_event_group, LOGGER_SERIAL_BIT); xSemaphoreGive(mutex); return len; } -bool logger_read(uint16_t* index, char** str, uint16_t* len) +/** + * ⚠️ API antiga: devolve ponteiro interno do buffer. + * Só é segura se o caller COPIAR imediatamente e garantir que não há novas escritas. + * Recomendo usar logger_read_copy(). + */ +bool logger_read(uint16_t *index, char **str, uint16_t *len) { + if (!mutex || !buffer || !index || !str || !len) + { + return false; + } + xSemaphoreTake(mutex, portMAX_DELAY); bool has_next = output_buffer_read(buffer, index, str, len); @@ -68,4 +157,36 @@ bool logger_read(uint16_t* index, char** str, uint16_t* len) xSemaphoreGive(mutex); return has_next; -} \ No newline at end of file +} + +// ✅ API segura: copia a linha para buffer do caller (evita ponteiro ficar inválido após rotação) +bool logger_read_copy(uint16_t *index, char *out, uint16_t out_sz, uint16_t *out_len) +{ + if (!mutex || !buffer || !index || !out || out_sz == 0) + { + return false; + } + + xSemaphoreTake(mutex, portMAX_DELAY); + + char *ptr = NULL; + uint16_t len = 0; + bool ok = output_buffer_read(buffer, index, &ptr, &len); + if (!ok) + { + xSemaphoreGive(mutex); + return false; + } + + uint16_t n = (len < (out_sz - 1)) ? len : (out_sz - 1); + memcpy(out, ptr, n); + out[n] = '\0'; + + if (out_len) + { + *out_len = n; + } + + xSemaphoreGive(mutex); + return true; +} diff --git a/components/logger/src/output_buffer.c b/components/logger/src/output_buffer.c index 4e19938..9a43d71 100755 --- a/components/logger/src/output_buffer.c +++ b/components/logger/src/output_buffer.c @@ -1,86 +1,217 @@ -#include +#include +#include #include "output_buffer.h" - -output_buffer_t* output_buffer_create(uint16_t size) +output_buffer_t *output_buffer_create(uint16_t size) { - output_buffer_t* buffer = (output_buffer_t*)malloc(sizeof(output_buffer_t)); + if (size == 0) + { + return NULL; + } + + output_buffer_t *buffer = (output_buffer_t *)malloc(sizeof(output_buffer_t)); + if (!buffer) + { + return NULL; + } + + buffer->data = (uint8_t *)malloc((size_t)size); + if (!buffer->data) + { + free(buffer); + return NULL; + } buffer->size = size; buffer->count = 0; - buffer->data = (uint8_t*)malloc(sizeof(uint8_t) * size); buffer->append = buffer->data; return buffer; } -void output_buffer_delete(output_buffer_t* buffer) +void output_buffer_delete(output_buffer_t *buffer) { - free((void*)buffer->data); - free((void*)buffer); + if (!buffer) + { + return; + } + + if (buffer->data) + { + free((void *)buffer->data); + buffer->data = NULL; + } + + free((void *)buffer); } -void output_buffer_append_buf(output_buffer_t* buffer, const char* str, uint16_t len) +void output_buffer_append_buf(output_buffer_t *buffer, const char *str, uint16_t len) { - if (((buffer->append - buffer->data) + sizeof(uint16_t) + len) >= buffer->size) { - //rotate buffer - uint8_t* pos = buffer->data; + if (!buffer || !buffer->data || !str || len == 0) + { + return; + } + + // Garantir que nunca escrevemos entradas absurdamente grandes + if (len > buffer->size / 2) + { + // Tamanho de entrada demasiado grande para a lógica de rotação; + // corta-a para caber de forma segura. + len = buffer->size / 2; + } + + size_t used = (size_t)(buffer->append - buffer->data); + if (used > buffer->size) + { + // Estado incoerente: reset defensivo + buffer->append = buffer->data; + buffer->count = 0; + used = 0; + } + + // Se não couber mais esta entrada, rodar o buffer + if (used + sizeof(uint16_t) + len > buffer->size) + { + uint8_t *pos = buffer->data; uint16_t rotate_count = 0; - while ((pos - buffer->data) < buffer->size / 2) { - //seek first half + uint8_t *end = buffer->data + buffer->size; + + // Avança entradas até aproximadamente metade do buffer + while ((pos + sizeof(uint16_t)) < end && + (size_t)(pos - buffer->data) < buffer->size / 2) + { + uint16_t entry_len; - memcpy((void*)&entry_len, (void*)pos, sizeof(uint16_t)); - pos += entry_len + sizeof(uint16_t); + memcpy(&entry_len, pos, sizeof(uint16_t)); + + // Sanitizar entry_len para evitar corrupções + if (entry_len == 0 || entry_len > buffer->size) + { + // Corrompido → reset defensivo + pos = buffer->data; + rotate_count = 0; + buffer->count = 0; + break; + } + + if (pos + sizeof(uint16_t) + entry_len > end) + { + // Entrada incompleta na cauda → para por aqui + break; + } + + pos += sizeof(uint16_t) + entry_len; rotate_count++; } - memmove((void*)buffer->data, (void*)pos, buffer->size - (pos - buffer->data)); - buffer->count -= rotate_count; - buffer->append -= (pos - buffer->data); + // Compacta o que sobrou para o início + size_t remaining = (size_t)(end - pos); + memmove(buffer->data, pos, remaining); + + buffer->count = (buffer->count >= rotate_count) ? (buffer->count - rotate_count) : 0; + buffer->append = buffer->data + remaining; + + used = (size_t)(buffer->append - buffer->data); } - memcpy((void*)buffer->append, (void*)&len, sizeof(uint16_t)); + // Escreve [len][dados] + memcpy(buffer->append, &len, sizeof(uint16_t)); buffer->append += sizeof(uint16_t); - memcpy((void*)buffer->append, (void*)str, len); + memcpy(buffer->append, str, len); buffer->append += len; buffer->count++; } -void output_buffer_append_str(output_buffer_t* buffer, const char* str) +void output_buffer_append_str(output_buffer_t *buffer, const char *str) { - output_buffer_append_buf(buffer, str, strlen(str)); + if (!buffer || !str) + { + return; + } + + size_t len = strlen(str); + if (len == 0) + { + return; + } + + // A API pública em logger.c já limita o tamanho, mas aqui fazemos + // um clamp defensivo para o caso de uso direto. + if (len > UINT16_MAX) + { + len = UINT16_MAX; + } + + output_buffer_append_buf(buffer, str, (uint16_t)len); } -bool output_buffer_read(output_buffer_t* buffer, uint16_t* index, char** str, uint16_t* len) +bool output_buffer_read(output_buffer_t *buffer, uint16_t *index, char **str, uint16_t *len) { - if (*index > buffer->count) { + if (!buffer || !buffer->data || !index || !str || !len) + { + return false; + } + + if (*index > buffer->count) + { *index = buffer->count; } - bool has_next = false; - - if (*index < buffer->count) { - uint8_t* pos = buffer->data; - uint16_t current = 0; - - while (current != *index) { - uint16_t entry_len; - memcpy((void*)&entry_len, (void*)pos, sizeof(uint16_t)); - pos += entry_len + sizeof(uint16_t); - current++; - } - - memcpy((void*)len, (void*)pos, sizeof(uint16_t)); - pos += sizeof(uint16_t); - *str = (char*)pos; - - (*index)++; - - has_next = true; + if (*index >= buffer->count) + { + return false; } - return has_next; -} \ No newline at end of file + uint8_t *pos = buffer->data; + uint8_t *end = buffer->data + buffer->size; + uint16_t current = 0; + + // Avança até à entrada [*index] + while (current < *index) + { + if (pos + sizeof(uint16_t) > end) + { + // Dados corrompidos ou índice fora → fail-safe + return false; + } + + uint16_t entry_len; + memcpy(&entry_len, pos, sizeof(uint16_t)); + + if (entry_len == 0 || entry_len > buffer->size) + { + // Corrompido → aborta + return false; + } + + if (pos + sizeof(uint16_t) + entry_len > end) + { + return false; + } + + pos += sizeof(uint16_t) + entry_len; + current++; + } + + // Agora pos aponta para o len da entrada desejada + if (pos + sizeof(uint16_t) > end) + { + return false; + } + + memcpy(len, pos, sizeof(uint16_t)); + pos += sizeof(uint16_t); + + if (pos + *len > end) + { + return false; + } + + *str = (char *)pos; + (*index)++; + + return true; +} diff --git a/components/meter_manager/CMakeLists.txt b/components/meter_manager/CMakeLists.txt index b585222..5bf131a 100755 --- a/components/meter_manager/CMakeLists.txt +++ b/components/meter_manager/CMakeLists.txt @@ -1,29 +1,30 @@ -# List the source files to be compiled +# components/meter_manager/CMakeLists.txt + set(srcs - "driver/meter_ade7758/meter_ade7758.c" - "driver/meter_ade7758/ade7758.c" - "driver/meter_orno/meter_orno513.c" - "driver/meter_orno/meter_orno526.c" - "driver/meter_orno/meter_orno516.c" - "driver/meter_orno/meter_dts6619.c" - "driver/meter_orno/meter_dds661.c" - "driver/meter_orno/meter_ea777.c" - "driver/meter_orno/modbus_params.c" - "driver/meter_zigbee/meter_zigbee.c" - "src/meter_manager.c" - "src/meter_events.c" + driver/meter_ade7758/meter_ade7758.c + driver/meter_ade7758/ade7758.c + driver/meter_orno/meter_orno513.c + driver/meter_orno/meter_orno526.c + driver/meter_orno/meter_orno516.c + driver/meter_orno/meter_dts6619.c + driver/meter_orno/meter_dds661.c + driver/meter_orno/meter_ea777.c + driver/meter_orno/modbus_params.c + driver/meter_zigbee/meter_zigbee.c + src/meter_manager.c + src/meter_events.c ) -# List the include directories set(includes - "include" - "driver/meter_ade7758" - "driver/meter_orno" - "driver/meter_zigbee" + include + driver/meter_ade7758 + driver/meter_orno + driver/meter_zigbee ) -# Register the component with the ESP-IDF build system -idf_component_register(SRCS "${srcs}" - INCLUDE_DIRS "${includes}" - PRIV_REQUIRES nvs_flash - REQUIRES esp_event esp-modbus spi_bus_manager network) +idf_component_register( + SRCS ${srcs} + INCLUDE_DIRS ${includes} + REQUIRES esp_event + PRIV_REQUIRES esp-modbus spi_bus_manager storage_service network +) diff --git a/components/meter_manager/driver/meter_ade7758/meter_ade7758.c b/components/meter_manager/driver/meter_ade7758/meter_ade7758.c index 9dd0f10..ddfbb52 100755 --- a/components/meter_manager/driver/meter_ade7758/meter_ade7758.c +++ b/components/meter_manager/driver/meter_ade7758/meter_ade7758.c @@ -53,7 +53,7 @@ static void meter_ade7758_post_event(const meter_ade7758_internal_data_t *data) memcpy(evt.irms, data->irms, sizeof(evt.irms)); memcpy(evt.watt, data->watt, sizeof(evt.watt)); - esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10)); + esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); if (err != ESP_OK) { ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); } diff --git a/components/meter_manager/driver/meter_orno/meter_dds661.c b/components/meter_manager/driver/meter_orno/meter_dds661.c index a478753..3a9c051 100755 --- a/components/meter_manager/driver/meter_orno/meter_dds661.c +++ b/components/meter_manager/driver/meter_orno/meter_dds661.c @@ -197,7 +197,7 @@ static void serial_mdb_task(void *param) memcpy(evt.irms, current, sizeof(evt.irms)); memcpy(evt.watt, watt, sizeof(evt.watt)); - esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10)); + esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); vTaskDelay(UPDATE_INTERVAL); } } diff --git a/components/meter_manager/driver/meter_orno/meter_dts6619.c b/components/meter_manager/driver/meter_orno/meter_dts6619.c index a6f37c1..d469c55 100755 --- a/components/meter_manager/driver/meter_orno/meter_dts6619.c +++ b/components/meter_manager/driver/meter_orno/meter_dts6619.c @@ -138,7 +138,7 @@ static void meter_dts6619_post_event(float *voltage, float *current, int *power_ memcpy(evt.watt, power_w, sizeof(evt.watt)); esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, - &evt, sizeof(evt), pdMS_TO_TICKS(10)); + &evt, sizeof(evt), portMAX_DELAY); if (err != ESP_OK) { ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); diff --git a/components/meter_manager/driver/meter_orno/meter_dts6619.h b/components/meter_manager/driver/meter_orno/meter_dts6619.h index f71926e..a843a1c 100755 --- a/components/meter_manager/driver/meter_orno/meter_dts6619.h +++ b/components/meter_manager/driver/meter_orno/meter_dts6619.h @@ -1,35 +1,32 @@ -#ifndef METER_EA777_H_ -#define METER_EA777_H_ +#ifndef METER_DTS6619_H_ +#define METER_DTS6619_H_ #include #include #include "esp_err.h" -#ifdef __cplusplus -extern "C" { -#endif - /** - * @brief Inicializa o driver do medidor EA777 (UART RS485, Modbus, registradores). + * @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores). * * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. */ -esp_err_t meter_ea777_init(void); +esp_err_t meter_dts6619_init(void); /** - * @brief Inicia a tarefa de leitura de dados do medidor EA777. + * @brief Inicia a tarefa de leitura de dados do medidor DTS6619. * * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. */ -esp_err_t meter_ea777_start(void); +esp_err_t meter_dts6619_start(void); /** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor EA777. + * @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS6619. */ -void meter_ea777_stop(void); +void meter_dts6619_stop(void); + #ifdef __cplusplus } #endif -#endif /* METER_EA777_H_ */ +#endif /* METER_DTS6619_H_ */ diff --git a/components/meter_manager/driver/meter_orno/meter_ea777.c b/components/meter_manager/driver/meter_orno/meter_ea777.c index e20682f..e353733 100755 --- a/components/meter_manager/driver/meter_orno/meter_ea777.c +++ b/components/meter_manager/driver/meter_orno/meter_ea777.c @@ -7,6 +7,7 @@ #include "driver/uart.h" #include #include +#include "meter_ea777.h" #define TAG "serial_mdb_ea777" @@ -148,7 +149,7 @@ static void meter_ea777_post_event(float *voltage, float *current, int *power_w, memcpy(evt.watt, power_w, sizeof(evt.watt)); esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, - &evt, sizeof(evt), pdMS_TO_TICKS(10)); + &evt, sizeof(evt), portMAX_DELAY); if (err != ESP_OK) { ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); diff --git a/components/meter_manager/driver/meter_orno/meter_ea777.h b/components/meter_manager/driver/meter_orno/meter_ea777.h index a843a1c..8fc9e2f 100755 --- a/components/meter_manager/driver/meter_orno/meter_ea777.h +++ b/components/meter_manager/driver/meter_orno/meter_ea777.h @@ -1,32 +1,35 @@ -#ifndef METER_DTS6619_H_ -#define METER_DTS6619_H_ +#ifndef METER_EA777_H_ +#define METER_EA777_H_ #include #include #include "esp_err.h" +#ifdef __cplusplus +extern "C" { +#endif + /** - * @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores). + * @brief Inicializa o driver do medidor EA777 (UART RS485, Modbus, registradores). * * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. */ -esp_err_t meter_dts6619_init(void); +esp_err_t meter_ea777_init(void); /** - * @brief Inicia a tarefa de leitura de dados do medidor DTS6619. + * @brief Inicia a tarefa de leitura de dados do medidor EA777. * * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. */ -esp_err_t meter_dts6619_start(void); +esp_err_t meter_ea777_start(void); /** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS6619. + * @brief Para a tarefa de leitura e limpa os dados internos do medidor EA777. */ -void meter_dts6619_stop(void); - +void meter_ea777_stop(void); #ifdef __cplusplus } #endif -#endif /* METER_DTS6619_H_ */ +#endif /* METER_EA777_H_ */ \ No newline at end of file diff --git a/components/meter_manager/driver/meter_orno/meter_orno513.c b/components/meter_manager/driver/meter_orno/meter_orno513.c index 7a9cc1d..a81da44 100755 --- a/components/meter_manager/driver/meter_orno/meter_orno513.c +++ b/components/meter_manager/driver/meter_orno/meter_orno513.c @@ -8,11 +8,12 @@ #define TAG "serial_mdb_orno513" + #define MB_PORT_NUM 2 #define MB_DEV_SPEED 9600 #define MB_UART_TXD 17 #define MB_UART_RXD 16 -#define MB_UART_RTS 5 +#define MB_UART_RTS 2 #define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS) #define POLL_INTERVAL (100 / portTICK_PERIOD_MS) @@ -129,7 +130,7 @@ static void serial_mdb_task(void *param) { memcpy(evt.irms, current, sizeof(evt.irms)); memcpy(evt.watt, watt, sizeof(evt.watt)); - esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10)); + esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); vTaskDelay(UPDATE_INTERVAL); diff --git a/components/meter_manager/driver/meter_orno/meter_orno516.c b/components/meter_manager/driver/meter_orno/meter_orno516.c index 9670787..df7290f 100755 --- a/components/meter_manager/driver/meter_orno/meter_orno516.c +++ b/components/meter_manager/driver/meter_orno/meter_orno516.c @@ -91,7 +91,7 @@ static void meter_orno516_post_event(float *voltage, float *current, int *power) memcpy(evt.watt, power, sizeof(evt.watt)); esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, - &evt, sizeof(evt), pdMS_TO_TICKS(10)); + &evt, sizeof(evt), portMAX_DELAY); if (err != ESP_OK) { ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); diff --git a/components/meter_manager/driver/meter_orno/meter_orno526.c b/components/meter_manager/driver/meter_orno/meter_orno526.c index 8a1946a..b8d6e09 100755 --- a/components/meter_manager/driver/meter_orno/meter_orno526.c +++ b/components/meter_manager/driver/meter_orno/meter_orno526.c @@ -207,7 +207,7 @@ static void serial_mdb_task(void *param) memcpy(evt.irms, current, sizeof(evt.irms)); memcpy(evt.watt, watt, sizeof(evt.watt)); - esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10)); + esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); vTaskDelay(UPDATE_INTERVAL); } } diff --git a/components/meter_manager/driver/meter_zigbee/meter_zigbee.c b/components/meter_manager/driver/meter_zigbee/meter_zigbee.c index 42b5586..812dbd2 100755 --- a/components/meter_manager/driver/meter_zigbee/meter_zigbee.c +++ b/components/meter_manager/driver/meter_zigbee/meter_zigbee.c @@ -12,7 +12,7 @@ #define TAG "meter_zigbee" // UART config -#define UART_PORT UART_NUM_1 +#define UART_PORT UART_NUM_2 #define TXD_PIN GPIO_NUM_17 #define RXD_PIN GPIO_NUM_16 #define UART_BUF_SIZE 128 @@ -85,7 +85,7 @@ static void meter_zigbee_post_event(void) { METER_EVENT_DATA_READY, &evt, sizeof(evt), - pdMS_TO_TICKS(10)); + portMAX_DELAY); if (err != ESP_OK) { ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); @@ -94,8 +94,8 @@ static void meter_zigbee_post_event(void) { static void handle_zigbee_frame(const uint8_t *buf, size_t len) { - ESP_LOGI(TAG, "Received UART frame (%d bytes):", len); - ESP_LOG_BUFFER_HEX(TAG, buf, len); + ESP_LOGD(TAG, "Received UART frame (%d bytes):", len); + //ESP_LOG_BUFFER_HEX(TAG, buf, len); if (len < RX_FRAME_SIZE) { ESP_LOGW(TAG, "Invalid frame: too short (len = %d)", len); @@ -118,7 +118,7 @@ static void handle_zigbee_frame(const uint8_t *buf, size_t len) { float current = current_raw / 1000.0f; float power = power_raw; - ESP_LOGI(TAG, "Parsed Attr 0x%04X: V=%.1fV I=%.2fA P=%.1fW", attr, volt, current, power); + ESP_LOGD(TAG, "Parsed Attr 0x%04X: V=%.1fV I=%.2fA P=%.1fW", attr, volt, current, power); if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE) { switch (attr) { @@ -224,13 +224,8 @@ esp_err_t meter_zigbee_start(void) { - void meter_zigbee_stop(void) { - //send_stop_command(); - - //vTaskDelay(pdMS_TO_TICKS(100)); // Aguarda o outro lado processar - if (meter_zigbee_task) { vTaskDelete(meter_zigbee_task); meter_zigbee_task = NULL; diff --git a/components/meter_manager/include/meter_events.h b/components/meter_manager/include/meter_events.h index 05b7e63..6cf042a 100644 --- a/components/meter_manager/include/meter_events.h +++ b/components/meter_manager/include/meter_events.h @@ -3,6 +3,7 @@ #include "esp_event.h" #include "meter_manager.h" // Para meter_type_t +#include // Para int64_t #ifdef __cplusplus extern "C" { @@ -16,20 +17,27 @@ typedef enum { METER_EVENT_DATA_READY = 0, METER_EVENT_ERROR, METER_EVENT_STARTED, - METER_EVENT_STOPPED + METER_EVENT_STOPPED, + METER_EVENT_CONFIG_UPDATED // Novo: configuração (grid/evse) atualizada } meter_event_id_t; // Estrutura de dados enviados com METER_EVENT_DATA_READY typedef struct { const char *source; // "GRID" ou "EVSE" - float vrms[3]; // Tensão por fase - float irms[3]; // Corrente por fase - int watt[3]; // Potência ativa por fase - float frequency; // Frequência da rede (Hz) - float power_factor; // Fator de potência - float total_energy; // Energia acumulada (kWh) + float vrms[3]; // Tensão por fase + float irms[3]; // Corrente por fase + int watt[3]; // Potência ativa por fase + float frequency; // Frequência da rede (Hz) + float power_factor; // Fator de potência + float total_energy; // Energia acumulada (kWh) } meter_event_data_t; +// Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED +typedef struct { + meter_type_t grid_type; // Tipo de contador configurado para o GRID + meter_type_t evse_type; // Tipo de contador configurado para a EVSE + int64_t timestamp_us; // Momento da atualização (esp_timer_get_time) +} meter_config_event_t; #ifdef __cplusplus } diff --git a/components/meter_manager/src/meter_manager.c b/components/meter_manager/src/meter_manager.c index 6a30c36..7d51435 100755 --- a/components/meter_manager/src/meter_manager.c +++ b/components/meter_manager/src/meter_manager.c @@ -10,10 +10,15 @@ #include "meter_zigbee.h" #include "meter_ea777.h" -#include "nvs_flash.h" -#include "nvs.h" #include + #include "network_events.h" +#include "meter_events.h" +#include "esp_event.h" +#include "esp_timer.h" + +// NEW: +#include "storage_service.h" static const char *TAG = "meter_manager"; @@ -21,179 +26,200 @@ static const char *TAG = "meter_manager"; static meter_type_t meter_evse_type = METER_TYPE_NONE; static meter_type_t meter_grid_type = METER_TYPE_NONE; -#define NVS_NAMESPACE "meterconfig" -#define NVS_EVSE_MODEL "evse_model" -#define NVS_GRID_MODEL "grid_model" +#define STORE_NAMESPACE "meterconfig" +#define STORE_EVSE_MODEL "evse_model" +#define STORE_GRID_MODEL "grid_model" +// timeouts storage +#define STORAGE_TO pdMS_TO_TICKS(800) + +// ------------------------------------------------------------------ +// Helpers storage (robustos a queue cheia) +// ------------------------------------------------------------------ + +static esp_err_t storage_try_flush(void) +{ + return storage_flush_sync(pdMS_TO_TICKS(2000)); +} + +static esp_err_t storage_try_set_u8(const char *ns, const char *key, uint8_t v) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t err = storage_set_u8_async(ns, key, v); + if (err == ESP_OK) + return ESP_OK; + + if (err == ESP_ERR_TIMEOUT) + { + (void)storage_try_flush(); + continue; + } + return err; + } + return ESP_ERR_TIMEOUT; +} + +static esp_err_t storage_get_u8_default(const char *ns, const char *key, uint8_t *out, uint8_t def) +{ + if (!out) + return ESP_ERR_INVALID_ARG; + + uint8_t v = def; + esp_err_t err = storage_get_u8_sync(ns, key, &v, STORAGE_TO); + if (err == ESP_OK) + { + *out = v; + return ESP_OK; + } + + // se não existir / erro, devolve default + *out = def; + return err; +} + +// ------------------------------------------------------------------ +/* static void meter_manager_network_event_handler(void *arg, esp_event_base_t base, int32_t event_id, void *data) { + (void)arg; + (void)data; + if (base != NETWORK_EVENTS) return; switch (event_id) { case NETWORK_EVENT_AP_STARTED: - ESP_LOGI(TAG, "Recebido NETWORK_EVENT_AP_STARTED, parando medidor de grid"); - // meter_manager_grid_stop(); + ESP_LOGD(TAG, "Recebido NETWORK_EVENT_AP_STARTED, parando medidor de grid"); break; case NETWORK_EVENT_AP_STOP: - ESP_LOGI(TAG, "Recebido NETWORK_EVENT_AP_STOP, reiniciando medidor de grid"); - // meter_manager_grid_start(); + ESP_LOGD(TAG, "Recebido NETWORK_EVENT_AP_STOP, reiniciando medidor de grid"); break; case NETWORK_EVENT_STA_GOT_IP: - ESP_LOGI(TAG, "Recebido NETWORK_EVENT_STA_GOT_IP"); - // opcional: reiniciar ou logar + ESP_LOGD(TAG, "Recebido NETWORK_EVENT_STA_GOT_IP"); break; default: break; } } +*/ -// Função unificada para ler ou inicializar um modelo de medidor +// Função unificada para ler ou inicializar um modelo de medidor (via storage_service) static esp_err_t load_or_init_meter_model(const char *key, meter_type_t *type) { - nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open NVS handle for %s: %s", key, esp_err_to_name(err)); - return err; - } + if (!type) + return ESP_ERR_INVALID_ARG; + + // tenta ler + uint8_t value = 0xFF; + esp_err_t err = storage_get_u8_sync(STORE_NAMESPACE, key, &value, STORAGE_TO); - uint8_t value = 0; - err = nvs_get_u8(handle, key, &value); if (err == ESP_OK && value < 255) { *type = (meter_type_t)value; - ESP_LOGI(TAG, "Loaded meter type %d from NVS key '%s'", value, key); - } - else - { - *type = METER_TYPE_NONE; - nvs_set_u8(handle, key, *type); - nvs_commit(handle); - ESP_LOGW(TAG, "Invalid or missing key '%s', setting default (NONE)", key); + ESP_LOGD(TAG, "Loaded meter type %u from storage key '%s/%s'", (unsigned)value, STORE_NAMESPACE, key); + return ESP_OK; } - nvs_close(handle); - return ESP_OK; + // se não existir / inválido -> default NONE e grava + *type = METER_TYPE_NONE; + + esp_err_t w = storage_try_set_u8(STORE_NAMESPACE, key, (uint8_t)(*type)); + if (w != ESP_OK) + ESP_LOGE(TAG, "Failed to init key '%s/%s' to NONE: %s", STORE_NAMESPACE, key, esp_err_to_name(w)); + + (void)storage_try_flush(); + + ESP_LOGW(TAG, "Invalid/missing key '%s/%s' (read=%s), setting default (NONE)", + STORE_NAMESPACE, key, esp_err_to_name(err)); + + return ESP_OK; // seguimos com default } -static esp_err_t write_meter_model_to_nvs(const char *key, meter_type_t meter_type) +static esp_err_t write_meter_model_to_storage(const char *key, meter_type_t meter_type) { - nvs_handle_t handle; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); + esp_err_t err = storage_try_set_u8(STORE_NAMESPACE, key, (uint8_t)meter_type); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to open NVS handle for writing"); + ESP_LOGE(TAG, "Failed to write meter type to storage key '%s/%s': %s", + STORE_NAMESPACE, key, esp_err_to_name(err)); return err; } - err = nvs_set_u8(handle, key, (uint8_t)meter_type); - if (err == ESP_OK) - { - err = nvs_commit(handle); - ESP_LOGI(TAG, "Saved meter type %d to NVS key '%s'", meter_type, key); - } - else - { - ESP_LOGE(TAG, "Failed to write meter type to NVS key '%s'", key); - } - - nvs_close(handle); - return err; + (void)storage_try_flush(); + ESP_LOGD(TAG, "Saved meter type %d to storage key '%s/%s'", (int)meter_type, STORE_NAMESPACE, key); + return ESP_OK; } /** - * @brief Initializes the meter manager system. - * - * This function initializes both the EVSE and GRID meters, - * and registers the event handler to listen for NETWORK_EVENTS - * (e.g., AP started/stopped, STA got IP). - * - * @return esp_err_t ESP_OK on success, or an error code. + * @brief Inicializa o sistema de meter manager. */ esp_err_t meter_manager_init(void) { - esp_err_t err; + // garantir storage pronto + esp_err_t s = storage_service_init(); + if (s != ESP_OK) + ESP_LOGE(TAG, "storage_service_init failed: %s", esp_err_to_name(s)); - // Initialize EVSE meter (habilite quando quiser) - // err = meter_manager_evse_init(); - // if (err != ESP_OK) return err; + esp_err_t err; // Initialize GRID meter err = meter_manager_grid_init(); if (err != ESP_OK) return err; - // Register handler for custom network events - ESP_LOGI(TAG, "Registering network event handler"); - return esp_event_handler_register(NETWORK_EVENTS, - ESP_EVENT_ANY_ID, - meter_manager_network_event_handler, - NULL); + // Regista handler para eventos de rede + /* + ESP_LOGD(TAG, "Registering network event handler"); + err = esp_event_handler_register( + NETWORK_EVENTS, + ESP_EVENT_ANY_ID, + meter_manager_network_event_handler, + NULL); + */ + + if (err != ESP_OK) + return err; + + // Emite um evento inicial de configuração + meter_config_event_t ev = { + .grid_type = meter_manager_grid_get_model(), + .evse_type = meter_manager_evse_get_model(), + .timestamp_us = esp_timer_get_time()}; + esp_event_post(METER_EVENT, + METER_EVENT_CONFIG_UPDATED, + &ev, + sizeof(ev), + 0); + + return ESP_OK; } -/** - * @brief Starts all configured meters (EVSE and GRID). - * - * This function starts the EVSE and GRID meters based on their configured types. - * It does not register event handlers — that is handled by `meter_manager_init()`. - * - * @return esp_err_t ESP_OK on success, or an error code from one of the start calls. - */ esp_err_t meter_manager_start(void) { - esp_err_t err; - - // Start EVSE meter (habilite quando quiser) - // err = meter_manager_evse_start(); - // if (err != ESP_OK) return err; - // Start GRID meter - err = meter_manager_grid_start(); - if (err != ESP_OK) - return err; - - return ESP_OK; + return meter_manager_grid_start(); } -/** - * @brief Stops all meters and unregisters event handlers. - * - * This function gracefully stops the EVSE and GRID meters - * and unregisters the previously registered network event handler. - * - * @return esp_err_t ESP_OK on success, or an error code. - */ esp_err_t meter_manager_stop(void) { - esp_err_t err; - - // Stop EVSE meter - // err = meter_manager_evse_stop(); - // if (err != ESP_OK) return err; - // Stop GRID meter - err = meter_manager_grid_stop(); - if (err != ESP_OK) - return err; - - return ESP_OK; + return meter_manager_grid_stop(); } // ---------- EVSE ---------- esp_err_t meter_manager_evse_init() { - esp_err_t err = load_or_init_meter_model(NVS_EVSE_MODEL, &meter_evse_type); + esp_err_t err = load_or_init_meter_model(STORE_EVSE_MODEL, &meter_evse_type); if (err != ESP_OK) return err; - ESP_LOGI(TAG, "Initializing EVSE meter of type %s", meter_type_to_str(meter_evse_type)); + ESP_LOGD(TAG, "Initializing EVSE meter of type %s", meter_type_to_str(meter_evse_type)); switch (meter_evse_type) { @@ -292,13 +318,13 @@ esp_err_t meter_manager_evse_stop(void) esp_err_t meter_manager_grid_init() { - esp_err_t err = load_or_init_meter_model(NVS_GRID_MODEL, &meter_grid_type); + esp_err_t err = load_or_init_meter_model(STORE_GRID_MODEL, &meter_grid_type); if (err != ESP_OK) return err; - ESP_LOGI(TAG, "Initializing GRID meter of type %s", meter_type_to_str(meter_grid_type)); + ESP_LOGD(TAG, "Initializing GRID meter of type %s", meter_type_to_str(meter_grid_type)); - switch (meter_grid_type) // corrigido: ORNO-513 -> driver orno513 + switch (meter_grid_type) { case METER_TYPE_NONE: return ESP_OK; @@ -334,7 +360,7 @@ esp_err_t meter_manager_grid_start() case METER_TYPE_ADE7758: return meter_ade7758_start(); case METER_TYPE_ORNO513: - return meter_orno513_start(); // corrigido + return meter_orno513_start(); case METER_TYPE_ORNO516: return meter_orno516_start(); case METER_TYPE_ORNO526: @@ -364,7 +390,7 @@ esp_err_t meter_manager_grid_stop(void) meter_ade7758_stop(); break; case METER_TYPE_ORNO513: - meter_orno513_stop(); // corrigido + meter_orno513_stop(); break; case METER_TYPE_ORNO516: meter_orno516_stop(); @@ -391,18 +417,64 @@ esp_err_t meter_manager_grid_stop(void) return ESP_OK; } -// ---------- Utilidades ---------- +// ---------- Utilidades / setters com evento ---------- esp_err_t meter_manager_evse_set_model(meter_type_t meter_type) { + esp_err_t err; + + err = meter_manager_evse_stop(); + if (err != ESP_OK) + ESP_LOGW(TAG, "Failed to stop EVSE meter before changing model: %s", esp_err_to_name(err)); + meter_evse_type = meter_type; - return write_meter_model_to_nvs(NVS_EVSE_MODEL, meter_evse_type); + + err = write_meter_model_to_storage(STORE_EVSE_MODEL, meter_evse_type); + if (err != ESP_OK) + return err; + + err = meter_manager_evse_init(); + if (err == ESP_OK) + err = meter_manager_evse_start(); + + meter_config_event_t ev = { + .grid_type = meter_manager_grid_get_model(), + .evse_type = meter_manager_evse_get_model(), + .timestamp_us = esp_timer_get_time()}; + esp_err_t evt_err = esp_event_post(METER_EVENT, METER_EVENT_CONFIG_UPDATED, &ev, sizeof(ev), portMAX_DELAY); + if (evt_err != ESP_OK) + ESP_LOGW(TAG, "Failed to post METER_EVENT_CONFIG_UPDATED (EVSE): %s", esp_err_to_name(evt_err)); + + return err; } esp_err_t meter_manager_grid_set_model(meter_type_t meter_type) { + esp_err_t err; + + err = meter_manager_grid_stop(); + if (err != ESP_OK) + ESP_LOGW(TAG, "Failed to stop GRID meter before changing model: %s", esp_err_to_name(err)); + meter_grid_type = meter_type; - return write_meter_model_to_nvs(NVS_GRID_MODEL, meter_grid_type); + + err = write_meter_model_to_storage(STORE_GRID_MODEL, meter_grid_type); + if (err != ESP_OK) + return err; + + err = meter_manager_grid_init(); + if (err == ESP_OK) + err = meter_manager_grid_start(); + + meter_config_event_t ev = { + .grid_type = meter_manager_grid_get_model(), + .evse_type = meter_manager_evse_get_model(), + .timestamp_us = esp_timer_get_time()}; + esp_err_t evt_err = esp_event_post(METER_EVENT, METER_EVENT_CONFIG_UPDATED, &ev, sizeof(ev), portMAX_DELAY); + if (evt_err != ESP_OK) + ESP_LOGW(TAG, "Failed to post METER_EVENT_CONFIG_UPDATED (GRID): %s", esp_err_to_name(evt_err)); + + return err; } bool meter_manager_evse_is_enabled(void) @@ -410,15 +482,8 @@ bool meter_manager_evse_is_enabled(void) return meter_manager_evse_get_model() != METER_TYPE_NONE; } -meter_type_t meter_manager_evse_get_model(void) -{ - return meter_evse_type; -} - -meter_type_t meter_manager_grid_get_model(void) -{ - return meter_grid_type; -} +meter_type_t meter_manager_evse_get_model(void) { return meter_evse_type; } +meter_type_t meter_manager_grid_get_model(void) { return meter_grid_type; } const char *meter_type_to_str(meter_type_t type) { @@ -453,6 +518,7 @@ meter_type_t string_to_meter_type(const char *str) { if (!str) return METER_TYPE_NONE; + if (strcmp(str, "IC ADE") == 0) return METER_TYPE_ADE7758; if (strcmp(str, "ORNO-513") == 0) @@ -471,5 +537,6 @@ meter_type_t string_to_meter_type(const char *str) return METER_TYPE_TRIF_ZIGBEE; if (strcmp(str, "EA-777") == 0) return METER_TYPE_EA777; + return METER_TYPE_NONE; } diff --git a/components/network/CMakeLists.txt b/components/network/CMakeLists.txt index 6de558a..cef87e6 100755 --- a/components/network/CMakeLists.txt +++ b/components/network/CMakeLists.txt @@ -5,4 +5,4 @@ set(srcs idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" - PRIV_REQUIRES nvs_flash esp_netif esp_wifi mdns esp_event) \ No newline at end of file + PRIV_REQUIRES nvs_flash esp_netif esp_wifi storage_service mdns esp_event) \ No newline at end of file diff --git a/components/network/src/network.c b/components/network/src/network.c index 8854a43..475887b 100755 --- a/components/network/src/network.c +++ b/components/network/src/network.c @@ -11,22 +11,24 @@ #include "freertos/event_groups.h" #include "esp_log.h" +#include "esp_err.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_netif.h" #include "esp_mac.h" -#include "nvs.h" -#include "nvs_flash.h" #include "mdns.h" #include "network_events.h" #include "network.h" +// NEW: +#include "storage_service.h" + // ----------------------------------------------------------------------------- // Config // ----------------------------------------------------------------------------- -#define AP_SSID "plx-%02x%02x%02x" // SSID do AP (usa 3 bytes do MAC) -#define MDNS_SSID "plx%02x" // hostname mDNS (usa 2 bytes do MAC) +#define AP_SSID "plx-%02x%02x%02x" +#define MDNS_SSID "plx%02x" #define NVS_NAMESPACE "wifi" #define NVS_ENABLED "enabled" @@ -35,60 +37,106 @@ // Comprimentos com terminador #define SSID_MAX_LEN 32 -#define PASS_MAX_LEN 64 // 63 chars + '\0' -#define SSID_BUF_SZ (SSID_MAX_LEN + 1) // 33 -#define PASS_BUF_SZ (PASS_MAX_LEN + 1) // 65 +#define PASS_MAX_LEN 64 +#define SSID_BUF_SZ (SSID_MAX_LEN + 1) +#define PASS_BUF_SZ (PASS_MAX_LEN + 1) static const char *TAG = "wifi"; +// Storage timeouts +#define STORE_TO pdMS_TO_TICKS(800) +#define STORE_FLUSH_TO pdMS_TO_TICKS(2000) + // ----------------------------------------------------------------------------- // Estado global // ----------------------------------------------------------------------------- -static nvs_handle_t nvs; static esp_netif_t *sta_netif; static esp_netif_t *ap_netif; EventGroupHandle_t wifi_event_group; -// Backoff simples para reconexão STA (agora sem delay no event handler) +// Backoff simples para reconexão STA static int s_retry_count = 0; static const int s_retry_max = 7; // ----------------------------------------------------------------------------- -// Helpers +// Helpers storage (robustos) // ----------------------------------------------------------------------------- -// Lê string do NVS com segurança (trunca se necessário) -static esp_err_t nvs_get_str_safe(nvs_handle_t h, const char *key, char *out, size_t out_sz) +static esp_err_t store_flush_best_effort(void) +{ + esp_err_t e = storage_flush_sync(STORE_FLUSH_TO); + if (e != ESP_OK) + ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e)); + return e; +} + +static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_u8_async(ns, key, v); + if (e == ESP_OK) + return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + return e; + } + return ESP_ERR_TIMEOUT; +} + +static esp_err_t store_set_str_best_effort(const char *ns, const char *key, const char *s) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_str_async(ns, key, s); + if (e == ESP_OK) + return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + return e; + } + return ESP_ERR_TIMEOUT; +} + +// Lê string de forma segura (lê para buffer grande e depois trunca para out) +// Nota: isto ajuda se houver lixo antigo no NVS com strings maiores do que o esperado. +static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out, size_t out_sz) { if (!out || out_sz == 0) return ESP_ERR_INVALID_ARG; + out[0] = '\0'; - size_t need = 0; - esp_err_t err = nvs_get_str(h, key, NULL, &need); - if (err == ESP_ERR_NVS_NOT_FOUND) - return ESP_OK; - if (err != ESP_OK) - return err; + // buffer grande (sem heap): usa o máximo definido no storage_service + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + memset(tmp, 0, sizeof(tmp)); - if (need == 0) - return ESP_OK; // vazio - - if (need > out_sz) + esp_err_t e = storage_get_str_sync(ns, key, tmp, sizeof(tmp), STORE_TO); + if (e == ESP_ERR_NOT_FOUND) { - // Truncar de forma segura - char *tmp = (char *)malloc(need); - if (!tmp) - return ESP_ERR_NO_MEM; - err = nvs_get_str(h, key, tmp, &need); - if (err == ESP_OK) - { - snprintf(out, out_sz, "%s", tmp); - } - free(tmp); - return err; + out[0] = '\0'; + return ESP_OK; } - return nvs_get_str(h, key, out, &need); + if (e != ESP_OK) + { + out[0] = '\0'; + return e; + } + + size_t n = strnlen(tmp, out_sz - 1); // no máximo out_sz-1 + memcpy(out, tmp, n); + out[n] = '\0'; + return ESP_OK; } // ----------------------------------------------------------------------------- @@ -96,6 +144,8 @@ static esp_err_t nvs_get_str_safe(nvs_handle_t h, const char *key, char *out, si // ----------------------------------------------------------------------------- static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + (void)arg; + if (event_base == WIFI_EVENT) { switch (event_id) @@ -128,7 +178,6 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_ case WIFI_EVENT_STA_CONNECTED: { ESP_LOGI(TAG, "STA associated (L2 connected)"); - // dispara evento genérico de STA_CONNECTED esp_event_post(NETWORK_EVENTS, NETWORK_EVENT_STA_CONNECTED, NULL, @@ -144,14 +193,12 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_ wifi_event_sta_disconnected_t *ev = (wifi_event_sta_disconnected_t *)event_data; ESP_LOGW(TAG, "STA disconnected, reason=%d", ev ? ev->reason : -1); - // dispara evento genérico de “STA desconectou” esp_event_post(NETWORK_EVENTS, NETWORK_EVENT_STA_DISCONNECTED, ev, ev ? sizeof(*ev) : 0, portMAX_DELAY); - // NÃO bloquear o event loop com vTaskDelay if (s_retry_count < s_retry_max) { esp_err_t err = esp_wifi_connect(); @@ -162,7 +209,7 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_ } else { - ESP_LOGW(TAG, "esp_wifi_connect failed (%d)", err); + ESP_LOGW(TAG, "esp_wifi_connect failed (%s)", esp_err_to_name(err)); } } else @@ -185,22 +232,12 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_ xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); s_retry_count = 0; - // Dispara evento “STA GOT IP” esp_event_post(NETWORK_EVENTS, NETWORK_EVENT_STA_GOT_IP, &event->ip_info, sizeof(event->ip_info), portMAX_DELAY); } - else if (event_id == IP_EVENT_GOT_IP6) - { - ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data; - ESP_LOGI(TAG, "WiFi STA got ip6: " IPV6STR, IPV62STR(event->ip6_info.ip)); - xEventGroupClearBits(wifi_event_group, WIFI_STA_DISCONNECTED_BIT); - xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); - s_retry_count = 0; - // se quiseres, podes também propagar um NETWORK_EVENT_STA_GOT_IP aqui - } else if (event_id == IP_EVENT_STA_LOST_IP) { ESP_LOGW(TAG, "STA lost IP"); @@ -227,28 +264,21 @@ static void sta_set_config(void) wifi_config.sta.pmf_cfg.capable = true; wifi_config.sta.pmf_cfg.required = false; - // buffers "seguros" vindos da NVS (estes sim têm terminador) - char ssid_buf[SSID_BUF_SZ] = {0}; // 33 (32 + '\0') - char pass_buf[PASS_BUF_SZ] = {0}; // 65 (64 + '\0') + char ssid_buf[SSID_BUF_SZ] = {0}; + char pass_buf[PASS_BUF_SZ] = {0}; wifi_get_ssid(ssid_buf); wifi_get_password(pass_buf); - // Copiar **sem** terminador para os campos do esp_wifi (32/64 bytes) - // SSID: max 32 - size_t ssid_len = strnlen(ssid_buf, SSID_MAX_LEN); // até 32 + size_t ssid_len = strnlen(ssid_buf, SSID_MAX_LEN); memcpy(wifi_config.sta.ssid, ssid_buf, ssid_len); - // Password WPA/WPA2: 8..63 chars (campo tem 64 bytes) - // (se usares rede aberta, pass_len pode ser 0) - size_t pass_len = strnlen(pass_buf, 63); // até 63 + size_t pass_len = strnlen(pass_buf, 63); memcpy(wifi_config.sta.password, pass_buf, pass_len); if (pass_len <= 63) - { wifi_config.sta.password[pass_len] = '\0'; - } ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); - ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); // v5.x + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); } static void ap_set_config(void) @@ -257,14 +287,13 @@ static void ap_set_config(void) wifi_config_t wifi_ap_config = {0}; wifi_ap_config.ap.max_connection = 1; - wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN; // para portal cativo, por exemplo + wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN; uint8_t mac[6]; esp_wifi_get_mac(WIFI_IF_AP, mac); snprintf((char *)wifi_ap_config.ap.ssid, sizeof(wifi_ap_config.ap.ssid), - AP_SSID, mac[3], mac[4], mac[5]); // "plx-XXXXXX" + AP_SSID, mac[3], mac[4], mac[5]); - // Só AP (não mexer na config STA aqui para não a limpar) ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config)); } @@ -283,7 +312,7 @@ static void sta_try_start(void) esp_err_t e = esp_wifi_start(); if (e != ESP_OK && e != ESP_ERR_WIFI_CONN) { - ESP_LOGW(TAG, "esp_wifi_start returned %d", e); + ESP_LOGW(TAG, "esp_wifi_start returned %s", esp_err_to_name(e)); } xEventGroupSetBits(wifi_event_group, WIFI_STA_MODE_BIT); } @@ -296,8 +325,10 @@ void wifi_ini(void) { ESP_LOGI(TAG, "Wifi init"); - // Abre NVS (assume que nvs_flash_init() já foi chamado no boot geral) - ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs)); + // garante storage pronto (não assume NVS handle aberto aqui) + esp_err_t se = storage_service_init(); + if (se != ESP_OK) + ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(se)); wifi_event_group = xEventGroupCreate(); @@ -310,14 +341,11 @@ void wifi_ini(void) ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); - // mDNS: usa dois bytes do MAC para identificar uint8_t mac[6]; esp_wifi_get_mac(WIFI_IF_STA, mac); char chargeid[16]; - // ex.: "plx00" snprintf(chargeid, sizeof(chargeid), MDNS_SSID, 0); - // Hostname das interfaces alinhado com o mDNS ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, chargeid)); ESP_ERROR_CHECK(esp_netif_set_hostname(ap_netif, chargeid)); @@ -335,10 +363,9 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password) { ESP_LOGI(TAG, "wifi_set_config(enabled=%d)", enabled); - // Validação (quando habilitar STA) + // Validação if (enabled) { - // SSID 1..32 if (ssid && (strlen(ssid) == 0 || strlen(ssid) > SSID_MAX_LEN)) { ESP_LOGE(TAG, "SSID out of range"); @@ -346,15 +373,15 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password) } if (!ssid) { - size_t len = 0; - esp_err_t e = nvs_get_str(nvs, NVS_SSID, NULL, &len); - if (e != ESP_OK || len <= 1) + char cur_ssid[SSID_BUF_SZ] = {0}; + wifi_get_ssid(cur_ssid); + if (strlen(cur_ssid) == 0) { ESP_LOGE(TAG, "Required SSID"); return ESP_ERR_INVALID_ARG; } } - // Password: 8..63 (se não for vazia). Aceita "" para rede open (caso uses). + if (password) { size_t lp = strlen(password); @@ -366,13 +393,27 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password) } } - // Persiste no NVS - ESP_ERROR_CHECK(nvs_set_u8(nvs, NVS_ENABLED, enabled)); + // Persiste via storage_service + esp_err_t err = store_set_u8_best_effort(NVS_NAMESPACE, NVS_ENABLED, enabled ? 1 : 0); + if (err != ESP_OK) + return err; + if (ssid) - ESP_ERROR_CHECK(nvs_set_str(nvs, NVS_SSID, ssid)); + { + err = store_set_str_best_effort(NVS_NAMESPACE, NVS_SSID, ssid); + if (err != ESP_OK) + return err; + } + if (password) - ESP_ERROR_CHECK(nvs_set_str(nvs, NVS_PASSWORD, password)); - ESP_ERROR_CHECK(nvs_commit(nvs)); + { + err = store_set_str_best_effort(NVS_NAMESPACE, NVS_PASSWORD, password); + if (err != ESP_OK) + return err; + } + + // Força persistência (para sobreviver a reboot imediato) + (void)store_flush_best_effort(); // Reinicia modo ESP_LOGI(TAG, "Stopping AP/STA"); @@ -380,7 +421,7 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password) esp_err_t e = esp_wifi_stop(); if (e != ESP_OK && e != ESP_ERR_WIFI_NOT_INIT && e != ESP_ERR_WIFI_STOP_STATE) { - ESP_LOGW(TAG, "esp_wifi_stop returned %d", e); + ESP_LOGW(TAG, "esp_wifi_stop returned %s", esp_err_to_name(e)); } sta_try_start(); @@ -402,21 +443,21 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps) esp_err_t err = esp_wifi_scan_start(NULL, true); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_scan_start failed (%d)", err); + ESP_LOGW(TAG, "esp_wifi_scan_start failed (%s)", esp_err_to_name(err)); return 0; } err = esp_wifi_scan_get_ap_records(&number, ap_info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed (%d)", err); + ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed (%s)", esp_err_to_name(err)); return 0; } err = esp_wifi_scan_get_ap_num(&ap_count); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_scan_get_ap_num failed (%d)", err); + ESP_LOGW(TAG, "esp_wifi_scan_get_ap_num failed (%s)", esp_err_to_name(err)); return 0; } @@ -424,7 +465,6 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps) for (int i = 0; (i < WIFI_SCAN_SCAN_LIST_SIZE) && (i < ap_count); i++) { - // garante que scan_aps[i].ssid tenha pelo menos 33 bytes snprintf(scan_aps[i].ssid, sizeof(scan_aps[i].ssid), "%s", (const char *)ap_info[i].ssid); scan_aps[i].rssi = ap_info[i].rssi; scan_aps[i].auth = ap_info[i].authmode != WIFI_AUTH_OPEN; @@ -436,29 +476,29 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps) bool wifi_get_enabled(void) { uint8_t value = 0; - esp_err_t e = nvs_get_u8(nvs, NVS_ENABLED, &value); - if (e == ESP_ERR_NVS_NOT_FOUND) + esp_err_t e = storage_get_u8_sync(NVS_NAMESPACE, NVS_ENABLED, &value, STORE_TO); + if (e == ESP_ERR_NOT_FOUND) return false; if (e != ESP_OK) { - ESP_LOGW(TAG, "nvs_get_u8(NVS_ENABLED) failed (%d), assuming disabled", e); + ESP_LOGW(TAG, "storage_get_u8_sync(enabled) failed (%s), assuming disabled", esp_err_to_name(e)); return false; } - return value; + return (value != 0); } void wifi_get_ssid(char *value) { if (!value) return; - nvs_get_str_safe(nvs, NVS_SSID, value, SSID_BUF_SZ); // 33 + (void)store_get_str_safe(NVS_NAMESPACE, NVS_SSID, value, SSID_BUF_SZ); } void wifi_get_password(char *value) { if (!value) return; - nvs_get_str_safe(nvs, NVS_PASSWORD, value, PASS_BUF_SZ); // 65 + (void)store_get_str_safe(NVS_NAMESPACE, NVS_PASSWORD, value, PASS_BUF_SZ); } void wifi_ap_start(void) @@ -474,7 +514,7 @@ void wifi_ap_start(void) esp_err_t e = esp_wifi_start(); if (e != ESP_OK && e != ESP_ERR_WIFI_CONN) { - ESP_LOGW(TAG, "esp_wifi_start (AP) returned %d", e); + ESP_LOGW(TAG, "esp_wifi_start (AP) returned %s", esp_err_to_name(e)); } xEventGroupSetBits(wifi_event_group, WIFI_AP_MODE_BIT); @@ -499,4 +539,4 @@ bool wifi_is_ap(void) EventBits_t bits = xEventGroupGetBits(wifi_event_group); return (bits & WIFI_AP_MODE_BIT) != 0; -} \ No newline at end of file +} diff --git a/components/ocpp/CMakeLists.txt b/components/ocpp/CMakeLists.txt index a60a825..47e949d 100755 --- a/components/ocpp/CMakeLists.txt +++ b/components/ocpp/CMakeLists.txt @@ -5,5 +5,4 @@ set(srcs idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" - PRIV_REQUIRES nvs_flash - REQUIRES esp_event config esp_wifi evse mongoose MicroOcpp MicroOcppMongoose) + REQUIRES esp_event storage_service config esp_wifi evse mongoose MicroOcpp MicroOcppMongoose) diff --git a/components/ocpp/src/ocpp.c b/components/ocpp/src/ocpp.c index 16dd3cb..6774edb 100755 --- a/components/ocpp/src/ocpp.c +++ b/components/ocpp/src/ocpp.c @@ -1,32 +1,37 @@ // components/ocpp/src/ocpp.c + #include #include #include #include +#include #include "esp_log.h" #include "esp_err.h" +#include "esp_timer.h" +#include "esp_event.h" +#include "esp_wifi.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "ocpp.h" #include "ocpp_events.h" -#include "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 +// NEW storage layer +#include "storage_service.h" + #define NVS_NAMESPACE "ocpp" #define NVS_OCPP_ENABLED "enabled" #define NVS_OCPP_SERVER "ocpp_server" @@ -59,19 +64,18 @@ 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_Wh; - // 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 + + int32_t sum_watt; + float avg_voltage; + float sum_current; + bool have_data; } ocpp_meter_cache_t; @@ -82,18 +86,101 @@ static esp_event_handler_instance_t s_meter_inst = NULL; // valor de oferta (A por conector) static float s_current_offered_A = 16.0f; -// novo input apropriado static float getCurrentOffered(void) { return s_current_offered_A; } -/* ========================= - * Task / Main Loop - * ========================= * - */ +// ----------------------------------------------------------------------------- +// Storage helpers (robustos) +// ----------------------------------------------------------------------------- +#define STORE_TO pdMS_TO_TICKS(800) +#define STORE_FLUSH_TO pdMS_TO_TICKS(2000) + +static void storage_init_best_effort(void) +{ + esp_err_t e = storage_service_init(); + if (e != ESP_OK) + ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(e)); +} + +static esp_err_t store_flush_best_effort(void) +{ + esp_err_t e = storage_flush_sync(STORE_FLUSH_TO); + if (e != ESP_OK) + ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e)); + return e; +} + +static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_u8_async(ns, key, v); + if (e == ESP_OK) + return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + return e; + } + return ESP_ERR_TIMEOUT; +} + +static esp_err_t store_set_str_best_effort(const char *ns, const char *key, const char *s) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_str_async(ns, key, s ? s : ""); + if (e == ESP_OK) + return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(portMAX_DELAY); + continue; + } + return e; + } + return ESP_ERR_TIMEOUT; +} + +// Lê string de forma segura (buffer grande -> truncagem segura para out) +static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out, size_t out_sz) +{ + if (!out || out_sz == 0) + return ESP_ERR_INVALID_ARG; + + out[0] = '\0'; + + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + memset(tmp, 0, sizeof(tmp)); + + esp_err_t e = storage_get_str_sync(ns, key, tmp, sizeof(tmp), STORE_TO); + if (e == ESP_ERR_NOT_FOUND) + return ESP_OK; + if (e != ESP_OK) + return e; + + size_t n = strnlen(tmp, out_sz - 1); + memcpy(out, tmp, n); + out[n] = '\0'; + + return ESP_OK; +} + +// ----------------------------------------------------------------------------- +// Task / Main Loop +// ----------------------------------------------------------------------------- static void ocpp_task_func(void *param) { + (void)param; + while (true) { if (enabled) @@ -106,7 +193,6 @@ static void ocpp_task_func(void *param) { s_evse_enabled = operative; - // >>> enviar OCPP_EVENT (remoto → local) ocpp_operative_event_t ev = { .operative = operative, .timestamp_us = esp_timer_get_time()}; @@ -115,8 +201,7 @@ static void ocpp_task_func(void *param) &ev, sizeof(ev), portMAX_DELAY); - ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d", - (int)operative); + ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d", (int)operative); } } else @@ -126,33 +211,21 @@ static void ocpp_task_func(void *param) } } -/* ========================= - * NVS GETs - * ========================= */ +// ----------------------------------------------------------------------------- +// Storage GETs +// ----------------------------------------------------------------------------- bool ocpp_get_enabled(void) { - 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; - } + storage_init_best_effort(); uint8_t value = 0; - err = nvs_get_u8(h, NVS_OCPP_ENABLED, &value); - nvs_close(h); + esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_OCPP_ENABLED, &value, STORE_TO); - if (err == ESP_ERR_NVS_NOT_FOUND) - return false; // default + if (err == ESP_ERR_NOT_FOUND) + return false; if (err != ESP_OK) { - ESP_LOGW(TAG, "nvs_get_u8(enabled) failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "storage_get_u8_sync(enabled) failed: %s", esp_err_to_name(err)); return false; } return value != 0; @@ -164,31 +237,12 @@ void ocpp_get_server(char *value /* out, size>=64 */) 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; - } + storage_init_best_effort(); - size_t len = 64; - err = nvs_get_str(h, NVS_OCPP_SERVER, value, &len); - nvs_close(h); - - if (err == ESP_ERR_NVS_NOT_FOUND) + esp_err_t e = store_get_str_safe(NVS_NAMESPACE, NVS_OCPP_SERVER, value, 64); + if (e != ESP_OK) { - value[0] = '\0'; - return; - } - if (err != ESP_OK) - { - ESP_LOGW(TAG, "nvs_get_str(server) failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "store_get_str_safe(server) failed: %s", esp_err_to_name(e)); value[0] = '\0'; } } @@ -199,92 +253,81 @@ void ocpp_get_charge_id(char *value /* out, size>=64 */) 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; - } + storage_init_best_effort(); - 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) + esp_err_t e = store_get_str_safe(NVS_NAMESPACE, NVS_OCPP_CHARGE_ID, value, 64); + if (e != ESP_OK) { - value[0] = '\0'; - return; - } - if (err != ESP_OK) - { - ESP_LOGW(TAG, "nvs_get_str(charge_id) failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "store_get_str_safe(charge_id) failed: %s", esp_err_to_name(e)); value[0] = '\0'; } } -/* ========================= - * NVS SETs - * ========================= */ -// --- SETTERS: RW (cria o namespace na 1ª escrita), commit e fecha --- +// ----------------------------------------------------------------------------- +// Storage SETs +// ----------------------------------------------------------------------------- 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) + storage_init_best_effort(); + + ESP_LOGI(TAG, "set enabled %d", value); + + esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_OCPP_ENABLED, value ? 1 : 0); + if (e != ESP_OK) { - ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + ESP_LOGE(TAG, "store_set_u8_best_effort(enabled) failed: %s", esp_err_to_name(e)); 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); + + (void)store_flush_best_effort(); 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) + storage_init_best_effort(); + + ESP_LOGI(TAG, "set server %s", value ? value : "(null)"); + + esp_err_t e = store_set_str_best_effort(NVS_NAMESPACE, NVS_OCPP_SERVER, value ? value : ""); + if (e != ESP_OK) { - ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + ESP_LOGE(TAG, "store_set_str_best_effort(server) failed: %s", esp_err_to_name(e)); 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)store_flush_best_effort(); } 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) + storage_init_best_effort(); + + ESP_LOGI(TAG, "set charge_id %s", value ? value : "(null)"); + + esp_err_t e = store_set_str_best_effort(NVS_NAMESPACE, NVS_OCPP_CHARGE_ID, value ? value : ""); + if (e != ESP_OK) { - ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err)); + ESP_LOGE(TAG, "store_set_str_best_effort(charge_id) failed: %s", esp_err_to_name(e)); 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); + + (void)store_flush_best_effort(); } +// ----------------------------------------------------------------------------- +// Event handlers (AUTH / EVSE / METER) +// ----------------------------------------------------------------------------- static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, void *event_data) { + (void)arg; + (void)base; + (void)id; + const auth_tag_verify_event_t *rq = (const auth_tag_verify_event_t *)event_data; if (!rq) return; - // Sanitizar/copiar a idTag char idtag[AUTH_TAG_MAX_LEN]; if (rq->tag[0] == '\0') { @@ -299,16 +342,13 @@ static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, vo 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); + 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); @@ -323,6 +363,7 @@ static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, vo static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { + (void)arg; if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL) return; @@ -343,12 +384,11 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi case EVSE_STATE_EVENT_CHARGING: s_ev_plugged = true; - s_ev_ready = true; // EV está a pedir/receber energia + s_ev_ready = true; break; case EVSE_STATE_EVENT_FAULT: default: - // em falha, considera não pronto (mantém plugged se quiseres) s_ev_ready = false; break; } @@ -356,6 +396,7 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi static void evse_enable_available_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { + (void)arg; if (base != EVSE_EVENTS || data == NULL) return; @@ -363,7 +404,8 @@ static void evse_enable_available_handler(void *arg, esp_event_base_t base, int3 { 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); + ESP_LOGI(TAG, "[EVSE] ENABLE_UPDATED: enabled=%d (ts=%lld)", + (int)e->enabled, (long long)e->timestamp_us); return; } @@ -371,23 +413,24 @@ static void evse_enable_available_handler(void *arg, esp_event_base_t base, int3 { 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); + ESP_LOGI(TAG, "[EVSE] AVAILABLE_UPDATED: available=%d (ts=%lld)", + (int)e->available, (long long)e->timestamp_us); return; } } static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *data) { + (void)arg; + if (base != METER_EVENT || id != METER_EVENT_DATA_READY || !data) return; const meter_event_data_t *evt = (const meter_event_data_t *)data; - // 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]; @@ -408,22 +451,21 @@ static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *d portEXIT_CRITICAL(&s_meter_mux); } -/* ========================= - * MicroOCPP Inputs/CBs - * ========================= */ +// ----------------------------------------------------------------------------- +// MicroOCPP Inputs/CBs +// ----------------------------------------------------------------------------- bool setConnectorPluggedInput(void) { - return s_ev_plugged; // EV fisicamente ligado + return s_ev_plugged; } bool setEvReadyInput(void) { - return s_ev_ready; // EV pede / pronto a carregar + return s_ev_ready; } bool setEvseReadyInput(void) { - // EVSE autorizado / operacional return s_evse_enabled && s_evse_available; } @@ -439,13 +481,10 @@ float setPowerMeterInput(void) 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; } @@ -461,14 +500,11 @@ float setEnergyMeterInput(void) portEXIT_CRITICAL(&s_meter_mux); if (!have) - { ESP_LOGW(TAG, "[METER] EnergyMeterInput: no data (return 0)"); - } else - { ESP_LOGD(TAG, "[METER] EnergyMeterInput: (%.1f Wh)", wh); - } - return wh; // agora devolve Wh + + return wh; } int setEnergyInput(void) @@ -491,13 +527,10 @@ float setCurrentInput(void) 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; } @@ -513,19 +546,16 @@ float setVoltageInput(void) 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 + float w = setPowerMeterInput(); ESP_LOGD(TAG, "[METER] PowerInput: %.1f W", w); return w; } @@ -598,111 +628,63 @@ bool setOnResetNotify(bool value) void notificationOutput(OCPP_Transaction *transaction, enum OCPP_TxNotification txNotification) { + (void)transaction; + ESP_LOGI(TAG, "TxNotification: %d", txNotification); switch (txNotification) { case Authorized: ESP_LOGI(TAG, "Authorized"); - // 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; } } -// 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; } @@ -728,12 +710,12 @@ const char *addErrorCodeInput(void) else if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT) ptr = "OtherError"; - return ptr; // NULL => sem erro + return ptr; } -/* ========================= - * Start / Stop OCPP - * ========================= */ +// ----------------------------------------------------------------------------- +// Start / Stop OCPP +// ----------------------------------------------------------------------------- void ocpp_start(void) { ESP_LOGI(TAG, "Starting OCPP"); @@ -744,6 +726,8 @@ void ocpp_start(void) return; } + storage_init_best_effort(); + enabled = ocpp_get_enabled(); if (!enabled) { @@ -767,15 +751,14 @@ void ocpp_start(void) 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 */ + serverstr, + charge_id, "", "", fsopt); @@ -788,19 +771,16 @@ void ocpp_start(void) 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 + ocpp_setEnergyMeterInput(&setEnergyInput); - /* Metering */ ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL); ocpp_addMeterValueInputFloat(&getCurrentOffered, "Current.Offered", "A", NULL, NULL); ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL); @@ -810,8 +790,7 @@ void ocpp_start(void) ocpp_addErrorCodeInput(&addErrorCodeInput); - /* Task */ - xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 5, &ocpp_task); + xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 3, &ocpp_task); if (!s_auth_verify_inst) { @@ -821,7 +800,6 @@ void ocpp_start(void) 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( @@ -829,7 +807,6 @@ void ocpp_start(void) &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( diff --git a/components/peripherals/CMakeLists.txt b/components/peripherals/CMakeLists.txt index a2b7127..426cef7 100755 --- a/components/peripherals/CMakeLists.txt +++ b/components/peripherals/CMakeLists.txt @@ -6,13 +6,11 @@ set(srcs "src/ac_relay.c" "src/socket_lock.c" "src/rcm.c" - "src/onewire.c" - "src/ds18x20.c" "src/temp_sensor.c" "src/ntc_sensor.c" ) idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" - PRIV_REQUIRES nvs_flash driver esp_adc esp_timer - REQUIRES config evse ntc_driver spi_bus_manager) + PRIV_REQUIRES driver esp_adc esp_timer + REQUIRES config evse ntc_driver spi_bus_manager storage_service) diff --git a/components/peripherals/include/adc121s021_dma.h b/components/peripherals/include/adc121s021_dma.h index 78c9232..a99a7da 100755 --- a/components/peripherals/include/adc121s021_dma.h +++ b/components/peripherals/include/adc121s021_dma.h @@ -1,12 +1,35 @@ +// components/peripherals/include/adc121s021_dma.h #ifndef ADC_DMA_H_ #define ADC_DMA_H_ - #include #include +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Inicializa o ADC121S021 no barramento SPI partilhado. + * + * - Garante que o spi_bus_manager foi inicializado. + * - Regista o dispositivo ADC no bus. + */ void adc121s021_dma_init(void); + +/** + * @brief Lê uma única amostra (12 bits) do ADC121S021. + * + * Esta função faz uma transação SPI bloqueante (polling), suficientemente + * rápida para uso em burst (100 amostras em ~2–3 ms). + * + * @param[out] sample Ponteiro onde será escrito o valor lido (0..4095). + * @return true em caso de sucesso, false se ocorrer erro. + */ bool adc121s021_dma_get_sample(uint16_t *sample); +#ifdef __cplusplus +} +#endif -#endif /* ADC_DMA_h_ */ +#endif /* ADC_DMA_H_ */ diff --git a/components/peripherals/include/ds18x20.h b/components/peripherals/include/ds18x20.h deleted file mode 100755 index cf725de..0000000 --- a/components/peripherals/include/ds18x20.h +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2016 Grzegorz Hetman - * Copyright (c) 2016 Alex Stewart - * Copyright (c) 2018 Ruslan V. Uss - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of itscontributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef _DS18X20_H -#define _DS18X20_H - -#include -#include "onewire.h" - -typedef onewire_addr_t ds18x20_addr_t; - -/** An address value which can be used to indicate "any device on the bus" */ -#define DS18X20_ANY ONEWIRE_NONE - -/** Family ID (lower address byte) of DS18B20 sensors */ -#define DS18B20_FAMILY_ID 0x28 - -/** Family ID (lower address byte) of DS18S20 sensors */ -#define DS18S20_FAMILY_ID 0x10 - -/** - * @brief Find the addresses of all ds18x20 devices on the bus. - * - * Scans the bus for all devices and places their addresses in the supplied - * array. If there are more than `addr_count` devices on the bus, only the - * first `addr_count` are recorded. - * - * @param pin The GPIO pin connected to the ds18x20 bus - * @param addr_list A pointer to an array of ::ds18x20_addr_t values. - * This will be populated with the addresses of the found - * devices. - * @param addr_count Number of slots in the `addr_list` array. At most this - * many addresses will be returned. - * @param found The number of devices found. Note that this may be less - * than, equal to, or more than `addr_count`, depending on - * how many ds18x20 devices are attached to the bus. - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18x20_scan_devices(gpio_num_t pin, ds18x20_addr_t *addr_list, size_t addr_count, size_t *found); - -/** - * @brief Tell one or more sensors to perform a temperature measurement and - * conversion (CONVERT_T) operation. - * - * This operation can take up to 750ms to complete. - * - * If `wait=true`, this routine will automatically drive the pin high for the - * necessary 750ms after issuing the command to ensure parasitically-powered - * devices have enough power to perform the conversion operation (for - * non-parasitically-powered devices, this is not necessary but does not - * hurt). If `wait=false`, this routine will drive the pin high, but will - * then return immediately. It is up to the caller to wait the requisite time - * and then depower the bus using onewire_depower() or by issuing another - * command once conversion is done. - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device on the bus. This can be set - * to ::DS18X20_ANY to send the command to all devices on the bus - * at the same time. - * @param wait Whether to wait for the necessary 750ms for the ds18x20 to - * finish performing the conversion before returning to the - * caller (You will normally want to do this). - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18x20_measure(gpio_num_t pin, ds18x20_addr_t addr, bool wait); - -/** - * @brief Read the value from the last CONVERT_T operation. - * - * This should be called after ds18x20_measure() to fetch the result of the - * temperature measurement. - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param temperature The temperature in degrees Celsius - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18x20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature); - -/** - * @brief Read the value from the last CONVERT_T operation (ds18b20 version). - * - * This should be called after ds18x20_measure() to fetch the result of the - * temperature measurement. - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param temperature The temperature in degrees Celsius - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18b20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature); - -/** - * @brief Read the value from the last CONVERT_T operation (ds18s20 version). - * - * This should be called after ds18x20_measure() to fetch the result of the - * temperature measurement. - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param temperature The temperature in degrees Celsius - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18s20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature); - -/** - * @brief Read the value from the last CONVERT_T operation for multiple devices. - * - * This should be called after ds18x20_measure() to fetch the result of the - * temperature measurement. - * - * @param pin The GPIO pin connected to the ds18x20 bus - * @param addr_list A list of addresses for devices to read. - * @param addr_count The number of entries in `addr_list`. - * @param result_list An array of int16_ts to hold the returned temperature - * values. It should have at least `addr_count` entries. - * - * @returns `ESP_OK` if all temperatures were fetched successfully - */ -esp_err_t ds18x20_read_temp_multi(gpio_num_t pin, ds18x20_addr_t *addr_list, size_t addr_count, int16_t *result_list); - -/** Perform a ds18x20_measure() followed by ds18s20_read_temperature() - * - * @param pin The GPIO pin connected to the ds18s20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param temperature The temperature in degrees Celsius - */ -esp_err_t ds18s20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature); - -/** Perform a ds18x20_measure() followed by ds18b20_read_temperature() - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param temperature The temperature in degrees Celsius - */ -esp_err_t ds18b20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature); - -/** Perform a ds18x20_measure() followed by ds18x20_read_temperature() - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param temperature The temperature in degrees Celsius - */ -esp_err_t ds18x20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature); - -/** - * @brief Perform a ds18x20_measure() followed by ds18x20_read_temp_multi() - * - * @param pin The GPIO pin connected to the ds18x20 bus - * @param addr_list A list of addresses for devices to read. - * @param addr_count The number of entries in `addr_list`. - * @param result_list An array of int16_ts to hold the returned temperature - * values. It should have at least `addr_count` entries. - * - * @returns `ESP_OK` if all temperatures were fetched successfully - */ -esp_err_t ds18x20_measure_and_read_multi(gpio_num_t pin, ds18x20_addr_t *addr_list, size_t addr_count, int16_t *result_list); - -/** - * @brief Read the scratchpad data for a particular ds18x20 device. - * - * This is not generally necessary to do directly. It is done automatically - * as part of ds18x20_read_temperature(). - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to read. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param buffer An 8-byte buffer to hold the read data. - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18x20_read_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t *buffer); - -/** - * @brief Write the scratchpad data for a particular ds18x20 device. - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to write. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * @param buffer An 3-byte buffer to hold the data to write - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18x20_write_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t *buffer); - -/** - * @brief Issue the copy scratchpad command, copying current scratchpad to - * EEPROM. - * - * @param pin The GPIO pin connected to the ds18x20 device - * @param addr The 64-bit address of the device to command. This can be set - * to ::DS18X20_ANY to read any device on the bus (but note - * that this will only work if there is exactly one device - * connected, or they will corrupt each others' transmissions) - * - * @returns `ESP_OK` if the command was successfully issued - */ -esp_err_t ds18x20_copy_scratchpad(gpio_num_t pin, ds18x20_addr_t addr); - - -#endif /* _DS18X20_H */ \ No newline at end of file diff --git a/components/peripherals/include/onewire.h b/components/peripherals/include/onewire.h deleted file mode 100755 index 3d94a91..0000000 --- a/components/peripherals/include/onewire.h +++ /dev/null @@ -1,277 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2014 zeroday nodemcu.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * ------------------------------------------------------------------------------- - * Portions copyright (C) 2000 Dallas Semiconductor Corporation, under the - * following additional terms: - * - * Except as contained in this notice, the name of Dallas Semiconductor - * shall not be used except as stated in the Dallas Semiconductor - * Branding Policy. - */ - -#ifndef ONEWIRE_H_ -#define ONEWIRE_H_ - -#include -#include -#include "driver/gpio.h" - -/** - * Type used to hold all 1-Wire device ROM addresses (64-bit) - */ -typedef uint64_t onewire_addr_t; - -/** - * Structure to contain the current state for onewire_search_next(), etc - */ -typedef struct -{ - uint8_t rom_no[8]; - uint8_t last_discrepancy; - bool last_device_found; -} onewire_search_t; - -/** - * ::ONEWIRE_NONE is an invalid ROM address that will never occur in a device - * (CRC mismatch), and so can be useful as an indicator for "no-such-device", - * etc. - */ -#define ONEWIRE_NONE ((onewire_addr_t)(0xffffffffffffffffLL)) - -/** - * @brief Perform a 1-Wire reset cycle. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * - * @return `true` if at least one device responds with a presence pulse, - * `false` if no devices were detected (or the bus is shorted, etc) - */ -bool onewire_reset(gpio_num_t pin); - -/** - * @brief Issue a 1-Wire "ROM select" command to select a particular device. - * - * It is necessary to call ::onewire_reset() before calling this function. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * @param addr The ROM address of the device to select - * - * @return `true` if the "ROM select" command could be successfully issued, - * `false` if there was an error. - */ -bool onewire_select(gpio_num_t pin, const onewire_addr_t addr); - -/** - * @brief Issue a 1-Wire "skip ROM" command to select *all* devices on the bus. - * - * It is necessary to call ::onewire_reset() before calling this function. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * - * @return `true` if the "skip ROM" command could be successfully issued, - * `false` if there was an error. - */ -bool onewire_skip_rom(gpio_num_t pin); - -/** - * @brief Write a byte on the onewire bus. - * - * The writing code uses open-drain mode and expects the pullup resistor to - * pull the line high when not driven low. If you need strong power after the - * write (e.g. DS18B20 in parasite power mode) then call ::onewire_power() - * after this is complete to actively drive the line high. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * @param v The byte value to write - * - * @return `true` if successful, `false` on error. - */ -bool onewire_write(gpio_num_t pin, uint8_t v); - -/** - * @brief Write multiple bytes on the 1-Wire bus. - * - * See ::onewire_write() for more info. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * @param buf A pointer to the buffer of bytes to be written - * @param count Number of bytes to write - * - * @return `true` if all bytes written successfully, `false` on error. - */ -bool onewire_write_bytes(gpio_num_t pin, const uint8_t *buf, size_t count); - -/** - * @brief Read a byte from a 1-Wire device. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * - * @return the read byte on success, negative value on error. - */ -int onewire_read(gpio_num_t pin); - -/** - * @brief Read multiple bytes from a 1-Wire device. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * @param[out] buf A pointer to the buffer to contain the read bytes - * @param count Number of bytes to read - * - * @return `true` on success, `false` on error. - */ -bool onewire_read_bytes(gpio_num_t pin, uint8_t *buf, size_t count); - -/** - * @brief Actively drive the bus high to provide extra power for certain - * operations of parasitically-powered devices. - * - * For parasitically-powered devices which need more power than can be - * provided via the normal pull-up resistor, it may be necessary for some - * operations to drive the bus actively high. This function can be used to - * perform that operation. - * - * The bus can be depowered once it is no longer needed by calling - * ::onewire_depower(), or it will be depowered automatically the next time - * ::onewire_reset() is called to start another command. - * - * @note Make sure the device(s) you are powering will not pull more current - * than the ESP32/ESP8266 is able to supply via its GPIO pins (this is - * especially important when multiple devices are on the same bus and - * they are all performing a power-intensive operation at the same time - * (i.e. multiple DS18B20 sensors, which have all been given a - * "convert T" operation by using ::onewire_skip_rom())). - * - * @note This routine will check to make sure that the bus is already high - * before driving it, to make sure it doesn't attempt to drive it high - * while something else is pulling it low (which could cause a reset or - * damage the ESP32/ESP8266). - * - * @param pin The GPIO pin connected to the 1-Wire bus. - * - * @return `true` on success, `false` on error. - */ -bool onewire_power(gpio_num_t pin); - -/** - * @brief Stop forcing power onto the bus. - * - * You only need to do this if you previously called ::onewire_power() to drive - * the bus high and now want to allow it to float instead. Note that - * onewire_reset() will also automatically depower the bus first, so you do - * not need to call this first if you just want to start a new operation. - * - * @param pin The GPIO pin connected to the 1-Wire bus. - */ -void onewire_depower(gpio_num_t pin); - -/** - * @brief Clear the search state so that it will start from the beginning on - * the next call to ::onewire_search_next(). - * - * @param[out] search The onewire_search_t structure to reset. - */ -void onewire_search_start(onewire_search_t *search); - -/** - * @brief Setup the search to search for devices with the specified - * "family code". - * - * @param[out] search The onewire_search_t structure to update. - * @param family_code The "family code" to search for. - */ -void onewire_search_prefix(onewire_search_t *search, uint8_t family_code); - -/** - * @brief Search for the next device on the bus. - * - * The order of returned device addresses is deterministic. You will always - * get the same devices in the same order. - * - * @note It might be a good idea to check the CRC to make sure you didn't get - * garbage. - * - * @return the address of the next device on the bus, or ::ONEWIRE_NONE if - * there is no next address. ::ONEWIRE_NONE might also mean that - * the bus is shorted, there are no devices, or you have already - * retrieved all of them. - */ -onewire_addr_t onewire_search_next(onewire_search_t *search, gpio_num_t pin); - -/** - * @brief Compute a Dallas Semiconductor 8 bit CRC. - * - * These are used in the ROM address and scratchpad registers to verify the - * transmitted data is correct. - */ -uint8_t onewire_crc8(const uint8_t *data, uint8_t len); - -/** - * @brief Compute the 1-Wire CRC16 and compare it against the received CRC. - * - * Example usage (reading a DS2408): - * @code{.c} - * // Put everything in a buffer so we can compute the CRC easily. - * uint8_t buf[13]; - * buf[0] = 0xF0; // Read PIO Registers - * buf[1] = 0x88; // LSB address - * buf[2] = 0x00; // MSB address - * onewire_write_bytes(pin, buf, 3); // Write 3 cmd bytes - * onewire_read_bytes(pin, buf+3, 10); // Read 6 data bytes, 2 0xFF, 2 CRC16 - * if (!onewire_check_crc16(buf, 11, &buf[11])) { - * // TODO: Handle error. - * } - * @endcode - * - * @param input Array of bytes to checksum. - * @param len Number of bytes in `input` - * @param inverted_crc The two CRC16 bytes in the received data. - * This should just point into the received data, - * *not* at a 16-bit integer. - * @param crc_iv The crc starting value (optional) - * - * @return `true` if the CRC matches, `false` otherwise. - */ -bool onewire_check_crc16(const uint8_t* input, size_t len, const uint8_t* inverted_crc, uint16_t crc_iv); - -/** - * @brief Compute a Dallas Semiconductor 16 bit CRC. - * - * This is required to check the integrity of data received from many 1-Wire - * devices. Note that the CRC computed here is *not* what you'll get from the - * 1-Wire network, for two reasons: - * - * 1. The CRC is transmitted bitwise inverted. - * 2. Depending on the endian-ness of your processor, the binary - * representation of the two-byte return value may have a different - * byte order than the two bytes you get from 1-Wire. - * - * @param input Array of bytes to checksum. - * @param len How many bytes are in `input`. - * @param crc_iv The crc starting value (optional) - * - * @return the CRC16, as defined by Dallas Semiconductor. - */ -uint16_t onewire_crc16(const uint8_t* input, size_t len, uint16_t crc_iv); - - -#endif /* ONEWIRE_H_ */ \ No newline at end of file diff --git a/components/peripherals/src/adc121s021_dma.c b/components/peripherals/src/adc121s021_dma.c index 419a0a8..e071609 100755 --- a/components/peripherals/src/adc121s021_dma.c +++ b/components/peripherals/src/adc121s021_dma.c @@ -1,28 +1,41 @@ +// components/peripherals/src/adc121s021_dma.c #include "driver/spi_master.h" #include "esp_log.h" +#include "esp_err.h" + #include "adc121s021_dma.h" #include "spi_bus_manager.h" #define TAG "adc_dma" -#define PIN_NUM_CS 5 -#define SAMPLE_SIZE_BYTES 2 -#define ADC_BITS 12 -#define SPI_CLOCK_HZ (6 * 1000 * 1000) // 6 MHz +// Pino de chip-select do ADC121S021 (ajusta se necessário) +#define PIN_NUM_CS 5 + +// ADC é 12-bit, mas transferimos 16 bits via SPI +#define ADC_BITS 12 + +// Clock SPI: 1 MHz → ~16 µs de transferência por amostra. +// Com um pequeno delay entre leituras, 100 amostras ficam em ~2–3 ms, +// o que é perfeito para analisar um PWM de 1 kHz a cada 100 ms. +#define SPI_CLOCK_HZ (1 * 1000 * 1000) static spi_device_handle_t adc_spi = NULL; void adc121s021_dma_init(void) { - if (adc_spi) { + if (adc_spi) + { ESP_LOGW(TAG, "ADC121S021 já foi inicializado."); return; } - if (!spi_bus_manager_is_initialized()) { + // Garante que o SPI bus partilhado está configurado + if (!spi_bus_manager_is_initialized()) + { ESP_LOGI(TAG, "SPI bus não inicializado. Inicializando..."); - esp_err_t err = spi_bus_manager_init(); // 🔧 CORRIGIDO: sem argumentos - if (err != ESP_OK) { + esp_err_t err = spi_bus_manager_init(); + if (err != ESP_OK) + { ESP_LOGE(TAG, "Falha ao inicializar o SPI bus: %s", esp_err_to_name(err)); return; } @@ -32,44 +45,55 @@ void adc121s021_dma_init(void) .clock_speed_hz = SPI_CLOCK_HZ, .mode = 0, .spics_io_num = PIN_NUM_CS, - .queue_size = 2, + .queue_size = 2, // suficiente para uso em burst .flags = SPI_DEVICE_NO_DUMMY, .pre_cb = NULL, .post_cb = NULL, }; esp_err_t err = spi_bus_add_device(spi_bus_manager_get_host(), &devcfg, &adc_spi); - if (err != ESP_OK) { + if (err != ESP_OK) + { ESP_LOGE(TAG, "Falha ao registrar ADC121S021 no SPI: %s", esp_err_to_name(err)); + adc_spi = NULL; return; } - ESP_LOGI(TAG, "ADC121S021 registrado no SPI com sucesso."); + ESP_LOGI(TAG, "ADC121S021 registrado no SPI (CS=%d, fSPI=%d Hz).", + PIN_NUM_CS, SPI_CLOCK_HZ); } bool adc121s021_dma_get_sample(uint16_t *sample) { - if (!adc_spi) { - ESP_LOGE(TAG, "ADC SPI não inicializado!"); + if (!sample) + { return false; } - uint8_t tx_buffer[2] = {0x00, 0x00}; // Dummy + if (!adc_spi) + { + ESP_LOGE(TAG, "ADC SPI não inicializado! Chama adc121s021_dma_init() primeiro."); + return false; + } + + uint8_t tx_buffer[2] = {0x00, 0x00}; // Dummy (ADC só precisa de clock) uint8_t rx_buffer[2] = {0}; spi_transaction_t t = { - .length = 16, + .length = 16, // 16 bits .tx_buffer = tx_buffer, .rx_buffer = rx_buffer, - .flags = 0 - }; + .flags = 0}; - esp_err_t err = spi_device_transmit(adc_spi, &t); - if (err != ESP_OK) { + // Polling transmit → menor overhead que fila + espera. + esp_err_t err = spi_device_polling_transmit(adc_spi, &t); + if (err != ESP_OK) + { ESP_LOGE(TAG, "Erro na transmissão SPI: %s", esp_err_to_name(err)); return false; } - *sample = ((rx_buffer[0] << 8) | rx_buffer[1]) & 0x0FFF; + // ADC121S021 devolve os 12 bits mais significativos em 16 bits. + *sample = (uint16_t)(((rx_buffer[0] << 8) | rx_buffer[1]) & 0x0FFF); return true; } diff --git a/components/peripherals/src/ds18x20.c b/components/peripherals/src/ds18x20.c deleted file mode 100755 index 14d1a34..0000000 --- a/components/peripherals/src/ds18x20.c +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2016 Grzegorz Hetman - * Copyright (c) 2016 Alex Stewart - * Copyright (c) 2018 Ruslan V. Uss - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of itscontributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - - -#include -#include -#include -#include -#include "ds18x20.h" - -#define ds18x20_WRITE_SCRATCHPAD 0x4E -#define ds18x20_READ_SCRATCHPAD 0xBE -#define ds18x20_COPY_SCRATCHPAD 0x48 -#define ds18x20_READ_EEPROM 0xB8 -#define ds18x20_READ_PWRSUPPLY 0xB4 -#define ds18x20_SEARCHROM 0xF0 -#define ds18x20_SKIP_ROM 0xCC -#define ds18x20_READROM 0x33 -#define ds18x20_MATCHROM 0x55 -#define ds18x20_ALARMSEARCH 0xEC -#define ds18x20_CONVERT_T 0x44 - -#define CHECK(x) do { esp_err_t __; if ((__ = x) != ESP_OK) return __; } while (0) -#define CHECK_ARG(VAL) do { if (!(VAL)) return ESP_ERR_INVALID_ARG; } while (0) - -static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; - -static const char* TAG = "ds18x20"; - -esp_err_t ds18x20_measure(gpio_num_t pin, ds18x20_addr_t addr, bool wait) -{ - if (!onewire_reset(pin)) - return ESP_ERR_INVALID_RESPONSE; - - if (addr == DS18X20_ANY) - onewire_skip_rom(pin); - else - onewire_select(pin, addr); - - portENTER_CRITICAL(&mux); - onewire_write(pin, ds18x20_CONVERT_T); - // For parasitic devices, power must be applied within 10us after issuing - // the convert command. - onewire_power(pin); - portEXIT_CRITICAL(&mux); - - if (wait){ - vTaskDelay(pdMS_TO_TICKS(750)); - onewire_depower(pin); - } - - return ESP_OK; -} - -esp_err_t ds18x20_read_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t* buffer) -{ - CHECK_ARG(buffer); - - uint8_t crc; - uint8_t expected_crc; - - if (!onewire_reset(pin)) - return ESP_ERR_INVALID_RESPONSE; - - if (addr == DS18X20_ANY) - onewire_skip_rom(pin); - else - onewire_select(pin, addr); - onewire_write(pin, ds18x20_READ_SCRATCHPAD); - - for (int i = 0; i < 8; i++) - buffer[i] = onewire_read(pin); - crc = onewire_read(pin); - - expected_crc = onewire_crc8(buffer, 8); - if (crc != expected_crc) - { - ESP_LOGE(TAG, "CRC check failed reading scratchpad: %02x %02x %02x %02x %02x %02x %02x %02x : %02x (expected %02x)", buffer[0], buffer[1], - buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7], crc, expected_crc); - return ESP_ERR_INVALID_CRC; - } - - return ESP_OK; -} - -esp_err_t ds18x20_write_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t* buffer) -{ - CHECK_ARG(buffer); - - if (!onewire_reset(pin)) - return ESP_ERR_INVALID_RESPONSE; - - if (addr == DS18X20_ANY) - onewire_skip_rom(pin); - else - onewire_select(pin, addr); - onewire_write(pin, ds18x20_WRITE_SCRATCHPAD); - - for (int i = 0; i < 3; i++) - onewire_write(pin, buffer[i]); - - return ESP_OK; -} - -esp_err_t ds18x20_copy_scratchpad(gpio_num_t pin, ds18x20_addr_t addr) -{ - if (!onewire_reset(pin)) - return ESP_ERR_INVALID_RESPONSE; - - if (addr == DS18X20_ANY) - onewire_skip_rom(pin); - else - onewire_select(pin, addr); - - portENTER_CRITICAL(&mux); - onewire_write(pin, ds18x20_COPY_SCRATCHPAD); - // For parasitic devices, power must be applied within 10us after issuing - // the convert command. - onewire_power(pin); - portEXIT_CRITICAL(&mux); - - // And then it needs to keep that power up for 10ms. - vTaskDelay(pdMS_TO_TICKS(10)); - onewire_depower(pin); - - return ESP_OK; -} - -esp_err_t ds18b20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature) -{ - CHECK_ARG(temperature); - - uint8_t scratchpad[8]; - int16_t temp; - - CHECK(ds18x20_read_scratchpad(pin, addr, scratchpad)); - - temp = scratchpad[1] << 8 | scratchpad[0]; - - *temperature = ((int16_t)temp * 625.0) / 100; - - return ESP_OK; -} - -esp_err_t ds18s20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature) -{ - CHECK_ARG(temperature); - - uint8_t scratchpad[8]; - int16_t temp; - - CHECK(ds18x20_read_scratchpad(pin, addr, scratchpad)); - - temp = scratchpad[1] << 8 | scratchpad[0]; - temp = ((temp & 0xfffe) << 3) + (16 - scratchpad[6]) - 4; - - *temperature = (temp * 625) / 100 - 25; - - return ESP_OK; -} - -esp_err_t ds18x20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature) -{ - if ((uint8_t)addr == DS18B20_FAMILY_ID) { - return ds18b20_read_temperature(pin, addr, temperature); - } else { - return ds18s20_read_temperature(pin, addr, temperature); - } -} - -esp_err_t ds18b20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature) -{ - CHECK_ARG(temperature); - - CHECK(ds18x20_measure(pin, addr, true)); - return ds18b20_read_temperature(pin, addr, temperature); -} - -esp_err_t ds18s20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature) -{ - CHECK_ARG(temperature); - - CHECK(ds18x20_measure(pin, addr, true)); - return ds18s20_read_temperature(pin, addr, temperature); -} - -esp_err_t ds18x20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature) -{ - CHECK_ARG(temperature); - - CHECK(ds18x20_measure(pin, addr, true)); - return ds18x20_read_temperature(pin, addr, temperature); -} - -esp_err_t ds18x20_measure_and_read_multi(gpio_num_t pin, ds18x20_addr_t* addr_list, size_t addr_count, int16_t* result_list) -{ - CHECK_ARG(result_list && addr_count); - - CHECK(ds18x20_measure(pin, DS18X20_ANY, true)); - - return ds18x20_read_temp_multi(pin, addr_list, addr_count, result_list); -} - -esp_err_t ds18x20_scan_devices(gpio_num_t pin, ds18x20_addr_t* addr_list, size_t addr_count, size_t* found) -{ - CHECK_ARG(addr_list && addr_count); - - onewire_search_t search; - onewire_addr_t addr; - - *found = 0; - onewire_search_start(&search); - while ((addr = onewire_search_next(&search, pin)) != ONEWIRE_NONE) - { - uint8_t family_id = (uint8_t)addr; - if (family_id == DS18B20_FAMILY_ID || family_id == DS18S20_FAMILY_ID) - { - if (*found < addr_count) - addr_list[*found] = addr; - *found += 1; - } - } - - return ESP_OK; -} - -esp_err_t ds18x20_read_temp_multi(gpio_num_t pin, ds18x20_addr_t* addr_list, size_t addr_count, int16_t* result_list) -{ - CHECK_ARG(result_list); - - esp_err_t res = ESP_OK; - for (size_t i = 0; i < addr_count; i++) - { - esp_err_t tmp = ds18x20_read_temperature(pin, addr_list[i], &result_list[i]); - if (tmp != ESP_OK) - res = tmp; - } - return res; -} diff --git a/components/peripherals/src/onewire.c b/components/peripherals/src/onewire.c deleted file mode 100755 index 175c1c0..0000000 --- a/components/peripherals/src/onewire.c +++ /dev/null @@ -1,498 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2014 zeroday nodemcu.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * ------------------------------------------------------------------------------- - * Portions copyright (C) 2000 Dallas Semiconductor Corporation, under the - * following additional terms: - * - * Except as contained in this notice, the name of Dallas Semiconductor - * shall not be used except as stated in the Dallas Semiconductor - * Branding Policy. - */ - -#include -#include -#include -#include "rom/ets_sys.h" - -#include "onewire.h" - -#define ONEWIRE_SELECT_ROM 0x55 -#define ONEWIRE_SKIP_ROM 0xcc -#define ONEWIRE_SEARCH 0xf0 -#define ONEWIRE_CRC8_TABLE - -static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; - -// Waits up to `max_wait` microseconds for the specified pin to go high. -// Returns true if successful, false if the bus never comes high (likely -// shorted). -static inline bool _onewire_wait_for_bus(gpio_num_t pin, int max_wait) -{ - bool state; - for (int i = 0; i < ((max_wait + 4) / 5); i++) { - if (gpio_get_level(pin)) - break; - ets_delay_us(5); - } - state = gpio_get_level(pin); - // Wait an extra 1us to make sure the devices have an adequate recovery - // time before we drive things low again. - ets_delay_us(1); - return state; -} - -static void setup_pin(gpio_num_t pin, bool open_drain) -{ - gpio_set_direction(pin, open_drain ? GPIO_MODE_INPUT_OUTPUT_OD : GPIO_MODE_OUTPUT); - // gpio_set_pull_mode(pin, GPIO_PULLUP_ONLY); -} - -// Perform the onewire reset function. We will wait up to 250uS for -// the bus to come high, if it doesn't then it is broken or shorted -// and we return false; -// -// Returns true if a device asserted a presence pulse, false otherwise. -// -bool onewire_reset(gpio_num_t pin) -{ - setup_pin(pin, true); - - gpio_set_level(pin, 1); - // wait until the wire is high... just in case - if (!_onewire_wait_for_bus(pin, 250)) - return false; - - gpio_set_level(pin, 0); - ets_delay_us(480); - - portENTER_CRITICAL(&mux); - gpio_set_level(pin, 1); // allow it to float - ets_delay_us(70); - bool r = !gpio_get_level(pin); - portEXIT_CRITICAL(&mux); - - // Wait for all devices to finish pulling the bus low before returning - if (!_onewire_wait_for_bus(pin, 410)) - return false; - - return r; -} - -static bool _onewire_write_bit(gpio_num_t pin, bool v) -{ - if (!_onewire_wait_for_bus(pin, 10)) - return false; - - portENTER_CRITICAL(&mux); - if (v) { - gpio_set_level(pin, 0); // drive output low - ets_delay_us(10); - gpio_set_level(pin, 1); // allow output high - ets_delay_us(55); - } else { - gpio_set_level(pin, 0); // drive output low - ets_delay_us(65); - gpio_set_level(pin, 1); // allow output high - } - ets_delay_us(1); - portEXIT_CRITICAL(&mux); - - return true; -} - -static int _onewire_read_bit(gpio_num_t pin) -{ - if (!_onewire_wait_for_bus(pin, 10)) - return -1; - - portENTER_CRITICAL(&mux); - gpio_set_level(pin, 0); - ets_delay_us(2); - gpio_set_level(pin, 1); // let pin float, pull up will raise - ets_delay_us(11); - int r = gpio_get_level(pin); // Must sample within 15us of start - ets_delay_us(48); - portEXIT_CRITICAL(&mux); - - return r; -} - -// Write a byte. The writing code uses open-drain mode and expects the pullup -// resistor to pull the line high when not driven low. If you need strong -// power after the write (e.g. DS18B20 in parasite power mode) then call -// onewire_power() after this is complete to actively drive the line high. -// -bool onewire_write(gpio_num_t pin, uint8_t v) -{ - for (uint8_t bitMask = 0x01; bitMask; bitMask <<= 1) - if (!_onewire_write_bit(pin, (bitMask & v))) - return false; - - return true; -} - -bool onewire_write_bytes(gpio_num_t pin, const uint8_t* buf, size_t count) -{ - for (size_t i = 0; i < count; i++) - if (!onewire_write(pin, buf[i])) - return false; - - return true; -} - -// Read a byte -// -int onewire_read(gpio_num_t pin) -{ - int r = 0; - - for (uint8_t bitMask = 0x01; bitMask; bitMask <<= 1) { - int bit = _onewire_read_bit(pin); - if (bit < 0) - return -1; - else if (bit) - r |= bitMask; - } - return r; -} - -bool onewire_read_bytes(gpio_num_t pin, uint8_t* buf, size_t count) -{ - size_t i; - int b; - - for (i = 0; i < count; i++) { - b = onewire_read(pin); - if (b < 0) - return false; - buf[i] = b; - } - return true; -} - -bool onewire_select(gpio_num_t pin, onewire_addr_t addr) -{ - uint8_t i; - - if (!onewire_write(pin, ONEWIRE_SELECT_ROM)) - return false; - - for (i = 0; i < 8; i++) { - if (!onewire_write(pin, addr & 0xff)) - return false; - addr >>= 8; - } - - return true; -} - -bool onewire_skip_rom(gpio_num_t pin) -{ - return onewire_write(pin, ONEWIRE_SKIP_ROM); -} - -bool onewire_power(gpio_num_t pin) -{ - // Make sure the bus is not being held low before driving it high, or we - // may end up shorting ourselves out. - if (!_onewire_wait_for_bus(pin, 10)) - return false; - - setup_pin(pin, false); - gpio_set_level(pin, 1); - - return true; -} - -void onewire_depower(gpio_num_t pin) -{ - setup_pin(pin, true); -} - -void onewire_search_start(onewire_search_t* search) -{ - // reset the search state - memset(search, 0, sizeof(*search)); -} - -void onewire_search_prefix(onewire_search_t* search, uint8_t family_code) -{ - uint8_t i; - - search->rom_no[0] = family_code; - for (i = 1; i < 8; i++) { - search->rom_no[i] = 0; - } - search->last_discrepancy = 64; - search->last_device_found = false; -} - -// Perform a search. If the next device has been successfully enumerated, its -// ROM address will be returned. If there are no devices, no further -// devices, or something horrible happens in the middle of the -// enumeration then ONEWIRE_NONE is returned. Use OneWire::reset_search() to -// start over. -// -// --- Replaced by the one from the Dallas Semiconductor web site --- -//-------------------------------------------------------------------------- -// Perform the 1-Wire Search Algorithm on the 1-Wire bus using the existing -// search state. -// Return 1 : device found, ROM number in ROM_NO buffer -// 0 : device not found, end of search -// -onewire_addr_t onewire_search_next(onewire_search_t* search, gpio_num_t pin) -{ - //TODO: add more checking for read/write errors - uint8_t id_bit_number; - uint8_t last_zero, search_result; - int rom_byte_number; - int8_t id_bit, cmp_id_bit; - onewire_addr_t addr; - unsigned char rom_byte_mask; - bool search_direction; - - // initialize for search - id_bit_number = 1; - last_zero = 0; - rom_byte_number = 0; - rom_byte_mask = 1; - search_result = 0; - - // if the last call was not the last one - if (!search->last_device_found) { - // 1-Wire reset - if (!onewire_reset(pin)) { - // reset the search - search->last_discrepancy = 0; - search->last_device_found = false; - return ONEWIRE_NONE; - } - - // issue the search command - onewire_write(pin, ONEWIRE_SEARCH); - - // loop to do the search - do { - // read a bit and its complement - id_bit = _onewire_read_bit(pin); - cmp_id_bit = _onewire_read_bit(pin); - - if ((id_bit == 1) && (cmp_id_bit == 1)) - break; - else { - // all devices coupled have 0 or 1 - if (id_bit != cmp_id_bit) - search_direction = id_bit; // bit write value for search - else { - // if this discrepancy if before the Last Discrepancy - // on a previous next then pick the same as last time - if (id_bit_number < search->last_discrepancy) - search_direction = ((search->rom_no[rom_byte_number] & rom_byte_mask) > 0); - else - // if equal to last pick 1, if not then pick 0 - search_direction = (id_bit_number == search->last_discrepancy); - - // if 0 was picked then record its position in LastZero - if (!search_direction) - last_zero = id_bit_number; - } - - // set or clear the bit in the ROM byte rom_byte_number - // with mask rom_byte_mask - if (search_direction) - search->rom_no[rom_byte_number] |= rom_byte_mask; - else - search->rom_no[rom_byte_number] &= ~rom_byte_mask; - - // serial number search direction write bit - _onewire_write_bit(pin, search_direction); - - // increment the byte counter id_bit_number - // and shift the mask rom_byte_mask - id_bit_number++; - rom_byte_mask <<= 1; - - // if the mask is 0 then go to new SerialNum byte rom_byte_number and reset mask - if (rom_byte_mask == 0) { - rom_byte_number++; - rom_byte_mask = 1; - } - } - } while (rom_byte_number < 8); // loop until through all ROM bytes 0-7 - - // if the search was successful then - if (!(id_bit_number < 65)) { - // search successful so set last_discrepancy,last_device_found,search_result - search->last_discrepancy = last_zero; - - // check for last device - if (search->last_discrepancy == 0) - search->last_device_found = true; - - search_result = 1; - } - } - - // if no device found then reset counters so next 'search' will be like a first - if (!search_result || !search->rom_no[0]) { - search->last_discrepancy = 0; - search->last_device_found = false; - return ONEWIRE_NONE; - } else { - addr = 0; - for (rom_byte_number = 7; rom_byte_number >= 0; rom_byte_number--) { - addr = (addr << 8) | search->rom_no[rom_byte_number]; - } - //printf("Ok I found something at %08x%08x...\n", (uint32_t)(addr >> 32), (uint32_t)addr); - } - return addr; -} - -// The 1-Wire CRC scheme is described in Maxim Application Note 27: -// "Understanding and Using Cyclic Redundancy Checks with Maxim iButton Products" -// - -#ifdef ONEWIRE_CRC8_TABLE -// This table comes from Dallas sample code where it is freely reusable, -// though Copyright (c) 2000 Dallas Semiconductor Corporation -static const uint8_t dscrc_table[] = { - 0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65, - 157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220, - 35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, - 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255, - 70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, - 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, - 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36, - 248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185, - 140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205, - 17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80, - 175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, - 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115, - 202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139, - 87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22, - 233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168, - 116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53 -}; - -// -// Compute a Dallas Semiconductor 8 bit CRC. These show up in the ROM -// and the registers. (note: this might better be done without to -// table, it would probably be smaller and certainly fast enough -// compared to all those delayMicrosecond() calls. But I got -// confused, so I use this table from the examples.) -// -uint8_t onewire_crc8(const uint8_t* data, uint8_t len) -{ - uint8_t crc = 0; - - while (len--) - crc = dscrc_table[crc ^ *data++]; - - return crc; -} -#else -// -// Compute a Dallas Semiconductor 8 bit CRC directly. -// this is much slower, but much smaller, than the lookup table. -// -uint8_t onewire_crc8(const uint8_t* data, uint8_t len) -{ - uint8_t crc = 0; - - while (len--) - { - uint8_t inbyte = *data++; - for (int i = 8; i; i--) - { - uint8_t mix = (crc ^ inbyte) & 0x01; - crc >>= 1; - if (mix) - crc ^= 0x8C; - inbyte >>= 1; - } - } - return crc; -} -#endif /* ONEWIRE_CRC8_TABLE */ - -// Compute the 1-Wire CRC16 and compare it against the received CRC. -// Example usage (reading a DS2408): -// // Put everything in a buffer so we can compute the CRC easily. -// uint8_t buf[13]; -// buf[0] = 0xF0; // Read PIO Registers -// buf[1] = 0x88; // LSB address -// buf[2] = 0x00; // MSB address -// WriteBytes(net, buf, 3); // Write 3 cmd bytes -// ReadBytes(net, buf+3, 10); // Read 6 data bytes, 2 0xFF, 2 CRC16 -// if (!CheckCRC16(buf, 11, &buf[11])) { -// // Handle error. -// } -// -// @param input - Array of bytes to checksum. -// @param len - How many bytes to use. -// @param inverted_crc - The two CRC16 bytes in the received data. -// This should just point into the received data, -// *not* at a 16-bit integer. -// @param crc - The crc starting value (optional) -// @return 1, iff the CRC matches. -bool onewire_check_crc16(const uint8_t* input, size_t len, const uint8_t* inverted_crc, uint16_t crc_iv) -{ - uint16_t crc = ~onewire_crc16(input, len, crc_iv); - return (crc & 0xFF) == inverted_crc[0] && (crc >> 8) == inverted_crc[1]; -} - -// Compute a Dallas Semiconductor 16 bit CRC. This is required to check -// the integrity of data received from many 1-Wire devices. Note that the -// CRC computed here is *not* what you'll get from the 1-Wire network, -// for two reasons: -// 1) The CRC is transmitted bitwise inverted. -// 2) Depending on the endian-ness of your processor, the binary -// representation of the two-byte return value may have a different -// byte order than the two bytes you get from 1-Wire. -// @param input - Array of bytes to checksum. -// @param len - How many bytes to use. -// @param crc - The crc starting value (optional) -// @return The CRC16, as defined by Dallas Semiconductor. -uint16_t onewire_crc16(const uint8_t* input, size_t len, uint16_t crc_iv) -{ - uint16_t crc = crc_iv; - static const uint8_t oddparity[16] = { 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0 }; - - uint16_t i; - for (i = 0; i < len; i++) { - // Even though we're just copying a byte from the input, - // we'll be doing 16-bit computation with it. - uint16_t cdata = input[i]; - cdata = (cdata ^ crc) & 0xff; - crc >>= 8; - - if (oddparity[cdata & 0x0F] ^ oddparity[cdata >> 4]) - crc ^= 0xC001; - - cdata <<= 6; - crc ^= cdata; - cdata <<= 1; - crc ^= cdata; - } - return crc; -} \ No newline at end of file diff --git a/components/peripherals/src/peripherals.c b/components/peripherals/src/peripherals.c index 11d87b9..b65c69a 100755 --- a/components/peripherals/src/peripherals.c +++ b/components/peripherals/src/peripherals.c @@ -1,7 +1,5 @@ #include "peripherals.h" #include "adc.h" -//#include "led.h" -// #include "buzzer.h" #include "proximity.h" #include "ac_relay.h" #include "socket_lock.h" @@ -11,13 +9,9 @@ void peripherals_init(void) { ac_relay_init(); - // led_init(); - // buzzer_init(); adc_init(); proximity_init(); // socket_lock_init(); // rcm_init(); - // energy_meter_init(); - // aux_init(); ntc_sensor_init(); } \ No newline at end of file diff --git a/components/peripherals/src/socket_lock.c b/components/peripherals/src/socket_lock.c index 3d644d5..c2e6e4e 100755 --- a/components/peripherals/src/socket_lock.c +++ b/components/peripherals/src/socket_lock.c @@ -4,12 +4,15 @@ #include "freertos/semphr.h" #include "freertos/timers.h" #include "esp_log.h" +#include "esp_err.h" #include "driver/gpio.h" -#include "nvs.h" #include "socket_lock.h" #include "board_config.h" +// NEW: +#include "storage_service.h" + #define NVS_NAMESPACE "socket_lock" #define NVS_OPERATING_TIME "op_time" #define NVS_BREAK_TIME "break_time" @@ -27,20 +30,68 @@ static const char* TAG = "socket_lock"; -static nvs_handle_t nvs; +// Storage timeouts (ajusta se quiseres) +#define STORE_TO pdMS_TO_TICKS(800) +#define STORE_FLUSH_TO pdMS_TO_TICKS(2000) static uint16_t operating_time = 300; - static uint16_t break_time = 1000; - -static bool detection_high; - +static bool detection_high = false; static uint8_t retry_count = 5; static socket_lock_status_t status; - static TaskHandle_t socket_lock_task; +// ----------------------------------------------------------------------------- +// Helpers storage (best effort) - iguais ao estilo do wifi.c +// ----------------------------------------------------------------------------- +static esp_err_t store_flush_best_effort(void) +{ + esp_err_t e = storage_flush_sync(STORE_FLUSH_TO); + if (e != ESP_OK) + ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e)); + return e; +} + +static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_u8_async(ns, key, v); + if (e == ESP_OK) return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + return e; + } + return ESP_ERR_TIMEOUT; +} + +static esp_err_t store_set_u16_best_effort(const char *ns, const char *key, uint16_t v) +{ + for (int attempt = 0; attempt < 3; ++attempt) + { + esp_err_t e = storage_set_u16_async(ns, key, v); + if (e == ESP_OK) return ESP_OK; + + if (e == ESP_ERR_TIMEOUT) + { + (void)store_flush_best_effort(); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + return e; + } + return ESP_ERR_TIMEOUT; +} + +// ----------------------------------------------------------------------------- +// Lock logic +// ----------------------------------------------------------------------------- static bool is_locked(void) { gpio_set_level(board_config.socket_lock_a_gpio, 1); @@ -58,31 +109,42 @@ bool socket_lock_is_locked_state(void) static void socket_lock_task_func(void* param) { - uint32_t notification; + (void)param; + uint32_t notification; TickType_t previous_tick = 0; uint8_t attempt = 0; - while (true) { - if (xTaskNotifyWait(0x00, 0xff, ¬ification, portMAX_DELAY)) { - if (notification & (LOCK_BIT | UNLOCK_BIT)) { + while (true) + { + if (xTaskNotifyWait(0x00, 0xff, ¬ification, portMAX_DELAY)) + { + if (notification & (LOCK_BIT | UNLOCK_BIT)) + { attempt = retry_count; } - if (notification & (UNLOCK_BIT | REPEAT_UNLOCK_BIT)) { + if (notification & (UNLOCK_BIT | REPEAT_UNLOCK_BIT)) + { gpio_set_level(board_config.socket_lock_a_gpio, 0); gpio_set_level(board_config.socket_lock_b_gpio, 1); vTaskDelay(pdMS_TO_TICKS(operating_time)); - if (!is_locked()) { + if (!is_locked()) + { ESP_LOGI(TAG, "Unlock OK"); status = SOCKED_LOCK_STATUS_IDLE; - } else { - if (attempt > 1) { + } + else + { + if (attempt > 1) + { ESP_LOGW(TAG, "Not unlocked yet, repeating..."); attempt--; xTaskNotify(socket_lock_task, REPEAT_UNLOCK_BIT, eSetBits); - } else { + } + else + { ESP_LOGE(TAG, "Not unlocked"); status = SOCKED_LOCK_STATUS_UNLOCKING_FAIL; } @@ -90,23 +152,33 @@ static void socket_lock_task_func(void* param) gpio_set_level(board_config.socket_lock_a_gpio, 0); gpio_set_level(board_config.socket_lock_b_gpio, 0); - } else if (notification & (LOCK_BIT | REPEAT_LOCK_BIT)) { - if (notification & LOCK_BIT) { - vTaskDelay(pdMS_TO_TICKS(LOCK_DELAY)); //delay before first lock attempt + } + else if (notification & (LOCK_BIT | REPEAT_LOCK_BIT)) + { + if (notification & LOCK_BIT) + { + vTaskDelay(pdMS_TO_TICKS(LOCK_DELAY)); // delay before first lock attempt } + gpio_set_level(board_config.socket_lock_a_gpio, 1); gpio_set_level(board_config.socket_lock_b_gpio, 0); vTaskDelay(pdMS_TO_TICKS(operating_time)); - if (is_locked()) { + if (is_locked()) + { ESP_LOGI(TAG, "Lock OK"); status = SOCKED_LOCK_STATUS_IDLE; - } else { - if (attempt > 1) { + } + else + { + if (attempt > 1) + { ESP_LOGW(TAG, "Not locked yet, repeating..."); attempt--; xTaskNotify(socket_lock_task, REPEAT_LOCK_BIT, eSetBits); - } else { + } + else + { ESP_LOGE(TAG, "Not locked"); status = SOCKED_LOCK_STATUS_LOCKING_FAIL; } @@ -117,7 +189,8 @@ static void socket_lock_task_func(void* param) } TickType_t delay_tick = xTaskGetTickCount() - previous_tick; - if (delay_tick < pdMS_TO_TICKS(break_time)) { + if (delay_tick < pdMS_TO_TICKS(break_time)) + { vTaskDelay(pdMS_TO_TICKS(break_time) - delay_tick); } previous_tick = xTaskGetTickCount(); @@ -125,34 +198,56 @@ static void socket_lock_task_func(void* param) } } +// ----------------------------------------------------------------------------- +// Init / API pública +// ----------------------------------------------------------------------------- void socket_lock_init(void) { - if (board_config.socket_lock) { - ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs)); + if (!board_config.socket_lock) + return; - nvs_get_u16(nvs, NVS_OPERATING_TIME, &operating_time); + // garante storage pronto + esp_err_t se = storage_service_init(); + if (se != ESP_OK) + ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(se)); - nvs_get_u16(nvs, NVS_BREAK_TIME, &break_time); + // Load config (best effort; se não existir, fica default) + { + uint16_t u16 = 0; + uint8_t u8 = 0; - nvs_get_u8(nvs, NVS_RETRY_COUNT, &retry_count); + esp_err_t e = storage_get_u16_sync(NVS_NAMESPACE, NVS_OPERATING_TIME, &u16, STORE_TO); + if (e == ESP_OK) operating_time = u16; + else if (e != ESP_ERR_NOT_FOUND) + ESP_LOGW(TAG, "load %s failed: %s", NVS_OPERATING_TIME, esp_err_to_name(e)); - uint8_t u8; - if (nvs_get_u8(nvs, NVS_DETECTION_HIGH, &u8) == ESP_OK) { - detection_high = u8; - } + e = storage_get_u16_sync(NVS_NAMESPACE, NVS_BREAK_TIME, &u16, STORE_TO); + if (e == ESP_OK) break_time = u16; + else if (e != ESP_ERR_NOT_FOUND) + ESP_LOGW(TAG, "load %s failed: %s", NVS_BREAK_TIME, esp_err_to_name(e)); - gpio_config_t io_conf = {}; + e = storage_get_u8_sync(NVS_NAMESPACE, NVS_RETRY_COUNT, &u8, STORE_TO); + if (e == ESP_OK) retry_count = u8; + else if (e != ESP_ERR_NOT_FOUND) + ESP_LOGW(TAG, "load %s failed: %s", NVS_RETRY_COUNT, esp_err_to_name(e)); - io_conf.mode = GPIO_MODE_OUTPUT; - io_conf.pin_bit_mask = BIT64(board_config.socket_lock_a_gpio) | BIT64(board_config.socket_lock_b_gpio); - ESP_ERROR_CHECK(gpio_config(&io_conf)); - - io_conf.mode = GPIO_MODE_INPUT; - io_conf.pin_bit_mask = BIT64(board_config.socket_lock_detection_gpio); - ESP_ERROR_CHECK(gpio_config(&io_conf)); - - xTaskCreate(socket_lock_task_func, "socket_lock_task", 2 * 1024, NULL, 10, &socket_lock_task); + e = storage_get_u8_sync(NVS_NAMESPACE, NVS_DETECTION_HIGH, &u8, STORE_TO); + if (e == ESP_OK) detection_high = (u8 != 0); + else if (e != ESP_ERR_NOT_FOUND) + ESP_LOGW(TAG, "load %s failed: %s", NVS_DETECTION_HIGH, esp_err_to_name(e)); } + + gpio_config_t io_conf = {}; + + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = BIT64(board_config.socket_lock_a_gpio) | BIT64(board_config.socket_lock_b_gpio); + ESP_ERROR_CHECK(gpio_config(&io_conf)); + + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = BIT64(board_config.socket_lock_detection_gpio); + ESP_ERROR_CHECK(gpio_config(&io_conf)); + + xTaskCreate(socket_lock_task_func, "socket_lock_task", 2 * 1024, NULL, 2, &socket_lock_task); } bool socket_lock_is_detection_high(void) @@ -164,8 +259,11 @@ void socket_lock_set_detection_high(bool _detection_high) { detection_high = _detection_high; - nvs_set_u8(nvs, NVS_DETECTION_HIGH, detection_high); - nvs_commit(nvs); + esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_DETECTION_HIGH, detection_high ? 1 : 0); + if (e != ESP_OK) + ESP_LOGW(TAG, "persist detect_hi failed: %s", esp_err_to_name(e)); + + (void)store_flush_best_effort(); } uint16_t socket_lock_get_operating_time(void) @@ -175,15 +273,22 @@ uint16_t socket_lock_get_operating_time(void) esp_err_t socket_lock_set_operating_time(uint16_t _operating_time) { - if (_operating_time < OPERATING_TIME_MIN || _operating_time > OPERATING_TIME_MAX) { + if (_operating_time < OPERATING_TIME_MIN || _operating_time > OPERATING_TIME_MAX) + { ESP_LOGE(TAG, "Operating time out of range"); return ESP_ERR_INVALID_ARG; } operating_time = _operating_time; - nvs_set_u16(nvs, NVS_OPERATING_TIME, operating_time); - nvs_commit(nvs); + esp_err_t e = store_set_u16_best_effort(NVS_NAMESPACE, NVS_OPERATING_TIME, operating_time); + if (e != ESP_OK) + { + ESP_LOGW(TAG, "persist op_time failed: %s", esp_err_to_name(e)); + return e; + } + + (void)store_flush_best_effort(); return ESP_OK; } @@ -195,8 +300,12 @@ uint8_t socket_lock_get_retry_count(void) void socket_lock_set_retry_count(uint8_t _retry_count) { retry_count = _retry_count; - nvs_set_u8(nvs, NVS_RETRY_COUNT, retry_count); - nvs_commit(nvs); + + esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_RETRY_COUNT, retry_count); + if (e != ESP_OK) + ESP_LOGW(TAG, "persist retry_count failed: %s", esp_err_to_name(e)); + + (void)store_flush_best_effort(); } uint16_t socket_lock_get_break_time(void) @@ -206,15 +315,22 @@ uint16_t socket_lock_get_break_time(void) esp_err_t socket_lock_set_break_time(uint16_t _break_time) { - if (_break_time < board_config.socket_lock_min_break_time) { - ESP_LOGE(TAG, "Operating time out of range"); + if (_break_time < board_config.socket_lock_min_break_time) + { + ESP_LOGE(TAG, "Break time out of range"); return ESP_ERR_INVALID_ARG; } break_time = _break_time; - nvs_set_u16(nvs, NVS_BREAK_TIME, break_time); - nvs_commit(nvs); + esp_err_t e = store_set_u16_best_effort(NVS_NAMESPACE, NVS_BREAK_TIME, break_time); + if (e != ESP_OK) + { + ESP_LOGW(TAG, "persist break_time failed: %s", esp_err_to_name(e)); + return e; + } + + (void)store_flush_best_effort(); return ESP_OK; } @@ -229,4 +345,4 @@ void socket_lock_set_locked(bool locked) socket_lock_status_t socket_lock_get_status(void) { return status; -} \ No newline at end of file +} diff --git a/components/peripherals/src/temp_sensor.c b/components/peripherals/src/temp_sensor.c index 0207792..8f1b056 100755 --- a/components/peripherals/src/temp_sensor.c +++ b/components/peripherals/src/temp_sensor.c @@ -38,7 +38,7 @@ void temp_sensor_init(void) lm75a_init(); - xTaskCreate(temp_sensor_task_func, "temp_sensor_task", 5 * 1024, NULL, 5, NULL); + xTaskCreate(temp_sensor_task_func, "temp_sensor_task", 5 * 1024, NULL, 2, NULL); } uint8_t temp_sensor_get_count(void) diff --git a/components/protocols/CMakeLists.txt b/components/protocols/CMakeLists.txt index 9efd1ba..d4ab773 100755 --- a/components/protocols/CMakeLists.txt +++ b/components/protocols/CMakeLists.txt @@ -1,14 +1,12 @@ idf_component_register( SRCS "src/protocols.c" - "src/json.c" "src/mqtt.c" INCLUDE_DIRS "include" PRIV_INCLUDE_DIRS "src" PRIV_REQUIRES - nvs_flash mqtt cjson vfs @@ -19,6 +17,7 @@ idf_component_register( config evse peripherals + meter_manager ocpp auth ) diff --git a/components/protocols/include/json.h b/components/protocols/include/json.h deleted file mode 100755 index c044b39..0000000 --- a/components/protocols/include/json.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef JSON_H_ -#define JSON_H_ - -#include -#include "esp_err.h" -#include "cJSON.h" - -/** - * @brief Gera um objeto JSON com a configuração atual do EVSE. - * - * Contém parâmetros como corrente máxima, limites de tempo, - * trava do conector, temperatura e configuração do OCPP. - * - * @return Ponteiro para cJSON (deve ser liberado com cJSON_Delete()). - */ -cJSON* json_get_evse_config(void); - -/** - * @brief Define a configuração do EVSE a partir de um objeto JSON. - * - * Aplica valores recebidos de protocolos como MQTT ou REST. - * - * @param root Objeto JSON com os campos válidos. - * @return ESP_OK se todos os parâmetros foram aplicados com sucesso. - */ -esp_err_t json_set_evse_config(cJSON* root); - -/** - * @brief Retorna o estado atual do EVSE em formato JSON. - * - * Inclui estado de operação, erros, limites, sessão atual e medições elétricas. - * - * @return Ponteiro para cJSON (deve ser liberado com cJSON_Delete()). - */ -cJSON* json_get_state(void); - - -#endif /* JSON_H_ */ diff --git a/components/protocols/include/mqtt.h b/components/protocols/include/mqtt.h index 20e7585..f858bf8 100755 --- a/components/protocols/include/mqtt.h +++ b/components/protocols/include/mqtt.h @@ -1,114 +1,115 @@ -#ifndef MQTT_H_ -#define MQTT_H_ +// === Início de: components/mqtt/include/mqtt.h === +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif -#include #include +#include #include "esp_err.h" +// Tamanhos máximos esperados pelos getters (buffers do chamador) +#define MQTT_SERVER_MAX_LEN 64 ///< host ou URI do broker +#define MQTT_BASE_TOPIC_MAX_LEN 64 ///< tópico base (ex: "evse") +#define MQTT_USERNAME_MAX_LEN 32 +#define MQTT_PASSWORD_MAX_LEN 64 + /** - * @file mqtt.h - * @brief MQTT configuration and control interface. + * @brief Inicializa o módulo MQTT. * - * This module provides initialization, configuration, - * and runtime access functions for the MQTT client. + * - Carrega configuração da NVS + * - Regista handlers de eventos (AUTH, SCHED, LOADBALANCER, METER, EVSE, NETWORK) + * - Cria a task de telemetria periódica * - * Configuration is persisted in NVS under namespace "mqtt". + * Não inicia a ligação ao broker imediatamente; isso acontece quando existir IP + * (NETWORK_EVENT_STA_GOT_IP) e `enabled == true`. */ - -/* -------------------------------------------------------------------------- */ -/* Definitions */ -/* -------------------------------------------------------------------------- */ - -#define MQTT_MAX_SERVER_LEN 64 /**< Max length for MQTT server URI */ -#define MQTT_MAX_USER_LEN 32 /**< Max length for MQTT username */ -#define MQTT_MAX_PASSWORD_LEN 64 /**< Max length for MQTT password */ -#define MQTT_MAX_BASE_TOPIC_LEN 64 /**< Max length for MQTT base topic */ - -/* -------------------------------------------------------------------------- */ -/* Public Functions */ -/* -------------------------------------------------------------------------- */ +esp_err_t mqtt_init(void); /** - * @brief Initialize MQTT subsystem. + * @brief Força tentativa de ligação ao broker (normalmente não é necessário chamar). * - * Loads configuration from NVS and starts background publish task - * if MQTT is enabled. Must be called once during system startup. + * Útil apenas se quiseres forçar manualmente depois de mudares config. */ -void mqtt_init(void); +esp_err_t mqtt_start(void); /** - * @brief Restart the MQTT client safely. - * - * Stops the current MQTT client (if running) and starts a new one - * with the configuration currently stored in NVS. - * - * Useful when changing Wi-Fi networks, credentials, or broker settings. - * - * @return ESP_OK on success, or an ESP_ERR_* code otherwise. + * @brief Pára o cliente MQTT e destrói o handle. */ -esp_err_t mqtt_restart(void); +void mqtt_stop(void); + +// ============================= +// Getters de configuração +// ============================= /** - * @brief Set and persist MQTT configuration parameters in NVS. - * - * Any NULL parameter will be skipped (the previous value remains stored). - * - * @param enabled Whether MQTT should be enabled (true/false). - * @param server Broker URI (e.g. "mqtt://192.168.1.10"). - * @param base_topic Base topic prefix for publish/subscribe. - * @param user MQTT username (optional). - * @param password MQTT password (optional). - * @param periodicity Publish interval (in seconds). Must be >0 when enabled. - * - * @return ESP_OK on success, or an ESP_ERR_* code otherwise. - */ -esp_err_t mqtt_set_config(bool enabled, - const char *server, - const char *base_topic, - const char *user, - const char *password, - uint16_t periodicity); - -/** - * @brief Get whether MQTT is enabled. - * - * @return true if enabled, false otherwise. + * @brief Indica se o MQTT está ativo (flag em config). */ bool mqtt_get_enabled(void); /** - * @brief Get MQTT broker URI stored in NVS. + * @brief Obtém o servidor MQTT (host ou URI). * - * @param[out] value Buffer to receive the URI (min length: MQTT_MAX_SERVER_LEN). + * @param server buffer de saída, com pelo menos MQTT_SERVER_MAX_LEN bytes. */ -void mqtt_get_server(char *value); +void mqtt_get_server(char *server); /** - * @brief Get MQTT base topic stored in NVS. + * @brief Obtém o tópico base (ex.: "evse"). * - * @param[out] value Buffer to receive the base topic (min length: MQTT_MAX_BASE_TOPIC_LEN). + * @param base_topic buffer de saída, com pelo menos MQTT_BASE_TOPIC_MAX_LEN bytes. */ -void mqtt_get_base_topic(char *value); +void mqtt_get_base_topic(char *base_topic); /** - * @brief Get MQTT username stored in NVS. + * @brief Obtém o username MQTT (se configurado). * - * @param[out] value Buffer to receive the username (min length: MQTT_MAX_USER_LEN). + * @param username buffer de saída, com pelo menos MQTT_USERNAME_MAX_LEN bytes. */ -void mqtt_get_user(char *value); +void mqtt_get_user(char *username); /** - * @brief Get MQTT password stored in NVS. + * @brief Obtém a password MQTT (se configurada). * - * @param[out] value Buffer to receive the password (min length: MQTT_MAX_PASSWORD_LEN). + * @param password buffer de saída, com pelo menos MQTT_PASSWORD_MAX_LEN bytes. */ -void mqtt_get_password(char *value); +void mqtt_get_password(char *password); /** - * @brief Get MQTT publish periodicity in seconds. - * - * @return Publish interval in seconds (default: 30). + * @brief Obtém a periodicidade da telemetria periódica (em segundos). */ uint16_t mqtt_get_periodicity(void); -#endif /* MQTT_H_ */ +// ============================= +// Setter de configuração +// ============================= + +/** + * @brief Atualiza a configuração MQTT (tipicamente chamado pelo endpoint REST). + * + * - Grava em NVS + * - Se já existir cliente ligado, pára e recria com a nova config + * - Se `enabled == true` e houver IP, tenta ligar ao broker + * + * @param enabled true para ativar MQTT + * @param host hostname ou URI (ex: "broker.hivemq.com" ou "mqtt://x.y.z") + * @param topic tópico base (ex: "evse") + * @param username username MQTT (ou NULL para manter/limpar) + * @param password password MQTT (ou NULL para manter/limpar) + * @param periodicity período da task de telemetria em segundos (ex: 30) + * + * @return ESP_OK em caso de sucesso, ou erro de NVS / MQTT. + */ +esp_err_t mqtt_set_config(bool enabled, + const char *host, + const char *topic, + const char *username, + const char *password, + int periodicity); + +#ifdef __cplusplus +} +#endif + +// === Fim de: components/mqtt/include/mqtt.h === diff --git a/components/protocols/src/json.c b/components/protocols/src/json.c deleted file mode 100755 index 42ba899..0000000 --- a/components/protocols/src/json.c +++ /dev/null @@ -1,204 +0,0 @@ -// === Início de: components/protocols/src/json.c === -#include -#include "sdkconfig.h" -#include "freertos/FreeRTOS.h" -#include "esp_wifi.h" -#include "esp_timer.h" -#include "esp_chip_info.h" -#include "esp_mac.h" -#include "esp_log.h" - -#include "json.h" -#include "mqtt.h" -#include "network.h" -#include "evse_error.h" -#include "evse_api.h" -#include "auth.h" -#include "evse_limits.h" -#include "evse_state.h" -#include "evse_config.h" -#include "ocpp.h" -#include "board_config.h" -#include "socket_lock.h" -#include "proximity.h" -#include "temp_sensor.h" -#include "evse_meter.h" - -static const char *TAG = "json"; - -// -// ===== EVSE CONFIG JSON ===== -// -cJSON *json_get_evse_config(void) -{ - cJSON *root = cJSON_CreateObject(); - if (!root) - return NULL; - - 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_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()); - cJSON_AddNumberToObject(root, "consumptionLimit", evse_get_consumption_limit()); - //cJSON_AddNumberToObject(root, "defaultConsumptionLimit", evse_get_default_consumption_limit()); - cJSON_AddNumberToObject(root, "chargingTimeLimit", evse_get_charging_time_limit()); - //cJSON_AddNumberToObject(root, "defaultChargingTimeLimit", evse_get_default_charging_time_limit()); - cJSON_AddNumberToObject(root, "underPowerLimit", evse_get_under_power_limit()); - //cJSON_AddNumberToObject(root, "defaultUnderPowerLimit", evse_get_default_under_power_limit()); - - cJSON_AddNumberToObject(root, "socketLockOperatingTime", socket_lock_get_operating_time()); - cJSON_AddNumberToObject(root, "socketLockBreakTime", socket_lock_get_break_time()); - cJSON_AddBoolToObject(root, "socketLockDetectionHigh", socket_lock_is_detection_high()); - cJSON_AddNumberToObject(root, "socketLockRetryCount", socket_lock_get_retry_count()); - - char str[64]; - cJSON_AddBoolToObject(root, "enabledocpp", ocpp_get_enabled()); - ocpp_get_server(str); - cJSON_AddStringToObject(root, "serverocpp", str); - - return root; -} - -// -// ===== SET EVSE CONFIG (from MQTT or REST) ===== -// -esp_err_t json_set_evse_config(cJSON *root) -{ - if (!root) - return ESP_ERR_INVALID_ARG; - - // Alguns setters retornam esp_err_t, outros void. Para manter compatibilidade, - // chamamos sem propagar erro (se existir, será tratado internamente). -#define SET_NUM(key, fn) \ - do \ - { \ - const cJSON *item = cJSON_GetObjectItem(root, key); \ - if (cJSON_IsNumber(item)) \ - { \ - fn(item->valuedouble); \ - } \ - } while (0) - -#define SET_BOOL(key, fn) \ - do \ - { \ - const cJSON *item = cJSON_GetObjectItem(root, key); \ - if (cJSON_IsBool(item)) \ - { \ - fn(cJSON_IsTrue(item)); \ - } \ - } while (0) - - SET_NUM("maxChargingCurrent", evse_set_max_charging_current); - SET_NUM("chargingCurrent", evse_set_charging_current); - SET_NUM("defaultChargingCurrent", evse_set_default_charging_current); - SET_BOOL("socketOutlet", evse_set_socket_outlet); - SET_BOOL("rcm", evse_set_rcm); - SET_NUM("temperatureThreshold", evse_set_temp_threshold); - SET_NUM("consumptionLimit", evse_set_consumption_limit); - //SET_NUM("defaultConsumptionLimit", evse_set_default_consumption_limit); - SET_NUM("chargingTimeLimit", evse_set_charging_time_limit); - //SET_NUM("defaultChargingTimeLimit", evse_set_default_charging_time_limit); - SET_NUM("underPowerLimit", evse_set_under_power_limit); - //SET_NUM("defaultUnderPowerLimit", evse_set_default_under_power_limit); - SET_NUM("socketLockOperatingTime", socket_lock_set_operating_time); - SET_NUM("socketLockBreakTime", socket_lock_set_break_time); - - const cJSON *retry = cJSON_GetObjectItem(root, "socketLockRetryCount"); - if (cJSON_IsNumber(retry)) - socket_lock_set_retry_count(retry->valueint); - - const cJSON *detect = cJSON_GetObjectItem(root, "socketLockDetectionHigh"); - if (cJSON_IsBool(detect)) - socket_lock_set_detection_high(cJSON_IsTrue(detect)); - - const cJSON *ocpp_enabled = cJSON_GetObjectItem(root, "enabledocpp"); - if (cJSON_IsBool(ocpp_enabled)) - ocpp_set_enabled(cJSON_IsTrue(ocpp_enabled)); - - const cJSON *ocpp_server = cJSON_GetObjectItem(root, "serverocpp"); - if (cJSON_IsString(ocpp_server)) - ocpp_set_server(cJSON_GetStringValue(ocpp_server)); - - ESP_LOGI(TAG, "EVSE configuration updated successfully"); - return ESP_OK; -} - -// -// ===== EVSE STATE JSON ===== -// -cJSON *json_get_state(void) -{ - cJSON *root = cJSON_CreateObject(); - if (!root) - return NULL; - - 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_get_mode()); - cJSON_AddBoolToObject(root, "limitReached", evse_is_limit_reached()); - - // Add error list - uint32_t error = evse_error_get_bits(); - if (error == 0) - { - cJSON_AddNullToObject(root, "errors"); - } - else - { - cJSON *errors = cJSON_CreateArray(); - if (error & EVSE_ERR_PILOT_FAULT_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("pilot_fault")); - if (error & EVSE_ERR_DIODE_SHORT_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("diode_short")); - if (error & EVSE_ERR_LOCK_FAULT_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("lock_fault")); - if (error & EVSE_ERR_UNLOCK_FAULT_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("unlock_fault")); - if (error & EVSE_ERR_RCM_TRIGGERED_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("rcm_triggered")); - if (error & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("rcm_selftest_fault")); - if (error & EVSE_ERR_TEMPERATURE_HIGH_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("temperature_high")); - if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT) - cJSON_AddItemToArray(errors, cJSON_CreateString("temperature_fault")); - cJSON_AddItemToObject(root, "errors", errors); - } - - // Session info - evse_session_t sess; - if (evse_get_session(&sess)) - { - cJSON_AddNumberToObject(root, "sessionTime", (double)sess.start_tick); - cJSON_AddNumberToObject(root, "chargingTime", (double)sess.duration_s); - cJSON_AddNumberToObject(root, "consumption", (double)sess.energy_wh); - } - else - { - cJSON_AddNullToObject(root, "sessionTime"); - cJSON_AddNumberToObject(root, "chargingTime", 0); - cJSON_AddNumberToObject(root, "consumption", 0); - } - - // Meter readings - float voltage[EVSE_METER_PHASE_COUNT]; - float current[EVSE_METER_PHASE_COUNT]; - int power[EVSE_METER_PHASE_COUNT]; - - evse_meter_get_voltage(voltage); - evse_meter_get_current(current); - evse_meter_get_power(power); - - cJSON_AddItemToObject(root, "power", cJSON_CreateIntArray(power, EVSE_METER_PHASE_COUNT)); - cJSON_AddItemToObject(root, "voltage", cJSON_CreateFloatArray(voltage, EVSE_METER_PHASE_COUNT)); - cJSON_AddItemToObject(root, "current", cJSON_CreateFloatArray(current, EVSE_METER_PHASE_COUNT)); - - return root; -} - -// === Fim de: components/protocols/src/json.c === diff --git a/components/protocols/src/mqtt.c b/components/protocols/src/mqtt.c index 0f32193..eadb935 100755 --- a/components/protocols/src/mqtt.c +++ b/components/protocols/src/mqtt.c @@ -1,437 +1,1230 @@ -// === Início de: components/protocols/src/mqtt.c === +// === Início de: components/mqtt/src/mqtt.c === + +#include "mqtt.h" + #include -#include +#include +#include +#include + +#include "esp_log.h" +#include "esp_err.h" +#include "esp_system.h" +#include "esp_event.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "freertos/semphr.h" -#include "esp_log.h" -#include "esp_event.h" #include "mqtt_client.h" -#include "nvs.h" +#include "cJSON.h" -#include "mqtt.h" -#include "json.h" -#include "board_config.h" -#include "mqtt_client.h" +// --- storage service (NVS-backed, debounced, async) --- +#include "storage_service.h" -#define NVS_NAMESPACE "mqtt" -#define NVS_ENABLED "enabled" -#define NVS_SERVER "server" -#define NVS_BASE_TOPIC "base_topic" -#define NVS_USER "user" -#define NVS_PASSWORD "password" -#define NVS_PERIODICITY "periodicity" +// --- módulos da aplicação --- +#include "network_events.h" + +#include "auth.h" +#include "auth_types.h" +#include "auth_events.h" + +#include "scheduler.h" +#include "scheduler_types.h" +#include "scheduler_events.h" + +#include "loadbalancer.h" +#include "loadbalancer_events.h" + +#include "meter_manager.h" +#include "meter_events.h" + +#include "evse_api.h" +#include "evse_config.h" +#include "evse_limits.h" +#include "evse_error.h" +#include "evse_events.h" + +// -------------------------------------------------- +// Config / estado interno +// -------------------------------------------------- static const char *TAG = "mqtt"; -static TaskHandle_t client_task = NULL; -static esp_mqtt_client_handle_t client = NULL; -static SemaphoreHandle_t mqtt_mutex = NULL; -static uint16_t periodicity = 30; // seconds -// Nem todas as versões do IDF têm esp_mqtt_client_is_connected(); usamos um flag. -static volatile bool mqtt_connected = false; +// Storage namespace/keys (mantém os mesmos nomes) +#define MQTT_NVS_NAMESPACE "mqttcfg" +#define MQTT_KEY_ENABLED "enabled" +#define MQTT_KEY_HOST "host" +#define MQTT_KEY_TOPIC "topic" +#define MQTT_KEY_USER "user" +#define MQTT_KEY_PASS "pass" +#define MQTT_KEY_PERIOD "period" -static esp_err_t open_mqtt_nvs(nvs_handle_t *handle) +#define MQTT_HOST_MAX_LEN 64 +#define MQTT_TOPIC_MAX_LEN 64 +#define MQTT_USER_MAX_LEN 32 +#define MQTT_PASS_MAX_LEN 64 + +#define MQTT_TOPIC_BUF_LEN 128 + +// Config atual em RAM +static bool s_mqtt_enabled = false; +static char s_mqtt_host[MQTT_HOST_MAX_LEN] = {0}; // host ou URI +static char s_mqtt_base_topic[MQTT_TOPIC_MAX_LEN] = "evse"; // default +static char s_mqtt_user[MQTT_USER_MAX_LEN] = {0}; +static char s_mqtt_pass[MQTT_PASS_MAX_LEN] = {0}; +static uint16_t s_mqtt_periodicity = 30; // segundos + +// Cliente MQTT +static esp_mqtt_client_handle_t s_client = NULL; +static bool s_connected = false; + +// Task de telemetria periódica +static TaskHandle_t s_telemetry_task = NULL; + +static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); } + +// -------------------------------------------------- +// Helpers STORAGE (substitui NVS direto) +// -------------------------------------------------- + +static void mqtt_load_config_from_storage(void) { - return nvs_open(NVS_NAMESPACE, NVS_READWRITE, handle); -} - -// -// Helper: safely publish JSON message -// -static void publish_message(const char *topic_suffix, cJSON *root) -{ - if (!root) return; - - if (xSemaphoreTake(mqtt_mutex, pdMS_TO_TICKS(500)) == pdTRUE) + esp_err_t err = storage_service_init(); + if (err != ESP_OK) { - if (client && mqtt_connected) + ESP_LOGE(TAG, "storage_service_init failed: %s", esp_err_to_name(err)); + return; + } + + bool needs_flush = false; + + // enabled + { + uint8_t u8 = 0; + err = storage_get_u8_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_ENABLED, &u8, TO_TICKS_MS(500)); + if (err == ESP_OK && u8 <= 1) + s_mqtt_enabled = (u8 != 0); + else { - char base[MQTT_MAX_BASE_TOPIC_LEN] = {0}; - mqtt_get_base_topic(base); + s_mqtt_enabled = false; + (void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_ENABLED, 0); + needs_flush = true; + } + } - char topic[128]; - snprintf(topic, sizeof(topic), "%s%s", base, topic_suffix); - - char *json = cJSON_PrintUnformatted(root); - if (json) - { - esp_mqtt_client_publish(client, topic, json, 0, 1, 0); - ESP_LOGI(TAG, "Published to %s", topic); - free(json); - } + // host + { + char tmp[MQTT_HOST_MAX_LEN] = {0}; + err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_HOST, tmp, sizeof(tmp), TO_TICKS_MS(500)); + if (err == ESP_OK) + { + strncpy(s_mqtt_host, tmp, sizeof(s_mqtt_host)); + s_mqtt_host[sizeof(s_mqtt_host) - 1] = '\0'; } else { - ESP_LOGW(TAG, "MQTT client not connected — skipping publish"); + s_mqtt_host[0] = '\0'; + // opcional: persistir vazio (para chave existir) + (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_HOST, ""); + needs_flush = true; } - xSemaphoreGive(mqtt_mutex); } - else + + // topic { - ESP_LOGW(TAG, "MQTT mutex timeout during publish"); - } -} - -// -// Subscriptions -// -static void subscribe_topics(void) -{ - if (!client) return; - - char base[MQTT_MAX_BASE_TOPIC_LEN] = {0}; - mqtt_get_base_topic(base); - - const char *subs[] = { - "/request/#", - "/set/config/#", - "/enable", - "/request/restart", - "/request/config/evse", - "/request/boardConfig" - }; - - for (size_t i = 0; i < sizeof(subs) / sizeof(subs[0]); i++) - { - char topic[MQTT_MAX_BASE_TOPIC_LEN + 32]; - snprintf(topic, sizeof(topic), "%s%s", base, subs[i]); - esp_mqtt_client_subscribe(client, topic, 0); - ESP_LOGI(TAG, "[MQTT] Subscribed: %s", topic); - } -} - -// -// Message handler -// -static void handle_message(const char *topic, const char *data) -{ - if (!topic || !data) return; - - char base[MQTT_MAX_BASE_TOPIC_LEN] = {0}; - mqtt_get_base_topic(base); - - if (strncmp(topic, base, strlen(base)) != 0) return; - - const char *sub_topic = &topic[strlen(base)]; - ESP_LOGI(TAG, "[MQTT] Received on %s: %s", sub_topic, data); - - if (strcmp(sub_topic, "/request/config/evse") == 0) - { - cJSON *root = json_get_evse_config(); - publish_message("/response/config/evse", root); - cJSON_Delete(root); - } - else if (strcmp(sub_topic, "/set/config/evse") == 0) - { - cJSON *root = cJSON_Parse(data); - if (!root) + char tmp[MQTT_TOPIC_MAX_LEN] = {0}; + err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_TOPIC, tmp, sizeof(tmp), TO_TICKS_MS(500)); + if (err == ESP_OK && tmp[0] != '\0') { - ESP_LOGE(TAG, "Invalid JSON payload on topic %s", topic); - return; + strncpy(s_mqtt_base_topic, tmp, sizeof(s_mqtt_base_topic)); + s_mqtt_base_topic[sizeof(s_mqtt_base_topic) - 1] = '\0'; + } + else + { + strncpy(s_mqtt_base_topic, "evse", sizeof(s_mqtt_base_topic)); + s_mqtt_base_topic[sizeof(s_mqtt_base_topic) - 1] = '\0'; + (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_TOPIC, s_mqtt_base_topic); + needs_flush = true; } - json_set_evse_config(root); - cJSON_Delete(root); } - else if (strcmp(sub_topic, "/request/restart") == 0) + + // user { - ESP_LOGW(TAG, "Restart request received (TODO: add auth check)"); - // esp_restart(); + char tmp[MQTT_USER_MAX_LEN] = {0}; + err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_USER, tmp, sizeof(tmp), TO_TICKS_MS(500)); + if (err == ESP_OK) + { + strncpy(s_mqtt_user, tmp, sizeof(s_mqtt_user)); + s_mqtt_user[sizeof(s_mqtt_user) - 1] = '\0'; + } + else + { + s_mqtt_user[0] = '\0'; + (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_USER, ""); + needs_flush = true; + } } + + // pass + { + char tmp[MQTT_PASS_MAX_LEN] = {0}; + err = storage_get_str_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_PASS, tmp, sizeof(tmp), TO_TICKS_MS(500)); + if (err == ESP_OK) + { + strncpy(s_mqtt_pass, tmp, sizeof(s_mqtt_pass)); + s_mqtt_pass[sizeof(s_mqtt_pass) - 1] = '\0'; + } + else + { + s_mqtt_pass[0] = '\0'; + (void)storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PASS, ""); + needs_flush = true; + } + } + + // period + { + uint16_t u16 = 30; + err = storage_get_u16_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_PERIOD, &u16, TO_TICKS_MS(500)); + if (err == ESP_OK && u16 > 0 && u16 < 3600) + s_mqtt_periodicity = u16; + else + { + s_mqtt_periodicity = 30; + (void)storage_set_u16_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PERIOD, s_mqtt_periodicity); + needs_flush = true; + } + } + + if (needs_flush) + { + // não bloqueia chamador; commit imediato no storage_task + (void)storage_flush_async(); + } + + ESP_LOGI(TAG, "Config MQTT (storage): enabled=%d host='%s' topic='%s' period=%u user='%s'", + s_mqtt_enabled, s_mqtt_host, s_mqtt_base_topic, (unsigned)s_mqtt_periodicity, s_mqtt_user); } -// -// MQTT event handler -// -static void event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +static esp_err_t mqtt_save_config_to_storage(void) { - esp_mqtt_event_handle_t event = event_data; + esp_err_t err = storage_service_init(); + if (err != ESP_OK) + return err; - switch (event_id) + esp_err_t e = ESP_OK; + + e = storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_ENABLED, s_mqtt_enabled ? 1 : 0); + if (e != ESP_OK) + err = e; + + e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_HOST, s_mqtt_host); + if (e != ESP_OK) + err = e; + + e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_TOPIC, s_mqtt_base_topic); + if (e != ESP_OK) + err = e; + + e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_USER, s_mqtt_user); + if (e != ESP_OK) + err = e; + + e = storage_set_str_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PASS, s_mqtt_pass); + if (e != ESP_OK) + err = e; + + e = storage_set_u16_async(MQTT_NVS_NAMESPACE, MQTT_KEY_PERIOD, s_mqtt_periodicity); + if (e != ESP_OK) + err = e; + + // commit imediato sem bloquear o caller + (void)storage_flush_async(); + + if (err != ESP_OK) + ESP_LOGE(TAG, "mqtt_save_config_to_storage had errors: %s", esp_err_to_name(err)); + + return err; +} + +// -------------------------------------------------- +// Getters públicos (usados por REST /config/mqtt) +// -------------------------------------------------- + +bool mqtt_get_enabled(void) { return s_mqtt_enabled; } + +void mqtt_get_server(char *server) +{ + if (!server) + return; + strncpy(server, s_mqtt_host, MQTT_HOST_MAX_LEN); + server[MQTT_HOST_MAX_LEN - 1] = '\0'; +} + +void mqtt_get_base_topic(char *base_topic) +{ + if (!base_topic) + return; + strncpy(base_topic, s_mqtt_base_topic, MQTT_TOPIC_MAX_LEN); + base_topic[MQTT_TOPIC_MAX_LEN - 1] = '\0'; +} + +void mqtt_get_user(char *username) +{ + if (!username) + return; + strncpy(username, s_mqtt_user, MQTT_USER_MAX_LEN); + username[MQTT_USER_MAX_LEN - 1] = '\0'; +} + +void mqtt_get_password(char *password) +{ + if (!password) + return; + strncpy(password, s_mqtt_pass, MQTT_PASS_MAX_LEN); + password[MQTT_PASS_MAX_LEN - 1] = '\0'; +} + +uint16_t mqtt_get_periodicity(void) { return s_mqtt_periodicity; } + +// -------------------------------------------------- +// Helpers de tópico / publish +// -------------------------------------------------- + +static void mqtt_build_topic(char *out, size_t out_len, const char *suffix) +{ + const char *base = (s_mqtt_base_topic[0] != '\0') ? s_mqtt_base_topic : "evse"; + if (suffix && suffix[0] != '\0') + snprintf(out, out_len, "%s/%s", base, suffix); + else + snprintf(out, out_len, "%s", base); +} + +static void mqtt_publish_raw(const char *subtopic, const char *payload, bool retain) +{ + if (!s_client || !s_connected || !s_mqtt_enabled || !payload) + return; + + char topic[MQTT_TOPIC_BUF_LEN]; + mqtt_build_topic(topic, sizeof(topic), subtopic); + + int msg_id = esp_mqtt_client_publish( + s_client, + topic, + payload, + 0, + 1, + retain ? 1 : 0); + + ESP_LOGD(TAG, "MQTT publish [%s] (id=%d): %s", topic, msg_id, payload); +} + +static void mqtt_publish_json(const char *subtopic, cJSON *obj, bool retain) +{ + if (!obj) + return; + + char *str = cJSON_PrintUnformatted(obj); + if (!str) { - case MQTT_EVENT_CONNECTED: - ESP_LOGI(TAG, "[MQTT] Connected to broker"); - mqtt_connected = true; - if (client_task) vTaskResume(client_task); - subscribe_topics(); - break; - - case MQTT_EVENT_DATA: - { - char topic[128] = {0}; - char data_buf[512] = {0}; - - int tlen = MIN(event->topic_len, (int)sizeof(topic) - 1); - int dlen = MIN(event->data_len, (int)sizeof(data_buf) - 1); - - memcpy(topic, event->topic, tlen); - memcpy(data_buf, event->data, dlen); - - handle_message(topic, data_buf); - break; + cJSON_Delete(obj); + return; } - case MQTT_EVENT_DISCONNECTED: - ESP_LOGW(TAG, "[MQTT] Disconnected from broker"); - mqtt_connected = false; - break; + mqtt_publish_raw(subtopic, str, retain); + + free(str); + cJSON_Delete(obj); +} + +// -------------------------------------------------- +// Helpers scheduler HH:MM +// -------------------------------------------------- + +static bool parse_hhmm(const char *s, uint16_t *out_min) +{ + if (!s || !out_min) + return false; + + int h = 0, m = 0; + if (sscanf(s, "%d:%d", &h, &m) != 2) + return false; + if (h < 0 || h > 23 || m < 0 || m > 59) + return false; + + *out_min = (uint16_t)(h * 60 + m); + return true; +} + +static void format_hhmm(uint16_t minutes, char *buf, size_t buf_sz) +{ + if (!buf || buf_sz < 6) + return; + minutes %= (24 * 60); + int h = minutes / 60; + int m = minutes % 60; + snprintf(buf, buf_sz, "%02d:%02d", h, m); +} + +// -------------------------------------------------- +// Publicar estado de cada componente +// -------------------------------------------------- + +// ---- AUTH ---- + +static void mqtt_auth_publish_state(void) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) + return; + + auth_mode_t mode = auth_get_mode(); + cJSON_AddStringToObject(root, "mode", auth_mode_to_str(mode)); + + int count = auth_get_tag_count(); + cJSON *tags = cJSON_CreateArray(); + for (int i = 0; i < count; ++i) + { + const char *tag = auth_get_tag_by_index(i); + if (tag) + cJSON_AddItemToArray(tags, cJSON_CreateString(tag)); + } + cJSON_AddItemToObject(root, "tags", tags); + + mqtt_publish_json("state/auth", root, true); +} + +// ---- Scheduler ---- + +static void mqtt_scheduler_publish_state(void) +{ + sched_config_t cfg = scheduler_get_config(); + bool allowed_now = scheduler_is_allowed_now(); + + cJSON *root = cJSON_CreateObject(); + if (!root) + return; + + cJSON_AddBoolToObject(root, "enabled", cfg.enabled); + cJSON_AddStringToObject(root, "mode", sched_mode_to_str(cfg.mode)); + + char buf[8]; + format_hhmm(cfg.start_min, buf, sizeof(buf)); + cJSON_AddStringToObject(root, "startTime", buf); + format_hhmm(cfg.end_min, buf, sizeof(buf)); + cJSON_AddStringToObject(root, "endTime", buf); + + cJSON_AddBoolToObject(root, "allowedNow", allowed_now); + + mqtt_publish_json("state/scheduler", root, true); +} + +// ---- Loadbalancer ---- + +static void mqtt_loadbalancer_publish_state(void) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) + return; + + bool enabled = loadbalancer_is_enabled(); + uint8_t limit = load_balancing_get_max_grid_current(); + + cJSON_AddBoolToObject(root, "loadBalancingEnabled", enabled); + cJSON_AddNumberToObject(root, "loadBalancingCurrentLimit", limit); + + mqtt_publish_json("state/loadbalancing", root, true); +} + +// ---- Meters: config + live ---- + +static void mqtt_meters_publish_config(void) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) + return; + + meter_type_t grid = meter_manager_grid_get_model(); + meter_type_t evse = meter_manager_evse_get_model(); + + cJSON_AddStringToObject(root, "gridmeter", meter_type_to_str(grid)); + cJSON_AddStringToObject(root, "evsemeter", meter_type_to_str(evse)); + + mqtt_publish_json("state/meters-config", root, true); +} + +static void mqtt_meters_publish_live(const meter_event_data_t *m, int64_t ts_us) +{ + if (!m) + return; + + cJSON *root = cJSON_CreateObject(); + if (!root) + return; + + cJSON_AddStringToObject(root, "source", m->source ? m->source : "GRID"); + cJSON_AddNumberToObject(root, "frequency", m->frequency); + cJSON_AddNumberToObject(root, "powerFactor", m->power_factor); + cJSON_AddNumberToObject(root, "totalEnergy", m->total_energy); + cJSON_AddNumberToObject(root, "timestampUs", (double)ts_us); + + cJSON *vr = cJSON_CreateArray(); + cJSON *ir = cJSON_CreateArray(); + cJSON *pw = cJSON_CreateArray(); + for (int i = 0; i < 3; ++i) + { + cJSON_AddItemToArray(vr, cJSON_CreateNumber(m->vrms[i])); + cJSON_AddItemToArray(ir, cJSON_CreateNumber(m->irms[i])); + cJSON_AddItemToArray(pw, cJSON_CreateNumber(m->watt[i])); + } + cJSON_AddItemToObject(root, "vrms", vr); + cJSON_AddItemToObject(root, "irms", ir); + cJSON_AddItemToObject(root, "watt", pw); + + const char *suffix = (m->source && strcmp(m->source, "EVSE") == 0) + ? "state/meter/evse" + : "state/meter/grid"; + + mqtt_publish_json(suffix, root, false); +} + +// ---- EVSE ---- + +static void mqtt_evse_publish_state(void) +{ + evse_state_t state = evse_get_state(); + + cJSON *dash = cJSON_CreateObject(); + if (!dash) + return; + + cJSON_AddStringToObject(dash, "status", evse_state_to_str(state)); + + cJSON *chargers = cJSON_CreateArray(); + cJSON *ch = cJSON_CreateObject(); + cJSON_AddNumberToObject(ch, "id", 1); + cJSON_AddStringToObject(ch, "status", evse_state_to_str(state)); + cJSON_AddNumberToObject(ch, "current", evse_get_runtime_charging_current()); + cJSON_AddNumberToObject(ch, "maxCurrent", evse_get_charging_current()); + + int power = evse_get_runtime_charging_current() * 230; + cJSON_AddNumberToObject(ch, "power", power); + + cJSON_AddItemToArray(chargers, ch); + cJSON_AddItemToObject(dash, "chargers", chargers); + + cJSON_AddNumberToObject(dash, "energyConsumed", evse_get_consumption_limit()); + cJSON_AddNumberToObject(dash, "chargingTime", evse_get_charging_time_limit()); + + cJSON *alerts = cJSON_CreateArray(); + if (evse_is_limit_reached()) + cJSON_AddItemToArray(alerts, cJSON_CreateString("Limite de consumo atingido.")); + if (!evse_config_is_available()) + cJSON_AddItemToArray(alerts, cJSON_CreateString("Estação indisponível.")); + if (!evse_config_is_enabled()) + cJSON_AddItemToArray(alerts, cJSON_CreateString("EVSE desativado.")); + cJSON_AddItemToObject(dash, "alerts", alerts); + + uint32_t error_bits = evse_get_error(); + cJSON *errors = cJSON_CreateArray(); + if (error_bits & EVSE_ERR_DIODE_SHORT_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Diodo curto-circuitado")); + if (error_bits & EVSE_ERR_LOCK_FAULT_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no travamento")); + if (error_bits & EVSE_ERR_UNLOCK_FAULT_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no destravamento")); + if (error_bits & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no autoteste do RCM")); + if (error_bits & EVSE_ERR_RCM_TRIGGERED_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("RCM disparado")); + if (error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Temperatura elevada")); + if (error_bits & EVSE_ERR_PILOT_FAULT_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Erro no sinal piloto")); + if (error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT) + cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no sensor de temperatura")); + cJSON_AddItemToObject(dash, "errors", errors); + + mqtt_publish_json("state/evse", dash, true); +} + +// -------------------------------------------------- +// Handlers de eventos de módulo -> publish +// -------------------------------------------------- + +static void mqtt_auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)data; + if (base != AUTH_EVENTS) + return; + + switch (id) + { + case AUTH_EVENT_INIT: + case AUTH_EVENT_MODE_CHANGED: + case AUTH_EVENT_TAG_SAVED: + mqtt_auth_publish_state(); + break; default: break; } } -// -// Background periodic publisher -// -static void client_task_func(void *param) +static void mqtt_scheduler_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { - for (;;) - { - if (client && mqtt_connected) - { - cJSON *root = json_get_state(); - publish_message("/state", root); - cJSON_Delete(root); - } - else - { - ESP_LOGD(TAG, "[MQTT] Not connected — skipping state publish"); - } + (void)arg; + (void)data; + if (base != SCHED_EVENTS) + return; - vTaskDelay(pdMS_TO_TICKS(periodicity * 1000)); + switch (id) + { + case SCHED_EVENT_INIT: + case SCHED_EVENT_WINDOW_CHANGED: + mqtt_scheduler_publish_state(); + break; + default: + break; } } -// -// Start/stop MQTT -// -static void client_start(void) +static void mqtt_loadbalancer_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { - char server[MQTT_MAX_SERVER_LEN] = {0}; - char user[MQTT_MAX_USER_LEN] = {0}; - char password[MQTT_MAX_PASSWORD_LEN] = {0}; + (void)arg; + (void)data; + if (base != LOADBALANCER_EVENTS) + return; - mqtt_get_server(server); - mqtt_get_user(user); - mqtt_get_password(password); - - if (!strlen(server)) + switch (id) { - ESP_LOGW(TAG, "[MQTT] No server configured"); + case LOADBALANCER_EVENT_INIT: + case LOADBALANCER_EVENT_STATE_CHANGED: + case LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT: + mqtt_loadbalancer_publish_state(); + break; + default: + break; + } +} + +static void mqtt_meter_event_handler(void *arg, esp_event_base_t base, int32_t id, void *ev_data) +{ + (void)arg; + if (base != METER_EVENT) + return; + + switch (id) + { + case METER_EVENT_DATA_READY: + { + const meter_event_data_t *m = (const meter_event_data_t *)ev_data; + int64_t now = esp_timer_get_time(); + mqtt_meters_publish_live(m, now); + break; + } + case METER_EVENT_CONFIG_UPDATED: + mqtt_meters_publish_config(); + break; + default: + break; + } +} + +static void mqtt_evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)data; + if (base != EVSE_EVENTS) + return; + + switch (id) + { + case EVSE_EVENT_INIT: + case EVSE_EVENT_STATE_CHANGED: + case EVSE_EVENT_CONFIG_UPDATED: + case EVSE_EVENT_ENABLE_UPDATED: + case EVSE_EVENT_AVAILABLE_UPDATED: + case EVSE_EVENT_ERROR_CHANGED: + mqtt_evse_publish_state(); + break; + default: + break; + } +} + +// -------------------------------------------------- +// Comandos MQTT (JSON -> chamar APIs públicas) +// -------------------------------------------------- + +static void handle_cmd_auth_mode_set(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in auth/mode/set"); return; } - esp_mqtt_client_config_t cfg = { - .broker.address.uri = server, - .credentials.username = user, - .credentials.authentication.password = password - }; - - if (!client) + cJSON *j_mode = cJSON_GetObjectItem(json, "mode"); + if (cJSON_IsString(j_mode) && j_mode->valuestring) { - client = esp_mqtt_client_init(&cfg); - esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, event_handler, client); - esp_mqtt_client_start(client); - ESP_LOGI(TAG, "[MQTT] Client started"); - } -} - -static void client_stop(void) -{ - if (client) - { - esp_mqtt_client_stop(client); - esp_mqtt_client_destroy(client); - client = NULL; - mqtt_connected = false; - ESP_LOGI(TAG, "[MQTT] Client stopped"); - } -} - -esp_err_t mqtt_restart(void) -{ - ESP_LOGI(TAG, "[MQTT] Restarting client..."); - - if (client) - { - esp_mqtt_client_stop(client); - esp_mqtt_client_destroy(client); - client = NULL; - mqtt_connected = false; - ESP_LOGI(TAG, "[MQTT] Existing client stopped"); - } - - vTaskDelay(pdMS_TO_TICKS(500)); - client_start(); - - if (client) - { - ESP_LOGI(TAG, "[MQTT] Restart complete"); - return ESP_OK; - } - - ESP_LOGE(TAG, "[MQTT] Restart failed — no valid configuration"); - return ESP_FAIL; -} - -// -// Public API -// -void mqtt_init(void) -{ - if (!mqtt_mutex) - mqtt_mutex = xSemaphoreCreateMutex(); - - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) == ESP_OK) - { - nvs_get_u16(handle, NVS_PERIODICITY, &periodicity); - nvs_close(handle); - if (periodicity == 0) periodicity = 30; - } - - esp_register_shutdown_handler(&client_stop); - xTaskCreate(client_task_func, "mqtt_client_task", 4 * 1024, NULL, 5, &client_task); - - if (mqtt_get_enabled()) - { - client_start(); - } -} - -esp_err_t mqtt_set_config(bool enabled, const char *server, const char *base_topic, - const char *user, const char *password, uint16_t _periodicity) -{ - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) != ESP_OK) - return ESP_ERR_INVALID_STATE; - - char full_server[MQTT_MAX_SERVER_LEN]; - if (server && strncmp(server, "mqtt://", 7) != 0 && strncmp(server, "tcp://", 6) != 0) - { - snprintf(full_server, sizeof(full_server), "mqtt://%s", server); - server = full_server; - } - - if (enabled) - { - if (!server || !*server || !base_topic || !*base_topic || !_periodicity) + auth_mode_t mode; + if (auth_mode_from_str(j_mode->valuestring, &mode)) { - nvs_close(handle); - return ESP_ERR_INVALID_ARG; + ESP_LOGI(TAG, "MQTT: setting auth mode = %s", j_mode->valuestring); + auth_set_mode(mode); + } + else + { + ESP_LOGW(TAG, "Invalid auth mode: %s", j_mode->valuestring); } } - if (server) nvs_set_str(handle, NVS_SERVER, server); - if (base_topic) nvs_set_str(handle, NVS_BASE_TOPIC, base_topic); - if (user) nvs_set_str(handle, NVS_USER, user); - if (password) nvs_set_str(handle, NVS_PASSWORD, password); + cJSON_Delete(json); +} - nvs_set_u8(handle, NVS_ENABLED, enabled ? 1 : 0); - nvs_set_u16(handle, NVS_PERIODICITY, _periodicity); - periodicity = _periodicity ? _periodicity : periodicity; +static void handle_cmd_auth_tag_add(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in auth/tags/add"); + return; + } - esp_err_t err = nvs_commit(handle); - nvs_close(handle); + cJSON *j_tag = cJSON_GetObjectItem(json, "tag"); + if (cJSON_IsString(j_tag) && j_tag->valuestring) + { + ESP_LOGI(TAG, "MQTT: adding tag '%s'", j_tag->valuestring); + if (!auth_add_tag(j_tag->valuestring)) + ESP_LOGW(TAG, "auth_add_tag failed"); + mqtt_auth_publish_state(); + } - if (err != ESP_OK) - return err; + cJSON_Delete(json); +} - if (enabled) - client_start(); +static void handle_cmd_auth_tag_remove(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in auth/tags/remove"); + return; + } + + cJSON *j_tag = cJSON_GetObjectItem(json, "tag"); + if (cJSON_IsString(j_tag) && j_tag->valuestring) + { + ESP_LOGI(TAG, "MQTT: removing tag '%s'", j_tag->valuestring); + if (!auth_remove_tag(j_tag->valuestring)) + ESP_LOGW(TAG, "auth_remove_tag failed"); + mqtt_auth_publish_state(); + } + + cJSON_Delete(json); +} + +static void handle_cmd_scheduler_set(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in scheduler/set"); + return; + } + + sched_config_t cfg = scheduler_get_config(); + + cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled"); + if (cJSON_IsBool(j_enabled)) + cfg.enabled = cJSON_IsTrue(j_enabled); + + cJSON *j_mode = cJSON_GetObjectItem(json, "mode"); + if (cJSON_IsString(j_mode) && j_mode->valuestring) + { + sched_mode_t m; + if (!sched_mode_from_str(j_mode->valuestring, &m)) + { + ESP_LOGW(TAG, "Invalid scheduler mode: %s", j_mode->valuestring); + cJSON_Delete(json); + return; + } + cfg.mode = m; + } + + cJSON *j_start = cJSON_GetObjectItem(json, "startTime"); + if (cJSON_IsString(j_start) && j_start->valuestring) + { + uint16_t minutes; + if (!parse_hhmm(j_start->valuestring, &minutes)) + { + ESP_LOGW(TAG, "Invalid startTime: %s", j_start->valuestring); + cJSON_Delete(json); + return; + } + cfg.start_min = minutes; + } + + cJSON *j_end = cJSON_GetObjectItem(json, "endTime"); + if (cJSON_IsString(j_end) && j_end->valuestring) + { + uint16_t minutes; + if (!parse_hhmm(j_end->valuestring, &minutes)) + { + ESP_LOGW(TAG, "Invalid endTime: %s", j_end->valuestring); + cJSON_Delete(json); + return; + } + cfg.end_min = minutes; + } + + scheduler_set_config(&cfg); + cJSON_Delete(json); +} + +static void handle_cmd_loadbalancing_set(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in loadbalancing/set"); + return; + } + + cJSON *j_enabled = cJSON_GetObjectItem(json, "loadBalancingEnabled"); + if (j_enabled && cJSON_IsBool(j_enabled)) + { + bool enabled = cJSON_IsTrue(j_enabled); + ESP_LOGI(TAG, "MQTT: set loadbalancer enabled = %d", enabled); + loadbalancer_set_enabled(enabled); + } + + cJSON *j_limit = cJSON_GetObjectItem(json, "loadBalancingCurrentLimit"); + if (j_limit && cJSON_IsNumber(j_limit)) + { + int limit = j_limit->valueint; + if (limit < 6 || limit > 100) + { + ESP_LOGW(TAG, "Invalid loadBalancingCurrentLimit: %d", limit); + } + else + { + ESP_LOGI(TAG, "MQTT: set max_grid_current = %d", limit); + esp_err_t err = load_balancing_set_max_grid_current((uint8_t)limit); + if (err != ESP_OK) + ESP_LOGE(TAG, "load_balancing_set_max_grid_current failed: %s", esp_err_to_name(err)); + } + } + + cJSON_Delete(json); +} + +static void handle_cmd_meters_set(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in meters/set"); + return; + } + + cJSON *j_grid = cJSON_GetObjectItem(json, "gridmeter"); + if (j_grid && cJSON_IsString(j_grid)) + { + meter_type_t type = string_to_meter_type(j_grid->valuestring); + ESP_LOGI(TAG, "MQTT: set gridmeter = %s", j_grid->valuestring); + meter_manager_grid_set_model(type); + } + + cJSON *j_evse = cJSON_GetObjectItem(json, "evsemeter"); + if (j_evse && cJSON_IsString(j_evse)) + { + meter_type_t type = string_to_meter_type(j_evse->valuestring); + ESP_LOGI(TAG, "MQTT: set evsemeter = %s", j_evse->valuestring); + meter_manager_evse_set_model(type); + } + + cJSON_Delete(json); +} + +static void handle_cmd_evse_settings(const char *payload) +{ + cJSON *json = cJSON_Parse(payload); + if (!json) + { + ESP_LOGW(TAG, "Invalid JSON in evse/settings"); + return; + } + + cJSON *j_current = cJSON_GetObjectItem(json, "currentLimit"); + if (j_current && cJSON_IsNumber(j_current)) + { + int curr = j_current->valueint; + ESP_LOGI(TAG, "MQTT: set EVSE currentLimit = %d", curr); + evse_set_charging_current((uint8_t)curr); + } + + cJSON *j_temp = cJSON_GetObjectItem(json, "temperatureLimit"); + if (j_temp && cJSON_IsNumber(j_temp)) + { + int t = j_temp->valueint; + ESP_LOGI(TAG, "MQTT: set EVSE temperatureLimit = %d", t); + evse_set_temp_threshold((uint8_t)t); + } + + cJSON_Delete(json); + mqtt_evse_publish_state(); +} + +// -------------------------------------------------- +// Handler de eventos MQTT +// -------------------------------------------------- + +static void mqtt_subscribe_all(void) +{ + if (!s_client) + return; + + char topic[MQTT_TOPIC_BUF_LEN]; + + mqtt_build_topic(topic, sizeof(topic), "cmd/auth/mode/set"); + esp_mqtt_client_subscribe(s_client, topic, 1); + + mqtt_build_topic(topic, sizeof(topic), "cmd/auth/tags/add"); + esp_mqtt_client_subscribe(s_client, topic, 1); + + mqtt_build_topic(topic, sizeof(topic), "cmd/auth/tags/remove"); + esp_mqtt_client_subscribe(s_client, topic, 1); + + mqtt_build_topic(topic, sizeof(topic), "cmd/scheduler/set"); + esp_mqtt_client_subscribe(s_client, topic, 1); + + mqtt_build_topic(topic, sizeof(topic), "cmd/loadbalancing/set"); + esp_mqtt_client_subscribe(s_client, topic, 1); + + mqtt_build_topic(topic, sizeof(topic), "cmd/meters/set"); + esp_mqtt_client_subscribe(s_client, topic, 1); + + mqtt_build_topic(topic, sizeof(topic), "cmd/evse/settings"); + esp_mqtt_client_subscribe(s_client, topic, 1); +} + +static void mqtt_dispatch_command(const char *topic, const char *data) +{ + if (!topic || !data) + return; + + const char *base = (s_mqtt_base_topic[0] != '\0') ? s_mqtt_base_topic : "evse"; + size_t base_len = strlen(base); + + if (strncmp(topic, base, base_len) != 0) + return; + + const char *sub = topic + base_len; + if (*sub == '/') + sub++; + + if (strcmp(sub, "cmd/auth/mode/set") == 0) + handle_cmd_auth_mode_set(data); + else if (strcmp(sub, "cmd/auth/tags/add") == 0) + handle_cmd_auth_tag_add(data); + else if (strcmp(sub, "cmd/auth/tags/remove") == 0) + handle_cmd_auth_tag_remove(data); + else if (strcmp(sub, "cmd/scheduler/set") == 0) + handle_cmd_scheduler_set(data); + else if (strcmp(sub, "cmd/loadbalancing/set") == 0) + handle_cmd_loadbalancing_set(data); + else if (strcmp(sub, "cmd/meters/set") == 0) + handle_cmd_meters_set(data); + else if (strcmp(sub, "cmd/evse/settings") == 0) + handle_cmd_evse_settings(data); else - client_stop(); + ESP_LOGW(TAG, "MQTT command in unknown topic: %s", topic); +} + +static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) +{ + (void)event->client; + + switch (event->event_id) + { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + s_connected = true; + mqtt_subscribe_all(); + + mqtt_auth_publish_state(); + mqtt_scheduler_publish_state(); + mqtt_loadbalancer_publish_state(); + mqtt_meters_publish_config(); + mqtt_evse_publish_state(); + break; + + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + s_connected = false; + break; + + case MQTT_EVENT_DATA: + { + int tlen = event->topic_len; + int dlen = event->data_len; + + if (tlen <= 0 || dlen <= 0 || !event->topic || !event->data) + break; + + char *topic = (char *)malloc((size_t)tlen + 1); + char *payload = (char *)malloc((size_t)dlen + 1); + if (!topic || !payload) + { + free(topic); + free(payload); + ESP_LOGE(TAG, "MQTT_EVENT_DATA: malloc failed"); + break; + } + + memcpy(topic, event->topic, (size_t)tlen); + topic[tlen] = '\0'; + + memcpy(payload, event->data, (size_t)dlen); + payload[dlen] = '\0'; + + ESP_LOGI(TAG, "MQTT DATA topic='%s' payload='%s'", topic, payload); + mqtt_dispatch_command(topic, payload); + + free(topic); + free(payload); + break; + } + + default: + break; + } return ESP_OK; } -/* ---------------------- Getters públicos (faltavam) ---------------------- */ - -bool mqtt_get_enabled(void) +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { - nvs_handle_t handle; - uint8_t en = 0; - if (open_mqtt_nvs(&handle) == ESP_OK) + (void)handler_args; + (void)base; + (void)event_id; + mqtt_event_handler_cb((esp_mqtt_event_handle_t)event_data); +} + +// -------------------------------------------------- +// Ligação MQTT (start/stop) + handler de rede +// -------------------------------------------------- + +static void mqtt_client_start_if_needed(void) +{ + if (!s_mqtt_enabled) { - nvs_get_u8(handle, NVS_ENABLED, &en); - nvs_close(handle); + ESP_LOGI(TAG, "MQTT disabled, not starting client"); + return; } - return en != 0; -} -static void nvs_get_str_into(nvs_handle_t handle, const char *key, char *out, size_t outlen) -{ - if (!out || outlen == 0) return; - out[0] = '\0'; - size_t len = outlen; - esp_err_t err = nvs_get_str(handle, key, out, &len); - if (err != ESP_OK) out[0] = '\0'; -} - -void mqtt_get_server(char *value) -{ - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) == ESP_OK) + if (s_client) { - nvs_get_str_into(handle, NVS_SERVER, value, MQTT_MAX_SERVER_LEN); - nvs_close(handle); + ESP_LOGI(TAG, "MQTT client already created"); + return; } - else if (value) value[0] = '\0'; -} -void mqtt_get_base_topic(char *value) -{ - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) == ESP_OK) + if (s_mqtt_host[0] == '\0') { - nvs_get_str_into(handle, NVS_BASE_TOPIC, value, MQTT_MAX_BASE_TOPIC_LEN); - nvs_close(handle); + ESP_LOGW(TAG, "MQTT host not set"); + return; } - else if (value) value[0] = '\0'; -} -void mqtt_get_user(char *value) -{ - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) == ESP_OK) + char uri[MQTT_HOST_MAX_LEN + 10]; + + if (strncmp(s_mqtt_host, "mqtt://", 7) == 0 || + strncmp(s_mqtt_host, "mqtts://", 8) == 0) { - nvs_get_str_into(handle, NVS_USER, value, MQTT_MAX_USER_LEN); - nvs_close(handle); + strncpy(uri, s_mqtt_host, sizeof(uri)); + uri[sizeof(uri) - 1] = '\0'; } - else if (value) value[0] = '\0'; -} - -void mqtt_get_password(char *value) -{ - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) == ESP_OK) + else { - nvs_get_str_into(handle, NVS_PASSWORD, value, MQTT_MAX_PASSWORD_LEN); - nvs_close(handle); + snprintf(uri, sizeof(uri), "mqtt://%s", s_mqtt_host); + } + + esp_mqtt_client_config_t cfg = { + .broker.address.uri = uri, + }; + + if (s_mqtt_user[0] != '\0') + cfg.credentials.username = s_mqtt_user; + if (s_mqtt_pass[0] != '\0') + cfg.credentials.authentication.password = s_mqtt_pass; + + s_client = esp_mqtt_client_init(&cfg); + if (!s_client) + { + ESP_LOGE(TAG, "esp_mqtt_client_init failed"); + return; + } + + esp_mqtt_client_register_event( + s_client, + ESP_EVENT_ANY_ID, + mqtt_event_handler, + NULL); + + esp_err_t err = esp_mqtt_client_start(s_client); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "esp_mqtt_client_start failed: %s", esp_err_to_name(err)); + esp_mqtt_client_destroy(s_client); + s_client = NULL; } - else if (value) value[0] = '\0'; } -uint16_t mqtt_get_periodicity(void) +static void mqtt_client_stop(void) { - // devolve o cache atual; se não tiver sido carregado, lê NVS - if (periodicity == 0) + if (!s_client) + return; + + esp_mqtt_client_stop(s_client); + esp_mqtt_client_destroy(s_client); + s_client = NULL; + s_connected = false; +} + +static void mqtt_network_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)data; + if (base != NETWORK_EVENTS) + return; + + switch (id) { - nvs_handle_t handle; - if (open_mqtt_nvs(&handle) == ESP_OK) + case NETWORK_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "NETWORK_EVENT_STA_GOT_IP -> tentar ligar MQTT"); + mqtt_client_start_if_needed(); + break; + default: + break; + } +} + +// -------------------------------------------------- +// Task de telemetria periódica (opcional) +// -------------------------------------------------- + +static void mqtt_telemetry_task(void *arg) +{ + (void)arg; + TickType_t last_wake = xTaskGetTickCount(); + + while (1) + { + uint16_t period = s_mqtt_periodicity; + if (period == 0) + period = 30; + + if (s_mqtt_enabled && s_client && s_connected) { - nvs_get_u16(handle, NVS_PERIODICITY, &periodicity); - nvs_close(handle); - if (periodicity == 0) periodicity = 30; - } - else - { - periodicity = 30; + mqtt_evse_publish_state(); + mqtt_loadbalancer_publish_state(); + mqtt_scheduler_publish_state(); } + + vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(period * 1000)); } - return periodicity; } -// === Fim de: components/protocols/src/mqtt.c === +// -------------------------------------------------- +// API pública +// -------------------------------------------------- + +esp_err_t mqtt_init(void) +{ + mqtt_load_config_from_storage(); + + ESP_ERROR_CHECK(esp_event_handler_register( + AUTH_EVENTS, ESP_EVENT_ANY_ID, mqtt_auth_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register( + SCHED_EVENTS, ESP_EVENT_ANY_ID, mqtt_scheduler_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register( + LOADBALANCER_EVENTS, ESP_EVENT_ANY_ID, mqtt_loadbalancer_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register( + METER_EVENT, ESP_EVENT_ANY_ID, mqtt_meter_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register( + EVSE_EVENTS, ESP_EVENT_ANY_ID, mqtt_evse_event_handler, NULL)); + + ESP_ERROR_CHECK(esp_event_handler_register( + NETWORK_EVENTS, ESP_EVENT_ANY_ID, mqtt_network_event_handler, NULL)); + + if (!s_telemetry_task) + { + xTaskCreate( + mqtt_telemetry_task, + "mqtt_telemetry_task", + 4096, + NULL, + 3, + &s_telemetry_task); + } + + return ESP_OK; +} + +esp_err_t mqtt_start(void) +{ + mqtt_client_start_if_needed(); + return ESP_OK; +} + +void mqtt_stop(void) +{ + mqtt_client_stop(); +} + +esp_err_t mqtt_set_config(bool enabled, + const char *host, + const char *topic, + const char *username, + const char *password, + int periodicity) +{ + bool reconnect_needed = s_connected || s_client; + if (reconnect_needed) + mqtt_client_stop(); + + s_mqtt_enabled = enabled; + + if (host) + strncpy(s_mqtt_host, host, sizeof(s_mqtt_host)); + s_mqtt_host[sizeof(s_mqtt_host) - 1] = '\0'; + + if (topic) + strncpy(s_mqtt_base_topic, topic, sizeof(s_mqtt_base_topic)); + s_mqtt_base_topic[sizeof(s_mqtt_base_topic) - 1] = '\0'; + + if (username) + strncpy(s_mqtt_user, username, sizeof(s_mqtt_user)); + s_mqtt_user[sizeof(s_mqtt_user) - 1] = '\0'; + + if (password) + strncpy(s_mqtt_pass, password, sizeof(s_mqtt_pass)); + s_mqtt_pass[sizeof(s_mqtt_pass) - 1] = '\0'; + + if (periodicity > 0 && periodicity < 3600) + s_mqtt_periodicity = (uint16_t)periodicity; + + esp_err_t err = mqtt_save_config_to_storage(); + if (err != ESP_OK) + return err; + + if (s_mqtt_enabled) + mqtt_client_start_if_needed(); + + return ESP_OK; +} + +// === Fim de: components/mqtt/src/mqtt.c === diff --git a/components/rest_api/src/dashboard_api.c b/components/rest_api/src/dashboard_api.c index ed5b767..2fdc5d4 100644 --- a/components/rest_api/src/dashboard_api.c +++ b/components/rest_api/src/dashboard_api.c @@ -25,7 +25,7 @@ static esp_err_t dashboard_get_handler(httpd_req_t *req) { cJSON_AddNumberToObject(charger1, "id", 1); cJSON_AddStringToObject(charger1, "status", evse_state_to_str(state)); cJSON_AddNumberToObject(charger1, "current", evse_get_runtime_charging_current()); - cJSON_AddNumberToObject(charger1, "maxCurrent", evse_get_max_charging_current()); + cJSON_AddNumberToObject(charger1, "maxCurrent", evse_get_charging_current()); // Calcular a potência com base na corrente (considerando 230V) int power = (evse_get_runtime_charging_current()) * 230; diff --git a/components/rest_api/src/evse_link_config_api.c b/components/rest_api/src/evse_link_config_api.c index 2c7af56..7672cf4 100644 --- a/components/rest_api/src/evse_link_config_api.c +++ b/components/rest_api/src/evse_link_config_api.c @@ -11,7 +11,7 @@ static esp_err_t link_config_get_handler(httpd_req_t *req) { uint8_t mode = evse_link_get_mode(); // 0=MASTER,1=SLAVE uint8_t self_id = evse_link_get_self_id(); - ESP_LOGI(TAG, "GET link config: enabled=%d mode=%u id=%u", + ESP_LOGD(TAG, "GET link config: enabled=%d mode=%u id=%u", enabled, mode, self_id); httpd_resp_set_type(req, "application/json"); @@ -23,7 +23,7 @@ static esp_err_t link_config_get_handler(httpd_req_t *req) { char *s = cJSON_Print(root); httpd_resp_sendstr(req, s); - ESP_LOGI(TAG, " payload: %s", s); + ESP_LOGD(TAG, " payload: %s", s); free(s); cJSON_Delete(root); return ESP_OK; @@ -38,7 +38,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) { return ESP_FAIL; } buf[len] = '\0'; - ESP_LOGI(TAG, "POST link config: %s", buf); + ESP_LOGD(TAG, "POST link config: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { @@ -50,7 +50,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) { cJSON *j_en = cJSON_GetObjectItem(json, "linkEnabled"); if (j_en && cJSON_IsBool(j_en)) { evse_link_set_enabled(cJSON_IsTrue(j_en)); - ESP_LOGI(TAG, " set enabled = %d", cJSON_IsTrue(j_en)); + ESP_LOGD(TAG, " set enabled = %d", cJSON_IsTrue(j_en)); } // linkMode @@ -67,7 +67,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) { "Invalid linkMode (must be MASTER or SLAVE)"); return ESP_FAIL; } - ESP_LOGI(TAG, " set mode = %s", m); + ESP_LOGD(TAG, " set mode = %s", m); } // linkSelfId @@ -81,7 +81,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) { return ESP_FAIL; } evse_link_set_self_id((uint8_t)id); - ESP_LOGI(TAG, " set self_id = %d", id); + ESP_LOGD(TAG, " set self_id = %d", id); } cJSON_Delete(json); diff --git a/components/rest_api/src/evse_settings_api.c b/components/rest_api/src/evse_settings_api.c index 3608fd1..efa9137 100755 --- a/components/rest_api/src/evse_settings_api.c +++ b/components/rest_api/src/evse_settings_api.c @@ -12,7 +12,7 @@ static const char *TAG = "evse_settings_api"; static esp_err_t config_settings_get_handler(httpd_req_t *req) { httpd_resp_set_type(req, "application/json"); cJSON *config = cJSON_CreateObject(); - cJSON_AddNumberToObject(config, "currentLimit", evse_get_max_charging_current()); + cJSON_AddNumberToObject(config, "currentLimit", evse_get_charging_current()); cJSON_AddNumberToObject(config, "temperatureLimit", evse_get_temp_threshold()); const char *json_str = cJSON_Print(config); httpd_resp_sendstr(req, json_str); @@ -36,7 +36,7 @@ static esp_err_t config_settings_post_handler(httpd_req_t *req) { } cJSON *current = cJSON_GetObjectItem(json, "currentLimit"); - if (current) evse_set_max_charging_current(current->valueint); + if (current) evse_set_charging_current(current->valueint); cJSON *temp = cJSON_GetObjectItem(json, "temperatureLimit"); if (temp) evse_set_temp_threshold(temp->valueint); diff --git a/components/rest_api/src/loadbalancing_settings_api.c b/components/rest_api/src/loadbalancing_settings_api.c index 0d363db..0c4d81b 100644 --- a/components/rest_api/src/loadbalancing_settings_api.c +++ b/components/rest_api/src/loadbalancing_settings_api.c @@ -10,7 +10,7 @@ static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) { bool enabled = loadbalancer_is_enabled(); uint8_t currentLimit = load_balancing_get_max_grid_current(); - ESP_LOGI(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit); + ESP_LOGD(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit); httpd_resp_set_type(req, "application/json"); @@ -21,7 +21,7 @@ static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) { const char *json_str = cJSON_Print(config); httpd_resp_sendstr(req, json_str); - ESP_LOGI(TAG, "Returned config: %s", json_str); + ESP_LOGD(TAG, "Returned config: %s", json_str); free((void *)json_str); cJSON_Delete(config); @@ -40,7 +40,7 @@ static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { } buf[len] = '\0'; - ESP_LOGI(TAG, "Received POST data: %s", buf); + ESP_LOGD(TAG, "Received POST data: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { @@ -54,7 +54,7 @@ static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { if (enabled_item && cJSON_IsBool(enabled_item)) { bool isEnabled = cJSON_IsTrue(enabled_item); loadbalancer_set_enabled(isEnabled); - ESP_LOGI(TAG, "Updated loadBalancingEnabled to: %d", isEnabled); + ESP_LOGD(TAG, "Updated loadBalancingEnabled to: %d", isEnabled); } // Atualizar limite de corrente @@ -78,7 +78,7 @@ static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { return ESP_FAIL; } - ESP_LOGI(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit); + ESP_LOGD(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit); } cJSON_Delete(json); diff --git a/components/rest_api/src/meters_settings_api.c b/components/rest_api/src/meters_settings_api.c index 3aca068..7dd1983 100644 --- a/components/rest_api/src/meters_settings_api.c +++ b/components/rest_api/src/meters_settings_api.c @@ -158,13 +158,13 @@ void register_meters_data_handlers(httpd_handle_t server, void *ctx) .user_ctx = ctx}; httpd_register_uri_handler(server, &live); - ESP_LOGI(TAG, "REST /api/v1/meters/live registrado"); + ESP_LOGD(TAG, "REST /api/v1/meters/live registrado"); } // Função para recuperar as configurações dos contadores static esp_err_t meters_config_get_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Received GET request for /api/v1/config/meters"); + ESP_LOGD(TAG, "Received GET request for /api/v1/config/meters"); httpd_resp_set_type(req, "application/json"); @@ -174,8 +174,8 @@ static esp_err_t meters_config_get_handler(httpd_req_t *req) meter_type_t gridmeterType = meter_manager_grid_get_model(); meter_type_t evsemeterType = meter_manager_evse_get_model(); - ESP_LOGI(TAG, "Grid meter type: %s", meter_type_to_str(gridmeterType)); - ESP_LOGI(TAG, "EVSE meter type: %s", meter_type_to_str(evsemeterType)); + ESP_LOGD(TAG, "Grid meter type: %s", meter_type_to_str(gridmeterType)); + ESP_LOGD(TAG, "EVSE meter type: %s", meter_type_to_str(evsemeterType)); // Adicionando os tipos de contadores ao objeto JSON cJSON_AddStringToObject(config, "gridmeter", meter_type_to_str(gridmeterType)); @@ -183,7 +183,7 @@ static esp_err_t meters_config_get_handler(httpd_req_t *req) // Convertendo o objeto JSON para uma string const char *json_str = cJSON_Print(config); - ESP_LOGI(TAG, "Returning meters config: %s", json_str); + ESP_LOGD(TAG, "Returning meters config: %s", json_str); httpd_resp_sendstr(req, json_str); @@ -197,7 +197,7 @@ static esp_err_t meters_config_get_handler(httpd_req_t *req) // Função para atualizar as configurações dos contadores static esp_err_t meters_config_post_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Received POST request for /api/v1/config/meters"); + ESP_LOGD(TAG, "Received POST request for /api/v1/config/meters"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); @@ -211,7 +211,7 @@ static esp_err_t meters_config_post_handler(httpd_req_t *req) buf[len] = '\0'; // Garantir que a string está terminada - ESP_LOGI(TAG, "Received POST data: %s", buf); + ESP_LOGD(TAG, "Received POST data: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) @@ -227,7 +227,7 @@ static esp_err_t meters_config_post_handler(httpd_req_t *req) if (gridmeter) { meter_type_t gridType = string_to_meter_type(gridmeter->valuestring); // Usando a função string_to_meter_type - ESP_LOGI(TAG, "Updating grid meter type to: %s", gridmeter->valuestring); + ESP_LOGD(TAG, "Updating grid meter type to: %s", gridmeter->valuestring); meter_manager_grid_set_model(gridType); } @@ -235,14 +235,14 @@ static esp_err_t meters_config_post_handler(httpd_req_t *req) if (evsemeter) { meter_type_t evseType = string_to_meter_type(evsemeter->valuestring); // Usando a função string_to_meter_type - ESP_LOGI(TAG, "Updating EVSE meter type to: %s", evsemeter->valuestring); + ESP_LOGD(TAG, "Updating EVSE meter type to: %s", evsemeter->valuestring); meter_manager_evse_set_model(evseType); } cJSON_Delete(json); httpd_resp_sendstr(req, "Meters updated successfully"); - ESP_LOGI(TAG, "Meters configuration updated successfully"); + ESP_LOGD(TAG, "Meters configuration updated successfully"); return ESP_OK; } diff --git a/components/rest_api/src/network_api.c b/components/rest_api/src/network_api.c index 08da017..bda3e68 100755 --- a/components/rest_api/src/network_api.c +++ b/components/rest_api/src/network_api.c @@ -19,14 +19,14 @@ typedef struct { static void wifi_apply_config_task(void *param) { wifi_task_data_t *data = (wifi_task_data_t *)param; - ESP_LOGI("wifi_task", "Applying Wi-Fi config in background task"); + ESP_LOGD("wifi_task", "Applying Wi-Fi config in background task"); wifi_set_config(data->enabled, data->ssid, data->password); free(data); vTaskDelete(NULL); } static esp_err_t wifi_get_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Handling GET /api/v1/config/wifi"); + ESP_LOGD(TAG, "Handling GET /api/v1/config/wifi"); httpd_resp_set_type(req, "application/json"); @@ -56,7 +56,7 @@ static esp_err_t wifi_get_handler(httpd_req_t *req) { } static esp_err_t wifi_post_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Handling POST /api/v1/config/wifi"); + ESP_LOGD(TAG, "Handling POST /api/v1/config/wifi"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); @@ -112,7 +112,7 @@ static esp_err_t wifi_post_handler(httpd_req_t *req) { static esp_err_t config_mqtt_get_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Handling GET /api/v1/config/mqtt"); + ESP_LOGD(TAG, "Handling GET /api/v1/config/mqtt"); httpd_resp_set_type(req, "application/json"); @@ -128,13 +128,13 @@ static esp_err_t config_mqtt_get_handler(httpd_req_t *req) mqtt_get_user(username); mqtt_get_password(password); - ESP_LOGI(TAG, "MQTT Config:"); - ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false"); - ESP_LOGI(TAG, " Server: %s", server); - ESP_LOGI(TAG, " Topic: %s", base_topic); - ESP_LOGI(TAG, " Username: %s", username); - ESP_LOGI(TAG, " Password: %s", password); - ESP_LOGI(TAG, " Periodicity: %d", periodicity); + ESP_LOGD(TAG, "MQTT Config:"); + ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false"); + ESP_LOGD(TAG, " Server: %s", server); + ESP_LOGD(TAG, " Topic: %s", base_topic); + ESP_LOGD(TAG, " Username: %s", username); + ESP_LOGD(TAG, " Password: %s", password); + ESP_LOGD(TAG, " Periodicity: %d", periodicity); cJSON *config = cJSON_CreateObject(); cJSON_AddBoolToObject(config, "enabled", enabled); @@ -156,7 +156,7 @@ static esp_err_t config_mqtt_get_handler(httpd_req_t *req) static esp_err_t config_mqtt_post_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "Handling POST /api/v1/config/mqtt"); + ESP_LOGD(TAG, "Handling POST /api/v1/config/mqtt"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); @@ -166,7 +166,7 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req) return ESP_FAIL; } buf[len] = '\0'; - ESP_LOGI(TAG, "Received JSON: %s", buf); + ESP_LOGD(TAG, "Received JSON: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { @@ -197,13 +197,13 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req) cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity"); if (cJSON_IsNumber(j_periodicity)) periodicity = j_periodicity->valueint; - ESP_LOGI(TAG, "Applying MQTT config:"); - ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false"); - ESP_LOGI(TAG, " Host: %s", host); - ESP_LOGI(TAG, " Topic: %s", topic); - ESP_LOGI(TAG, " Username: %s", username); - ESP_LOGI(TAG, " Password: %s", password); - ESP_LOGI(TAG, " Periodicity: %d", periodicity); + ESP_LOGD(TAG, "Applying MQTT config:"); + ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false"); + ESP_LOGD(TAG, " Host: %s", host); + ESP_LOGD(TAG, " Topic: %s", topic); + ESP_LOGD(TAG, " Username: %s", username); + ESP_LOGD(TAG, " Password: %s", password); + ESP_LOGD(TAG, " Periodicity: %d", periodicity); esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity); if (err != ESP_OK) { diff --git a/components/rest_api/src/rest_main.c b/components/rest_api/src/rest_main.c index de9c1ef..6a40c6d 100755 --- a/components/rest_api/src/rest_main.c +++ b/components/rest_api/src/rest_main.c @@ -11,6 +11,9 @@ #include "static_file_api.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + static const char *TAG = "rest_main"; esp_err_t rest_server_init(const char *base_path) @@ -30,6 +33,13 @@ esp_err_t rest_server_init(const char *base_path) config.uri_match_fn = httpd_uri_match_wildcard; config.max_uri_handlers = 32; + // Aumenta stack do httpd (handlers + cJSON + etc) + config.stack_size = 8192; // tenta 8192; se necessário 12288 + config.task_priority = tskIDLE_PRIORITY + 5; + + // Opcional (ajuda com muitas conexões/keep-alive) + config.lru_purge_enable = true; + httpd_handle_t server = NULL; esp_err_t err = httpd_start(&server, &config); if (err != ESP_OK) @@ -41,21 +51,19 @@ esp_err_t rest_server_init(const char *base_path) ESP_LOGI(TAG, "HTTP server started successfully"); - // Register endpoint groups - register_evse_settings_handlers(server, ctx); // Apenas chamando a função sem comparação - register_network_handlers(server, ctx); // Apenas chamando a função sem comparação - register_ocpp_handlers(server, ctx); // Apenas chamando a função sem comparação - register_auth_handlers(server, ctx); // Apenas chamando a função sem comparação - register_dashboard_handlers(server, ctx); // Apenas chamando a função sem comparação - register_meters_settings_handlers(server, ctx); // Apenas chamando a função sem comparação - register_loadbalancing_settings_handlers(server, ctx); // Apenas chamando a função sem comparação + // Registar handlers + register_evse_settings_handlers(server, ctx); + register_network_handlers(server, ctx); + register_ocpp_handlers(server, ctx); + register_auth_handlers(server, ctx); + register_dashboard_handlers(server, ctx); + register_meters_settings_handlers(server, ctx); + register_loadbalancing_settings_handlers(server, ctx); register_link_config_handlers(server, ctx); register_meters_data_handlers(server, ctx); register_scheduler_settings_handlers(server, ctx); - - register_static_file_handlers(server, ctx); // Apenas chamando a função sem comparação + register_static_file_handlers(server, ctx); ESP_LOGI(TAG, "All REST API endpoint groups registered successfully"); - return ESP_OK; } diff --git a/components/rest_api/src/scheduler_settings_api.c b/components/rest_api/src/scheduler_settings_api.c index 419ec47..34e09ef 100755 --- a/components/rest_api/src/scheduler_settings_api.c +++ b/components/rest_api/src/scheduler_settings_api.c @@ -50,7 +50,7 @@ static void format_hhmm(uint16_t minutes, char *buf, size_t buf_sz) * ========================= */ static esp_err_t scheduler_config_get_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "GET /api/v1/config/scheduler"); + ESP_LOGD(TAG, "GET /api/v1/config/scheduler"); httpd_resp_set_type(req, "application/json"); @@ -95,7 +95,7 @@ static esp_err_t scheduler_config_get_handler(httpd_req_t *req) * ========================= */ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) { - ESP_LOGI(TAG, "POST /api/v1/config/scheduler"); + ESP_LOGD(TAG, "POST /api/v1/config/scheduler"); // NOTA: para payloads pequenos 512 bytes chega; se quiseres robustez total, // usa req->content_len e faz um loop com httpd_req_recv. @@ -108,7 +108,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) return ESP_FAIL; } buf[len] = '\0'; - ESP_LOGI(TAG, "Body: %s", buf); + ESP_LOGD(TAG, "Body: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) @@ -126,7 +126,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) if (cJSON_IsBool(j_enabled)) { cfg.enabled = cJSON_IsTrue(j_enabled); - ESP_LOGI(TAG, " enabled = %d", cfg.enabled); + ESP_LOGD(TAG, " enabled = %d", cfg.enabled); } // mode @@ -143,7 +143,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) return ESP_FAIL; } cfg.mode = m; - ESP_LOGI(TAG, " mode = %s", sched_mode_to_str(cfg.mode)); + ESP_LOGD(TAG, " mode = %s", sched_mode_to_str(cfg.mode)); } // startTime (string "HH:MM") @@ -160,7 +160,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) return ESP_FAIL; } cfg.start_min = minutes; - ESP_LOGI(TAG, " start_min = %u", (unsigned)cfg.start_min); + ESP_LOGD(TAG, " start_min = %u", (unsigned)cfg.start_min); } // endTime (string "HH:MM") @@ -177,7 +177,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) return ESP_FAIL; } cfg.end_min = minutes; - ESP_LOGI(TAG, " end_min = %u", (unsigned)cfg.end_min); + ESP_LOGD(TAG, " end_min = %u", (unsigned)cfg.end_min); } // (Opcional) validações extra: @@ -220,5 +220,5 @@ void register_scheduler_settings_handlers(httpd_handle_t server, void *ctx) .user_ctx = ctx}; httpd_register_uri_handler(server, &post_uri); - ESP_LOGI(TAG, "Scheduler REST handlers registered"); + ESP_LOGD(TAG, "Scheduler REST handlers registered"); } diff --git a/components/rest_api/webfolder/assets/index-19gq1t3T.js b/components/rest_api/webfolder/assets/index-CH8H7Z_T.js similarity index 67% rename from components/rest_api/webfolder/assets/index-19gq1t3T.js rename to components/rest_api/webfolder/assets/index-CH8H7Z_T.js index 35c2520..bd6dd7c 100644 --- a/components/rest_api/webfolder/assets/index-19gq1t3T.js +++ b/components/rest_api/webfolder/assets/index-CH8H7Z_T.js @@ -1,4 +1,4 @@ -(function(){const d=document.createElement("link").relList;if(d&&d.supports&&d.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))s(h);new MutationObserver(h=>{for(const y of h)if(y.type==="childList")for(const v of y.addedNodes)v.tagName==="LINK"&&v.rel==="modulepreload"&&s(v)}).observe(document,{childList:!0,subtree:!0});function o(h){const y={};return h.integrity&&(y.integrity=h.integrity),h.referrerPolicy&&(y.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?y.credentials="include":h.crossOrigin==="anonymous"?y.credentials="omit":y.credentials="same-origin",y}function s(h){if(h.ep)return;h.ep=!0;const y=o(h);fetch(h.href,y)}})();function ch(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Dr={exports:{}},Hn={};/** +(function(){const d=document.createElement("link").relList;if(d&&d.supports&&d.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))s(h);new MutationObserver(h=>{for(const y of h)if(y.type==="childList")for(const v of y.addedNodes)v.tagName==="LINK"&&v.rel==="modulepreload"&&s(v)}).observe(document,{childList:!0,subtree:!0});function o(h){const y={};return h.integrity&&(y.integrity=h.integrity),h.referrerPolicy&&(y.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?y.credentials="include":h.crossOrigin==="anonymous"?y.credentials="omit":y.credentials="same-origin",y}function s(h){if(h.ep)return;h.ep=!0;const y=o(h);fetch(h.href,y)}})();function ch(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Mr={exports:{}},Hn={};/** * @license React * react-jsx-runtime.production.js * @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Gd;function K0(){if(Gd)return Hn;Gd=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.fragment");function o(s,h,y){var v=null;if(y!==void 0&&(v=""+y),h.key!==void 0&&(v=""+h.key),"key"in h){y={};for(var A in h)A!=="key"&&(y[A]=h[A])}else y=h;return h=y.ref,{$$typeof:i,type:s,key:v,ref:h!==void 0?h:null,props:y}}return Hn.Fragment=d,Hn.jsx=o,Hn.jsxs=o,Hn}var Xd;function J0(){return Xd||(Xd=1,Dr.exports=K0()),Dr.exports}var r=J0(),Mr={exports:{}},ne={};/** + */var Gd;function Ky(){if(Gd)return Hn;Gd=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.fragment");function o(s,h,y){var v=null;if(y!==void 0&&(v=""+y),h.key!==void 0&&(v=""+h.key),"key"in h){y={};for(var R in h)R!=="key"&&(y[R]=h[R])}else y=h;return h=y.ref,{$$typeof:i,type:s,key:v,ref:h!==void 0?h:null,props:y}}return Hn.Fragment=d,Hn.jsx=o,Hn.jsxs=o,Hn}var Xd;function Jy(){return Xd||(Xd=1,Mr.exports=Ky()),Mr.exports}var r=Jy(),Dr={exports:{}},ne={};/** * @license React * react.production.js * @@ -14,7 +14,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Qd;function k0(){if(Qd)return ne;Qd=1;var i=Symbol.for("react.transitional.element"),d=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),h=Symbol.for("react.profiler"),y=Symbol.for("react.consumer"),v=Symbol.for("react.context"),A=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),R=Symbol.for("react.lazy"),M=Symbol.iterator;function S(b){return b===null||typeof b!="object"?null:(b=M&&b[M]||b["@@iterator"],typeof b=="function"?b:null)}var B={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,Y={};function Q(b,H,K){this.props=b,this.context=H,this.refs=Y,this.updater=K||B}Q.prototype.isReactComponent={},Q.prototype.setState=function(b,H){if(typeof b!="object"&&typeof b!="function"&&b!=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,b,H,"setState")},Q.prototype.forceUpdate=function(b){this.updater.enqueueForceUpdate(this,b,"forceUpdate")};function q(){}q.prototype=Q.prototype;function Z(b,H,K){this.props=b,this.context=H,this.refs=Y,this.updater=K||B}var L=Z.prototype=new q;L.constructor=Z,w(L,Q.prototype),L.isPureReactComponent=!0;var $=Array.isArray,J={H:null,A:null,T:null,S:null,V:null},me=Object.prototype.hasOwnProperty;function V(b,H,K,G,F,oe){return K=oe.ref,{$$typeof:i,type:b,key:H,ref:K!==void 0?K:null,props:oe}}function fe(b,H){return V(b.type,H,void 0,void 0,void 0,b.props)}function ee(b){return typeof b=="object"&&b!==null&&b.$$typeof===i}function Ce(b){var H={"=":"=0",":":"=2"};return"$"+b.replace(/[=:]/g,function(K){return H[K]})}var yt=/\/+/g;function Je(b,H){return typeof b=="object"&&b!==null&&b.key!=null?Ce(""+b.key):H.toString(36)}function Ml(){}function zl(b){switch(b.status){case"fulfilled":return b.value;case"rejected":throw b.reason;default:switch(typeof b.status=="string"?b.then(Ml,Ml):(b.status="pending",b.then(function(H){b.status==="pending"&&(b.status="fulfilled",b.value=H)},function(H){b.status==="pending"&&(b.status="rejected",b.reason=H)})),b.status){case"fulfilled":return b.value;case"rejected":throw b.reason}}throw b}function ke(b,H,K,G,F){var oe=typeof b;(oe==="undefined"||oe==="boolean")&&(b=null);var ae=!1;if(b===null)ae=!0;else switch(oe){case"bigint":case"string":case"number":ae=!0;break;case"object":switch(b.$$typeof){case i:case d:ae=!0;break;case R:return ae=b._init,ke(ae(b._payload),H,K,G,F)}}if(ae)return F=F(b),ae=G===""?"."+Je(b,0):G,$(F)?(K="",ae!=null&&(K=ae.replace(yt,"$&/")+"/"),ke(F,H,K,"",function(nl){return nl})):F!=null&&(ee(F)&&(F=fe(F,K+(F.key==null||b&&b.key===F.key?"":(""+F.key).replace(yt,"$&/")+"/")+ae)),H.push(F)),1;ae=0;var ut=G===""?".":G+":";if($(b))for(var Te=0;Te>>1,b=C[xe];if(0>>1;xeh(G,te))Fh(oe,G)?(C[xe]=oe,C[F]=te,xe=F):(C[xe]=G,C[K]=te,xe=K);else if(Fh(oe,te))C[xe]=oe,C[F]=te,xe=F;else break e}}return X}function h(C,X){var te=C.sortIndex-X.sortIndex;return te!==0?te:C.id-X.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var y=performance;i.unstable_now=function(){return y.now()}}else{var v=Date,A=v.now();i.unstable_now=function(){return v.now()-A}}var p=[],m=[],R=1,M=null,S=3,B=!1,w=!1,Y=!1,Q=!1,q=typeof setTimeout=="function"?setTimeout:null,Z=typeof clearTimeout=="function"?clearTimeout:null,L=typeof setImmediate<"u"?setImmediate:null;function $(C){for(var X=o(m);X!==null;){if(X.callback===null)s(m);else if(X.startTime<=C)s(m),X.sortIndex=X.expirationTime,d(p,X);else break;X=o(m)}}function J(C){if(Y=!1,$(C),!w)if(o(p)!==null)w=!0,me||(me=!0,Je());else{var X=o(m);X!==null&&ke(J,X.startTime-C)}}var me=!1,V=-1,fe=5,ee=-1;function Ce(){return Q?!0:!(i.unstable_now()-eeC&&Ce());){var xe=M.callback;if(typeof xe=="function"){M.callback=null,S=M.priorityLevel;var b=xe(M.expirationTime<=C);if(C=i.unstable_now(),typeof b=="function"){M.callback=b,$(C),X=!0;break t}M===o(p)&&s(p),$(C)}else s(p);M=o(p)}if(M!==null)X=!0;else{var H=o(m);H!==null&&ke(J,H.startTime-C),X=!1}}break e}finally{M=null,S=te,B=!1}X=void 0}}finally{X?Je():me=!1}}}var Je;if(typeof L=="function")Je=function(){L(yt)};else if(typeof MessageChannel<"u"){var Ml=new MessageChannel,zl=Ml.port2;Ml.port1.onmessage=yt,Je=function(){zl.postMessage(null)}}else Je=function(){q(yt,0)};function ke(C,X){V=q(function(){C(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(C){C.callback=null},i.unstable_forceFrameRate=function(C){0>C||125xe?(C.sortIndex=te,d(m,C),o(p)===null&&C===o(m)&&(Y?(Z(V),V=-1):Y=!0,ke(J,te-xe))):(C.sortIndex=b,d(p,C),w||B||(w=!0,me||(me=!0,Je()))),C},i.unstable_shouldYield=Ce,i.unstable_wrapCallback=function(C){var X=S;return function(){var te=S;S=X;try{return C.apply(this,arguments)}finally{S=te}}}}(_r)),_r}var Kd;function F0(){return Kd||(Kd=1,Cr.exports=W0()),Cr.exports}var Ur={exports:{}},Fe={};/** + */var Vd;function Wy(){return Vd||(Vd=1,function(i){function d(C,Z){var te=C.length;C.push(Z);e:for(;0>>1,p=C[xe];if(0>>1;xeh(X,te))Fh(oe,X)?(C[xe]=oe,C[F]=te,xe=F):(C[xe]=X,C[K]=te,xe=K);else if(Fh(oe,te))C[xe]=oe,C[F]=te,xe=F;else break e}}return Z}function h(C,Z){var te=C.sortIndex-Z.sortIndex;return te!==0?te:C.id-Z.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var y=performance;i.unstable_now=function(){return y.now()}}else{var v=Date,R=v.now();i.unstable_now=function(){return v.now()-R}}var x=[],m=[],N=1,D=null,S=3,q=!1,U=!1,Y=!1,Q=!1,H=typeof setTimeout=="function"?setTimeout:null,G=typeof clearTimeout=="function"?clearTimeout:null,B=typeof setImmediate<"u"?setImmediate:null;function $(C){for(var Z=o(m);Z!==null;){if(Z.callback===null)s(m);else if(Z.startTime<=C)s(m),Z.sortIndex=Z.expirationTime,d(x,Z);else break;Z=o(m)}}function J(C){if(Y=!1,$(C),!U)if(o(x)!==null)U=!0,me||(me=!0,Je());else{var Z=o(m);Z!==null&&ke(J,Z.startTime-C)}}var me=!1,V=-1,fe=5,ee=-1;function Ce(){return Q?!0:!(i.unstable_now()-eeC&&Ce());){var xe=D.callback;if(typeof xe=="function"){D.callback=null,S=D.priorityLevel;var p=xe(D.expirationTime<=C);if(C=i.unstable_now(),typeof p=="function"){D.callback=p,$(C),Z=!0;break t}D===o(x)&&s(x),$(C)}else s(x);D=o(x)}if(D!==null)Z=!0;else{var L=o(m);L!==null&&ke(J,L.startTime-C),Z=!1}}break e}finally{D=null,S=te,q=!1}Z=void 0}}finally{Z?Je():me=!1}}}var Je;if(typeof B=="function")Je=function(){B(yt)};else if(typeof MessageChannel<"u"){var Dl=new MessageChannel,zl=Dl.port2;Dl.port1.onmessage=yt,Je=function(){zl.postMessage(null)}}else Je=function(){H(yt,0)};function ke(C,Z){V=H(function(){C(i.unstable_now())},Z)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(C){C.callback=null},i.unstable_forceFrameRate=function(C){0>C||125xe?(C.sortIndex=te,d(m,C),o(x)===null&&C===o(m)&&(Y?(G(V),V=-1):Y=!0,ke(J,te-xe))):(C.sortIndex=p,d(x,C),U||q||(U=!0,me||(me=!0,Je()))),C},i.unstable_shouldYield=Ce,i.unstable_wrapCallback=function(C){var Z=S;return function(){var te=S;S=Z;try{return C.apply(this,arguments)}finally{S=te}}}}(_r)),_r}var Kd;function Fy(){return Kd||(Kd=1,Cr.exports=Wy()),Cr.exports}var Ur={exports:{}},Fe={};/** * @license React * react-dom.production.js * @@ -30,7 +30,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Jd;function P0(){if(Jd)return Fe;Jd=1;var i=qr();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(),Ur.exports=P0(),Ur.exports}/** + */var Jd;function Py(){if(Jd)return Fe;Jd=1;var i=qr();function d(x){var m="https://react.dev/errors/"+x;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(d){console.error(d)}}return i(),Ur.exports=Py(),Ur.exports}/** * @license React * react-dom-client.production.js * @@ -38,14 +38,14 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var $d;function ey(){if($d)return Ln;$d=1;var i=F0(),d=qr(),o=I0();function s(e){var t="https://react.dev/errors/"+e;if(1b||(e.current=xe[b],xe[b]=null,b--)}function G(e,t){b++,xe[b]=e.current,e.current=t}var F=H(null),oe=H(null),ae=H(null),ut=H(null);function Te(e,t){switch(G(ae,t),G(oe,e),G(F,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=yd(t),e=gd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}K(F),G(F,e)}function nl(){K(F),K(oe),K(ae)}function mi(e){e.memoizedState!==null&&G(ut,e);var t=F.current,l=gd(t,e.type);t!==l&&(G(oe,e),G(F,l))}function Zn(e){oe.current===e&&(K(F),K(oe)),ut.current===e&&(K(ut),zn._currentValue=te)}var yi=Object.prototype.hasOwnProperty,gi=i.unstable_scheduleCallback,vi=i.unstable_cancelCallback,jh=i.unstable_shouldYield,Nh=i.unstable_requestPaint,zt=i.unstable_now,Ah=i.unstable_getCurrentPriorityLevel,Jr=i.unstable_ImmediatePriority,kr=i.unstable_UserBlockingPriority,Vn=i.unstable_NormalPriority,Rh=i.unstable_LowPriority,$r=i.unstable_IdlePriority,Oh=i.log,Dh=i.unstable_setDisableYieldValue,Ba=null,it=null;function ul(e){if(typeof Oh=="function"&&Dh(e),it&&typeof it.setStrictMode=="function")try{it.setStrictMode(Ba,e)}catch{}}var ct=Math.clz32?Math.clz32:Ch,Mh=Math.log,zh=Math.LN2;function Ch(e){return e>>>=0,e===0?32:31-(Mh(e)/zh|0)|0}var Kn=256,Jn=4194304;function Cl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function kn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var f=a&134217727;return f!==0?(a=f&~u,a!==0?n=Cl(a):(c&=f,c!==0?n=Cl(c):l||(l=f&~e,l!==0&&(n=Cl(l))))):(f=a&~u,f!==0?n=Cl(f):c!==0?n=Cl(c):l||(l=a&~e,l!==0&&(n=Cl(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function qa(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function _h(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Wr(){var e=Kn;return Kn<<=1,(Kn&4194048)===0&&(Kn=256),e}function Fr(){var e=Jn;return Jn<<=1,(Jn&62914560)===0&&(Jn=4194304),e}function pi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ya(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Uh(e,t,l,a,n,u){var c=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var f=e.entanglements,g=e.expirationTimes,N=e.hiddenUpdates;for(l=c&~l;0p||(e.current=xe[p],xe[p]=null,p--)}function X(e,t){p++,xe[p]=e.current,e.current=t}var F=L(null),oe=L(null),ae=L(null),ut=L(null);function Te(e,t){switch(X(ae,t),X(oe,e),X(F,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=yd(t),e=gd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}K(F),X(F,e)}function nl(){K(F),K(oe),K(ae)}function mi(e){e.memoizedState!==null&&X(ut,e);var t=F.current,l=gd(t,e.type);t!==l&&(X(oe,e),X(F,l))}function Zn(e){oe.current===e&&(K(F),K(oe)),ut.current===e&&(K(ut),zn._currentValue=te)}var yi=Object.prototype.hasOwnProperty,gi=i.unstable_scheduleCallback,vi=i.unstable_cancelCallback,jh=i.unstable_shouldYield,Nh=i.unstable_requestPaint,zt=i.unstable_now,Ah=i.unstable_getCurrentPriorityLevel,Jr=i.unstable_ImmediatePriority,kr=i.unstable_UserBlockingPriority,Vn=i.unstable_NormalPriority,Rh=i.unstable_LowPriority,$r=i.unstable_IdlePriority,Oh=i.log,Mh=i.unstable_setDisableYieldValue,Ba=null,it=null;function ul(e){if(typeof Oh=="function"&&Mh(e),it&&typeof it.setStrictMode=="function")try{it.setStrictMode(Ba,e)}catch{}}var ct=Math.clz32?Math.clz32:Ch,Dh=Math.log,zh=Math.LN2;function Ch(e){return e>>>=0,e===0?32:31-(Dh(e)/zh|0)|0}var Kn=256,Jn=4194304;function Cl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function kn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,c=e.pingedLanes;e=e.warmLanes;var f=a&134217727;return f!==0?(a=f&~u,a!==0?n=Cl(a):(c&=f,c!==0?n=Cl(c):l||(l=f&~e,l!==0&&(n=Cl(l))))):(f=a&~u,f!==0?n=Cl(f):c!==0?n=Cl(c):l||(l=a&~e,l!==0&&(n=Cl(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function qa(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function _h(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Wr(){var e=Kn;return Kn<<=1,(Kn&4194048)===0&&(Kn=256),e}function Fr(){var e=Jn;return Jn<<=1,(Jn&62914560)===0&&(Jn=4194304),e}function pi(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Ya(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Uh(e,t,l,a,n,u){var c=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var f=e.entanglements,g=e.expirationTimes,A=e.hiddenUpdates;for(l=c&~l;0)":-1n||g[a]!==N[n]){var z=` +`+Ti+e+is}var ji=!1;function Ni(e,t){if(!e||ji)return"";ji=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var a={DetermineComponentFrameRoot:function(){try{if(t){var w=function(){throw Error()};if(Object.defineProperty(w.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(w,[])}catch(M){var O=M}Reflect.construct(e,[],w)}else{try{w.call()}catch(M){O=M}e.call(w.prototype)}}else{try{throw Error()}catch(M){O=M}(w=e())&&typeof w.catch=="function"&&w.catch(function(){})}}catch(M){if(M&&O&&typeof M.stack=="string")return[M.stack,O.stack]}return[null,null]}};a.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var n=Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot,"name");n&&n.configurable&&Object.defineProperty(a.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var u=a.DetermineComponentFrameRoot(),c=u[0],f=u[1];if(c&&f){var g=c.split(` +`),A=f.split(` +`);for(n=a=0;an||g[a]!==A[n]){var z=` `+g[a].replace(" at new "," at ");return e.displayName&&z.includes("")&&(z=z.replace("",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{ji=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?ta(l):""}function Yh(e){switch(e.tag){case 26:case 27:case 5:return ta(e.type);case 16:return ta("Lazy");case 13:return ta("Suspense");case 19:return ta("SuspenseList");case 0:case 15:return Ni(e.type,!1);case 11:return Ni(e.type.render,!1);case 1:return Ni(e.type,!0);case 31:return ta("Activity");default:return""}}function cs(e){try{var t="";do t+=Yh(e),e=e.return;while(e);return t}catch(l){return` Error generating stack: `+l.message+` -`+l.stack}}function gt(e){switch(typeof e){case"bigint":case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function rs(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Gh(e){var t=rs(e)?"checked":"value",l=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),a=""+e[t];if(!e.hasOwnProperty(t)&&typeof l<"u"&&typeof l.get=="function"&&typeof l.set=="function"){var n=l.get,u=l.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return n.call(this)},set:function(c){a=""+c,u.call(this,c)}}),Object.defineProperty(e,t,{enumerable:l.enumerable}),{getValue:function(){return a},setValue:function(c){a=""+c},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Fn(e){e._valueTracker||(e._valueTracker=Gh(e))}function ss(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var l=t.getValue(),a="";return e&&(a=rs(e)?e.checked?"true":"false":e.value),e=a,e!==l?(t.setValue(e),!0):!1}function Pn(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Xh=/[\n"\\]/g;function vt(e){return e.replace(Xh,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Ai(e,t,l,a,n,u,c,f){e.name="",c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"?e.type=c:e.removeAttribute("type"),t!=null?c==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+gt(t)):e.value!==""+gt(t)&&(e.value=""+gt(t)):c!=="submit"&&c!=="reset"||e.removeAttribute("value"),t!=null?Ri(e,c,gt(t)):l!=null?Ri(e,c,gt(l)):a!=null&&e.removeAttribute("value"),n==null&&u!=null&&(e.defaultChecked=!!u),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?e.name=""+gt(f):e.removeAttribute("name")}function fs(e,t,l,a,n,u,c,f){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(e.type=u),t!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||t!=null))return;l=l!=null?""+gt(l):"",t=t!=null?""+gt(t):l,f||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=f?e.checked:!!a,e.defaultChecked=!!a,c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(e.name=c)}function Ri(e,t,l){t==="number"&&Pn(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function la(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ci=!1;if(Gt)try{var Za={};Object.defineProperty(Za,"passive",{get:function(){Ci=!0}}),window.addEventListener("test",Za,Za),window.removeEventListener("test",Za,Za)}catch{Ci=!1}var cl=null,_i=null,eu=null;function vs(){if(eu)return eu;var e,t=_i,l=t.length,a,n="value"in cl?cl.value:cl.textContent,u=n.length;for(e=0;e=Ja),Ts=" ",js=!1;function Ns(e,t){switch(e){case"keyup":return gm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function As(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var ia=!1;function pm(e,t){switch(e){case"compositionend":return As(t);case"keypress":return t.which!==32?null:(js=!0,Ts);case"textInput":return e=t.data,e===Ts&&js?null:e;default:return null}}function bm(e,t){if(ia)return e==="compositionend"||!Bi&&Ns(e,t)?(e=vs(),eu=_i=cl=null,ia=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=Us(l)}}function Hs(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Hs(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ls(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Pn(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=Pn(e.document)}return t}function Gi(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Rm=Gt&&"documentMode"in document&&11>=document.documentMode,ca=null,Xi=null,Fa=null,Qi=!1;function Bs(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Qi||ca==null||ca!==Pn(a)||(a=ca,"selectionStart"in a&&Gi(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Fa&&Wa(Fa,a)||(Fa=a,a=Zu(Xi,"onSelect"),0>=c,n-=c,Qt=1<<32-ct(t)+n|l<u?u:8;var c=C.T,f={};C.T=f,Oc(e,!1,t,l);try{var g=n(),N=C.S;if(N!==null&&N(f,g),g!==null&&typeof g=="object"&&typeof g.then=="function"){var z=Hm(g,a);hn(e,t,z,ht(e))}else hn(e,t,a,ht(e))}catch(U){hn(e,t,{then:function(){},status:"rejected",reason:U},ht())}finally{X.p=u,C.T=c}}function Gm(){}function Ac(e,t,l,a){if(e.tag!==5)throw Error(s(476));var n=Yf(e).queue;qf(e,n,t,te,l===null?Gm:function(){return Gf(e),l(a)})}function Yf(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:te,baseState:te,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jt,lastRenderedState:te},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jt,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Gf(e){var t=Yf(e).next.queue;hn(e,t,{},ht())}function Rc(){return We(zn)}function Xf(){return Ue().memoizedState}function Qf(){return Ue().memoizedState}function Xm(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=ht();e=fl(l);var a=ol(t,e,l);a!==null&&(mt(a,t,l),cn(a,t,l)),t={cache:ac()},e.payload=t;return}t=t.return}}function Qm(e,t,l){var a=ht();l={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null},ju(e)?Vf(t,l):(l=Ji(e,t,l,a),l!==null&&(mt(l,e,a),Kf(l,t,a)))}function Zf(e,t,l){var a=ht();hn(e,t,l,a)}function hn(e,t,l,a){var n={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null};if(ju(e))Vf(t,n);else{var u=e.alternate;if(e.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var c=t.lastRenderedState,f=u(c,l);if(n.hasEagerState=!0,n.eagerState=f,rt(f,c))return cu(e,t,n,0),Ee===null&&iu(),!1}catch{}finally{}if(l=Ji(e,t,n,a),l!==null)return mt(l,e,a),Kf(l,t,a),!0}return!1}function Oc(e,t,l,a){if(a={lane:2,revertLane:ir(),action:a,hasEagerState:!1,eagerState:null,next:null},ju(e)){if(t)throw Error(s(479))}else t=Ji(e,l,a,2),t!==null&&mt(t,e,2)}function ju(e){var t=e.alternate;return e===ue||t!==null&&t===ue}function Vf(e,t){va=pu=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function Kf(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,Ir(e,l)}}var Nu={readContext:We,use:xu,useCallback:Me,useContext:Me,useEffect:Me,useImperativeHandle:Me,useLayoutEffect:Me,useInsertionEffect:Me,useMemo:Me,useReducer:Me,useRef:Me,useState:Me,useDebugValue:Me,useDeferredValue:Me,useTransition:Me,useSyncExternalStore:Me,useId:Me,useHostTransitionStatus:Me,useFormState:Me,useActionState:Me,useOptimistic:Me,useMemoCache:Me,useCacheRefresh:Me},Jf={readContext:We,use:xu,useCallback:function(e,t){return tt().memoizedState=[e,t===void 0?null:t],e},useContext:We,useEffect:Mf,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Tu(4194308,4,Uf.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Tu(4194308,4,e,t)},useInsertionEffect:function(e,t){Tu(4,2,e,t)},useMemo:function(e,t){var l=tt();t=t===void 0?null:t;var a=e();if(Vl){ul(!0);try{e()}finally{ul(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=tt();if(l!==void 0){var n=l(t);if(Vl){ul(!0);try{l(t)}finally{ul(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=Qm.bind(null,ue,e),[a.memoizedState,e]},useRef:function(e){var t=tt();return e={current:e},t.memoizedState=e},useState:function(e){e=Ec(e);var t=e.queue,l=Zf.bind(null,ue,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:jc,useDeferredValue:function(e,t){var l=tt();return Nc(l,e,t)},useTransition:function(){var e=Ec(!1);return e=qf.bind(null,ue,e.queue,!0,!1),tt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ue,n=tt();if(he){if(l===void 0)throw Error(s(407));l=l()}else{if(l=t(),Ee===null)throw Error(s(349));(se&124)!==0||mf(a,t,l)}n.memoizedState=l;var u={value:l,getSnapshot:t};return n.queue=u,Mf(gf.bind(null,a,u,e),[e]),a.flags|=2048,ba(9,Eu(),yf.bind(null,a,u,l,t),null),l},useId:function(){var e=tt(),t=Ee.identifierPrefix;if(he){var l=Zt,a=Qt;l=(a&~(1<<32-ct(a)-1)).toString(32)+l,t="«"+t+"R"+l,l=bu++,0I?(Ge=W,W=null):Ge=W.sibling;var de=O(T,W,j[I],_);if(de===null){W===null&&(W=Ge);break}e&&W&&de.alternate===null&&t(T,W),x=u(de,x,I),ie===null?k=de:ie.sibling=de,ie=de,W=Ge}if(I===j.length)return l(T,W),he&&ql(T,I),k;if(W===null){for(;II?(Ge=W,W=null):Ge=W.sibling;var Ol=O(T,W,de.value,_);if(Ol===null){W===null&&(W=Ge);break}e&&W&&Ol.alternate===null&&t(T,W),x=u(Ol,x,I),ie===null?k=Ol:ie.sibling=Ol,ie=Ol,W=Ge}if(de.done)return l(T,W),he&&ql(T,I),k;if(W===null){for(;!de.done;I++,de=j.next())de=U(T,de.value,_),de!==null&&(x=u(de,x,I),ie===null?k=de:ie.sibling=de,ie=de);return he&&ql(T,I),k}for(W=a(W);!de.done;I++,de=j.next())de=D(W,T,I,de.value,_),de!==null&&(e&&de.alternate!==null&&W.delete(de.key===null?I:de.key),x=u(de,x,I),ie===null?k=de:ie.sibling=de,ie=de);return e&&W.forEach(function(V0){return t(T,V0)}),he&&ql(T,I),k}function be(T,x,j,_){if(typeof j=="object"&&j!==null&&j.type===w&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case S:e:{for(var k=j.key;x!==null;){if(x.key===k){if(k=j.type,k===w){if(x.tag===7){l(T,x.sibling),_=n(x,j.props.children),_.return=T,T=_;break e}}else if(x.elementType===k||typeof k=="object"&&k!==null&&k.$$typeof===fe&&$f(k)===x.type){l(T,x.sibling),_=n(x,j.props),yn(_,j),_.return=T,T=_;break e}l(T,x);break}else t(T,x);x=x.sibling}j.type===w?(_=Ll(j.props.children,T.mode,_,j.key),_.return=T,T=_):(_=su(j.type,j.key,j.props,null,T.mode,_),yn(_,j),_.return=T,T=_)}return c(T);case B:e:{for(k=j.key;x!==null;){if(x.key===k)if(x.tag===4&&x.stateNode.containerInfo===j.containerInfo&&x.stateNode.implementation===j.implementation){l(T,x.sibling),_=n(x,j.children||[]),_.return=T,T=_;break e}else{l(T,x);break}else t(T,x);x=x.sibling}_=Wi(j,T.mode,_),_.return=T,T=_}return c(T);case fe:return k=j._init,j=k(j._payload),be(T,x,j,_)}if(ke(j))return le(T,x,j,_);if(Je(j)){if(k=Je(j),typeof k!="function")throw Error(s(150));return j=k.call(j),P(T,x,j,_)}if(typeof j.then=="function")return be(T,x,Au(j),_);if(j.$$typeof===L)return be(T,x,hu(T,j),_);Ru(T,j)}return typeof j=="string"&&j!==""||typeof j=="number"||typeof j=="bigint"?(j=""+j,x!==null&&x.tag===6?(l(T,x.sibling),_=n(x,j),_.return=T,T=_):(l(T,x),_=$i(j,T.mode,_),_.return=T,T=_),c(T)):l(T,x)}return function(T,x,j,_){try{mn=0;var k=be(T,x,j,_);return xa=null,k}catch(W){if(W===nn||W===yu)throw W;var ie=st(29,W,null,T.mode);return ie.lanes=_,ie.return=T,ie}finally{}}}var Sa=Wf(!0),Ff=Wf(!1),Et=H(null),_t=null;function hl(e){var t=e.alternate;G(He,He.current&1),G(Et,e),_t===null&&(t===null||ga.current!==null||t.memoizedState!==null)&&(_t=e)}function Pf(e){if(e.tag===22){if(G(He,He.current),G(Et,e),_t===null){var t=e.alternate;t!==null&&t.memoizedState!==null&&(_t=e)}}else ml()}function ml(){G(He,He.current),G(Et,Et.current)}function kt(e){K(Et),_t===e&&(_t=null),K(He)}var He=H(0);function Ou(e){for(var t=e;t!==null;){if(t.tag===13){var l=t.memoizedState;if(l!==null&&(l=l.dehydrated,l===null||l.data==="$?"||pr(l)))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&128)!==0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}function Dc(e,t,l,a){t=e.memoizedState,l=l(a,t),l=l==null?t:R({},t,l),e.memoizedState=l,e.lanes===0&&(e.updateQueue.baseState=l)}var Mc={enqueueSetState:function(e,t,l){e=e._reactInternals;var a=ht(),n=fl(a);n.payload=t,l!=null&&(n.callback=l),t=ol(e,n,a),t!==null&&(mt(t,e,a),cn(t,e,a))},enqueueReplaceState:function(e,t,l){e=e._reactInternals;var a=ht(),n=fl(a);n.tag=1,n.payload=t,l!=null&&(n.callback=l),t=ol(e,n,a),t!==null&&(mt(t,e,a),cn(t,e,a))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var l=ht(),a=fl(l);a.tag=2,t!=null&&(a.callback=t),t=ol(e,a,l),t!==null&&(mt(t,e,l),cn(t,e,l))}};function If(e,t,l,a,n,u,c){return e=e.stateNode,typeof e.shouldComponentUpdate=="function"?e.shouldComponentUpdate(a,u,c):t.prototype&&t.prototype.isPureReactComponent?!Wa(l,a)||!Wa(n,u):!0}function eo(e,t,l,a){e=t.state,typeof t.componentWillReceiveProps=="function"&&t.componentWillReceiveProps(l,a),typeof t.UNSAFE_componentWillReceiveProps=="function"&&t.UNSAFE_componentWillReceiveProps(l,a),t.state!==e&&Mc.enqueueReplaceState(t,t.state,null)}function Kl(e,t){var l=t;if("ref"in t){l={};for(var a in t)a!=="ref"&&(l[a]=t[a])}if(e=e.defaultProps){l===t&&(l=R({},l));for(var n in e)l[n]===void 0&&(l[n]=e[n])}return l}var Du=typeof reportError=="function"?reportError:function(e){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof e=="object"&&e!==null&&typeof e.message=="string"?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",e);return}console.error(e)};function to(e){Du(e)}function lo(e){console.error(e)}function ao(e){Du(e)}function Mu(e,t){try{var l=e.onUncaughtError;l(t.value,{componentStack:t.stack})}catch(a){setTimeout(function(){throw a})}}function no(e,t,l){try{var a=e.onCaughtError;a(l.value,{componentStack:l.stack,errorBoundary:t.tag===1?t.stateNode:null})}catch(n){setTimeout(function(){throw n})}}function zc(e,t,l){return l=fl(l),l.tag=3,l.payload={element:null},l.callback=function(){Mu(e,t)},l}function uo(e){return e=fl(e),e.tag=3,e}function io(e,t,l,a){var n=l.type.getDerivedStateFromError;if(typeof n=="function"){var u=a.value;e.payload=function(){return n(u)},e.callback=function(){no(t,l,a)}}var c=l.stateNode;c!==null&&typeof c.componentDidCatch=="function"&&(e.callback=function(){no(t,l,a),typeof n!="function"&&(xl===null?xl=new Set([this]):xl.add(this));var f=a.stack;this.componentDidCatch(a.value,{componentStack:f!==null?f:""})})}function Vm(e,t,l,a,n){if(l.flags|=32768,a!==null&&typeof a=="object"&&typeof a.then=="function"){if(t=l.alternate,t!==null&&tn(t,l,n,!0),l=Et.current,l!==null){switch(l.tag){case 13:return _t===null?tr():l.alternate===null&&Oe===0&&(Oe=3),l.flags&=-257,l.flags|=65536,l.lanes=n,a===ic?l.flags|=16384:(t=l.updateQueue,t===null?l.updateQueue=new Set([a]):t.add(a),ar(e,a,n)),!1;case 22:return l.flags|=65536,a===ic?l.flags|=16384:(t=l.updateQueue,t===null?(t={transitions:null,markerInstances:null,retryQueue:new Set([a])},l.updateQueue=t):(l=t.retryQueue,l===null?t.retryQueue=new Set([a]):l.add(a)),ar(e,a,n)),!1}throw Error(s(435,l.tag))}return ar(e,a,n),tr(),!1}if(he)return t=Et.current,t!==null?((t.flags&65536)===0&&(t.flags|=256),t.flags|=65536,t.lanes=n,a!==Ii&&(e=Error(s(422),{cause:a}),en(pt(e,l)))):(a!==Ii&&(t=Error(s(423),{cause:a}),en(pt(t,l))),e=e.current.alternate,e.flags|=65536,n&=-n,e.lanes|=n,a=pt(a,l),n=zc(e.stateNode,a,n),sc(e,n),Oe!==4&&(Oe=2)),!1;var u=Error(s(520),{cause:a});if(u=pt(u,l),En===null?En=[u]:En.push(u),Oe!==4&&(Oe=2),t===null)return!0;a=pt(a,l),l=t;do{switch(l.tag){case 3:return l.flags|=65536,e=n&-n,l.lanes|=e,e=zc(l.stateNode,a,e),sc(l,e),!1;case 1:if(t=l.type,u=l.stateNode,(l.flags&128)===0&&(typeof t.getDerivedStateFromError=="function"||u!==null&&typeof u.componentDidCatch=="function"&&(xl===null||!xl.has(u))))return l.flags|=65536,n&=-n,l.lanes|=n,n=uo(n),io(n,e,l,a),sc(l,n),!1}l=l.return}while(l!==null);return!1}var co=Error(s(461)),qe=!1;function Qe(e,t,l,a){t.child=e===null?Ff(t,null,l,a):Sa(t,e.child,l,a)}function ro(e,t,l,a,n){l=l.render;var u=t.ref;if("ref"in a){var c={};for(var f in a)f!=="ref"&&(c[f]=a[f])}else c=a;return Ql(t),a=mc(e,t,l,c,u,n),f=yc(),e!==null&&!qe?(gc(e,t,n),$t(e,t,n)):(he&&f&&Fi(t),t.flags|=1,Qe(e,t,a,n),t.child)}function so(e,t,l,a,n){if(e===null){var u=l.type;return typeof u=="function"&&!ki(u)&&u.defaultProps===void 0&&l.compare===null?(t.tag=15,t.type=u,fo(e,t,u,a,n)):(e=su(l.type,null,a,t,t.mode,n),e.ref=t.ref,e.return=t,t.child=e)}if(u=e.child,!qc(e,n)){var c=u.memoizedProps;if(l=l.compare,l=l!==null?l:Wa,l(c,a)&&e.ref===t.ref)return $t(e,t,n)}return t.flags|=1,e=Xt(u,a),e.ref=t.ref,e.return=t,t.child=e}function fo(e,t,l,a,n){if(e!==null){var u=e.memoizedProps;if(Wa(u,a)&&e.ref===t.ref)if(qe=!1,t.pendingProps=a=u,qc(e,n))(e.flags&131072)!==0&&(qe=!0);else return t.lanes=e.lanes,$t(e,t,n)}return Cc(e,t,l,a,n)}function oo(e,t,l){var a=t.pendingProps,n=a.children,u=e!==null?e.memoizedState:null;if(a.mode==="hidden"){if((t.flags&128)!==0){if(a=u!==null?u.baseLanes|l:l,e!==null){for(n=t.child=e.child,u=0;n!==null;)u=u|n.lanes|n.childLanes,n=n.sibling;t.childLanes=u&~a}else t.childLanes=0,t.child=null;return ho(e,t,a,l)}if((l&536870912)!==0)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&mu(t,u!==null?u.cachePool:null),u!==null?ff(t,u):oc(),Pf(t);else return t.lanes=t.childLanes=536870912,ho(e,t,u!==null?u.baseLanes|l:l,l)}else u!==null?(mu(t,u.cachePool),ff(t,u),ml(),t.memoizedState=null):(e!==null&&mu(t,null),oc(),ml());return Qe(e,t,n,l),t.child}function ho(e,t,l,a){var n=uc();return n=n===null?null:{parent:we._currentValue,pool:n},t.memoizedState={baseLanes:l,cachePool:n},e!==null&&mu(t,null),oc(),Pf(t),e!==null&&tn(e,t,a,!0),null}function zu(e,t){var l=t.ref;if(l===null)e!==null&&e.ref!==null&&(t.flags|=4194816);else{if(typeof l!="function"&&typeof l!="object")throw Error(s(284));(e===null||e.ref!==l)&&(t.flags|=4194816)}}function Cc(e,t,l,a,n){return Ql(t),l=mc(e,t,l,a,void 0,n),a=yc(),e!==null&&!qe?(gc(e,t,n),$t(e,t,n)):(he&&a&&Fi(t),t.flags|=1,Qe(e,t,l,n),t.child)}function mo(e,t,l,a,n,u){return Ql(t),t.updateQueue=null,l=df(t,a,l,n),of(e),a=yc(),e!==null&&!qe?(gc(e,t,u),$t(e,t,u)):(he&&a&&Fi(t),t.flags|=1,Qe(e,t,l,u),t.child)}function yo(e,t,l,a,n){if(Ql(t),t.stateNode===null){var u=oa,c=l.contextType;typeof c=="object"&&c!==null&&(u=We(c)),u=new l(a,u),t.memoizedState=u.state!==null&&u.state!==void 0?u.state:null,u.updater=Mc,t.stateNode=u,u._reactInternals=t,u=t.stateNode,u.props=a,u.state=t.memoizedState,u.refs={},cc(t),c=l.contextType,u.context=typeof c=="object"&&c!==null?We(c):oa,u.state=t.memoizedState,c=l.getDerivedStateFromProps,typeof c=="function"&&(Dc(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&&Mc.enqueueReplaceState(u,u.state,null),sn(t,a,u,n),rn(),u.state=t.memoizedState),typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!0}else if(e===null){u=t.stateNode;var f=t.memoizedProps,g=Kl(l,f);u.props=g;var N=u.context,z=l.contextType;c=oa,typeof z=="object"&&z!==null&&(c=We(z));var U=l.getDerivedStateFromProps;z=typeof U=="function"||typeof u.getSnapshotBeforeUpdate=="function",f=t.pendingProps!==f,z||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(f||N!==c)&&eo(t,u,a,c),sl=!1;var O=t.memoizedState;u.state=O,sn(t,a,u,n),rn(),N=t.memoizedState,f||O!==N||sl?(typeof U=="function"&&(Dc(t,l,U,a),N=t.memoizedState),(g=sl||If(t,l,g,a,O,N,c))?(z||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount()),typeof u.componentDidMount=="function"&&(t.flags|=4194308)):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=a,t.memoizedState=N),u.props=a,u.state=N,u.context=c,a=g):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!1)}else{u=t.stateNode,rc(e,t),c=t.memoizedProps,z=Kl(l,c),u.props=z,U=t.pendingProps,O=u.context,N=l.contextType,g=oa,typeof N=="object"&&N!==null&&(g=We(N)),f=l.getDerivedStateFromProps,(N=typeof f=="function"||typeof u.getSnapshotBeforeUpdate=="function")||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(c!==U||O!==g)&&eo(t,u,a,g),sl=!1,O=t.memoizedState,u.state=O,sn(t,a,u,n),rn();var D=t.memoizedState;c!==U||O!==D||sl||e!==null&&e.dependencies!==null&&du(e.dependencies)?(typeof f=="function"&&(Dc(t,l,f,a),D=t.memoizedState),(z=sl||If(t,l,z,a,O,D,g)||e!==null&&e.dependencies!==null&&du(e.dependencies))?(N||typeof u.UNSAFE_componentWillUpdate!="function"&&typeof u.componentWillUpdate!="function"||(typeof u.componentWillUpdate=="function"&&u.componentWillUpdate(a,D,g),typeof u.UNSAFE_componentWillUpdate=="function"&&u.UNSAFE_componentWillUpdate(a,D,g)),typeof u.componentDidUpdate=="function"&&(t.flags|=4),typeof u.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof u.componentDidUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=1024),t.memoizedProps=a,t.memoizedState=D),u.props=a,u.state=D,u.context=g,a=z):(typeof u.componentDidUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||c===e.memoizedProps&&O===e.memoizedState||(t.flags|=1024),a=!1)}return u=a,zu(e,t),a=(t.flags&128)!==0,u||a?(u=t.stateNode,l=a&&typeof l.getDerivedStateFromError!="function"?null:u.render(),t.flags|=1,e!==null&&a?(t.child=Sa(t,e.child,null,n),t.child=Sa(t,null,l,n)):Qe(e,t,l,n),t.memoizedState=u.state,e=t.child):e=$t(e,t,n),e}function go(e,t,l,a){return Ia(),t.flags|=256,Qe(e,t,l,a),t.child}var _c={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Uc(e){return{baseLanes:e,cachePool:tf()}}function wc(e,t,l){return e=e!==null?e.childLanes&~l:0,t&&(e|=Tt),e}function vo(e,t,l){var a=t.pendingProps,n=!1,u=(t.flags&128)!==0,c;if((c=u)||(c=e!==null&&e.memoizedState===null?!1:(He.current&2)!==0),c&&(n=!0,t.flags&=-129),c=(t.flags&32)!==0,t.flags&=-33,e===null){if(he){if(n?hl(t):ml(),he){var f=Re,g;if(g=f){e:{for(g=f,f=Ct;g.nodeType!==8;){if(!f){f=null;break e}if(g=Dt(g.nextSibling),g===null){f=null;break e}}f=g}f!==null?(t.memoizedState={dehydrated:f,treeContext:Bl!==null?{id:Qt,overflow:Zt}:null,retryLane:536870912,hydrationErrors:null},g=st(18,null,null,0),g.stateNode=f,g.return=t,t.child=g,Pe=t,Re=null,g=!0):g=!1}g||Gl(t)}if(f=t.memoizedState,f!==null&&(f=f.dehydrated,f!==null))return pr(f)?t.lanes=32:t.lanes=536870912,null;kt(t)}return f=a.children,a=a.fallback,n?(ml(),n=t.mode,f=Cu({mode:"hidden",children:f},n),a=Ll(a,n,l,null),f.return=t,a.return=t,f.sibling=a,t.child=f,n=t.child,n.memoizedState=Uc(l),n.childLanes=wc(e,c,l),t.memoizedState=_c,a):(hl(t),Hc(t,f))}if(g=e.memoizedState,g!==null&&(f=g.dehydrated,f!==null)){if(u)t.flags&256?(hl(t),t.flags&=-257,t=Lc(e,t,l)):t.memoizedState!==null?(ml(),t.child=e.child,t.flags|=128,t=null):(ml(),n=a.fallback,f=t.mode,a=Cu({mode:"visible",children:a.children},f),n=Ll(n,f,l,null),n.flags|=2,a.return=t,n.return=t,a.sibling=n,t.child=a,Sa(t,e.child,null,l),a=t.child,a.memoizedState=Uc(l),a.childLanes=wc(e,c,l),t.memoizedState=_c,t=n);else if(hl(t),pr(f)){if(c=f.nextSibling&&f.nextSibling.dataset,c)var N=c.dgst;c=N,a=Error(s(419)),a.stack="",a.digest=c,en({value:a,source:null,stack:null}),t=Lc(e,t,l)}else if(qe||tn(e,t,l,!1),c=(l&e.childLanes)!==0,qe||c){if(c=Ee,c!==null&&(a=l&-l,a=(a&42)!==0?1:bi(a),a=(a&(c.suspendedLanes|l))!==0?0:a,a!==0&&a!==g.retryLane))throw g.retryLane=a,fa(e,a),mt(c,e,a),co;f.data==="$?"||tr(),t=Lc(e,t,l)}else f.data==="$?"?(t.flags|=192,t.child=e.child,t=null):(e=g.treeContext,Re=Dt(f.nextSibling),Pe=t,he=!0,Yl=null,Ct=!1,e!==null&&(xt[St++]=Qt,xt[St++]=Zt,xt[St++]=Bl,Qt=e.id,Zt=e.overflow,Bl=t),t=Hc(t,a.children),t.flags|=4096);return t}return n?(ml(),n=a.fallback,f=t.mode,g=e.child,N=g.sibling,a=Xt(g,{mode:"hidden",children:a.children}),a.subtreeFlags=g.subtreeFlags&65011712,N!==null?n=Xt(N,n):(n=Ll(n,f,l,null),n.flags|=2),n.return=t,a.return=t,a.sibling=n,t.child=a,a=n,n=t.child,f=e.child.memoizedState,f===null?f=Uc(l):(g=f.cachePool,g!==null?(N=we._currentValue,g=g.parent!==N?{parent:N,pool:N}:g):g=tf(),f={baseLanes:f.baseLanes|l,cachePool:g}),n.memoizedState=f,n.childLanes=wc(e,c,l),t.memoizedState=_c,a):(hl(t),l=e.child,e=l.sibling,l=Xt(l,{mode:"visible",children:a.children}),l.return=t,l.sibling=null,e!==null&&(c=t.deletions,c===null?(t.deletions=[e],t.flags|=16):c.push(e)),t.child=l,t.memoizedState=null,l)}function Hc(e,t){return t=Cu({mode:"visible",children:t},e.mode),t.return=e,e.child=t}function Cu(e,t){return e=st(22,e,null,t),e.lanes=0,e.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},e}function Lc(e,t,l){return Sa(t,e.child,null,l),e=Hc(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function po(e,t,l){e.lanes|=t;var a=e.alternate;a!==null&&(a.lanes|=t),tc(e.return,t,l)}function Bc(e,t,l,a,n){var u=e.memoizedState;u===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:a,tail:l,tailMode:n}:(u.isBackwards=t,u.rendering=null,u.renderingStartTime=0,u.last=a,u.tail=l,u.tailMode=n)}function bo(e,t,l){var a=t.pendingProps,n=a.revealOrder,u=a.tail;if(Qe(e,t,a.children,l),a=He.current,(a&2)!==0)a=a&1|2,t.flags|=128;else{if(e!==null&&(e.flags&128)!==0)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&po(e,l,t);else if(e.tag===19)po(e,l,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}a&=1}switch(G(He,a),n){case"forwards":for(l=t.child,n=null;l!==null;)e=l.alternate,e!==null&&Ou(e)===null&&(n=l),l=l.sibling;l=n,l===null?(n=t.child,t.child=null):(n=l.sibling,l.sibling=null),Bc(t,!1,n,l,u);break;case"backwards":for(l=null,n=t.child,t.child=null;n!==null;){if(e=n.alternate,e!==null&&Ou(e)===null){t.child=n;break}e=n.sibling,n.sibling=l,l=n,n=e}Bc(t,!0,l,null,u);break;case"together":Bc(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function $t(e,t,l){if(e!==null&&(t.dependencies=e.dependencies),bl|=t.lanes,(l&t.childLanes)===0)if(e!==null){if(tn(e,t,l,!1),(l&t.childLanes)===0)return null}else return null;if(e!==null&&t.child!==e.child)throw Error(s(153));if(t.child!==null){for(e=t.child,l=Xt(e,e.pendingProps),t.child=l,l.return=t;e.sibling!==null;)e=e.sibling,l=l.sibling=Xt(e,e.pendingProps),l.return=t;l.sibling=null}return t.child}function qc(e,t){return(e.lanes&t)!==0?!0:(e=e.dependencies,!!(e!==null&&du(e)))}function Km(e,t,l){switch(t.tag){case 3:Te(t,t.stateNode.containerInfo),rl(t,we,e.memoizedState.cache),Ia();break;case 27:case 5:mi(t);break;case 4:Te(t,t.stateNode.containerInfo);break;case 10:rl(t,t.type,t.memoizedProps.value);break;case 13:var a=t.memoizedState;if(a!==null)return a.dehydrated!==null?(hl(t),t.flags|=128,null):(l&t.child.childLanes)!==0?vo(e,t,l):(hl(t),e=$t(e,t,l),e!==null?e.sibling:null);hl(t);break;case 19:var n=(e.flags&128)!==0;if(a=(l&t.childLanes)!==0,a||(tn(e,t,l,!1),a=(l&t.childLanes)!==0),n){if(a)return bo(e,t,l);t.flags|=128}if(n=t.memoizedState,n!==null&&(n.rendering=null,n.tail=null,n.lastEffect=null),G(He,He.current),a)break;return null;case 22:case 23:return t.lanes=0,oo(e,t,l);case 24:rl(t,we,e.memoizedState.cache)}return $t(e,t,l)}function xo(e,t,l){if(e!==null)if(e.memoizedProps!==t.pendingProps)qe=!0;else{if(!qc(e,l)&&(t.flags&128)===0)return qe=!1,Km(e,t,l);qe=(e.flags&131072)!==0}else qe=!1,he&&(t.flags&1048576)!==0&&ks(t,ou,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var a=t.elementType,n=a._init;if(a=n(a._payload),t.type=a,typeof a=="function")ki(a)?(e=Kl(a,e),t.tag=1,t=yo(null,t,a,e,l)):(t.tag=0,t=Cc(null,t,a,e,l));else{if(a!=null){if(n=a.$$typeof,n===$){t.tag=11,t=ro(null,t,a,e,l);break e}else if(n===V){t.tag=14,t=so(null,t,a,e,l);break e}}throw t=zl(a)||a,Error(s(306,t,""))}}return t;case 0:return Cc(e,t,t.type,t.pendingProps,l);case 1:return a=t.type,n=Kl(a,t.pendingProps),yo(e,t,a,n,l);case 3:e:{if(Te(t,t.stateNode.containerInfo),e===null)throw Error(s(387));a=t.pendingProps;var u=t.memoizedState;n=u.element,rc(e,t),sn(t,a,null,l);var c=t.memoizedState;if(a=c.cache,rl(t,we,a),a!==u.cache&&lc(t,[we],l,!0),rn(),a=c.element,u.isDehydrated)if(u={element:a,isDehydrated:!1,cache:c.cache},t.updateQueue.baseState=u,t.memoizedState=u,t.flags&256){t=go(e,t,a,l);break e}else if(a!==n){n=pt(Error(s(424)),t),en(n),t=go(e,t,a,l);break e}else{switch(e=t.stateNode.containerInfo,e.nodeType){case 9:e=e.body;break;default:e=e.nodeName==="HTML"?e.ownerDocument.body:e}for(Re=Dt(e.firstChild),Pe=t,he=!0,Yl=null,Ct=!0,l=Ff(t,null,a,l),t.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling}else{if(Ia(),a===n){t=$t(e,t,l);break e}Qe(e,t,a,l)}t=t.child}return t;case 26:return zu(e,t),e===null?(l=jd(t.type,null,t.pendingProps,null))?t.memoizedState=l:he||(l=t.type,e=t.pendingProps,a=Ku(ae.current).createElement(l),a[$e]=t,a[Ie]=e,Ve(a,l,e),Be(a),t.stateNode=a):t.memoizedState=jd(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return mi(t),e===null&&he&&(a=t.stateNode=Sd(t.type,t.pendingProps,ae.current),Pe=t,Ct=!0,n=Re,Tl(t.type)?(br=n,Re=Dt(a.firstChild)):Re=n),Qe(e,t,t.pendingProps.children,l),zu(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&he&&((n=a=Re)&&(a=x0(a,t.type,t.pendingProps,Ct),a!==null?(t.stateNode=a,Pe=t,Re=Dt(a.firstChild),Ct=!1,n=!0):n=!1),n||Gl(t)),mi(t),n=t.type,u=t.pendingProps,c=e!==null?e.memoizedProps:null,a=u.children,yr(n,u)?a=null:c!==null&&yr(n,c)&&(t.flags|=32),t.memoizedState!==null&&(n=mc(e,t,Bm,null,null,l),zn._currentValue=n),zu(e,t),Qe(e,t,a,l),t.child;case 6:return e===null&&he&&((e=l=Re)&&(l=S0(l,t.pendingProps,Ct),l!==null?(t.stateNode=l,Pe=t,Re=null,e=!0):e=!1),e||Gl(t)),null;case 13:return vo(e,t,l);case 4:return Te(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=Sa(t,null,a,l):Qe(e,t,a,l),t.child;case 11:return ro(e,t,t.type,t.pendingProps,l);case 7:return Qe(e,t,t.pendingProps,l),t.child;case 8:return Qe(e,t,t.pendingProps.children,l),t.child;case 12:return Qe(e,t,t.pendingProps.children,l),t.child;case 10:return a=t.pendingProps,rl(t,t.type,a.value),Qe(e,t,a.children,l),t.child;case 9:return n=t.type._context,a=t.pendingProps.children,Ql(t),n=We(n),a=a(n),t.flags|=1,Qe(e,t,a,l),t.child;case 14:return so(e,t,t.type,t.pendingProps,l);case 15:return fo(e,t,t.type,t.pendingProps,l);case 19:return bo(e,t,l);case 31:return a=t.pendingProps,l=t.mode,a={mode:a.mode,children:a.children},e===null?(l=Cu(a,l),l.ref=t.ref,t.child=l,l.return=t,t=l):(l=Xt(e.child,a),l.ref=t.ref,t.child=l,l.return=t,t=l),t;case 22:return oo(e,t,l);case 24:return Ql(t),a=We(we),e===null?(n=uc(),n===null&&(n=Ee,u=ac(),n.pooledCache=u,u.refCount++,u!==null&&(n.pooledCacheLanes|=l),n=u),t.memoizedState={parent:a,cache:n},cc(t),rl(t,we,n)):((e.lanes&l)!==0&&(rc(e,t),sn(t,null,null,l),rn()),n=e.memoizedState,u=t.memoizedState,n.parent!==a?(n={parent:a,cache:a},t.memoizedState=n,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=n),rl(t,we,a)):(a=u.cache,rl(t,we,a),a!==n.cache&&lc(t,[we],l,!0))),Qe(e,t,t.pendingProps.children,l),t.child;case 29:throw t.pendingProps}throw Error(s(156,t.tag))}function Wt(e){e.flags|=4}function So(e,t){if(t.type!=="stylesheet"||(t.state.loading&4)!==0)e.flags&=-16777217;else if(e.flags|=16777216,!Dd(t)){if(t=Et.current,t!==null&&((se&4194048)===se?_t!==null:(se&62914560)!==se&&(se&536870912)===0||t!==_t))throw un=ic,lf;e.flags|=8192}}function _u(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag!==22?Fr():536870912,e.lanes|=t,Na|=t)}function gn(e,t){if(!he)switch(e.tailMode){case"hidden":t=e.tail;for(var l=null;t!==null;)t.alternate!==null&&(l=t),t=t.sibling;l===null?e.tail=null:l.sibling=null;break;case"collapsed":l=e.tail;for(var a=null;l!==null;)l.alternate!==null&&(a=l),l=l.sibling;a===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:a.sibling=null}}function Ne(e){var t=e.alternate!==null&&e.alternate.child===e.child,l=0,a=0;if(t)for(var n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags&65011712,a|=n.flags&65011712,n.return=e,n=n.sibling;else for(n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags,a|=n.flags,n.return=e,n=n.sibling;return e.subtreeFlags|=a,e.childLanes=l,t}function Jm(e,t,l){var a=t.pendingProps;switch(Pi(t),t.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Ne(t),null;case 1:return Ne(t),null;case 3:return l=t.stateNode,a=null,e!==null&&(a=e.memoizedState.cache),t.memoizedState.cache!==a&&(t.flags|=2048),Kt(we),nl(),l.pendingContext&&(l.context=l.pendingContext,l.pendingContext=null),(e===null||e.child===null)&&(Pa(t)?Wt(t):e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,Fs())),Ne(t),null;case 26:return l=t.memoizedState,e===null?(Wt(t),l!==null?(Ne(t),So(t,l)):(Ne(t),t.flags&=-16777217)):l?l!==e.memoizedState?(Wt(t),Ne(t),So(t,l)):(Ne(t),t.flags&=-16777217):(e.memoizedProps!==a&&Wt(t),Ne(t),t.flags&=-16777217),null;case 27:Zn(t),l=ae.current;var n=t.type;if(e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Wt(t);else{if(!a){if(t.stateNode===null)throw Error(s(166));return Ne(t),null}e=F.current,Pa(t)?$s(t):(e=Sd(n,a,l),t.stateNode=e,Wt(t))}return Ne(t),null;case 5:if(Zn(t),l=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Wt(t);else{if(!a){if(t.stateNode===null)throw Error(s(166));return Ne(t),null}if(e=F.current,Pa(t))$s(t);else{switch(n=Ku(ae.current),e){case 1:e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case 2:e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;default:switch(l){case"svg":e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case"math":e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;case"script":e=n.createElement("div"),e.innerHTML=" + diff --git a/components/scheduler/CMakeLists.txt b/components/scheduler/CMakeLists.txt index 398788b..43c093c 100755 --- a/components/scheduler/CMakeLists.txt +++ b/components/scheduler/CMakeLists.txt @@ -3,5 +3,5 @@ set(srcs "src/scheduler_types.c" "src/scheduler.c" "src/scheduler_events.c") idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "include" PRIV_INCLUDE_DIRS "src" - PRIV_REQUIRES nvs_flash esp_timer + PRIV_REQUIRES esp_timer REQUIRES esp_event evse) \ No newline at end of file diff --git a/components/scheduler/src/scheduler.c b/components/scheduler/src/scheduler.c index df72b73..b550c3e 100755 --- a/components/scheduler/src/scheduler.c +++ b/components/scheduler/src/scheduler.c @@ -8,8 +8,9 @@ #include "esp_log.h" #include "esp_event.h" -#include "nvs_flash.h" -#include "nvs.h" +#include "esp_err.h" + +#include "storage_service.h" #include #include @@ -26,112 +27,154 @@ static sched_config_t s_cfg = { static bool s_allowed_now = true; static TaskHandle_t s_task_handle = NULL; -/* ===== NVS ===== */ +/* ===== Storage ===== */ #define NVS_NAMESPACE "scheduler" -#define NVS_KEY_ENABLED "enabled" -#define NVS_KEY_MODE "mode" -#define NVS_KEY_START_MIN "start_min" -#define NVS_KEY_END_MIN "end_min" +#define NVS_KEY_ENABLED "enabled" // u8 +#define NVS_KEY_MODE "mode" // u8 +#define NVS_KEY_START_MIN "start_min" // u16 +#define NVS_KEY_END_MIN "end_min" // u16 -static void load_config_from_nvs(sched_config_t *out) +static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); } + +static bool cfg_sanitize(sched_config_t *cfg) +{ + if (!cfg) + return false; + + bool changed = false; + + // enabled é bool + cfg->enabled = cfg->enabled ? true : false; + + // mode válido + if (cfg->mode > SCHED_MODE_SIMPLE) + { + cfg->mode = SCHED_MODE_DISABLED; + changed = true; + } + + // start/end em minutos (0..1440) + if (cfg->start_min > (24 * 60)) + { + cfg->start_min = 0; + changed = true; + } + if (cfg->end_min > (24 * 60)) + { + cfg->end_min = 24 * 60; + changed = true; + } + + return changed; +} + +static void load_config_from_storage(sched_config_t *out) { if (!out) return; - *out = s_cfg; // defaults + // defaults + *out = s_cfg; - nvs_handle_t h; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "No scheduler namespace in NVS (%s), using defaults", - esp_err_to_name(err)); - return; - } + bool needs_flush = false; - uint8_t u8; - uint16_t u16; + uint8_t u8 = 0; + uint16_t u16 = 0; + esp_err_t err; - if (nvs_get_u8(h, NVS_KEY_ENABLED, &u8) == ESP_OK) + // enabled + err = storage_get_u8_sync(NVS_NAMESPACE, NVS_KEY_ENABLED, &u8, TO_TICKS_MS(800)); + if (err == ESP_OK && u8 <= 1) { out->enabled = (u8 != 0); } - if (nvs_get_u8(h, NVS_KEY_MODE, &u8) == ESP_OK) + else { - if (u8 <= (uint8_t)SCHED_MODE_SIMPLE) - { - out->mode = (sched_mode_t)u8; - } + out->enabled = false; + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_ENABLED, 0); + needs_flush = true; + ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default=false (persisted)", + esp_err_to_name(err)); } - if (nvs_get_u16(h, NVS_KEY_START_MIN, &u16) == ESP_OK) + + // mode + err = storage_get_u8_sync(NVS_NAMESPACE, NVS_KEY_MODE, &u8, TO_TICKS_MS(800)); + if (err == ESP_OK && u8 <= (uint8_t)SCHED_MODE_SIMPLE) + { + out->mode = (sched_mode_t)u8; + } + else + { + out->mode = SCHED_MODE_DISABLED; + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_MODE, (uint8_t)out->mode); + needs_flush = true; + ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default=DISABLED (persisted)", + esp_err_to_name(err)); + } + + // start_min + err = storage_get_u16_sync(NVS_NAMESPACE, NVS_KEY_START_MIN, &u16, TO_TICKS_MS(800)); + if (err == ESP_OK && u16 <= (24 * 60)) { out->start_min = u16; } - if (nvs_get_u16(h, NVS_KEY_END_MIN, &u16) == ESP_OK) + else + { + out->start_min = 0; + (void)storage_set_u16_async(NVS_NAMESPACE, NVS_KEY_START_MIN, out->start_min); + needs_flush = true; + ESP_LOGW(TAG, "Missing/invalid start_min (%s) -> default=0 (persisted)", + esp_err_to_name(err)); + } + + // end_min + err = storage_get_u16_sync(NVS_NAMESPACE, NVS_KEY_END_MIN, &u16, TO_TICKS_MS(800)); + if (err == ESP_OK && u16 <= (24 * 60)) { out->end_min = u16; } + else + { + out->end_min = 24 * 60; + (void)storage_set_u16_async(NVS_NAMESPACE, NVS_KEY_END_MIN, out->end_min); + needs_flush = true; + ESP_LOGW(TAG, "Missing/invalid end_min (%s) -> default=1440 (persisted)", + esp_err_to_name(err)); + } - nvs_close(h); + // sanity final (e persistir se ajustou algo) + if (cfg_sanitize(out)) + { + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_ENABLED, out->enabled ? 1 : 0); + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_MODE, (uint8_t)out->mode); + (void)storage_set_u16_async(NVS_NAMESPACE, NVS_KEY_START_MIN, out->start_min); + (void)storage_set_u16_async(NVS_NAMESPACE, NVS_KEY_END_MIN, out->end_min); + needs_flush = true; + } - ESP_LOGI(TAG, "Loaded from NVS: enabled=%d mode=%d start=%u end=%u", - out->enabled, out->mode, + if (needs_flush) + { + (void)storage_flush_sync(TO_TICKS_MS(2000)); + } + + ESP_LOGI(TAG, "Loaded: enabled=%d mode=%d start=%u end=%u", + out->enabled, (int)out->mode, (unsigned)out->start_min, (unsigned)out->end_min); } -static void save_config_to_nvs(const sched_config_t *cfg) +static void save_config_to_storage(const sched_config_t *cfg) { if (!cfg) return; - nvs_handle_t h; - esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); - return; - } + // Debounced writes + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_ENABLED, cfg->enabled ? 1 : 0); + (void)storage_set_u8_async(NVS_NAMESPACE, NVS_KEY_MODE, (uint8_t)cfg->mode); + (void)storage_set_u16_async(NVS_NAMESPACE, NVS_KEY_START_MIN, cfg->start_min); + (void)storage_set_u16_async(NVS_NAMESPACE, NVS_KEY_END_MIN, cfg->end_min); - err = nvs_set_u8(h, NVS_KEY_ENABLED, cfg->enabled ? 1 : 0); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_u8(enabled) failed: %s", esp_err_to_name(err)); - goto out; - } - - err = nvs_set_u8(h, NVS_KEY_MODE, (uint8_t)cfg->mode); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_u8(mode) failed: %s", esp_err_to_name(err)); - goto out; - } - - err = nvs_set_u16(h, NVS_KEY_START_MIN, cfg->start_min); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_u16(start_min) failed: %s", esp_err_to_name(err)); - goto out; - } - - err = nvs_set_u16(h, NVS_KEY_END_MIN, cfg->end_min); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_set_u16(end_min) failed: %s", esp_err_to_name(err)); - goto out; - } - - err = nvs_commit(h); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "nvs_commit failed: %s", esp_err_to_name(err)); - } - else - { - ESP_LOGI(TAG, "Scheduler config saved to NVS"); - } - -out: - nvs_close(h); + // opcional: flush imediato + // (void)storage_flush_async(); } /* ===== Lógica de janelas ===== */ @@ -173,17 +216,18 @@ static void scheduler_recompute_and_emit(void) sched_event_state_t ev = { .allowed_now = s_allowed_now}; - esp_event_post(SCHED_EVENTS, - SCHED_EVENT_WINDOW_CHANGED, - &ev, - sizeof(ev), - portMAX_DELAY); + (void)esp_event_post(SCHED_EVENTS, + SCHED_EVENT_WINDOW_CHANGED, + &ev, + sizeof(ev), + portMAX_DELAY); } } /* ===== Task do scheduler ===== */ static void scheduler_task(void *arg) { + (void)arg; const TickType_t period = pdMS_TO_TICKS(60000); // 60s ESP_LOGI(TAG, "Scheduler task started"); @@ -198,8 +242,18 @@ static void scheduler_task(void *arg) /* ===== API pública ===== */ void scheduler_init(void) { - // 1) carregar config - load_config_from_nvs(&s_cfg); + // 0) garante storage + esp_err_t se = storage_service_init(); + if (se != ESP_OK) + { + ESP_LOGE(TAG, "storage_service_init failed: %s (using defaults in RAM)", esp_err_to_name(se)); + // segue com defaults + } + else + { + // 1) carregar config + load_config_from_storage(&s_cfg); + } // 2) calcular estado inicial s_allowed_now = compute_allowed_now(&s_cfg); @@ -207,23 +261,23 @@ void scheduler_init(void) // 3) enviar evento INIT sched_event_state_t ev = { .allowed_now = s_allowed_now}; - esp_event_post(SCHED_EVENTS, - SCHED_EVENT_INIT, - &ev, - sizeof(ev), - portMAX_DELAY); + (void)esp_event_post(SCHED_EVENTS, + SCHED_EVENT_INIT, + &ev, + sizeof(ev), + portMAX_DELAY); ESP_LOGI(TAG, "Init: allowed_now=%d", s_allowed_now); // 4) criar a task if (s_task_handle == NULL) { - xTaskCreate( + (void)xTaskCreate( scheduler_task, "scheduler_task", 4 * 1024, NULL, - 3, // prioridade razoável + 3, &s_task_handle); } } @@ -234,7 +288,9 @@ void scheduler_set_config(const sched_config_t *cfg) return; s_cfg = *cfg; - save_config_to_nvs(&s_cfg); + (void)cfg_sanitize(&s_cfg); + + save_config_to_storage(&s_cfg); // recomputa imediatamente para refletir mudança scheduler_recompute_and_emit(); diff --git a/components/storage_service/CMakeLists.txt b/components/storage_service/CMakeLists.txt new file mode 100755 index 0000000..494ecff --- /dev/null +++ b/components/storage_service/CMakeLists.txt @@ -0,0 +1,8 @@ +set(srcs +"src/storage_service.c" + ) + + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "include" + REQUIRES nvs_flash) diff --git a/components/storage_service/include/storage_service.h b/components/storage_service/include/storage_service.h new file mode 100755 index 0000000..db4f403 --- /dev/null +++ b/components/storage_service/include/storage_service.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "nvs.h" // para ESP_ERR_NVS_INVALID_LENGTH + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * NVS limita namespace e key a 15 chars (+ '\0') + */ +#define STORAGE_NS_MAX_LEN 16 +#define STORAGE_KEY_MAX_LEN 16 + +#ifndef STORAGE_QUEUE_LEN +#define STORAGE_QUEUE_LEN 32 +#endif + +#ifndef STORAGE_MAX_PENDING +#define STORAGE_MAX_PENDING 48 +#endif + +#ifndef STORAGE_COMMIT_DEBOUNCE_MS +#define STORAGE_COMMIT_DEBOUNCE_MS 500 +#endif + +#ifndef STORAGE_MAX_VALUE_BYTES +#define STORAGE_MAX_VALUE_BYTES 96 +#endif + + /** + * Inicializa o serviço e cria a task interna. + * + * Requisitos: + * - nvs_flash_init() deve ter sido chamado antes. + */ + esp_err_t storage_service_init(void); + + // -------------------- Async setters (não bloqueiam; commit é debounced) -------------------- + + esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v); + esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v); + esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v); + + esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str); + esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len); + + esp_err_t storage_erase_key_async(const char *ns, const char *key); + + /** Força commit imediato (async). */ + esp_err_t storage_flush_async(void); + + // -------------------- Sync getters (bloqueiam até ler do NVS/pending) -------------------- + + esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to); + esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to); + esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to); + + esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to); + + /** + * Blob sync (semântica semelhante a nvs_get_blob): + * - Se out == NULL: devolve ESP_OK e coloca em *inout_len o tamanho requerido + * - Se out != NULL e *inout_len < requerido: devolve ESP_ERR_NVS_INVALID_LENGTH e atualiza *inout_len com requerido + * - Se OK: copia e atualiza *inout_len com o tamanho real + */ + esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to); + + /** Força commit imediato (sync). */ + esp_err_t storage_flush_sync(TickType_t to); + + // -------------------- (upgrade opcional) Sync setters -------------------- + // Úteis no boot/migração quando queres garantia forte e/ou a fila async pode estar cheia. + + esp_err_t storage_set_u8_sync(const char *ns, const char *key, uint8_t v, TickType_t to); + esp_err_t storage_set_u16_sync(const char *ns, const char *key, uint16_t v, TickType_t to); + esp_err_t storage_set_u32_sync(const char *ns, const char *key, uint32_t v, TickType_t to); + + esp_err_t storage_set_str_sync(const char *ns, const char *key, const char *str, TickType_t to); + esp_err_t storage_set_blob_sync(const char *ns, const char *key, const void *data, size_t len, TickType_t to); + + esp_err_t storage_erase_key_sync(const char *ns, const char *key, TickType_t to); + +#ifdef __cplusplus +} +#endif diff --git a/components/storage_service/src/storage_service.c b/components/storage_service/src/storage_service.c new file mode 100755 index 0000000..68ec836 --- /dev/null +++ b/components/storage_service/src/storage_service.c @@ -0,0 +1,1262 @@ +// ========================= +// storage_service.c (revisto) +// - FIX: commit não limpa pending se nvs_set falhar (limpa por-entry somente se aplicada + commit OK) +// - FIX: sync_req respeita timeout total (budget) +// - blob_query_only agora é respeitado (query não lê payload) +// - BLOB size=0 tratado +// - (upgrade opcional) adiciona setters sync (backward-compatible) +// ========================= + +#include "storage_service.h" + +#include +#include + +#include "esp_log.h" +#include "esp_err.h" +#include "nvs.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" + +static const char *TAG = "storage_service"; + +typedef enum +{ + OP_SET_U8, + OP_SET_U16, + OP_SET_U32, + OP_SET_STR, + OP_SET_BLOB, + + OP_ERASE_KEY, + + OP_GET_U8, + OP_GET_U16, + OP_GET_U32, + OP_GET_STR, + OP_GET_BLOB, + + OP_FLUSH, +} storage_op_t; + +typedef enum +{ + T_U8, + T_U16, + T_U32, + T_STR, + T_BLOB, +} storage_type_t; + +typedef struct +{ + esp_err_t err; + uint32_t value; // U8/U16/U32 + size_t len; // STR/BLOB: tamanho (STR sem '\0', BLOB tamanho real) + uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // payload para STR/BLOB (até MAX) +} storage_resp_t; + +typedef struct +{ + storage_op_t op; + char ns[STORAGE_NS_MAX_LEN]; + char key[STORAGE_KEY_MAX_LEN]; + + // SET/GET numéricos + uint32_t value; + + // SET STR/BLOB: bytes e len (copiados na mensagem => seguro async) + uint16_t len; + uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; + + // GET_BLOB: se true, apenas retorna len (não lê payload) + bool blob_query_only; + + QueueHandle_t resp_q; // opcional (GET/FLUSH/SET sync) +} storage_msg_t; + +typedef struct +{ + bool used; + bool erase; + char ns[STORAGE_NS_MAX_LEN]; + char key[STORAGE_KEY_MAX_LEN]; + storage_type_t type; + + uint32_t value; // U8/U16/U32 + uint16_t len; // STR/BLOB (até MAX) + uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // STR/BLOB +} pending_item_t; + +static bool s_inited = false; + +// Queue principal estática (evita malloc) +static StaticQueue_t s_qbuf; +static uint8_t s_qstorage[STORAGE_QUEUE_LEN * sizeof(storage_msg_t)]; +static QueueHandle_t s_q = NULL; + +// Pending table (evita malloc) +static pending_item_t s_pending[STORAGE_MAX_PENDING]; +static size_t s_pending_count = 0; + +// debounce +static bool s_dirty = false; +static TickType_t s_commit_deadline = 0; + +// Sync: fila global + mutex global (1 request sync por vez) +static StaticQueue_t s_sync_qbuf; +static uint8_t s_sync_qstor[sizeof(storage_resp_t)]; +static QueueHandle_t s_sync_q = NULL; + +static StaticSemaphore_t s_sync_mtx_buf; +static SemaphoreHandle_t s_sync_mtx = NULL; + +// -------------------- helpers -------------------- + +static inline esp_err_t map_not_found(esp_err_t e) +{ + return (e == ESP_ERR_NVS_NOT_FOUND) ? ESP_ERR_NOT_FOUND : e; +} + +static bool safe_copy_str(char *dst, size_t dst_sz, const char *src) +{ + if (!dst || dst_sz == 0 || !src) + return false; + size_t n = strnlen(src, dst_sz); + if (n >= dst_sz) + return false; + memcpy(dst, src, n); + dst[n] = '\0'; + return true; +} + +static TickType_t ticks_elapsed(TickType_t start, TickType_t now) +{ + // FreeRTOS ticks wrap: subtração unsigned é ok + return now - start; +} + +static TickType_t budget_left(TickType_t start, TickType_t to) +{ + if (to == portMAX_DELAY) + return portMAX_DELAY; + + TickType_t now = xTaskGetTickCount(); + TickType_t elapsed = ticks_elapsed(start, now); + if (elapsed >= to) + return 0; + return to - elapsed; +} + +static void pending_clear_slot(int idx) +{ + if (idx < 0 || idx >= (int)STORAGE_MAX_PENDING) + return; + + if (s_pending[idx].used) + { + s_pending[idx].used = false; + s_pending[idx].erase = false; + s_pending[idx].len = 0; + s_pending[idx].value = 0; + memset(s_pending[idx].bytes, 0, sizeof(s_pending[idx].bytes)); + s_pending[idx].ns[0] = '\0'; + s_pending[idx].key[0] = '\0'; + + if (s_pending_count > 0) + s_pending_count--; + } +} + +static void mark_dirty(void) +{ + s_dirty = true; + s_commit_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(STORAGE_COMMIT_DEBOUNCE_MS); +} + +static int find_pending(const char *ns, const char *key) +{ + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) == 0 && + strncmp(s_pending[i].key, key, STORAGE_KEY_MAX_LEN) == 0) + { + return i; + } + } + return -1; +} + +static bool pending_is_erased(const char *ns, const char *key) +{ + int idx = find_pending(ns, key); + if (idx < 0) + return false; + return s_pending[idx].used && s_pending[idx].erase; +} + +static int alloc_pending_slot(void) +{ + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + return i; + } + return -1; +} + +static esp_err_t ensure_pending_entry(int *out_idx, const char *ns, const char *key) +{ + int idx = find_pending(ns, key); + if (idx < 0) + { + idx = alloc_pending_slot(); + if (idx < 0) + return ESP_ERR_NO_MEM; + + memset(&s_pending[idx], 0, sizeof(s_pending[idx])); + s_pending[idx].used = true; + + if (!safe_copy_str(s_pending[idx].ns, sizeof(s_pending[idx].ns), ns)) + { + s_pending[idx].used = false; + return ESP_ERR_INVALID_ARG; + } + if (!safe_copy_str(s_pending[idx].key, sizeof(s_pending[idx].key), key)) + { + s_pending[idx].used = false; + return ESP_ERR_INVALID_ARG; + } + s_pending_count++; + } + if (out_idx) + *out_idx = idx; + return ESP_OK; +} + +static esp_err_t pending_set_num(const char *ns, const char *key, storage_type_t type, uint32_t v) +{ + int idx = -1; + esp_err_t err = ensure_pending_entry(&idx, ns, key); + if (err != ESP_OK) + return err; + + s_pending[idx].erase = false; + s_pending[idx].type = type; + s_pending[idx].value = v; + s_pending[idx].len = 0; + + mark_dirty(); + return ESP_OK; +} + +static esp_err_t pending_set_bytes(const char *ns, const char *key, storage_type_t type, + const uint8_t *bytes, uint16_t len) +{ + if (!bytes && len > 0) + return ESP_ERR_INVALID_ARG; + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + + int idx = -1; + esp_err_t err = ensure_pending_entry(&idx, ns, key); + if (err != ESP_OK) + return err; + + s_pending[idx].erase = false; + s_pending[idx].type = type; + s_pending[idx].value = 0; + s_pending[idx].len = len; + + if (len > 0) + memcpy(s_pending[idx].bytes, bytes, len); + if (len < STORAGE_MAX_VALUE_BYTES) + memset(&s_pending[idx].bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); + + mark_dirty(); + return ESP_OK; +} + +static esp_err_t pending_erase(const char *ns, const char *key) +{ + int idx = -1; + esp_err_t err = ensure_pending_entry(&idx, ns, key); + if (err != ESP_OK) + return err; + + s_pending[idx].erase = true; + mark_dirty(); + return ESP_OK; +} + +static bool pending_get_num(const char *ns, const char *key, storage_type_t type, uint32_t *out) +{ + int idx = find_pending(ns, key); + if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type) + return false; + + if (out) + *out = s_pending[idx].value; + return true; +} + +static bool pending_get_bytes(const char *ns, const char *key, storage_type_t type, + uint8_t *out, size_t out_sz, uint16_t *out_len) +{ + int idx = find_pending(ns, key); + if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type) + return false; + + uint16_t len = s_pending[idx].len; + + if (out_len) + *out_len = len; + + if (out) + { + size_t n = (len <= out_sz) ? (size_t)len : out_sz; + if (n > 0) + memcpy(out, s_pending[idx].bytes, n); + } + return true; +} + +// -------------------- Commit (FIX: limpa por-entry) -------------------- + +// Aplica uma pending entry no handle já aberto. +// Retorna ESP_OK se a operação foi aplicada com sucesso (ou erase de key inexistente). +static esp_err_t apply_pending_item(nvs_handle_t h, const pending_item_t *it) +{ + if (!it || !it->used) + return ESP_ERR_INVALID_ARG; + + if (it->erase) + { + esp_err_t e = nvs_erase_key(h, it->key); + if (e == ESP_ERR_NVS_NOT_FOUND) + return ESP_OK; // semântica: apagar key que não existe não é erro + return e; + } + + switch (it->type) + { + case T_U8: + return nvs_set_u8(h, it->key, (uint8_t)(it->value & 0xFF)); + case T_U16: + return nvs_set_u16(h, it->key, (uint16_t)(it->value & 0xFFFF)); + case T_U32: + return nvs_set_u32(h, it->key, it->value); + + case T_STR: + { + // garantir null-termination para nvs_set_str + uint16_t len = it->len; + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + if (len > 0) + memcpy(tmp, it->bytes, len); + tmp[len] = '\0'; + return nvs_set_str(h, it->key, tmp); + } + + case T_BLOB: + { + uint16_t len = it->len; + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + return nvs_set_blob(h, it->key, it->bytes, (size_t)len); + } + + default: + return ESP_ERR_INVALID_STATE; + } +} + +// Commit: agrupa por namespace e faz 1 commit por namespace +static esp_err_t commit_all_pending(void) +{ + if (s_pending_count == 0) + { + s_dirty = false; + return ESP_OK; + } + + esp_err_t overall = ESP_OK; + + // Lista de namespaces únicos (sem heap) + char ns_list[STORAGE_MAX_PENDING][STORAGE_NS_MAX_LEN]; + int ns_count = 0; + + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + + bool seen = false; + for (int j = 0; j < ns_count; ++j) + { + if (strncmp(ns_list[j], s_pending[i].ns, STORAGE_NS_MAX_LEN) == 0) + { + seen = true; + break; + } + } + if (!seen && ns_count < (int)STORAGE_MAX_PENDING) + { + (void)safe_copy_str(ns_list[ns_count], sizeof(ns_list[ns_count]), s_pending[i].ns); + ns_count++; + } + } + + for (int n = 0; n < ns_count; ++n) + { + const char *ns = ns_list[n]; + nvs_handle_t h; + + esp_err_t err_open = nvs_open(ns, NVS_READWRITE, &h); + if (err_open != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open('%s') failed: %s", ns, esp_err_to_name(err_open)); + overall = err_open; + continue; + } + + // Marca quais slots foram aplicados com sucesso para limpar apenas esses + bool clear_mask[STORAGE_MAX_PENDING]; + memset(clear_mask, 0, sizeof(clear_mask)); + + bool any_applied = false; + bool all_applied_ok = true; + + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0) + continue; + + any_applied = true; + + esp_err_t e = apply_pending_item(h, &s_pending[i]); + if (e == ESP_OK) + { + clear_mask[i] = true; // candidato a limpar após commit OK + } + else + { + all_applied_ok = false; + overall = e; + ESP_LOGE(TAG, "apply %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(e)); + // Mantém entry no pending para retry (ou debug). Não marca para limpar. + } + } + + esp_err_t err_commit = ESP_OK; + if (any_applied) + { + err_commit = nvs_commit(h); + if (err_commit != ESP_OK) + { + overall = err_commit; + ESP_LOGE(TAG, "commit('%s') failed: %s", ns, esp_err_to_name(err_commit)); + } + } + + nvs_close(h); + + // FIX: só limpa entries que: + // - foram aplicadas com sucesso (clear_mask[i]=true) + // - E o commit do namespace foi OK + if (err_commit == ESP_OK) + { + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (clear_mask[i]) + pending_clear_slot(i); + } + } + else + { + // Se commit falhou, não limpamos nada (nem as que aplicaram), + // para garantir retry consistente. + } + + (void)all_applied_ok; // mantido se quiseres usar para métricas no futuro + } + + if (s_pending_count == 0) + { + s_dirty = false; + } + else + { + // ainda há itens -> rearmar debounce para tentar novamente + mark_dirty(); + } + + return overall; +} + +// -------------------- Read helpers (cópia na resposta) -------------------- + +static esp_err_t nvs_read_num(storage_type_t type, const char *ns, const char *key, uint32_t *out) +{ + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint32_t pv = 0; + if (pending_get_num(ns, key, type, &pv)) + { + if (out) + *out = pv; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + switch (type) + { + case T_U8: + { + uint8_t v8 = 0; + err = nvs_get_u8(h, key, &v8); + if (err == ESP_OK && out) + *out = v8; + break; + } + case T_U16: + { + uint16_t v16 = 0; + err = nvs_get_u16(h, key, &v16); + if (err == ESP_OK && out) + *out = v16; + break; + } + case T_U32: + { + uint32_t v32 = 0; + err = nvs_get_u32(h, key, &v32); + if (err == ESP_OK && out) + *out = v32; + break; + } + default: + err = ESP_ERR_INVALID_ARG; + break; + } + + nvs_close(h); + return map_not_found(err); +} + +static esp_err_t nvs_read_str_to_resp(const char *ns, const char *key, storage_resp_t *resp) +{ + if (!resp) + return ESP_ERR_INVALID_ARG; + + resp->len = 0; + + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint16_t plen = 0; + if (pending_get_bytes(ns, key, T_STR, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen)) + { + resp->len = (size_t)plen; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + // 1) query required size (inclui '\0') + size_t req = 0; + err = nvs_get_str(h, key, NULL, &req); + if (err != ESP_OK) + { + nvs_close(h); + return map_not_found(err); + } + + if (req == 0) + { + nvs_close(h); + resp->len = 0; + return ESP_OK; + } + + size_t str_len = req - 1; // sem '\0' + resp->len = str_len; + + if (str_len > STORAGE_MAX_VALUE_BYTES) + { + nvs_close(h); + return ESP_ERR_NVS_INVALID_LENGTH; + } + + // 2) read + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + size_t tmp_sz = sizeof(tmp); + err = nvs_get_str(h, key, tmp, &tmp_sz); + nvs_close(h); + + if (err != ESP_OK) + return map_not_found(err); + + size_t n = strnlen(tmp, STORAGE_MAX_VALUE_BYTES); + memcpy(resp->bytes, tmp, n); + resp->len = n; + return ESP_OK; +} + +static esp_err_t nvs_query_blob_len(const char *ns, const char *key, size_t *out_len) +{ + if (!out_len) + return ESP_ERR_INVALID_ARG; + + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint16_t plen = 0; + if (pending_get_bytes(ns, key, T_BLOB, NULL, 0, &plen)) + { + *out_len = (size_t)plen; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + size_t req = 0; + err = nvs_get_blob(h, key, NULL, &req); + nvs_close(h); + + if (err != ESP_OK) + return map_not_found(err); + + *out_len = req; + return ESP_OK; +} + +static esp_err_t nvs_read_blob_to_resp(const char *ns, const char *key, storage_resp_t *resp) +{ + if (!resp) + return ESP_ERR_INVALID_ARG; + + resp->len = 0; + + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint16_t plen = 0; + if (pending_get_bytes(ns, key, T_BLOB, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen)) + { + resp->len = (size_t)plen; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + // query size + size_t req = 0; + err = nvs_get_blob(h, key, NULL, &req); + if (err != ESP_OK) + { + nvs_close(h); + if (err == ESP_ERR_NVS_NOT_FOUND) + resp->len = 0; + return map_not_found(err); + } + + resp->len = req; + + // size == 0 => nada a ler + if (req == 0) + { + nvs_close(h); + return ESP_OK; + } + + // Se maior que o máximo, não lemos payload + if (req > STORAGE_MAX_VALUE_BYTES) + { + nvs_close(h); + return ESP_OK; + } + + // read payload + size_t tmp = req; + err = nvs_get_blob(h, key, resp->bytes, &tmp); + nvs_close(h); + + if (err != ESP_OK) + return map_not_found(err); + + resp->len = tmp; + return ESP_OK; +} + +// -------------------- Task -------------------- + +static void storage_task(void *arg) +{ + (void)arg; + storage_msg_t msg; + + while (true) + { + TickType_t now = xTaskGetTickCount(); + + TickType_t wait = portMAX_DELAY; + if (s_dirty) + { + if (now >= s_commit_deadline) + { + (void)commit_all_pending(); + continue; + } + else + { + wait = s_commit_deadline - now; + } + } + + if (xQueueReceive(s_q, &msg, wait) != pdTRUE) + { + if (s_dirty && xTaskGetTickCount() >= s_commit_deadline) + { + (void)commit_all_pending(); + } + continue; + } + + storage_resp_t resp; + memset(&resp, 0, sizeof(resp)); + resp.err = ESP_OK; + + switch (msg.op) + { + case OP_SET_U8: + resp.err = pending_set_num(msg.ns, msg.key, T_U8, msg.value & 0xFF); + break; + case OP_SET_U16: + resp.err = pending_set_num(msg.ns, msg.key, T_U16, msg.value & 0xFFFF); + break; + case OP_SET_U32: + resp.err = pending_set_num(msg.ns, msg.key, T_U32, msg.value); + break; + + case OP_SET_STR: + resp.err = pending_set_bytes(msg.ns, msg.key, T_STR, msg.bytes, msg.len); + break; + + case OP_SET_BLOB: + resp.err = pending_set_bytes(msg.ns, msg.key, T_BLOB, msg.bytes, msg.len); + break; + + case OP_ERASE_KEY: + resp.err = pending_erase(msg.ns, msg.key); + break; + + case OP_GET_U8: + resp.err = nvs_read_num(T_U8, msg.ns, msg.key, &resp.value); + break; + case OP_GET_U16: + resp.err = nvs_read_num(T_U16, msg.ns, msg.key, &resp.value); + break; + case OP_GET_U32: + resp.err = nvs_read_num(T_U32, msg.ns, msg.key, &resp.value); + break; + + case OP_GET_STR: + resp.err = nvs_read_str_to_resp(msg.ns, msg.key, &resp); + break; + + case OP_GET_BLOB: + if (msg.blob_query_only) + { + size_t len = 0; + resp.err = nvs_query_blob_len(msg.ns, msg.key, &len); + resp.len = (resp.err == ESP_OK) ? len : 0; + } + else + { + // query+read (se couber) + resp.err = nvs_read_blob_to_resp(msg.ns, msg.key, &resp); + } + break; + + case OP_FLUSH: + resp.err = commit_all_pending(); + break; + + default: + resp.err = ESP_ERR_INVALID_ARG; + break; + } + + if (msg.resp_q) + { + (void)xQueueSend(msg.resp_q, &resp, 0); + } + } +} + +// -------------------- Public API -------------------- + +esp_err_t storage_service_init(void) +{ + if (s_inited) + return ESP_OK; + + s_q = xQueueCreateStatic(STORAGE_QUEUE_LEN, sizeof(storage_msg_t), s_qstorage, &s_qbuf); + if (!s_q) + return ESP_ERR_NO_MEM; + + s_sync_q = xQueueCreateStatic(1, sizeof(storage_resp_t), s_sync_qstor, &s_sync_qbuf); + if (!s_sync_q) + return ESP_ERR_NO_MEM; + + s_sync_mtx = xSemaphoreCreateMutexStatic(&s_sync_mtx_buf); + if (!s_sync_mtx) + return ESP_ERR_NO_MEM; + + memset(s_pending, 0, sizeof(s_pending)); + s_pending_count = 0; + s_dirty = false; + + BaseType_t ok = xTaskCreate(storage_task, "storage", 8192, NULL, 5, NULL); + if (ok != pdPASS) + return ESP_ERR_NO_MEM; + + s_inited = true; + ESP_LOGI(TAG, "storage_service init OK (queue=%d pending=%d debounce=%dms max_bytes=%d)", + STORAGE_QUEUE_LEN, STORAGE_MAX_PENDING, STORAGE_COMMIT_DEBOUNCE_MS, STORAGE_MAX_VALUE_BYTES); + return ESP_OK; +} + +static esp_err_t send_msg(const storage_msg_t *m, TickType_t to) +{ + if (!s_inited || !s_q) + return ESP_ERR_INVALID_STATE; + return (xQueueSend(s_q, m, to) == pdTRUE) ? ESP_OK : ESP_ERR_TIMEOUT; +} + +// -------------------- sync core (FIX: budget timeout) -------------------- + +static esp_err_t sync_req(const storage_msg_t *req, storage_resp_t *out_resp, TickType_t to) +{ + if (!s_inited || !s_sync_q || !s_sync_mtx) + return ESP_ERR_INVALID_STATE; + + TickType_t start = xTaskGetTickCount(); + + TickType_t b = budget_left(start, to); + if (b == 0) + return ESP_ERR_TIMEOUT; + + if (xSemaphoreTake(s_sync_mtx, b) != pdTRUE) + return ESP_ERR_TIMEOUT; + + // limpar resposta anterior + xQueueReset(s_sync_q); + + storage_msg_t m = *req; + m.resp_q = s_sync_q; + + b = budget_left(start, to); + if (b == 0) + { + xSemaphoreGive(s_sync_mtx); + return ESP_ERR_TIMEOUT; + } + + esp_err_t err = send_msg(&m, b); + if (err != ESP_OK) + { + xSemaphoreGive(s_sync_mtx); + return err; + } + + storage_resp_t r; + memset(&r, 0, sizeof(r)); + + b = budget_left(start, to); + if (b == 0) + { + xSemaphoreGive(s_sync_mtx); + return ESP_ERR_TIMEOUT; + } + + if (xQueueReceive(s_sync_q, &r, b) != pdTRUE) + { + xSemaphoreGive(s_sync_mtx); + return ESP_ERR_TIMEOUT; + } + + if (out_resp) + *out_resp = r; + + xSemaphoreGive(s_sync_mtx); + return r.err; +} + +// -------------------- async setters -------------------- + +static esp_err_t set_async_num(storage_op_t op, const char *ns, const char *key, uint32_t v) +{ + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = op; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + m.value = v; + return send_msg(&m, 0); +} + +esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v) +{ + return set_async_num(OP_SET_U8, ns, key, v); +} + +esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v) +{ + return set_async_num(OP_SET_U16, ns, key, v); +} + +esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v) +{ + return set_async_num(OP_SET_U32, ns, key, v); +} + +esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str) +{ + if (!str) + return ESP_ERR_INVALID_ARG; + + size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1); + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_SET_STR; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + + m.len = (uint16_t)len; + if (len > 0) + memcpy(m.bytes, str, len); + if (len < STORAGE_MAX_VALUE_BYTES) + memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); + + return send_msg(&m, 0); +} + +esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len) +{ + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + if (len > 0 && !data) + return ESP_ERR_INVALID_ARG; + + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_SET_BLOB; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + + m.len = (uint16_t)len; + if (len > 0) + memcpy(m.bytes, data, len); + if (len < STORAGE_MAX_VALUE_BYTES) + memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); + + return send_msg(&m, 0); +} + +esp_err_t storage_erase_key_async(const char *ns, const char *key) +{ + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_ERASE_KEY; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + return send_msg(&m, 0); +} + +esp_err_t storage_flush_async(void) +{ + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_FLUSH; + return send_msg(&m, 0); +} + +// -------------------- sync getters -------------------- + +esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_U8; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + if (err == ESP_OK && out) + *out = (uint8_t)(r.value & 0xFF); + return err; +} + +esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_U16; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + if (err == ESP_OK && out) + *out = (uint16_t)(r.value & 0xFFFF); + return err; +} + +esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_U32; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + if (err == ESP_OK && out) + *out = r.value; + return err; +} + +esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to) +{ + if (!out || out_sz == 0) + return ESP_ERR_INVALID_ARG; + + out[0] = '\0'; + + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_STR; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + + if (err != ESP_OK) + { + out[out_sz - 1] = '\0'; + return err; + } + + if (r.len >= out_sz) + { + out[0] = '\0'; + out[out_sz - 1] = '\0'; + return ESP_ERR_NVS_INVALID_LENGTH; + } + + if (r.len > 0) + memcpy(out, r.bytes, r.len); + out[r.len] = '\0'; + return ESP_OK; +} + +esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to) +{ + if (!inout_len) + return ESP_ERR_INVALID_ARG; + + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_BLOB; + req.blob_query_only = (out == NULL); + + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + + if (err != ESP_OK) + { + if (out == NULL && err == ESP_ERR_NOT_FOUND) + *inout_len = 0; + return err; + } + + // query mode: só devolver tamanho requerido + if (out == NULL) + { + *inout_len = r.len; + return ESP_OK; + } + + // read mode: valida tamanhos + if (r.len > *inout_len) + { + *inout_len = r.len; + return ESP_ERR_NVS_INVALID_LENGTH; + } + if (r.len > STORAGE_MAX_VALUE_BYTES) + { + *inout_len = r.len; + return ESP_ERR_NVS_INVALID_LENGTH; + } + + if (r.len > 0) + memcpy(out, r.bytes, r.len); + *inout_len = r.len; + return ESP_OK; +} + +esp_err_t storage_flush_sync(TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_FLUSH; + storage_resp_t r; + return sync_req(&req, &r, to); +} + +// -------------------- (upgrade opcional) setters sync -------------------- + +static esp_err_t set_sync_num(storage_op_t op, const char *ns, const char *key, uint32_t v, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = op; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + req.value = v; + + storage_resp_t r; + return sync_req(&req, &r, to); +} + +esp_err_t storage_set_u8_sync(const char *ns, const char *key, uint8_t v, TickType_t to) +{ + return set_sync_num(OP_SET_U8, ns, key, v, to); +} + +esp_err_t storage_set_u16_sync(const char *ns, const char *key, uint16_t v, TickType_t to) +{ + return set_sync_num(OP_SET_U16, ns, key, v, to); +} + +esp_err_t storage_set_u32_sync(const char *ns, const char *key, uint32_t v, TickType_t to) +{ + return set_sync_num(OP_SET_U32, ns, key, v, to); +} + +esp_err_t storage_set_str_sync(const char *ns, const char *key, const char *str, TickType_t to) +{ + if (!str) + return ESP_ERR_INVALID_ARG; + + size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1); + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_SET_STR; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + req.len = (uint16_t)len; + if (len > 0) + memcpy(req.bytes, str, len); + + storage_resp_t r; + return sync_req(&req, &r, to); +} + +esp_err_t storage_set_blob_sync(const char *ns, const char *key, const void *data, size_t len, TickType_t to) +{ + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + if (len > 0 && !data) + return ESP_ERR_INVALID_ARG; + + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_SET_BLOB; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + req.len = (uint16_t)len; + if (len > 0) + memcpy(req.bytes, data, len); + + storage_resp_t r; + return sync_req(&req, &r, to); +} + +esp_err_t storage_erase_key_sync(const char *ns, const char *key, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_ERASE_KEY; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + return sync_req(&req, &r, to); +} diff --git a/dependencies.lock b/dependencies.lock index 75fbdb5..c242beb 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -30,7 +30,7 @@ dependencies: type: service version: 1.0.18 espressif/mdns: - component_hash: 3ec0af5f6bce310512e90f482388d21cc7c0e99668172d2f895356165fc6f7c5 + component_hash: 29e47564b1a7ee778135e17fbbf2a2773f71c97ebabfe626c8eda7c958a7ad16 dependencies: - name: idf require: private @@ -38,7 +38,7 @@ dependencies: source: registry_url: https://components.espressif.com/ type: service - version: 1.8.2 + version: 1.9.1 espressif/ntc_driver: component_hash: 2e4752aa8fc9768365ee9198ba800141402a7466ff3133f617f1beab87bcc2ef dependencies: @@ -63,6 +63,6 @@ direct_dependencies: - espressif/mdns - espressif/ntc_driver - idf -manifest_hash: dd985fc132a07a7ca76fb5d4ccd1526be3bbb783f2a8ca2db065457e686212bd +manifest_hash: a9e96056e96b13af371d5c8d87f85c85e64afca6e06d96c10ecf1e3537459911 target: esp32 version: 2.0.0 diff --git a/main/idf_component.yml b/main/idf_component.yml index bba7198..59a411e 100755 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,8 +1,11 @@ ## IDF Component Manager Manifest File +version: "2.0.0" +targets: + - esp32 + dependencies: - espressif/mdns: =* - espressif/ntc_driver: ^0.3.0 - espressif/esp-modbus: =1.0.18 - idf: - version: '>=5.1.0' - espressif/cjson: ^1.7.19 + espressif/cjson: "^1.7.19" + espressif/esp-modbus: "^1.0.18" + espressif/mdns: "^1.8.2" + espressif/ntc_driver: "^0.3.0" + diff --git a/main/main.c b/main/main.c index 7bbdd98..2786a85 100755 --- a/main/main.c +++ b/main/main.c @@ -33,9 +33,10 @@ #include "ocpp.h" #include "led.h" #include "scheduler.h" +#include "storage_service.h" #define AP_CONNECTION_TIMEOUT 120000 -#define RESET_HOLD_TIME 30000 +#define RESET_HOLD_TIME 30000 // ms #define DEBOUNCE_TIME_MS 50 #define PRESS_BIT BIT0 @@ -48,6 +49,9 @@ static TickType_t press_tick = 0; static volatile TickType_t last_interrupt_tick = 0; static bool pressed = false; +// Spinlock para garantir debounce seguro na ISR +static portMUX_TYPE button_spinlock = portMUX_INITIALIZER_UNLOCKED; + // // File system (SPIFFS) init and info // @@ -56,9 +60,15 @@ 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) - ESP_LOGI(TAG, "Partition %s: total: %d, used: %d", conf->partition_label, total, used); + { + ESP_LOGI(TAG, "Partition %s: total: %d, used: %d", + conf->partition_label, (int)total, (int)used); + } else - ESP_LOGE(TAG, "Failed to get SPIFFS info: %s", esp_err_to_name(ret)); + { + ESP_LOGE(TAG, "Failed to get SPIFFS info (%s): %s", + conf->partition_label, esp_err_to_name(ret)); + } } static void fs_init(void) @@ -87,6 +97,7 @@ static void fs_init(void) // static void wifi_event_task_func(void *param) { + (void)param; EventBits_t mode_bits; for (;;) { @@ -107,6 +118,7 @@ static void wifi_event_task_func(void *param) pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT)) & WIFI_AP_CONNECTED_BIT) { + // Espera sair do AP xEventGroupWaitBits( wifi_event_group, WIFI_AP_DISCONNECTED_BIT, @@ -118,12 +130,15 @@ static void wifi_event_task_func(void *param) { if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) { + // Timeout sem cliente ligado // wifi_ap_stop(); + ESP_LOGW(TAG, "AP timeout sem conexões"); } } } else if (mode_bits & WIFI_STA_MODE_BIT) { + // Apenas aguarda desconexão STA xEventGroupWaitBits( wifi_event_group, WIFI_STA_DISCONNECTED_BIT, @@ -135,21 +150,30 @@ static void wifi_event_task_func(void *param) } // -// Button press handler +// Button press handler (short press => AP) // static void handle_button_press(void) { + // Pode ser chamado cedo demais se a ordem de init mudar + if (wifi_event_group == NULL) + { + ESP_LOGW(TAG, "Wi-Fi ainda não inicializado, ignorando botão Wi-Fi"); + return; + } + if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) { - ESP_LOGI(TAG, "Starting Wi-Fi AP mode"); + ESP_LOGI(TAG, "Starting Wi-Fi AP mode (short press)"); wifi_ap_start(); } } -// Task to handle button press/release notifications +// Task para lidar com notificações de botão (PRESS / RELEASE) static void user_input_task_func(void *param) { + (void)param; uint32_t notification; + for (;;) { if (xTaskNotifyWait( @@ -163,60 +187,64 @@ static void user_input_task_func(void *param) press_tick = xTaskGetTickCount(); pressed = true; ESP_LOGI(TAG, "Button Pressed"); - handle_button_press(); + // Decisão (short/long) é feita na soltura } if ((notification & RELEASED_BIT) && pressed) { pressed = false; - TickType_t held = xTaskGetTickCount() - press_tick; - ESP_LOGI(TAG, "Button Released (held %u ms)", (unsigned)pdTICKS_TO_MS(held)); + TickType_t held_ticks = xTaskGetTickCount() - press_tick; + uint32_t held_ms = pdTICKS_TO_MS(held_ticks); - if (held >= pdMS_TO_TICKS(RESET_HOLD_TIME)) + ESP_LOGI(TAG, "Button Released (held %u ms)", (unsigned)held_ms); + + if (held_ms >= RESET_HOLD_TIME) { ESP_LOGW(TAG, "Long press: erasing NVS + reboot"); nvs_flash_erase(); esp_restart(); } + else + { + // Short press: apenas habilita modo AP + handle_button_press(); + } } } } } -// ISR for button GPIO interrupt (active-low) +// ISR para GPIO do botão (ativo em nível baixo) static void IRAM_ATTR button_isr_handler(void *arg) { + (void)arg; BaseType_t higher_task_woken = pdFALSE; TickType_t now = xTaskGetTickCountFromISR(); + portENTER_CRITICAL_ISR(&button_spinlock); if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) { + portEXIT_CRITICAL_ISR(&button_spinlock); return; } last_interrupt_tick = now; if (user_input_task == NULL) { + portEXIT_CRITICAL_ISR(&button_spinlock); return; } int level = gpio_get_level(board_config.button_wifi_gpio); - if (level == 0) - { - xTaskNotifyFromISR( - user_input_task, - PRESS_BIT, - eSetBits, - &higher_task_woken); - } - else - { - xTaskNotifyFromISR( - user_input_task, - RELEASED_BIT, - eSetBits, - &higher_task_woken); - } + uint32_t bits = (level == 0) ? PRESS_BIT : RELEASED_BIT; + + xTaskNotifyFromISR( + user_input_task, + bits, + eSetBits, + &higher_task_woken); + + portEXIT_CRITICAL_ISR(&button_spinlock); if (higher_task_woken) { @@ -242,12 +270,15 @@ static void button_init(void) // static void init_modules(void) { + + ESP_ERROR_CHECK(storage_service_init()); + peripherals_init(); led_init(); - wifi_ini(); + wifi_ini(); // garante wifi_event_group inicializado buzzer_init(); ESP_ERROR_CHECK(rest_server_init("/data")); - protocols_init(); + evse_manager_init(); evse_init(); auth_init(); @@ -257,6 +288,7 @@ static void init_modules(void) evse_link_init(); ocpp_start(); scheduler_init(); + protocols_init(); } // @@ -286,17 +318,20 @@ void app_main(void) board_config_load(); - // 1) cria a task que recebe notificações do botão - xTaskCreate(user_input_task_func, "user_input_task", 4 * 1024, NULL, 3, &user_input_task); - - // 2) agora é seguro registrar ISR do botão - button_init(); - - // 3) inicia o resto do sistema + // 1) Inicia todos os módulos (inclui Wi-Fi, EVSE, etc.) init_modules(); - // 4) tasks auxiliares - xTaskCreate(wifi_event_task_func, "wifi_event_task", 8 * 1024, NULL, 3, NULL); + // 2) Cria a task que recebe notificações do botão + BaseType_t rc; + rc = xTaskCreate(user_input_task_func, "user_input_task", 4 * 1024, NULL, 3, &user_input_task); + configASSERT(rc == pdPASS); + + // 3) Agora é seguro registrar ISR do botão + button_init(); + + // 4) Task auxiliar para eventos Wi-Fi + rc = xTaskCreate(wifi_event_task_func, "wifi_event_task", 8 * 1024, NULL, 3, NULL); + configASSERT(rc == pdPASS); } // === Fim de: main/main.c === diff --git a/managed_components/espressif__mdns/.component_hash b/managed_components/espressif__mdns/.component_hash index 60f4024..3bf22d0 100644 --- a/managed_components/espressif__mdns/.component_hash +++ b/managed_components/espressif__mdns/.component_hash @@ -1 +1 @@ -3ec0af5f6bce310512e90f482388d21cc7c0e99668172d2f895356165fc6f7c5 \ No newline at end of file +29e47564b1a7ee778135e17fbbf2a2773f71c97ebabfe626c8eda7c958a7ad16 \ No newline at end of file diff --git a/managed_components/espressif__mdns/.cz.yaml b/managed_components/espressif__mdns/.cz.yaml index 2b6a744..277c642 100644 --- a/managed_components/espressif__mdns/.cz.yaml +++ b/managed_components/espressif__mdns/.cz.yaml @@ -3,6 +3,6 @@ commitizen: bump_message: 'bump(mdns): $current_version -> $new_version' pre_bump_hooks: python ../../ci/changelog.py mdns tag_format: mdns-v$version - version: 1.8.2 + version: 1.9.1 version_files: - idf_component.yml diff --git a/managed_components/espressif__mdns/CHANGELOG.md b/managed_components/espressif__mdns/CHANGELOG.md index 75a81db..7387aea 100644 --- a/managed_components/espressif__mdns/CHANGELOG.md +++ b/managed_components/espressif__mdns/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [1.9.1](https://github.com/espressif/esp-protocols/commits/mdns-v1.9.1) + +### Bug Fixes + +- Fix to use tagged AFL image + minor format fix ([2b2f009a](https://github.com/espressif/esp-protocols/commit/2b2f009a)) +- Fix unused variable `dcst` warning for wifi-remote chips ([081eef88](https://github.com/espressif/esp-protocols/commit/081eef88)) + +## [1.9.0](https://github.com/espressif/esp-protocols/commits/mdns-v1.9.0) + +### Features + +- support null value for boolean txt records ([fa96de3b](https://github.com/espressif/esp-protocols/commit/fa96de3b)) + +### Bug Fixes + +- Add test case for bool/NULL txt handling ([5068f221](https://github.com/espressif/esp-protocols/commit/5068f221)) +- Temporary fix for build issues on IDF master ([0197c994](https://github.com/espressif/esp-protocols/commit/0197c994)) +- Add tests for delegated answers ([487a746d](https://github.com/espressif/esp-protocols/commit/487a746d)) +- Add fuzzing into mdns CI ([af6bb1b5](https://github.com/espressif/esp-protocols/commit/af6bb1b5)) +- Host test to use hw_support include dir ([8bba3a97](https://github.com/espressif/esp-protocols/commit/8bba3a97)) +- Fixes case where we create our own malloc/free allocators, therefore we need to call mdns_mem_free and not free ([63bf7091](https://github.com/espressif/esp-protocols/commit/63bf7091)) +- put srv/txt records in additional section for ptr queries ([b7b8c5db](https://github.com/espressif/esp-protocols/commit/b7b8c5db)) + +### Updated + +- ci(common): Update test component dir for IDFv6.0 ([18418c83](https://github.com/espressif/esp-protocols/commit/18418c83)) + ## [1.8.2](https://github.com/espressif/esp-protocols/commits/mdns-v1.8.2) ### Bug Fixes diff --git a/managed_components/espressif__mdns/examples/query_advertise/main/idf_component.yml b/managed_components/espressif__mdns/examples/query_advertise/main/idf_component.yml index e9277df..f7245a2 100644 --- a/managed_components/espressif__mdns/examples/query_advertise/main/idf_component.yml +++ b/managed_components/espressif__mdns/examples/query_advertise/main/idf_component.yml @@ -1,8 +1,6 @@ dependencies: - ## Required IDF version - idf: ">=5.0" espressif/mdns: - version: "^1.0.0" - override_path: "../../../" + version: ^1.0.0 + idf: '>=5.0' protocol_examples_common: path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/managed_components/espressif__mdns/idf_component.yml b/managed_components/espressif__mdns/idf_component.yml index c920b72..6465f4d 100644 --- a/managed_components/espressif__mdns/idf_component.yml +++ b/managed_components/espressif__mdns/idf_component.yml @@ -7,7 +7,7 @@ documentation: https://docs.espressif.com/projects/esp-protocols/mdns/docs/lates issues: https://github.com/espressif/esp-protocols/issues repository: git://github.com/espressif/esp-protocols.git repository_info: - commit_sha: e9d7350219dfb5e39eb56e5ef60c094190888c55 + commit_sha: 3bfa00389de6f0d6d40efda8bea808380899a43d path: components/mdns url: https://github.com/espressif/esp-protocols/tree/master/components/mdns -version: 1.8.2 +version: 1.9.1 diff --git a/managed_components/espressif__mdns/mdns.c b/managed_components/espressif__mdns/mdns.c index 20ee5ee..22cfaf7 100644 --- a/managed_components/espressif__mdns/mdns.c +++ b/managed_components/espressif__mdns/mdns.c @@ -33,7 +33,7 @@ static void _mdns_browse_send(mdns_browse_t *browse, mdns_if_t interface); #if ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(5, 1, 0) #define MDNS_ESP_WIFI_ENABLED CONFIG_SOC_WIFI_SUPPORTED #else -#define MDNS_ESP_WIFI_ENABLED CONFIG_ESP_WIFI_ENABLED +#define MDNS_ESP_WIFI_ENABLED (CONFIG_ESP_WIFI_ENABLED || CONFIG_ESP_WIFI_REMOTE_ENABLED) #endif #if MDNS_ESP_WIFI_ENABLED && (CONFIG_MDNS_PREDEF_NETIF_STA || CONFIG_MDNS_PREDEF_NETIF_AP) @@ -1795,8 +1795,8 @@ static bool _mdns_create_answer_from_service(mdns_tx_packet_t *packet, mdns_serv // According to RFC6763-section12.1, for DNS-SD, SRV, TXT and all address records // should be included in additional records. if (!_mdns_alloc_answer(&packet->answers, MDNS_TYPE_PTR, service, NULL, false, false) || - !_mdns_alloc_answer(is_delegated ? &packet->additional : &packet->answers, MDNS_TYPE_SRV, service, NULL, send_flush, false) || - !_mdns_alloc_answer(is_delegated ? &packet->additional : &packet->answers, MDNS_TYPE_TXT, service, NULL, send_flush, false) || + !_mdns_alloc_answer(&packet->additional, MDNS_TYPE_SRV, service, NULL, send_flush, false) || + !_mdns_alloc_answer(&packet->additional, MDNS_TYPE_TXT, service, NULL, send_flush, false) || !_mdns_alloc_answer((shared || is_delegated) ? &packet->additional : &packet->answers, MDNS_TYPE_A, service, host, send_flush, false) || !_mdns_alloc_answer((shared || is_delegated) ? &packet->additional : &packet->answers, MDNS_TYPE_AAAA, service, host, @@ -2656,13 +2656,18 @@ static mdns_txt_linked_item_t *_mdns_allocate_txt(size_t num_items, mdns_txt_ite mdns_mem_free(new_item); break; } - new_item->value = mdns_mem_strdup(txt[i].value); - if (!new_item->value) { - mdns_mem_free((char *)new_item->key); - mdns_mem_free(new_item); - break; + if (txt[i].value) { + new_item->value = mdns_mem_strdup(txt[i].value); + if (!new_item->value) { + mdns_mem_free((char *)new_item->key); + mdns_mem_free(new_item); + break; + } + new_item->value_len = strlen(new_item->value); + } else { + new_item->value = NULL; + new_item->value_len = 0; } - new_item->value_len = strlen(new_item->value); new_item->next = new_txt; new_txt = new_item; } @@ -4488,9 +4493,9 @@ void mdns_preset_if_handle_system_event(void *arg, esp_event_base_t event_base, return; } - esp_netif_dhcp_status_t dcst; #if MDNS_ESP_WIFI_ENABLED && (CONFIG_MDNS_PREDEF_NETIF_STA || CONFIG_MDNS_PREDEF_NETIF_AP) if (event_base == WIFI_EVENT) { + esp_netif_dhcp_status_t dcst; switch (event_id) { case WIFI_EVENT_STA_CONNECTED: if (!esp_netif_dhcpc_get_status(esp_netif_from_preset_if(MDNS_IF_STA), &dcst)) { @@ -4517,6 +4522,7 @@ void mdns_preset_if_handle_system_event(void *arg, esp_event_base_t event_base, #endif #if CONFIG_ETH_ENABLED && CONFIG_MDNS_PREDEF_NETIF_ETH if (event_base == ETH_EVENT) { + esp_netif_dhcp_status_t dcst; switch (event_id) { case ETHERNET_EVENT_CONNECTED: if (!esp_netif_dhcpc_get_status(esp_netif_from_preset_if(MDNS_IF_ETH), &dcst)) { diff --git a/managed_components/espressif__mdns/private_include/mdns_private.h b/managed_components/espressif__mdns/private_include/mdns_private.h index ce4c96b..6bc0891 100644 --- a/managed_components/espressif__mdns/private_include/mdns_private.h +++ b/managed_components/espressif__mdns/private_include/mdns_private.h @@ -154,7 +154,7 @@ } \ } -#define queueFree(type, queue) while (queue) { type * _q = queue; queue = queue->next; free(_q); } +#define queueFree(type, queue) while (queue) { type * _q = queue; queue = queue->next; mdns_mem_free(_q); } #define PCB_STATE_IS_PROBING(s) (s->state > PCB_OFF && s->state < PCB_ANNOUNCE_1) #define PCB_STATE_IS_ANNOUNCING(s) (s->state > PCB_PROBE_3 && s->state < PCB_RUNNING) diff --git a/managed_components/espressif__mdns/tests/host_test/dnsfixture.py b/managed_components/espressif__mdns/tests/host_test/dnsfixture.py index 6dcf0c9..c16b061 100644 --- a/managed_components/espressif__mdns/tests/host_test/dnsfixture.py +++ b/managed_components/espressif__mdns/tests/host_test/dnsfixture.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import logging import re @@ -92,10 +92,58 @@ class DnsPythonWrapper: if expect is None: expect = name if expected: - assert any(expect in answer for answer in answers), f"Expected record '{expect}' not found in answer section" + assert any(expect in answer for answer in answers), f"Expected record '{expect}' not in answer section" else: assert not any(expect in answer for answer in answers), f"Unexpected record '{expect}' found in answer section" + def parse_section(self, response, section: str, rdtype_text: str): + """Parse a specific response section (answer, authority, additional) for given rdtype. + + Returns list of textual records for that rdtype. + """ + out = [] + if not response: + return out + rrsets = [] + if section == 'answer': + rrsets = response.answer + elif section == 'authority': + rrsets = response.authority + elif section == 'additional': + rrsets = response.additional + else: + raise ValueError('invalid section') + for rr in rrsets: + if dns.rdatatype.to_text(rr.rdtype) != rdtype_text: + continue + for item in rr.items: + full = ( + f'{rr.name} {rr.ttl} ' + f'{dns.rdataclass.to_text(rr.rdclass)} ' + f'{dns.rdatatype.to_text(rr.rdtype)} ' + f'{item.to_text()}' + ) + out.append(full) + return out + + def check_additional(self, response, rdtype_text: str, owner_contains: str, expected: bool = True, expect_substr: str | None = None): + """Check Additional section for an RR of type rdtype_text whose owner includes owner_contains. + + If expect_substr is provided, also require it to appear in the textual RR. + """ + records = self.parse_section(response, 'additional', rdtype_text) + logger.info(f'additional({rdtype_text}): {records}') + + def _matches(line: str) -> bool: + in_owner = owner_contains in line + has_val = (expect_substr in line) if expect_substr else True + return in_owner and has_val + found = any(_matches(r) for r in records) + if expected: + assert found, f"Expected {rdtype_text} for {owner_contains} in Additional not found" + else: + assert not found, f"Unexpected {rdtype_text} for {owner_contains} found in Additional" + if __name__ == '__main__': if len(sys.argv) < 3: diff --git a/managed_components/espressif__mdns/tests/host_test/pytest_mdns.py b/managed_components/espressif__mdns/tests/host_test/pytest_mdns.py index f8b95f5..95fefc2 100644 --- a/managed_components/espressif__mdns/tests/host_test/pytest_mdns.py +++ b/managed_components/espressif__mdns/tests/host_test/pytest_mdns.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import logging @@ -65,6 +65,17 @@ def test_add_service(mdns_console, dig_app): dig_app.check_record('_http._tcp.local', query_type='PTR', expected=True) +def test_ptr_additional_records_for_service(dig_app): + # Query PTR for the service type and ensure SRV/TXT are in Additional (RFC 6763 §12.1) + resp = dig_app.run_query('_http._tcp.local', query_type='PTR') + # Answer section should have at least one PTR to the instance + answers = dig_app.parse_answer_section(resp, 'PTR') + assert any('test_service._http._tcp.local' in a for a in answers) + # Additional section should include SRV and TXT for the same instance + dig_app.check_additional(resp, 'SRV', 'test_service._http._tcp.local', expected=True) + dig_app.check_additional(resp, 'TXT', 'test_service._http._tcp.local', expected=True) + + def test_remove_service(mdns_console, dig_app): mdns_console.send_input('mdns_service_remove _http _tcp') mdns_console.send_input('mdns_service_lookup _http _tcp') diff --git a/managed_components/espressif__mdns/tests/test_afl_fuzz_host/esp32_mock.h b/managed_components/espressif__mdns/tests/test_afl_fuzz_host/esp32_mock.h index 68a3461..70a2037 100644 --- a/managed_components/espressif__mdns/tests/test_afl_fuzz_host/esp32_mock.h +++ b/managed_components/espressif__mdns/tests/test_afl_fuzz_host/esp32_mock.h @@ -55,8 +55,7 @@ #define pdMS_TO_TICKS(a) a #define xSemaphoreTake(s,d) true -#define xTaskDelete(a) -#define vTaskDelete(a) free(a) +#define vTaskDelete(a) free(NULL) #define xSemaphoreGive(s) #define xQueueCreateMutex(s) #define _mdns_pcb_init(a,b) true @@ -66,7 +65,7 @@ #define vSemaphoreDelete(s) free(s) #define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U #define xTaskCreatePinnedToCore(a,b,c,d,e,f,g) *(f) = malloc(1) -#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) true +#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) ((void*)1) #define vTaskDelay(m) usleep((m)*0) #define esp_random() (rand()%UINT32_MAX) @@ -139,4 +138,8 @@ TaskHandle_t xTaskGetCurrentTaskHandle(void); void xTaskNotifyGive(TaskHandle_t task); BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, uint32_t *value, TickType_t wait_time); +static inline void xTaskGetStaticBuffers(void *pvTaskBuffer, void *pvStackBuffer, void *pvTaskTCB) +{ +} + #endif //_ESP32_COMPAT_H_ diff --git a/managed_components/espressif__mdns/tests/test_afl_fuzz_host/test.c b/managed_components/espressif__mdns/tests/test_afl_fuzz_host/test.c index afbf023..d753dc9 100644 --- a/managed_components/espressif__mdns/tests/test_afl_fuzz_host/test.c +++ b/managed_components/espressif__mdns/tests/test_afl_fuzz_host/test.c @@ -78,30 +78,20 @@ static int mdns_test_service_txt_set(const char *service, const char *proto, ui static int mdns_test_sub_service_add(const char *sub_name, const char *service_name, const char *proto, uint32_t port) { if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) { - // This is expected failure as the service thread is not running + return ESP_FAIL; } - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) { return ESP_FAIL; } - int ret = mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name); - a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); - return ret; + return mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name); } static int mdns_test_service_add(const char *service_name, const char *proto, uint32_t port) { if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) { - // This is expected failure as the service thread is not running + return ESP_FAIL; } - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) { return ESP_FAIL; @@ -266,9 +256,6 @@ int main(int argc, char **argv) } #ifndef MDNS_NO_SERVICES mdns_service_remove_all(); - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); #endif ForceTaskDelete(); mdns_free(); diff --git a/managed_components/espressif__mdns/tests/unit_test/CMakeLists.txt b/managed_components/espressif__mdns/tests/unit_test/CMakeLists.txt index 450d475..32bdad8 100644 --- a/managed_components/espressif__mdns/tests/unit_test/CMakeLists.txt +++ b/managed_components/espressif__mdns/tests/unit_test/CMakeLists.txt @@ -1,7 +1,15 @@ # This is the project CMakeLists.txt file for the test subproject cmake_minimum_required(VERSION 3.16) -set(EXTRA_COMPONENT_DIRS ../.. "$ENV{IDF_PATH}/tools/unit-test-app/components") - include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "6.0") + set(test_component_dir $ENV{IDF_PATH}/tools/test_apps/components) +else() + set(test_component_dir $ENV{IDF_PATH}/tools/unit-test-app/components) +endif() + +set(EXTRA_COMPONENT_DIRS ../.. + ${test_component_dir}) + project(mdns_test) diff --git a/managed_components/espressif__mdns/tests/unit_test/main/test_mdns.c b/managed_components/espressif__mdns/tests/unit_test/main/test_mdns.c index 6b9bfbe..6ad72d6 100644 --- a/managed_components/espressif__mdns/tests/unit_test/main/test_mdns.c +++ b/managed_components/espressif__mdns/tests/unit_test/main/test_mdns.c @@ -61,6 +61,45 @@ TEST(mdns, init_deinit) esp_event_loop_delete_default(); } +TEST(mdns, boolean_txt_null_value) +{ + mdns_result_t *results = NULL; + test_case_uses_tcpip(); + TEST_ASSERT_EQUAL(ESP_OK, esp_event_loop_create_default()); + TEST_ASSERT_EQUAL(ESP_OK, mdns_init()); + TEST_ASSERT_EQUAL(ESP_OK, mdns_hostname_set(MDNS_HOSTNAME)); + + TEST_ASSERT_EQUAL(ESP_OK, mdns_service_add(MDNS_INSTANCE, MDNS_SERVICE_NAME, MDNS_SERVICE_PROTO, MDNS_SERVICE_PORT, NULL, 0)); + + mdns_txt_item_t txt_data[] = { + {"bool", NULL}, + {"key", "value"}, + }; + const size_t txt_data_count = sizeof(txt_data) / sizeof(txt_data[0]); + TEST_ASSERT_EQUAL(ESP_OK, mdns_service_txt_set(MDNS_SERVICE_NAME, MDNS_SERVICE_PROTO, txt_data, txt_data_count)); + yield_to_all_priorities(); + + TEST_ASSERT_EQUAL(ESP_OK, mdns_lookup_selfhosted_service(NULL, MDNS_SERVICE_NAME, MDNS_SERVICE_PROTO, 1, &results)); + TEST_ASSERT_NOT_EQUAL(NULL, results); + TEST_ASSERT_NOT_EQUAL(NULL, results->txt); + TEST_ASSERT_EQUAL(txt_data_count, results->txt_count); + + bool found_bool = false; + for (size_t i = 0; i < results->txt_count; ++i) { + if (strcmp(results->txt[i].key, "bool") == 0) { + TEST_ASSERT_NOT_EQUAL(NULL, results->txt_value_len); + TEST_ASSERT_EQUAL_UINT8(0, results->txt_value_len[i]); + found_bool = true; + } + } + TEST_ASSERT_TRUE(found_bool); + mdns_query_results_free(results); + + TEST_ASSERT_EQUAL(ESP_OK, mdns_service_remove(MDNS_SERVICE_NAME, MDNS_SERVICE_PROTO)); + mdns_free(); + esp_event_loop_delete_default(); +} + TEST(mdns, api_fails_with_expected_err) { mdns_txt_item_t serviceTxtData[CONFIG_MDNS_MAX_SERVICES] = { {NULL, NULL}, @@ -290,6 +329,7 @@ TEST_GROUP_RUNNER(mdns) RUN_TEST_CASE(mdns, init_deinit) RUN_TEST_CASE(mdns, add_remove_service) RUN_TEST_CASE(mdns, add_remove_deleg_service) + RUN_TEST_CASE(mdns, boolean_txt_null_value) } diff --git a/partitions.csv b/partitions.csv index e867783..630dbf8 100755 --- a/partitions.csv +++ b/partitions.csv @@ -1,7 +1,6 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x4000, -phy_init, data, phy, 0xd000, 0x1000, -factory, app, factory, 0x10000, 3500K, -fctry, data, nvs, , 0x6000 -cfg, data, spiffs, , 16K, -data, data, spiffs, , 304K, +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0xA000, +phy_init, data, phy, 0x13000, 0x1000, +factory, app, factory, 0x20000, 3500K, +cfg, data, spiffs, 0x38B000, 0x4000, +data, data, spiffs, 0x38F000, 0x4C000, diff --git a/projeto_parte1.c b/projeto_parte1.c index b3d5c1a..ced03e8 100644 --- a/projeto_parte1.c +++ b/projeto_parte1.c @@ -1,2507 +1,1183 @@ . -// === Início de: components/meter_manager/driver/meter_ade7758/meter_ade7758.h === -#pragma once +// === Início de: components/storage_service/src/storage_service.c === +// ========================= +// storage_service.c (corrigido) +// - Remove ponteiros out_ptr/inout_len_ptr da mensagem +// - GET_STR/GET_BLOB devolvem dados via storage_resp_t (cópia) +// - Evita corrupção se houver timeout +// ========================= -#ifdef __cplusplus -extern "C" { -#endif +#include "storage_service.h" -#include +#include #include + +#include "esp_log.h" #include "esp_err.h" +#include "nvs.h" -/** - * @brief Inicializa o driver do medidor ADE7758 (SPI, mutex, registradores). - */ -esp_err_t meter_ade7758_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor ADE7758. - */ -esp_err_t meter_ade7758_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor ADE7758. - */ -void meter_ade7758_stop(void); - - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/meter_manager/driver/meter_ade7758/meter_ade7758.h === - - -// === Início de: components/meter_manager/driver/meter_ade7758/ade7758.h === -#include "driver/spi_common.h" -#include "driver/spi_master.h" - - - -#define WRITE 0x80 // WRITE bit BT7 to write to registers -#define CLKIN 10000000 // ADE7758 frec, 10.000000MHz -#define PERIODO 50 // Actually it is frequency, it is used to calculate the amount of Cycles that it accumulates for energy. -#define PHASE_A 1 -#define PHASE_B 2 -#define PHASE_C 3 - - -//Register address - -//------Name--------Address---------Lenght -#define AWATTHR 0x01 //---------16 -#define BWATTHR 0x02 //---------16 -#define CWATTHR 0x03 //---------16 - -#define AVARHR 0x04 //---------16 -#define BVARHR 0x05 //---------16 -#define CVARHR 0x06 //---------16 - -#define AVAHR 0x07 //---------16 -#define BVAHR 0x08 //---------16 -#define CVAHR 0x09 //---------16 - -#define AIRMS 0x0A //---------24 -#define BIRMS 0x0B //---------24 -#define CIRMS 0x0C //---------24 - -#define AVRMS 0x0D //---------24 -#define BVRMS 0x0E //---------24 -#define CVRMS 0x0F //---------24 - -#define FREQ 0x10 //---------12 -#define TEMP 0x11 //---------8 -#define WFORM 0x12 //---------24 -#define OPMODE 0x13 //---------8 -#define MMODE 0x14 //---------8 -#define WAVMODE 0x15 //---------8 -#define COMPMODE 0x16 //---------8 -#define LCYCMODE 0x17 //---------8 -#define MASK 0x18 //---------24 -#define STATUS 0x19 //---------24 -#define RSTATUS 0x1A //---------24 -#define ZXTOUT 0x1B //---------16 -#define LINECYC 0x1C //---------16 -#define SAGCYC 0x1D //---------8 -#define SAGLVL 0x1E //---------8 -#define VPINTLVL 0x1F //---------8 -#define IPINTLVL 0x20 //---------8 -#define VPEAK 0x21 //---------8 -#define IPEAK 0x22 //---------8 -#define GAIN 0x23 //---------8 -#define AVRMSGAIN 0x24 //---------12 -#define BVRMSGAIN 0x25 //---------12 -#define CVRMSGAIN 0x26 //---------12 -#define AIGAIN 0x27 //---------12 -#define BIGAIN 0x28 //---------12 -#define CIGAIN 0x29 //---------12 -#define AWG 0x2A //---------12 -#define BWG 0x2B //---------12 -#define CWG 0x2C //---------12 -#define AVARG 0x2D //---------12 -#define BVARG 0x2E //---------12 -#define CVARG 0x2F //---------12 -#define AVAG 0x30 //---------12 -#define BVAG 0X31 //---------12 -#define CVAG 0x32 //---------12 -#define AVRMSOS 0x33 //---------12 -#define BVRMSOS 0X34 //---------12 -#define CVRMSOS 0X35 //---------12 -#define AIRMSOS 0X36 //---------12 -#define BIRMSOS 0X37 //---------12 -#define CIRMSOS 0X38 //---------12 -#define AWATTOS 0X39 //---------12 -#define BWATTOS 0X3A //---------12 -#define CWATTOS 0X3B //---------12 -#define AVAROS 0X3C //---------12 -#define BVAROS 0X3D //---------12 -#define CVAROS 0X3E //---------12 -#define APHCAL 0X3F //---------7 -#define BPHCAL 0X40 //---------7 -#define CPHCAL 0X41 //---------7 -#define WDIV 0X42 //---------8 -#define VARDIV 0X43 //---------8 -#define VADIV 0X44 //---------8 -#define APCFNUM 0X45 //---------16 -#define APCFDEN 0X46 //---------12 -#define VARCFNUM 0X47 //---------16 -#define VARCFDEN 0X48 //---------12 - -#define CHKSUM 0X7E //---------8 -#define VERSION 0x7f //---------8 -#define DUMMY_BYTE 0xFF - - -//bits - -/** -OPERATIONAL MODE REGISTER (0x13) -The general configuration of the ADE7758 is defined by writing to the OPMODE register. -Table 18 summarizes the functionality of each bit in the OPMODE register. - -Bit Location Bit Mnemonic Default Value Description -0 DISHPF 0 The HPFs in all current channel inputs are disabled when this bit is set. -1 DISLPF 0 The LPFs after the watt and VAR multipliers are disabled when this bit is set. -2 DISCF 1 The frequency outputs APCF and VARCF are disabled when this bit is set. -3 to 5 DISMOD 0 By setting these bits, ADE7758�s ADCs can be turned off. In normal operation, these bits should be left at Logic 0. - DISMOD[2:0] Description - 0 0 0 Normal operation. - 1 0 0 Redirect the voltage inputs to the signal paths for the current channels and the current inputs to the signal paths for the voltage channels. - 0 0 1 Switch off only the current channel ADCs. - 1 0 1 Switch off current channel ADCs and redirect the current input signals to the voltage channel signal paths. - 0 1 0 Switch off only the voltage channel ADCs. - 1 1 0 Switch off voltage channel ADCs and redirect the voltage input signals to the current channel signal paths. - 0 1 1 Put the ADE7758 in sleep mode. - 1 1 1 Put the ADE7758 in power-down mode (reduces AIDD to 1 mA typ). -6 SWRST 0 Software Chip Reset. A data transfer to the ADE7758 should not take place for at least 18 �s after a software reset. -7 RESERVED 0 This should be left at 0. - -*/ - -#define DISHPF 0x01 -#define DISLPF 0x02 -#define DISCF 0x04 -#define SWRST 0x40 - -/** -MEASUREMENT MODE REGISTER (0x14) -The configuration of the PERIOD and peak measurements made by the ADE7758 is defined by writing to the MMODE register. -Table 19 summarizes the functionality of each bit in the MMODE register. - -Bit Location Bit Mnemonic Default Value Description -0 to 1 FREQSEL 0 These bits are used to select the source of the measurement of the voltage line frequency. - FREQSEL1 FREQSEL0 Source - 0 0 Phase A - 0 1 Phase B - 1 0 Phase C - 1 1 Reserved -2 to 4 PEAKSEL 7 These bits select the phases used for the voltage and current peak registers. - Setting Bit 2 switches the IPEAK and VPEAK registers to hold the absolute values - of the largest current and voltage waveform (over a fixed number of half-line cycles) - from Phase A. The number of half-line cycles is determined by the content of the - LINECYC register. At the end of the LINECYC number of half-line cycles, the content - of the registers is replaced with the new peak values. Similarly, setting Bit 3 turns - on the peak detection for Phase B, and Bit 4 for Phase C. Note that if more than one - bit is set, the VPEAK and IPEAK registers can hold values from two different phases, that is, - the voltage and current peak are independently processed (see the Peak Current Detection section). -5 to 7 PKIRQSEL 7 These bits select the phases used for the peak interrupt detection. - Setting Bit 5 switches on the monitoring of the absolute current and voltage waveform to Phase A. - Similarly, setting Bit 6 turns on the waveform detection for Phase B, and Bit 7 for Phase C. - Note that more than one bit can be set for detection on multiple phases. - If the absolute values of the voltage or current waveform samples in the selected phases exceeds - the preset level specified in the VPINTLVL or IPINTLVL registers the corresponding bit(s) in the - STATUS registers are set (see the Peak Current Detection section). - -*/ - -#define FREQSEL0 0x01 -#define FREQSEL1 0x02 - - -/** -WAVEFORM MODE REGISTER (0x15) -The waveform sampling mode of the ADE7758 is defined by writing to the WAVMODE register. -Table 20 summarizes the functionality of each bit in the WAVMODE register. - -Bit Location Bit Mnemonic Default Value Description -0 to 1 PHSEL 0 These bits are used to select the phase of the waveform sample. - PHSEL[1:0] Source - 0 0 Phase A - 0 1 Phase B - 1 0 Phase C - 1 1 Reserved -2 to 4 WAVSEL 0 These bits are used to select the type of waveform. - WAVSEL[2:0] Source - 0 0 0 Current - 0 0 1 Voltage - 0 1 0 Active Power Multiplier Output - 0 1 1 Reactive Power Multiplier Output - 1 0 0 VA Multiplier Output - -Others- Reserved -5 to 6 DTRT 0 These bits are used to select the data rate. - DTRT[1:0] Update Rate - 0 0 26.04 kSPS (CLKIN/3/128) - 0 1 13.02 kSPS (CLKIN/3/256) - 1 0 6.51 kSPS (CLKIN/3/512) - 1 1 3.25 kSPS (CLKIN/3/1024) -7 VACF 0 Setting this bit to Logic 1 switches the VARCF output pin to an output - frequency that is proportional to the total apparent power (VA). - In the default state, Logic 0, the VARCF pin outputs a frequency proportional - to the total reactive power (VAR). -*/ - - - -/** -COMPUTATIONAL MODE REGISTER (0x16) -The computational method of the ADE7758 is defined by writing to the COMPMODE register. - -Bit Location Bit Mnemonic Default Value Description -0 to 1 CONSEL 0 These bits are used to select the input to the energy accumulation registers. - CONSEL[1:0] = 11 is reserved. IA, IB, and IC are IA, IB, and IC phase shifted by �90�, respectively. - Registers CONSEL[1, 0] = 00 CONSEL[1, 0] = 01 CONSEL[1, 0] = 10 - AWATTHR VA � IA VA � (IA � IB) VA � (IA�IB) - BWATTHR VB � IB 0 0 - CWATTHR VC � IC VC � (IC � IB) VC � IC - - AVARHR VA � IA VA � (IA � IB) VA � (IA�IB) - BVARHR VB � IB 0 0 - CVARHR VC � IC VC � (IC � IB) VC � IC - - AVAHR VARMS � IARMS VARMS � IARMS VARMS � ARMS - BVAHR VBRMS � IBRMS (VARMS + VCRMS)/2 � IBRMS VARMS � IBRMS - CVAHR VCRMS � ICRMS VCRMS � ICRMS VCRMS � ICRMS - -2 to 4 TERMSEL 7 These bits are used to select the phases to be included in the APCF and VARCF pulse outputs. - Setting Bit 2 selects Phase A (the inputs to AWATTHR and AVARHR registers) to be included. - Bit 3 and Bit 4 are for Phase B and Phase C, respectively. - Setting all three bits enables the sum of all three phases to be included in the frequency outputs - (see the Active Power Frequency Output and the Reactive Power Frequency Output sections). - -5 ABS 0 Setting this bit places the APCF output pin in absolute only mode. - Namely, the APCF output frequency is proportional to the sum of the absolute values of the watt-hour - accumulation registers (AWATTHR, BWATTHR, and CWATTHR). - Note that this bit only affects the APCF pin and has no effect on the content of the corresponding - registers. - -6 SAVAR 0 Setting this bit places the VARCF output pin in the signed adjusted mode. - Namely, the VARCF output frequency is proportional to the sign-adjusted sum of the VAR-hour accumulation - registers (AVARHR, BVARHR, and CVARHR). - The sign of the VAR is determined from the sign of the watt calculation from the corresponding phase, - that is, the sign of the VAR is flipped if the sign of the watt is negative, and if the watt is positive, - there is no change to the sign of the VAR. - Note that this bit only affects the VARCF pin and has no effect on the content of the corresponding - registers. - -7 NOLOAD 0 Setting this bit activates the no-load threshold in the ADE7758. -*/ - - -/** -LINE CYCLE ACCUMULATION MODE REGISTER (0x17) -The functionalities involved the line-cycle accumulation mode in the ADE7758 are defined by writing to the LCYCMODE register. - -Bit Location Bit Mnemonic Default Value Description - -0 LWATT 0 Setting this bit places the watt-hour accumulation registers - (AWATTHR, BWATTHR, and CWATTHR registers) into line-cycle accumulation mode. -1 LVAR 0 Setting this bit places the VAR-hour accumulation registers (AVARHR, BVARHR, and CVARHR registers) - into line-cycle accumulation mode. -2 LVA 0 Setting this bit places the VA-hour accumulation registers (AVAHR, BVAHR, and CVAHR registers) - into line-cycle accumulation mode. -3 to 5 ZXSEL 7 These bits select the phases used for counting the number of zero crossings in the line-cycle - accumulation mode. Bit 3, Bit 4, and Bit 5 select Phase A, Phase B, and Phase C, respectively. - More than one phase can be selected for the zero-crossing detection, - and the accumulation time is shortened accordingly. -6 RSTREAD 1 Setting this bit enables the read-with-reset for all the WATTHR, VARHR, and VAHR registers for all three - phases, that is, a read to those registers resets the registers to 0 after the content of the registers - have been read. This bit should be set to Logic 0 when the LWATT, LVAR, or LVA bits are set to Logic 1. -7 FREQSEL 0 Setting this bit causes the FREQ (0x10) register to display the period, instead of the frequency of the - line input. -*/ - - -#define LWATT 0x01 -#define LVAR 0x02 -#define LVA 0x04 -#define ZXSEL_A 0x08 -#define ZXSEL_B 0x10 -#define ZXSEL_C 0x20 -#define RSTREAD 0x40 -#define FREQSEL 0x80 - - - -/** INTERRUPT MASK REGISTER (0x18) -When an interrupt event occurs in the ADE7758, the IRQ logic output goes active low if the mask bit for this event is Logic 1 in the MASK register. -The IRQ logic output is reset to its default collector open state when the RSTATUS register is read. -describes the function of each bit in the interrupt mask register. -**/ - -// The next table summarizes the function of each bit for -// the Interrupt Enable Register - -/* Bit Mask // Bit Location / Description -#define AEHF 0x0001 // bit 0 - Enables an interrupt when there is a change in Bit 14 of any one of the three WATTHR registers, that is, the WATTHR register is half full. -#define REHF 0x0002 // bit 1 - Enables an interrupt when there is a change in Bit 14 of any one of the three VARHR registers, that is, the VARHR register is half full. -#define VAEHF 0x0004 // bit 2 - Enables an interrupt when there is a 0 to 1 transition in the MSB of any one of the three VAHR registers, that is, the VAHR register is half full. -#define SAGA 0x0008 // bit 3 - Enables an interrupt when there is a SAG on the line voltage of the Phase A. -#define SAGB 0x0010 // bit 4 - Enables an interrupt when there is a SAG on the line voltage of the Phase B. -#define SAGC 0x0020 // bit 5 - Enables an interrupt when there is a SAG on the line voltage of the Phase C. -#define ZXTOA 0x0040 // bit 6 - Enables an interrupt when there is a zero-crossing timeout detection on Phase A. -#define ZXTOB 0x0080 // bit 7 - Enables an interrupt when there is a zero-crossing timeout detection on Phase B. -#define ZXTOC 0x0100 // bit 8 - Enables an interrupt when there is a zero-crossing timeout detection on Phase C. -#define ZXA 0x0200 // bit 9 - Enables an interrupt when there is a zero crossing in the voltage channel of Phase A -#define ZXB 0x0400 // bit 10 - Enables an interrupt when there is a zero crossing in the voltage channel of Phase B -#define ZXC 0x0800 // bit 11 - Enables an interrupt when there is a zero crossing in the voltage channel of Phase C -#define LENERGY 0x1000 // bit 12 - Enables an interrupt when the energy accumulations over LINECYC are finished. -//RESERVED 0x2000 // bit 13 - RESERVED -#define PKV 0x4000 // bit 14 - Enables an interrupt when the voltage input selected in the MMODE register is above the value in the VPINTLVL register. -#define PKI 0x8000 // bit 15 - Enables an interrupt when the current input selected in the MMODE register is above the value in the IPINTLVL register. -#define WFSM 0x010000 // bit 16 - Enables an interrupt when data is present in the WAVEMODE register. -#define REVPAP 0x020000 // bit 17 - Enables an interrupt when there is a sign change in the watt calculation among any one of the phases specified by the TERMSEL bits in the COMPMODE register. -#define REVPRP 0x040000 // bit 18 - Enables an interrupt when there is a sign change in the VAR calculation among any one of the phases specified by the TERMSEL bits in the COMPMODE register. -#define SEQERR 0x080000 // bit 19 - Enables an interrupt when the zero crossing from Phase A is followed not by the zero crossing of Phase C but with that of Phase B. -*/ -/** INTERRUPT STATUS REGISTER (0x19)/RESET INTERRUPT STATUS REGISTER (0x1A) -The interrupt status register is used to determine the source of an interrupt event. -When an interrupt event occurs in the ADE7758, the corresponding flag in the interrupt status register is set. -The IRQ pin goes active low if the corresponding bit in the interrupt mask register is set. -When the MCU services the interrupt, it must first carry out a read from the interrupt status register to determine the source of the interrupt. -All the interrupts in the interrupt status register stay at their logic high state after an event occurs. -The state of the interrupt bit in the interrupt status register is reset to its default value once the reset interrupt status register is read. -**/ - -// The next table summarizes the function of each bit for -// the Interrupt Status Register, the Reset Interrupt Status Register. - -// Bit Mask // Bit Location / Description -#define AEHF 0x0001 // bit 0 - Indicates that an interrupt was caused by a change in Bit 14 among any one of the three WATTHR registers, that is, the WATTHR register is half full. -#define REHF 0x0002 // bit 1 - Indicates that an interrupt was caused by a change in Bit 14 among any one of the three VARHR registers, that is, the VARHR register is half full. -#define VAEHF 0x0004 // bit 2 - Indicates that an interrupt was caused by a 0 to 1 transition in Bit 15 among any one of the three VAHR registers, that is, the VAHR register is half full. -#define SAGA 0x0008 // bit 3 - Indicates that an interrupt was caused by a SAG on the line voltage of the Phase A. -#define SAGB 0x0010 // bit 4 - Indicates that an interrupt was caused by a SAG on the line voltage of the Phase B. -#define SAGC 0x0020 // bit 5 - Indicates that an interrupt was caused by a SAG on the line voltage of the Phase C. -#define ZXTOA 0x0040 // bit 6 - Indicates that an interrupt was caused by a missing zero crossing on the line voltage of the Phase A. -#define ZXTOB 0x0080 // bit 7 - Indicates that an interrupt was caused by a missing zero crossing on the line voltage of the Phase B. -#define ZXTOC 0x0100 // bit 8 - Indicates that an interrupt was caused by a missing zero crossing on the line voltage of the Phase C -#define ZXA 0x0200 // bit 9 - Indicates a detection of a rising edge zero crossing in the voltage channel of Phase A. -#define ZXB 0x0400 // bit 10 - Indicates a detection of a rising edge zero crossing in the voltage channel of Phase B -#define ZXC 0x0800 // bit 11 - Indicates a detection of a rising edge zero crossing in the voltage channel of Phase C -#define LENERGY 0x1000 // bit 12 - In line energy accumulation, indicates the end of an integration over an integer number of half- line cycles (LINECYC). See the Calibration section. -#define RESET 0x2000 // bit 13 - Indicates that the 5 V power supply is below 4 V. Enables a software reset of the ADE7758 and sets the registers back to their default values. This bit in the STATUS or RSTATUS register is logic high for only one clock cycle after a reset event. -#define PKV 0x4000 // bit 14 - Indicates that an interrupt was caused when the selected voltage input is above the value in the VPINTLVL register. -#define PKI 0x8000 // bit 15 - Indicates that an interrupt was caused when the selected current input is above the value in the IPINTLVL register. -#define WFSM 0x010000 // bit 16 - Indicates that new data is present in the waveform register. -#define REVPAP 0x020000 // bit 17 - Indicates that an interrupt was caused by a sign change in the watt calculation among any one of the phases specified by the TERMSEL bits in the COMPMODE register. -#define REVPRP 0x040000 // bit 18 - Indicates that an interrupt was caused by a sign change in the VAR calculation among any one of the phases specified by the TERMSEL bits in the COMPMODE register. -#define SEQERR 0x080000 // bit 19 - Indicates that an interrupt was caused by a zero crossing from Phase A followed not by the zero crossing of Phase C but by that of Phase B. - - -//constants -#define GAIN_1 0x00 -#define GAIN_2 0x01 -#define GAIN_4 0x02 -#define INTEGRATOR_ON 1 -#define INTEGRATOR_OFF 0 -#define FULLSCALESELECT_0_5V 0x00 -#define FULLSCALESELECT_0_25V 0x01 -#define FULLSCALESELECT_0_125V 0x02 - -esp_err_t transferByte(const uint8_t reg_addr, const uint8_t data, const uint8_t command); -esp_err_t transferMultiplesBytes(const uint8_t reg_addr, uint8_t *tx_buf, uint8_t *rx_buf, size_t data_length, const uint8_t command); - -esp_err_t Init(const spi_host_device_t spi_peripheral, const int pin_miso, const int pin_mosi, const int pin_sclk); -esp_err_t InitSpi(const int ss); - -esp_err_t RegisterDevice(const uint8_t mode, const int ss, const int addr_length, const int command_length, const int bus_speed); -uint8_t ReadRegister(const uint8_t reg_addr, const uint8_t command); -esp_err_t WriteRegister(const uint8_t reg_addr, const uint8_t reg_data, const uint8_t command); -esp_err_t WriteRegisterMultipleBytes(const uint8_t reg_addr, uint8_t *reg_data_buffer, const uint8_t byte_count, const uint8_t command); -esp_err_t ReadRegisterMultipleBytes(const uint8_t reg_addr, uint8_t *reg_data_buffer, const uint8_t byte_count, const uint8_t command); -spi_device_handle_t GetHandle(); - - -//---------------------------------------------------------------------------- -// Modes and configurations -//---------------------------------------------------------------------------- -void setOpMode(uint8_t m); -uint8_t getOpMode(); -void setMMode(uint8_t m); -uint8_t getMMode(); -void setWavMode(uint8_t m); -uint8_t getWavMode(); -void setCompMode(uint8_t m); -uint8_t getCompMode(); -void setLcycMode(uint8_t m); -uint8_t getLcycMode(); -void gainSetup(uint8_t integrator, uint8_t scale, uint8_t PGA2, uint8_t PGA1); -void setupDivs(uint8_t Watt_div,uint8_t VAR_div,uint8_t VA_div); -uint32_t getMaskInterrupts(); -void setMaskInterrupts(uint32_t m); -uint32_t getStatus(); -uint32_t resetStatus(); -int32_t getAIRMS(); -int32_t getBIRMS(); -int32_t getCIRMS(); -int32_t getAVRMS(); -int32_t getBVRMS(); -int32_t getCVRMS(); -uint32_t avrms(); -uint32_t bvrms(); -uint32_t cvrms(); -uint32_t airms(); -uint32_t birms(); -int32_t cirms(); -int32_t getFreq(); -void setLineCyc(uint32_t d); -int32_t getACurrentOffset(); -int32_t getBCurrentOffset(); -int32_t getCCurrentOffset(); -void setACurrentOffset(int32_t o); -void setBCurrentOffset(int32_t o); -void setCCurrentOffset(int32_t o); -int32_t getAVoltageOffset(); -int32_t getBVoltageOffset(); -int32_t getCVoltageOffset(); -void setAVoltageOffset(int32_t o); -void setBVoltageOffset(int32_t o); -void setCVoltageOffset(int32_t o); -void setAWattOffset(int32_t o); -void setBWattOffset(int32_t o); -void setCWattOffset(int32_t o); -void setZeroCrossingTimeout(int32_t d); -int32_t getZeroCrossingTimeout(); -uint8_t setPotLine(uint8_t Phase, uint32_t Ciclos); -int32_t getWatt(uint8_t Phase); -int32_t getVar(uint8_t Phase); -int32_t getVa(uint8_t Phase); -uint8_t getVersion(); -uint8_t read8(uint8_t reg); -uint32_t read16(uint8_t reg); -uint32_t read24(uint8_t reg); - -esp_err_t write24(uint8_t reg, uint32_t data); -esp_err_t write16(uint8_t reg, uint32_t data); -esp_err_t write8(uint8_t reg, uint8_t data); - -void enableADE7758Chip(); -void disableADE7758Chip(); -void setAPCFDEN(int32_t d); -int32_t getAPCFDEN(); -void setAPCFNUM(int32_t d); -int32_t getAPCFNUM(); -void setVARCFNUM(int32_t d); -int32_t getVARCFNUM(); -void setVARCFDEN(int32_t d); -int32_t getVARCFDEN(); -void setAWG(int32_t d); -int32_t getAWG(); -void setBWG(int32_t d); -void setCWG(int32_t d); -void setAVARG(int32_t d); -int32_t getAVARG(); -void setBVARG(int32_t d); -int32_t getBVARG(); -void setCVARG(int32_t d); -int32_t getCVARG(); -void setAVAG(int32_t d); -void setBVAG(int32_t d); -void setCVAG(int32_t d); -// === Fim de: components/meter_manager/driver/meter_ade7758/ade7758.h === - - -// === Início de: components/meter_manager/driver/meter_orno/modbus_params.h === -/* - * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef _DEVICE_PARAMS -#define _DEVICE_PARAMS - -#include - -#pragma pack(push, 1) - -// Discrete Inputs -typedef struct { - uint8_t discrete_input0 : 1; - uint8_t discrete_input1 : 1; - uint8_t discrete_input2 : 1; - uint8_t discrete_input3 : 1; - uint8_t discrete_input4 : 1; - uint8_t discrete_input5 : 1; - uint8_t discrete_input6 : 1; - uint8_t discrete_input7 : 1; - uint8_t discrete_input_port1; - uint8_t discrete_input_port2; -} discrete_reg_params_t; - -// Coils -typedef struct { - uint8_t coils_port0; - uint8_t coils_port1; - uint8_t coils_port2; -} coil_reg_params_t; - -// Input Registers (pode manter caso use em outro driver) -typedef struct { - float input_data0; - float input_data1; - float input_data2; - float input_data3; - uint16_t data[150]; - float input_data4; - float input_data5; - float input_data6; - float input_data7; - uint16_t data_block1[150]; -} input_reg_params_t; - - -// Holding Registers (ajustado para os campos usados no ORNO 516) -typedef struct { - float l1_current; - float l2_current; - float l3_current; - - float l1_voltage; - float l2_voltage; - float l3_voltage; - - float active_energy; - float reactive_energy; - - float active_power; - float apparent_power; - float reactive_power; - - float frequency; - float power_factor; -} holding_reg_params_t; - - - -#pragma pack(pop) - -// Instâncias globais das estruturas -extern holding_reg_params_t holding_reg_params; -extern input_reg_params_t input_reg_params; -extern coil_reg_params_t coil_reg_params; -extern discrete_reg_params_t discrete_reg_params; - -#endif // !_DEVICE_PARAMS - -// === Fim de: components/meter_manager/driver/meter_orno/modbus_params.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_orno.h === -#ifndef ORNO_MODBUS_H_ -#define ORNO_MODBUS_H_ - -#include -#include "esp_err.h" - - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor (SPI, mutex, registradores ADE7758). - */ -esp_err_t meter_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor. - */ -esp_err_t meter_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos. - */ -void meter_stop(void); - -/** - * @brief Verifica se o medidor está em execução. - * - * @return true se a tarefa estiver ativa, false caso contrário. - */ -bool meter_is_running(void); - -/** - * @brief Limpa os dados armazenados no medidor (zera todos os valores). - */ -void meter_clear_data(void); - -// ----- Leituras por fase (L1, L2, L3) ----- - -// Tensão RMS (em volts) -float meter_get_vrms_l1(void); -float meter_get_vrms_l2(void); -float meter_get_vrms_l3(void); - -// Corrente RMS (em amperes) -float meter_get_irms_l1(void); -float meter_get_irms_l2(void); -float meter_get_irms_l3(void); - -// Potência ativa (W) -int meter_get_watt_l1(void); -int meter_get_watt_l2(void); -int meter_get_watt_l3(void); - -// Potência reativa (VAR) -int meter_get_var_l1(void); -int meter_get_var_l2(void); -int meter_get_var_l3(void); - -// Potência aparente (VA) -int meter_get_va_l1(void); -int meter_get_va_l2(void); -int meter_get_va_l3(void); - -// (Opcional) contador de watchdog para diagnóstico -uint32_t meter_get_watchdog_counter(void); - -#ifdef __cplusplus -} -#endif - -#endif /* ORNO_MODBUS_H_ */ - -// === Fim de: components/meter_manager/driver/meter_orno/meter_orno.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_ea777.h === -#ifndef METER_DTS6619_H_ -#define METER_DTS6619_H_ - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores). - * - * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. - */ -esp_err_t meter_dts6619_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor DTS6619. - * - * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. - */ -esp_err_t meter_dts6619_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS6619. - */ -void meter_dts6619_stop(void); - - -#ifdef __cplusplus -} -#endif - -#endif /* METER_DTS6619_H_ */ - -// === Fim de: components/meter_manager/driver/meter_orno/meter_ea777.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_orno513.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor ORNO 513 (SPI, mutex, registradores). - */ -esp_err_t meter_orno513_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor ORNO 513. - */ -esp_err_t meter_orno513_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor ORNO 513. - */ -void meter_orno513_stop(void); - - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/meter_manager/driver/meter_orno/meter_orno513.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_orno516.h === -#ifndef METER_ORNO516_H_ -#define METER_ORNO516_H_ - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor ORNO 516 (SPI, mutex, registradores). - * - * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. - */ -esp_err_t meter_orno516_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor ORNO 516. - * - * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. - */ -esp_err_t meter_orno516_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor ORNO 516. - */ -void meter_orno516_stop(void); - - -#ifdef __cplusplus -} -#endif - -#endif /* METER_ORNO516_H_ */ - -// === Fim de: components/meter_manager/driver/meter_orno/meter_orno516.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_orno526.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor ORNO 526 (SPI, mutex, registradores). - */ -esp_err_t meter_orno526_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor ORNO 526. - */ -esp_err_t meter_orno526_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor ORNO 526. - */ -void meter_orno526_stop(void); - - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/meter_manager/driver/meter_orno/meter_orno526.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_dds661.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor DDS 661 (SPI, mutex, registradores). - */ -esp_err_t meter_dds661_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor DDS 661. - */ -esp_err_t meter_dds661_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor DDS 661. - */ -void meter_dds661_stop(void); - - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/meter_manager/driver/meter_orno/meter_dds661.h === - - -// === Início de: components/meter_manager/driver/meter_orno/meter_dts6619.h === -#ifndef METER_EA777_H_ -#define METER_EA777_H_ - -#include -#include -#include "esp_err.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Inicializa o driver do medidor EA777 (UART RS485, Modbus, registradores). - * - * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. - */ -esp_err_t meter_ea777_init(void); - -/** - * @brief Inicia a tarefa de leitura de dados do medidor EA777. - * - * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. - */ -esp_err_t meter_ea777_start(void); - -/** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor EA777. - */ -void meter_ea777_stop(void); - -#ifdef __cplusplus -} -#endif - -#endif /* METER_EA777_H_ */ - -// === Fim de: components/meter_manager/driver/meter_orno/meter_dts6619.h === - - -// === Início de: components/meter_manager/driver/meter_zigbee/meter_zigbee.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include "esp_err.h" - -/** - * @brief Inicializa o driver do medidor Zigbee (UART, mutex, etc.). - * - * @return ESP_OK se a inicialização for bem-sucedida, erro caso contrário. - */ -esp_err_t meter_zigbee_init(void); - -/** - * @brief Inicia a tarefa de leitura dos dados do medidor Zigbee. - * - * @return ESP_OK se a tarefa for iniciada com sucesso, erro caso contrário. - */ -esp_err_t meter_zigbee_start(void); - -/** - * @brief Interrompe a tarefa e limpa recursos (UART, mutex, etc.). - */ -void meter_zigbee_stop(void); - - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/meter_manager/driver/meter_zigbee/meter_zigbee.h === - - -// === Início de: components/meter_manager/include/meter_manager.h === -#ifndef METER_MANAGER_H -#define METER_MANAGER_H - -#include "esp_err.h" -#include - -/** - * @brief Supported meter types for EVSE and Grid. - */ -typedef enum { - METER_TYPE_NONE, // No meter - METER_TYPE_ADE7758, // ADE7758 meter - METER_TYPE_ORNO513, // ORNO-513 - METER_TYPE_ORNO516, // ORNO-516 - METER_TYPE_ORNO526, // ORNO-516 - METER_TYPE_DDS661, // DDS-661 - METER_TYPE_DTS6619, // dts6619 - METER_TYPE_MONO_ZIGBEE, // Zigbee single-phase - METER_TYPE_TRIF_ZIGBEE, // Zigbee three-phase - METER_TYPE_EA777 // EA777 -} meter_type_t; - -/** - * @brief Initializes the meter manager system. - * - * Registers network event handlers and initializes both EVSE and GRID meters. - */ -esp_err_t meter_manager_init(void); - - -/** - * @brief Starts all configured meters (EVSE and GRID). - * - * @return esp_err_t ESP_OK on success, or an error code from one of the start calls. - */ -esp_err_t meter_manager_start(void); - - -/** - * @brief Stops all meters and unregisters network event handlers. - */ -esp_err_t meter_manager_stop(void); - -/** - * @brief EVSE Meter Management - */ - -/** - * @brief Initializes the EVSE meter based on configured type. - */ -esp_err_t meter_manager_evse_init(void); - -/** - * @brief Starts the EVSE meter. - */ -esp_err_t meter_manager_evse_start(void); - -/** - * @brief Stops the EVSE meter. - */ -esp_err_t meter_manager_evse_stop(void); - -/** - * @brief Returns true if an EVSE meter is configured (not NONE). - */ -bool meter_manager_evse_is_enabled(void); - -/** - * @brief Sets the EVSE meter type and saves it to NVS. - */ -esp_err_t meter_manager_evse_set_model(meter_type_t meter_type); - -/** - * @brief Gets the current EVSE meter type. - */ -meter_type_t meter_manager_evse_get_model(void); - -/** - * @brief Grid Meter Management - */ - -/** - * @brief Initializes the Grid meter based on configured type. - */ -esp_err_t meter_manager_grid_init(void); - -/** - * @brief Starts the Grid meter. - */ -esp_err_t meter_manager_grid_start(void); - -/** - * @brief Stops the Grid meter. - */ -esp_err_t meter_manager_grid_stop(void); - -/** - * @brief Sets the Grid meter type and saves it to NVS. - */ -esp_err_t meter_manager_grid_set_model(meter_type_t meter_type); - -/** - * @brief Gets the current Grid meter type. - */ -meter_type_t meter_manager_grid_get_model(void); - -/** - * @brief Utility functions - */ - -/** - * @brief Converts a meter_type_t to a human-readable string. - */ -const char* meter_type_to_str(meter_type_t type); - -/** - * @brief Converts a string to a meter_type_t. - */ -meter_type_t string_to_meter_type(const char *str); - -#endif // METER_MANAGER_H - -// === Fim de: components/meter_manager/include/meter_manager.h === - - -// === Início de: components/meter_manager/include/meter_events.h === -#ifndef METER_EVENTS_H -#define METER_EVENTS_H - -#include "esp_event.h" -#include "meter_manager.h" // Para meter_type_t - -#ifdef __cplusplus -extern "C" { -#endif - -// Base de eventos dos medidores -ESP_EVENT_DECLARE_BASE(METER_EVENT); - -// IDs de eventos emitidos por medidores -typedef enum { - METER_EVENT_DATA_READY = 0, - METER_EVENT_ERROR, - METER_EVENT_STARTED, - METER_EVENT_STOPPED -} meter_event_id_t; - -// Estrutura de dados enviados com METER_EVENT_DATA_READY -typedef struct { - const char *source; // "GRID" ou "EVSE" - float vrms[3]; // Tensão por fase - float irms[3]; // Corrente por fase - int watt[3]; // Potência ativa por fase - float frequency; // Frequência da rede (Hz) - float power_factor; // Fator de potência - float total_energy; // Energia acumulada (kWh) -} meter_event_data_t; - - -#ifdef __cplusplus -} -#endif - -#endif // METER_EVENTS_H - -// === Fim de: components/meter_manager/include/meter_events.h === - - -// === Início de: components/loadbalancer/include/loadbalancer_events.h === -#pragma once -#include "esp_event.h" -#include -#include -#include "esp_timer.h" - -ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); - -typedef enum { - LOADBALANCER_EVENT_INIT, - LOADBALANCER_EVENT_STATE_CHANGED, - LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT, - LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, - LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, - LOADBALANCER_EVENT_SLAVE_STATUS -} loadbalancer_event_id_t; - -typedef struct { - bool enabled; - int64_t timestamp_us; -} loadbalancer_state_event_t; - -// (opcional) -typedef struct { - float limit; - int64_t timestamp_us; -} loadbalancer_global_limit_event_t; - -typedef struct { - uint8_t slave_id; - uint16_t max_current; - int64_t timestamp_us; -} loadbalancer_master_limit_event_t; - -typedef struct { - uint8_t slave_id; - uint16_t max_current; - int64_t timestamp_us; -} loadbalancer_slave_limit_event_t; - -typedef struct { - uint8_t slave_id; // ID do slave que reportou - bool charging; // Status de carregamento - float hw_max_current; // Limite máximo de corrente do hardware informado - float runtime_current; // Corrente atual de carregamento (A) - int64_t timestamp_us; // Momento em que o status foi coletado -} loadbalancer_slave_status_event_t; -// === Fim de: components/loadbalancer/include/loadbalancer_events.h === - - -// === Início de: components/loadbalancer/include/loadbalancer.h === -#ifndef LOADBALANCER_H_ -#define LOADBALANCER_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include "esp_err.h" - - -/** - * @brief Inicializa o módulo de load balancer - */ -void loadbalancer_init(void); - -/** - * @brief Task contínua do algoritmo de balanceamento - */ -void loadbalancer_task(void *param); - -/** - * @brief Ativa ou desativa o load balancing - */ -void loadbalancer_set_enabled(bool value); - -/** - * @brief Verifica se o load balancing está ativo - */ -bool loadbalancer_is_enabled(void); - -/** - * @brief Define a corrente máxima do grid - */ -esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current); - -/** - * @brief Obtém a corrente máxima do grid - */ -uint8_t load_balancing_get_max_grid_current(void); - -#ifdef __cplusplus -} -#endif - -#endif /* LOADBALANCER_H_ */ - -// === Fim de: components/loadbalancer/include/loadbalancer.h === - - -// === Início de: components/loadbalancer/include/input_filter.h === -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct { - float alpha; ///< Fator de suavização (0.0 a 1.0) - float value; ///< Último valor filtrado - int initialized; ///< Flag de inicialização -} input_filter_t; - -/** - * @brief Inicializa o filtro com o fator alpha desejado. - * @param filter Ponteiro para a estrutura do filtro - * @param alpha Valor entre 0.0 (mais lento) e 1.0 (sem filtro) - */ -void input_filter_init(input_filter_t *filter, float alpha); - -/** - * @brief Atualiza o valor filtrado com uma nova entrada. - * @param filter Ponteiro para o filtro - * @param input Valor bruto - * @return Valor suavizado - */ -float input_filter_update(input_filter_t *filter, float input); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/loadbalancer/include/input_filter.h === - - -// === Início de: components/auth/include/auth.h === -#pragma once -#include -#include "auth_types.h" // enum + MAX LEN para API - -#ifdef __cplusplus -extern "C" { -#endif - -/* Evento auxiliar legado/útil (resultado local) */ -typedef struct { - char tag[AUTH_TAG_MAX_LEN]; - bool authorized; -} auth_event_t; - -void auth_init(void); -void auth_set_mode(auth_mode_t mode); -auth_mode_t auth_get_mode(void); - -bool auth_add_tag(const char *tag); -bool auth_remove_tag(const char *tag); -bool auth_tag_exists(const char *tag); -void auth_list_tags(void); - -void auth_process_tag(const char *tag); -void auth_wait_for_tag_registration(void); - -int auth_get_tag_count(void); -const char *auth_get_tag_by_index(int index); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/auth/include/auth.h === - - -// === Início de: components/auth/include/auth_events.h === -#pragma once -#include "esp_event.h" -#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 = 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_TAG_MAX_LEN]; - bool authorized; -} auth_tag_event_data_t; - -typedef struct { - 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; - -// === Fim de: components/auth/include/auth_events.h === - - -// === Início de: components/auth/include/wiegand.h === -/* - * Copyright (c) 2021 Ruslan V. Uss - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of itscontributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * @file wiegand.h - * @defgroup wiegand wiegand - * @{ - * - * ESP-IDF Wiegand protocol receiver - * - * Copyright (c) 2021 Ruslan V. Uss - * - * BSD Licensed as described in the file LICENSE - */ -#ifndef __WIEGAND_H__ -#define __WIEGAND_H__ - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct wiegand_reader wiegand_reader_t; - -typedef void (*wiegand_callback_t)(wiegand_reader_t *reader); - -/** - * Bit and byte order of data - */ -typedef enum { - WIEGAND_MSB_FIRST = 0, - WIEGAND_LSB_FIRST -} wiegand_order_t; - -/** - * Wiegand reader descriptor - */ -struct wiegand_reader -{ - gpio_num_t gpio_d0, gpio_d1; - wiegand_callback_t callback; - wiegand_order_t bit_order; - wiegand_order_t byte_order; - - uint8_t *buf; - size_t size; - size_t bits; - esp_timer_handle_t timer; - bool start_parity; - bool enabled; -}; - -/** - * @brief Create and initialize reader instance. - * - * @param reader Reader descriptor - * @param gpio_d0 GPIO pin for D0 - * @param gpio_d1 GPIO pin for D0 - * @param internal_pullups Enable internal pull-up resistors for D0 and D1 GPIO - * @param buf_size Reader buffer size in bytes, must be large enough to - * contain entire Wiegand key - * @param callback Callback function for processing received codes - * @param bit_order Bit order of data - * @param byte_order Byte order of data - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_init(wiegand_reader_t *reader, gpio_num_t gpio_d0, gpio_num_t gpio_d1, - bool internal_pullups, size_t buf_size, wiegand_callback_t callback, wiegand_order_t bit_order, - wiegand_order_t byte_order); - -/** - * @brief Disable reader - * - * While reader is disabled, it will not receive new data - * - * @param reader Reader descriptor - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_disable(wiegand_reader_t *reader); - -/** - * @brief Enable reader - * - * @param reader Reader descriptor - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_enable(wiegand_reader_t *reader); - -/** - * @brief Delete reader instance. - * - * @param reader Reader descriptor - * @return `ESP_OK` on success - */ -esp_err_t wiegand_reader_done(wiegand_reader_t *reader); - -#ifdef __cplusplus -} -#endif - -/**@}*/ - -#endif /* __WIEGAND_H__ */ - -// === Fim de: components/auth/include/wiegand.h === - - -// === Início de: components/auth/include/auth_types.h === -#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 - -// === Fim de: components/auth/include/auth_types.h === - - -// === Início de: components/auth/include/wiegand_reader.h === -#ifndef WIEGAND_READER_H -#define WIEGAND_READER_H - -#ifdef __cplusplus -extern "C" { -#endif - -void initWiegand(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIEGAND_READER_H - -// === Fim de: components/auth/include/wiegand_reader.h === - - -// === 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 ampères (ex: 16 = 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" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Reinicia a máquina de estados do EVSE para o estado inicial (A). - */ -void evse_fsm_reset(void); - -/** - * @brief Processa uma leitura do sinal de piloto e atualiza a máquina de estados do EVSE. - * - * Esta função deve ser chamada periodicamente pelo núcleo de controle para - * avaliar mudanças no estado do conector, disponibilidade do carregador e - * autorização do usuário. - * - * @param pilot_voltage Leitura atual da tensão do sinal piloto. - * @param authorized Indica se o carregamento foi autorizado. - * @param available Indica se o carregador está disponível (ex: sem falhas). - * @param enabled Indica se o carregador está habilitado via software. - */ -void evse_fsm_process(pilot_voltage_t pilot_voltage, bool authorized, bool available, bool enabled); - -#ifdef __cplusplus -} -#endif - -#endif // EVSE_FSM_H - -// === Fim de: components/evse/include/evse_fsm.h === - - -// === Início de: components/evse/include/evse_hardware.h === -#ifndef EVSE_HARDWARE_H -#define EVSE_HARDWARE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -/** - * @brief Inicializa todos os periféricos de hardware do EVSE (pilot, relé, trava, etc.) - */ -void evse_hardware_init(void); - -/** - * @brief Executa atualizações periódicas no hardware (tick) - */ -void evse_hardware_tick(void); - -/** - * @brief Verifica se o sinal piloto está em nível alto (12V) - */ -bool evse_hardware_is_pilot_high(void); - -/** - * @brief Verifica se o veículo está fisicamente conectado via Proximity - */ -bool evse_hardware_is_vehicle_connected(void); - -/** - * @brief Verifica se há consumo de energia (corrente detectada) - */ -bool evse_hardware_is_energy_detected(void); - -/** - * @brief Liga o relé de fornecimento de energia - */ -void evse_hardware_relay_on(void); - -/** - * @brief Desliga o relé de fornecimento de energia - */ -void evse_hardware_relay_off(void); - -/** - * @brief Consulta o estado atual do relé - * @return true se ligado, false se desligado - */ -bool evse_hardware_relay_status(void); - -/** - * @brief Aciona a trava física do conector - */ -void evse_hardware_lock(void); - -/** - * @brief Libera a trava física do conector - */ -void evse_hardware_unlock(void); - -/** - * @brief Verifica se o conector está travado - */ -bool evse_hardware_is_locked(void); - -#ifdef __cplusplus -} -#endif - -#endif // EVSE_HARDWARE_H - -// === Fim de: components/evse/include/evse_hardware.h === - - -// === Início de: components/evse/include/evse_config.h === -#ifndef EVSE_CONFIG_H -#define EVSE_CONFIG_H - -#include -#include -#include "esp_err.h" -#include "freertos/FreeRTOS.h" -#include "evse_events.h" - -#ifdef __cplusplus -extern "C" { -#endif - -// ======================== -// Limites Globais (Defines) -// ======================== - -// Corrente máxima de carregamento (configurável pelo usuário) -#define MIN_CHARGING_CURRENT_LIMIT 6 // A -#define MAX_CHARGING_CURRENT_LIMIT 32 // A - -// Corrente via cabo (proximity) — se configurável -#define MIN_CABLE_CURRENT_LIMIT 6 // A -#define MAX_CABLE_CURRENT_LIMIT 63 // A - -// ======================== -// Funções de Configuração -// ======================== - -// Inicialização -esp_err_t evse_config_init(void); -void evse_check_defaults(void); - -// Corrente de carregamento -uint8_t evse_get_max_charging_current(void); -esp_err_t evse_set_max_charging_current(uint8_t value); - -uint16_t evse_get_charging_current(void); -esp_err_t evse_set_charging_current(uint16_t value); - -uint16_t evse_get_default_charging_current(void); -esp_err_t evse_set_default_charging_current(uint16_t value); - -// Configuração de socket outlet -bool evse_get_socket_outlet(void); -esp_err_t evse_set_socket_outlet(bool socket_outlet); - -void evse_set_runtime_charging_current(uint16_t value); -uint16_t evse_get_runtime_charging_current(void); - - -// RCM -bool evse_is_rcm(void); -esp_err_t evse_set_rcm(bool rcm); - -// Temperatura -uint8_t evse_get_temp_threshold(void); -esp_err_t evse_set_temp_threshold(uint8_t threshold); - -// Disponibilidade -bool evse_config_is_available(void); -void evse_config_set_available(bool available); - -// Ativação/desativação do EVSE -bool evse_config_is_enabled(void); -void evse_config_set_enabled(bool enabled); - -#ifdef __cplusplus -} -#endif - -#endif // EVSE_CONFIG_H - -// === Fim de: components/evse/include/evse_config.h === - - -// === Início de: components/evse/include/evse_state.h === -#ifndef EVSE_STATE_H -#define EVSE_STATE_H - -#include -#include "freertos/FreeRTOS.h" -#include "evse_events.h" - -#ifdef __cplusplus -extern "C" { -#endif - -// ============================ -// EVSE Pilot Signal States -// ============================ - -typedef enum { - EVSE_STATE_A, // EV Not Connected (12V) - EVSE_STATE_B1, // EV Connected (9V, Not Authorized) - EVSE_STATE_B2, // EV Connected (9V, Authorized and Ready) - EVSE_STATE_C1, // Charging Requested (6V, Relay Off) - EVSE_STATE_C2, // Charging Active (6V, Relay On) - EVSE_STATE_D1, // Ventilation Required (3V, Relay Off) - EVSE_STATE_D2, // Ventilation Active (3V, Relay On) - EVSE_STATE_E, // Error: Pilot Short to Ground (0V) - EVSE_STATE_F // Fault: No Pilot or EVSE Unavailable -} evse_state_t; - -// ============================ -// Initialization -// ============================ - -/** - * @brief Initializes the EVSE state machine and default state. - */ -void evse_state_init(void); - -/** - * @brief Periodic tick for state handling (optional hook). - */ -void evse_state_tick(void); - -// ============================ -// State Access & Control -// ============================ - -/** - * @brief Returns the current EVSE state. - */ -evse_state_t evse_get_state(void); - -/** - * @brief Sets the current EVSE state and emits a change event if needed. - */ -void evse_set_state(evse_state_t state); - -/** - * @brief Converts the state enum into a human-readable string. - */ -const char* evse_state_to_str(evse_state_t state); - -// ============================ -// State Evaluation Helpers -// ============================ - -/** - * @brief True if EV is in an active session (B2, C1, C2). - */ -bool evse_state_is_session(evse_state_t state); - -/** - * @brief True if EV is actively charging (C1, C2). - */ -bool evse_state_is_charging(evse_state_t state); - -/** - * @brief True if EV is physically plugged in (B1 and beyond). - */ -bool evse_state_is_plugged(evse_state_t state); - -// ============================ -// Authorization Control -// ============================ - -/** - * @brief Sets whether the EV is authorized to charge. - */ -void evse_state_set_authorized(bool authorized); - -/** - * @brief Gets whether the EV is currently authorized. - */ -bool evse_state_get_authorized(void); - -#ifdef __cplusplus -} -#endif - -#endif // EVSE_STATE_H - -// === Fim de: components/evse/include/evse_state.h === - - -// === Início de: components/evse/include/evse_error.h === -// === Início de: components/evse/include/evse_error.h === -#ifndef EVSE_ERROR_H -#define EVSE_ERROR_H - -#include -#include -#include "evse_pilot.h" - -// Bits que auto-limpam passado um timeout -#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); -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); - -// ---------------------------------------------------- -// Semântica sticky: flag "todos erros limpos" -// ---------------------------------------------------- -// Fica true quando TODOS os erros são limpos. -// Volta a false assim que qualquer erro novo aparece. -// Permanece true até o consumidor limpar explicitamente. -bool evse_error_cleared_flag(void); -void evse_error_reset_flag(void); - -#endif // EVSE_ERROR_H -// === Fim de: components/evse/include/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 === -// === 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. - */ -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 Convenience alias for evse_get_limit_reached(). - */ -bool evse_is_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). - */ -uint16_t evse_get_under_power_limit(void); -void evse_set_under_power_limit(uint16_t value); - -#ifdef __cplusplus -} -#endif - -#endif // EVSE_LIMITS_H -// === Fim de: components/evse/include/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 -#include -#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_ENABLE_UPDATED, - EVSE_EVENT_AVAILABLE_UPDATED, - EVSE_EVENT_SESSION, -} evse_event_id_t; - -// ----------------- -// Eventos de STATE -// ----------------- -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; - -// ----------------- -// Eventos de SESSÃO -// ----------------- -typedef enum { - EVSE_SESSION_EVENT_STARTED = 0, - EVSE_SESSION_EVENT_FINISHED, -} evse_session_event_type_t; - -typedef struct { - evse_session_event_type_t type; ///< STARTED / FINISHED - - // campos básicos da sessão, em tipos simples: - uint32_t session_id; ///< opcional, se tiveres um ID - uint32_t duration_s; ///< duração em segundos (0 no STARTED) - uint32_t energy_wh; ///< energia em Wh (0 no STARTED) - uint32_t avg_power_w; ///< potência média em W (0 no STARTED) - - bool is_current; ///< true se ainda estiver em curso -} evse_session_event_data_t; - - -// ----------------- -// Eventos de CONFIG -// ----------------- -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; - -// 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 - -// === 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); - -// =============================== -// Configuration / Availability -// =============================== - -/** - * @brief Enable or disable the EVSE (software flag, persisted in NVS). - */ -void evse_set_enabled(bool value); - -/** - * @brief Returns true if the EVSE is currently available for use. - */ -bool evse_is_available(void); - -/** - * @brief Set EVSE availability flag (may be persisted in NVS). - */ -void evse_set_available(bool value); - -// =============================== -// 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/scheduler/include/scheduler_types.h === -// components/scheduler/include/scheduler_types.h -#pragma once -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - SCHED_MODE_DISABLED = 0, // não faz gating - SCHED_MODE_SIMPLE, // por ex: janela diária única - SCHED_MODE_WEEKLY // por ex: janelas por dia da semana -} sched_mode_t; - -typedef struct { - bool enabled; // gating ativo? - sched_mode_t mode; - - // exemplo bem simples: uma janela diária [start_min..end_min[ - // minutos desde meia-noite, 0..1439 - uint16_t start_min; - uint16_t end_min; -} sched_config_t; - -const char *sched_mode_to_str(sched_mode_t mode); -bool sched_mode_from_str(const char *s, sched_mode_t *out); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/scheduler/include/scheduler_types.h === - - -// === Início de: components/scheduler/include/scheduler_events.h === -// scheduler_events.h -#pragma once -#include "esp_event.h" -#include - -ESP_EVENT_DECLARE_BASE(SCHED_EVENTS); +static const char *TAG = "storage_service"; typedef enum { - SCHED_EVENT_INIT = 0, // envia estado inicial - SCHED_EVENT_WINDOW_CHANGED, // allowed_now mudou -} sched_event_id_t; + OP_SET_U8, + OP_SET_U16, + OP_SET_U32, + OP_SET_STR, + OP_SET_BLOB, + + OP_ERASE_KEY, + + OP_GET_U8, + OP_GET_U16, + OP_GET_U32, + OP_GET_STR, + OP_GET_BLOB, + + OP_FLUSH, +} storage_op_t; + +typedef enum +{ + T_U8, + T_U16, + T_U32, + T_STR, + T_BLOB, +} storage_type_t; typedef struct { - bool allowed_now; -} sched_event_state_t; + esp_err_t err; + uint32_t value; // U8/U16/U32 + size_t len; // STR/BLOB: tamanho (STR sem '\0', BLOB tamanho real) + uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // payload para STR/BLOB (até MAX) +} storage_resp_t; -// === Fim de: components/scheduler/include/scheduler_events.h === +typedef struct +{ + storage_op_t op; + char ns[STORAGE_NS_MAX_LEN]; + char key[STORAGE_KEY_MAX_LEN]; + + // SET/GET numéricos + uint32_t value; + + // SET STR/BLOB: bytes e len (copiados na mensagem => seguro async) + uint16_t len; + uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; + + // Apenas para ajudar semântica do GET_BLOB (query vs read) + bool blob_query_only; + + QueueHandle_t resp_q; // opcional (GET/FLUSH sync) +} storage_msg_t; + +typedef struct +{ + bool used; + bool erase; + char ns[STORAGE_NS_MAX_LEN]; + char key[STORAGE_KEY_MAX_LEN]; + storage_type_t type; + + uint32_t value; // U8/U16/U32 + uint16_t len; // STR/BLOB (até MAX) + uint8_t bytes[STORAGE_MAX_VALUE_BYTES]; // STR/BLOB +} pending_item_t; + +static bool s_inited = false; + +// Queue principal estática (evita malloc) +static StaticQueue_t s_qbuf; +static uint8_t s_qstorage[STORAGE_QUEUE_LEN * sizeof(storage_msg_t)]; +static QueueHandle_t s_q = NULL; + +// Pending table (evita malloc) +static pending_item_t s_pending[STORAGE_MAX_PENDING]; +static size_t s_pending_count = 0; + +// debounce +static bool s_dirty = false; +static TickType_t s_commit_deadline = 0; + +// Sync: fila global + mutex global +static StaticQueue_t s_sync_qbuf; +static uint8_t s_sync_qstor[sizeof(storage_resp_t)]; +static QueueHandle_t s_sync_q = NULL; + +static StaticSemaphore_t s_sync_mtx_buf; +static SemaphoreHandle_t s_sync_mtx = NULL; + +static inline esp_err_t map_not_found(esp_err_t e) +{ + return (e == ESP_ERR_NVS_NOT_FOUND) ? ESP_ERR_NOT_FOUND : e; +} + +static bool safe_copy_str(char *dst, size_t dst_sz, const char *src) +{ + if (!dst || dst_sz == 0 || !src) + return false; + size_t n = strnlen(src, dst_sz); + if (n >= dst_sz) + return false; + memcpy(dst, src, n); + dst[n] = '\0'; + return true; +} + +static int find_pending(const char *ns, const char *key) +{ + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) == 0 && + strncmp(s_pending[i].key, key, STORAGE_KEY_MAX_LEN) == 0) + { + return i; + } + } + return -1; +} + +static bool pending_is_erased(const char *ns, const char *key) +{ + int idx = find_pending(ns, key); + if (idx < 0) + return false; + return s_pending[idx].used && s_pending[idx].erase; +} + +static int alloc_pending_slot(void) +{ + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + return i; + } + return -1; +} + +static void mark_dirty(void) +{ + s_dirty = true; + s_commit_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(STORAGE_COMMIT_DEBOUNCE_MS); +} + +static esp_err_t ensure_pending_entry(int *out_idx, const char *ns, const char *key) +{ + int idx = find_pending(ns, key); + if (idx < 0) + { + idx = alloc_pending_slot(); + if (idx < 0) + return ESP_ERR_NO_MEM; + + memset(&s_pending[idx], 0, sizeof(s_pending[idx])); + s_pending[idx].used = true; + + if (!safe_copy_str(s_pending[idx].ns, sizeof(s_pending[idx].ns), ns)) + { + s_pending[idx].used = false; + return ESP_ERR_INVALID_ARG; + } + if (!safe_copy_str(s_pending[idx].key, sizeof(s_pending[idx].key), key)) + { + s_pending[idx].used = false; + return ESP_ERR_INVALID_ARG; + } + s_pending_count++; + } + if (out_idx) + *out_idx = idx; + return ESP_OK; +} + +static esp_err_t pending_set_num(const char *ns, const char *key, storage_type_t type, uint32_t v) +{ + int idx = -1; + esp_err_t err = ensure_pending_entry(&idx, ns, key); + if (err != ESP_OK) + return err; + + s_pending[idx].erase = false; + s_pending[idx].type = type; + s_pending[idx].value = v; + s_pending[idx].len = 0; + + mark_dirty(); + return ESP_OK; +} + +static esp_err_t pending_set_bytes(const char *ns, const char *key, storage_type_t type, + const uint8_t *bytes, uint16_t len) +{ + if (!bytes && len > 0) + return ESP_ERR_INVALID_ARG; + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + + int idx = -1; + esp_err_t err = ensure_pending_entry(&idx, ns, key); + if (err != ESP_OK) + return err; + + s_pending[idx].erase = false; + s_pending[idx].type = type; + s_pending[idx].value = 0; + s_pending[idx].len = len; + + if (len > 0) + memcpy(s_pending[idx].bytes, bytes, len); + if (len < STORAGE_MAX_VALUE_BYTES) + memset(&s_pending[idx].bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); + + mark_dirty(); + return ESP_OK; +} + +static esp_err_t pending_erase(const char *ns, const char *key) +{ + int idx = -1; + esp_err_t err = ensure_pending_entry(&idx, ns, key); + if (err != ESP_OK) + return err; + + s_pending[idx].erase = true; + mark_dirty(); + return ESP_OK; +} + +static bool pending_get_num(const char *ns, const char *key, storage_type_t type, uint32_t *out) +{ + int idx = find_pending(ns, key); + if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type) + return false; + + if (out) + *out = s_pending[idx].value; + return true; +} + +static bool pending_get_bytes(const char *ns, const char *key, storage_type_t type, + uint8_t *out, size_t out_sz, uint16_t *out_len) +{ + int idx = find_pending(ns, key); + if (idx < 0 || !s_pending[idx].used || s_pending[idx].erase || s_pending[idx].type != type) + return false; + + uint16_t len = s_pending[idx].len; + + if (out_len) + *out_len = len; + + if (out) + { + size_t n = (len <= out_sz) ? (size_t)len : out_sz; + if (n > 0) + memcpy(out, s_pending[idx].bytes, n); + } + return true; +} + +// Commit: agrupa por namespace e faz 1 commit por namespace +static esp_err_t commit_all_pending(void) +{ + if (s_pending_count == 0) + { + s_dirty = false; + return ESP_OK; + } + + esp_err_t overall = ESP_OK; + + // Lista de namespaces únicos (sem heap) + char ns_list[STORAGE_MAX_PENDING][STORAGE_NS_MAX_LEN]; + int ns_count = 0; + + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + + bool seen = false; + for (int j = 0; j < ns_count; ++j) + { + if (strncmp(ns_list[j], s_pending[i].ns, STORAGE_NS_MAX_LEN) == 0) + { + seen = true; + break; + } + } + if (!seen && ns_count < (int)STORAGE_MAX_PENDING) + { + strncpy(ns_list[ns_count], s_pending[i].ns, STORAGE_NS_MAX_LEN - 1); + ns_list[ns_count][STORAGE_NS_MAX_LEN - 1] = '\0'; + ns_count++; + } + } + + for (int n = 0; n < ns_count; ++n) + { + const char *ns = ns_list[n]; + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READWRITE, &h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "nvs_open('%s') failed: %s", ns, esp_err_to_name(err)); + overall = err; + continue; + } + + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0) + continue; + + if (s_pending[i].erase) + { + err = nvs_erase_key(h, s_pending[i].key); + if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) + { + ESP_LOGE(TAG, "erase %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(err)); + overall = err; + } + continue; + } + + switch (s_pending[i].type) + { + case T_U8: + err = nvs_set_u8(h, s_pending[i].key, (uint8_t)(s_pending[i].value & 0xFF)); + break; + case T_U16: + err = nvs_set_u16(h, s_pending[i].key, (uint16_t)(s_pending[i].value & 0xFFFF)); + break; + case T_U32: + err = nvs_set_u32(h, s_pending[i].key, s_pending[i].value); + break; + + case T_STR: + { + // garantir null-termination para nvs_set_str + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + uint16_t len = s_pending[i].len; + if (len > STORAGE_MAX_VALUE_BYTES) + { + err = ESP_ERR_INVALID_SIZE; + break; + } + if (len > 0) + memcpy(tmp, s_pending[i].bytes, len); + tmp[len] = '\0'; + err = nvs_set_str(h, s_pending[i].key, tmp); + break; + } + + case T_BLOB: + { + uint16_t len = s_pending[i].len; + if (len > STORAGE_MAX_VALUE_BYTES) + { + err = ESP_ERR_INVALID_SIZE; + break; + } + err = nvs_set_blob(h, s_pending[i].key, s_pending[i].bytes, (size_t)len); + break; + } + + default: + err = ESP_ERR_INVALID_STATE; + break; + } + + if (err != ESP_OK) + { + ESP_LOGE(TAG, "set %s/%s failed: %s", ns, s_pending[i].key, esp_err_to_name(err)); + overall = err; + } + } + + err = nvs_commit(h); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "commit('%s') failed: %s", ns, esp_err_to_name(err)); + overall = err; + } + + nvs_close(h); + + if (err == ESP_OK) + { + for (int i = 0; i < (int)STORAGE_MAX_PENDING; ++i) + { + if (!s_pending[i].used) + continue; + if (strncmp(s_pending[i].ns, ns, STORAGE_NS_MAX_LEN) != 0) + continue; + s_pending[i].used = false; + if (s_pending_count > 0) + s_pending_count--; + } + } + } + + if (s_pending_count == 0) + s_dirty = false; + else + mark_dirty(); + + return overall; +} + +// -------- Read helpers: devolvem para buffers internos (cópia na resposta) -------- + +static esp_err_t nvs_read_num(storage_type_t type, const char *ns, const char *key, uint32_t *out) +{ + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint32_t pv = 0; + if (pending_get_num(ns, key, type, &pv)) + { + if (out) + *out = pv; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + switch (type) + { + case T_U8: + { + uint8_t v8 = 0; + err = nvs_get_u8(h, key, &v8); + if (err == ESP_OK && out) + *out = v8; + break; + } + case T_U16: + { + uint16_t v16 = 0; + err = nvs_get_u16(h, key, &v16); + if (err == ESP_OK && out) + *out = v16; + break; + } + case T_U32: + { + uint32_t v32 = 0; + err = nvs_get_u32(h, key, &v32); + if (err == ESP_OK && out) + *out = v32; + break; + } + default: + err = ESP_ERR_INVALID_ARG; + break; + } + + nvs_close(h); + return map_not_found(err); +} + +static esp_err_t nvs_read_str_to_resp(const char *ns, const char *key, storage_resp_t *resp) +{ + if (!resp) + return ESP_ERR_INVALID_ARG; + + resp->len = 0; + + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint16_t plen = 0; + if (pending_get_bytes(ns, key, T_STR, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen)) + { + resp->len = (size_t)plen; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + // 1) query required size (inclui '\0') + size_t req = 0; + err = nvs_get_str(h, key, NULL, &req); + if (err != ESP_OK) + { + nvs_close(h); + return map_not_found(err); + } + + if (req == 0) + { + nvs_close(h); + resp->len = 0; + return ESP_OK; + } + + size_t str_len = req - 1; // sem '\0' + resp->len = str_len; + + if (str_len > STORAGE_MAX_VALUE_BYTES) + { + nvs_close(h); + return ESP_ERR_NVS_INVALID_LENGTH; + } + + // 2) read + char tmp[STORAGE_MAX_VALUE_BYTES + 1]; + size_t tmp_sz = sizeof(tmp); + err = nvs_get_str(h, key, tmp, &tmp_sz); + nvs_close(h); + + if (err != ESP_OK) + return map_not_found(err); + + size_t n = strnlen(tmp, STORAGE_MAX_VALUE_BYTES); + memcpy(resp->bytes, tmp, n); + resp->len = n; + return ESP_OK; +} + +static esp_err_t nvs_read_blob_to_resp(const char *ns, const char *key, storage_resp_t *resp) +{ + if (!resp) + return ESP_ERR_INVALID_ARG; + + resp->len = 0; + + if (pending_is_erased(ns, key)) + return ESP_ERR_NOT_FOUND; + + uint16_t plen = 0; + if (pending_get_bytes(ns, key, T_BLOB, resp->bytes, STORAGE_MAX_VALUE_BYTES, &plen)) + { + resp->len = (size_t)plen; + return ESP_OK; + } + + nvs_handle_t h; + esp_err_t err = nvs_open(ns, NVS_READONLY, &h); + if (err != ESP_OK) + return map_not_found(err); + + // query size + size_t req = 0; + err = nvs_get_blob(h, key, NULL, &req); + if (err != ESP_OK) + { + nvs_close(h); + if (err == ESP_ERR_NVS_NOT_FOUND) + resp->len = 0; + return map_not_found(err); + } + + resp->len = req; + + // Se maior que o máximo, não lemos payload (caller pode só estar a fazer query) + if (req > STORAGE_MAX_VALUE_BYTES) + { + nvs_close(h); + return ESP_OK; // wrapper decide ESP_ERR_NVS_INVALID_LENGTH se tentar ler com buffer + } + + // read payload + size_t tmp = req; + err = nvs_get_blob(h, key, resp->bytes, &tmp); + nvs_close(h); + + if (err != ESP_OK) + return map_not_found(err); + + resp->len = tmp; + return ESP_OK; +} + +// -------- Task -------- + +static void storage_task(void *arg) +{ + (void)arg; + storage_msg_t msg; + + while (true) + { + TickType_t now = xTaskGetTickCount(); + + TickType_t wait = portMAX_DELAY; + if (s_dirty) + { + if (now >= s_commit_deadline) + { + (void)commit_all_pending(); + continue; + } + else + { + wait = s_commit_deadline - now; + } + } + + if (xQueueReceive(s_q, &msg, wait) != pdTRUE) + { + if (s_dirty && xTaskGetTickCount() >= s_commit_deadline) + { + (void)commit_all_pending(); + } + continue; + } + + storage_resp_t resp; + memset(&resp, 0, sizeof(resp)); + resp.err = ESP_OK; + + switch (msg.op) + { + case OP_SET_U8: + resp.err = pending_set_num(msg.ns, msg.key, T_U8, msg.value & 0xFF); + break; + case OP_SET_U16: + resp.err = pending_set_num(msg.ns, msg.key, T_U16, msg.value & 0xFFFF); + break; + case OP_SET_U32: + resp.err = pending_set_num(msg.ns, msg.key, T_U32, msg.value); + break; + + case OP_SET_STR: + resp.err = pending_set_bytes(msg.ns, msg.key, T_STR, msg.bytes, msg.len); + break; + + case OP_SET_BLOB: + resp.err = pending_set_bytes(msg.ns, msg.key, T_BLOB, msg.bytes, msg.len); + break; + + case OP_ERASE_KEY: + resp.err = pending_erase(msg.ns, msg.key); + break; + + case OP_GET_U8: + resp.err = nvs_read_num(T_U8, msg.ns, msg.key, &resp.value); + break; + case OP_GET_U16: + resp.err = nvs_read_num(T_U16, msg.ns, msg.key, &resp.value); + break; + case OP_GET_U32: + resp.err = nvs_read_num(T_U32, msg.ns, msg.key, &resp.value); + break; + + case OP_GET_STR: + resp.err = nvs_read_str_to_resp(msg.ns, msg.key, &resp); + break; + + case OP_GET_BLOB: + // sempre fazemos "query+maybe-read" para preencher resp.len (e bytes se couber) + resp.err = nvs_read_blob_to_resp(msg.ns, msg.key, &resp); + break; + + case OP_FLUSH: + resp.err = commit_all_pending(); + break; + + default: + resp.err = ESP_ERR_INVALID_ARG; + break; + } + + if (msg.resp_q) + { + (void)xQueueSend(msg.resp_q, &resp, 0); + } + } +} + +// -------- Public API -------- + +esp_err_t storage_service_init(void) +{ + if (s_inited) + return ESP_OK; + + s_q = xQueueCreateStatic(STORAGE_QUEUE_LEN, sizeof(storage_msg_t), s_qstorage, &s_qbuf); + if (!s_q) + return ESP_ERR_NO_MEM; + + s_sync_q = xQueueCreateStatic(1, sizeof(storage_resp_t), s_sync_qstor, &s_sync_qbuf); + if (!s_sync_q) + return ESP_ERR_NO_MEM; + + s_sync_mtx = xSemaphoreCreateMutexStatic(&s_sync_mtx_buf); + if (!s_sync_mtx) + return ESP_ERR_NO_MEM; + + memset(s_pending, 0, sizeof(s_pending)); + s_pending_count = 0; + s_dirty = false; + + BaseType_t ok = xTaskCreate(storage_task, "storage", 8192, NULL, 5, NULL); + if (ok != pdPASS) + return ESP_ERR_NO_MEM; + + s_inited = true; + ESP_LOGI(TAG, "storage_service init OK (queue=%d pending=%d debounce=%dms max_bytes=%d)", + STORAGE_QUEUE_LEN, STORAGE_MAX_PENDING, STORAGE_COMMIT_DEBOUNCE_MS, STORAGE_MAX_VALUE_BYTES); + return ESP_OK; +} + +static esp_err_t send_msg(const storage_msg_t *m, TickType_t to) +{ + if (!s_inited || !s_q) + return ESP_ERR_INVALID_STATE; + return (xQueueSend(s_q, m, to) == pdTRUE) ? ESP_OK : ESP_ERR_TIMEOUT; +} + +static esp_err_t set_async_num(storage_op_t op, const char *ns, const char *key, uint32_t v) +{ + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = op; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + m.value = v; + return send_msg(&m, 0); +} + +esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v) +{ + return set_async_num(OP_SET_U8, ns, key, v); +} + +esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v) +{ + return set_async_num(OP_SET_U16, ns, key, v); +} + +esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v) +{ + return set_async_num(OP_SET_U32, ns, key, v); +} + +esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str) +{ + if (!str) + return ESP_ERR_INVALID_ARG; + + // CORRIGIDO: deteta strings > MAX (sem truncar silenciosamente) + size_t len = strnlen(str, STORAGE_MAX_VALUE_BYTES + 1); + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_SET_STR; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + + m.len = (uint16_t)len; + if (len > 0) + memcpy(m.bytes, str, len); + if (len < STORAGE_MAX_VALUE_BYTES) + memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); + + return send_msg(&m, 0); +} + +esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len) +{ + if (len > STORAGE_MAX_VALUE_BYTES) + return ESP_ERR_INVALID_SIZE; + if (len > 0 && !data) + return ESP_ERR_INVALID_ARG; + + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_SET_BLOB; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + + m.len = (uint16_t)len; + if (len > 0) + memcpy(m.bytes, data, len); + if (len < STORAGE_MAX_VALUE_BYTES) + memset(&m.bytes[len], 0, STORAGE_MAX_VALUE_BYTES - len); + + return send_msg(&m, 0); +} + +esp_err_t storage_erase_key_async(const char *ns, const char *key) +{ + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_ERASE_KEY; + if (!safe_copy_str(m.ns, sizeof(m.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(m.key, sizeof(m.key), key)) + return ESP_ERR_INVALID_ARG; + return send_msg(&m, 0); +} + +esp_err_t storage_flush_async(void) +{ + storage_msg_t m; + memset(&m, 0, sizeof(m)); + m.op = OP_FLUSH; + return send_msg(&m, 0); +} + +static esp_err_t sync_req(const storage_msg_t *req, storage_resp_t *out_resp, TickType_t to) +{ + if (!s_inited || !s_sync_q || !s_sync_mtx) + return ESP_ERR_INVALID_STATE; + + if (xSemaphoreTake(s_sync_mtx, to) != pdTRUE) + return ESP_ERR_TIMEOUT; + + xQueueReset(s_sync_q); + + storage_msg_t m = *req; + m.resp_q = s_sync_q; + + esp_err_t err = send_msg(&m, to); + if (err != ESP_OK) + { + xSemaphoreGive(s_sync_mtx); + return err; + } + + storage_resp_t r; + memset(&r, 0, sizeof(r)); + + if (xQueueReceive(s_sync_q, &r, to) != pdTRUE) + { + xSemaphoreGive(s_sync_mtx); + return ESP_ERR_TIMEOUT; + } + + if (out_resp) + *out_resp = r; + + xSemaphoreGive(s_sync_mtx); + return r.err; +} + +esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_U8; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + if (err == ESP_OK && out) + *out = (uint8_t)(r.value & 0xFF); + return err; +} + +esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_U16; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + if (err == ESP_OK && out) + *out = (uint16_t)(r.value & 0xFFFF); + return err; +} + +esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_U32; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + if (err == ESP_OK && out) + *out = r.value; + return err; +} + +esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to) +{ + if (!out || out_sz == 0) + return ESP_ERR_INVALID_ARG; + + out[0] = '\0'; + + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_STR; + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + + if (err != ESP_OK) + { + out[out_sz - 1] = '\0'; + return err; + } + + // r.len é o tamanho real (sem '\0') + if (r.len >= out_sz) + { + // espelhar semântica tipo NVS (buffer pequeno) + out[0] = '\0'; + out[out_sz - 1] = '\0'; + return ESP_ERR_NVS_INVALID_LENGTH; + } + + if (r.len > 0) + memcpy(out, r.bytes, r.len); + out[r.len] = '\0'; + return ESP_OK; +} + +esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to) +{ + if (!inout_len) + return ESP_ERR_INVALID_ARG; + + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_GET_BLOB; + req.blob_query_only = (out == NULL); + + if (!safe_copy_str(req.ns, sizeof(req.ns), ns)) + return ESP_ERR_INVALID_ARG; + if (!safe_copy_str(req.key, sizeof(req.key), key)) + return ESP_ERR_INVALID_ARG; + + storage_resp_t r; + esp_err_t err = sync_req(&req, &r, to); + + if (err != ESP_OK) + { + if (out == NULL && err == ESP_ERR_NOT_FOUND) + *inout_len = 0; + return err; + } + + // query mode: só devolver tamanho requerido + if (out == NULL) + { + *inout_len = r.len; + return ESP_OK; + } + + // read mode: valida tamanhos + if (r.len > *inout_len) + { + *inout_len = r.len; + return ESP_ERR_NVS_INVALID_LENGTH; + } + if (r.len > STORAGE_MAX_VALUE_BYTES) + { + *inout_len = r.len; + return ESP_ERR_NVS_INVALID_LENGTH; + } + + if (r.len > 0) + memcpy(out, r.bytes, r.len); + *inout_len = r.len; + return ESP_OK; +} + +esp_err_t storage_flush_sync(TickType_t to) +{ + storage_msg_t req; + memset(&req, 0, sizeof(req)); + req.op = OP_FLUSH; + storage_resp_t r; + return sync_req(&req, &r, to); +} + +// === Fim de: components/storage_service/src/storage_service.c === -// === Início de: components/scheduler/include/scheduler.h === -// components/scheduler/include/scheduler.h +// === Início de: components/storage_service/include/storage_service.h === #pragma once -#include -#include "scheduler_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -void scheduler_init(void); -void scheduler_set_config(const sched_config_t *cfg); -sched_config_t scheduler_get_config(void); -bool scheduler_is_allowed_now(void); - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/scheduler/include/scheduler.h === - - -// === Início de: components/ocpp/include/ocpp_events.h === -#pragma once -#include "esp_event.h" -#include #include +#include -#ifdef __cplusplus -extern "C" { -#endif - -/* Base de eventos do OCPP (igual ao padrão usado em auth_events.h) */ -ESP_EVENT_DECLARE_BASE(OCPP_EVENTS); - -/* IDs de eventos do OCPP */ -typedef enum { - OCPP_EVENT_CONNECTED = 0, // payload: const char* (server URL) – opcional - OCPP_EVENT_DISCONNECTED, // payload: NULL - OCPP_EVENT_AUTHORIZED, // payload: ocpp_idtag_event_t (opcional) - OCPP_EVENT_AUTH_REJECTED, // payload: ocpp_idtag_event_t (opcional) - OCPP_EVENT_AUTH_TIMEOUT, // payload: NULL - OCPP_EVENT_REMOTE_START, // payload: ocpp_idtag_event_t (opcional) - OCPP_EVENT_REMOTE_STOP, // payload: NULL - OCPP_EVENT_START_TX, // payload: ocpp_tx_event_t (opcional) - OCPP_EVENT_STOP_TX, // payload: ocpp_reason_event_t (opcional) - OCPP_EVENT_RESET, // payload: NULL - OCPP_EVENT_OPERATIVE_UPDATED -} ocpp_event_id_t; - -/* Limites de strings simples (evita dependência de auth.h) */ -#define OCPP_IDTAG_MAX 32 -#define OCPP_REASON_MAX 32 - -/* Payloads opcionais */ -typedef struct { - char idTag[OCPP_IDTAG_MAX]; -} ocpp_idtag_event_t; - -typedef struct { - int tx_id; // se disponível -} ocpp_tx_event_t; - -typedef struct { - char reason[OCPP_REASON_MAX]; -} ocpp_reason_event_t; - -// Payload do novo evento -typedef struct { - bool operative; // true = Operative, false = Inoperative - int64_t timestamp_us; // esp_timer_get_time() -} ocpp_operative_event_t; - -#ifdef __cplusplus -} -#endif - -// === Fim de: components/ocpp/include/ocpp_events.h === - - -// === Início de: components/ocpp/include/ocpp.h === -#ifndef OCPP_H_ -#define OCPP_H_ - -#include -#include +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "nvs.h" // para ESP_ERR_NVS_INVALID_LENGTH (documentação/semântica do blob) #ifdef __cplusplus extern "C" { #endif /** - * @brief Start OCPP + * NVS limita namespace e key a 15 chars (+ '\0') + * (ver docs do NVS: NVS_KEY_NAME_MAX_SIZE / NVS_NS_NAME_MAX_SIZE) */ -void ocpp_start(void); +#define STORAGE_NS_MAX_LEN 16 +#define STORAGE_KEY_MAX_LEN 16 /** - * @brief Stop OCPP + * Ajusta conforme o teu sistema. + * Nota: setters async usam xQueueSend(..., 0) -> podem falhar com ESP_ERR_TIMEOUT se a fila estiver cheia. */ -void ocpp_stop(void); +#ifndef STORAGE_QUEUE_LEN +#define STORAGE_QUEUE_LEN 32 +#endif -/* Config getters / setters */ -bool ocpp_get_enabled(void); -void ocpp_set_enabled(bool value); +#ifndef STORAGE_MAX_PENDING +#define STORAGE_MAX_PENDING 48 +#endif -void ocpp_get_server(char *value); // buffer >= 64 -void ocpp_set_server(char *value); +#ifndef STORAGE_COMMIT_DEBOUNCE_MS +#define STORAGE_COMMIT_DEBOUNCE_MS 500 +#endif -void ocpp_get_charge_id(char *value); // buffer >= 64 -void ocpp_set_charge_id(char *value); +/** + * Tamanho máximo (bytes) para STR/BLOB guardado em pending/queue. + * IMPORTANTE: + * - Este limite aplica-se aos SETs (async) e ao cache pending. + * - Leituras (sync) a partir do NVS também ficam, na prática, limitadas por este design + * se esperares que valores maiores existam no NVS (recomenda-se alinhar o teu sistema a este máximo). + */ +#ifndef STORAGE_MAX_VALUE_BYTES +#define STORAGE_MAX_VALUE_BYTES 96 +#endif -/* Estado de conexão */ -bool ocpp_is_connected(void); +/** + * Inicializa o serviço e cria a task interna. + * + * Requisitos: + * - nvs_flash_init() deve ter sido chamado antes (normalmente no arranque da app). + */ +esp_err_t storage_service_init(void); + +// -------------------- Async setters (não bloqueiam; commit é debounced) -------------------- + +/** + * Retorna: + * - ESP_OK + * - ESP_ERR_INVALID_ARG (ns/key inválidos ou > 15 chars) + * - ESP_ERR_TIMEOUT (fila cheia) + * - ESP_ERR_INVALID_STATE (serviço não inicializado) + */ +esp_err_t storage_set_u8_async(const char *ns, const char *key, uint8_t v); +esp_err_t storage_set_u16_async(const char *ns, const char *key, uint16_t v); +esp_err_t storage_set_u32_async(const char *ns, const char *key, uint32_t v); + +/** + * Retorna: + * - ESP_OK + * - ESP_ERR_INVALID_ARG (ns/key/str inválidos) + * - ESP_ERR_INVALID_SIZE (str > STORAGE_MAX_VALUE_BYTES) + * - ESP_ERR_TIMEOUT (fila cheia) + * - ESP_ERR_INVALID_STATE (serviço não inicializado) + */ +esp_err_t storage_set_str_async(const char *ns, const char *key, const char *str); + +/** + * Retorna: + * - ESP_OK + * - ESP_ERR_INVALID_ARG (ns/key inválidos; data NULL com len>0) + * - ESP_ERR_INVALID_SIZE (len > STORAGE_MAX_VALUE_BYTES) + * - ESP_ERR_TIMEOUT (fila cheia) + * - ESP_ERR_INVALID_STATE (serviço não inicializado) + */ +esp_err_t storage_set_blob_async(const char *ns, const char *key, const void *data, size_t len); + +esp_err_t storage_erase_key_async(const char *ns, const char *key); + +/** Força commit imediato (async). Mesmas notas de fila cheia/invalid state. */ +esp_err_t storage_flush_async(void); + +// -------------------- Sync getters (bloqueiam até ler do NVS/pending) -------------------- +/** + * NOTAS IMPORTANTES: + * - Funções sync NÃO devem ser chamadas em ISR (usam mutex/queues e podem bloquear). + * - O timeout `to` aplica-se ao lock + envio/receção da resposta. + * + * Retorna tipicamente: + * - ESP_OK + * - ESP_ERR_NOT_FOUND (chave não existe) + * - ESP_ERR_TIMEOUT + * - outros erros do NVS + */ +esp_err_t storage_get_u8_sync(const char *ns, const char *key, uint8_t *out, TickType_t to); +esp_err_t storage_get_u16_sync(const char *ns, const char *key, uint16_t *out, TickType_t to); +esp_err_t storage_get_u32_sync(const char *ns, const char *key, uint32_t *out, TickType_t to); + +/** + * Lê string para `out` com tamanho `out_sz`. + * Em sucesso, `out` fica sempre null-terminated. + * + * Retorna: + * - ESP_OK + * - ESP_ERR_NOT_FOUND + * - ESP_ERR_INVALID_ARG (out/out_sz inválidos; ns/key inválidos) + * - ESP_ERR_TIMEOUT + * - erros do NVS + */ +esp_err_t storage_get_str_sync(const char *ns, const char *key, char *out, size_t out_sz, TickType_t to); + +/** + * Blob sync (semântica semelhante a nvs_get_blob): + * - Se out == NULL: devolve ESP_OK e coloca em *inout_len o tamanho requerido + * - Se out != NULL e *inout_len < requerido: devolve ESP_ERR_NVS_INVALID_LENGTH e atualiza *inout_len com requerido + * - Se OK: copia e atualiza *inout_len com o tamanho real + * + * Retorna: + * - ESP_OK + * - ESP_ERR_NOT_FOUND + * - ESP_ERR_NVS_INVALID_LENGTH + * - ESP_ERR_INVALID_ARG (inout_len NULL; ns/key inválidos) + * - ESP_ERR_TIMEOUT + * - erros do NVS + */ +esp_err_t storage_get_blob_sync(const char *ns, const char *key, void *out, size_t *inout_len, TickType_t to); + +/** Força commit imediato (sync). Mesmas notas de timeout/ISR. */ +esp_err_t storage_flush_sync(TickType_t to); #ifdef __cplusplus } #endif -#endif /* OCPP_H_ */ - -// === Fim de: components/ocpp/include/ocpp.h === +// === Fim de: components/storage_service/include/storage_service.h === diff --git a/readproject.py b/readproject.py index 49f39c3..0fbbe12 100644 --- a/readproject.py +++ b/readproject.py @@ -1,8 +1,8 @@ import os -TAMANHO_MAX = 100000 # Limite por arquivo +TAMANHO_MAX = 200000 # Limite por arquivo -def coletar_arquivos(diretorios, extensoes=(".h")): +def coletar_arquivos(diretorios, extensoes=(".c", ".h")): arquivos = [] for diretorio in diretorios: for raiz, pastas, nomes_arquivos in os.walk(diretorio): @@ -51,9 +51,9 @@ def unir_em_partes(arquivos, prefixo="projeto_parte", limite=TAMANHO_MAX): print(f"🔹 Arquivos gerados: {parte}") def main(): - diretorio_main = "main" + diretorio_main = "" #"main" componentes_escolhidos = [ - "meter_manager", "loadbalancer", "auth", "evse", "scheduler", "ocpp" + "storage_service" ] diretorios_componentes = [os.path.join("components", nome) for nome in componentes_escolhidos]