Compare commits

..

4 Commits

Author SHA1 Message Date
286028b6a8 fix evse_link 2026-01-24 16:56:51 +00:00
023644a887 new upgrade 2025-12-21 23:28:26 +00:00
82fa194bd8 v1.1 2025-12-10 12:59:55 +00:00
e6e2622a95 new module 2025-12-09 11:48:31 +00:00
186 changed files with 12621 additions and 13442 deletions

209
README.md
View File

@@ -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) - IEC-style EVSE state machine (Control Pilot A/B/C/D)
[![License](https://img.shields.io/github/license/dzurikmiroslav/esp32-evse.svg)](LICENSE.md) - 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. - EVSE manager (`evse_manager`) coordinating:
Source code ist not hardcoded to GPIOs or other hardware design features. - Hardware layer (`evse_hardware`)
All code is written in ESP-IDF without additional mapping layer like Arduino. - 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. ### Networking & REST
For example, on following scheme is minimal EVSE circuit with ESP32 devkit.
![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 ### Storage
#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
```
### 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.

View File

@@ -1,12 +1,12 @@
DEVICE_NAME=Custom EVSE DEVICE_NAME=ChargeFlow
#LEDs #LEDs
LED_CHARGING=n led_blue=n
LED_CHARGING_GPIO= led_blue_GPIO=
LED_ERROR=n led_red=n
LED_ERROR_GPIO= led_red_GPIO=
LED_STOP=n led_green=n
LED_STOP_GPIO= led_green_GPIO=
#Button #Button
BUTTON_WIFI_GPIO=32 BUTTON_WIFI_GPIO=32
@@ -34,33 +34,4 @@ AC_RELAY_GPIO=25
SOCKET_LOCK=n SOCKET_LOCK=n
SOCKET_LOCK_A_GPIO= SOCKET_LOCK_A_GPIO=
SOCKET_LOCK_B_GPIO= SOCKET_LOCK_B_GPIO=
SOCKET_LOCK_DETECTION_GPIO= SOCKET_LOCK_DETECTION_GPIO=
#Energy meter (none | cur | cur_vlt)
ENERGY_METER=none
ENERGY_METER_THREE_PHASES=n
#Energy meter internal (ENERGY_METER=cur | cur_vlt)
ENERGY_METER_L1_CUR_ADC_CHANNEL=
ENERGY_METER_L2_CUR_ADC_CHANNEL=
ENERGY_METER_L3_CUR_ADC_CHANNEL=
ENERGY_METER_CUR_SCALE=
#Energy meter internal (ENERGY_METER=cur_vlt)
ENERGY_METER_L1_VLT_ADC_CHANNEL=
ENERGY_METER_L2_VLT_ADC_CHANNEL=
ENERGY_METER_L3_VLT_ADC_CHANNEL=
ENERGY_METER_VLT_SCALE=
#Serial (SERIAL_X=none|uart|rs485)
SERIAL_1=none
SERIAL_1_NAME=UART 1
SERIAL_1_RXD_GPIO=
SERIAL_1_TXD_GPIO=
SERIAL_1_RTS_GPIO=
SERIAL_2=none
SERIAL_2_NAME=UART 2
SERIAL_2_RXD_GPIO=
SERIAL_2_TXD_GPIO=
SERIAL_2_RTS_GPIO=

View File

@@ -1,12 +1,12 @@
DEVICE_NAME=Plixin Evse DEVICE_NAME=ChargeFlow
#LEDs #LEDs
LED_CHARGING=y led_blue=y
LED_CHARGING_GPIO=14 led_blue_GPIO=14
LED_ERROR=y led_red=y
LED_ERROR_GPIO=26 led_red_GPIO=26
LED_STOP=y led_green=y
LED_STOP_GPIO=12 led_green_GPIO=12
#BUZZER #BUZZER
BUZZER=y BUZZER=y
@@ -41,85 +41,4 @@ AC_RELAY_GPIO=25
SOCKET_LOCK=n SOCKET_LOCK=n
SOCKET_LOCK_A_GPIO= SOCKET_LOCK_A_GPIO=
SOCKET_LOCK_B_GPIO= SOCKET_LOCK_B_GPIO=
SOCKET_LOCK_DETECTION_GPIO= SOCKET_LOCK_DETECTION_GPIO=
#Energy meter (none | cur | cur_vlt)
ENERGY_METER=none
ENERGY_METER_THREE_PHASES=n
#Energy meter internal (ENERGY_METER=cur | cur_vlt)
ENERGY_METER_L1_CUR_ADC_CHANNEL=7
ENERGY_METER_L2_CUR_ADC_CHANNEL=
ENERGY_METER_L3_CUR_ADC_CHANNEL=
ENERGY_METER_CUR_SCALE=0.090909091
#Energy meter internal (ENERGY_METER=cur_vlt)
ENERGY_METER_L1_VLT_ADC_CHANNEL=
ENERGY_METER_L2_VLT_ADC_CHANNEL=
ENERGY_METER_L3_VLT_ADC_CHANNEL=
ENERGY_METER_VLT_SCALE=0.47
#AUX
AUX_IN_1=n
AUX_IN_1_NAME=
AUX_IN_1_GPIO=
AUX_IN_2=n
AUX_IN_2_NAME=
AUX_IN_2_GPIO=
AUX_IN_3=n
AUX_IN_3_NAME=
AUX_IN_3_GPIO=
AUX_IN_4=n
AUX_IN_4_NAME=
AUX_IN_4_GPIO=
AUX_OUT_1=n
AUX_OUT_1_NAME=
AUX_OUT_1_GPIO=
AUX_OUT_2=n
AUX_OUT_2_NAME=
AUX_OUT_2_GPIO=
AUX_OUT_3=n
AUX_OUT_3_NAME=
AUX_OUT_3_GPIO=
AUX_OUT_4=n
AUX_OUT_4_NAME=
AUX_OUT_4_GPIO=
AUX_AIN_1=n
AUX_AIN_1_NAME=
AUX_AIN_1_ADC_CHANNEL=
AUX_AIN_2=n
AUX_AIN_2_NAME=
AUX_AIN_2_ADC_CHANNEL=
#Serial (SERIAL_X=none|uart|rs485)
SERIAL_1=none
SERIAL_1_NAME=UART via USB
SERIAL_1_RXD_GPIO=
SERIAL_1_TXD_GPIO=
SERIAL_1_RTS_GPIO=
SERIAL_2=none
SERIAL_2_NAME=RS485
SERIAL_2_RXD_GPIO=
SERIAL_2_TXD_GPIO=
SERIAL_2_RTS_GPIO=
SERIAL_3=none
SERIAL_3_NAME=UART
SERIAL_3_RXD_GPIO=
SERIAL_3_TXD_GPIO=
SERIAL_3_RTS_GPIO=
#Onewire devices
ONEWIRE=n
ONEWIRE_GPIO=
ONEWIRE_TEMP_SENSOR=n

View File

@@ -1,12 +1,12 @@
DEVICE_NAME=ESP32-S2-DA EVSE DEVICE_NAME=ChargeFlow
#LEDs #LEDs
LED_CHARGING=y led_blue=y
LED_CHARGING_GPIO=36 led_blue_GPIO=36
LED_ERROR=y led_red=y
LED_ERROR_GPIO=37 led_red_GPIO=37
LED_STOP=y led_green=y
LED_STOP_GPIO=35 led_green_GPIO=35
#Button #Button
BUTTON_WIFI_GPIO=32 BUTTON_WIFI_GPIO=32
@@ -42,78 +42,3 @@ SOCKET_LOCK_MIN_BREAK_TIME=1000
RCM=n RCM=n
RCM_GPIO=41 RCM_GPIO=41
RCM_TEST_GPIO=26 RCM_TEST_GPIO=26
#Energy meter (none | cur | cur_vlt)
ENERGY_METER=cur_vlt
ENERGY_METER_THREE_PHASES=y
#Energy meter internal (ENERGY_METER=cur | cur_vlt)
ENERGY_METER_L1_CUR_ADC_CHANNEL=4
ENERGY_METER_L2_CUR_ADC_CHANNEL=5
ENERGY_METER_L3_CUR_ADC_CHANNEL=6
ENERGY_METER_CUR_SCALE=0.090909091
#Energy meter internal (ENERGY_METER=cur_vlt)
ENERGY_METER_L1_VLT_ADC_CHANNEL=7
ENERGY_METER_L2_VLT_ADC_CHANNEL=8
ENERGY_METER_L3_VLT_ADC_CHANNEL=9
ENERGY_METER_VLT_SCALE=0.47
#AUX
AUX_IN_1=n
AUX_IN_1_NAME=
AUX_IN_1_GPIO=
AUX_IN_2=n
AUX_IN_2_NAME=
AUX_IN_2_GPIO=
AUX_IN_3=n
AUX_IN_3_NAME=
AUX_IN_3_GPIO=
AUX_IN_4=n
AUX_IN_4_NAME=
AUX_IN_4_GPIO=
AUX_OUT_1=n
AUX_OUT_1_NAME=
AUX_OUT_1_GPIO=
AUX_OUT_2=n
AUX_OUT_2_NAME=
AUX_OUT_2_GPIO=
AUX_OUT_3=n
AUX_OUT_3_NAME=
AUX_OUT_3_GPIO=
AUX_OUT_4=n
AUX_OUT_4_NAME=
AUX_OUT_4_GPIO=
AUX_AIN_1=n
AUX_AIN_1_NAME=
AUX_AIN_1_ADC_CHANNEL=
AUX_AIN_2=n
AUX_AIN_2_NAME=
AUX_AIN_2_ADC_CHANNEL=
#Serial (SERIAL_X=none|uart|rs485)
SERIAL_1=none
SERIAL_1_NAME=UART
SERIAL_1_RXD_GPIO=
SERIAL_1_TXD_GPIO=
SERIAL_1_RTS_GPIO=
SERIAL_2=none
SERIAL_2_NAME=RS-485
SERIAL_2_RXD_GPIO=
SERIAL_2_TXD_GPIO=
SERIAL_2_RTS_GPIO=
#Onewire devices
ONEWIRE=n
ONEWIRE_GPIO=
ONEWIRE_TEMP_SENSOR=n

View File

@@ -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}" idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src" PRIV_INCLUDE_DIRS "src"
PRIV_REQUIRES nvs_flash driver esp_timer PRIV_REQUIRES driver esp_timer
REQUIRES esp_event esp_idf_lib_helpers evse ocpp) REQUIRES esp_event evse ocpp evse_link storage_service)

View File

@@ -1,22 +1,25 @@
// components/auth/src/auth.c
#include "auth.h" #include "auth.h"
#include "auth_events.h" #include "auth_events.h"
#include "esp_event.h" #include "esp_event.h"
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/queue.h> #include <freertos/queue.h>
#include <freertos/task.h>
#include <esp_log.h> #include <esp_log.h>
#include <esp_err.h>
#include <string.h> #include <string.h>
#include <strings.h> // <-- necessário para strcasecmp #include <strings.h>
#include <stdio.h>
#include "wiegand_reader.h" #include "wiegand_reader.h"
#include "nvs_flash.h"
#include "nvs.h" #include "storage_service.h"
#include "esp_random.h"
#include "evse_link.h"
#include "evse_link_events.h"
#define MAX_TAGS 50 #define MAX_TAGS 50
static const char *TAG = "Auth"; static const char *TAG = "Auth";
/* ===== Estado ===== */ /* ===== Estado ===== */
@@ -25,260 +28,422 @@ static bool waiting_for_registration = false;
static char valid_tags[MAX_TAGS][AUTH_TAG_MAX_LEN]; static char valid_tags[MAX_TAGS][AUTH_TAG_MAX_LEN];
static int tag_count = 0; static int tag_count = 0;
static uint32_t s_next_req_id = 1; static uint32_t s_next_req_id = 1;
static bool s_wiegand_started = false;
/* ===== NVS keys ===== */ /* ===== Storage keys ===== */
#define NVS_NAMESPACE "auth" #define NVS_NAMESPACE "auth"
#define NVS_TAG_PREFIX "tag_" #define NVS_TAG_PREFIX "tag_"
#define NVS_TAG_COUNT_KEY "count" #define NVS_TAG_COUNT_KEY "count"
#define NVS_MODE_KEY "mode" // uint8_t #define NVS_MODE_KEY "mode" // uint8_t
/* ========================= // timeout para operações sync do storage
* NVS Persistence (tags) #define STORAGE_TO pdMS_TO_TICKS(2000)
* ========================= */
static void load_tags_from_nvs(void) {
nvs_handle_t handle;
if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) {
ESP_LOGW(TAG, "No stored tags in NVS");
return;
}
uint8_t count = 0;
if (nvs_get_u8(handle, NVS_TAG_COUNT_KEY, &count) != ESP_OK) {
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);
if (nvs_get_str(handle, key, tag_buf, &len) == 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++;
}
}
nvs_close(handle);
ESP_LOGI(TAG, "Loaded %d tags from NVS", tag_count);
}
static void save_tags_to_nvs(void) {
nvs_handle_t handle;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS to save tags");
return;
}
nvs_set_u8(handle, NVS_TAG_COUNT_KEY, tag_count);
for (int i = 0; i < tag_count; i++) {
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
nvs_set_str(handle, key, valid_tags[i]);
}
nvs_commit(handle);
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;
if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &h) == ESP_OK) {
uint8_t u = (uint8_t)AUTH_MODE_OPEN;
if (nvs_get_u8(h, NVS_MODE_KEY, &u) == ESP_OK) {
if (u <= (uint8_t)AUTH_MODE_OCPP_RFID) s_mode = (auth_mode_t)u;
}
nvs_close(h);
} else {
ESP_LOGW(TAG, "No stored auth mode in NVS; default OPEN");
}
ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode));
}
static void save_mode_to_nvs(auth_mode_t mode) {
nvs_handle_t h;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, NVS_MODE_KEY, (uint8_t)mode);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode));
} else {
ESP_LOGE(TAG, "Failed to save auth mode to NVS");
}
}
/* ========================= /* =========================
* Helpers * Helpers
* ========================= */ * ========================= */
static bool is_tag_valid(const char *tag) { 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) { for (int i = 0; i < tag_count; i++)
{
if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0)
return true; return true;
}
} }
return false; 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;
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_LOGD(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag);
esp_err_t err = esp_event_post(
AUTH_EVENTS,
AUTH_EVENT_TAG_PROCESSED,
&ev,
sizeof(ev),
portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to post AUTH_EVENT_TAG_PROCESSED (remote grant): %s",
esp_err_to_name(err));
}
}
/* ========================= /* =========================
* Public API * Public API
* ========================= */ * ========================= */
void auth_init(void) { 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());
if (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID) { 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)
{
initWiegand(); initWiegand();
s_wiegand_started = true;
ESP_LOGI(TAG, "Wiegand reader initialized (mode=%s)", auth_mode_to_str(s_mode)); ESP_LOGI(TAG, "Wiegand reader initialized (mode=%s)", auth_mode_to_str(s_mode));
} else { }
else
{
ESP_LOGI(TAG, "Mode OPEN: Wiegand not started"); ESP_LOGI(TAG, "Mode OPEN: Wiegand not started");
} }
auth_mode_event_data_t evt = { .mode = s_mode }; // bridge EVSE-Link -> AUTH
{
esp_err_t err = esp_event_handler_register(
EVSE_LINK_EVENTS,
LINK_EVENT_REMOTE_AUTH_GRANTED,
on_remote_auth_grant,
NULL);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to register EVSE-Link auth grant handler: %s",
esp_err_to_name(err));
}
}
auth_mode_event_data_t evt = {.mode = s_mode};
esp_event_post(AUTH_EVENTS, AUTH_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY); esp_event_post(AUTH_EVENTS, AUTH_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY);
ESP_LOGI(TAG, "AUTH INIT sent (mode=%s)", auth_mode_to_str(s_mode)); ESP_LOGI(TAG, "AUTH INIT sent (mode=%s)", auth_mode_to_str(s_mode));
} }
void auth_set_mode(auth_mode_t mode) { void auth_set_mode(auth_mode_t mode)
if (mode < AUTH_MODE_OPEN || mode > AUTH_MODE_OCPP_RFID) { {
if (mode < AUTH_MODE_OPEN || mode > AUTH_MODE_OCPP_RFID)
{
ESP_LOGW(TAG, "Invalid mode: %d", (int)mode); ESP_LOGW(TAG, "Invalid mode: %d", (int)mode);
return; return;
} }
if (mode == s_mode) {
ESP_LOGI(TAG, "Mode unchanged: %s", auth_mode_to_str(mode)); if (mode == s_mode)
{
ESP_LOGD(TAG, "Mode unchanged: %s", auth_mode_to_str(mode));
return; return;
} }
auth_mode_t old = s_mode;
s_mode = mode; s_mode = mode;
save_mode_to_nvs(mode); save_mode_to_storage(mode);
// Nota: se precisares, aqui podes parar/iniciar o Wiegand consoante o modo. bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID);
if (s_mode == AUTH_MODE_OPEN) {
ESP_LOGI(TAG, "Mode set to OPEN"); if (need_wiegand && !s_wiegand_started)
} else { {
ESP_LOGI(TAG, "Mode set to %s; ensure Wiegand reader is running", auth_mode_to_str(s_mode)); 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)
{
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_LOGD(TAG, "Mode changed %s -> %s, no change in Wiegand state",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
} }
auth_mode_event_data_t evt = { .mode = 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); esp_event_post(AUTH_EVENTS, AUTH_EVENT_MODE_CHANGED, &evt, sizeof(evt), portMAX_DELAY);
} }
auth_mode_t auth_get_mode(void) { auth_mode_t auth_get_mode(void)
{
return s_mode; return s_mode;
} }
bool auth_add_tag(const char *tag) { bool auth_add_tag(const char *tag)
if (tag_count >= MAX_TAGS) return false; {
if (!tag || strlen(tag) >= AUTH_TAG_MAX_LEN) return false; if (!tag || strlen(tag) >= AUTH_TAG_MAX_LEN)
if (is_tag_valid(tag)) return true; // já existe return false;
if (tag_count >= MAX_TAGS)
return false;
if (is_tag_valid(tag))
return true;
strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1); strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0'; valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++; tag_count++;
save_tags_to_nvs(); save_tags_to_storage();
ESP_LOGI(TAG, "Tag added: %s", tag); ESP_LOGD(TAG, "Tag added: %s", tag);
return true; return true;
} }
bool auth_remove_tag(const char *tag) { bool auth_remove_tag(const char *tag)
for (int i = 0; i < tag_count; i++) { {
if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0) { if (!tag)
for (int j = i; j < tag_count - 1; j++) { return false;
for (int i = 0; i < tag_count; i++)
{
if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0)
{
for (int j = i; j < tag_count - 1; j++)
{
strncpy(valid_tags[j], valid_tags[j + 1], AUTH_TAG_MAX_LEN); strncpy(valid_tags[j], valid_tags[j + 1], AUTH_TAG_MAX_LEN);
valid_tags[j][AUTH_TAG_MAX_LEN - 1] = '\0';
} }
tag_count--; tag_count--;
save_tags_to_nvs(); save_tags_to_storage();
ESP_LOGI(TAG, "Tag removed: %s", tag); ESP_LOGD(TAG, "Tag removed: %s", tag);
return true; return true;
} }
} }
return false; return false;
} }
bool auth_tag_exists(const char *tag) { bool auth_tag_exists(const char *tag)
{
if (!tag)
return false;
return is_tag_valid(tag); return is_tag_valid(tag);
} }
void auth_list_tags(void) { void auth_list_tags(void)
ESP_LOGI(TAG, "Registered Tags (%d):", tag_count); {
for (int i = 0; i < tag_count; i++) { ESP_LOGD(TAG, "Registered Tags (%d):", tag_count);
ESP_LOGI(TAG, "- %s", valid_tags[i]); for (int i = 0; i < tag_count; i++)
} ESP_LOGD(TAG, "- %s", valid_tags[i]);
} }
void auth_wait_for_tag_registration(void) { void auth_wait_for_tag_registration(void)
if (s_mode != AUTH_MODE_LOCAL_RFID) { {
if (s_mode != AUTH_MODE_LOCAL_RFID)
{
ESP_LOGW(TAG, "Registration is only available in LOCAL mode"); ESP_LOGW(TAG, "Registration is only available in LOCAL mode");
return; return;
} }
waiting_for_registration = true; 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) { void auth_process_tag(const char *tag)
if (!tag || !*tag) { {
if (!tag || !*tag)
{
ESP_LOGW(TAG, "NULL/empty tag received"); ESP_LOGW(TAG, "NULL/empty tag received");
return; return;
} }
switch (s_mode) { switch (s_mode)
case AUTH_MODE_OPEN: { {
// Sem verificação; normalmente nem é necessário evento. case AUTH_MODE_OPEN:
ESP_LOGI(TAG, "Mode OPEN: tag=%s (no verification)", tag); ESP_LOGD(TAG, "Mode OPEN: tag=%s (no verification)", tag);
break; break;
}
case AUTH_MODE_LOCAL_RFID: { case AUTH_MODE_LOCAL_RFID:
if (waiting_for_registration) { {
waiting_for_registration = false; if (waiting_for_registration)
{
waiting_for_registration = false;
if (auth_add_tag(tag)) { if (auth_add_tag(tag))
auth_tag_event_data_t ev = {0}; {
strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1); auth_tag_event_data_t ev = {0};
ev.authorized = true; strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1);
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, &ev, sizeof(ev), portMAX_DELAY); ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ESP_LOGI(TAG, "Tag registered: %s", tag); ev.authorized = true;
} else {
ESP_LOGW(TAG, "Failed to register tag: %s", tag); esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED,
} &ev, sizeof(ev), portMAX_DELAY);
return; ESP_LOGD(TAG, "Tag registered: %s", tag);
} }
else
auth_tag_event_data_t ev = {0}; {
strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1); ESP_LOGW(TAG, "Failed to register tag: %s", tag);
ev.authorized = is_tag_valid(tag); }
return;
ESP_LOGI(TAG, "LOCAL tag %s: %s", tag, ev.authorized ? "AUTHORIZED" : "DENIED");
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, &ev, sizeof(ev), portMAX_DELAY);
break;
} }
case AUTH_MODE_OCPP_RFID: { auth_tag_event_data_t ev = {0};
// Não decide localmente. Pede validação ao OCPP. strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1);
auth_tag_verify_event_t rq = {0}; ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
strncpy(rq.tag, tag, AUTH_TAG_MAX_LEN - 1); ev.authorized = is_tag_valid(tag);
rq.req_id = s_next_req_id++;
ESP_LOGI(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)", rq.tag, (unsigned)rq.req_id); ESP_LOGD(TAG, "LOCAL tag %s: %s", tag,
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY, &rq, sizeof(rq), portMAX_DELAY); ev.authorized ? "AUTHORIZED" : "DENIED");
break;
} esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED,
&ev, sizeof(ev), portMAX_DELAY);
break;
}
case AUTH_MODE_OCPP_RFID:
{
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_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;
}
} }
} }
int auth_get_tag_count(void) { int auth_get_tag_count(void)
{
return tag_count; return tag_count;
} }
const char *auth_get_tag_by_index(int index) { const char *auth_get_tag_by_index(int index)
if (index < 0 || index >= tag_count) return NULL; {
if (index < 0 || index >= tag_count)
return NULL;
return valid_tags[index]; return valid_tags[index];
} }

View File

@@ -1,3 +1,4 @@
// === Início de: components/auth/src/wiegand.c ===
/** /**
* @file wiegand.c * @file wiegand.c
* *
@@ -6,9 +7,13 @@
#include <esp_log.h> #include <esp_log.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <esp_idf_lib_helpers.h> #include <esp_attr.h> // <- para IRAM_ATTR
#include "wiegand.h" #include "wiegand.h"
#ifndef IRAM_ATTR
#define IRAM_ATTR
#endif
static const char *TAG = "wiegand"; static const char *TAG = "wiegand";
#define TIMER_INTERVAL_US 50000 // 50ms #define TIMER_INTERVAL_US 50000 // 50ms
@@ -39,11 +44,7 @@ static void isr_enable(wiegand_reader_t *reader)
gpio_set_intr_type(reader->gpio_d1, GPIO_INTR_NEGEDGE); gpio_set_intr_type(reader->gpio_d1, GPIO_INTR_NEGEDGE);
} }
#if HELPER_TARGET_IS_ESP32
static void IRAM_ATTR isr_handler(void *arg) static void IRAM_ATTR isr_handler(void *arg)
#else
static void isr_handler(void *arg)
#endif
{ {
wiegand_reader_t *reader = (wiegand_reader_t *)arg; wiegand_reader_t *reader = (wiegand_reader_t *)arg;
if (!reader->enabled) if (!reader->enabled)
@@ -181,3 +182,5 @@ esp_err_t wiegand_reader_done(wiegand_reader_t *reader)
return ESP_OK; return ESP_OK;
} }
// === Fim de: components/auth/src/wiegand.c ===

View File

@@ -1,23 +1,23 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <inttypes.h> #include <inttypes.h>
#include <stdbool.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <freertos/queue.h> #include <freertos/queue.h>
#include <esp_log.h> #include <esp_log.h>
#include <wiegand.h> #include <wiegand.h>
#include "auth.h" #include "auth.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#define CONFIG_EXAMPLE_BUF_SIZE 50 #define CONFIG_EXAMPLE_BUF_SIZE 50
#define IDTAG_MAX_LEN 20 #define IDTAG_MAX_LEN 20
static const char *TAG = "WiegandReader"; static const char *TAG = "WiegandReader";
// ---- Parâmetro global/locale para controlar a convenção de paridade ---- // ---- Parâmetro global/local para controlar a convenção de paridade ----
static const bool PARITY_INVERTED = false; // mude para true se o seu leitor vier "invertido" static const bool PARITY_INVERTED = false; // mude para true se o seu leitor vier "invertido"
static wiegand_reader_t reader; static wiegand_reader_t reader;
@@ -35,7 +35,7 @@ static inline uint8_t get_bit_msb_first(const uint8_t *buf, size_t bit_index)
return (buf[bit_index / 8] >> (7 - (bit_index % 8))) & 0x01; return (buf[bit_index / 8] >> (7 - (bit_index % 8))) & 0x01;
} }
// Versão parametrizável // Versão parametrizável de verificação de paridade
static bool check_parity_param(const uint8_t *buf, size_t bits, bool invert) static bool check_parity_param(const uint8_t *buf, size_t bits, bool invert)
{ {
if (bits != 26 && bits != 34) if (bits != 26 && bits != 34)
@@ -55,8 +55,8 @@ static bool check_parity_param(const uint8_t *buf, size_t bits, bool invert)
if (get_bit_msb_first(buf, i)) if (get_bit_msb_first(buf, i))
right++; right++;
} }
else else // 34 bits
{ // 34 {
for (int i = 1; i <= 16; i++) for (int i = 1; i <= 16; i++)
if (get_bit_msb_first(buf, i)) if (get_bit_msb_first(buf, i))
left++; left++;
@@ -117,24 +117,28 @@ static bool wiegand_extract_fc_cn(const uint8_t *buf, size_t bits, uint32_t *fc,
{ {
if (bits != 26 && bits != 34) if (bits != 26 && bits != 34)
return false; return false;
uint32_t payload = 0; uint32_t payload = 0;
size_t payload_bits = bits - 2; size_t payload_bits = bits - 2;
for (size_t i = 0; i < payload_bits; i++) for (size_t i = 0; i < payload_bits; i++)
{ {
size_t bit = 1 + i; // ignora bit de paridade inicial size_t bit = 1 + i; // ignora bit de paridade inicial
uint8_t bitval = (buf[bit / 8] >> (7 - (bit % 8))) & 0x01; uint8_t bitval = (buf[bit / 8] >> (7 - (bit % 8))) & 0x01;
payload = (payload << 1) | bitval; payload = (payload << 1) | bitval;
} }
if (bits == 26) if (bits == 26)
{ {
*fc = (payload >> 16) & 0xFF; // 8b *fc = (payload >> 16) & 0xFF; // 8b
*cn = payload & 0xFFFF; // 16b *cn = payload & 0xFFFF; // 16b
} }
else else // 34 bits
{ {
*fc = (payload >> 16) & 0xFFFF; // 16b *fc = (payload >> 16) & 0xFFFF; // 16b
*cn = payload & 0xFFFF; // 16b *cn = payload & 0xFFFF; // 16b
} }
return true; return true;
} }
@@ -145,10 +149,14 @@ static bool build_idtag_w26_4B(uint32_t fc, uint32_t cn, char *out, size_t outle
(uint8_t)((cn >> 8) & 0xFF), (uint8_t)((cn >> 8) & 0xFF),
(uint8_t)(cn & 0xFF), (uint8_t)(cn & 0xFF),
0}; 0};
raw[3] = crc8_atm(raw, 3); raw[3] = crc8_atm(raw, 3);
if (outlen < 9) if (outlen < 9)
return false; return false;
int n = snprintf(out, outlen, "%02X%02X%02X%02X", raw[0], raw[1], raw[2], raw[3]);
int n = snprintf(out, outlen, "%02X%02X%02X%02X",
raw[0], raw[1], raw[2], raw[3]);
return (n > 0 && (size_t)n < outlen); return (n > 0 && (size_t)n < outlen);
} }
@@ -160,31 +168,42 @@ static bool build_idtag_w34_7B(uint32_t fc, uint32_t cn, char *out, size_t outle
raw[2] = (uint8_t)(fc & 0xFF); raw[2] = (uint8_t)(fc & 0xFF);
raw[3] = (uint8_t)((cn >> 8) & 0xFF); raw[3] = (uint8_t)((cn >> 8) & 0xFF);
raw[4] = (uint8_t)(cn & 0xFF); raw[4] = (uint8_t)(cn & 0xFF);
uint16_t crc = crc16_ibm(raw, 5); uint16_t crc = crc16_ibm(raw, 5);
raw[5] = (uint8_t)((crc >> 8) & 0xFF); raw[5] = (uint8_t)((crc >> 8) & 0xFF);
raw[6] = (uint8_t)(crc & 0xFF); raw[6] = (uint8_t)(crc & 0xFF);
if (outlen < 15) if (outlen < 15)
return false; return false;
int n = snprintf(out, outlen, "%02X%02X%02X%02X%02X%02X%02X", int n = snprintf(out, outlen, "%02X%02X%02X%02X%02X%02X%02X",
raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6]); raw[0], raw[1], raw[2], raw[3],
raw[4], raw[5], raw[6]);
return (n > 0 && (size_t)n < outlen); return (n > 0 && (size_t)n < outlen);
} }
// Se o callback for de ISR, troque para xQueueSendToBackFromISR. // Callback chamado pelo esp_timer_task (contexto de task, NÃO ISR)
static void reader_callback(wiegand_reader_t *r) static void reader_callback(wiegand_reader_t *r)
{ {
if (!queue)
{
ESP_LOGW(TAG, "Queue not ready, dropping packet");
return;
}
data_packet_t p = {0}; data_packet_t p = {0};
p.bits = r->bits; p.bits = r->bits;
p.bytes = (r->bits + 7) / 8; p.bytes = (r->bits + 7) / 8;
if (p.bytes > sizeof(p.data)) if (p.bytes > sizeof(p.data))
p.bytes = sizeof(p.data); p.bytes = sizeof(p.data);
memcpy(p.data, r->buf, p.bytes); memcpy(p.data, r->buf, p.bytes);
BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (xQueueSendToBack(queue, &p, 0) != pdPASS)
// Se NÃO for ISR, use xQueueSendToBack(queue, &p, 0); {
xQueueSendToBackFromISR(queue, &p, &xHigherPriorityTaskWoken); ESP_LOGW(TAG, "Queue full, dropping Wiegand packet");
if (xHigherPriorityTaskWoken) }
portYIELD_FROM_ISR();
} }
static void wiegand_task(void *arg) static void wiegand_task(void *arg)
@@ -197,13 +216,20 @@ static void wiegand_task(void *arg)
return; return;
} }
ESP_ERROR_CHECK(wiegand_reader_init(&reader, 21, 22, ESP_ERROR_CHECK(wiegand_reader_init(&reader,
true, CONFIG_EXAMPLE_BUF_SIZE, reader_callback, WIEGAND_MSB_FIRST, WIEGAND_LSB_FIRST)); 21, 22, // GPIO D0, D1
true, // internal pullups
CONFIG_EXAMPLE_BUF_SIZE,
reader_callback,
WIEGAND_MSB_FIRST,
WIEGAND_LSB_FIRST));
data_packet_t p; data_packet_t p;
for (;;) for (;;)
{ {
ESP_LOGI(TAG, "Waiting for Wiegand data..."); ESP_LOGI(TAG, "Waiting for Wiegand data...");
if (xQueueReceive(queue, &p, portMAX_DELAY) != pdPASS) if (xQueueReceive(queue, &p, portMAX_DELAY) != pdPASS)
continue; continue;
@@ -216,7 +242,7 @@ static void wiegand_task(void *arg)
continue; continue;
} }
char tag[21] = {0}; // OCPP 1.6: máx 20 chars (+NUL) char tag[IDTAG_MAX_LEN + 1] = {0}; // OCPP 1.6: máx 20 chars (+NUL)
uint32_t fc = 0, cn = 0; uint32_t fc = 0, cn = 0;
if (!wiegand_extract_fc_cn(p.data, p.bits, &fc, &cn)) if (!wiegand_extract_fc_cn(p.data, p.bits, &fc, &cn))
@@ -230,8 +256,8 @@ static void wiegand_task(void *arg)
{ {
ok = build_idtag_w26_4B(fc, cn, tag, sizeof(tag)); // 8 hex ok = build_idtag_w26_4B(fc, cn, tag, sizeof(tag)); // 8 hex
} }
else else // 34
{ // 34 {
ok = build_idtag_w34_7B(fc, cn, tag, sizeof(tag)); // 14 hex ok = build_idtag_w34_7B(fc, cn, tag, sizeof(tag)); // 14 hex
} }
@@ -250,9 +276,9 @@ static void wiegand_sim_task(void *arg)
{ {
// lista fixa de idTags simuladas // lista fixa de idTags simuladas
static const char *idtaglist[] = { static const char *idtaglist[] = {
"0000004134962107", "00000041349",
"W2602312345", "W2602312345",
"W34ABCDE12345", "W34ABCDE123",
}; };
const size_t list_size = sizeof(idtaglist) / sizeof(idtaglist[0]); const size_t list_size = sizeof(idtaglist) / sizeof(idtaglist[0]);
@@ -263,7 +289,7 @@ static void wiegand_sim_task(void *arg)
ESP_LOGI(TAG, "Simulação -> idTag: %s", idtaglist[i]); ESP_LOGI(TAG, "Simulação -> idTag: %s", idtaglist[i]);
auth_process_tag(idtaglist[i]); auth_process_tag(idtaglist[i]);
// espera 20 segundos entre cada tag // espera 30 segundos entre cada tag
vTaskDelay(pdMS_TO_TICKS(30000)); vTaskDelay(pdMS_TO_TICKS(30000));
} }
} }
@@ -274,6 +300,8 @@ void initWiegand(void)
ESP_LOGI(TAG, "Initializing Wiegand reader"); ESP_LOGI(TAG, "Initializing Wiegand reader");
xTaskCreate(wiegand_task, TAG, configMINIMAL_STACK_SIZE * 4, NULL, 4, NULL); xTaskCreate(wiegand_task, TAG, configMINIMAL_STACK_SIZE * 4, NULL, 4, NULL);
// Para testes, podes ativar o simulador:
// ESP_LOGI(TAG, "Inicializando Wiegand simulado"); // ESP_LOGI(TAG, "Inicializando Wiegand simulado");
// xTaskCreate(wiegand_sim_task, "WiegandSim", configMINIMAL_STACK_SIZE * 3, NULL, 3, NULL); // xTaskCreate(wiegand_sim_task, "WiegandSim",
// configMINIMAL_STACK_SIZE * 3, NULL, 3, NULL);
} }

View File

@@ -8,5 +8,5 @@ idf_component_register(
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src" PRIV_INCLUDE_DIRS "src"
REQUIRES esp_event REQUIRES esp_event
PRIV_REQUIRES driver nvs_flash esp_timer evse PRIV_REQUIRES driver esp_timer evse network
) )

View File

@@ -18,50 +18,32 @@
#include <time.h> #include <time.h>
#include <string.h> #include <string.h>
// ===================== Configuração padrão (pode migrar para Kconfig) ===================== // ===================== Configuração padrão =====================
#ifndef CONFIG_BUZZER_GPIO
#define CONFIG_BUZZER_GPIO GPIO_NUM_27 #define CONFIG_BUZZER_GPIO GPIO_NUM_27
#endif
#ifndef CONFIG_BUZZER_MODE_PASSIVE // 1 = PASSIVE (PWM), 0 = ACTIVE (on/off) // 1 = PASSIVE (PWM), 0 = ACTIVE (on/off)
#define CONFIG_BUZZER_MODE_PASSIVE 1 #define CONFIG_BUZZER_MODE_PASSIVE 0
#endif
#ifndef CONFIG_BUZZER_FREQ_HZ
#define CONFIG_BUZZER_FREQ_HZ 3500 #define CONFIG_BUZZER_FREQ_HZ 3500
#endif
#ifndef CONFIG_BUZZER_DUTY_PCT
#define CONFIG_BUZZER_DUTY_PCT 70 #define CONFIG_BUZZER_DUTY_PCT 70
#endif
#ifndef CONFIG_BUZZER_QUEUE_LEN
#define CONFIG_BUZZER_QUEUE_LEN 8 #define CONFIG_BUZZER_QUEUE_LEN 8
#endif
#ifndef CONFIG_BUZZER_TASK_STACK
#define CONFIG_BUZZER_TASK_STACK 2048 #define CONFIG_BUZZER_TASK_STACK 2048
#endif
#ifndef CONFIG_BUZZER_TASK_PRIO
#define CONFIG_BUZZER_TASK_PRIO (tskIDLE_PRIORITY + 1) #define CONFIG_BUZZER_TASK_PRIO (tskIDLE_PRIORITY + 1)
#endif
#ifndef CONFIG_BUZZER_MIN_GAP_MS // anti-spam (gap mínimo entre toques) // anti-spam (gap mínimo entre toques)
#define CONFIG_BUZZER_MIN_GAP_MS 70 #define CONFIG_BUZZER_MIN_GAP_MS 70
#endif
#ifndef CONFIG_BUZZER_ENABLE_DEFAULT
#define CONFIG_BUZZER_ENABLE_DEFAULT 1 #define CONFIG_BUZZER_ENABLE_DEFAULT 1
#endif
#ifndef CONFIG_BUZZER_QUIET_START_MIN // quiet hours start (minutos desde 00:00) // quiet hours start (minutos desde 00:00)
#define CONFIG_BUZZER_QUIET_START_MIN (22 * 60) #define CONFIG_BUZZER_QUIET_START_MIN (22 * 60)
#endif
#ifndef CONFIG_BUZZER_QUIET_END_MIN // quiet hours end (minutos desde 00:00) // quiet hours end (minutos desde 00:00)
#define CONFIG_BUZZER_QUIET_END_MIN (7 * 60) #define CONFIG_BUZZER_QUIET_END_MIN (7 * 60)
#endif
// ===================== Tipos e estado ===================== // ===================== Tipos e estado =====================
static const char *TAG = "Buzzer"; static const char *TAG = "Buzzer";
@@ -415,11 +397,25 @@ static void network_event_handler(void *handler_args, esp_event_base_t base, int
{ {
if (base != NETWORK_EVENTS) if (base != NETWORK_EVENTS)
return; return;
if (id == NETWORK_EVENT_AP_STARTED)
ESP_LOGD(TAG, "Network event id=%d", (int)id);
buzzer_event_data_t evt = {0};
switch ((network_event_id_t)id)
{ {
buzzer_event_data_t evt = {.pattern = BUZZER_PATTERN_AP_START}; case NETWORK_EVENT_AP_STARTED:
esp_event_post(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, &evt, sizeof(evt), portMAX_DELAY); case NETWORK_EVENT_STA_CONNECTED:
// Usa padrão de AP_START para indicar rede disponível
evt.pattern = BUZZER_PATTERN_AP_START;
break;
default:
// Para já, ignorar outros eventos de rede
return;
} }
esp_event_post(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, &evt, sizeof(evt), portMAX_DELAY);
} }
static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *event_data) static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *event_data)
@@ -506,7 +502,7 @@ void buzzer_init(void)
ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, auth_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, auth_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, auth_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, auth_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_EVENTS, NETWORK_EVENT_AP_STARTED, network_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_EVENTS, ESP_EVENT_ANY_ID, network_event_handler, NULL));
ESP_LOGI(TAG, "Buzzer initialized on GPIO %d (%s), freq=%lu Hz, duty=%u%%, enabled=%d", ESP_LOGI(TAG, "Buzzer initialized on GPIO %d (%s), freq=%lu Hz, duty=%u%%, enabled=%d",
s_buzzer_cfg.gpio, s_buzzer_cfg.gpio,
@@ -522,7 +518,7 @@ void buzzer_deinit(void)
(void)esp_event_handler_unregister(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler); (void)esp_event_handler_unregister(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler);
(void)esp_event_handler_unregister(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, auth_event_handler); (void)esp_event_handler_unregister(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, auth_event_handler);
(void)esp_event_handler_unregister(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, auth_event_handler); (void)esp_event_handler_unregister(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, auth_event_handler);
(void)esp_event_handler_unregister(NETWORK_EVENTS, NETWORK_EVENT_AP_STARTED, network_event_handler); (void)esp_event_handler_unregister(NETWORK_EVENTS, ESP_EVENT_ANY_ID, network_event_handler);
if (s_buzzer_q) if (s_buzzer_q)
{ {

View File

@@ -2,5 +2,5 @@ set(srcs
"board_config.c") "board_config.c")
idf_component_register(SRCS "${srcs}" idf_component_register(SRCS "${srcs}"
PRIV_REQUIRES nvs_flash PRIV_REQUIRES
INCLUDE_DIRS "include") INCLUDE_DIRS "include")

View File

@@ -15,32 +15,6 @@ bool atob(const char *value)
return value[0] == 'y'; return value[0] == 'y';
} }
board_config_energy_meter_t atoem(const char *value)
{
if (!strcmp(value, "cur"))
{
return BOARD_CONFIG_ENERGY_METER_CUR;
}
if (!strcmp(value, "cur_vlt"))
{
return BOARD_CONFIG_ENERGY_METER_CUR_VLT;
}
return BOARD_CONFIG_ENERGY_METER_NONE;
}
board_config_serial_t atoser(const char *value)
{
if (!strcmp(value, "uart"))
{
return BOARD_CONFIG_SERIAL_UART;
}
if (!strcmp(value, "rs485"))
{
return BOARD_CONFIG_SERIAL_RS485;
}
return BOARD_CONFIG_SERIAL_NONE;
}
#define SET_CONFIG_VALUE(name, prop, convert_fn) \ #define SET_CONFIG_VALUE(name, prop, convert_fn) \
if (!strcmp(key, name)) \ if (!strcmp(key, name)) \
{ \ { \
@@ -94,16 +68,14 @@ void board_config_load()
if (value != NULL) if (value != NULL)
{ {
SET_CONFIG_VALUE_STR("DEVICE_NAME", device_name); SET_CONFIG_VALUE_STR("DEVICE_NAME", device_name);
SET_CONFIG_VALUE("LED_CHARGING", led_charging, atob); SET_CONFIG_VALUE("led_blue", led_blue, atob);
SET_CONFIG_VALUE("LED_CHARGING_GPIO", led_charging_gpio, atoi); SET_CONFIG_VALUE("led_blue_GPIO", led_blue_gpio, atoi);
SET_CONFIG_VALUE("LED_ERROR", led_error, atob); SET_CONFIG_VALUE("led_red", led_red, atob);
SET_CONFIG_VALUE("LED_ERROR_GPIO", led_error_gpio, atoi); SET_CONFIG_VALUE("led_red_GPIO", led_red_gpio, atoi);
SET_CONFIG_VALUE("LED_STOP", led_stop, atob); SET_CONFIG_VALUE("led_green", led_green, atob);
SET_CONFIG_VALUE("LED_STOP_GPIO", led_stop_gpio, atoi); SET_CONFIG_VALUE("led_green_GPIO", led_green_gpio, atoi);
SET_CONFIG_VALUE("BUZZER", buzzer, atob); SET_CONFIG_VALUE("BUZZER", buzzer, atob);
SET_CONFIG_VALUE("BUZZER_GPIO", buzzer_gpio, atoi); SET_CONFIG_VALUE("BUZZER_GPIO", buzzer_gpio, atoi);
SET_CONFIG_VALUE("BUTTON_WIFI_GPIO", button_wifi_gpio, atoi); SET_CONFIG_VALUE("BUTTON_WIFI_GPIO", button_wifi_gpio, atoi);
SET_CONFIG_VALUE("PILOT_PWM_GPIO", pilot_pwm_gpio, atoi); SET_CONFIG_VALUE("PILOT_PWM_GPIO", pilot_pwm_gpio, atoi);
SET_CONFIG_VALUE("PILOT_ADC_CHANNEL", pilot_adc_channel, atoi); SET_CONFIG_VALUE("PILOT_ADC_CHANNEL", pilot_adc_channel, atoi);
@@ -130,76 +102,6 @@ void board_config_load()
SET_CONFIG_VALUE("RCM", rcm, atob); SET_CONFIG_VALUE("RCM", rcm, atob);
SET_CONFIG_VALUE("RCM_GPIO", rcm_gpio, atoi); SET_CONFIG_VALUE("RCM_GPIO", rcm_gpio, atoi);
SET_CONFIG_VALUE("RCM_TEST_GPIO", rcm_test_gpio, atoi); SET_CONFIG_VALUE("RCM_TEST_GPIO", rcm_test_gpio, atoi);
SET_CONFIG_VALUE("ENERGY_METER", energy_meter, atoem);
SET_CONFIG_VALUE("ENERGY_METER_THREE_PHASES", energy_meter_three_phases, atob);
SET_CONFIG_VALUE("ENERGY_METER_L1_CUR_ADC_CHANNEL", energy_meter_l1_cur_adc_channel, atoi);
SET_CONFIG_VALUE("ENERGY_METER_L2_CUR_ADC_CHANNEL", energy_meter_l2_cur_adc_channel, atoi);
SET_CONFIG_VALUE("ENERGY_METER_L3_CUR_ADC_CHANNEL", energy_meter_l3_cur_adc_channel, atoi);
SET_CONFIG_VALUE("ENERGY_METER_CUR_SCALE", energy_meter_cur_scale, atoff);
SET_CONFIG_VALUE("ENERGY_METER_L1_VLT_ADC_CHANNEL", energy_meter_l1_vlt_adc_channel, atoi);
SET_CONFIG_VALUE("ENERGY_METER_L2_VLT_ADC_CHANNEL", energy_meter_l2_vlt_adc_channel, atoi);
SET_CONFIG_VALUE("ENERGY_METER_L3_VLT_ADC_CHANNEL", energy_meter_l3_vlt_adc_channel, atoi);
SET_CONFIG_VALUE("ENERGY_METER_VLT_SCALE", energy_meter_vlt_scale, atoff);
SET_CONFIG_VALUE("AUX_IN_1", aux_in_1, atob);
SET_CONFIG_VALUE_STR("AUX_IN_1_NAME", aux_in_1_name);
SET_CONFIG_VALUE("AUX_IN_1_GPIO", aux_in_1_gpio, atoi);
SET_CONFIG_VALUE("AUX_IN_2", aux_in_2, atob);
SET_CONFIG_VALUE_STR("AUX_IN_2_NAME", aux_in_2_name);
SET_CONFIG_VALUE("AUX_IN_2_GPIO", aux_in_2_gpio, atoi);
SET_CONFIG_VALUE("AUX_IN_3", aux_in_3, atob);
SET_CONFIG_VALUE_STR("AUX_IN_3_NAME", aux_in_3_name);
SET_CONFIG_VALUE("AUX_IN_3_GPIO", aux_in_3_gpio, atoi);
SET_CONFIG_VALUE("AUX_IN_4", aux_in_4, atob);
SET_CONFIG_VALUE_STR("AUX_IN_4_NAME", aux_in_4_name);
SET_CONFIG_VALUE("AUX_IN_4_GPIO", aux_in_4_gpio, atoi);
SET_CONFIG_VALUE("AUX_OUT_1", aux_out_1, atob);
SET_CONFIG_VALUE_STR("AUX_OUT_1_NAME", aux_out_1_name);
SET_CONFIG_VALUE("AUX_OUT_1_GPIO", aux_out_1_gpio, atoi);
SET_CONFIG_VALUE("AUX_OUT_2", aux_out_2, atob);
SET_CONFIG_VALUE_STR("AUX_OUT_2_NAME", aux_out_2_name);
SET_CONFIG_VALUE("AUX_OUT_2_GPIO", aux_out_2_gpio, atoi);
SET_CONFIG_VALUE("AUX_OUT_3", aux_out_3, atob);
SET_CONFIG_VALUE_STR("AUX_OUT_3_NAME", aux_out_3_name);
SET_CONFIG_VALUE("AUX_OUT_3_GPIO", aux_out_3_gpio, atoi);
SET_CONFIG_VALUE("AUX_OUT_4", aux_out_4, atob);
SET_CONFIG_VALUE_STR("AUX_OUT_4_NAME", aux_out_4_name);
SET_CONFIG_VALUE("AUX_OUT_4_GPIO", aux_out_4_gpio, atoi);
SET_CONFIG_VALUE("AUX_AIN_1", aux_ain_1, atob);
SET_CONFIG_VALUE_STR("AUX_AIN_1_NAME", aux_ain_1_name);
SET_CONFIG_VALUE("AUX_AIN_1_ADC_CHANNEL", aux_ain_1_adc_channel, atoi);
SET_CONFIG_VALUE("AUX_AIN_2", aux_ain_2, atob);
SET_CONFIG_VALUE_STR("AUX_AIN_2_NAME", aux_ain_2_name);
SET_CONFIG_VALUE("AUX_AIN_2_ADC_CHANNEL", aux_ain_2_adc_channel, atoi);
/*
#if CONFIG_ESP_CONSOLE_UART_NUM != 0
SET_CONFIG_VALUE("SERIAL_1", serial_1, atoser);
SET_CONFIG_VALUE_STR("SERIAL_1_NAME", serial_1_name);
SET_CONFIG_VALUE("SERIAL_1_RXD_GPIO", serial_1_rxd_gpio, atoi);
SET_CONFIG_VALUE("SERIAL_1_TXD_GPIO", serial_1_txd_gpio, atoi);
SET_CONFIG_VALUE("SERIAL_1_RTS_GPIO", serial_1_rts_gpio, atoi);
#endif // CONFIG_ESP_CONSOLE_UART_NUM != 0
#if CONFIG_ESP_CONSOLE_UART_NUM != 1
SET_CONFIG_VALUE("SERIAL_2", serial_2, atoser);
SET_CONFIG_VALUE_STR("SERIAL_2_NAME", serial_2_name);
SET_CONFIG_VALUE("SERIAL_2_RXD_GPIO", serial_2_rxd_gpio, atoi);
SET_CONFIG_VALUE("SERIAL_2_TXD_GPIO", serial_2_txd_gpio, atoi);
SET_CONFIG_VALUE("SERIAL_2_RTS_GPIO", serial_2_rts_gpio, atoi);
#endif // CONFIG_ESP_CONSOLE_UART_NUM != 1
#if SOC_UART_NUM > 2
#if CONFIG_ESP_CONSOLE_UART_NUM != 2
SET_CONFIG_VALUE("SERIAL_3", serial_3, atoser);
SET_CONFIG_VALUE_STR("SERIAL_3_NAME", serial_3_name);
SET_CONFIG_VALUE("SERIAL_3_RXD_GPIO", serial_3_rxd_gpio, atoi);
SET_CONFIG_VALUE("SERIAL_3_TXD_GPIO", serial_3_txd_gpio, atoi);
SET_CONFIG_VALUE("SERIAL_3_RTS_GPIO", serial_3_rts_gpio, atoi);
#endif / CONFIG_ESP_CONSOLE_UART_NUM != 2
#endif // SOC_UART_NUM > 2
SET_CONFIG_VALUE("ONEWIRE", onewire, atob);
SET_CONFIG_VALUE("ONEWIRE_GPIO", onewire_gpio, atoi);
SET_CONFIG_VALUE("ONEWIRE_TEMP_SENSOR", onewire_temp_sensor, atob);
ESP_LOGE(TAG, "Unknown config value %s=%s", key, value);
*/
} }
} }
} }

View File

@@ -5,13 +5,15 @@
#include "hal/gpio_types.h" #include "hal/gpio_types.h"
#include "soc/soc_caps.h" #include "soc/soc_caps.h"
typedef enum { typedef enum
{
BOARD_CONFIG_ENERGY_METER_NONE, BOARD_CONFIG_ENERGY_METER_NONE,
BOARD_CONFIG_ENERGY_METER_CUR, BOARD_CONFIG_ENERGY_METER_CUR,
BOARD_CONFIG_ENERGY_METER_CUR_VLT BOARD_CONFIG_ENERGY_METER_CUR_VLT
} board_config_energy_meter_t; } board_config_energy_meter_t;
typedef enum { typedef enum
{
BOARD_CONFIG_SERIAL_NONE, BOARD_CONFIG_SERIAL_NONE,
BOARD_CONFIG_SERIAL_UART, BOARD_CONFIG_SERIAL_UART,
BOARD_CONFIG_SERIAL_RS485 BOARD_CONFIG_SERIAL_RS485
@@ -21,12 +23,12 @@ typedef struct
{ {
char device_name[32]; char device_name[32];
bool led_charging : 1; bool led_blue : 1;
gpio_num_t led_charging_gpio; gpio_num_t led_blue_gpio;
bool led_error : 1; bool led_red : 1;
gpio_num_t led_error_gpio; gpio_num_t led_red_gpio;
bool led_stop : 1; bool led_green : 1;
gpio_num_t led_stop_gpio; gpio_num_t led_green_gpio;
bool buzzer : 1; bool buzzer : 1;
gpio_num_t buzzer_gpio; gpio_num_t buzzer_gpio;
@@ -63,80 +65,6 @@ typedef struct
gpio_num_t rcm_gpio; gpio_num_t rcm_gpio;
gpio_num_t rcm_test_gpio; gpio_num_t rcm_test_gpio;
board_config_energy_meter_t energy_meter;
bool energy_meter_three_phases : 1;
adc_channel_t energy_meter_l1_cur_adc_channel;
adc_channel_t energy_meter_l2_cur_adc_channel;
adc_channel_t energy_meter_l3_cur_adc_channel;
float energy_meter_cur_scale;
adc_channel_t energy_meter_l1_vlt_adc_channel;
adc_channel_t energy_meter_l2_vlt_adc_channel;
adc_channel_t energy_meter_l3_vlt_adc_channel;
float energy_meter_vlt_scale;
bool aux_in_1 : 1;
char aux_in_1_name[8];
gpio_num_t aux_in_1_gpio;
bool aux_in_2 : 1;
char aux_in_2_name[8];
gpio_num_t aux_in_2_gpio;
bool aux_in_3 : 1;
char aux_in_3_name[8];
gpio_num_t aux_in_3_gpio;
bool aux_in_4 : 1;
char aux_in_4_name[8];
gpio_num_t aux_in_4_gpio;
bool aux_out_1 : 1;
char aux_out_1_name[8];
gpio_num_t aux_out_1_gpio;
bool aux_out_2 : 1;
char aux_out_2_name[8];
gpio_num_t aux_out_2_gpio;
bool aux_out_3 : 1;
char aux_out_3_name[8];
gpio_num_t aux_out_3_gpio;
bool aux_out_4 : 1;
char aux_out_4_name[8];
gpio_num_t aux_out_4_gpio;
bool aux_ain_1 : 1;
char aux_ain_1_name[8];
adc_channel_t aux_ain_1_adc_channel;
bool aux_ain_2 : 1;
char aux_ain_2_name[8];
adc_channel_t aux_ain_2_adc_channel;
board_config_serial_t serial_1;
char serial_1_name[16];
gpio_num_t serial_1_rxd_gpio;
gpio_num_t serial_1_txd_gpio;
gpio_num_t serial_1_rts_gpio;
board_config_serial_t serial_2;
char serial_2_name[16];
gpio_num_t serial_2_rxd_gpio;
gpio_num_t serial_2_txd_gpio;
gpio_num_t serial_2_rts_gpio;
#if SOC_UART_NUM > 2
board_config_serial_t serial_3;
char serial_3_name[16];
gpio_num_t serial_3_rxd_gpio;
gpio_num_t serial_3_txd_gpio;
gpio_num_t serial_3_rts_gpio;
#endif /* SOC_UART_NUM > 2 */
bool onewire : 1;
gpio_num_t onewire_gpio;
bool onewire_temp_sensor : 1;
} board_config_t; } board_config_t;
extern board_config_t board_config; extern board_config_t board_config;

View File

@@ -1,20 +0,0 @@
name: esp_idf_lib_helpers
description: Common support library for esp-idf-lib
version: 1.2.0
groups:
- common
code_owners:
- trombik
- UncleRus
depends:
- freertos
thread_safe: n/a
targets:
- esp32
- esp8266
- esp32s2
- esp32c3
license: ISC
copyrights:
- name: trombik
year: 2019

View File

@@ -1,4 +0,0 @@
idf_component_register(
INCLUDE_DIRS .
REQUIRES freertos
)

View File

@@ -1,13 +0,0 @@
Copyright (c) 2019 Tomoyuki Sakurai <y@trombik.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,8 +0,0 @@
COMPONENT_ADD_INCLUDEDIRS = .
ifdef CONFIG_IDF_TARGET_ESP8266
COMPONENT_DEPENDS = esp8266 freertos
else
COMPONENT_DEPENDS = freertos
endif

View File

@@ -1,84 +0,0 @@
/*
* Copyright (c) 2019 Tomoyuki Sakurai <y@trombik.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#if !defined(__ESP_IDF_LIB_HELPERS__H__)
#define __ESP_IDF_LIB_HELPERS__H__
/* XXX this header file does not need to include freertos/FreeRTOS.h.
* but without it, ESP8266 RTOS SDK does not include `sdkconfig.h` in correct
* order. as this header depends on sdkconfig.h, sdkconfig.h must be included
* first. however, the SDK includes this header first, then includes
* `sdkconfig.h` when freertos/FreeRTOS.h is not explicitly included. an
* evidence can be found in `build/${COMPONENT}/${COMPONENT}.d` in a failed
* build.
*/
#include <freertos/FreeRTOS.h>
#include <esp_idf_version.h>
#if !defined(ESP_IDF_VERSION) || !defined(ESP_IDF_VERSION_VAL)
#error Unknown ESP-IDF/ESP8266 RTOS SDK version
#endif
/* Minimal supported version for ESP32, ESP32S2 */
#define HELPER_ESP32_MIN_VER ESP_IDF_VERSION_VAL(3, 3, 5)
/* Minimal supported version for ESP8266 */
#define HELPER_ESP8266_MIN_VER ESP_IDF_VERSION_VAL(3, 3, 0)
/* HELPER_TARGET_IS_ESP32
* 1 when the target is esp32
*/
#if defined(CONFIG_IDF_TARGET_ESP32) \
|| defined(CONFIG_IDF_TARGET_ESP32S2) \
|| defined(CONFIG_IDF_TARGET_ESP32S3) \
|| defined(CONFIG_IDF_TARGET_ESP32C2) \
|| defined(CONFIG_IDF_TARGET_ESP32C3) \
|| defined(CONFIG_IDF_TARGET_ESP32C6) \
|| defined(CONFIG_IDF_TARGET_ESP32H2)
#define HELPER_TARGET_IS_ESP32 (1)
#define HELPER_TARGET_IS_ESP8266 (0)
/* HELPER_TARGET_IS_ESP8266
* 1 when the target is esp8266
*/
#elif defined(CONFIG_IDF_TARGET_ESP8266)
#define HELPER_TARGET_IS_ESP32 (0)
#define HELPER_TARGET_IS_ESP8266 (1)
#else
#error BUG: cannot determine the target
#endif
#if HELPER_TARGET_IS_ESP32 && ESP_IDF_VERSION < HELPER_ESP32_MIN_VER
#error Unsupported ESP-IDF version. Please update!
#endif
#if HELPER_TARGET_IS_ESP8266 && ESP_IDF_VERSION < HELPER_ESP8266_MIN_VER
#error Unsupported ESP8266 RTOS SDK version. Please update!
#endif
/* show the actual values for debugging */
#if DEBUG
#define VALUE_TO_STRING(x) #x
#define VALUE(x) VALUE_TO_STRING(x)
#define VAR_NAME_VALUE(var) #var "=" VALUE(var)
#pragma message(VAR_NAME_VALUE(CONFIG_IDF_TARGET_ESP32C3))
#pragma message(VAR_NAME_VALUE(CONFIG_IDF_TARGET_ESP32H2))
#pragma message(VAR_NAME_VALUE(CONFIG_IDF_TARGET_ESP32S2))
#pragma message(VAR_NAME_VALUE(CONFIG_IDF_TARGET_ESP32))
#pragma message(VAR_NAME_VALUE(CONFIG_IDF_TARGET_ESP8266))
#pragma message(VAR_NAME_VALUE(ESP_IDF_VERSION_MAJOR))
#endif
#endif

View File

@@ -1,21 +0,0 @@
#if CONFIG_IDF_TARGET_ESP32
#include <esp32/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32C2
#include <esp32c2/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32C3
#include <esp32c3/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32C6
#include <esp32c6/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32H2
#include <esp32h2/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32H4
#include <esp32h4/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32S2
#include <esp32s2/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP32S3
#include <esp32s3/rom/ets_sys.h>
#elif CONFIG_IDF_TARGET_ESP8266
#include <rom/ets_sys.h>
#else
#error "ets_sys: Unknown target"
#endif

View File

@@ -17,6 +17,6 @@ set(srcs
idf_component_register( idf_component_register(
SRCS ${srcs} SRCS ${srcs}
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash driver PRIV_REQUIRES driver
REQUIRES peripherals auth loadbalancer REQUIRES peripherals auth loadbalancer scheduler storage_service
) )

View File

@@ -1,216 +1,289 @@
#include <inttypes.h> // For PRI macros #include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "evse_config.h" #include "evse_config.h"
#include "board_config.h" #include "board_config.h"
#include "evse_limits.h" #include "evse_limits.h"
#include "evse_api.h" // <— para evse_get_state / evse_state_is_charging #include "evse_api.h"
#include "evse_state.h"
#include "esp_log.h" #include "esp_log.h"
#include "nvs.h" #include "esp_err.h"
#include "esp_timer.h" #include "esp_check.h"
#include "storage_service.h"
static const char *TAG = "evse_config"; static const char *TAG = "evse_config";
#define NVS_NAMESPACE "evse_config"
static nvs_handle_t nvs;
// ======================== // ========================
// 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) // 1) Hardware (FIXO)
static uint16_t charging_current_runtime = 0; // Runtime only static const uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
static bool socket_outlet;
static bool rcm; // 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 uint8_t temp_threshold = 60;
static bool require_auth;
// Availability / Enable flags // Availability / Enable flags (persistidos)
static bool is_available = true; static bool is_available = true;
static bool is_enabled = 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 // Initialization
// ======================== // ========================
esp_err_t evse_config_init(void) esp_err_t evse_config_init(void)
{ {
ESP_LOGD(TAG, "Initializing NVS configuration..."); // garante storage iniciado
return nvs_open("evse", NVS_READWRITE, &nvs); 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) void evse_check_defaults(void)
{ {
esp_err_t err; esp_err_t err;
uint8_t u8; uint8_t u8 = 0;
uint16_t u16; uint16_t u16 = 0;
uint32_t u32;
bool needs_commit = false;
uint8_t u8_bool;
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 ESP_LOGD(TAG, "Checking default parameters (sync persistence)...");
err = nvs_get_u8(nvs, "max_chrg_curr", &u8);
if (err != ESP_OK || u8 < MIN_CHARGING_CURRENT_LIMIT || u8 > MAX_CHARGING_CURRENT_LIMIT)
{
max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
nvs_set_u8(nvs, "max_chrg_curr", max_charging_current);
needs_commit = true;
ESP_LOGW(TAG, "Invalid or missing max_chrg_curr, resetting to %d", max_charging_current);
}
else
{
max_charging_current = u8;
}
// -----------------------------------------
// Charging current (default, persisted) // 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; charging_current = max_charging_current;
nvs_set_u16(nvs, "def_chrg_curr", charging_current);
needs_commit = true; esp_err_t se = storage_set_u16_sync(NVS_NAMESPACE, "def_chrg_curr", charging_current, wr_to);
ESP_LOGW(TAG, "Invalid or missing def_chrg_curr, resetting to %d", charging_current); 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 else
{ {
charging_current = u16; charging_current = u16;
} }
// Runtime charging current initialized from persisted default // runtime inicializa a partir do default
charging_current_runtime = max_charging_current; 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); // Socket outlet (persisted) + capability gate
require_auth = (err == ESP_OK && u8 <= 1) ? u8 : false; // -----------------------------------------
if (err != ESP_OK) 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); bool wanted = (u8 != 0);
needs_commit = true;
}
// Socket outlet if (wanted && !board_config.proximity)
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).");
}
// Save to NVS if needed
if (needs_commit)
{
err = nvs_commit(nvs);
if (err == ESP_OK)
{ {
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 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 // Charging current getters/setters
// ======================== // ========================
uint8_t evse_get_max_charging_current(void) uint8_t evse_get_max_charging_current(void) { return max_charging_current; }
{
return max_charging_current;
}
esp_err_t evse_set_max_charging_current(uint8_t value) uint16_t evse_get_charging_current(void) { return charging_current; }
{
if (value < MIN_CHARGING_CURRENT_LIMIT || value > MAX_CHARGING_CURRENT_LIMIT)
return ESP_ERR_INVALID_ARG;
max_charging_current = value;
evse_set_runtime_charging_current(value);
nvs_set_u8(nvs, "max_chrg_curr", value);
return nvs_commit(nvs);
}
uint16_t evse_get_charging_current(void)
{
return charging_current;
}
esp_err_t evse_set_charging_current(uint16_t value) 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; return ESP_ERR_INVALID_ARG;
if (value == charging_current)
{
evse_set_runtime_charging_current(value);
return ESP_OK;
}
charging_current = value; charging_current = value;
nvs_set_u16(nvs, "def_chrg_curr", value);
return nvs_commit(nvs);
}
uint16_t evse_get_default_charging_current(void) esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_chrg_curr", value);
{ if (err != ESP_OK)
uint16_t value; {
if (nvs_get_u16(nvs, "def_chrg_curr", &value) == ESP_OK) // Em runtime, isto pode falhar por fila cheia. RAM fica correta; persistência é best-effort.
return value; ESP_LOGE(TAG, "Failed to persist def_chrg_curr async=%u: %s", (unsigned)value, esp_err_to_name(err));
return charging_current; return err;
} }
esp_err_t evse_set_default_charging_current(uint16_t value) evse_set_runtime_charging_current(value);
{ return ESP_OK;
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);
} }
// ======================== // ========================
@@ -218,142 +291,120 @@ esp_err_t evse_set_default_charging_current(uint16_t value)
// ======================== // ========================
void evse_set_runtime_charging_current(uint16_t value) void evse_set_runtime_charging_current(uint16_t value)
{ {
if (value > max_charging_current) if (value > max_charging_current)
{
value = max_charging_current; value = max_charging_current;
}
else if (value < MIN_CHARGING_CURRENT_LIMIT) else if (value < MIN_CHARGING_CURRENT_LIMIT)
{
value = MIN_CHARGING_CURRENT_LIMIT; value = MIN_CHARGING_CURRENT_LIMIT;
}
charging_current_runtime = value; charging_current_runtime = value;
ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime);
// --- PUBLICA ALTERAÇÃO DE CONFIG DO EVSE ---
evse_config_event_data_t evt = {
.charging = evse_state_is_charging(evse_get_state()),
.hw_max_current = (float)evse_get_max_charging_current(),
.runtime_current = (float)evse_get_runtime_charging_current(),
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_CONFIG_UPDATED,
&evt,
sizeof(evt),
portMAX_DELAY);
} }
uint16_t evse_get_runtime_charging_current(void) uint16_t evse_get_runtime_charging_current(void) { return charging_current_runtime; }
{
return charging_current_runtime;
}
// ======================== // ========================
// Socket outlet // Socket outlet
// ======================== // ========================
bool evse_get_socket_outlet(void) bool evse_get_socket_outlet(void) { return socket_outlet; }
{
return socket_outlet;
}
esp_err_t evse_set_socket_outlet(bool value) esp_err_t evse_set_socket_outlet(bool value)
{ {
if (value && !board_config.proximity) if (value && !board_config.proximity)
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
if (value == socket_outlet)
return ESP_OK;
socket_outlet = value; 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 // RCM
// ======================== // ========================
bool evse_is_rcm(void) bool evse_is_rcm(void) { return rcm; }
{
return rcm;
}
esp_err_t evse_set_rcm(bool value) esp_err_t evse_set_rcm(bool value)
{ {
if (value && !board_config.rcm) if (value && !board_config.rcm)
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
if (value == rcm)
return ESP_OK;
rcm = value; 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 // Temperature
// ======================== // ========================
uint8_t evse_get_temp_threshold(void) uint8_t evse_get_temp_threshold(void) { return temp_threshold; }
{
return temp_threshold;
}
esp_err_t evse_set_temp_threshold(uint8_t value) esp_err_t evse_set_temp_threshold(uint8_t value)
{ {
if (value < 40 || value > 80) if (value < 40 || value > 80)
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
if (value == temp_threshold)
return ESP_OK;
temp_threshold = value; 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 // Availability
// ======================== // ========================
bool evse_config_is_available(void) bool evse_config_is_available(void) { return is_available; }
{
return is_available;
}
void evse_config_set_available(bool available) void evse_config_set_available(bool available)
{ {
is_available = available ? true : false; bool newv = available;
if (newv == is_available)
return;
// Persist is_available = newv;
esp_err_t err = nvs_set_u8(nvs, "available", (uint8_t)is_available);
if (err == ESP_OK) esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "available", (uint8_t)is_available);
err = nvs_commit(nvs);
if (err != ESP_OK) if (err != ESP_OK)
{ ESP_LOGE(TAG, "Failed to persist 'available' async=%u: %s", (unsigned)is_available, esp_err_to_name(err));
ESP_LOGE(TAG, "Failed to persist 'available': %s", esp_err_to_name(err));
}
// AVAILABLE_UPDATED
evse_available_event_data_t e = {
.available = is_available,
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
} }
// ======================== // ========================
// Enable/Disable // Enable/Disable
// ======================== // ========================
bool evse_config_is_enabled(void) bool evse_config_is_enabled(void) { return is_enabled; }
{
return is_enabled;
}
void evse_config_set_enabled(bool enabled) void evse_config_set_enabled(bool enabled)
{ {
is_enabled = enabled ? true : false; bool newv = enabled;
if (newv == is_enabled)
return;
// Persist is_enabled = newv;
esp_err_t err = nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled);
if (err == ESP_OK) esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "enabled", (uint8_t)is_enabled);
err = nvs_commit(nvs);
if (err != ESP_OK) if (err != ESP_OK)
{ ESP_LOGE(TAG, "Failed to persist 'enabled' async=%u: %s", (unsigned)is_enabled, esp_err_to_name(err));
ESP_LOGE(TAG, "Failed to persist 'enabled': %s", esp_err_to_name(err));
}
// ENABLE_UPDATED
evse_enable_event_data_t e = {
.enabled = is_enabled,
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
} }

View File

@@ -1,5 +1,4 @@
// evse_core.c - Main EVSE control logic // components/evse/evse_core.c
#include "evse_fsm.h" #include "evse_fsm.h"
#include "evse_error.h" #include "evse_error.h"
#include "evse_limits.h" #include "evse_limits.h"
@@ -16,67 +15,115 @@ static const char *TAG = "evse_core";
static SemaphoreHandle_t mutex; static SemaphoreHandle_t mutex;
static evse_state_t last_state = EVSE_STATE_A; 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); static void evse_core_task(void *arg);
// ================================ void evse_init(void)
// Initialization {
// ================================
void evse_init(void) {
ESP_LOGI(TAG, "EVSE Init"); ESP_LOGI(TAG, "EVSE Init");
mutex = xSemaphoreCreateMutex(); // Optional: use static version for deterministic memory mutex = xSemaphoreCreateMutex();
if (!mutex)
{
ESP_LOGE(TAG, "Failed to create EVSE core mutex");
return;
}
evse_check_defaults(); evse_check_defaults();
evse_fsm_reset(); evse_fsm_reset();
pilot_set_level(true); // Enable pilot output 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)
// Main Processing Logic {
// ================================ if (!mutex)
{
return;
}
void evse_process(void) {
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
pilot_voltage_t pilot_voltage; pilot_voltage_t pilot_raw;
bool is_n12v = false; bool is_n12v = false;
pilot_measure(&pilot_voltage, &is_n12v); pilot_measure(&pilot_raw, &is_n12v);
ESP_LOGD(TAG, "Pilot: %d, -12V: %s", pilot_voltage, is_n12v ? "yes" : "no"); 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); evse_error_check(pilot_voltage, is_n12v);
// Só chama FSM, que decide tudo // ✅ 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( evse_fsm_process(
pilot_voltage, pilot_voltage,
evse_state_get_authorized(), evse_state_get_authorized(),
evse_config_is_available(), available,
evse_config_is_enabled() enabled);
);
evse_limits_check(); evse_limits_check();
evse_state_t current = evse_get_state(); if (evse_is_limit_reached())
if (current != last_state) { {
//ESP_LOGI(TAG, "State changed: %s → %s", evse_state_to_str(last_state), evse_state_to_str(current)); if (evse_state_get_authorized())
last_state = current; {
ESP_LOGW(TAG, "Charging limit reached → revoking authorization");
evse_state_set_authorized(false);
}
} }
evse_mark_error_cleared(); evse_state_t current = evse_get_state();
if (current != last_state)
{
last_state = current;
}
xSemaphoreGive(mutex); xSemaphoreGive(mutex);
} }
// ================================ static void evse_core_task(void *arg)
// Background Task {
// ================================ (void)arg;
while (true)
static void evse_core_task(void *arg) { {
while (true) {
evse_process(); evse_process();
vTaskDelay(pdMS_TO_TICKS(100)); // 10 Hz cycle vTaskDelay(pdMS_TO_TICKS(100));
} }
} }

View File

@@ -1,120 +1,310 @@
#include "evse_error.h" #include "evse_error.h"
#include "evse_config.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/portmacro.h"
#include "esp_log.h" #include "esp_log.h"
#include "ntc_sensor.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"; static const char *TAG = "evse_error";
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 visíveis foram limpos"
static bool error_cleared = false; static bool error_cleared = false;
void evse_error_init(void) { // Proteção contra concorrência
// Inicialização do sistema de erros static portMUX_TYPE error_mux = portMUX_INITIALIZER_UNLOCKED;
}
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v) { // ----------------------------------------------------
ESP_LOGD(TAG, "Verificando erro: pilot_voltage = %d, is_n12v = %s", // Helper: publicar evento de alteração de erro (visible_bits)
pilot_voltage, is_n12v ? "true" : "false"); // ----------------------------------------------------
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(),
};
// Falha elétrica geral no pilot esp_err_t err = esp_event_post(
if (pilot_voltage == PILOT_VOLTAGE_1) { EVSE_EVENTS,
if (!(error_bits & EVSE_ERR_PILOT_FAULT_BIT)) { // Verifica se o erro já foi registrado EVSE_EVENT_ERROR_CHANGED,
evse_error_set(EVSE_ERR_PILOT_FAULT_BIT); &ev,
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)"); sizeof(ev),
} portMAX_DELAY);
}
// Falta de -12V durante PWM (C ou D) if (err != ESP_OK)
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v) { {
if (!(error_bits & EVSE_ERR_DIODE_SHORT_BIT)) { // Verifica se o erro já foi registrado ESP_LOGW(TAG, "Falha ao publicar EVSE_EVENT_ERROR_CHANGED: %s",
evse_error_set(EVSE_ERR_DIODE_SHORT_BIT); esp_err_to_name(err));
ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)");
ESP_LOGW(TAG, "Verificando erro: pilot_voltage = %d, is_n12v = %s", pilot_voltage, is_n12v ? "true" : "false");
}
} }
} }
void evse_temperature_check(void) { // ----------------------------------------------------
float temp_c = ntc_temp_sensor(); // leitura atual (última medida válida) // Helpers internos
uint8_t threshold = evse_get_temp_threshold(); // padrão 60°C, configurável // ----------------------------------------------------
static bool raw_has_bit(uint32_t bit)
{
bool v;
portENTER_CRITICAL(&error_mux);
v = ((raw_bits & bit) != 0);
portEXIT_CRITICAL(&error_mux);
return v;
}
// Log informativo com os valores static void reconcile_visible_locked(TickType_t now)
ESP_LOGD(TAG, "Verificando temperatura: atual = %.2f °C, limite = %d °C", temp_c, threshold); {
// Se existem erros reais, o visível segue imediatamente
// Se a temperatura parecer inválida, aplica erro de sensor if (raw_bits != 0)
if (temp_c < -40.0f || temp_c > 150.0f) { {
if (!(error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT)) { // Verifica se o erro já foi registrado visible_bits = raw_bits;
evse_error_set(EVSE_ERR_TEMPERATURE_FAULT_BIT); clear_deadline = 0;
ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado"); error_cleared = false;
}
return; return;
} }
evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT); // leitura válida // raw_bits == 0
if (visible_bits == 0)
if (temp_c >= threshold) { {
if (!(error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT)) { // Verifica se o erro já foi registrado clear_deadline = 0;
evse_error_set(EVSE_ERR_TEMPERATURE_HIGH_BIT); return;
ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C", temp_c, threshold);
}
} else {
evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT);
} }
}
uint32_t evse_get_error(void) { // Ainda há erro visível (holdoff). Arma deadline 1x.
return error_bits; if (clear_deadline == 0)
} {
clear_deadline = now + pdMS_TO_TICKS(EVSE_ERROR_COOLDOWN_MS);
bool evse_is_error_cleared(void) { return;
return error_cleared;
}
void evse_mark_error_cleared(void) {
error_cleared = false;
}
// Já existentes
void evse_error_set(uint32_t bitmask) {
error_bits |= bitmask;
if (bitmask & EVSE_ERR_AUTO_CLEAR_BITS) {
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); // 60s
} }
}
void evse_error_clear(uint32_t bitmask) { // Expirou -> limpar finalmente
bool had_error = error_bits != 0; if ((int32_t)(now - clear_deadline) >= 0)
error_bits &= ~bitmask; {
visible_bits = 0;
if (had_error && error_bits == 0) { clear_deadline = 0;
error_cleared = true; error_cleared = true;
} }
} }
void evse_error_tick(void) { // ----------------------------------------------------
if ((error_bits & EVSE_ERR_AUTO_CLEAR_BITS) && xTaskGetTickCount() >= auto_clear_timeout) { // API pública
evse_error_clear(EVSE_ERR_AUTO_CLEAR_BITS); // ----------------------------------------------------
auto_clear_timeout = 0; 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);
} }
} }
bool evse_error_is_active(void) { uint32_t evse_get_error(void)
return error_bits != 0; {
portENTER_CRITICAL(&error_mux);
uint32_t val = visible_bits;
portEXIT_CRITICAL(&error_mux);
return val;
} }
uint32_t evse_error_get_bits(void) { bool evse_error_is_active(void)
return error_bits; {
return evse_get_error() != 0;
} }
void evse_error_reset_flag(void) { uint32_t evse_error_get_bits(void)
{
return evse_get_error();
}
bool evse_error_cleared_flag(void)
{
portENTER_CRITICAL(&error_mux);
bool v = error_cleared;
portEXIT_CRITICAL(&error_mux);
return v;
}
void evse_error_reset_flag(void)
{
portENTER_CRITICAL(&error_mux);
error_cleared = false; error_cleared = false;
portEXIT_CRITICAL(&error_mux);
} }
bool evse_error_cleared_flag(void) { void evse_error_set(uint32_t bitmask)
return error_cleared; {
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
old_vis = visible_bits;
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);
old_vis = visible_bits;
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);
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);
}
}
// ----------------------------------------------------
// Checks (raw -> set/clear)
// ----------------------------------------------------
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v)
{
ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s",
pilot_voltage, is_n12v ? "true" : "false");
// 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);
}
}
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)
{
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);
}
} }

View File

@@ -1,3 +1,4 @@
// components/evse/evse_fsm.c
#include "evse_fsm.h" #include "evse_fsm.h"
#include "evse_api.h" #include "evse_api.h"
#include "evse_pilot.h" #include "evse_pilot.h"
@@ -17,193 +18,245 @@ static const char *TAG = "evse_fsm";
#define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif #endif
static bool c1_d1_waiting = false; void evse_fsm_reset(void)
static TickType_t c1_d1_relay_to = 0; {
void evse_fsm_reset(void) {
evse_set_state(EVSE_STATE_A); evse_set_state(EVSE_STATE_A);
c1_d1_waiting = false;
c1_d1_relay_to = 0;
} }
// ... includes e defines como já estão /**
* @brief Atualiza saídas de hardware (pilot, relé, trava) em função do estado lógico.
static void update_outputs(evse_state_t state) { */
static void update_outputs(evse_state_t state)
{
const uint16_t current = evse_get_runtime_charging_current(); const uint16_t current = evse_get_runtime_charging_current();
uint8_t cable_max_current = evse_get_max_charging_current(); uint8_t cable_max_current = evse_get_max_charging_current();
const bool socket_outlet = evse_get_socket_outlet(); const bool socket_outlet = evse_get_socket_outlet();
if (socket_outlet) { if (socket_outlet)
{
cable_max_current = proximity_get_max_current(); 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 (evse_get_error() != 0)
if (ac_relay_get_state()) { {
if (ac_relay_get_state())
{
ac_relay_set_state(false); ac_relay_set_state(false);
ESP_LOGW(TAG, "ERRO ativo: relé estava ligado, agora desligado por segurança!"); ESP_LOGW(TAG, "ERRO ativo: relé estava ligado, agora desligado por segurança!");
} }
ac_relay_set_state(false); // redundância tolerável else
pilot_set_level(true); // sinal pilot sempre 12V (A) {
if (board_config.socket_lock && socket_outlet) { 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); socket_lock_set_locked(false);
} }
return; return;
} }
// Fluxo normal switch (state)
switch (state) { {
case EVSE_STATE_A: case EVSE_STATE_A:
case EVSE_STATE_E: case EVSE_STATE_E:
case EVSE_STATE_F: case EVSE_STATE_F:
ac_relay_set_state(false); ac_relay_set_state(false);
pilot_set_level(state == EVSE_STATE_A);
if (board_config.socket_lock && socket_outlet) {
socket_lock_set_locked(false);
}
break;
case EVSE_STATE_B1: // A → pilot alto (+12V), E/F → pilot OFF
pilot_set_level(true); pilot_set_level(state == EVSE_STATE_A);
ac_relay_set_state(false);
if (board_config.socket_lock && socket_outlet) {
socket_lock_set_locked(true);
}
if (rcm_test()) { if (board_config.socket_lock && socket_outlet)
//ESP_LOGI(TAG, "RCM self test passed"); {
} else { socket_lock_set_locked(false);
//ESP_LOGW(TAG, "RCM self test failed");
}
break;
case EVSE_STATE_B2:
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(false);
break;
case EVSE_STATE_C1:
case EVSE_STATE_D1: {
pilot_set_amps(MIN(current, cable_max_current)); // mantém PWM
ac_relay_set_state(false); // relé ainda desligado
c1_d1_waiting = true;
c1_d1_relay_to = xTaskGetTickCount() + pdMS_TO_TICKS(6000);
break;
} }
case EVSE_STATE_C2: break;
case EVSE_STATE_D2:
pilot_set_amps(MIN(current, cable_max_current)); case EVSE_STATE_B1:
ac_relay_set_state(true); // Só chega aqui se não há erro! pilot_set_level(true);
break; ac_relay_set_state(false);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
(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);
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 - centraliza a lógica de erro e de todos os estados /**
* @brief Máquina de estados principal do EVSE (IEC 61851).
*/
void evse_fsm_process( void evse_fsm_process(
pilot_voltage_t pilot_voltage, pilot_voltage_t pilot_voltage,
bool authorized, bool authorized,
bool available, bool available,
bool enabled bool enabled)
) { {
// Proteção total: erro força F sempre! // 1) Erros globais: dominam qualquer outra lógica
if (evse_get_error() != 0) { uint32_t err_bits = evse_get_error();
if (evse_get_state() != EVSE_STATE_F) { if (err_bits != 0)
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado FAULT (F)"); {
evse_set_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 %s",
evse_state_to_str(forced_state));
evse_set_state(forced_state);
} }
update_outputs(EVSE_STATE_F);
update_outputs(forced_state);
return; return;
} }
TickType_t now = xTaskGetTickCount(); evse_state_t curr = evse_get_state();
evse_state_t prev = evse_get_state();
evse_state_t curr = prev;
switch (curr) { switch (curr)
case EVSE_STATE_A: {
if (!available) { case EVSE_STATE_A:
evse_set_state(EVSE_STATE_F); if (!available)
} else if (pilot_voltage == PILOT_VOLTAGE_9) { {
evse_set_state(EVSE_STATE_B1); evse_set_state(EVSE_STATE_F);
} }
else if (pilot_voltage == PILOT_VOLTAGE_9)
{
evse_set_state(EVSE_STATE_B1);
}
break;
case EVSE_STATE_B1:
case EVSE_STATE_B2:
if (!available)
{
evse_set_state(EVSE_STATE_F);
break;
}
switch (pilot_voltage)
{
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break; break;
case EVSE_STATE_B1: case PILOT_VOLTAGE_9:
case EVSE_STATE_B2: evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
if (!available) {
evse_set_state(EVSE_STATE_F);
break;
}
switch (pilot_voltage) {
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break;
case PILOT_VOLTAGE_9:
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
break;
case PILOT_VOLTAGE_6:
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
break;
default:
break;
}
break; break;
case EVSE_STATE_C1: case PILOT_VOLTAGE_6:
case EVSE_STATE_D1: evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
if (c1_d1_waiting && now >= c1_d1_relay_to) {
ac_relay_set_state(false);
c1_d1_waiting = false;
if (!available) {
evse_set_state(EVSE_STATE_F);
break;
}
}
__attribute__((fallthrough));
case EVSE_STATE_C2:
case EVSE_STATE_D2:
if (!enabled || !available) {
evse_set_state((curr == EVSE_STATE_D2 || curr == EVSE_STATE_D1)
? EVSE_STATE_D1 : EVSE_STATE_C1);
break;
}
switch (pilot_voltage) {
case PILOT_VOLTAGE_6:
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
break;
case PILOT_VOLTAGE_3:
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
break;
case PILOT_VOLTAGE_9:
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
break;
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break;
default:
break;
}
break; break;
case EVSE_STATE_E: case PILOT_VOLTAGE_3:
// Estado elétrico grave: só reset manual evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
break; break;
case EVSE_STATE_F: default:
// Fault: só sai se disponível e sem erro break;
if (available && evse_get_error() == 0) { }
evse_set_state(EVSE_STATE_A); break;
case EVSE_STATE_C1:
case EVSE_STATE_D1:
case EVSE_STATE_C2:
case EVSE_STATE_D2:
if (!available)
{
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; 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:
// ✅ 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:
if (available && evse_get_error() == 0)
{
evse_set_state(EVSE_STATE_A);
}
break;
} }
evse_state_t next = evse_get_state(); evse_state_t next = evse_get_state();
update_outputs(next); update_outputs(next);
if (next != prev) {
ESP_LOGI(TAG, "State changed: %s -> %s",
evse_state_to_str(prev),
evse_state_to_str(next));
}
} }

View File

@@ -1,49 +1,125 @@
#include <inttypes.h> // for PRIu32 #include <inttypes.h>
#include <stdbool.h>
#include "evse_state.h" #include "evse_state.h"
#include "evse_api.h" #include "evse_api.h"
#include "evse_limits.h" #include "evse_limits.h"
#include "evse_meter.h" #include "evse_meter.h"
#include "evse_session.h" #include "evse_session.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_err.h"
#include "esp_check.h"
#include "storage_service.h"
// ======================== #define NVS_NAMESPACE "evse_limits"
// External state references static const char *TAG = "evse_limits";
// ========================
//extern evse_state_t current_state; // Current EVSE FSM state
//extern TickType_t session_start_tick; // Timestamp of charging session start
// ========================
// Concurrency protection
// ========================
static portMUX_TYPE evse_mux = portMUX_INITIALIZER_UNLOCKED; static portMUX_TYPE evse_mux = portMUX_INITIALIZER_UNLOCKED;
// ======================== static bool limit_reached = false;
// Runtime state (volatile) 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 inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
static uint32_t consumption_limit = 0; // Energy limit in Wh static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(1000); }
static uint32_t charging_time_limit = 0; // Time limit in seconds
static uint16_t under_power_limit = 0; // Minimum acceptable power in W
// ======================== // ---------------------------------
// Default (persistent) limits // 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;
}
static uint32_t default_consumption_limit = 0; void evse_limits_check_defaults(void)
static uint32_t default_charging_time_limit = 0; {
static uint16_t default_under_power_limit = 0; esp_err_t err;
bool needs_flush = false;
// ======================== uint32_t u32 = 0;
// Limit status flag uint16_t u16 = 0;
// ========================
bool evse_get_limit_reached(void) { 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; bool val;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
val = limit_reached; val = limit_reached;
@@ -51,17 +127,23 @@ bool evse_get_limit_reached(void) {
return val; return val;
} }
void evse_set_limit_reached(bool v) { void evse_set_limit_reached(bool v)
{
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
limit_reached = v; limit_reached = v;
portEXIT_CRITICAL(&evse_mux); portEXIT_CRITICAL(&evse_mux);
} }
// ======================== bool evse_is_limit_reached(void)
// Runtime limit accessors {
// ======================== return evse_get_limit_reached();
}
uint32_t evse_get_consumption_limit(void) { // ---------------------------------
// Consumption limit
// ---------------------------------
uint32_t evse_get_consumption_limit(void)
{
uint32_t val; uint32_t val;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
val = consumption_limit; val = consumption_limit;
@@ -69,13 +151,35 @@ uint32_t evse_get_consumption_limit(void) {
return val; return val;
} }
void evse_set_consumption_limit(uint32_t value) { void evse_set_consumption_limit(uint32_t value)
{
bool changed = false;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
consumption_limit = value; if (consumption_limit != value)
{
consumption_limit = value;
changed = true;
}
portEXIT_CRITICAL(&evse_mux); portEXIT_CRITICAL(&evse_mux);
if (!changed)
return;
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", value);
if (err != ESP_OK)
{
ESP_LOGE(TAG,
"Failed to persist consumption limit (%" PRIu32 " Wh): %s",
value, esp_err_to_name(err));
}
} }
uint32_t evse_get_charging_time_limit(void) { // ---------------------------------
// Charging time limit
// ---------------------------------
uint32_t evse_get_charging_time_limit(void)
{
uint32_t val; uint32_t val;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
val = charging_time_limit; val = charging_time_limit;
@@ -83,13 +187,35 @@ uint32_t evse_get_charging_time_limit(void) {
return val; return val;
} }
void evse_set_charging_time_limit(uint32_t value) { void evse_set_charging_time_limit(uint32_t value)
{
bool changed = false;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
charging_time_limit = value; if (charging_time_limit != value)
{
charging_time_limit = value;
changed = true;
}
portEXIT_CRITICAL(&evse_mux); portEXIT_CRITICAL(&evse_mux);
if (!changed)
return;
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", value);
if (err != ESP_OK)
{
ESP_LOGE(TAG,
"Failed to persist charging time limit (%" PRIu32 " s): %s",
value, esp_err_to_name(err));
}
} }
uint16_t evse_get_under_power_limit(void) { // ---------------------------------
// Under-power limit
// ---------------------------------
uint16_t evse_get_under_power_limit(void)
{
uint16_t val; uint16_t val;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
val = under_power_limit; val = under_power_limit;
@@ -97,92 +223,79 @@ uint16_t evse_get_under_power_limit(void) {
return val; return val;
} }
void evse_set_under_power_limit(uint16_t value) { void evse_set_under_power_limit(uint16_t value)
{
bool changed = false;
portENTER_CRITICAL(&evse_mux); portENTER_CRITICAL(&evse_mux);
under_power_limit = value; if (under_power_limit != value)
portEXIT_CRITICAL(&evse_mux); {
} under_power_limit = value;
changed = true;
// ========================
// Default (persistent) limit accessors
// These values can be stored/restored via NVS
// ========================
uint32_t evse_get_default_consumption_limit(void) {
return default_consumption_limit;
}
void evse_set_default_consumption_limit(uint32_t value) {
default_consumption_limit = value;
}
uint32_t evse_get_default_charging_time_limit(void) {
return default_charging_time_limit;
}
void evse_set_default_charging_time_limit(uint32_t value) {
default_charging_time_limit = value;
}
uint16_t evse_get_default_under_power_limit(void) {
return default_under_power_limit;
}
void evse_set_default_under_power_limit(uint16_t value) {
default_under_power_limit = value;
}
bool evse_is_limit_reached(void) {
return evse_get_limit_reached();
}
// ========================
// Limit checking logic
// This function must be called periodically while charging.
// It will flag the session as "limit reached" when thresholds are violated.
// ========================
void evse_limits_check(void) {
// Only check during an active charging session
if (!evse_state_is_charging(evse_get_state())) {
return;
} }
portEXIT_CRITICAL(&evse_mux);
if (!changed)
return;
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", value);
if (err != ESP_OK)
{
ESP_LOGE(TAG,
"Failed to persist under-power limit (%" PRIu32 " W): %s",
(uint32_t)value, esp_err_to_name(err));
}
}
// ---------------------------------
// Runtime check
// ---------------------------------
void evse_limits_check(void)
{
// Só faz sentido quando há energia ativa (C2/D2)
if (!evse_state_is_charging(evse_get_state()))
return;
evse_session_t sess; evse_session_t sess;
// Retrieve accumulated data for the current session if (!evse_session_get(&sess) || !sess.is_current)
if (!evse_session_get(&sess) || !sess.is_current) {
// If there's no active session, abort
return; 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; bool reached = false;
// 1) Energy consumption limit (Wh) if (cons_lim > 0 && sess.energy_wh >= cons_lim)
if (consumption_limit > 0 && sess.energy_wh >= consumption_limit) { {
ESP_LOGW("EVSE_LIMITS", ESP_LOGW(TAG, "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
"Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh", sess.energy_wh, cons_lim);
sess.energy_wh, consumption_limit);
reached = true; reached = true;
} }
// 2) Charging time limit (seconds) if (time_lim > 0 && sess.duration_s >= time_lim)
if (charging_time_limit > 0 && sess.duration_s >= charging_time_limit) { {
ESP_LOGW("EVSE_LIMITS", ESP_LOGW(TAG, "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
"Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s", sess.duration_s, time_lim);
sess.duration_s, charging_time_limit);
reached = true; reached = true;
} }
// 3) Under-power limit (instantaneous power) int32_t p = evse_meter_get_instant_power();
uint32_t inst_power = evse_meter_get_instant_power(); uint32_t inst_power = (p > 0) ? (uint32_t)p : 0;
if (under_power_limit > 0 && inst_power < under_power_limit) {
ESP_LOGW("EVSE_LIMITS", if (unp_lim > 0 && inst_power < (uint32_t)unp_lim)
"Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W", {
(uint32_t)inst_power, ESP_LOGW(TAG, "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
(uint32_t)under_power_limit); inst_power, (uint32_t)unp_lim);
reached = true; reached = true;
} }
if (reached) { if (reached)
evse_set_limit_reached(true); evse_set_limit_reached(true);
} }
}

View File

@@ -1,4 +1,3 @@
// === Início de: components/evse/evse_manager.c ===
#include "evse_manager.h" #include "evse_manager.h"
#include "evse_state.h" #include "evse_state.h"
#include "evse_error.h" #include "evse_error.h"
@@ -7,69 +6,124 @@
#include "evse_api.h" #include "evse_api.h"
#include "evse_meter.h" #include "evse_meter.h"
#include "evse_session.h" #include "evse_session.h"
#include "evse_config.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "freertos/queue.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h"
#include "esp_err.h"
#include <string.h> #include <string.h>
#include <inttypes.h> #include <inttypes.h>
#include "auth_events.h" #include "auth_events.h"
#include "loadbalancer_events.h" #include "loadbalancer_events.h"
#include "ocpp_events.h" #include "ocpp_events.h"
#include "esp_event.h" #include "scheduler_events.h"
static const char *TAG = "EVSE_Manager"; static const char *TAG = "EVSE_Manager";
static SemaphoreHandle_t evse_mutex; static SemaphoreHandle_t evse_mutex;
// ✅ Proteção para flags partilhadas (event handlers vs task)
static portMUX_TYPE s_mgr_mux = portMUX_INITIALIZER_UNLOCKED;
static bool auth_enabled = false; static bool auth_enabled = false;
// Estado de pausa controlado pelo Load Balancer // Estado de pausa controlado pelo Load Balancer
static bool lb_paused = false; static bool lb_paused = false;
static bool lb_prev_authorized = false; static bool lb_prev_authorized = false;
#define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo // Estado de janela do scheduler
static bool s_sched_allowed = true;
static portMUX_TYPE s_sched_mux = portMUX_INITIALIZER_UNLOCKED;
// ================= Helpers internos ================= #define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo
static void lb_clear_pause_state(void) static void lb_clear_pause_state(void)
{ {
portENTER_CRITICAL(&s_mgr_mux);
lb_paused = false; lb_paused = false;
lb_prev_authorized = false; lb_prev_authorized = false;
portEXIT_CRITICAL(&s_mgr_mux);
}
bool evse_sched_is_allowed(void)
{
bool v;
portENTER_CRITICAL(&s_sched_mux);
v = s_sched_allowed;
portEXIT_CRITICAL(&s_sched_mux);
return v;
} }
static void evse_manager_handle_auth_on_tick(void) static void evse_manager_handle_auth_on_tick(void)
{ {
if (auth_enabled) bool sched_allowed = evse_sched_is_allowed();
uint32_t err_bits = evse_get_error(); // inclui holdoff interno
bool has_error = (err_bits != 0);
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) if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A)
{ {
ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization."); ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization.");
evse_state_set_authorized(false); evse_state_set_authorized(false);
// Desconexão física invalida qualquer pausa pendente do LB
lb_clear_pause_state(); lb_clear_pause_state();
} }
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.");
evse_state_set_authorized(false);
}
} }
else else
{ {
// Se autenticação está desativada, garante autorização sempre ativa bool limit_hit = evse_is_limit_reached();
if (!evse_state_get_authorized()) 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())
{
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);
}
if (!local_lb_paused && sched_allowed && can_operate &&
!has_error && !limit_hit &&
!evse_state_get_authorized())
{ {
evse_state_set_authorized(true); evse_state_set_authorized(true);
ESP_LOGI(TAG, "Authentication disabled → forced authorization."); ESP_LOGI(TAG, "Authentication disabled → forced authorization (schedule ok, no error/limits).");
// Em modo OPEN, pausa do LB não é tão relevante, mas limpamos mesmo assim
lb_clear_pause_state(); lb_clear_pause_state();
} }
} }
} }
// ===== Task de ciclo principal =====
static void evse_manager_task(void *arg) static void evse_manager_task(void *arg)
{ {
(void)arg;
while (true) while (true)
{ {
evse_manager_tick(); evse_manager_tick();
@@ -79,10 +133,8 @@ static void evse_manager_task(void *arg)
static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *data) static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{ {
if (base != AUTH_EVENTS || !data) (void)arg;
return; if (base != AUTH_EVENTS || !data) return;
auth_mode_t g_mode = AUTH_MODE_OPEN;
switch (id) switch (id)
{ {
@@ -91,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; 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"); ESP_LOGI(TAG, "Tag %s -> %s", evt->tag, evt->authorized ? "AUTHORIZED" : "DENIED");
evse_state_set_authorized(evt->authorized); evse_state_set_authorized(evt->authorized);
// Qualquer alteração explícita de auth invalida pausa do LB
lb_clear_pause_state(); lb_clear_pause_state();
break; break;
} }
@@ -101,122 +151,110 @@ static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *da
case AUTH_EVENT_INIT: case AUTH_EVENT_INIT:
{ {
const auth_mode_event_data_t *evt = (const auth_mode_event_data_t *)data; 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(evt->mode));
ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(g_mode));
if (g_mode == AUTH_MODE_OPEN)
{
evse_state_set_authorized(true);
auth_enabled = false;
}
else
{
evse_state_set_authorized(false);
auth_enabled = true;
}
// 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(); lb_clear_pause_state();
break; break;
} }
} }
} }
// ===== Tratador de eventos de loadbalancer =====
static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base, static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) int32_t event_id, void *event_data)
{ {
(void)handler_arg;
(void)event_base;
if (!event_data) return;
if (event_id == LOADBALANCER_EVENT_INIT || event_id == LOADBALANCER_EVENT_STATE_CHANGED) if (event_id == LOADBALANCER_EVENT_INIT || event_id == LOADBALANCER_EVENT_STATE_CHANGED)
{ {
const loadbalancer_state_event_t *evt = (const loadbalancer_state_event_t *)event_data; const loadbalancer_state_event_t *evt = (const loadbalancer_state_event_t *)event_data;
ESP_LOGI(TAG, "Loadbalancer %s (ts: %lld)", ESP_LOGI(TAG, "Loadbalancer %s (ts: %lld)",
evt->enabled ? "ENABLED" : "DISABLED", evt->enabled ? "ENABLED" : "DISABLED",
(long long)evt->timestamp_us); (long long)evt->timestamp_us);
// Ações adicionais podem ser adicionadas aqui conforme necessário
} }
else if (event_id == LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT) else if (event_id == LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT)
{ {
const loadbalancer_master_limit_event_t *evt = const loadbalancer_master_limit_event_t *evt =
(const loadbalancer_master_limit_event_t *)event_data; (const loadbalancer_master_limit_event_t *)event_data;
ESP_LOGI(TAG, ESP_LOGI(TAG, "Novo limite de corrente (master): %u A (ts: %lld)",
"Novo limite de corrente (master): %u A (ts: %lld)",
evt->max_current, (long long)evt->timestamp_us); evt->max_current, (long long)evt->timestamp_us);
if (evt->max_current == 0) if (evt->max_current == 0)
{ {
// Suspensão por LB (não interessa se é OPEN ou RFID/OCPP) bool prev_auth = evse_state_get_authorized();
lb_paused = true;
lb_prev_authorized = 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)"); ESP_LOGI(TAG, "[LB] limit=0A → pausando sessão (authorized=false)");
evse_state_set_authorized(false); evse_state_set_authorized(false);
} }
else
{
ESP_LOGD(TAG, "[LB] limit=0A → já não estava autorizado");
}
} }
else else
{ {
// Ajusta corrente em runtime
evse_set_runtime_charging_current(evt->max_current); evse_set_runtime_charging_current(evt->max_current);
if (lb_paused) bool was_paused;
{ bool prev_auth;
lb_paused = false;
// Só retomamos se EVSE estiver operacional 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 = bool can_resume =
(evse_get_error() == 0) && (evse_get_error() == 0) &&
evse_config_is_available() && evse_config_is_available() &&
evse_config_is_enabled(); evse_config_is_enabled() &&
evse_sched_is_allowed() &&
!evse_is_limit_reached();
if (!can_resume) if (!can_resume)
{ {
ESP_LOGW(TAG, ESP_LOGW(TAG,
"[LB] limit=%uA → não retoma automaticamente (erro/indisponível/desabilitado)", "[LB] limit=%uA → não retoma automaticamente (erro/indisp/desab/fora de horário/limite)",
evt->max_current); evt->max_current);
lb_clear_pause_state(); lb_clear_pause_state();
return; 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 ESP_LOGI(TAG, "[LB] limit=%uA → modo OPEN, reautorizando", evt->max_current);
ESP_LOGI(TAG,
"[LB] limit=%uA → modo OPEN, reautorizando (authorized=true)",
evt->max_current);
evse_state_set_authorized(true); evse_state_set_authorized(true);
} }
else else
{ {
// RFID/OCPP: só retoma se havia autorização antes da pausa if (prev_auth)
if (lb_prev_authorized)
{ {
ESP_LOGI(TAG, ESP_LOGI(TAG, "[LB] limit=%uA → RFID/OCPP, retomando autorização anterior", evt->max_current);
"[LB] limit=%uA → RFID/OCPP, retomando autorização anterior (auto-resume)",
evt->max_current);
evse_state_set_authorized(true); 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; lb_prev_authorized = false;
} portEXIT_CRITICAL(&s_mgr_mux);
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);
} }
} }
} }
@@ -224,8 +262,8 @@ static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base
static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *data) static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{ {
if (base != OCPP_EVENTS) (void)arg;
return; if (base != OCPP_EVENTS) return;
switch (id) switch (id)
{ {
@@ -236,13 +274,10 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
break; break;
case OCPP_EVENT_AUTH_REJECTED: 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: 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); evse_state_set_authorized(false);
lb_clear_pause_state(); lb_clear_pause_state();
break; break;
@@ -253,25 +288,11 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
lb_clear_pause_state(); lb_clear_pause_state();
break; 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: case OCPP_EVENT_START_TX:
ESP_LOGI(TAG, "[OCPP] StartTx"); ESP_LOGI(TAG, "[OCPP] StartTx");
// StartTx em si não precisa mexer em auth, mas limpamos estado de pausa por segurança
lb_clear_pause_state(); lb_clear_pause_state();
break; 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: case OCPP_EVENT_OPERATIVE_UPDATED:
{ {
if (!data) if (!data)
@@ -283,10 +304,7 @@ 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", ESP_LOGI(TAG, "[OCPP] OperativeUpdated: operative=%d ts=%lld",
(int)ev->operative, (long long)ev->timestamp_us); (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); evse_config_set_enabled(ev->operative);
// Opcional: poderias também limpar a pausa aqui, dependendo da política
// lb_clear_pause_state();
break; break;
} }
@@ -296,12 +314,37 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
} }
} }
// ===== Inicialização ===== 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;
const sched_event_state_t *ev = (const sched_event_state_t *)data;
portENTER_CRITICAL(&s_sched_mux);
s_sched_allowed = ev->allowed_now;
portEXIT_CRITICAL(&s_sched_mux);
ESP_LOGI(TAG, "[SCHED] allowed_now=%d", (int)ev->allowed_now);
if (!ev->allowed_now && evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed → stopping session (authorized=false)");
evse_state_set_authorized(false);
}
}
void evse_manager_init(void) void evse_manager_init(void)
{ {
evse_mutex = xSemaphoreCreateMutex(); evse_mutex = xSemaphoreCreateMutex();
configASSERT(evse_mutex != NULL);
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_config_init();
evse_error_init(); evse_error_init();
evse_hardware_init(); evse_hardware_init();
evse_state_init(); evse_state_init();
@@ -311,12 +354,14 @@ void evse_manager_init(void)
ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, ESP_EVENT_ANY_ID, &on_auth_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, ESP_EVENT_ANY_ID, &on_auth_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, ESP_EVENT_ANY_ID, &on_loadbalancer_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, ESP_EVENT_ANY_ID, &on_loadbalancer_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(OCPP_EVENTS, ESP_EVENT_ANY_ID, &on_ocpp_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(OCPP_EVENTS, ESP_EVENT_ANY_ID, &on_ocpp_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(SCHED_EVENTS, ESP_EVENT_ANY_ID, &on_sched_event, NULL));
ESP_LOGI(TAG, "EVSE Manager inicializado."); ESP_LOGI(TAG, "EVSE Manager inicializado.");
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) void evse_manager_tick(void)
{ {
xSemaphoreTake(evse_mutex, portMAX_DELAY); xSemaphoreTake(evse_mutex, portMAX_DELAY);
@@ -331,4 +376,3 @@ void evse_manager_tick(void)
xSemaphoreGive(evse_mutex); xSemaphoreGive(evse_mutex);
} }
// === Fim de: components/evse/evse_manager.c ===

View File

@@ -48,7 +48,7 @@ void evse_meter_on_meter_event(void *arg, void *event_data)
meter_data.energy_wh = (uint32_t)(evt->total_energy); meter_data.energy_wh = (uint32_t)(evt->total_energy);
xSemaphoreGive(meter_mutex); xSemaphoreGive(meter_mutex);
ESP_LOGI(TAG, ESP_LOGD(TAG,
"Meter updated: power[W]={%" PRIu32 ",%" PRIu32 ",%" PRIu32 "}, " "Meter updated: power[W]={%" PRIu32 ",%" PRIu32 ",%" PRIu32 "}, "
"voltage[V]={%.2f,%.2f,%.2f}, " "voltage[V]={%.2f,%.2f,%.2f}, "
"current[A]={%.2f,%.2f,%.2f}, " "current[A]={%.2f,%.2f,%.2f}, "

View File

@@ -1,8 +1,7 @@
// components/evse/evse_pilot.c
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <math.h>
#include <string.h>
#include "driver/ledc.h" #include "driver/ledc.h"
#include "esp_err.h" #include "esp_err.h"
@@ -13,176 +12,232 @@
#include "adc121s021_dma.h" #include "adc121s021_dma.h"
#include "board_config.h" #include "board_config.h"
#define PILOT_PWM_TIMER LEDC_TIMER_0 #define PILOT_PWM_TIMER LEDC_TIMER_0
#define PILOT_PWM_CHANNEL LEDC_CHANNEL_0 #define PILOT_PWM_CHANNEL LEDC_CHANNEL_0
#define PILOT_PWM_SPEED_MODE LEDC_LOW_SPEED_MODE #define PILOT_PWM_SPEED_MODE LEDC_LOW_SPEED_MODE
#define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT #define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT
#define PILOT_PWM_MAX_DUTY 1023 #define PILOT_PWM_MAX_DUTY 1023
#define NUM_PILOT_SAMPLES 100 // --- Configuração de amostragem do Pilot ---
#define MAX_SAMPLE_ATTEMPTS 1000 #define NUM_PILOT_SAMPLES 100
#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior #define MAX_SAMPLE_ATTEMPTS 1000
// ADC121S021 setup #define PILOT_SAMPLE_DELAY_US 10
#define ADC121_VREF_MV 3300 // AJUSTE conforme Vref do seu hardware!
#define ADC121_MAX 4095 // 12 bits // 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 const char *TAG = "evse_pilot";
// Memoização de estado para evitar comandos/logs desnecessários typedef enum {
static int last_pilot_level = -1; PILOT_MODE_DC_HIGH = 0, // +12V (nível alto)
static uint32_t last_pwm_duty = 0; PILOT_MODE_DC_LOW, // nível baixo / pilot desligado (dependente do hardware)
PILOT_MODE_PWM // PWM ativo
} pilot_mode_t;
// Função para converter leitura bruta do ADC para mV static pilot_mode_t s_mode = PILOT_MODE_DC_LOW;
static int adc_raw_to_mv(uint16_t raw) { static uint32_t last_pwm_duty = 0;
return (raw * ADC121_VREF_MV) / ADC121_MAX;
// ---------------------
// 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) void pilot_init(void)
{ {
// PWM (LEDC) configuração // Configura timer do PWM do Pilot (1 kHz)
ledc_timer_config_t ledc_timer = { ledc_timer_config_t ledc_timer = {
.speed_mode = PILOT_PWM_SPEED_MODE, .speed_mode = PILOT_PWM_SPEED_MODE,
.timer_num = PILOT_PWM_TIMER, .timer_num = PILOT_PWM_TIMER,
.duty_resolution = PILOT_PWM_DUTY_RES, .duty_resolution = PILOT_PWM_DUTY_RES,
.freq_hz = 1000, .freq_hz = 1000, // 1 kHz (IEC 61851)
.clk_cfg = LEDC_AUTO_CLK .clk_cfg = LEDC_AUTO_CLK
}; };
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Canal do PWM no pino configurado em board_config
ledc_channel_config_t ledc_channel = { ledc_channel_config_t ledc_channel = {
.speed_mode = PILOT_PWM_SPEED_MODE, .speed_mode = PILOT_PWM_SPEED_MODE,
.channel = PILOT_PWM_CHANNEL, .channel = PILOT_PWM_CHANNEL,
.timer_sel = PILOT_PWM_TIMER, .timer_sel = PILOT_PWM_TIMER,
.intr_type = LEDC_INTR_DISABLE, .intr_type = LEDC_INTR_DISABLE,
.gpio_num = board_config.pilot_pwm_gpio, .gpio_num = board_config.pilot_pwm_gpio,
.duty = 0, .duty = 0,
.hpoint = 0 .hpoint = 0
}; };
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0));
//ESP_ERROR_CHECK(ledc_fade_func_install(0));
// Inicializa ADC121S021 externo // 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(); 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; // só muda se necessário pilot_mode_t target = high ? PILOT_MODE_DC_HIGH : PILOT_MODE_DC_LOW;
last_pilot_level = level;
ESP_LOGI(TAG, "Set level %d", level); // Se já estiver no modo DC desejado e sem PWM ativo, ignora
ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, level ? 1 : 0); if (s_mode == target && last_pwm_duty == 0) {
last_pwm_duty = 0; // PWM parado 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) 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: 680 A)", amps); ESP_LOGE(TAG, "Invalid ampere value: %d A (valid: 680 A)", amps);
return; return;
} }
uint32_t duty_percent; uint32_t duty_percent;
if (amps <= 51)
if (amps <= 51) { {
duty_percent = (amps * 10) / 6; // Duty (%) = Amps / 0.6 duty_percent = (amps * 10) / 6;
} else { }
duty_percent = (amps * 10) / 25 + 64; // Duty (%) = (Amps / 2.5) + 64 else
{
duty_percent = (amps * 10) / 25 + 64;
} }
if (duty_percent > 100) duty_percent = 100; if (duty_percent > 100) duty_percent = 100;
uint32_t duty = (PILOT_PWM_MAX_DUTY * duty_percent) / 100; uint32_t duty = (PILOT_PWM_MAX_DUTY * duty_percent) / 100;
if (last_pilot_level == 0 && last_pwm_duty == duty) return; // Se já estiver em PWM com o mesmo duty, ignora
last_pilot_level = 0; if (s_mode == PILOT_MODE_PWM && last_pwm_duty == duty) {
return;
}
s_mode = PILOT_MODE_PWM;
last_pwm_duty = duty; 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); amps, (int)duty, PILOT_PWM_MAX_DUTY, (int)duty_percent);
ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty); ESP_ERROR_CHECK(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_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL));
} }
bool pilot_get_state(void) { bool pilot_get_state(void)
// true se estamos em 12V fixo; false se PWM ou -12V {
// Se quiser diferenciar PWM, guarde um flag quando chamar set_amps. // "Alto" significa DC +12V (estado A). PWM não conta como DC high.
return (last_pilot_level == 1) && (last_pwm_duty == 0); return (s_mode == PILOT_MODE_DC_HIGH);
}
static int compare_int(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
static int select_low_median_qsort(int *src, int n, int percent) {
int k = (n * percent) / 100;
if (k == 0) k = 1;
int *copy = alloca(n * sizeof(int));
memcpy(copy, src, n * sizeof(int));
qsort(copy, n, sizeof(int), compare_int);
return copy[k / 2];
}
static int select_high_median_qsort(int *src, int n, int percent) {
int k = (n * percent) / 100;
if (k == 0) k = 1;
int *copy = alloca(n * sizeof(int));
memcpy(copy, src, n * sizeof(int));
qsort(copy, n, sizeof(int), compare_int);
return copy[n - k + (k / 2)];
} }
// ---------------------
// Medição do sinal de Pilot (PWM 1 kHz J1772)
// ---------------------
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12) void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12)
{ {
ESP_LOGD(TAG, "pilot_measure"); ESP_LOGD(TAG, "pilot_measure");
int samples[NUM_PILOT_SAMPLES]; uint16_t samples[NUM_PILOT_SAMPLES];
int collected = 0, attempts = 0; int collected = 0;
uint16_t adc_sample = 0; int attempts = 0;
// Lê samples usando ADC121S021 externo while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS)
while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS) { {
adc_sample = 0; uint16_t adc_sample;
if (adc121s021_dma_get_sample(&adc_sample)) {
if (adc121s021_dma_get_sample(&adc_sample))
{
samples[collected++] = adc_sample; samples[collected++] = adc_sample;
esp_rom_delay_us(10); esp_rom_delay_us(PILOT_SAMPLE_DELAY_US);
} else { }
else
{
esp_rom_delay_us(100); esp_rom_delay_us(100);
attempts++; attempts++;
} }
} }
if (collected < NUM_PILOT_SAMPLES) { if (collected < NUM_PILOT_SAMPLES)
{
ESP_LOGW(TAG, "Timeout on sample read (%d/%d)", collected, NUM_PILOT_SAMPLES); ESP_LOGW(TAG, "Timeout on sample read (%d/%d)", collected, NUM_PILOT_SAMPLES);
*up_voltage = PILOT_VOLTAGE_1; *up_voltage = PILOT_VOLTAGE_1;
*down_voltage_n12 = false; *down_voltage_n12 = false;
return; return;
} }
int high_raw = select_high_median_qsort(samples, collected, PILOT_EXTREME_PERCENT); // Ordena as amostras para eliminar extremos (ruído/espúrios)
int low_raw = select_low_median_qsort(samples, collected, PILOT_EXTREME_PERCENT); qsort(samples, collected, sizeof(uint16_t), compare_uint16);
int k = (collected * PILOT_EXTREME_PERCENT) / 100;
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 - 1 - (k / 2);
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 high_mv = adc_raw_to_mv(high_raw);
int low_mv = adc_raw_to_mv(low_raw); int low_mv = adc_raw_to_mv(low_raw);
// Aplica thresholds definidos em board_config (em mV) // Determina o nível positivo (+12, +9, +6, +3 ou <3 V)
if (high_mv >= board_config.pilot_down_threshold_12) if (high_mv >= board_config.pilot_down_threshold_12)
{
*up_voltage = PILOT_VOLTAGE_12; *up_voltage = PILOT_VOLTAGE_12;
}
else if (high_mv >= board_config.pilot_down_threshold_9) else if (high_mv >= board_config.pilot_down_threshold_9)
{
*up_voltage = PILOT_VOLTAGE_9; *up_voltage = PILOT_VOLTAGE_9;
}
else if (high_mv >= board_config.pilot_down_threshold_6) else if (high_mv >= board_config.pilot_down_threshold_6)
{
*up_voltage = PILOT_VOLTAGE_6; *up_voltage = PILOT_VOLTAGE_6;
}
else if (high_mv >= board_config.pilot_down_threshold_3) else if (high_mv >= board_config.pilot_down_threshold_3)
{
*up_voltage = PILOT_VOLTAGE_3; *up_voltage = PILOT_VOLTAGE_3;
}
else else
{
*up_voltage = PILOT_VOLTAGE_1; *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); *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);
} }

View File

@@ -1,82 +1,220 @@
/* #include <inttypes.h>
* evse_session.c
* Implementation of evse_session module using instantaneous power accumulation
*/
#include <inttypes.h> // for PRIu32
#include "evse_session.h" #include "evse_session.h"
#include "evse_meter.h" #include "evse_meter.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
#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 const char *TAG = "evse_session";
// Internal state
static TickType_t session_start_tick = 0; static TickType_t session_start_tick = 0;
static uint32_t watt_seconds = 0; // accumulator of W·s
// 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 evse_session_t last_session;
static bool last_session_valid = false; static bool last_session_valid = false;
static uint32_t session_counter = 0;
void evse_session_init(void) { static portMUX_TYPE session_mux = portMUX_INITIALIZER_UNLOCKED;
session_start_tick = 0;
watt_seconds = 0; static void post_session_event(const evse_session_event_data_t *evt)
last_session_valid = false; {
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_start(void) { void evse_session_init(void)
session_start_tick = xTaskGetTickCount(); {
watt_seconds = 0; portENTER_CRITICAL(&session_mux);
ESP_LOGI(TAG, "Session started at tick %u", (unsigned)session_start_tick); session_start_tick = 0;
session_start_us = 0;
last_tick_us = 0;
watt_microseconds = 0;
last_session_valid = false;
session_counter = 0;
portEXIT_CRITICAL(&session_mux);
} }
void evse_session_end(void) { void evse_session_start(void)
if (session_start_tick == 0) { {
TickType_t tick = xTaskGetTickCount();
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
session_start_tick = tick;
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 (id=%" PRIu32 ") tick=%u us=%" PRId64,
id, (unsigned)tick, now_us);
evse_session_event_data_t evt = {
.type = EVSE_SESSION_EVENT_STARTED,
.session_id = id,
.duration_s = 0,
.energy_wh = 0,
.avg_power_w = 0,
.is_current = true,
};
post_session_event(&evt);
}
void evse_session_end(void)
{
TickType_t start_tick;
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)
{
portEXIT_CRITICAL(&session_mux);
ESP_LOGW(TAG, "evse_session_end called without active session"); ESP_LOGW(TAG, "evse_session_end called without active session");
return; return;
} }
TickType_t now = xTaskGetTickCount();
uint32_t duration_s = (now - session_start_tick) / configTICK_RATE_HZ;
uint32_t energy_wh = watt_seconds / 3600U;
uint32_t avg_power = duration_s > 0 ? watt_seconds / duration_s : 0;
last_session.start_tick = session_start_tick; start_tick = session_start_tick;
last_session.duration_s = duration_s; start_us = session_start_us;
last_session.energy_wh = energy_wh; w_us = watt_microseconds;
last_session.avg_power_w = avg_power; id = session_counter;
last_session.is_current = false;
last_session_valid = true;
session_start_tick = 0; session_start_tick = 0;
ESP_LOGI(TAG, "Session ended: duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg_power=%" PRIu32 " W", session_start_us = 0;
(uint32_t)duration_s, (uint32_t)energy_wh, (uint32_t)avg_power); last_tick_us = 0;
watt_microseconds = 0;
portEXIT_CRITICAL(&session_mux);
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;
last_session.duration_s = duration_s;
last_session.energy_wh = energy_wh;
last_session.avg_power_w = avg_power;
last_session.is_current = false;
last_session_valid = true;
portEXIT_CRITICAL(&session_mux);
ESP_LOGI(TAG,
"Session ended (id=%" PRIu32 "): duration=%" PRIu32 " s, energy=%" PRIu32
" Wh, avg_power=%" PRIu32 " W",
id, duration_s, energy_wh, avg_power);
evse_session_event_data_t evt = {
.type = EVSE_SESSION_EVENT_FINISHED,
.session_id = id,
.duration_s = duration_s,
.energy_wh = energy_wh,
.avg_power_w = avg_power,
.is_current = false,
};
post_session_event(&evt);
} }
void evse_session_tick(void) { void evse_session_tick(void)
if (session_start_tick == 0) return; {
// Should be called every second (or known interval) // Potência instantânea pode ser negativa (ruído/overflow de sensor) -> clamp
uint32_t power_w = evse_meter_get_instant_power(); int p = evse_meter_get_instant_power();
watt_seconds += power_w; 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)
{
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);
} }
bool evse_session_get(evse_session_t *out) { bool evse_session_get(evse_session_t *out)
if (out == NULL) return false; {
if (out == NULL)
return false;
if (session_start_tick != 0) { TickType_t start_tick;
TickType_t now = xTaskGetTickCount(); int64_t start_us;
uint32_t duration_s = (now - session_start_tick) / configTICK_RATE_HZ; uint64_t w_us;
uint32_t energy_wh = watt_seconds / 3600U; bool has_current;
uint32_t avg_power = duration_s > 0 ? watt_seconds / duration_s : 0; evse_session_t last_copy;
bool last_valid;
out->start_tick = session_start_tick; int64_t now_us = esp_timer_get_time();
out->duration_s = duration_s;
out->energy_wh = energy_wh; portENTER_CRITICAL(&session_mux);
out->avg_power_w = avg_power; start_tick = session_start_tick;
out->is_current = true; start_us = session_start_us;
w_us = watt_microseconds;
has_current = (session_start_tick != 0);
last_copy = last_session;
last_valid = last_session_valid;
portEXIT_CRITICAL(&session_mux);
if (has_current)
{
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_tick;
out->duration_s = duration_s;
out->energy_wh = energy_wh;
out->avg_power_w = avg_power;
out->is_current = true;
return true; return true;
} }
if (last_session_valid) { if (last_valid)
*out = last_session; {
*out = last_copy;
return true; return true;
} }

View File

@@ -5,150 +5,203 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h"
// ========================= #define EVSE_EVENT_POST_TIMEOUT_MS 50
// Internal State Variables
// =========================
static evse_state_t current_state = EVSE_STATE_A; static evse_state_t current_state = EVSE_STATE_A;
static bool is_authorized = false; static bool is_authorized = false;
static portMUX_TYPE state_mux = portMUX_INITIALIZER_UNLOCKED; static portMUX_TYPE state_mux = portMUX_INITIALIZER_UNLOCKED;
static const char *TAG = "evse_state"; static const char *TAG = "evse_state";
// ========================= static evse_state_event_t map_state_to_event(evse_state_t s)
// Internal Mapping {
// ========================= switch (s)
{
case EVSE_STATE_A:
return EVSE_STATE_EVENT_IDLE;
static evse_state_event_t map_state_to_event(evse_state_t s) { case EVSE_STATE_B1:
switch (s) { case EVSE_STATE_B2:
case EVSE_STATE_A: return EVSE_STATE_EVENT_IDLE; return EVSE_STATE_EVENT_WAITING;
case EVSE_STATE_B1:
case EVSE_STATE_B2: return EVSE_STATE_EVENT_WAITING; case EVSE_STATE_C1:
case EVSE_STATE_C1: case EVSE_STATE_C2:
case EVSE_STATE_C2: return EVSE_STATE_EVENT_CHARGING; case EVSE_STATE_D1:
case EVSE_STATE_E: case EVSE_STATE_D2:
case EVSE_STATE_F: return EVSE_STATE_EVENT_FAULT; return EVSE_STATE_EVENT_CHARGING;
default: return EVSE_STATE_EVENT_IDLE;
case EVSE_STATE_E:
case EVSE_STATE_F:
return EVSE_STATE_EVENT_FAULT;
default:
return EVSE_STATE_EVENT_IDLE;
} }
} }
// ========================= static void post_evse_event(evse_event_id_t id, const void *data, size_t len)
// Public API {
// ========================= 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; bool changed = false;
evse_state_t prev_state; evse_state_t prev_state;
bool start_session = false; 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); portENTER_CRITICAL(&state_mux);
prev_state = current_state; prev_state = current_state;
if (new_state != current_state) {
// se entrou em charging pela primeira vez if (new_state != current_state)
if (evse_state_is_charging(new_state) && !evse_state_is_charging(prev_state)) { {
start_session = true;
} // Sessão começa quando entra em energia (relé ON)
// se saiu de charging para qualquer outro if (evse_state_is_power_flowing(new_state) && !evse_state_is_power_flowing(prev_state))
else if (!evse_state_is_charging(new_state) && evse_state_is_charging(prev_state)) { {
end_session = true; start_session = true;
} }
current_state = new_state; // Sessão termina quando sai de energia
changed = true; 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); portEXIT_CRITICAL(&state_mux);
// 2) Executa start/end de sessão FORA da região crítica, evitando logs/alloc dentro dela // Fora da região crítica
if (start_session) { if (start_session)
{
evse_session_start(); evse_session_start();
} }
if (end_session) { if (end_session)
{
evse_session_end(); evse_session_end();
} }
// 3) Se mudou o estado, faz log e dispara evento if (changed)
if (changed) { {
const char *prev_str = evse_state_to_str(prev_state); ESP_LOGI(TAG, "State changed: %s → %s",
const char *curr_str = evse_state_to_str(new_state); evse_state_to_str(prev_state),
ESP_LOGI(TAG, "State changed: %s → %s", prev_str, curr_str); evse_state_to_str(new_state));
evse_state_event_data_t evt = { evse_state_event_data_t evt = {
.state = map_state_to_event(new_state) .state = map_state_to_event(new_state)};
}; post_evse_event(EVSE_EVENT_STATE_CHANGED, &evt, sizeof(evt));
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_STATE_CHANGED,
&evt,
sizeof(evt),
portMAX_DELAY);
} }
} }
evse_state_t evse_get_state(void)
{
evse_state_t evse_get_state(void) {
portENTER_CRITICAL(&state_mux); portENTER_CRITICAL(&state_mux);
evse_state_t s = current_state; evse_state_t s = current_state;
portEXIT_CRITICAL(&state_mux); portEXIT_CRITICAL(&state_mux);
return s; return s;
} }
const char* evse_state_to_str(evse_state_t state) { const char *evse_state_to_str(evse_state_t state)
switch (state) { {
case EVSE_STATE_A: return "A - EV Not Connected (12V)"; switch (state)
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_A:
case EVSE_STATE_C1: return "C1 - Charging Requested (6V, Relay Off)"; return "A - EV Not Connected (12V)";
case EVSE_STATE_C2: return "C2 - Charging Active (6V, Relay On)"; case EVSE_STATE_B1:
case EVSE_STATE_D1: return "D1 - Ventilation Required (3V, Relay Off)"; return "B1 - EV Connected (9V, Not Authorized)";
case EVSE_STATE_D2: return "D2 - Ventilation Active (3V, Relay On)"; case EVSE_STATE_B2:
case EVSE_STATE_E: return "E - Error: Control Pilot Shorted to Ground (0V)"; return "B2 - EV Connected (9V, Authorized and Ready)";
case EVSE_STATE_F: return "F - Fault: EVSE Unavailable or No Pilot Signal"; case EVSE_STATE_C1:
default: return "Unknown State"; 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); portENTER_CRITICAL(&state_mux);
current_state = EVSE_STATE_A; current_state = EVSE_STATE_A;
is_authorized = true; is_authorized = false;
portEXIT_CRITICAL(&state_mux); 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 = { evse_state_event_data_t evt = {
.state = map_state_to_event(current_state) .state = map_state_to_event(current_state)};
}; post_evse_event(EVSE_EVENT_INIT, &evt, sizeof(evt));
esp_event_post(EVSE_EVENTS, EVSE_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY);
} }
void evse_state_tick(void) { void evse_state_tick(void)
// Placeholder for future state logic {
// placeholder
} }
bool evse_state_is_charging(evse_state_t state) { void evse_state_set_authorized(bool authorized)
return state == EVSE_STATE_C1 || state == EVSE_STATE_C2; {
}
bool evse_state_is_plugged(evse_state_t state) {
return state == EVSE_STATE_B1 || state == EVSE_STATE_B2 ||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2;
}
bool evse_state_is_session(evse_state_t state) {
return state == EVSE_STATE_B2 || state == EVSE_STATE_C1 || state == EVSE_STATE_C2;
}
void evse_state_set_authorized(bool authorized) {
portENTER_CRITICAL(&state_mux); portENTER_CRITICAL(&state_mux);
is_authorized = authorized; is_authorized = authorized;
portEXIT_CRITICAL(&state_mux); portEXIT_CRITICAL(&state_mux);
} }
bool evse_state_get_authorized(void) { bool evse_state_get_authorized(void)
{
portENTER_CRITICAL(&state_mux); portENTER_CRITICAL(&state_mux);
bool result = is_authorized; bool result = is_authorized;
portEXIT_CRITICAL(&state_mux); portEXIT_CRITICAL(&state_mux);

View File

@@ -71,6 +71,24 @@ void evse_state_set_authorized(bool authorized);
*/ */
bool evse_state_get_authorized(void); 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 // Limit Status
@@ -85,4 +103,4 @@ bool evse_is_limit_reached(void);
} }
#endif #endif
#endif // EVSE_API_H #endif // EVSE_API_H

View File

@@ -15,11 +15,9 @@ extern "C" {
// Limites Globais (Defines) // Limites Globais (Defines)
// ======================== // ========================
// Corrente máxima de carregamento (configurável pelo usuário)
#define MIN_CHARGING_CURRENT_LIMIT 6 // A #define MIN_CHARGING_CURRENT_LIMIT 6 // A
#define MAX_CHARGING_CURRENT_LIMIT 32 // A #define MAX_CHARGING_CURRENT_LIMIT 32 // A
// Corrente via cabo (proximity) — se configurável
#define MIN_CABLE_CURRENT_LIMIT 6 // A #define MIN_CABLE_CURRENT_LIMIT 6 // A
#define MAX_CABLE_CURRENT_LIMIT 63 // A #define MAX_CABLE_CURRENT_LIMIT 63 // A
@@ -31,23 +29,20 @@ extern "C" {
esp_err_t evse_config_init(void); esp_err_t evse_config_init(void);
void evse_check_defaults(void); void evse_check_defaults(void);
// Corrente de carregamento // Corrente máxima de hardware (fixa)
uint8_t evse_get_max_charging_current(void); 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); uint16_t evse_get_charging_current(void);
esp_err_t evse_set_charging_current(uint16_t value); esp_err_t evse_set_charging_current(uint16_t value);
uint16_t evse_get_default_charging_current(void); // Corrente runtime (RAM) <= max hardware (load balancer pode alterar)
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); void evse_set_runtime_charging_current(uint16_t value);
uint16_t evse_get_runtime_charging_current(void); 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 // RCM
bool evse_is_rcm(void); bool evse_is_rcm(void);

View File

@@ -5,41 +5,45 @@
#include <stdbool.h> #include <stdbool.h>
#include "evse_pilot.h" #include "evse_pilot.h"
// ----------------------------------------------------
#define EVSE_ERR_AUTO_CLEAR_BITS ( \ // Holdoff interno pós-erro (sem expor "cooldown" ao resto)
EVSE_ERR_DIODE_SHORT_BIT | \ // ----------------------------------------------------
EVSE_ERR_TEMPERATURE_HIGH_BIT | \ // Após TODOS os erros reais desaparecerem (raw_bits == 0),
EVSE_ERR_RCM_TRIGGERED_BIT ) // 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 // Error bits
#define EVSE_ERR_DIODE_SHORT_BIT (1 << 0) #define EVSE_ERR_DIODE_SHORT_BIT (1 << 0)
#define EVSE_ERR_LOCK_FAULT_BIT (1 << 1) #define EVSE_ERR_LOCK_FAULT_BIT (1 << 1)
#define EVSE_ERR_UNLOCK_FAULT_BIT (1 << 2) #define EVSE_ERR_UNLOCK_FAULT_BIT (1 << 2)
#define EVSE_ERR_RCM_SELFTEST_FAULT_BIT (1 << 3) #define EVSE_ERR_RCM_SELFTEST_FAULT_BIT (1 << 3)
#define EVSE_ERR_RCM_TRIGGERED_BIT (1 << 4) #define EVSE_ERR_RCM_TRIGGERED_BIT (1 << 4)
#define EVSE_ERR_TEMPERATURE_HIGH_BIT (1 << 5) #define EVSE_ERR_TEMPERATURE_HIGH_BIT (1 << 5)
#define EVSE_ERR_PILOT_FAULT_BIT (1 << 6) #define EVSE_ERR_PILOT_FAULT_BIT (1 << 6)
#define EVSE_ERR_TEMPERATURE_FAULT_BIT (1 << 7) #define EVSE_ERR_TEMPERATURE_FAULT_BIT (1 << 7)
// Inicialização do módulo de erros // Inicialização do módulo de erros
void evse_error_init(void); void evse_error_init(void);
// Verificações e monitoramento // Verificações e monitoramento
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v); void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v);
void evse_temperature_check(void); void evse_temperature_check(void);
void evse_error_tick(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); uint32_t evse_get_error(void);
bool evse_is_error_cleared(void);
void evse_mark_error_cleared(void);
void evse_error_set(uint32_t bitmask); void evse_error_set(uint32_t bitmask);
void evse_error_clear(uint32_t bitmask); void evse_error_clear(uint32_t bitmask);
bool evse_error_is_active(void); bool evse_error_is_active(void);
uint32_t evse_error_get_bits(void); uint32_t evse_error_get_bits(void);
void evse_error_reset_flag(void);
// ----------------------------------------------------
// Semântica sticky: flag "todos erros limpos"
// (fica true quando o erro visível chega a 0; pode ser útil para UI/logs)
// ----------------------------------------------------
bool evse_error_cleared_flag(void); bool evse_error_cleared_flag(void);
void evse_error_reset_flag(void);
#endif // EVSE_ERROR_H #endif // EVSE_ERROR_H

View File

@@ -2,6 +2,9 @@
#define EVSE_EVENTS_H #define EVSE_EVENTS_H
#pragma once #pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_event.h" #include "esp_event.h"
ESP_EVENT_DECLARE_BASE(EVSE_EVENTS); ESP_EVENT_DECLARE_BASE(EVSE_EVENTS);
@@ -12,8 +15,13 @@ typedef enum {
EVSE_EVENT_CONFIG_UPDATED, EVSE_EVENT_CONFIG_UPDATED,
EVSE_EVENT_ENABLE_UPDATED, EVSE_EVENT_ENABLE_UPDATED,
EVSE_EVENT_AVAILABLE_UPDATED, EVSE_EVENT_AVAILABLE_UPDATED,
EVSE_EVENT_SESSION,
EVSE_EVENT_ERROR_CHANGED,
} evse_event_id_t; } evse_event_id_t;
// -----------------
// Eventos de STATE
// -----------------
typedef enum { typedef enum {
EVSE_STATE_EVENT_IDLE, EVSE_STATE_EVENT_IDLE,
EVSE_STATE_EVENT_WAITING, EVSE_STATE_EVENT_WAITING,
@@ -25,22 +33,52 @@ typedef struct {
evse_state_event_t state; evse_state_event_t state;
} evse_state_event_data_t; } 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 { typedef struct {
bool charging; // Estado de carregamento evse_session_event_type_t type; ///< STARTED / FINISHED
float hw_max_current; // Corrente máxima suportada pelo hardware
float runtime_current; // Corrente de carregamento em uso uint32_t session_id;
int64_t timestamp_us; // Momento da atualização uint32_t duration_s;
uint32_t energy_wh;
uint32_t avg_power_w;
bool is_current;
} evse_session_event_data_t;
// -----------------
// Eventos de CONFIG
// -----------------
typedef struct {
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} evse_config_event_data_t; } evse_config_event_data_t;
// Eventos simples e específicos
typedef struct { typedef struct {
bool enabled; // novo estado de enabled bool enabled;
int64_t timestamp_us; // epoch micros int64_t timestamp_us;
} evse_enable_event_data_t; } evse_enable_event_data_t;
typedef struct { typedef struct {
bool available; // novo estado de available bool available;
int64_t timestamp_us; // epoch micros int64_t timestamp_us;
} evse_available_event_data_t; } 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 #endif // EVSE_EVENTS_H

View File

@@ -1,3 +1,4 @@
// === Início de: components/evse/include/evse_limits.h ===
#ifndef EVSE_LIMITS_H #ifndef EVSE_LIMITS_H
#define EVSE_LIMITS_H #define EVSE_LIMITS_H
@@ -15,7 +16,6 @@ extern "C" {
/** /**
* @brief Sets the internal 'limit reached' flag. * @brief Sets the internal 'limit reached' flag.
* Called internally when a limit condition is triggered.
*/ */
void evse_set_limit_reached(bool value); void evse_set_limit_reached(bool value);
@@ -24,6 +24,11 @@ void evse_set_limit_reached(bool value);
*/ */
bool evse_get_limit_reached(void); 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). * @brief Checks if any session limit has been exceeded (energy, time or power).
* Should be called periodically during charging. * Should be called periodically during charging.
@@ -48,30 +53,13 @@ void evse_set_charging_time_limit(uint32_t value);
/** /**
* @brief Get/set minimum acceptable power level (in Watts). * @brief Get/set minimum acceptable power level (in Watts).
* If the power remains below this for a long time, the session may be interrupted.
*/ */
uint16_t evse_get_under_power_limit(void); uint16_t evse_get_under_power_limit(void);
void evse_set_under_power_limit(uint16_t value); void evse_set_under_power_limit(uint16_t value);
// ============================
// Default (Persistent) Limits
// ============================
/**
* @brief Default values used after system boot or reset.
* These can be restored from NVS or fallback values.
*/
uint32_t evse_get_default_consumption_limit(void);
void evse_set_default_consumption_limit(uint32_t value);
uint32_t evse_get_default_charging_time_limit(void);
void evse_set_default_charging_time_limit(uint32_t value);
uint16_t evse_get_default_under_power_limit(void);
void evse_set_default_under_power_limit(uint16_t value);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif // EVSE_LIMITS_H #endif // EVSE_LIMITS_H
// === Fim de: components/evse/include/evse_limits.h ===

View File

@@ -2,65 +2,43 @@
#define PILOT_H_ #define PILOT_H_
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C"
{
#endif #endif
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
/** typedef enum
* @brief Níveis categóricos de tensão no sinal CP (Control Pilot) {
*/ PILOT_VOLTAGE_12,
typedef enum PILOT_VOLTAGE_9,
{ PILOT_VOLTAGE_6,
PILOT_VOLTAGE_12, ///< Estado A: +12V PILOT_VOLTAGE_3,
PILOT_VOLTAGE_9, ///< Estado B: +9V PILOT_VOLTAGE_1
PILOT_VOLTAGE_6, ///< Estado C: +6V } pilot_voltage_t;
PILOT_VOLTAGE_3, ///< Estado D: +3V
PILOT_VOLTAGE_1 ///< Estado E/F: abaixo de 3V
} pilot_voltage_t;
/** void pilot_init(void);
* @brief Inicializa o driver do sinal Pilot
*/
void pilot_init(void);
/** /**
* @brief Define o nível do Pilot: +12V ou -12V * @brief Define o pilot em modo DC.
* *
* @param level true = +12V, false = -12V * @param high true = nível alto (+12V)
*/ * false = nível baixo (-12V)
void pilot_set_level(bool level); */
void pilot_set_level(bool high);
/** void pilot_set_amps(uint16_t amps);
* @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_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12);
* @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);
/** bool pilot_get_state(void);
* @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);
/** typedef struct
* @brief Cache interno opcional dos níveis de tensão reais do Pilot {
*/ uint16_t high_mv;
typedef struct { uint16_t low_mv;
uint16_t high_mv; ///< Pico positivo medido (mV) } pilot_voltage_cache_t;
uint16_t low_mv; ///< Pico negativo medido (mV)
} pilot_voltage_cache_t;
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -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 #ifndef EVSE_SESSION_H
#define EVSE_SESSION_H #define EVSE_SESSION_H
@@ -15,39 +9,23 @@
* @brief Charging session statistics * @brief Charging session statistics
*/ */
typedef struct { typedef struct {
TickType_t start_tick; ///< tick when session began TickType_t start_tick; ///< tick when session began (debug/trace)
uint32_t duration_s; ///< total duration in seconds uint32_t duration_s; ///< total duration in seconds (tempo real)
uint32_t energy_wh; ///< total energy consumed in Wh uint32_t energy_wh; ///< total energy consumed in Wh (tempo real)
uint32_t avg_power_w; ///< average power in W uint32_t avg_power_w; ///< average power in W
bool is_current; ///< true if session still in progress bool is_current; ///< true if session still in progress
} evse_session_t; } evse_session_t;
/**
* @brief Initialize the session module
*/
void evse_session_init(void); void evse_session_init(void);
/**
* @brief Mark the beginning of a charging session
*/
void evse_session_start(void); void evse_session_start(void);
/**
* @brief Mark the end of the charging session and store it as "last session"
*/
void evse_session_end(void); 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); 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); bool evse_session_get(evse_session_t *out);
#endif // EVSE_SESSION_H #endif // EVSE_SESSION_H

View File

@@ -6,90 +6,68 @@
#include "evse_events.h" #include "evse_events.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C"
{
#endif #endif
// ============================ typedef enum
// EVSE Pilot Signal States {
// ============================ 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 { // Initialization
EVSE_STATE_A, // EV Not Connected (12V) void evse_state_init(void);
EVSE_STATE_B1, // EV Connected (9V, Not Authorized) void evse_state_tick(void);
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;
// ============================ // State Access & Control
// Initialization 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. // State Evaluation Helpers
*/ // ---------------------------
void evse_state_init(void);
/** /**
* @brief Periodic tick for state handling (optional hook). * @brief True se existe uma sessão "lógica" ativa (carro ligado e autorizado/pronto ou a carregar).
*/ * Inclui B2, C1/C2, D1/D2.
void evse_state_tick(void); */
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. * @brief True se o EV pediu carga mas o relé ainda está OFF (C1/D1).
*/ */
evse_state_t evse_get_state(void); bool evse_state_is_requesting(evse_state_t state);
/** /**
* @brief Sets the current EVSE state and emits a change event if needed. * @brief True se há fluxo de energia (alias explícito para charging).
*/ */
void evse_set_state(evse_state_t state); bool evse_state_is_power_flowing(evse_state_t state);
/** /**
* @brief Converts the state enum into a human-readable string. * @brief True se o EV está fisicamente ligado (B1 e além).
*/ */
const char* evse_state_to_str(evse_state_t state); bool evse_state_is_plugged(evse_state_t state);
// ============================ // Authorization Control
// State Evaluation Helpers void evse_state_set_authorized(bool authorized);
// ============================ bool evse_state_get_authorized(void);
/**
* @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 #ifdef __cplusplus
} }

View File

@@ -9,8 +9,8 @@ set(srcs
idf_component_register(SRCS "${srcs}" idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_REQUIRES driver esp_timer nvs_flash PRIV_REQUIRES driver esp_timer
REQUIRES config evse loadbalancer) REQUIRES config evse loadbalancer storage_service)

View File

@@ -1,3 +1,4 @@
// === Início de: components/evse_link/include/evse_link.h ===
#ifndef EVSE_LINK_H_ #ifndef EVSE_LINK_H_
#define EVSE_LINK_H_ #define EVSE_LINK_H_
@@ -43,3 +44,5 @@ void evse_link_set_enabled(bool enabled);
bool evse_link_is_enabled(void); bool evse_link_is_enabled(void);
#endif // EVSE_LINK_H_ #endif // EVSE_LINK_H_
// === Fim de: components/evse_link/include/evse_link.h ===

View File

@@ -2,15 +2,28 @@
#define EVSE_LINK_EVENTS_H_ #define EVSE_LINK_EVENTS_H_
#include "esp_event.h" #include "esp_event.h"
#include <stdint.h>
ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS); ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS);
#define EVSE_LINK_TAG_MAX_LEN 32
typedef enum { typedef enum {
LINK_EVENT_FRAME_RECEIVED, // qualquer frame válido LINK_EVENT_FRAME_RECEIVED,
LINK_EVENT_SLAVE_ONLINE, // heartbeat recebido primeira vez LINK_EVENT_SLAVE_ONLINE, // payload: evse_link_slave_presence_event_t
LINK_EVENT_SLAVE_OFFLINE, // sem heartbeat no timeout LINK_EVENT_SLAVE_OFFLINE, // payload: evse_link_slave_presence_event_t (master-side) ou NULL (slave-side fallback)
LINK_EVENT_MASTER_POLL_SENT, // opcional: poll enviado pelo master LINK_EVENT_MASTER_POLL_SENT,
LINK_EVENT_CURRENT_LIMIT_APPLIED, LINK_EVENT_CURRENT_LIMIT_APPLIED,
LINK_EVENT_SLAVE_CONFIG_UPDATED // <- NOVO evento LINK_EVENT_SLAVE_CONFIG_UPDATED,
LINK_EVENT_REMOTE_AUTH_GRANTED
} evse_link_event_t; } evse_link_event_t;
typedef struct {
char tag[EVSE_LINK_TAG_MAX_LEN];
} evse_link_auth_grant_event_t;
typedef struct {
uint8_t slave_id;
} evse_link_slave_presence_event_t;
#endif // EVSE_LINK_EVENTS_H_ #endif // EVSE_LINK_EVENTS_H_

View File

@@ -5,18 +5,24 @@
#include <stdbool.h> #include <stdbool.h>
#include "driver/uart.h" #include "driver/uart.h"
// UART configuration // UART instance and configuration
#define UART_PORT UART_NUM_2 #define UART_PORT UART_NUM_2
#define UART_BAUDRATE 115200 #define UART_BAUDRATE 9600
#define UART_RX_BUF_SIZE 256 #define UART_RX_BUF_SIZE 256
// GPIO pin assignments for UART // GPIO pin assignments for RS-485 UART
#define TX_PIN 21 // GPIO21 -> RX on other board // Ajuste conforme seu hardware
#define RX_PIN 22 // GPIO22 -> TX on other board #define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
#define TX_PIN MB_UART_TXD
#define RX_PIN MB_UART_RXD
#define RTS_PIN MB_UART_RTS
// Frame delimiters // Frame delimiters
#define MAGIC_START 0x7E #define MAGIC_START 0x7E
#define MAGIC_END 0x7F #define MAGIC_END 0x7F
// Maximum payload (excluding sequence byte) // Maximum payload (excluding sequence byte)
#define EVSE_LINK_MAX_PAYLOAD 254 #define EVSE_LINK_MAX_PAYLOAD 254

View File

@@ -1,177 +1,200 @@
// components/evse_link/src/evse_link.c // components/evse_link/src/evse_link.c
//
// Camada de transporte EVSE-Link:
// - carrega config (mode/self_id/enabled)
// - init do framing
// - task RX (UART -> framing)
// - entrega frames completos ao callback registado
//
// NOTA: a logica de protocolo (CMD_POLL / ACK / etc.) deve ficar em
// evse_link_master.c / evse_link_slave.c.
#include "evse_link.h" #include "evse_link.h"
#include "evse_link_framing.h" #include "evse_link_framing.h"
#include "driver/uart.h" #include "driver/uart.h"
#include "nvs.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h"
#include <stdbool.h>
#include <stdint.h>
#include "storage_service.h"
static const char *TAG = "evse_link"; static const char *TAG = "evse_link";
// NVS keys
#define _NVS_NAMESPACE "evse_link" #define _NVS_NAMESPACE "evse_link"
#define _NVS_MODE_KEY "mode" #define _KEY_MODE "mode"
#define _NVS_ID_KEY "self_id" #define _KEY_SELF_ID "self_id"
#define _NVS_ENABLED_KEY "enabled" #define _KEY_ENABLED "enabled"
// UART parameters
#define UART_PORT UART_NUM_2
#define UART_RX_BUF_SIZE 256
// Runtime config
static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER; static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER;
static uint8_t _self_id = 0x01; static uint8_t _self_id = 0x01;
static bool _enabled = false; static bool _enabled = false;
// Registered Rx callback
static evse_link_rx_cb_t _rx_cb = NULL; static evse_link_rx_cb_t _rx_cb = NULL;
static bool s_evse_link_inited = false;
// Forward declarations
extern void evse_link_master_init(void); extern void evse_link_master_init(void);
extern void evse_link_slave_init(void); extern void evse_link_slave_init(void);
static void framing_rx_cb(uint8_t src, uint8_t dest, static void framing_rx_cb(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) const uint8_t *payload, uint8_t len)
{ {
ESP_LOGD(TAG, "framing_rx_cb: src=0x%02X dest=0x%02X len=%u", src, dest, len);
if (_rx_cb) if (_rx_cb)
{
_rx_cb(src, dest, payload, len); _rx_cb(src, dest, payload, len);
}
} }
// Register protocol-level Rx callback
void evse_link_register_rx_cb(evse_link_rx_cb_t cb) void evse_link_register_rx_cb(evse_link_rx_cb_t cb)
{ {
_rx_cb = cb; _rx_cb = cb;
} }
// Load config from NVS
enum
{
EV_OK = ESP_OK
};
static void load_link_config(void) static void load_link_config(void)
{ {
nvs_handle_t handle; uint8_t u8 = 0;
if (nvs_open(_NVS_NAMESPACE, NVS_READONLY, &handle) != EV_OK)
{
ESP_LOGW(TAG, "NVS open failed, using defaults");
return;
}
uint8_t mode, id, en;
if (nvs_get_u8(handle, _NVS_MODE_KEY, &mode) == EV_OK &&
(mode == EVSE_LINK_MODE_MASTER || mode == EVSE_LINK_MODE_SLAVE))
{
_mode = (evse_link_mode_t)mode;
}
if (nvs_get_u8(handle, _NVS_ID_KEY, &id) == EV_OK)
{
_self_id = id;
}
if (nvs_get_u8(handle, _NVS_ENABLED_KEY, &en) == EV_OK)
{
_enabled = (en != 0);
}
nvs_close(handle);
}
// Save config to NVS esp_err_t err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_MODE, &u8, pdMS_TO_TICKS(500));
static void save_link_config(void) if (err == ESP_OK && (u8 == (uint8_t)EVSE_LINK_MODE_MASTER || u8 == (uint8_t)EVSE_LINK_MODE_SLAVE))
{ _mode = (evse_link_mode_t)u8;
nvs_handle_t handle;
if (nvs_open(_NVS_NAMESPACE, NVS_READWRITE, &handle) == EV_OK)
{
nvs_set_u8(handle, _NVS_MODE_KEY, (uint8_t)_mode);
nvs_set_u8(handle, _NVS_ID_KEY, _self_id);
nvs_set_u8(handle, _NVS_ENABLED_KEY, _enabled ? 1 : 0);
nvs_commit(handle);
nvs_close(handle);
}
else else
{ {
ESP_LOGE(TAG, "Failed to save NVS"); _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", esp_err_to_name(err));
}
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", esp_err_to_name(err), _self_id);
}
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", esp_err_to_name(err));
} }
} }
// Getters/setters static void save_link_config(void)
{
(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);
}
void evse_link_set_mode(evse_link_mode_t m) 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; _mode = m;
save_link_config(); save_link_config();
} }
evse_link_mode_t evse_link_get_mode(void) { return _mode; } evse_link_mode_t evse_link_get_mode(void) { return _mode; }
void evse_link_set_self_id(uint8_t id) void evse_link_set_self_id(uint8_t id)
{ {
if (_self_id == id)
return;
_self_id = id; _self_id = id;
save_link_config(); save_link_config();
} }
uint8_t evse_link_get_self_id(void) { return _self_id; } uint8_t evse_link_get_self_id(void) { return _self_id; }
void evse_link_set_enabled(bool en) void evse_link_set_enabled(bool en)
{ {
if (_enabled == en)
return;
_enabled = en; _enabled = en;
save_link_config(); save_link_config();
} }
bool evse_link_is_enabled(void) { return _enabled; } bool evse_link_is_enabled(void) { return _enabled; }
// RX task: reads bytes from UART and feeds framing
static void evse_link_rx_task(void *arg) static void evse_link_rx_task(void *arg)
{ {
(void)arg;
ESP_LOGI(TAG, "evse_link_rx_task started");
uint8_t buf[UART_RX_BUF_SIZE]; uint8_t buf[UART_RX_BUF_SIZE];
while (true) while (true)
{ {
int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000)); int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000));
if (len > 0) if (len > 0)
{ {
ESP_LOGD(TAG, "UART RX: len=%d first=0x%02X last=0x%02X", len, buf[0], buf[len - 1]);
for (int i = 0; i < len; ++i) for (int i = 0; i < len; ++i)
{
evse_link_recv_byte(buf[i]); evse_link_recv_byte(buf[i]);
}
} }
} }
} }
// Initialize EVSE-Link component
void evse_link_init(void) void evse_link_init(void)
{ {
load_link_config(); if (s_evse_link_inited)
{
ESP_LOGW(TAG, "evse_link_init called twice; ignoring");
return;
}
s_evse_link_inited = true;
esp_err_t se = storage_service_init();
if (se == ESP_OK)
load_link_config();
else
ESP_LOGE(TAG, "storage_service_init failed: %s (defaults in RAM)", esp_err_to_name(se));
ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d", ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d",
_mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S', _mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S',
_self_id, _enabled); _self_id, _enabled);
if (!_enabled) if (!_enabled)
return; return;
// 1) framing layer init (sets up mutex, UART driver, etc.)
evse_link_framing_init(); evse_link_framing_init();
evse_link_framing_register_cb(framing_rx_cb); evse_link_framing_register_cb(framing_rx_cb);
// 2) start RX task if (xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL) != pdPASS)
xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL); {
ESP_LOGE(TAG, "Failed to create evse_link_rx task");
return;
}
// 3) delegate to master or slave
if (_mode == EVSE_LINK_MODE_MASTER) if (_mode == EVSE_LINK_MODE_MASTER)
{
evse_link_master_init(); evse_link_master_init();
}
else else
{
evse_link_slave_init(); evse_link_slave_init();
}
} }
// Send a frame (delegates to framing module)
bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len) bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len)
{ {
if (!evse_link_is_enabled()) if (!evse_link_is_enabled())
return false; return false;
uint8_t src = evse_link_get_self_id(); uint8_t src = evse_link_get_self_id();
return evse_link_framing_send(dest, src, payload, len); return evse_link_framing_send(dest, src, payload, len);
} }
// Receive byte (delegates to framing module)
void evse_link_recv_byte(uint8_t byte) void evse_link_recv_byte(uint8_t byte)
{ {
evse_link_framing_recv_byte(byte); evse_link_framing_recv_byte(byte);

View File

@@ -1,82 +1,284 @@
// components/evse_link_framing/src/evse_link_framing.c // components/evse_link/src/evse_link_framing.c
//
// EVSE-Link framing (RS-485 HALF DUPLEX via UART driver)
// - Usa UART_MODE_RS485_HALF_DUPLEX (driver controla RTS => DE//RE)
// - Configura RX timeout + RX full threshold para evitar “len=120”
// - Remove controlo manual de GPIO do RTS
//
// Requisitos:
// - MAX3485 com DE e /RE juntos ligados ao RTS_PIN
// - RTS_PIN definido em evse_link_framing.h (ex.: GPIO2; recomendado mudar no futuro)
//
// Notas:
// - Se o teu hardware inverter a lógica do RTS (raro), define EVSE_LINK_RTS_INVERT=1
#include "evse_link_framing.h" #include "evse_link_framing.h"
#include "driver/uart.h" #include "driver/uart.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include <string.h> #include <string.h>
#include <stdint.h>
#include <stdbool.h>
static const char *TAG = "evse_framing"; static const char *TAG = "evse_framing";
static SemaphoreHandle_t tx_mutex; static SemaphoreHandle_t tx_mutex = NULL;
static uint8_t seq = 0; static uint8_t seq = 0;
static evse_link_frame_cb_t rx_cb = NULL; static evse_link_frame_cb_t rx_cb = NULL;
// CRC-8 (polynomial 0x07) static bool s_framing_inited = false;
static uint8_t crc8(const uint8_t *data, uint8_t len) {
uint8_t crc = 0; // ---- Tunables (fallbacks) ----
for (uint8_t i = 0; i < len; ++i) { #ifndef EVSE_LINK_INTERBYTE_TIMEOUT_US
crc ^= data[i]; // Timeout inter-byte: se um frame morrer a meio, reseta o parser
for (uint8_t b = 0; b < 8; ++b) { #define EVSE_LINK_INTERBYTE_TIMEOUT_US 5000
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1); #endif
}
// Rate-limit para warnings (em microsegundos)
#define LOG_RATELIMIT_US 1000000 // 1s
// RX tuning (evita acumular ~120 bytes antes de "acordar")
#ifndef EVSE_LINK_RX_TIMEOUT
// Timeout do UART TOUT feature (em "character times"). 3..10 funciona bem.
#define EVSE_LINK_RX_TIMEOUT 3
#endif
#ifndef EVSE_LINK_RX_FULL_THRESH
// Gera interrupção quando FIFO tem pelo menos N bytes (1..120). 4 é um bom default.
#define EVSE_LINK_RX_FULL_THRESH 1
#endif
#ifndef EVSE_LINK_RTS_INVERT
// Se precisares inverter RTS (muito raro), define para 1 no build.
#define EVSE_LINK_RTS_INVERT 0
#endif
static inline bool log_ratelimit_ok(int64_t *last_us, int64_t interval_us)
{
const int64_t now = esp_timer_get_time();
if (*last_us == 0 || (now - *last_us) > interval_us)
{
*last_us = now;
return true;
}
return false;
}
// CRC-8 (poly 0x07), MSB-first, init=0x00
static uint8_t crc8_update(uint8_t crc, uint8_t data)
{
crc ^= data;
for (uint8_t b = 0; b < 8; ++b)
{
if (crc & 0x80)
crc = (uint8_t)((crc << 1) ^ 0x07);
else
crc <<= 1;
} }
return crc; return crc;
} }
void evse_link_framing_init(void) { static esp_err_t configure_uart(void)
// Create mutex for TX {
tx_mutex = xSemaphoreCreateMutex(); uart_config_t cfg = {
// Install UART driver
uart_driver_install(UART_PORT, UART_RX_BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_PORT, &(uart_config_t){
.baud_rate = UART_BAUDRATE, .baud_rate = UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS, .data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE, .parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1, .stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
}); .source_clk = UART_SCLK_DEFAULT,
uart_set_pin(UART_PORT, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); };
esp_err_t err = uart_param_config(UART_PORT, &cfg);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
return err;
}
// TX/RX/RTS na UART (RTS controla DE//RE em RS485 half-duplex)
err = uart_set_pin(UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
return err;
}
// RS-485 HALF DUPLEX (driver controla RTS automaticamente)
err = uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_mode(RS485_HALF_DUPLEX) failed: %s", esp_err_to_name(err));
return err;
}
// Ajustes para RX responsivo (evita "len=120")
(void)uart_set_rx_full_threshold(UART_PORT, EVSE_LINK_RX_FULL_THRESH);
(void)uart_set_rx_timeout(UART_PORT, EVSE_LINK_RX_TIMEOUT);
// Opcional: inverter RTS se hardware exigir
if (EVSE_LINK_RTS_INVERT)
{
(void)uart_set_line_inverse(UART_PORT, UART_SIGNAL_RTS_INV);
ESP_LOGW(TAG, "RS485 driver: RTS inverted");
}
ESP_LOGW(TAG, "RS485 driver enabled: UART%d TX=%d RX=%d RTS=%d baud=%d rx_to=%d rx_thresh=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE,
EVSE_LINK_RX_TIMEOUT, EVSE_LINK_RX_FULL_THRESH);
return ESP_OK;
}
void evse_link_framing_init(void)
{
if (s_framing_inited)
{
ESP_LOGI(TAG, "Framing already initialized");
return;
}
if (!tx_mutex)
{
tx_mutex = xSemaphoreCreateMutex();
if (!tx_mutex)
{
ESP_LOGE(TAG, "Failed to create TX mutex");
return;
}
}
// Se o driver já estiver instalado, só reconfigura e aplica RX tuning.
if (uart_is_driver_installed(UART_PORT))
{
esp_err_t err = configure_uart();
if (err == ESP_OK)
{
s_framing_inited = true;
ESP_LOGW(TAG, "UART%d driver already installed -> configured for RS485 HALF DUPLEX", UART_PORT);
(void)uart_flush_input(UART_PORT);
}
else
{
ESP_LOGE(TAG, "Failed to configure already-installed UART%d", UART_PORT);
}
return;
}
// Instala driver UART
esp_err_t err = uart_driver_install(UART_PORT,
UART_RX_BUF_SIZE * 2, // RX buffer (ringbuffer)
0, // TX buffer (não usado)
0, // event queue size
NULL, // event queue
0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return;
}
err = configure_uart();
if (err != ESP_OK)
return;
(void)uart_flush_input(UART_PORT);
s_framing_inited = true;
ESP_LOGI(TAG, "Framing init (RS485 driver): UART%d TX=%d RX=%d RTS=%d baud=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE);
} }
bool evse_link_framing_send(uint8_t dest, uint8_t src, bool evse_link_framing_send(uint8_t dest, uint8_t src,
const uint8_t *payload, uint8_t len) { const uint8_t *payload, uint8_t len)
if (len > EVSE_LINK_MAX_PAYLOAD) return false; {
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) return false; if (!s_framing_inited)
{
ESP_LOGE(TAG, "Framing not initialized");
return false;
}
if (len > EVSE_LINK_MAX_PAYLOAD)
{
ESP_LOGW(TAG, "Payload too large: %u (max=%u)", len, EVSE_LINK_MAX_PAYLOAD);
return false;
}
if (len > 0 && payload == NULL)
{
ESP_LOGW(TAG, "Invalid send: len=%u but payload=NULL", len);
return false;
}
if (!tx_mutex)
{
ESP_LOGE(TAG, "TX mutex is NULL (framing_init not called?)");
return false;
}
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE)
{
ESP_LOGW(TAG, "Failed to take TX mutex");
return false;
}
// Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END // Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END
// LEN on wire = SEQ + PAYLOAD (>=1)
uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7]; uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7];
int idx = 0; int idx = 0;
frame[idx++] = MAGIC_START; frame[idx++] = MAGIC_START;
frame[idx++] = dest; frame[idx++] = dest;
frame[idx++] = src; frame[idx++] = src;
frame[idx++] = len + 1; // +1 for SEQ frame[idx++] = (uint8_t)(len + 1);
frame[idx++] = seq; frame[idx++] = seq;
memcpy(&frame[idx], payload, len);
idx += len;
// CRC covers DEST + SRC + LEN + SEQ + PAYLOAD if (len > 0)
uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; {
memcpy(crc_input, &frame[1], 3 + 1 + len); memcpy(&frame[idx], payload, len);
frame[idx++] = crc8(crc_input, 3 + 1 + len); idx += len;
}
// CRC: DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc = 0;
crc = crc8_update(crc, dest);
crc = crc8_update(crc, src);
crc = crc8_update(crc, (uint8_t)(len + 1));
crc = crc8_update(crc, seq);
for (uint8_t i = 0; i < len; ++i)
crc = crc8_update(crc, payload[i]);
frame[idx++] = crc;
frame[idx++] = MAGIC_END; frame[idx++] = MAGIC_END;
uart_write_bytes(UART_PORT, (const char *)frame, idx); int written = uart_write_bytes(UART_PORT, (const char *)frame, idx);
uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(10)); if (written != idx)
{
ESP_LOGW(TAG, "uart_write_bytes wrote %d of %d", written, idx);
}
// Aguarda TX terminar; driver RS485 devolve RTS para RX automaticamente.
(void)uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(700));
xSemaphoreGive(tx_mutex); xSemaphoreGive(tx_mutex);
ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u", ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u",
dest, src, len, seq); dest, src, len, seq);
seq++; // increment sequence after sending seq++;
return true; return true;
} }
void evse_link_framing_recv_byte(uint8_t b) { void evse_link_framing_recv_byte(uint8_t b)
// State machine for frame parsing {
static enum { static enum {
ST_WAIT_START, ST_WAIT_START = 0,
ST_WAIT_DEST, ST_WAIT_DEST,
ST_WAIT_SRC, ST_WAIT_SRC,
ST_WAIT_LEN, ST_WAIT_LEN,
@@ -86,17 +288,46 @@ void evse_link_framing_recv_byte(uint8_t b) {
ST_WAIT_END ST_WAIT_END
} rx_state = ST_WAIT_START; } rx_state = ST_WAIT_START;
static uint8_t rx_dest; static uint8_t rx_dest;
static uint8_t rx_src; static uint8_t rx_src;
static uint8_t rx_len; static uint8_t rx_len; // inclui SEQ + payload (>=1)
static uint8_t rx_seq; static uint8_t rx_seq;
static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD]; static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD];
static uint8_t rx_pos; static uint8_t rx_pos;
static uint8_t rx_crc; static uint8_t rx_crc;
switch (rx_state) { static int64_t s_last_byte_us = 0;
static int64_t s_last_bad_len_log_us = 0;
static int64_t s_last_bad_crc_log_us = 0;
#define RESET_PARSER() \
do \
{ \
rx_state = ST_WAIT_START; \
rx_dest = 0; \
rx_src = 0; \
rx_len = 0; \
rx_seq = 0; \
rx_pos = 0; \
rx_crc = 0; \
} while (0)
const int64_t now_us = esp_timer_get_time();
// Timeout inter-byte: frame morreu a meio -> reseta
if (rx_state != ST_WAIT_START && s_last_byte_us != 0 &&
(now_us - s_last_byte_us) > EVSE_LINK_INTERBYTE_TIMEOUT_US)
{
RESET_PARSER();
}
s_last_byte_us = now_us;
switch (rx_state)
{
case ST_WAIT_START: case ST_WAIT_START:
if (b == MAGIC_START) { if (b == MAGIC_START)
{
rx_pos = 0;
rx_state = ST_WAIT_DEST; rx_state = ST_WAIT_DEST;
} }
break; break;
@@ -112,7 +343,20 @@ void evse_link_framing_recv_byte(uint8_t b) {
break; break;
case ST_WAIT_LEN: case ST_WAIT_LEN:
rx_len = b; // includes SEQ + payload rx_len = b;
// rx_len = SEQ + payload => >=1 e <= MAX+1
if (rx_len < 1 || rx_len > (uint8_t)(EVSE_LINK_MAX_PAYLOAD + 1))
{
if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "Invalid LEN=%u (max=%u), dropping frame",
rx_len, (unsigned)(EVSE_LINK_MAX_PAYLOAD + 1));
}
RESET_PARSER();
break;
}
rx_pos = 0; rx_pos = 0;
rx_state = ST_WAIT_SEQ; rx_state = ST_WAIT_SEQ;
break; break;
@@ -123,11 +367,26 @@ void evse_link_framing_recv_byte(uint8_t b) {
break; break;
case ST_READING: case ST_READING:
rx_buf[rx_pos++] = b; {
if (rx_pos >= (rx_len - 1)) { // all payload bytes read const uint8_t payload_len = (uint8_t)(rx_len - 1);
rx_state = ST_WAIT_CRC;
if (payload_len > EVSE_LINK_MAX_PAYLOAD)
{
if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "Payload len too big: %u", (unsigned)payload_len);
}
RESET_PARSER();
break;
} }
if (rx_pos < EVSE_LINK_MAX_PAYLOAD)
rx_buf[rx_pos++] = b;
if (rx_pos >= payload_len)
rx_state = ST_WAIT_CRC;
break; break;
}
case ST_WAIT_CRC: case ST_WAIT_CRC:
rx_crc = b; rx_crc = b;
@@ -135,34 +394,47 @@ void evse_link_framing_recv_byte(uint8_t b) {
break; break;
case ST_WAIT_END: case ST_WAIT_END:
if (b == MAGIC_END) { if (b == MAGIC_END)
// Build data for CRC calculation {
uint8_t temp[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; uint8_t expected = 0;
int temp_len = 0; expected = crc8_update(expected, rx_dest);
temp[temp_len++] = rx_dest; expected = crc8_update(expected, rx_src);
temp[temp_len++] = rx_src; expected = crc8_update(expected, rx_len);
temp[temp_len++] = rx_len; expected = crc8_update(expected, rx_seq);
temp[temp_len++] = rx_seq;
memcpy(&temp[temp_len], rx_buf, rx_len - 1); const uint8_t payload_len = (uint8_t)(rx_len - 1);
temp_len += rx_len - 1; for (uint8_t i = 0; i < payload_len; ++i)
expected = crc8_update(expected, rx_buf[i]);
if (expected == rx_crc)
{
if (rx_cb)
rx_cb(rx_src, rx_dest, rx_buf, payload_len);
uint8_t expected = crc8(temp, temp_len);
if (expected == rx_crc) {
if (rx_cb) {
rx_cb(rx_src, rx_dest, rx_buf, rx_len - 1);
}
ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u", ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u",
rx_src, rx_dest, rx_len - 1, rx_seq); rx_src, rx_dest, payload_len, rx_seq);
} else { }
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X", else
expected, rx_crc); {
if (log_ratelimit_ok(&s_last_bad_crc_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X",
expected, rx_crc);
}
} }
} }
rx_state = ST_WAIT_START; RESET_PARSER();
break;
default:
RESET_PARSER();
break; break;
} }
#undef RESET_PARSER
} }
void evse_link_framing_register_cb(evse_link_frame_cb_t cb) { void evse_link_framing_register_cb(evse_link_frame_cb_t cb)
{
rx_cb = cb; rx_cb = cb;
} }

View File

@@ -1,152 +1,390 @@
// === components/evse_link/src/evse_link_master.c === // components/evse_link/src/evse_link_master.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX (ACK deferido para task -> não bloqueia RX)
// 2) Dedupe de ACK por slave (evita rajadas se RX vier em chunks / queue acumular)
// 3) Log quando ACK queue enche (antes era silencioso)
// 4) Proteção concorrente no s_presence (int64_t não é atómico no ESP32 32-bit)
// 5) Comentário do poll corrigido + opção de jitter no ACK
//
// NOTA: Mantive o teu comportamento (POLL a cada 10s). Se quiseres, muda para 30s.
#include "evse_link.h" #include "evse_link.h"
#include "evse_link_events.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h" #include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_timer.h"
#include "esp_random.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <string.h>
#include "loadbalancer_events.h" #include "loadbalancer_events.h"
#include "auth_events.h"
static const char *TAG = "evse_link_master"; static const char *TAG = "evse_link_master";
// Link commands // Link commands (opcode no payload[0])
#define CMD_POLL 0x01 #define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 #define CMD_HEARTBEAT 0x02
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_CONFIG_BROADCAST 0x03 #define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08 #define CMD_SET_CURRENT 0x08
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A
// payload lengths (exclui byte de opcode) // payload lengths (INCLUI opcode)
#define LEN_POLL_REQ 1 // [ CMD_POLL ] #define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ]
#define LEN_POLL_RESP 9 // [ CMD_POLL, float V(4), float I(4) ] #define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, ... ]
#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ] #define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, amps_lo, amps_hi ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1
// polling / heartbeat timers interval // Presence monitoring
typedef struct { #define PRESENCE_CHECK_MS 5000
#define SLAVE_OFFLINE_TIMEOUT_MS 180000
// ACK defer
#define ACK_QUEUE_LEN 16
// backoff para espaçar ACK (evita burst)
// antes era 0..15ms; aqui fica ligeiramente mais suave.
#define ACK_BACKOFF_MIN_MS 5
#define ACK_BACKOFF_MAX_MS 35 // jitter 5..35ms
typedef struct
{
int64_t last_seen_us;
bool online;
} slave_presence_t;
static slave_presence_t s_presence[256];
static TimerHandle_t s_presence_timer = NULL;
// Proteção concorrente (int64_t não é atómico no ESP32)
static portMUX_TYPE s_presence_mux = portMUX_INITIALIZER_UNLOCKED;
typedef struct
{
TimerHandle_t timer; TimerHandle_t timer;
TickType_t interval; TickType_t interval;
} timer_def_t; } timer_def_t;
static timer_def_t poll_timer = { .timer = NULL, .interval = pdMS_TO_TICKS(30000) };
static timer_def_t hb_timer = { .timer = NULL, .interval = pdMS_TO_TICKS(30000) }; // POLL a cada 60s (comentário corrigido)
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(60000)};
static bool s_handlers_registered = false;
// ACK task/queue
static QueueHandle_t s_ack_q = NULL;
static TaskHandle_t s_ack_task = NULL;
// Dedupe: 1 ACK pendente por slave
static bool s_ack_pending[256] = {0};
static void post_presence_event(evse_link_event_t evt, uint8_t slave_id)
{
evse_link_slave_presence_event_t p = {.slave_id = slave_id};
(void)esp_event_post(EVSE_LINK_EVENTS, evt, &p, sizeof(p), portMAX_DELAY);
}
static void mark_slave_seen(uint8_t slave_id)
{
const int64_t now = esp_timer_get_time();
bool was_offline = false;
portENTER_CRITICAL(&s_presence_mux);
slave_presence_t *p = &s_presence[slave_id];
p->last_seen_us = now;
if (!p->online)
{
p->online = true;
was_offline = true;
}
portEXIT_CRITICAL(&s_presence_mux);
if (was_offline)
{
ESP_LOGI(TAG, "Slave 0x%02X ONLINE", slave_id);
post_presence_event(LINK_EVENT_SLAVE_ONLINE, slave_id);
}
}
static void presence_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
const int64_t now = esp_timer_get_time();
const int64_t timeout_us = (int64_t)SLAVE_OFFLINE_TIMEOUT_MS * 1000;
const uint8_t self = evse_link_get_self_id();
for (int i = 0; i < 256; ++i)
{
if ((uint8_t)i == self)
continue;
bool online;
int64_t last_seen;
portENTER_CRITICAL(&s_presence_mux);
online = s_presence[i].online;
last_seen = s_presence[i].last_seen_us;
portEXIT_CRITICAL(&s_presence_mux);
if (!online)
continue;
if (last_seen > 0 && (now - last_seen) > timeout_us)
{
portENTER_CRITICAL(&s_presence_mux);
s_presence[i].online = false;
portEXIT_CRITICAL(&s_presence_mux);
ESP_LOGW(TAG, "Slave 0x%02X OFFLINE (no heartbeat for %d ms)", i, SLAVE_OFFLINE_TIMEOUT_MS);
post_presence_event(LINK_EVENT_SLAVE_OFFLINE, (uint8_t)i);
}
}
}
// Enfileira ACK sem duplicar por slave
static void enqueue_ack(uint8_t slave_id)
{
if (!s_ack_q)
return;
// Dedupe: se já existe ACK pendente para este slave, não enfileira outro
if (s_ack_pending[slave_id])
return;
s_ack_pending[slave_id] = true;
if (xQueueSendToBack(s_ack_q, &slave_id, 0) != pdTRUE)
{
s_ack_pending[slave_id] = false;
ESP_LOGW(TAG, "ACK queue full, dropping ACK for 0x%02X", slave_id);
}
}
// --- ACK task (não bloqueia RX) ---
static void ack_task(void *arg)
{
(void)arg;
for (;;)
{
uint8_t slave_id = 0;
if (xQueueReceive(s_ack_q, &slave_id, portMAX_DELAY) != pdTRUE)
continue;
// libera dedupe
s_ack_pending[slave_id] = false;
// backoff com jitter
uint32_t backoff = ACK_BACKOFF_MIN_MS +
(esp_random() % (ACK_BACKOFF_MAX_MS - ACK_BACKOFF_MIN_MS + 1));
vTaskDelay(pdMS_TO_TICKS(backoff));
uint8_t ack[] = {CMD_HEARTBEAT_ACK};
bool ok = evse_link_send(slave_id, ack, sizeof(ack));
ESP_LOGI(TAG, "CMD_HEARTBEAT_ACK to 0x%02X ok=%d", slave_id, ok);
}
}
// --- Send new limit to slave --- // --- Send new limit to slave ---
static void on_new_limit(void* arg, esp_event_base_t base, int32_t id, void* data) { static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *data)
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT) return; {
const loadbalancer_slave_limit_event_t *evt = data; (void)arg;
uint8_t slave_id = evt->slave_id; (void)base;
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT || data == NULL)
return;
const loadbalancer_slave_limit_event_t *evt = (const loadbalancer_slave_limit_event_t *)data;
uint8_t slave_id = evt->slave_id;
uint16_t max_current = evt->max_current; uint16_t max_current = evt->max_current;
uint8_t buf[LEN_SET_CURRENT] = { uint8_t buf[LEN_SET_CURRENT] = {
CMD_SET_CURRENT, CMD_SET_CURRENT,
(uint8_t)(max_current & 0xFF), (uint8_t)(max_current & 0xFF),
(uint8_t)(max_current >> 8) (uint8_t)(max_current >> 8)};
};
evse_link_send(slave_id, buf, sizeof(buf)); (void)evse_link_send(slave_id, buf, sizeof(buf));
ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, max_current); ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, (unsigned)max_current);
}
// --- Bridge AUTH -> EVSE-Link ---
static void on_auth_result(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL)
return;
const auth_tag_event_data_t *ev = (const auth_tag_event_data_t *)data;
if (!ev->authorized)
{
ESP_LOGI(TAG, "Tag %s not authorized, not propagating to slaves", ev->tag);
return;
}
uint8_t buf[1 + EVSE_LINK_TAG_MAX_LEN];
buf[0] = CMD_AUTH_GRANTED;
// Copia tag e garante NUL
strncpy((char *)&buf[1], ev->tag, EVSE_LINK_TAG_MAX_LEN - 1);
((char *)&buf[1])[EVSE_LINK_TAG_MAX_LEN - 1] = '\0';
// Payload inclui opcode + string + NUL
uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1);
(void)evse_link_send(0xFF, buf, payload_len);
ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED (broadcast) tag=%s", (char *)&buf[1]);
} }
// --- Polling broadcast callback --- // --- Polling broadcast callback ---
static void poll_timer_cb(TimerHandle_t xTimer) { static void poll_timer_cb(TimerHandle_t xTimer)
ESP_LOGD(TAG, "Broadcasting CMD_POLL to all slaves");; {
// Optionally post event LINK_EVENT_MASTER_POLL_SENT (void)xTimer;
}
// --- Heartbeat timeout callback --- uint8_t poll[] = {CMD_POLL};
static void hb_timer_cb(TimerHandle_t xTimer) { bool ok = evse_link_send(0xFF, poll, sizeof(poll));
ESP_LOGW(TAG, "Heartbeat timeout: possible slave offline"); ESP_LOGI(TAG, "POLL send ok=%d", ok);
// post event LINK_EVENT_SLAVE_OFFLINE ???
} }
static void on_frame_master(uint8_t src, uint8_t dest, static void on_frame_master(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) { const uint8_t *payload, uint8_t len)
if (len < 1) return; {
const uint8_t self = evse_link_get_self_id();
// ignora eco do próprio master e frames que não são para nós nem broadcast
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
return;
uint8_t cmd = payload[0]; uint8_t cmd = payload[0];
switch (cmd) { switch (cmd)
case CMD_HEARTBEAT: { {
if (len != 6) { // CMD + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi case CMD_HEARTBEAT:
{
ESP_LOGD(TAG, "HEARTBEAT from 0x%02X: %u bytes", src, len);
if (len != LEN_HEARTBEAT)
{
ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len); ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len);
return; return;
} }
bool charging = payload[1] != 0;
uint16_t hw_max = payload[2] | (payload[3] << 8);
uint16_t runtime = payload[4] | (payload[5] << 8);
ESP_LOGI(TAG, "Heartbeat from 0x%02X: charging=%d hw_max=%uA runtime=%uA", bool charging = payload[1] != 0;
src, charging, hw_max, runtime); uint16_t hw_max = (uint16_t)(payload[2] | ((uint16_t)payload[3] << 8));
uint16_t runtime = (uint16_t)(payload[4] | ((uint16_t)payload[5] << 8));
mark_slave_seen(src);
loadbalancer_slave_status_event_t status = { loadbalancer_slave_status_event_t status = {
.slave_id = src, .slave_id = src,
.charging = charging, .charging = charging,
.hw_max_current = (float)hw_max, .hw_max_current = (float)hw_max,
.runtime_current = (float)runtime, // corrente real medida no slave .runtime_current = (float)runtime,
.timestamp_us = esp_timer_get_time() .timestamp_us = esp_timer_get_time()};
};
esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_STATUS,
&status, sizeof(status), portMAX_DELAY);
// Enviar ACK de volta (void)esp_event_post(LOADBALANCER_EVENTS,
uint8_t ack[] = { CMD_HEARTBEAT_ACK }; LOADBALANCER_EVENT_SLAVE_STATUS,
evse_link_send(src, ack, sizeof(ack)); &status, sizeof(status), portMAX_DELAY);
ESP_LOGD(TAG, "Sent HEARTBEAT_ACK to 0x%02X", src);
// ACK deferido e deduplicado
enqueue_ack(src);
break; break;
} }
case CMD_POLL:
ESP_LOGD(TAG, "Received POLL_RESP from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST: case CMD_CONFIG_BROADCAST:
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", if (len >= LEN_CONFIG_BROADCAST)
src, payload[1]); ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", src, payload[1]);
else
ESP_LOGW(TAG, "CONFIG_BROADCAST ack short len=%u from 0x%02X", len, src);
break; break;
default: default:
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src); ESP_LOGD(TAG, "Cmd 0x%02X from 0x%02X (ignored/unknown)", cmd, src);
break;
} }
} }
void evse_link_master_init(void)
// --- Master initialization --- {
void evse_link_master_init(void) { if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled())
if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled()) {
return; return;
}
ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id()); ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_master); evse_link_register_rx_cb(on_frame_master);
// register loadbalancer event // Cria queue/task de ACK uma vez
ESP_ERROR_CHECK( if (s_ack_q == NULL)
esp_event_handler_register( {
s_ack_q = xQueueCreate(ACK_QUEUE_LEN, sizeof(uint8_t));
if (!s_ack_q)
{
ESP_LOGE(TAG, "Failed to create ACK queue");
}
else
{
if (xTaskCreate(ack_task, "evse_ack", 4096, NULL, 4, &s_ack_task) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create ACK task");
vQueueDelete(s_ack_q);
s_ack_q = NULL;
s_ack_task = NULL;
}
}
}
if (!s_handlers_registered)
{
s_handlers_registered = true;
ESP_ERROR_CHECK(esp_event_handler_register(
LOADBALANCER_EVENTS, LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
on_new_limit, on_new_limit,
NULL NULL));
)
);
// create and start poll timer ESP_ERROR_CHECK(esp_event_handler_register(
poll_timer.timer = xTimerCreate("poll_tmr", AUTH_EVENTS,
poll_timer.interval, AUTH_EVENT_TAG_PROCESSED,
pdTRUE, NULL, on_auth_result,
poll_timer_cb); NULL));
xTimerStart(poll_timer.timer, 0); }
// create and start heartbeat monitor timer if (poll_timer.timer == NULL)
hb_timer.timer = xTimerCreate("hb_tmr", {
hb_timer.interval, poll_timer.timer = xTimerCreate("poll_tmr",
pdFALSE, NULL, poll_timer.interval,
hb_timer_cb); pdTRUE, NULL,
xTimerStart(hb_timer.timer, 0); poll_timer_cb);
if (poll_timer.timer)
(void)xTimerStart(poll_timer.timer, 0);
}
if (s_presence_timer == NULL)
{
s_presence_timer = xTimerCreate("presence_tmr",
pdMS_TO_TICKS(PRESENCE_CHECK_MS),
pdTRUE, NULL,
presence_timer_cb);
if (s_presence_timer)
(void)xTimerStart(s_presence_timer, 0);
else
ESP_LOGE(TAG, "Failed to create presence timer");
}
} }

View File

@@ -1,164 +1,477 @@
// === components/evse_link/src/evse_link_slave.c === // components/evse_link/src/evse_link_slave.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX: confirmação (heartbeat) é deferida via queue/task.
// 2) Proteger safe_mode (e flags relacionadas) com mux (evita race entre RX e timer).
// 3) Opção de política no fallback: por default faz PAUSE (mais seguro). Pode ser alterado por macro.
// 4) Reduzir ruído de logs em caminho quente (RX frames em DEBUG).
// 5) Manter semântica: só sai de safe_mode com comando explícito de potência (SET_CURRENT / RESUME).
// 6) Mantém lógica de pause SET_CURRENT=0 e resume quando >0.
//
// Nota: A enum do evse_state_event_data_t que enviaste está correta para o handler.
#include "evse_link.h" #include "evse_link.h"
#include "evse_link_events.h" #include "evse_link_events.h"
#include "loadbalancer_events.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/timers.h" #include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/portmacro.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_random.h"
#include "esp_err.h"
#include "evse_events.h" #include "evse_events.h"
#include "evse_state.h" #include "evse_state.h"
#include "evse_config.h" #include "evse_config.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
static const char *TAG = "evse_link_slave"; static const char *TAG = "evse_link_slave";
// Link commands #define MASTER_ID 0x01
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 // not used by slave
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_HEARTBEAT_ACK 0x09
// payload lengths (exclui seq byte) // Commands (opcode no payload[0])
#define LEN_POLL_REQ 1 // [ CMD_POLL ] #define CMD_POLL 0x01
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ] #define CMD_HEARTBEAT 0x02
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ] #define CMD_CONFIG_BROADCAST 0x03
#define LEN_HEARTBEAT_ACK 1 // [ CMD_HEARTBEAT_ACK ] #define CMD_SET_CURRENT 0x08
#define LEN_HEARTBEAT 6 // CMD_HEARTBEAT + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi #define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A
// #define CMD_RESUME 0x0B // se existir
// Timing // lengths (INCLUI opcode)
#define FALLBACK_TIMEOUT_MS 120000 #define LEN_SET_CURRENT 3
#define FALLBACK_TIMEOUT_MS 180000
// --- Política de fallback ---
// 1 = mais seguro: PAUSE (revoga autorização)
// 0 = força corrente mínima (pode continuar a carregar dependendo do core)
#ifndef EVSE_LINK_FALLBACK_PAUSE
#define EVSE_LINK_FALLBACK_PAUSE 1
#endif
// --- Confirmações via heartbeat (deferidas) ---
#define HB_REQ_QUEUE_LEN 8
typedef enum
{
HB_REQ_SEND = 1,
} hb_req_t;
static TimerHandle_t fallback_timer = NULL; static TimerHandle_t fallback_timer = NULL;
static bool safe_mode = false; static TaskHandle_t hb_task_handle = NULL;
// --- Helper to send a heartbeat frame --- // Task para enviar heartbeat sem bloquear RX callback
static void send_heartbeat_frame(void) { static TaskHandle_t hb_sender_task_handle = NULL;
bool charging = evse_state_is_charging(evse_get_state()); static QueueHandle_t hb_req_q = NULL;
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA", // "safe mode" (master offline): aplica fallback local e nao sai com POLL/ACK.
charging, hw_max, runtime); // Só sai com comando explícito de potência (SET_CURRENT / RESUME).
static bool safe_mode = false;
static uint16_t saved_runtime_limit = 0; // informativo
// "remote pause" (SET_CURRENT=0)
static bool paused_by_master = false;
static bool paused_prev_authorized = false;
static portMUX_TYPE s_state_mux = portMUX_INITIALIZER_UNLOCKED;
static bool evse_handler_registered = false;
static size_t bounded_strlen_u8(const uint8_t *s, size_t max_len)
{
size_t i = 0;
if (!s)
return 0;
while (i < max_len && s[i] != 0)
i++;
return i;
}
static void send_heartbeat_frame_now(void)
{
bool charging = evse_state_is_charging(evse_get_state());
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
uint8_t hb[] = { uint8_t hb[] = {
CMD_HEARTBEAT, CMD_HEARTBEAT,
charging ? 1 : 0, charging ? 1 : 0,
(uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8), (uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8),
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8) (uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)};
};
// Broadcast to master (0xFF) (void)evse_link_send(MASTER_ID, hb, sizeof(hb)); // UNICAST
evse_link_send(0xFF, hb, sizeof(hb)); ESP_LOGI(TAG, "Send Heartbeat Frame");
} }
// pede heartbeat sem bloquear quem chama (RX callback, event handler, etc.)
// --- EVSE state change handler --- static void request_heartbeat_send(void)
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { {
if (base!=EVSE_EVENTS || id!=EVSE_EVENT_STATE_CHANGED || data==NULL) return; if (hb_req_q)
const evse_state_event_data_t *evt = data; {
if (evt->state==EVSE_STATE_EVENT_IDLE || evt->state==EVSE_STATE_EVENT_CHARGING) { hb_req_t req = HB_REQ_SEND;
send_heartbeat_frame(); // não bloqueia; se encher, apenas ignora (heartbeat periódico já existe)
(void)xQueueSendToBack(hb_req_q, &req, 0);
} }
else
{
// fallback: se queue ainda não existe, manda direto
send_heartbeat_frame_now();
}
}
static void hb_sender_task(void *arg)
{
(void)arg;
hb_req_t req;
for (;;)
{
if (xQueueReceive(hb_req_q, &req, portMAX_DELAY) != pdTRUE)
continue;
if (req == HB_REQ_SEND)
{
send_heartbeat_frame_now();
}
}
}
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
return;
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
// Enforce pause: se algo tentar voltar a carregar enquanto paused_by_master, revoga auth.
bool paused;
portENTER_CRITICAL(&s_state_mux);
paused = paused_by_master;
portEXIT_CRITICAL(&s_state_mux);
if (paused && evt->state == EVSE_STATE_EVENT_CHARGING)
{
// Garante que não continua a carregar por clamp/auto-auth
evse_state_set_authorized(false);
}
// Envia heartbeat quando entra em IDLE ou CHARGING (estado relevante)
if (evt->state == EVSE_STATE_EVENT_IDLE || evt->state == EVSE_STATE_EVENT_CHARGING)
request_heartbeat_send();
}
// Sai de safe-mode APENAS com comando explícito (SET_CURRENT / RESUME).
static void maybe_exit_safe_mode_on_explicit_power_cmd(uint8_t cmd)
{
bool in_safe;
portENTER_CRITICAL(&s_state_mux);
in_safe = safe_mode;
portEXIT_CRITICAL(&s_state_mux);
if (!in_safe)
return;
if (cmd == CMD_SET_CURRENT /*|| cmd == CMD_RESUME*/)
{
portENTER_CRITICAL(&s_state_mux);
safe_mode = false;
portEXIT_CRITICAL(&s_state_mux);
ESP_LOGI(TAG, "Exiting safe mode due to explicit cmd 0x%02X", cmd);
}
}
static void apply_pause_by_master(void)
{
bool prev_auth = evse_state_get_authorized();
portENTER_CRITICAL(&s_state_mux);
paused_by_master = true;
paused_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_state_mux);
// Revoga autorização para parar contactor/pilot via core
if (prev_auth)
evse_state_set_authorized(false);
// Mantém runtime num valor seguro (não 0, pois clamp -> 6A).
// O "pause" efetivo é pela autorização=false.
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
}
static void clear_pause_by_master_if_any(void)
{
bool was_paused;
bool prev_auth;
portENTER_CRITICAL(&s_state_mux);
was_paused = paused_by_master;
prev_auth = paused_prev_authorized;
paused_by_master = false;
paused_prev_authorized = false;
portEXIT_CRITICAL(&s_state_mux);
// Se estava autorizado antes do pause, tenta reautorizar
if (was_paused && prev_auth)
evse_state_set_authorized(true);
} }
static void on_frame_slave(uint8_t src, uint8_t dest, static void on_frame_slave(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) { const uint8_t *payload, uint8_t len)
if (dest != evse_link_get_self_id() && dest != 0xFF) return; {
if (len < 1) return; const uint8_t self = evse_link_get_self_id();
// Muito verboso em caminho quente; deixa em DEBUG
ESP_LOGD(TAG, "RX frames (src=0x%02X dest=0x%02X len=%u self=0x%02X)", src, dest, len, self);
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
{
ESP_LOGW(TAG, "RX invalid: payload NULL or len<1 (len=%u)", len);
return;
}
// Só aceitar comandos do master
if (src != MASTER_ID)
{
ESP_LOGW(TAG, "RX ignore: non-master src=0x%02X", src);
return;
}
// Qualquer frame válido do master => link vivo (reset do fallback)
if (fallback_timer)
(void)xTimerReset(fallback_timer, 0);
uint8_t cmd = payload[0]; uint8_t cmd = payload[0];
switch (cmd) {
switch (cmd)
{
case CMD_POLL: case CMD_POLL:
ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src); // Liveness only. Não sai de safe-mode e não restaura limites.
ESP_LOGI(TAG, "CMD_POLL from 0x%02X", src);
break; break;
case CMD_CONFIG_BROADCAST: case CMD_CONFIG_BROADCAST:
ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src); ESP_LOGI(TAG, "CMD_CONFIG_BROADCAST from 0x%02X", src);
break; break;
case CMD_SET_CURRENT: {
uint16_t amps = payload[1] | (payload[2] << 8);
evse_set_runtime_charging_current(amps);
ESP_LOGI(TAG, "Applied runtime limit: %uA from master 0x%02X", amps, src);
esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
case CMD_HEARTBEAT_ACK: case CMD_HEARTBEAT_ACK:
ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src); ESP_LOGI(TAG, "HEARTBEAT_ACK from 0x%02X", src);
if (fallback_timer) {
xTimerReset(fallback_timer, 0);
if (safe_mode) {
safe_mode = false;
uint16_t current = evse_get_runtime_charging_current();
evse_set_runtime_charging_current(current);
ESP_LOGI(TAG, "Exiting safe mode, restoring %uA", current);
}
}
break; break;
case CMD_SET_CURRENT:
{
ESP_LOGI(TAG, "SET_CURRENT from 0x%02X", src);
if (len < LEN_SET_CURRENT)
{
ESP_LOGW(TAG, "SET_CURRENT invalid len=%u from 0x%02X", len, src);
break;
}
uint16_t amps = (uint16_t)(payload[1] | ((uint16_t)payload[2] << 8));
// Comando explícito => pode sair de safe-mode (mesmo se for pause)
maybe_exit_safe_mode_on_explicit_power_cmd(cmd);
if (amps == 0)
{
// PAUSE explícito
ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE (src=0x%02X)", src);
apply_pause_by_master();
// Confirma sem bloquear RX
request_heartbeat_send();
// Publica evento com 0A (semântica: pause)
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
clear_pause_by_master_if_any();
evse_set_runtime_charging_current(amps);
// confirma sem bloquear RX
request_heartbeat_send();
ESP_LOGI(TAG, "Applied runtime limit: %uA from 0x%02X", (unsigned)amps, src);
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
case CMD_AUTH_GRANTED:
{
if (len < 2)
{
ESP_LOGW(TAG, "AUTH_GRANTED invalid len=%u from 0x%02X", len, src);
break;
}
const uint8_t *tag_ptr = &payload[1];
size_t tag_buf_len = (size_t)(len - 1);
size_t tag_len = bounded_strlen_u8(tag_ptr, tag_buf_len);
evse_link_auth_grant_event_t ev = {0};
size_t copy_len = tag_len;
if (copy_len > (EVSE_LINK_TAG_MAX_LEN - 1))
copy_len = EVSE_LINK_TAG_MAX_LEN - 1;
if (copy_len > 0)
memcpy(ev.tag, tag_ptr, copy_len);
ev.tag[copy_len] = '\0';
// AUTH_GRANTED não deve sair de safe-mode automaticamente
ESP_LOGI(TAG, "AUTH_GRANTED from 0x%02X tag='%s'", src, ev.tag);
esp_err_t err = esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_REMOTE_AUTH_GRANTED,
&ev, sizeof(ev),
portMAX_DELAY);
if (err != ESP_OK)
ESP_LOGE(TAG, "Failed to post LINK_EVENT_REMOTE_AUTH_GRANTED: %s", esp_err_to_name(err));
break;
}
default: default:
ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src); ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
break;
} }
} }
static void slave_heartbeat_task(void *arg)
{
(void)arg;
const uint32_t period_ms = 60000;
uint8_t id = evse_link_get_self_id();
// --- Periodic heartbeat task --- // desfasamento por ID: 2s, 4s, 6s...
static void slave_heartbeat_task(void *arg) { vTaskDelay(pdMS_TO_TICKS((uint32_t)id * 2000));
const TickType_t interval = pdMS_TO_TICKS(10000);
for (;;) { for (;;)
send_heartbeat_frame(); {
vTaskDelay(interval); request_heartbeat_send();
// jitter opcional
uint32_t jitter_ms = esp_random() % 201; // 0..200ms
vTaskDelay(pdMS_TO_TICKS(period_ms + jitter_ms));
} }
} }
// --- Fallback safe mode callback --- static void fallback_timer_cb(TimerHandle_t xTimer)
static void fallback_timer_cb(TimerHandle_t xTimer) { {
if (!safe_mode) { (void)xTimer;
// entra safe_mode uma vez
bool already_safe;
portENTER_CRITICAL(&s_state_mux);
already_safe = safe_mode;
if (!safe_mode)
safe_mode = true; safe_mode = true;
ESP_LOGW(TAG, "Fallback timeout: entering safe mode"); portEXIT_CRITICAL(&s_state_mux);
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
esp_event_post(EVSE_LINK_EVENTS, if (already_safe)
LINK_EVENT_SLAVE_OFFLINE, return;
NULL, 0, portMAX_DELAY);
} saved_runtime_limit = evse_get_runtime_charging_current();
#if EVSE_LINK_FALLBACK_PAUSE
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA). Policy=PAUSE",
(unsigned)saved_runtime_limit);
// pausar é mais seguro quando o master “morre”
apply_pause_by_master();
#else
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA, forcing %uA). Policy=MIN",
(unsigned)saved_runtime_limit, (unsigned)MIN_CHARGING_CURRENT_LIMIT);
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
#endif
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE,
NULL, 0, portMAX_DELAY);
// opcional: manda heartbeat para indicar estado atual
request_heartbeat_send();
} }
// --- Slave initialization --- void evse_link_slave_init(void)
void evse_link_slave_init(void) { {
if (evse_link_get_mode()!=EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) return; if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled())
return;
ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id()); ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_slave); evse_link_register_rx_cb(on_frame_slave);
// start periodic heartbeat // cria queue/task do sender (para não mandar UART TX no callback RX)
xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 5, NULL); if (hb_req_q == NULL)
{
// fallback timer hb_req_q = xQueueCreate(HB_REQ_QUEUE_LEN, sizeof(hb_req_t));
fallback_timer = xTimerCreate("fallback_tmr", if (!hb_req_q)
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS), {
pdFALSE, NULL, ESP_LOGE(TAG, "Failed to create HB request queue (fallback to direct send)");
fallback_timer_cb); }
if (fallback_timer) { else
xTimerStart(fallback_timer, 0); {
if (xTaskCreate(hb_sender_task, "hb_sender", 3072, NULL, 3, &hb_sender_task_handle) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create hb_sender task");
vQueueDelete(hb_req_q);
hb_req_q = NULL;
hb_sender_task_handle = NULL;
}
}
} }
// react to EVSE state changes if (hb_task_handle == NULL)
ESP_ERROR_CHECK( {
esp_event_handler_register( if (xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 3, &hb_task_handle) != pdPASS)
EVSE_EVENTS, {
EVSE_EVENT_STATE_CHANGED, ESP_LOGE(TAG, "Failed to create slave_heartbeat_task");
evse_event_handler, hb_task_handle = NULL;
NULL }
) }
);
if (fallback_timer == NULL)
{
fallback_timer = xTimerCreate("fallback_tmr",
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS),
pdFALSE, NULL,
fallback_timer_cb);
if (fallback_timer)
(void)xTimerStart(fallback_timer, 0);
else
ESP_LOGE(TAG, "Failed to create fallback timer");
}
else
{
(void)xTimerReset(fallback_timer, 0);
}
if (!evse_handler_registered)
{
ESP_ERROR_CHECK(esp_event_handler_register(
EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED,
evse_event_handler, NULL));
evse_handler_registered = true;
}
} }

View File

@@ -1,3 +0,0 @@
idf_component_register(SRCS "button/button.c" "button/button_obj.cpp"
INCLUDE_DIRS "button/include"
REQUIRES "driver")

View File

@@ -1,6 +0,0 @@
menu "GPIO Button"
config IO_GLITCH_FILTER_TIME_MS
int "IO glitch filter timer ms (10~100)"
range 10 100
default 50
endmenu

View File

@@ -1,353 +0,0 @@
// Copyright 2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#include <freertos/timers.h>
#include <esp_log.h>
#include <driver/gpio.h>
#include <iot_button.h>
#define IOT_CHECK(tag, a, ret) if(!(a)) { \
ESP_LOGE(tag,"%s:%d (%s)", __FILE__, __LINE__, __FUNCTION__); \
return (ret); \
}
#define ERR_ASSERT(tag, param) IOT_CHECK(tag, (param) == ESP_OK, ESP_FAIL)
#define POINT_ASSERT(tag, param, ret) IOT_CHECK(tag, (param) != NULL, (ret))
typedef enum {
BUTTON_STATE_IDLE = 0,
BUTTON_STATE_PUSH,
BUTTON_STATE_PRESSED,
} button_status_t;
typedef struct button_dev button_dev_t;
typedef struct btn_cb button_cb_t;
struct btn_cb{
TickType_t interval;
button_cb cb;
void* arg;
uint8_t on_press;
TimerHandle_t tmr;
button_dev_t *pbtn;
button_cb_t *next_cb;
};
struct button_dev{
uint8_t io_num;
uint8_t active_level;
uint32_t serial_thres_sec;
uint8_t taskq_on;
QueueHandle_t taskq;
QueueHandle_t argq;
button_status_t state;
button_cb_t tap_short_cb;
button_cb_t tap_psh_cb;
button_cb_t tap_rls_cb;
button_cb_t press_serial_cb;
button_cb_t* cb_head;
};
#define BUTTON_GLITCH_FILTER_TIME_MS CONFIG_IO_GLITCH_FILTER_TIME_MS
static const char* TAG = "button";
static void button_press_cb(TimerHandle_t tmr)
{
button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr);
button_dev_t* btn = btn_cb->pbtn;
// low, then restart
if (btn->active_level == gpio_get_level(btn->io_num)) {
btn->state = BUTTON_STATE_PRESSED;
if (btn->taskq != NULL && btn->argq != NULL && btn->taskq_on && !btn_cb->on_press) {
void *tmp = btn_cb->cb;
xQueueOverwrite(btn->taskq, &tmp);
xQueueOverwrite(btn->argq, &btn_cb->arg);
} else if (btn_cb->cb) {
btn_cb->cb(btn_cb->arg);
}
}
}
static void button_tap_psh_cb(TimerHandle_t tmr)
{
button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr);
button_dev_t* btn = btn_cb->pbtn;
xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY);
int lv = gpio_get_level(btn->io_num);
if (btn->active_level == lv) {
// True implies key is pressed
btn->state = BUTTON_STATE_PUSH;
if (btn->press_serial_cb.tmr) {
xTimerChangePeriod(btn->press_serial_cb.tmr, btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, portMAX_DELAY);
xTimerReset(btn->press_serial_cb.tmr, portMAX_DELAY);
}
if (btn->tap_psh_cb.cb) {
btn->tap_psh_cb.cb(btn->tap_psh_cb.arg);
}
} else {
// 50ms, check if this is a real key up
if (btn->tap_rls_cb.tmr) {
xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY);
xTimerReset(btn->tap_rls_cb.tmr, portMAX_DELAY);
}
}
}
static void button_tap_rls_cb(TimerHandle_t tmr)
{
button_cb_t* btn_cb = (button_cb_t*) pvTimerGetTimerID(tmr);
button_dev_t* btn = btn_cb->pbtn;
xTimerStop(btn->tap_rls_cb.tmr, portMAX_DELAY);
if (btn->active_level == gpio_get_level(btn->io_num)) {
} else {
// high, then key is up
button_cb_t *pcb = btn->cb_head;
while (pcb != NULL) {
if (pcb->tmr != NULL) {
xTimerStop(pcb->tmr, portMAX_DELAY);
}
pcb = pcb->next_cb;
}
if (btn->taskq != NULL && btn->argq != NULL && btn->taskq_on && uxQueueMessagesWaiting(btn->taskq) != 0 && btn->state != BUTTON_STATE_IDLE) {
void (*task)(void*);
void *arg;
xQueueReceive(btn->taskq, &task, 0);
xQueueReceive(btn->argq, &arg, 0);
task(arg);
}
if (btn->press_serial_cb.tmr && btn->press_serial_cb.tmr != NULL) {
xTimerStop(btn->press_serial_cb.tmr, portMAX_DELAY);
}
if (btn->tap_short_cb.cb && btn->state == BUTTON_STATE_PUSH) {
btn->tap_short_cb.cb(btn->tap_short_cb.arg);
}
if(btn->tap_rls_cb.cb && btn->state != BUTTON_STATE_IDLE) {
btn->tap_rls_cb.cb(btn->tap_rls_cb.arg);
}
btn->state = BUTTON_STATE_IDLE;
}
}
static void button_press_serial_cb(TimerHandle_t tmr)
{
button_dev_t* btn = (button_dev_t*) pvTimerGetTimerID(tmr);
if (btn->press_serial_cb.cb) {
btn->press_serial_cb.cb(btn->press_serial_cb.arg);
}
xTimerChangePeriod(btn->press_serial_cb.tmr, btn->press_serial_cb.interval, portMAX_DELAY);
xTimerReset(btn->press_serial_cb.tmr, portMAX_DELAY);
}
static void button_gpio_isr_handler(void* arg)
{
button_dev_t* btn = (button_dev_t*) arg;
portBASE_TYPE HPTaskAwoken = pdFALSE;
int level = gpio_get_level(btn->io_num);
if (level == btn->active_level) {
if (btn->tap_psh_cb.tmr) {
xTimerStopFromISR(btn->tap_psh_cb.tmr, &HPTaskAwoken);
xTimerResetFromISR(btn->tap_psh_cb.tmr, &HPTaskAwoken);
}
button_cb_t *pcb = btn->cb_head;
while (pcb != NULL) {
if (pcb->tmr != NULL) {
xTimerStopFromISR(pcb->tmr, &HPTaskAwoken);
xTimerResetFromISR(pcb->tmr, &HPTaskAwoken);
}
pcb = pcb->next_cb;
}
} else {
// 50ms, check if this is a real key up
if (btn->tap_rls_cb.tmr) {
xTimerStopFromISR(btn->tap_rls_cb.tmr, &HPTaskAwoken);
xTimerResetFromISR(btn->tap_rls_cb.tmr, &HPTaskAwoken);
}
}
if(HPTaskAwoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
static void button_free_tmr(TimerHandle_t* tmr)
{
if (tmr && *tmr) {
xTimerStop(*tmr, portMAX_DELAY);
xTimerDelete(*tmr, portMAX_DELAY);
*tmr = NULL;
}
}
esp_err_t iot_button_delete(button_handle_t btn_handle)
{
POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG);
button_dev_t* btn = (button_dev_t*) btn_handle;
gpio_set_intr_type(btn->io_num, GPIO_INTR_DISABLE);
gpio_isr_handler_remove(btn->io_num);
button_free_tmr(&btn->tap_rls_cb.tmr);
button_free_tmr(&btn->tap_psh_cb.tmr);
button_free_tmr(&btn->tap_short_cb.tmr);
button_free_tmr(&btn->press_serial_cb.tmr);
button_cb_t *pcb = btn->cb_head;
while (pcb != NULL) {
button_cb_t *cb_next = pcb->next_cb;
button_free_tmr(&pcb->tmr);
free(pcb);
pcb = cb_next;
}
free(btn);
return ESP_OK;
}
button_handle_t iot_button_create(gpio_num_t gpio_num, button_active_t active_level)
{
IOT_CHECK(TAG, gpio_num < GPIO_NUM_MAX, NULL);
button_dev_t* btn = (button_dev_t*) calloc(1, sizeof(button_dev_t));
POINT_ASSERT(TAG, btn, NULL);
btn->active_level = active_level;
btn->io_num = gpio_num;
btn->state = BUTTON_STATE_IDLE;
btn->taskq_on = 0;
btn->taskq = xQueueCreate(1, sizeof(void*));
btn->argq = xQueueCreate(1, sizeof(void *));
btn->tap_rls_cb.arg = NULL;
btn->tap_rls_cb.cb = NULL;
btn->tap_rls_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS;
btn->tap_rls_cb.pbtn = btn;
btn->tap_rls_cb.tmr = xTimerCreate("btn_rls_tmr", btn->tap_rls_cb.interval, pdFALSE,
&btn->tap_rls_cb, button_tap_rls_cb);
btn->tap_psh_cb.arg = NULL;
btn->tap_psh_cb.cb = NULL;
btn->tap_psh_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS;
btn->tap_psh_cb.pbtn = btn;
btn->tap_psh_cb.tmr = xTimerCreate("btn_psh_tmr", btn->tap_psh_cb.interval, pdFALSE,
&btn->tap_psh_cb, button_tap_psh_cb);
gpio_install_isr_service(0);
gpio_config_t gpio_conf;
gpio_conf.intr_type = GPIO_INTR_ANYEDGE;
gpio_conf.mode = GPIO_MODE_INPUT;
gpio_conf.pin_bit_mask = (uint64_t)1 << gpio_num;
gpio_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_conf.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_conf);
gpio_isr_handler_add(gpio_num, button_gpio_isr_handler, btn);
return (button_handle_t) btn;
}
esp_err_t iot_button_rm_cb(button_handle_t btn_handle, button_cb_type_t type)
{
button_dev_t* btn = (button_dev_t*) btn_handle;
button_cb_t* btn_cb = NULL;
if (type == BUTTON_CB_PUSH) {
btn_cb = &btn->tap_psh_cb;
} else if (type == BUTTON_CB_RELEASE) {
btn_cb = &btn->tap_rls_cb;
} else if (type == BUTTON_CB_TAP) {
btn_cb = &btn->tap_short_cb;
} else if (type == BUTTON_CB_SERIAL) {
btn_cb = &btn->press_serial_cb;
}
btn_cb->cb = NULL;
btn_cb->arg = NULL;
btn_cb->pbtn = btn;
button_free_tmr(&btn_cb->tmr);
return ESP_OK;
}
esp_err_t iot_button_set_serial_cb(button_handle_t btn_handle, uint32_t start_after_sec, TickType_t interval_tick, button_cb cb, void* arg)
{
button_dev_t* btn = (button_dev_t*) btn_handle;
btn->serial_thres_sec = start_after_sec;
if (btn->press_serial_cb.tmr == NULL) {
btn->press_serial_cb.tmr = xTimerCreate("btn_serial_tmr", btn->serial_thres_sec*1000 / portTICK_PERIOD_MS,
pdFALSE, btn, button_press_serial_cb);
}
btn->press_serial_cb.arg = arg;
btn->press_serial_cb.cb = cb;
btn->press_serial_cb.interval = interval_tick;
btn->press_serial_cb.pbtn = btn;
xTimerChangePeriod(btn->press_serial_cb.tmr, btn->serial_thres_sec*1000 / portTICK_PERIOD_MS, portMAX_DELAY);
return ESP_OK;
}
esp_err_t iot_button_set_evt_cb(button_handle_t btn_handle, button_cb_type_t type, button_cb cb, void* arg)
{
POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG);
button_dev_t* btn = (button_dev_t*) btn_handle;
if (type == BUTTON_CB_PUSH) {
btn->tap_psh_cb.arg = arg;
btn->tap_psh_cb.cb = cb;
btn->tap_psh_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS;
btn->tap_psh_cb.pbtn = btn;
xTimerChangePeriod(btn->tap_psh_cb.tmr, btn->tap_psh_cb.interval, portMAX_DELAY);
} else if (type == BUTTON_CB_RELEASE) {
btn->tap_rls_cb.arg = arg;
btn->tap_rls_cb.cb = cb;
btn->tap_rls_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS;
btn->tap_rls_cb.pbtn = btn;
xTimerChangePeriod(btn->tap_rls_cb.tmr, btn->tap_psh_cb.interval, portMAX_DELAY);
} else if (type == BUTTON_CB_TAP) {
btn->tap_short_cb.arg = arg;
btn->tap_short_cb.cb = cb;
btn->tap_short_cb.interval = BUTTON_GLITCH_FILTER_TIME_MS / portTICK_PERIOD_MS;
btn->tap_short_cb.pbtn = btn;
} else if (type == BUTTON_CB_SERIAL) {
iot_button_set_serial_cb(btn_handle, 1, 1000 / portTICK_PERIOD_MS, cb, arg);
}
return ESP_OK;
}
esp_err_t iot_button_add_on_press_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg)
{
POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG);
IOT_CHECK(TAG, press_sec != 0, ESP_ERR_INVALID_ARG);
button_dev_t* btn = (button_dev_t*) btn_handle;
button_cb_t* cb_new = (button_cb_t*) calloc(1, sizeof(button_cb_t));
POINT_ASSERT(TAG, cb_new, ESP_FAIL);
cb_new->on_press = 1;
cb_new->arg = arg;
cb_new->cb = cb;
cb_new->interval = press_sec * 1000 / portTICK_PERIOD_MS;
cb_new->pbtn = btn;
cb_new->tmr = xTimerCreate("btn_press_tmr", cb_new->interval, pdFALSE, cb_new, button_press_cb);
cb_new->next_cb = btn->cb_head;
btn->cb_head = cb_new;
return ESP_OK;
}
esp_err_t iot_button_add_on_release_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg)
{
POINT_ASSERT(TAG, btn_handle, ESP_ERR_INVALID_ARG);
IOT_CHECK(TAG, press_sec != 0, ESP_ERR_INVALID_ARG);
button_dev_t* btn = (button_dev_t*) btn_handle;
button_cb_t* cb_new = (button_cb_t*) calloc(1, sizeof(button_cb_t));
POINT_ASSERT(TAG, cb_new, ESP_FAIL);
btn->taskq_on = 1;
cb_new->arg = arg;
cb_new->cb = cb;
cb_new->interval = press_sec * 1000 / portTICK_PERIOD_MS;
cb_new->pbtn = btn;
cb_new->tmr = xTimerCreate("btn_press_tmr", cb_new->interval, pdFALSE, cb_new, button_press_cb);
cb_new->next_cb = btn->cb_head;
btn->cb_head = cb_new;
return ESP_OK;
}

View File

@@ -1,54 +0,0 @@
// Copyright 2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_system.h>
#include <iot_button.h>
CButton::CButton(gpio_num_t gpio_num, button_active_t active_level)
{
m_btn_handle = iot_button_create(gpio_num, active_level);
}
CButton::~CButton()
{
iot_button_delete(m_btn_handle);
m_btn_handle = NULL;
}
esp_err_t CButton::set_evt_cb(button_cb_type_t type, button_cb cb, void* arg)
{
return iot_button_set_evt_cb(m_btn_handle, type, cb, arg);
}
esp_err_t CButton::set_serial_cb(button_cb cb, void* arg, TickType_t interval_tick, uint32_t start_after_sec)
{
return iot_button_set_serial_cb(m_btn_handle, start_after_sec, interval_tick, cb, arg);
}
esp_err_t CButton::add_on_press_cb(uint32_t press_sec, button_cb cb, void* arg)
{
return iot_button_add_on_press_cb(m_btn_handle, press_sec, cb, arg);
}
esp_err_t CButton::add_on_release_cb(uint32_t press_sec, button_cb cb, void* arg)
{
return iot_button_add_on_release_cb(m_btn_handle, press_sec, cb, arg);
}
esp_err_t CButton::rm_cb(button_cb_type_t type)
{
return iot_button_rm_cb(m_btn_handle, type);
}

View File

@@ -1,272 +0,0 @@
// Copyright 2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _IOT_BUTTON_H_
#define _IOT_BUTTON_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <driver/gpio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
typedef void (* button_cb)(void*);
typedef void* button_handle_t;
typedef enum {
BUTTON_ACTIVE_HIGH = 1, /*!<button active level: high level*/
BUTTON_ACTIVE_LOW = 0, /*!<button active level: low level*/
} button_active_t;
typedef enum {
BUTTON_CB_PUSH = 0, /*!<button push callback event */
BUTTON_CB_RELEASE, /*!<button release callback event */
BUTTON_CB_TAP, /*!<button quick tap callback event(will not trigger if there already is a "PRESS" event) */
BUTTON_CB_SERIAL, /*!<button serial trigger callback event */
} button_cb_type_t;
/**
* @brief Init button functions
*
* @param gpio_num GPIO index of the pin that the button uses
* @param active_level button hardware active level.
* For "BUTTON_ACTIVE_LOW" it means when the button pressed, the GPIO will read low level.
*
* @return A button_handle_t handle to the created button object, or NULL in case of error.
*/
button_handle_t iot_button_create(gpio_num_t gpio_num, button_active_t active_level);
/**
* @brief Register a callback function for a serial trigger event.
*
* @param btn_handle handle of the button object
* @param start_after_sec define the time after which to start serial trigger action
* @param interval_tick serial trigger interval
* @param cb callback function for "TAP" action.
* @param arg Parameter for callback function
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t iot_button_set_serial_cb(button_handle_t btn_handle, uint32_t start_after_sec, TickType_t interval_tick, button_cb cb, void* arg);
/**
* @brief Register a callback function for a button_cb_type_t action.
*
* @param btn_handle handle of the button object
* @param type callback function type
* @param cb callback function for "TAP" action.
* @param arg Parameter for callback function
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t iot_button_set_evt_cb(button_handle_t btn_handle, button_cb_type_t type, button_cb cb, void* arg);
/**
* @brief Callbacks invoked as timer events occur while button is pressed.
* Example: If a button is configured for 2 sec, 5 sec and 7 sec callbacks and if the button is pressed for 6 sec then 2 sec callback would be invoked at 2 sec event and 5 sec callback would be invoked at 5 sec event
*
* @param btn_handle handle of the button object
* @param press_sec the callback function would be called if you press the button for a specified period of time
* @param cb callback function for "PRESS and HOLD" action.
* @param arg Parameter for callback function
*
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t iot_button_add_on_press_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg);
/**
* @brief Single callback invoked according to the latest timer event on button release.
* Example: If a button is configured for 2 sec, 5 sec and 7 sec callbacks and if the button is released at 6 sec then only 5 sec callback would be invoked
*
* @param btn_handle handle of the button object
* @param press_sec the callback function would be called if you press the button for a specified period of time
* @param cb callback function for "PRESS and RELEASE" action.
* @param arg Parameter for callback function
*
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t iot_button_add_on_release_cb(button_handle_t btn_handle, uint32_t press_sec, button_cb cb, void* arg);
/**
* @brief Delete button object and free memory
* @param btn_handle handle of the button object
*
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t iot_button_delete(button_handle_t btn_handle);
/**
* @brief Remove callback
*
* @param btn_handle The handle of the button object
* @param type callback function event type
*
* @return
* - ESP_OK Success
*/
esp_err_t iot_button_rm_cb(button_handle_t btn_handle, button_cb_type_t type);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
/**
* class of button
* simple usage:
* CButton* btn = new CButton(BUTTON_IO_NUM, BUTTON_ACTIVE_LEVEL, BUTTON_SERIAL_TRIGGER, 3);
* btn->add_cb(BUTTON_CB_PUSH, button_tap_cb, (void*) push, 50 / portTICK_PERIOD_MS);
* btn->add_custom_cb(5, button_press_5s_cb, NULL);
* ......
* delete btn;
*/
class CButton
{
private:
button_handle_t m_btn_handle;
/**
* prevent copy constructing
*/
CButton(const CButton&);
CButton& operator = (const CButton&);
public:
/**
* @brief constructor of CButton
*
* @param gpio_num GPIO index of the pin that the button uses
* @param active_level button hardware active level.
* For "BUTTON_ACTIVE_LOW" it means when the button pressed, the GPIO will read low level.
*/
CButton(gpio_num_t gpio_num, button_active_t active_level = BUTTON_ACTIVE_LOW);
~CButton();
/**
* @brief Register a callback function for a button_cb_type_t action.
*
* @param type callback function type
* @param cb callback function for "TAP" action.
* @param arg Parameter for callback function
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t set_evt_cb(button_cb_type_t type, button_cb cb, void* arg);
/**
* @brief Register a callback function for a serial trigger event.
*
* @param btn_handle handle of the button object
* @param start_after_sec define the time after which to start serial trigger action
* @param interval_tick serial trigger interval
* @param cb callback function for "TAP" action.
* @param arg Parameter for callback function
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t set_serial_cb(button_cb cb, void* arg, TickType_t interval_tick, uint32_t start_after_sec);
/**
* @brief Callbacks invoked as timer events occur while button is pressed
*
* @param press_sec the callback function would be called if you press the button for a specified period of time
* @param cb callback function for "PRESS and HOLD" action.
* @param arg Parameter for callback function
*
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t add_on_press_cb(uint32_t press_sec, button_cb cb, void* arg);
/**
* @brief Single callback invoked according to the latest timer event on button release.
*
* @param press_sec the callback function would be called if you press the button for a specified period of time
* @param cb callback function for "PRESS and RELEASE" action.
* @param arg Parameter for callback function
*
* @note
* Button callback functions execute in the context of the timer service task.
* It is therefore essential that button callback functions never attempt to block.
* For example, a button callback function must not call vTaskDelay(), vTaskDelayUntil(),
* or specify a non zero block time when accessing a queue or a semaphore.
* @return
* - ESP_OK Success
* - ESP_FAIL Parameter error
*/
esp_err_t add_on_release_cb(uint32_t press_sec, button_cb cb, void* arg);
/**
* @brief Remove callback
*
* @param type callback function event type
*
* @return
* - ESP_OK Success
*/
esp_err_t rm_cb(button_cb_type_t type);
};
#endif
#endif

View File

@@ -1,2 +0,0 @@
COMPONENT_ADD_INCLUDEDIRS := ./button/include
COMPONENT_SRCDIRS := ./button

6
components/led/CMakeLists.txt Executable file
View File

@@ -0,0 +1,6 @@
set(srcs "src/led.c" "src/ledc_driver.c")
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES driver esp_timer
REQUIRES config evse)

78
components/led/include/led.h Executable file
View File

@@ -0,0 +1,78 @@
#ifndef LED_H_
#define LED_H_
#include <stdint.h>
#include <stdbool.h>
/**
* @brief Identificadores dos LEDs disponíveis no hardware (por cor)
*/
typedef enum
{
LED_ID_GREEN = 0,
LED_ID_BLUE,
LED_ID_RED,
LED_ID_MAX
} led_id_t;
/**
* @brief Padrões de comportamento possíveis para os LEDs.
*
* Estes padrões são usados tanto pela lógica interna (estado EVSE, efeitos
* de sessão, etc.) como por código externo que queira forçar um determinado
* comportamento num LED.
*
* Nota:
* - LED_PATTERN_BREATHING é implementado por uma task interna (HSV).
* - LED_PATTERN_CHARGING_EFFECT é legado/experimental e pode não ser usado
* diretamente na implementação atual.
*/
typedef enum
{
LED_PATTERN_OFF, ///< LED sempre desligado
LED_PATTERN_ON, ///< LED sempre ligado
LED_PATTERN_BLINK, ///< Pisca com ciclo padrão (500ms on / 500ms off)
LED_PATTERN_BLINK_FAST, ///< Pisca rápido (250ms on / 250ms off)
LED_PATTERN_BLINK_SLOW, ///< Pisca lento (500ms on / 1500ms off)
LED_PATTERN_CHARGING_EFFECT, ///< Efeito de carregamento (2s on / 1s off) - legado/opcional
LED_PATTERN_BREATHING ///< Efeito "breathing" (brilho suave via HSV)
} led_pattern_t;
/**
* @brief Inicializa o subsistema de LEDs com base na configuração da placa.
*
* - Configura o driver LEDC com os GPIOs definidos em board_config.
* - Cria a task de efeitos (para padrões como BREATHING).
* - Regista handlers dos eventos EVSE (estado e sessão) para aplicar
* padrões automáticos em função do estado do carregador.
*
* Deve ser chamada uma única vez na inicialização do sistema.
*/
void led_init(void);
/**
* @brief Define diretamente o tempo ligado/desligado de um LED.
*
* Esta função permite criar padrões personalizados à parte da tabela
* led_pattern_t. Normalmente, código de alto nível deve preferir
* led_apply_pattern(), deixando a lógica de tempos a cargo do módulo.
*
* @param led_id Identificador do LED (ver led_id_t)
* @param ontime Tempo ligado em milissegundos (0 = sempre off)
* @param offtime Tempo desligado em milissegundos (0 = sempre on)
*/
void led_set_state(led_id_t led_id, uint16_t ontime, uint16_t offtime);
/**
* @brief Aplica um padrão de piscar/efeito definido ao LED.
*
* Esta é a API recomendada para controlar LEDs externamente. A implementação
* interna do módulo também a usa para reagir a eventos EVSE (estado de
* carregamento, início/fim de sessão, etc.).
*
* @param led_id Identificador do LED (ver led_id_t)
* @param pattern Padrão desejado (ver led_pattern_t)
*/
void led_apply_pattern(led_id_t led_id, led_pattern_t pattern);
#endif /* LED_H_ */

View File

@@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "driver/gpio.h"
/**
* @brief Inicializa o controlador LEDC para os 3 LEDs (R,G,B)
*
* @param gpio_red GPIO ligado ao LED vermelho (via ULN2003)
* @param gpio_green GPIO ligado ao LED verde (via ULN2003)
* @param gpio_blue GPIO ligado ao LED azul (via ULN2003)
*
* @return ESP_OK em sucesso, erro caso contrário
*/
esp_err_t ledc_init(gpio_num_t gpio_red,
gpio_num_t gpio_green,
gpio_num_t gpio_blue);
/**
* @brief Define a intensidade RGB (0255 por cor)
*
* @param red Intensidade do vermelho (0255)
* @param green Intensidade do verde (0255)
* @param blue Intensidade do azul (0255)
*
* @return ESP_OK em sucesso, erro caso contrário
*/
esp_err_t ledc_set_rgb(uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Desliga todos os LEDs (R,G,B)
*/
esp_err_t ledc_clear(void);

598
components/led/src/led.c Executable file
View File

@@ -0,0 +1,598 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_event.h"
#include <inttypes.h> // para PRIu32
#include "led.h"
#include "board_config.h"
#include "evse_events.h"
#include "evse_state.h"
#include "ledc_driver.h"
#define BLOCK_TIME portMAX_DELAY
static const char *TAG = "led";
typedef struct
{
bool present; ///< LED existe nesta placa
bool on : 1; ///< estado lógico atual (ligado/desligado)
uint16_t ontime; ///< ms ligado
uint16_t offtime; ///< ms desligado
TimerHandle_t timer; ///< timer de piscar (para padrões que usam on/off)
led_pattern_t pattern; ///< padrão atual
uint8_t blink_count; ///< reservado para padrões mais complexos
} led_t;
static led_t leds[LED_ID_MAX] = {0};
// ----------------------------
// Tabela de padrões (tempo on/off)
// ----------------------------
typedef struct
{
uint16_t on_ms;
uint16_t off_ms;
} led_timing_t;
// índice = led_pattern_t
static const led_timing_t led_pattern_table[] = {
[LED_PATTERN_OFF] = {0, 0},
[LED_PATTERN_ON] = {1, 0}, // 1ms só para cair na lógica "sempre ligado"
[LED_PATTERN_BLINK] = {500, 500},
[LED_PATTERN_BLINK_FAST] = {250, 250},
[LED_PATTERN_BLINK_SLOW] = {500, 1500},
[LED_PATTERN_CHARGING_EFFECT] = {2000, 1000},
// Para BREATHING, o temporizador FreeRTOS NÃO é usado; tratamos via task de efeitos.
// Mesmo assim, usamos (1,0) para marcar LED como "on" logicamente.
[LED_PATTERN_BREATHING] = {1, 0},
};
#define LED_PATTERN_COUNT (sizeof(led_pattern_table) / sizeof(led_pattern_table[0]))
// ----------------------------
// Estado base + efeitos de sessão
// ----------------------------
typedef enum
{
SESSION_EFFECT_NONE = 0,
SESSION_EFFECT_START,
SESSION_EFFECT_FINISH,
} led_session_effect_type_t;
static evse_state_event_t current_state_mode = EVSE_STATE_EVENT_IDLE;
static bool session_effect_active = false;
static TimerHandle_t session_effect_timer = NULL;
static led_session_effect_type_t session_effect_type = SESSION_EFFECT_NONE;
static uint8_t session_effect_phase = 0;
// Forwards
static void led_update_rgb_from_state(void);
static void led_effect_task(void *arg);
static void evse_led_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);
static void evse_session_led_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data);
static void session_effect_timer_cb(TimerHandle_t xTimer);
static void led_apply_state_mode(evse_state_event_t state);
// ----------------------------
// Timer de piscar (para padrões on/off)
// ----------------------------
static void led_timer_callback(TimerHandle_t xTimer)
{
led_t *led = (led_t *)pvTimerGetTimerID(xTimer);
led->on = !led->on;
uint32_t next_time = led->on ? led->ontime : led->offtime;
xTimerChangePeriod(led->timer, pdMS_TO_TICKS(next_time), BLOCK_TIME);
// Atualiza hardware (via LEDC).
led_update_rgb_from_state();
}
// ----------------------------
// Atualiza hardware a partir do estado lógico dos LEDs
// (para padrões "normais": ON/OFF/BLINK/...)
// ----------------------------
static void led_update_rgb_from_state(void)
{
uint32_t red = 0;
uint32_t green = 0;
uint32_t blue = 0;
if (LED_ID_RED < LED_ID_MAX && leds[LED_ID_RED].present && leds[LED_ID_RED].on)
red = 255;
if (LED_ID_GREEN < LED_ID_MAX && leds[LED_ID_GREEN].present && leds[LED_ID_GREEN].on)
green = 255;
if (LED_ID_BLUE < LED_ID_MAX && leds[LED_ID_BLUE].present && leds[LED_ID_BLUE].on)
blue = 255;
ledc_set_rgb(red, green, blue);
}
// ----------------------------
// Task de efeitos (BREATHING)
// ----------------------------
static void led_effect_task(void *arg)
{
// v = "intensidade" lógica 0100
uint32_t v = 0;
bool up = true;
const uint32_t v_min = 0;
const uint32_t v_max = 100;
const uint32_t step = 3;
const TickType_t delay_breathe = pdMS_TO_TICKS(50); // velocidade da respiração
const TickType_t delay_idle = pdMS_TO_TICKS(1000); // quando não há LED em BREATHING
for (;;)
{
// Verifica se algum LED está em BREATHING
bool has_breath = false;
led_id_t breath_id = LED_ID_MAX;
for (int i = 0; i < LED_ID_MAX; ++i)
{
if (leds[i].present && leds[i].pattern == LED_PATTERN_BREATHING)
{
has_breath = true;
breath_id = (led_id_t)i;
break;
}
}
if (has_breath)
{
// Usa o valor atual de v para calcular o brilho (0255)
uint32_t brightness = (v * 255U) / 100U;
uint32_t r = 0, g = 0, b = 0;
switch (breath_id)
{
case LED_ID_RED:
r = brightness;
break;
case LED_ID_GREEN:
g = brightness;
break;
case LED_ID_BLUE:
b = brightness;
break;
default:
// fallback: usa azul se algo estranho acontecer
b = brightness;
break;
}
// Aplica só um canal, sem misturar cores
ledc_set_rgb(r, g, b);
// Se estiver completamente apagado (v == 0),
// queremos que fique 500 ms off antes de voltar a subir
TickType_t delay = (v == v_min) ? pdMS_TO_TICKS(500) : delay_breathe;
// Atualiza v para a próxima iteração (triângulo 0→100→0)
if (up)
{
if (v + step >= v_max)
{
v = v_max;
up = false;
}
else
{
v += step;
}
}
else
{
if (v <= v_min + step)
{
v = v_min;
up = true;
}
else
{
v -= step;
}
}
vTaskDelay(delay);
}
else
{
// Ninguém em BREATHING: deixa padrões normais controlarem o LED
led_update_rgb_from_state();
vTaskDelay(delay_idle);
// Opcional: quando sair de BREATHING e voltar mais tarde,
// começa de novo a partir de apagado
v = v_min;
up = true;
}
}
}
// ----------------------------
// Aplica padrão em função do estado EVSE (base)
// GARANTINDO apenas 1 LED por estado
// ----------------------------
static void led_apply_state_mode(evse_state_event_t state)
{
// Desliga padrões anteriores para todos os canais
for (int i = 0; i < LED_ID_MAX; ++i)
{
led_apply_pattern((led_id_t)i, LED_PATTERN_OFF);
}
switch (state)
{
case EVSE_STATE_EVENT_IDLE:
// IDLE → verde fixo (claro e visível)
led_apply_pattern(LED_ID_GREEN, LED_PATTERN_ON);
break;
case EVSE_STATE_EVENT_WAITING:
// WAITING → azul a piscar lento
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_BLINK_SLOW);
break;
case EVSE_STATE_EVENT_CHARGING:
// CHARGING → azul "breathing"
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_BREATHING);
break;
case EVSE_STATE_EVENT_FAULT:
// FAULT → vermelho a piscar rápido
led_apply_pattern(LED_ID_RED, LED_PATTERN_BLINK_FAST);
break;
default:
break;
}
led_update_rgb_from_state();
}
// ----------------------------
// Timer callback do efeito de sessão
// Efeitos usam sempre UM LED forte
// ----------------------------
static void session_effect_timer_cb(TimerHandle_t xTimer)
{
switch (session_effect_type)
{
case SESSION_EFFECT_START:
session_effect_phase++;
switch (session_effect_phase)
{
case 1:
// Fase 1: depois de flash sólido, passa a azul a piscar rápido
for (int i = 0; i < LED_ID_MAX; ++i)
{
led_apply_pattern((led_id_t)i, LED_PATTERN_OFF);
}
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_BLINK_FAST);
led_update_rgb_from_state();
// Mantém piscar rápido mais um bocado
xTimerChangePeriod(session_effect_timer,
pdMS_TO_TICKS(5000),
BLOCK_TIME);
xTimerStart(session_effect_timer, BLOCK_TIME);
break;
case 2:
default:
// Fim do efeito de START → volta ao estado base (tipicamente CHARGING)
session_effect_active = false;
session_effect_type = SESSION_EFFECT_NONE;
session_effect_phase = 0;
led_apply_state_mode(current_state_mode);
break;
}
break;
case SESSION_EFFECT_FINISH:
session_effect_phase++;
switch (session_effect_phase)
{
case 1:
// Fase 1: depois de flash sólido, passa a azul a piscar rápido
for (int i = 0; i < LED_ID_MAX; ++i)
{
led_apply_pattern((led_id_t)i, LED_PATTERN_OFF);
}
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_BLINK_FAST);
led_update_rgb_from_state();
// Mantém piscar rápido mais tempo (destaque de fim de sessão)
xTimerChangePeriod(session_effect_timer,
pdMS_TO_TICKS(5000),
BLOCK_TIME);
xTimerStart(session_effect_timer, BLOCK_TIME);
break;
case 2:
default:
// Fim do efeito de FINISH → volta ao estado base (IDLE/WAITING)
session_effect_active = false;
session_effect_type = SESSION_EFFECT_NONE;
session_effect_phase = 0;
led_apply_state_mode(current_state_mode);
break;
}
break;
case SESSION_EFFECT_NONE:
default:
session_effect_active = false;
session_effect_phase = 0;
led_apply_state_mode(current_state_mode);
break;
}
}
// ----------------------------
// Event Handler: EVSE State
// ----------------------------
static void evse_led_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
{
return;
}
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
ESP_LOGD(TAG, "EVSE State Changed: state=%d", evt->state);
// Atualiza o estado base
current_state_mode = evt->state;
// Se estiver a decorrer um efeito de sessão, não mexe agora nos LEDs.
if (session_effect_active)
{
return;
}
led_apply_state_mode(current_state_mode);
}
// ----------------------------
// Event Handler: EVSE Session
// (efeitos de início/fim de sessão, 1 LED de cada vez)
// ----------------------------
static void evse_session_led_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base != EVSE_EVENTS || id != EVSE_EVENT_SESSION || data == NULL)
{
return;
}
const evse_session_event_data_t *evt =
(const evse_session_event_data_t *)data;
ESP_LOGD(TAG,
"EVSE Session Event: type=%d, id=%" PRIu32
", duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg=%" PRIu32 " W, current=%d",
(int)evt->type,
evt->session_id,
evt->duration_s,
evt->energy_wh,
evt->avg_power_w,
evt->is_current);
// Marca que um efeito de sessão está ativo
session_effect_active = true;
session_effect_phase = 0;
if (session_effect_timer)
{
xTimerStop(session_effect_timer, BLOCK_TIME);
}
// Apaga tudo antes de iniciar o efeito
for (int i = 0; i < LED_ID_MAX; ++i)
{
led_apply_pattern((led_id_t)i, LED_PATTERN_OFF);
}
switch (evt->type)
{
case EVSE_SESSION_EVENT_STARTED:
// Efeito de início:
// Fase 0: azul sólido curto
session_effect_type = SESSION_EFFECT_START;
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_ON);
led_update_rgb_from_state();
xTimerChangePeriod(session_effect_timer,
pdMS_TO_TICKS(300), // 0.3 s flash
BLOCK_TIME);
xTimerStart(session_effect_timer, BLOCK_TIME);
break;
case EVSE_SESSION_EVENT_FINISHED:
// Efeito de fim:
// Fase 0: azul sólido curto
session_effect_type = SESSION_EFFECT_FINISH;
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_ON);
led_update_rgb_from_state();
xTimerChangePeriod(session_effect_timer,
pdMS_TO_TICKS(300), // 0.3 s flash
BLOCK_TIME);
xTimerStart(session_effect_timer, BLOCK_TIME);
break;
default:
// Se for tipo desconhecido, desiste do efeito e volta ao estado base
session_effect_active = false;
session_effect_type = SESSION_EFFECT_NONE;
session_effect_phase = 0;
led_apply_state_mode(current_state_mode);
break;
}
}
// ----------------------------
// Inicialização
// ----------------------------
void led_init(void)
{
// Marca quais LEDs existem de acordo com o board_config
leds[LED_ID_GREEN].present = board_config.led_green;
leds[LED_ID_BLUE].present = board_config.led_blue;
leds[LED_ID_RED].present = board_config.led_red;
// Inicializa LEDC com os GPIOs definidos na board
esp_err_t err = ledc_init(board_config.led_red_gpio,
board_config.led_green_gpio,
board_config.led_blue_gpio);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init LEDC: %s", esp_err_to_name(err));
}
// Regista handler de evento EVSE - STATE
ESP_ERROR_CHECK(esp_event_handler_register(
EVSE_EVENTS,
EVSE_EVENT_STATE_CHANGED,
evse_led_event_handler,
NULL));
// Regista handler de evento EVSE - SESSION
ESP_ERROR_CHECK(esp_event_handler_register(
EVSE_EVENTS,
EVSE_EVENT_SESSION,
evse_session_led_event_handler,
NULL));
// Cria task de efeitos (breathing)
xTaskCreate(led_effect_task, "led_effect_task", 2048, NULL, 1, NULL);
// Cria timer one-shot para efeitos de sessão
session_effect_timer = xTimerCreate(
"session_eff",
pdMS_TO_TICKS(1000), // valor default; ajustado com xTimerChangePeriod
pdFALSE, // one-shot
NULL,
session_effect_timer_cb);
ESP_LOGI(TAG, "LED system initialized");
// Estado inicial: IDLE
evse_state_event_data_t evt = {
.state = EVSE_STATE_EVENT_IDLE};
evse_led_event_handler(NULL, EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, &evt);
}
// ----------------------------
// API Pública
// ----------------------------
void led_set_state(led_id_t led_id, uint16_t ontime, uint16_t offtime)
{
if (led_id >= LED_ID_MAX)
{
return;
}
led_t *led = &leds[led_id];
if (!led->present)
{
return;
}
if (led->ontime == ontime && led->offtime == offtime)
{
return;
}
if (led->timer)
{
xTimerStop(led->timer, BLOCK_TIME);
}
led->ontime = ontime;
led->offtime = offtime;
if (ontime == 0)
{
// sempre desligado
led->on = false;
}
else if (offtime == 0)
{
// sempre ligado
led->on = true;
}
else
{
// pisca
led->on = true;
if (!led->timer)
{
// nome só para debug; opcional
led->timer = xTimerCreate("led_timer",
pdMS_TO_TICKS(ontime),
pdFALSE,
(void *)led,
led_timer_callback);
}
if (led->timer)
{
xTimerStart(led->timer, BLOCK_TIME);
}
}
// Atualiza hardware (para estados sem BREATHING)
led_update_rgb_from_state();
}
void led_apply_pattern(led_id_t id, led_pattern_t pattern)
{
if (id >= LED_ID_MAX)
{
return;
}
led_t *led = &leds[id];
if (!led->present)
{
return;
}
if ((unsigned)pattern >= LED_PATTERN_COUNT)
{
ESP_LOGW(TAG, "Invalid LED pattern %d", pattern);
return;
}
if (led->pattern == pattern)
{
return;
}
if (led->timer)
{
xTimerStop(led->timer, BLOCK_TIME);
}
led->pattern = pattern;
led->blink_count = 0;
const led_timing_t *cfg = &led_pattern_table[pattern];
led_set_state(id, cfg->on_ms, cfg->off_ms);
// led_set_state já chama led_update_rgb_from_state()
}

129
components/led/src/ledc_driver.c Executable file
View File

@@ -0,0 +1,129 @@
/*
* LEDC driver para 3 LEDs (R,G,B) controlados via ULN2003
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#include "esp_err.h"
#include "ledc_driver.h"
// Pode ser futuramente ligado a uma opção de Kconfig.
// Para o teu hardware (comum a 12 V via ULN2003), visto do ESP é ativo-alto.
#define IS_ACTIVE_HIGH 1
#define LEDC_LS_TIMER LEDC_TIMER_2
#define LEDC_LS_MODE LEDC_LOW_SPEED_MODE
// Canais usados: 2, 3, 4
#define LEDC_CH_RED LEDC_CHANNEL_2
#define LEDC_CH_GREEN LEDC_CHANNEL_3
#define LEDC_CH_BLUE LEDC_CHANNEL_4
#define LEDC_NUM_CHANNELS (3)
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT
#define LEDC_DUTY_MAX (8192 - 1)
#define LEDC_FREQUENCY (5000) // 5 kHz
static ledc_channel_config_t ledc_channel[LEDC_NUM_CHANNELS] = {
{
.channel = LEDC_CH_RED,
.duty = 0,
.gpio_num = -1, // preenchido em runtime
.speed_mode = LEDC_LS_MODE,
.hpoint = 0,
.timer_sel = LEDC_LS_TIMER,
},
{
.channel = LEDC_CH_GREEN,
.duty = 0,
.gpio_num = -1,
.speed_mode = LEDC_LS_MODE,
.hpoint = 0,
.timer_sel = LEDC_LS_TIMER,
},
{
.channel = LEDC_CH_BLUE,
.duty = 0,
.gpio_num = -1,
.speed_mode = LEDC_LS_MODE,
.hpoint = 0,
.timer_sel = LEDC_LS_TIMER,
},
};
esp_err_t ledc_init(gpio_num_t gpio_red,
gpio_num_t gpio_green,
gpio_num_t gpio_blue)
{
// Configuração do timer
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.speed_mode = LEDC_LS_MODE,
.timer_num = LEDC_LS_TIMER,
.clk_cfg = LEDC_AUTO_CLK,
};
esp_err_t err = ledc_timer_config(&ledc_timer);
if (err != ESP_OK)
{
return err;
}
// Atribuir GPIOs aos canais
ledc_channel[0].gpio_num = gpio_red;
ledc_channel[1].gpio_num = gpio_green;
ledc_channel[2].gpio_num = gpio_blue;
// Configurar canais
for (int ch = 0; ch < LEDC_NUM_CHANNELS; ch++)
{
err = ledc_channel_config(&ledc_channel[ch]);
if (err != ESP_OK)
{
return err;
}
}
return ESP_OK;
}
esp_err_t ledc_set_rgb(uint32_t red, uint32_t green, uint32_t blue)
{
if (red > 255)
red = 255;
if (green > 255)
green = 255;
if (blue > 255)
blue = 255;
red = red * LEDC_DUTY_MAX / 255;
green = green * LEDC_DUTY_MAX / 255;
blue = blue * LEDC_DUTY_MAX / 255;
if (!IS_ACTIVE_HIGH)
{
red = LEDC_DUTY_MAX - red;
green = LEDC_DUTY_MAX - green;
blue = LEDC_DUTY_MAX - blue;
}
ledc_set_duty(LEDC_LS_MODE, LEDC_CH_RED, red);
ledc_update_duty(LEDC_LS_MODE, LEDC_CH_RED);
ledc_set_duty(LEDC_LS_MODE, LEDC_CH_GREEN, green);
ledc_update_duty(LEDC_LS_MODE, LEDC_CH_GREEN);
ledc_set_duty(LEDC_LS_MODE, LEDC_CH_BLUE, blue);
ledc_update_duty(LEDC_LS_MODE, LEDC_CH_BLUE);
return ESP_OK;
}
esp_err_t ledc_clear(void)
{
return ledc_set_rgb(0, 0, 0);
}

View File

@@ -1,5 +0,0 @@
set(srcs "ledc_driver.c")
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
PRIV_REQUIRES "driver")

View File

@@ -1,2 +0,0 @@
COMPONENT_ADD_INCLUDEDIRS := .
COMPONENT_SRCDIRS := .

View File

@@ -1,179 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/ledc.h>
#include <esp_err.h>
#include "ledc_driver.h"
/**
* @brief LEDC driver: Basic LEDC driver
*/
#define IS_ACTIVE_HIGH 0
#define LEDC_LS_TIMER LEDC_TIMER_0
#define LEDC_LS_MODE LEDC_LOW_SPEED_MODE
#define LEDC_LS_CH0_GPIO (0)
#define LEDC_LS_CH0_CHANNEL LEDC_CHANNEL_0
#define LEDC_LS_CH1_GPIO (1)
#define LEDC_LS_CH1_CHANNEL LEDC_CHANNEL_1
#define LEDC_LS_CH2_GPIO (8)
#define LEDC_LS_CH2_CHANNEL LEDC_CHANNEL_2
#define LEDC_NUM_CHANNELS (3)
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // Set duty resolution to 13 bits
#define LEDC_DUTY_MAX (8192 - 1) // (2 ** 13) - 1
#define LEDC_FREQUENCY (5000) // Frequency in Hertz. Set frequency at 5 kHz
/*
* Prepare individual configuration
* for each channel of LED Controller
* by selecting:
* - controller's channel number
* - output duty cycle, set initially to 0
* - GPIO number where LED is connected to
* - speed mode, either high or low
* - timer servicing selected channel
* Note: if different channels use one timer,
* then frequency and bit_num of these channels
* will be the same
*/
static ledc_channel_config_t ledc_channel[LEDC_NUM_CHANNELS] = {
{
.channel = LEDC_LS_CH0_CHANNEL,
.duty = 0,
.gpio_num = LEDC_LS_CH0_GPIO,
.speed_mode = LEDC_LS_MODE,
.hpoint = 0,
.timer_sel = LEDC_LS_TIMER,
},
{
.channel = LEDC_LS_CH1_CHANNEL,
.duty = 0,
.gpio_num = LEDC_LS_CH1_GPIO,
.speed_mode = LEDC_LS_MODE,
.hpoint = 0,
.timer_sel = LEDC_LS_TIMER, },
{
.channel = LEDC_LS_CH2_CHANNEL,
.duty = 0,
.gpio_num = LEDC_LS_CH2_GPIO,
.speed_mode = LEDC_LS_MODE,
.hpoint = 0,
.timer_sel = LEDC_LS_TIMER,
},
};
esp_err_t ledc_init(void)
{
/*
* Prepare and set configuration of timers
* that will be used by LED Controller
*/
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_DUTY_RES, // resolution of PWM duty
.freq_hz = LEDC_FREQUENCY, // frequency of PWM signal
.speed_mode = LEDC_LS_MODE, // timer mode
.timer_num = LEDC_LS_TIMER, // timer index
.clk_cfg = LEDC_AUTO_CLK, // Auto select the source clock
};
// Set configuration of timer0 for high speed channels
ledc_timer_config(&ledc_timer);
// Set LED Controller with previously prepared configuration
for (int ch = 0; ch < LEDC_NUM_CHANNELS; ch++) {
ledc_channel_config(&ledc_channel[ch]);
}
return ESP_OK;
}
static void ledc_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b)
{
h %= 360; // h -> [0,360]
uint32_t rgb_max = v * 2.55f;
uint32_t rgb_min = rgb_max * (100 - s) / 100.0f;
uint32_t i = h / 60;
uint32_t diff = h % 60;
// RGB adjustment amount by hue
uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60;
switch (i) {
case 0:
*r = rgb_max;
*g = rgb_min + rgb_adj;
*b = rgb_min;
break;
case 1:
*r = rgb_max - rgb_adj;
*g = rgb_max;
*b = rgb_min;
break;
case 2:
*r = rgb_min;
*g = rgb_max;
*b = rgb_min + rgb_adj;
break;
case 3:
*r = rgb_min;
*g = rgb_max - rgb_adj;
*b = rgb_max;
break;
case 4:
*r = rgb_min + rgb_adj;
*g = rgb_min;
*b = rgb_max;
break;
default:
*r = rgb_max;
*g = rgb_min;
*b = rgb_max - rgb_adj;
break;
}
}
esp_err_t ledc_set_rgb(uint32_t red, uint32_t green, uint32_t blue)
{
red = red * LEDC_DUTY_MAX / 255;
green = green * LEDC_DUTY_MAX / 255;
blue = blue * LEDC_DUTY_MAX / 255;
if (!IS_ACTIVE_HIGH) {
red = LEDC_DUTY_MAX - red;
green = LEDC_DUTY_MAX - green;
blue = LEDC_DUTY_MAX - blue;
}
ledc_set_duty(ledc_channel[0].speed_mode, ledc_channel[0].channel, red);
ledc_update_duty(ledc_channel[0].speed_mode, ledc_channel[0].channel);
ledc_set_duty(ledc_channel[1].speed_mode, ledc_channel[1].channel, green);
ledc_update_duty(ledc_channel[1].speed_mode, ledc_channel[1].channel);
ledc_set_duty(ledc_channel[2].speed_mode, ledc_channel[2].channel, blue);
ledc_update_duty(ledc_channel[2].speed_mode, ledc_channel[2].channel);
return ESP_OK;
}
esp_err_t ledc_set_hsv(uint32_t hue, uint32_t saturation, uint32_t value)
{
uint32_t red = 0;
uint32_t green = 0;
uint32_t blue = 0;
ledc_hsv2rgb(hue, saturation, value, &red, &green, &blue);
return ledc_set_rgb(red, green, blue);
}
esp_err_t ledc_clear()
{
return ledc_set_rgb(0, 0, 0);
return ESP_OK;
}

View File

@@ -1,49 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
/**
* @brief Initialize the LEDC RGB LED
*
* @return ESP_OK on success.
* @return error in case of failure.
*/
esp_err_t ledc_init(void);
/**
*
* @brief Set RGB value for the LED
*
* @param[in] red Intensity of Red color (0-100)
* @param[in] green Intensity of Green color (0-100)
* @param[in] blue Intensity of Green color (0-100)
*
* @return ESP_OK on success.
* @return error in case of failure.
*/
esp_err_t ledc_set_rgb(uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Set HSV value for the LED
*
* @param[in] hue Value of hue in arc degrees (0-360)
* @param[in] saturation Saturation in percentage (0-100)
* @param[in] value Value (also called Intensity) in percentage (0-100)
*
* @return ESP_OK on success.
* @return error in case of failure.
*/
esp_err_t ledc_set_hsv(uint32_t hue, uint32_t saturation, uint32_t value);
/**
* @brief Clear (turn off) the LED
* @return ESP_OK on success.
* @return error in case of failure.
*/
esp_err_t ledc_clear();

View File

@@ -1,8 +1,7 @@
set(srcs set(srcs
"src/input_filter.c" "src/loadbalancer.c" "src/loadbalancer_events.c" "src/input_filter.c" "src/loadbalancer.c" "src/pv_optimizer.c" "src/grid_limiter.c" "src/loadbalancer_events.c"
) )
idf_component_register(SRCS "${srcs}" idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash
REQUIRES esp_event esp_timer meter_manager evse) REQUIRES esp_event esp_timer meter_manager evse)

View File

@@ -0,0 +1,42 @@
#ifndef GRID_LIMITER_H_
#define GRID_LIMITER_H_
#ifdef __cplusplus
extern "C"
{
#endif
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "meter_events.h"
void grid_limiter_init(void);
void grid_limiter_set_enabled(bool en);
bool grid_limiter_is_enabled(void);
esp_err_t grid_limiter_set_max_import_a(uint8_t a);
uint8_t grid_limiter_get_max_import_a(void);
/**
* @brief Calcula um novo "total_budget_a" (<= current_total_a) para respeitar max_import_a.
*
* Preferência:
* - Usa watt_total (+import / -export) se existir
* - Caso watt_total==0, usa fallback_grid_current_a (magnitude)
*
* @param grid_evt último evento do GRID
* @param fallback_grid_current_a corrente filtrada (magnitude) como fallback
* @param current_total_a total atual a atribuir aos EVSE (A)
* @return total_budget_a (<= current_total_a)
*/
float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt,
float fallback_grid_current_a,
float current_total_a);
#ifdef __cplusplus
}
#endif
#endif /* GRID_LIMITER_H_ */

View File

@@ -9,36 +9,26 @@ extern "C" {
#include <stdint.h> #include <stdint.h>
#include "esp_err.h" #include "esp_err.h"
/**
* @brief Inicializa o módulo de load balancer
*/
void loadbalancer_init(void); void loadbalancer_init(void);
/** void loadbalancer_set_enabled(bool enabled);
* @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); bool loadbalancer_is_enabled(void);
/** // GRID limit (A)
* @brief Define a corrente máxima do grid void loadbalancer_grid_set_enabled(bool en);
*/ bool loadbalancer_grid_is_enabled(void);
esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current); esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a);
uint8_t loadbalancer_grid_get_max_import_a(void);
/** // PV optimizer (W)
* @brief Obtém a corrente máxima do grid void loadbalancer_pv_set_enabled(bool en);
*/ bool loadbalancer_pv_is_enabled(void);
uint8_t load_balancing_get_max_grid_current(void); esp_err_t loadbalancer_pv_set_max_import_w(int32_t w);
int32_t loadbalancer_pv_get_max_import_w(void);
// Aliases legacy (se quiseres manter chamadas antigas)
esp_err_t load_balancing_set_max_grid_current(uint8_t value);
uint8_t load_balancing_get_max_grid_current(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -1,17 +1,26 @@
// components/loadbalancer/include/loadbalancer_events.h
#pragma once #pragma once
#include "esp_event.h" #include "esp_event.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include "esp_timer.h" #include "esp_timer.h"
#ifdef __cplusplus
extern "C" {
#endif
ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS);
typedef enum { typedef enum {
LOADBALANCER_EVENT_INIT, LOADBALANCER_EVENT_INIT = 0,
LOADBALANCER_EVENT_STATE_CHANGED, LOADBALANCER_EVENT_STATE_CHANGED,
LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT, LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT,
// IMPORTANT: eventos separados e payloads diferentes
LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_STATUS LOADBALANCER_EVENT_SLAVE_STATUS
} loadbalancer_event_id_t; } loadbalancer_event_id_t;
@@ -20,18 +29,18 @@ typedef struct {
int64_t timestamp_us; int64_t timestamp_us;
} loadbalancer_state_event_t; } loadbalancer_state_event_t;
// (opcional)
typedef struct { typedef struct {
float limit; float limit;
int64_t timestamp_us; int64_t timestamp_us;
} loadbalancer_global_limit_event_t; } loadbalancer_global_limit_event_t;
// MASTER: NÃO tem slave_id
typedef struct { typedef struct {
uint8_t slave_id;
uint16_t max_current; uint16_t max_current;
int64_t timestamp_us; int64_t timestamp_us;
} loadbalancer_master_limit_event_t; } loadbalancer_master_limit_event_t;
// SLAVE: tem slave_id
typedef struct { typedef struct {
uint8_t slave_id; uint8_t slave_id;
uint16_t max_current; uint16_t max_current;
@@ -39,9 +48,13 @@ typedef struct {
} loadbalancer_slave_limit_event_t; } loadbalancer_slave_limit_event_t;
typedef struct { typedef struct {
uint8_t slave_id; // ID do slave que reportou uint8_t slave_id;
bool charging; // Status de carregamento bool charging;
float hw_max_current; // Limite máximo de corrente do hardware informado float hw_max_current;
float runtime_current; // Corrente atual de carregamento (A) float runtime_current;
int64_t timestamp_us; // Momento em que o status foi coletado int64_t timestamp_us;
} loadbalancer_slave_status_event_t; } loadbalancer_slave_status_event_t;
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,40 @@
#ifndef PV_OPTIMIZER_H_
#define PV_OPTIMIZER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "meter_events.h"
void pv_optimizer_init(void);
void pv_optimizer_set_enabled(bool en);
bool pv_optimizer_is_enabled(void);
esp_err_t pv_optimizer_set_max_import_w(int32_t w);
int32_t pv_optimizer_get_max_import_w(void);
/**
* @brief Calcula o budget TOTAL (A) para todos os EVSEs, para manter importação <= max_import_w.
*
* - max_import_w = 0 => modo "Só PV": tenta manter importação ~0 (só consome quando há exportação).
* - max_import_w > 0 => modo "PV-Grid": permite importar até esse valor.
*
* @param grid_evt Último evento do medidor GRID (watt_total assinado).
* @param last_total_cmd_a Soma da corrente comandada no ciclo anterior (A).
* @param total_hw_max_a Soma dos hw_max_current dos conectores ativos (A).
* @return budget_total_a (0..total_hw_max_a)
*/
float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt,
float last_total_cmd_a,
float total_hw_max_a);
#ifdef __cplusplus
}
#endif
#endif /* PV_OPTIMIZER_H_ */

View File

@@ -0,0 +1,123 @@
#include "grid_limiter.h"
#include "esp_log.h"
#include <math.h>
static const char *TAG = "grid_limiter";
#define DEFAULT_VOLTAGE_V (230.0f)
typedef struct
{
bool enabled;
uint8_t max_import_a;
} grid_cfg_t;
static grid_cfg_t s_cfg = {
.enabled = false,
.max_import_a = 32};
static float clamp_pf(float pf)
{
if (pf < 0.05f || pf > 1.2f)
return 1.0f;
return pf;
}
static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph)
{
float sum = 0.0f;
int cnt = 0;
if (!m)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
for (int i = 0; i < 3; i++)
{
if (m->vrms[i] > 80.0f)
{
sum += m->vrms[i];
cnt++;
}
}
if (cnt == 0)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
*v_avg = sum / (float)cnt;
*nph = cnt;
}
void grid_limiter_init(void) { /* nada */ }
void grid_limiter_set_enabled(bool en) { s_cfg.enabled = en; }
bool grid_limiter_is_enabled(void) { return s_cfg.enabled; }
esp_err_t grid_limiter_set_max_import_a(uint8_t a)
{
if (a < 6 || a > 100)
return ESP_ERR_INVALID_ARG;
s_cfg.max_import_a = a;
return ESP_OK;
}
uint8_t grid_limiter_get_max_import_a(void) { return s_cfg.max_import_a; }
float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt,
float fallback_grid_current_a,
float current_total_a)
{
if (!s_cfg.enabled)
return current_total_a;
if (current_total_a <= 0.0f)
return 0.0f;
float i_import = 0.0f;
if (grid_evt && grid_evt->watt_total > 0)
{
float v_avg;
int nph;
estimate_v_and_phases(grid_evt, &v_avg, &nph);
const float pf = clamp_pf(grid_evt->power_factor);
const float denom = v_avg * (float)nph * pf;
if (denom > 10.0f)
{
i_import = ((float)grid_evt->watt_total) / denom;
}
else
{
i_import = fallback_grid_current_a;
}
}
else
{
// export (<=0) => import=0; ou sem potência => fallback
if (grid_evt && grid_evt->watt_total < 0)
i_import = 0.0f;
else
i_import = fallback_grid_current_a;
}
if (i_import <= (float)s_cfg.max_import_a + 0.01f)
return current_total_a;
const float over = i_import - (float)s_cfg.max_import_a;
const float cut_a = ceilf(over); // conservador
float new_total = current_total_a - cut_a;
if (new_total < 0.0f)
new_total = 0.0f;
ESP_LOGD(TAG, "cap: i_import=%.2fA max=%uA over=%.2fA total=%.1fA -> %.1fA",
i_import, (unsigned)s_cfg.max_import_a, over, current_total_a, new_total);
return new_total;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
#include "pv_optimizer.h"
#include "esp_log.h"
#include <math.h>
static const char *TAG = "pv_optimizer";
// internos (fixos, como pediste)
#define PV_MIN_EXPORT_W (50) // deadband export (anti-oscilações)
#define PV_TOTAL_RAMP_STEP_A (2.0f) // step total por ciclo (como tens loop 5s)
#define DEFAULT_VOLTAGE_V (230.0f)
typedef struct
{
bool enabled;
int32_t max_import_w; // >=0
} pv_cfg_t;
static pv_cfg_t s_cfg = {
.enabled = false,
.max_import_w = 0};
static float clamp_pf(float pf)
{
if (pf < 0.05f || pf > 1.2f)
return 1.0f;
return pf;
}
static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph)
{
float sum = 0.0f;
int cnt = 0;
if (!m)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
for (int i = 0; i < 3; i++)
{
if (m->vrms[i] > 80.0f)
{
sum += m->vrms[i];
cnt++;
}
}
if (cnt == 0)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
*v_avg = sum / (float)cnt;
*nph = cnt;
}
void pv_optimizer_init(void)
{
// nada a fazer
}
void pv_optimizer_set_enabled(bool en) { s_cfg.enabled = en; }
bool pv_optimizer_is_enabled(void) { return s_cfg.enabled; }
esp_err_t pv_optimizer_set_max_import_w(int32_t w)
{
if (w < 0)
return ESP_ERR_INVALID_ARG;
s_cfg.max_import_w = w;
return ESP_OK;
}
int32_t pv_optimizer_get_max_import_w(void) { return s_cfg.max_import_w; }
static float ramp_total(float last_a, float target_a)
{
if (target_a > last_a + PV_TOTAL_RAMP_STEP_A)
return last_a + PV_TOTAL_RAMP_STEP_A;
if (target_a < last_a - PV_TOTAL_RAMP_STEP_A)
return last_a - PV_TOTAL_RAMP_STEP_A;
return target_a;
}
float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt,
float last_total_cmd_a,
float total_hw_max_a)
{
if (!s_cfg.enabled)
return total_hw_max_a;
if (!grid_evt)
return 0.0f;
// se meter não fornece potência (fica 0) não dá para PV -> conservador: não importa
// (podes mudar para "mantém last" se preferires)
if (grid_evt->watt_total == 0)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
float v_avg;
int nph;
estimate_v_and_phases(grid_evt, &v_avg, &nph);
const float pf = clamp_pf(grid_evt->power_factor);
const float w_per_a = v_avg * (float)nph * pf;
if (w_per_a < 10.0f)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
const int32_t p_grid_w = grid_evt->watt_total; // +import / -export
const int32_t target_import_w = s_cfg.max_import_w; // >=0
// deadband só para o "Só PV"
if (target_import_w == 0)
{
if (p_grid_w < 0)
{
int32_t export_w = -p_grid_w;
if (export_w < PV_MIN_EXPORT_W)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
}
else
{
// está a importar
if (p_grid_w < PV_MIN_EXPORT_W)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
}
}
// estima base-load com o comando anterior
const float p_evse_last_w = last_total_cmd_a * w_per_a;
const float p_base_w = (float)p_grid_w - p_evse_last_w;
// queremos p_grid -> target_import_w
float p_evse_target_w = (float)target_import_w - p_base_w;
// clamp [0..max]
if (p_evse_target_w < 0.0f)
p_evse_target_w = 0.0f;
const float p_evse_max_w = total_hw_max_a * w_per_a;
if (p_evse_target_w > p_evse_max_w)
p_evse_target_w = p_evse_max_w;
float target_total_a = p_evse_target_w / w_per_a;
if (target_total_a < 0.0f)
target_total_a = 0.0f;
if (target_total_a > total_hw_max_a)
target_total_a = total_hw_max_a;
float ramped = ramp_total(last_total_cmd_a, target_total_a);
ESP_LOGD(TAG, "pv: p_grid=%ldW target_imp=%ldW base=%.1fW last=%.1fA -> target=%.1fA (v=%.1f nph=%d pf=%.2f)",
(long)p_grid_w, (long)target_import_w, p_base_w, last_total_cmd_a, ramped, v_avg, nph, pf);
return ramped;
}

View File

@@ -1,7 +0,0 @@
set(srcs
"src/logger.c"
"src/output_buffer.c"
)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include")

View File

@@ -1,58 +0,0 @@
#ifndef LOGGER_H_
#define LOGGER_H_
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#define LOGGER_SERIAL_BIT BIT0
/**
* @brief Logger event group LOGGER_SERIAL_BIT
*
*/
extern EventGroupHandle_t logger_event_group;
/**
* @brief Initialize logger
*
*/
void logger_init(void);
/**
* @brief Print
*
* @param str
*/
void logger_print(const char* str);
/**
* @brief Print va
*
* @param str
* @param l
* @return int
*/
int logger_vprintf(const char* str, va_list l);
/**
* @brief Get entries count
*
* @return uint16_t
*/
uint16_t logger_count(void);
/**
* @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);
#endif /* LOGGER_H_ */

View File

@@ -1,24 +0,0 @@
#ifndef OUTPUT_BUFFER_H_
#define OUTPUT_BUFFER_H_
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint16_t size;
uint16_t count;
uint8_t* data;
uint8_t* append;
} output_buffer_t;
output_buffer_t* output_buffer_create(uint16_t size);
void output_buffer_delete(output_buffer_t* buffer);
void output_buffer_append_buf(output_buffer_t* buffer, const char* buf, uint16_t len);
void output_buffer_append_str(output_buffer_t* buffer, const char* str);
bool output_buffer_read(output_buffer_t* buffer, uint16_t *index, char **str, uint16_t* len);
#endif /* OUTPUT_BUFFER_H_ */

View File

@@ -1,71 +0,0 @@
#include <stdio.h>
#include <memory.h>
#include <sys/param.h>
#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
static SemaphoreHandle_t mutex;
static output_buffer_t * buffer = NULL;
EventGroupHandle_t logger_event_group = NULL;
void logger_init(void)
{
mutex = xSemaphoreCreateMutex();
logger_event_group = xEventGroupCreate();
buffer = output_buffer_create(LOG_BUFFER_SIZE);
}
uint16_t logger_count(void)
{
return buffer->count;
}
void logger_print(const char* str)
{
xSemaphoreTake(mutex, portMAX_DELAY);
output_buffer_append_str(buffer, str);
xEventGroupSetBits(logger_event_group, 0xFF);
xSemaphoreGive(mutex);
}
int logger_vprintf(const char* str, va_list l)
{
#ifdef CONFIG_ESP_CONSOLE_UART
vprintf(str, l);
#endif
xSemaphoreTake(mutex, portMAX_DELAY);
static char log[MAX_LOG_SIZE];
int len = vsnprintf(log, MAX_LOG_SIZE, str, l);
output_buffer_append_buf(buffer, log, len);
xEventGroupSetBits(logger_event_group, 0xFF);
xSemaphoreGive(mutex);
return len;
}
bool logger_read(uint16_t* index, char** str, uint16_t* len)
{
xSemaphoreTake(mutex, portMAX_DELAY);
bool has_next = output_buffer_read(buffer, index, str, len);
xSemaphoreGive(mutex);
return has_next;
}

View File

@@ -1,86 +0,0 @@
#include <memory.h>
#include "output_buffer.h"
output_buffer_t* output_buffer_create(uint16_t size)
{
output_buffer_t* buffer = (output_buffer_t*)malloc(sizeof(output_buffer_t));
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)
{
free((void*)buffer->data);
free((void*)buffer);
}
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;
uint16_t rotate_count = 0;
while ((pos - buffer->data) < buffer->size / 2) {
//seek first half
uint16_t entry_len;
memcpy((void*)&entry_len, (void*)pos, sizeof(uint16_t));
pos += entry_len + sizeof(uint16_t);
rotate_count++;
}
memmove((void*)buffer->data, (void*)pos, buffer->size - (pos - buffer->data));
buffer->count -= rotate_count;
buffer->append -= (pos - buffer->data);
}
memcpy((void*)buffer->append, (void*)&len, sizeof(uint16_t));
buffer->append += sizeof(uint16_t);
memcpy((void*)buffer->append, (void*)str, len);
buffer->append += len;
buffer->count++;
}
void output_buffer_append_str(output_buffer_t* buffer, const char* str)
{
output_buffer_append_buf(buffer, str, strlen(str));
}
bool output_buffer_read(output_buffer_t* buffer, uint16_t* index, char** str, uint16_t* len)
{
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;
}
return has_next;
}

View File

@@ -1,29 +1,31 @@
# List the source files to be compiled # components/meter_manager/CMakeLists.txt
set(srcs set(srcs
"driver/meter_ade7758/meter_ade7758.c" driver/meter_ade7758/meter_ade7758.c
"driver/meter_ade7758/ade7758.c" driver/meter_ade7758/ade7758.c
"driver/meter_orno/meter_orno513.c" driver/meter_modbus/meter_orno513.c
"driver/meter_orno/meter_orno526.c" driver/meter_modbus/meter_orno526.c
"driver/meter_orno/meter_orno516.c" driver/meter_modbus/meter_orno516.c
"driver/meter_orno/meter_dts6619.c" driver/meter_modbus/meter_dts6619.c
"driver/meter_orno/meter_dds661.c" driver/meter_modbus/meter_dds661.c
"driver/meter_orno/meter_ea777.c" driver/meter_modbus/meter_ea777.c
"driver/meter_orno/modbus_params.c" driver/meter_modbus/meter_dts024m.c
"driver/meter_zigbee/meter_zigbee.c" driver/meter_modbus/modbus_params.c
"src/meter_manager.c" driver/meter_zigbee/meter_zigbee.c
"src/meter_events.c" src/meter_manager.c
src/meter_events.c
) )
# List the include directories
set(includes set(includes
"include" include
"driver/meter_ade7758" driver/meter_ade7758
"driver/meter_orno" driver/meter_modbus
"driver/meter_zigbee" driver/meter_zigbee
) )
# Register the component with the ESP-IDF build system idf_component_register(
idf_component_register(SRCS "${srcs}" SRCS ${srcs}
INCLUDE_DIRS "${includes}" INCLUDE_DIRS ${includes}
PRIV_REQUIRES nvs_flash REQUIRES esp_event
REQUIRES esp_event esp-modbus spi_bus_manager network) PRIV_REQUIRES esp-modbus spi_bus_manager storage_service network
)

View File

@@ -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.irms, data->irms, sizeof(evt.irms));
memcpy(evt.watt, data->watt, sizeof(evt.watt)); 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) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
} }

View File

@@ -197,7 +197,7 @@ static void serial_mdb_task(void *param)
memcpy(evt.irms, current, sizeof(evt.irms)); memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt)); 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); vTaskDelay(UPDATE_INTERVAL);
} }
} }

View File

@@ -0,0 +1,542 @@
// meter_dts024m.c — Driver Modbus RTU para DTS024M (ESP-IDF / esp-modbus)
// Versão PRODUÇÃO (SEM AUTO-PROBE): parâmetros fixos (baud/parity/id/FC/base).
// Ajusta os #defines DTS024M_PROD_* conforme o teu medidor.
#include "meter_events.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "esp_log.h"
#include "esp_err.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stddef.h>
#include <string.h>
#include "meter_dts024m.h"
#define TAG "serial_mdb_dts024m"
// ===== UART / RS-485 =====
#define MB_PORT_NUM 2
// Ajuste os pinos conforme seu hardware
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (200 / portTICK_PERIOD_MS)
// ===== Helpers =====
#define STR(fieldname) ((const char *)(fieldname))
#define OPTS(min_val, max_val, step_val) {.opt1 = (min_val), .opt2 = (max_val), .opt3 = (step_val)}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// ===== Config PRODUÇÃO (sem AUTO-PROBE) =====
// Ajusta estes valores:
#define DTS024M_PROD_BAUD 2400
#define DTS024M_PROD_PARITY UART_PARITY_DISABLE // 0 = none; UART_PARITY_EVEN se 8E1
#define DTS024M_PROD_SLAVE_ID 1 // endereço Modbus (1..247)
#define DTS024M_PROD_AREA MB_PARAM_INPUT // MB_PARAM_INPUT (FC04) ou MB_PARAM_HOLDING (FC03)
#define DTS024M_PROD_BASE_OFFSET 0 // 0 ou 1 (depende se o mapa é 0-based ou 1-based)
// ===== Estado =====
static bool is_initialized = false;
static bool mb_started = false;
static TaskHandle_t meter_task = NULL;
// ============================================================================
// MAPA DE REGISTROS (template) — pode variar conforme firmware.
// Estes endereços são um “perfil” comum.
// ============================================================================
#define DTS024M_L1_VOLTAGE 0x0000 // U32, 0.01 V (2 regs)
#define DTS024M_L2_VOLTAGE 0x0002
#define DTS024M_L3_VOLTAGE 0x0004
#define DTS024M_L1_CURRENT 0x0006 // U32, 0.001 A (2 regs)
#define DTS024M_L2_CURRENT 0x0008
#define DTS024M_L3_CURRENT 0x000A
#define DTS024M_L1_ACTIVE_P 0x000C // I32 (twos complement), (depende do modelo/escala)
#define DTS024M_L2_ACTIVE_P 0x000E
#define DTS024M_L3_ACTIVE_P 0x0010
#define DTS024M_PF_L1 0x001E // I16 (twos complement), 0.001
#define DTS024M_PF_L2 0x001F
#define DTS024M_PF_L3 0x0020
#define DTS024M_FREQUENCY 0x002A // U16, 0.01 Hz
#define DTS024M_TOTAL_ACTIVE_E 0x0404 // U32, 0.01 kWh (2 regs)
// ============================================================================
// Conversões signed (twos complement) — porque o projeto não tem PARAM_TYPE_I*
// ============================================================================
static inline int32_t s32_from_u32(uint32_t x)
{
return (x & 0x80000000u) ? (int32_t)(x - 0x100000000ULL) : (int32_t)x;
}
static inline int16_t s16_from_u16(uint16_t x)
{
return (x & 0x8000u) ? (int16_t)(x - 0x10000u) : (int16_t)x;
}
// ============================================================================
// CIDs
// ============================================================================
enum
{
CID_DTS024M_L1_VOLTAGE = 0,
CID_DTS024M_L2_VOLTAGE,
CID_DTS024M_L3_VOLTAGE,
CID_DTS024M_L1_CURRENT,
CID_DTS024M_L2_CURRENT,
CID_DTS024M_L3_CURRENT,
CID_DTS024M_L1_ACTIVE_P,
CID_DTS024M_L2_ACTIVE_P,
CID_DTS024M_L3_ACTIVE_P,
CID_DTS024M_PF_L1,
CID_DTS024M_PF_L2,
CID_DTS024M_PF_L3,
CID_DTS024M_FREQUENCY,
CID_DTS024M_TOTAL_ACTIVE_E,
};
// ============================================================================
// DESCRIPTORS (TEMPLATE) — copiamos para RAM e ajustamos:
// - slave_id
// - base offset (0/1)
// - mb_param_type (HOLDING/INPUT)
// ============================================================================
static const mb_parameter_descriptor_t device_parameters_dts024m_tmpl[] = {
// Tensões (U32 / 2 regs) — 0.01 V
{CID_DTS024M_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L1_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_VOLTAGE, STR("L2 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L2_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_VOLTAGE, STR("L3 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L3_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// Correntes (U32 / 2 regs) — 0.001 A
{CID_DTS024M_L1_CURRENT, STR("L1 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L1_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_CURRENT, STR("L2 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L2_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_CURRENT, STR("L3 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L3_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// Potência ativa por fase (U32 / 2 regs no descriptor; interpretamos como signed I32)
{CID_DTS024M_L1_ACTIVE_P, STR("L1 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L1_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_ACTIVE_P, STR("L2 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L2_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_ACTIVE_P, STR("L3 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L3_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// PF (U16 / 1 reg; interpretamos como signed I16) — 0.001
{CID_DTS024M_PF_L1, STR("L1 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L1, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
{CID_DTS024M_PF_L2, STR("L2 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L2, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
{CID_DTS024M_PF_L3, STR("L3 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L3, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
// Frequência (U16 / 1 reg) — 0.01 Hz
{CID_DTS024M_FREQUENCY, STR("Frequency"), STR("Hz"), 1,
MB_PARAM_HOLDING, DTS024M_FREQUENCY, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Energia ativa total (U32 / 2 regs) — 0.01 kWh
{CID_DTS024M_TOTAL_ACTIVE_E, STR("Total Active Energy"), STR("kWh"), 1,
MB_PARAM_HOLDING, DTS024M_TOTAL_ACTIVE_E, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
};
static mb_parameter_descriptor_t device_parameters_dts024m[ARRAY_SIZE(device_parameters_dts024m_tmpl)];
static const uint16_t num_device_parameters_dts024m = ARRAY_SIZE(device_parameters_dts024m);
static void dts024m_build_descriptors(uint8_t slave_id, uint16_t base_offset, mb_param_type_t area)
{
memcpy(device_parameters_dts024m,
device_parameters_dts024m_tmpl,
sizeof(device_parameters_dts024m));
for (uint16_t i = 0; i < num_device_parameters_dts024m; ++i)
{
device_parameters_dts024m[i].mb_slave_addr = slave_id;
device_parameters_dts024m[i].mb_reg_start =
(uint16_t)(device_parameters_dts024m[i].mb_reg_start + base_offset);
device_parameters_dts024m[i].mb_param_type = area; // HOLDING (FC03) ou INPUT (FC04)
}
}
// ============================================================================
// Modbus master init (fixo) — garante ordem correta (start -> uart_set_mode)
// ============================================================================
static esp_err_t dts024m_master_reinit(uint32_t baud, uart_parity_t parity)
{
if (mb_started)
{
(void)mbc_master_destroy();
mb_started = false;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
}
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = baud,
.parity = parity};
void *handler = NULL;
esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler);
if (err != ESP_OK)
return err;
err = mbc_master_setup(&comm);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// IMPORTANTE: start antes de uart_set_mode (driver UART costuma ser instalado no start)
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
mb_started = true;
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
mb_started = false;
return err;
}
vTaskDelay(pdMS_TO_TICKS(40));
return ESP_OK;
}
// ============================================================================
// Post do evento de medição
// ============================================================================
static void meter_dts024m_post_event(float *voltage, float *current, int *power_w,
float freq_hz, float pf_avg, float total_kwh)
{
meter_event_data_t evt = {
.source = "GRID",
.frequency = freq_hz,
.power_factor = pf_avg,
.total_energy = total_kwh};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
}
}
// ============================================================================
// Task de polling
// ============================================================================
static void serial_mdb_dts024m_task(void *param)
{
(void)param;
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
float v[3] = {0};
float i[3] = {0};
float pf[3] = {0};
float freq = 0.0f;
float total_kwh = 0.0f;
int p_w[3] = {0};
vTaskDelay(pdMS_TO_TICKS(200)); // settle
while (1)
{
for (uint16_t cid = 0; cid < num_device_parameters_dts024m; cid++)
{
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
continue;
}
uint8_t type = 0;
uint16_t raw_u16 = 0;
uint32_t raw_u32 = 0;
void *value_ptr = &raw_u16;
// U32
switch (cid)
{
case CID_DTS024M_L1_VOLTAGE:
case CID_DTS024M_L2_VOLTAGE:
case CID_DTS024M_L3_VOLTAGE:
case CID_DTS024M_L1_CURRENT:
case CID_DTS024M_L2_CURRENT:
case CID_DTS024M_L3_CURRENT:
case CID_DTS024M_L1_ACTIVE_P:
case CID_DTS024M_L2_ACTIVE_P:
case CID_DTS024M_L3_ACTIVE_P:
case CID_DTS024M_TOTAL_ACTIVE_E:
value_ptr = &raw_u32;
break;
default:
value_ptr = &raw_u16;
break;
}
// 1 retry simples em caso de timeout (podes remover se quiseres menos carga)
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
if (err == ESP_ERR_TIMEOUT)
{
vTaskDelay(pdMS_TO_TICKS(60));
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
}
if (err == ESP_OK)
{
switch (cid)
{
// V (0.01V)
case CID_DTS024M_L1_VOLTAGE:
v[0] = ((float)raw_u32) * 0.01f;
break;
case CID_DTS024M_L2_VOLTAGE:
v[1] = ((float)raw_u32) * 0.01f;
break;
case CID_DTS024M_L3_VOLTAGE:
v[2] = ((float)raw_u32) * 0.01f;
break;
// I (0.001A)
case CID_DTS024M_L1_CURRENT:
i[0] = ((float)raw_u32) * 0.001f;
break;
case CID_DTS024M_L2_CURRENT:
i[1] = ((float)raw_u32) * 0.001f;
break;
case CID_DTS024M_L3_CURRENT:
i[2] = ((float)raw_u32) * 0.001f;
break;
// P ativa (twos complement I32) — atenção: escala depende do modelo
case CID_DTS024M_L1_ACTIVE_P:
p_w[0] = (int)s32_from_u32(raw_u32);
break;
case CID_DTS024M_L2_ACTIVE_P:
p_w[1] = (int)s32_from_u32(raw_u32);
break;
case CID_DTS024M_L3_ACTIVE_P:
p_w[2] = (int)s32_from_u32(raw_u32);
break;
// PF (twos complement I16; 0.001)
case CID_DTS024M_PF_L1:
pf[0] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
case CID_DTS024M_PF_L2:
pf[1] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
case CID_DTS024M_PF_L3:
pf[2] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
// Freq (0.01Hz)
case CID_DTS024M_FREQUENCY:
freq = ((float)raw_u16) * 0.01f;
break;
// Energia (0.01kWh)
case CID_DTS024M_TOTAL_ACTIVE_E:
total_kwh = ((float)raw_u32) * 0.01f;
break;
default:
break;
}
ESP_LOGD(TAG, "%s (cid=%u) ok (u16=%u u32=%u)",
desc->param_key, cid, (unsigned)raw_u16, (unsigned)raw_u32);
}
else
{
ESP_LOGE(TAG, "CID %u (%s) read failed: %s",
cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
// PF médio simples (ignora zeros)
float pf_sum = 0.0f;
int pf_cnt = 0;
for (int k = 0; k < 3; ++k)
{
if (pf[k] != 0.0f)
{
pf_sum += pf[k];
pf_cnt++;
}
}
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
meter_dts024m_post_event(v, i, p_w, freq, pf_avg, total_kwh);
vTaskDelay(UPDATE_INTERVAL);
}
}
// ============================================================================
// Init / Start / Stop
// ============================================================================
esp_err_t meter_dts024m_init(void)
{
if (is_initialized)
{
ESP_LOGW(TAG, "Already initialized");
return ESP_ERR_INVALID_STATE;
}
// init fixo (produção)
esp_err_t err = dts024m_master_reinit(DTS024M_PROD_BAUD, DTS024M_PROD_PARITY);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "master_reinit failed: %s", esp_err_to_name(err));
return err;
}
// monta descriptors reais com ID/offset/area fixos
dts024m_build_descriptors(DTS024M_PROD_SLAVE_ID, DTS024M_PROD_BASE_OFFSET, DTS024M_PROD_AREA);
// aplica descriptors reais
esp_err_t derr = mbc_master_set_descriptor(device_parameters_dts024m,
num_device_parameters_dts024m);
if (derr != ESP_OK)
{
ESP_LOGE(TAG, "set_descriptor failed: %s", esp_err_to_name(derr));
return derr;
}
is_initialized = true;
ESP_LOGI(TAG, "DTS024M initialized (PROD) baud=%d parity=%d id=%d area=%s base=%d",
DTS024M_PROD_BAUD,
(int)DTS024M_PROD_PARITY,
DTS024M_PROD_SLAVE_ID,
(DTS024M_PROD_AREA == MB_PARAM_HOLDING ? "FC03" : "FC04"),
DTS024M_PROD_BASE_OFFSET);
return ESP_OK;
}
esp_err_t meter_dts024m_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "Not initialized");
return ESP_ERR_INVALID_STATE;
}
if (meter_task == NULL)
{
xTaskCreate(serial_mdb_dts024m_task,
"meter_dts024m_task",
4096, NULL, 3, &meter_task);
ESP_LOGI(TAG, "DTS024M task started");
}
return ESP_OK;
}
void meter_dts024m_stop(void)
{
if (!is_initialized)
{
ESP_LOGW(TAG, "Not initialized, skipping stop");
return;
}
if (meter_task)
{
vTaskDelete(meter_task);
meter_task = NULL;
ESP_LOGI(TAG, "DTS024M task stopped");
}
if (mb_started)
{
(void)mbc_master_destroy();
mb_started = false;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
is_initialized = false;
ESP_LOGI(TAG, "Meter DTS024M cleaned up");
}

View File

@@ -0,0 +1,35 @@
#ifndef METER_DTS024M_H_
#define METER_DTS024M_H_
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Inicializa o driver do medidor DTS024M (UART RS485, Modbus, registradores).
*
* @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro.
*/
esp_err_t meter_dts024m_init(void);
/**
* @brief Inicia a tarefa de leitura de dados do medidor DTS024M.
*
* @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro.
*/
esp_err_t meter_dts024m_start(void);
/**
* @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS024M.
*/
void meter_dts024m_stop(void);
#ifdef __cplusplus
}
#endif
#endif /* METER_DTS024M_H_ */

View File

@@ -138,7 +138,7 @@ static void meter_dts6619_post_event(float *voltage, float *current, int *power_
memcpy(evt.watt, power_w, sizeof(evt.watt)); memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, 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) if (err != ESP_OK)
{ {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));

View File

@@ -7,14 +7,14 @@
/** /**
* @brief Inicializa o driver do medidor dts6619 (SPI, mutex, 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. * @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_dts6619_init(void);
/** /**
* @brief Inicia a tarefa de leitura de dados do medidor DTS6619. * @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. * @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_dts6619_start(void);
@@ -24,7 +24,6 @@ esp_err_t meter_dts6619_start(void);
*/ */
void meter_dts6619_stop(void); void meter_dts6619_stop(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -7,17 +7,18 @@
#include "driver/uart.h" #include "driver/uart.h"
#include <stddef.h> #include <stddef.h>
#include <string.h> #include <string.h>
#include "meter_ea777.h"
#define TAG "serial_mdb_ea777" #define TAG "serial_mdb_ea777"
// ===== UART / RS-485 ===== // ===== UART / RS-485 =====
#define MB_PORT_NUM 2 #define MB_PORT_NUM 1
#define MB_DEV_SPEED 9600 #define MB_DEV_SPEED 9600
// Ajuste os pinos conforme seu hardware // Ajuste os pinos conforme seu hardware
#define MB_UART_TXD 17 #define MB_UART_TXD 21
#define MB_UART_RXD 16 #define MB_UART_RXD 22
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 #define MB_UART_RTS UART_PIN_NO_CHANGE // sem DE/RE
// ===== Timings ===== // ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS) #define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
@@ -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)); memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, 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) if (err != ESP_OK)
{ {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
@@ -321,9 +322,10 @@ esp_err_t meter_ea777_init(void)
ESP_ERROR_CHECK(mbc_master_setup(&comm)); ESP_ERROR_CHECK(mbc_master_setup(&comm));
ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM,
MB_UART_TXD, MB_UART_RXD, MB_UART_TXD, MB_UART_RXD,
MB_UART_RTS, UART_PIN_NO_CHANGE)); UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(mbc_master_start()); ESP_ERROR_CHECK(mbc_master_start());
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART));
// ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART));
vTaskDelay(pdMS_TO_TICKS(50)); vTaskDelay(pdMS_TO_TICKS(50));
ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777, ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777,

View File

@@ -32,4 +32,4 @@ void meter_ea777_stop(void);
} }
#endif #endif
#endif /* METER_EA777_H_ */ #endif /* METER_EA777_H_ */

View File

@@ -8,11 +8,12 @@
#define TAG "serial_mdb_orno513" #define TAG "serial_mdb_orno513"
#define MB_PORT_NUM 2 #define MB_PORT_NUM 2
#define MB_DEV_SPEED 9600 #define MB_DEV_SPEED 9600
#define MB_UART_TXD 17 #define MB_UART_TXD 17
#define MB_UART_RXD 16 #define MB_UART_RXD 16
#define MB_UART_RTS 5 #define MB_UART_RTS 2
#define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS) #define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (100 / 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.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt)); 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); vTaskDelay(UPDATE_INTERVAL);

View File

@@ -91,7 +91,7 @@ static void meter_orno516_post_event(float *voltage, float *current, int *power)
memcpy(evt.watt, power, sizeof(evt.watt)); memcpy(evt.watt, power, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, 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) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));

View File

@@ -207,7 +207,7 @@ static void serial_mdb_task(void *param)
memcpy(evt.irms, current, sizeof(evt.irms)); memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt)); 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); vTaskDelay(UPDATE_INTERVAL);
} }
} }

View File

@@ -12,36 +12,37 @@
#define TAG "meter_zigbee" #define TAG "meter_zigbee"
// UART config // UART config
#define UART_PORT UART_NUM_1 #define UART_PORT UART_NUM_2
#define TXD_PIN GPIO_NUM_17 #define TXD_PIN GPIO_NUM_17
#define RXD_PIN GPIO_NUM_16 #define RXD_PIN GPIO_NUM_16
#define UART_BUF_SIZE 128 #define UART_BUF_SIZE 128
#define RX_FRAME_SIZE 14 #define RX_FRAME_SIZE 14
// Zigbee Attribute IDs // Zigbee Attribute IDs
#define ATTR_CURRENT_L1 0x0006 #define ATTR_CURRENT_L1 0x0006
#define ATTR_CURRENT_L2 0x0007 #define ATTR_CURRENT_L2 0x0007
#define ATTR_CURRENT_L3 0x0008 #define ATTR_CURRENT_L3 0x0008
#define ATTR_VOLTAGE_L1 0x0266 #define ATTR_VOLTAGE_L1 0x0266
#define ATTR_CURRENT_L1_ALT 0x0267 #define ATTR_CURRENT_L1_ALT 0x0267
#define ATTR_POWER_L1 0x0268 #define ATTR_POWER_L1 0x0268
#define ATTR_VOLTAGE_L2 0x0269 #define ATTR_VOLTAGE_L2 0x0269
#define ATTR_CURRENT_L2_ALT 0x026A #define ATTR_CURRENT_L2_ALT 0x026A
#define ATTR_POWER_L2 0x026B #define ATTR_POWER_L2 0x026B
#define ATTR_VOLTAGE_L3 0x026C #define ATTR_VOLTAGE_L3 0x026C
#define ATTR_CURRENT_L3_ALT 0x026D #define ATTR_CURRENT_L3_ALT 0x026D
#define ATTR_POWER_L3 0x026E #define ATTR_POWER_L3 0x026E
#define ATTR_FREQUENCY 0x0265 #define ATTR_FREQUENCY 0x0265
#define ATTR_POWER_FACTOR 0x020F #define ATTR_POWER_FACTOR 0x020F
#define ATTR_TOTAL_ENERGY 0x0201 #define ATTR_TOTAL_ENERGY 0x0201
#define PHASE_COUNT 3 #define PHASE_COUNT 3
#define PHASE_L1 0 #define PHASE_L1 0
#define PHASE_L2 1 #define PHASE_L2 1
#define PHASE_L3 2 #define PHASE_L3 2
// Internal meter state // Internal meter state
typedef struct { typedef struct
{
float vrms[PHASE_COUNT]; float vrms[PHASE_COUNT];
float irms[PHASE_COUNT]; float irms[PHASE_COUNT];
int watt[PHASE_COUNT]; int watt[PHASE_COUNT];
@@ -58,24 +59,28 @@ static meter_zigbee_data_t meter_data = {0};
static SemaphoreHandle_t meter_mutex = NULL; static SemaphoreHandle_t meter_mutex = NULL;
static TaskHandle_t meter_zigbee_task = NULL; static TaskHandle_t meter_zigbee_task = NULL;
bool meter_zigbee_is_running(void)
bool meter_zigbee_is_running(void) { {
return meter_zigbee_task != NULL; return meter_zigbee_task != NULL;
} }
void send_stop_command(void) { static inline int32_t tuya_power16_to_signed(uint16_t p)
//const char *cmd = "stop\n"; // Comando enviado para o outro lado interpretar e dormir {
//uart_write_bytes(UART_PORT, cmd, strlen(cmd)); // Igual ao quirk multi_dp_to_power()
//uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(100)); // Aguarda envio terminar if (p > 0x7FFF)
{
return (int32_t)((0x999A - p) * -1);
}
return (int32_t)p;
} }
static void meter_zigbee_post_event(void) { static void meter_zigbee_post_event(void)
{
meter_event_data_t evt = { meter_event_data_t evt = {
.source = "GRID", .source = "GRID",
.frequency = meter_data.frequency, .frequency = meter_data.frequency,
.power_factor = meter_data.power_factor, .power_factor = meter_data.power_factor,
.total_energy = meter_data.total_energy .total_energy = meter_data.total_energy};
};
memcpy(evt.vrms, meter_data.vrms, sizeof(evt.vrms)); memcpy(evt.vrms, meter_data.vrms, sizeof(evt.vrms));
memcpy(evt.irms, meter_data.irms, sizeof(evt.irms)); memcpy(evt.irms, meter_data.irms, sizeof(evt.irms));
@@ -85,19 +90,21 @@ static void meter_zigbee_post_event(void) {
METER_EVENT_DATA_READY, METER_EVENT_DATA_READY,
&evt, &evt,
sizeof(evt), sizeof(evt),
pdMS_TO_TICKS(10)); portMAX_DELAY);
if (err != ESP_OK) { if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
} }
} }
static void handle_zigbee_frame(const uint8_t *buf, size_t len)
{
ESP_LOGD(TAG, "Received UART frame (%d bytes):", len);
// ESP_LOG_BUFFER_HEX(TAG, buf, len);
static void handle_zigbee_frame(const uint8_t *buf, size_t len) { if (len < RX_FRAME_SIZE)
ESP_LOGI(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); ESP_LOGW(TAG, "Invalid frame: too short (len = %d)", len);
return; return;
} }
@@ -105,70 +112,85 @@ static void handle_zigbee_frame(const uint8_t *buf, size_t len) {
uint16_t attr = buf[2] | (buf[3] << 8); uint16_t attr = buf[2] | (buf[3] << 8);
uint8_t size = buf[5]; uint8_t size = buf[5];
if (size != 8) { if (size != 8)
{
ESP_LOGW(TAG, "Unsupported payload size: %d", size); ESP_LOGW(TAG, "Unsupported payload size: %d", size);
return; return;
} }
uint16_t volt_raw = (buf[6] << 8) | buf[7]; // payload 8 bytes começa em buf[6]
uint32_t current_raw = (buf[8] << 16) | (buf[9] << 8) | buf[10]; const uint8_t *p = &buf[6];
uint32_t power_raw = (buf[11] << 16) | (buf[12] << 8) | buf[13];
float volt = volt_raw / 10.0f; uint16_t volt_raw = ((uint16_t)p[0] << 8) | p[1];
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); uint16_t curr_raw_u16 = ((uint16_t)p[3] << 8) | p[4]; // 2 bytes
uint16_t pow_raw_u16 = ((uint16_t)p[6] << 8) | p[7]; // 2 bytes
if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE) { int32_t power = tuya_power16_to_signed(pow_raw_u16);
switch (attr) {
case ATTR_CURRENT_L1: float volt = volt_raw / 10.0f;
case ATTR_CURRENT_L1_ALT: float curr = curr_raw_u16 / 1000.0f;
meter_data.irms[PHASE_L1] = current;
meter_data.vrms[PHASE_L1] = volt; // Se queres “corrente com sinal”, deriva pelo sinal da potência:
meter_data.watt[PHASE_L1] = (int)power; float current = (power < 0) ? -curr : curr;
phase_updated[PHASE_L1] = true;
break; ESP_LOGD(TAG, "Attr 0x%04X: V=%.1fV I=%.3fA (signed=%+.3fA) P=%+ldW",
case ATTR_CURRENT_L2: attr, volt, curr, current, (long)power);
case ATTR_CURRENT_L2_ALT:
meter_data.irms[PHASE_L2] = current; if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
meter_data.vrms[PHASE_L2] = volt; {
meter_data.watt[PHASE_L2] = (int)power; switch (attr)
phase_updated[PHASE_L2] = true; {
break; case ATTR_CURRENT_L1:
case ATTR_CURRENT_L3: case ATTR_CURRENT_L1_ALT:
case ATTR_CURRENT_L3_ALT: meter_data.irms[PHASE_L1] = current;
meter_data.irms[PHASE_L3] = current; meter_data.vrms[PHASE_L1] = volt;
meter_data.vrms[PHASE_L3] = volt; meter_data.watt[PHASE_L1] = (int)power;
meter_data.watt[PHASE_L3] = (int)power; phase_updated[PHASE_L1] = true;
phase_updated[PHASE_L3] = true; break;
break; case ATTR_CURRENT_L2:
case ATTR_POWER_FACTOR: case ATTR_CURRENT_L2_ALT:
meter_data.power_factor = 0; meter_data.irms[PHASE_L2] = current;
break; meter_data.vrms[PHASE_L2] = volt;
case ATTR_FREQUENCY: meter_data.watt[PHASE_L2] = (int)power;
meter_data.frequency = 0; phase_updated[PHASE_L2] = true;
break; break;
case ATTR_TOTAL_ENERGY: case ATTR_CURRENT_L3:
meter_data.total_energy = 0; case ATTR_CURRENT_L3_ALT:
break; meter_data.irms[PHASE_L3] = current;
default: meter_data.vrms[PHASE_L3] = volt;
ESP_LOGW(TAG, "Unknown attr: 0x%04X", attr); meter_data.watt[PHASE_L3] = (int)power;
break; phase_updated[PHASE_L3] = true;
break;
case ATTR_POWER_FACTOR:
meter_data.power_factor = 0;
break;
case ATTR_FREQUENCY:
meter_data.frequency = 0;
break;
case ATTR_TOTAL_ENERGY:
meter_data.total_energy = 0;
break;
default:
ESP_LOGW(TAG, "Unknown attr: 0x%04X", attr);
break;
} }
xSemaphoreGive(meter_mutex); xSemaphoreGive(meter_mutex);
} }
// Verifica se todas as 3 fases foram atualizadas // Verifica se todas as 3 fases foram atualizadas
if (phase_updated[PHASE_L1] && phase_updated[PHASE_L2] && phase_updated[PHASE_L3]) { if (phase_updated[PHASE_L1] && phase_updated[PHASE_L2] && phase_updated[PHASE_L3])
{
meter_zigbee_post_event(); meter_zigbee_post_event();
memset(phase_updated, 0, sizeof(phase_updated)); memset(phase_updated, 0, sizeof(phase_updated));
} }
} }
static void meter_zigbee_task_func(void *param) { static void meter_zigbee_task_func(void *param)
{
uint8_t *buf = malloc(RX_FRAME_SIZE); uint8_t *buf = malloc(RX_FRAME_SIZE);
if (!buf) { if (!buf)
{
ESP_LOGE(TAG, "Failed to allocate buffer"); ESP_LOGE(TAG, "Failed to allocate buffer");
vTaskDelete(NULL); vTaskDelete(NULL);
return; return;
@@ -176,13 +198,19 @@ static void meter_zigbee_task_func(void *param) {
ESP_LOGI(TAG, "Zigbee meter task started"); ESP_LOGI(TAG, "Zigbee meter task started");
while (1) { while (1)
{
int len = uart_read_bytes(UART_PORT, buf, RX_FRAME_SIZE, pdMS_TO_TICKS(5000)); int len = uart_read_bytes(UART_PORT, buf, RX_FRAME_SIZE, pdMS_TO_TICKS(5000));
if (len == RX_FRAME_SIZE) { if (len == RX_FRAME_SIZE)
{
handle_zigbee_frame(buf, len); handle_zigbee_frame(buf, len);
} else if (len == 0) { }
else if (len == 0)
{
ESP_LOGD(TAG, "UART timeout with no data"); ESP_LOGD(TAG, "UART timeout with no data");
} else { }
else
{
ESP_LOGW(TAG, "Incomplete frame received (%d bytes)", len); ESP_LOGW(TAG, "Incomplete frame received (%d bytes)", len);
} }
} }
@@ -191,22 +219,24 @@ static void meter_zigbee_task_func(void *param) {
vTaskDelete(NULL); vTaskDelete(NULL);
} }
esp_err_t meter_zigbee_init(void) { esp_err_t meter_zigbee_init(void)
{
ESP_LOGI(TAG, "Initializing Zigbee meter"); ESP_LOGI(TAG, "Initializing Zigbee meter");
if (!meter_mutex) { if (!meter_mutex)
{
meter_mutex = xSemaphoreCreateMutex(); meter_mutex = xSemaphoreCreateMutex();
if (!meter_mutex) return ESP_ERR_NO_MEM; if (!meter_mutex)
return ESP_ERR_NO_MEM;
} }
uart_config_t config = { uart_config_t config = {
.baud_rate = 115200, .baud_rate = 115200,
.data_bits = UART_DATA_8_BITS, .data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE, .parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1, .stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT .source_clk = UART_SCLK_DEFAULT};
};
ESP_ERROR_CHECK(uart_param_config(UART_PORT, &config)); ESP_ERROR_CHECK(uart_param_config(UART_PORT, &config));
ESP_ERROR_CHECK(uart_set_pin(UART_PORT, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); ESP_ERROR_CHECK(uart_set_pin(UART_PORT, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
@@ -215,30 +245,28 @@ esp_err_t meter_zigbee_init(void) {
return ESP_OK; return ESP_OK;
} }
esp_err_t meter_zigbee_start(void) { esp_err_t meter_zigbee_start(void)
if (meter_zigbee_task) return ESP_ERR_INVALID_STATE; {
if (meter_zigbee_task)
return ESP_ERR_INVALID_STATE;
xTaskCreate(meter_zigbee_task_func, "meter_zigbee_task", 4096, NULL, 3, &meter_zigbee_task); xTaskCreate(meter_zigbee_task_func, "meter_zigbee_task", 4096, NULL, 3, &meter_zigbee_task);
return ESP_OK; return ESP_OK;
} }
void meter_zigbee_stop(void)
{
if (meter_zigbee_task)
{
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); vTaskDelete(meter_zigbee_task);
meter_zigbee_task = NULL; meter_zigbee_task = NULL;
} }
uart_driver_delete(UART_PORT); uart_driver_delete(UART_PORT);
if (meter_mutex) { if (meter_mutex)
{
vSemaphoreDelete(meter_mutex); vSemaphoreDelete(meter_mutex);
meter_mutex = NULL; meter_mutex = NULL;
} }

View File

@@ -2,34 +2,53 @@
#define METER_EVENTS_H #define METER_EVENTS_H
#include "esp_event.h" #include "esp_event.h"
#include "meter_manager.h" // Para meter_type_t #include "meter_manager.h" // meter_type_t
#include <stdint.h> // int32_t, int64_t
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C"
{
#endif #endif
// Base de eventos dos medidores // Base de eventos dos medidores
ESP_EVENT_DECLARE_BASE(METER_EVENT); ESP_EVENT_DECLARE_BASE(METER_EVENT);
// IDs de eventos emitidos por medidores // IDs de eventos emitidos por medidores
typedef enum { typedef enum
METER_EVENT_DATA_READY = 0, {
METER_EVENT_ERROR, METER_EVENT_DATA_READY = 0,
METER_EVENT_STARTED, METER_EVENT_ERROR,
METER_EVENT_STOPPED METER_EVENT_STARTED,
} meter_event_id_t; METER_EVENT_STOPPED,
METER_EVENT_CONFIG_UPDATED
} meter_event_id_t;
// Estrutura de dados enviados com METER_EVENT_DATA_READY // Estrutura de dados enviados com METER_EVENT_DATA_READY
typedef struct { // NOTA: campos não suportados pelo meter devem ficar a 0.
const char *source; // "GRID" ou "EVSE" typedef struct
float vrms[3]; // Tensão por fase {
float irms[3]; // Corrente por fase const char *source; // "GRID" ou "EVSE"
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;
float vrms[3]; // V por fase (0 se não existir)
float irms[3]; // A por fase (0 se não existir)
int32_t watt[3]; // W por fase (0 se não existir)
int32_t watt_total; // W total ASSINADO: +import / -export (0 se não existir)
float frequency; // Hz (0 se não existir)
float power_factor; // (0 se não existir)
float total_energy; // kWh (0 se não existir)
int64_t timestamp_us; // esp_timer_get_time() (0 => consumidor pode usar "now")
} meter_event_data_t;
// Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED
typedef struct
{
meter_type_t grid_type;
meter_type_t evse_type;
int64_t timestamp_us; // esp_timer_get_time()
} meter_config_event_t;
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -17,7 +17,8 @@ typedef enum {
METER_TYPE_DTS6619, // dts6619 METER_TYPE_DTS6619, // dts6619
METER_TYPE_MONO_ZIGBEE, // Zigbee single-phase METER_TYPE_MONO_ZIGBEE, // Zigbee single-phase
METER_TYPE_TRIF_ZIGBEE, // Zigbee three-phase METER_TYPE_TRIF_ZIGBEE, // Zigbee three-phase
METER_TYPE_EA777 // EA777 METER_TYPE_EA777, // EA777
METER_TYPE_DTS024M,
} meter_type_t; } meter_type_t;
/** /**

View File

@@ -9,11 +9,17 @@
#include "meter_dds661.h" #include "meter_dds661.h"
#include "meter_zigbee.h" #include "meter_zigbee.h"
#include "meter_ea777.h" #include "meter_ea777.h"
#include "meter_dts024m.h"
#include "nvs_flash.h"
#include "nvs.h"
#include <string.h> #include <string.h>
#include "network_events.h" #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"; static const char *TAG = "meter_manager";
@@ -21,179 +27,200 @@ static const char *TAG = "meter_manager";
static meter_type_t meter_evse_type = METER_TYPE_NONE; static meter_type_t meter_evse_type = METER_TYPE_NONE;
static meter_type_t meter_grid_type = METER_TYPE_NONE; static meter_type_t meter_grid_type = METER_TYPE_NONE;
#define NVS_NAMESPACE "meterconfig" #define STORE_NAMESPACE "meterconfig"
#define NVS_EVSE_MODEL "evse_model" #define STORE_EVSE_MODEL "evse_model"
#define NVS_GRID_MODEL "grid_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) 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) if (base != NETWORK_EVENTS)
return; return;
switch (event_id) switch (event_id)
{ {
case NETWORK_EVENT_AP_STARTED: case NETWORK_EVENT_AP_STARTED:
ESP_LOGI(TAG, "Recebido NETWORK_EVENT_AP_STARTED, parando medidor de grid"); ESP_LOGD(TAG, "Recebido NETWORK_EVENT_AP_STARTED, parando medidor de grid");
// meter_manager_grid_stop();
break; break;
case NETWORK_EVENT_AP_STOP: case NETWORK_EVENT_AP_STOP:
ESP_LOGI(TAG, "Recebido NETWORK_EVENT_AP_STOP, reiniciando medidor de grid"); ESP_LOGD(TAG, "Recebido NETWORK_EVENT_AP_STOP, reiniciando medidor de grid");
// meter_manager_grid_start();
break; break;
case NETWORK_EVENT_STA_GOT_IP: case NETWORK_EVENT_STA_GOT_IP:
ESP_LOGI(TAG, "Recebido NETWORK_EVENT_STA_GOT_IP"); ESP_LOGD(TAG, "Recebido NETWORK_EVENT_STA_GOT_IP");
// opcional: reiniciar ou logar
break; break;
default: default:
break; 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) static esp_err_t load_or_init_meter_model(const char *key, meter_type_t *type)
{ {
nvs_handle_t handle; if (!type)
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); return ESP_ERR_INVALID_ARG;
if (err != ESP_OK)
{ // tenta ler
ESP_LOGE(TAG, "Failed to open NVS handle for %s: %s", key, esp_err_to_name(err)); uint8_t value = 0xFF;
return err; 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) if (err == ESP_OK && value < 255)
{ {
*type = (meter_type_t)value; *type = (meter_type_t)value;
ESP_LOGI(TAG, "Loaded meter type %d from NVS key '%s'", value, key); ESP_LOGD(TAG, "Loaded meter type %u from storage key '%s/%s'", (unsigned)value, STORE_NAMESPACE, key);
} return ESP_OK;
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);
} }
nvs_close(handle); // se não existir / inválido -> default NONE e grava
return ESP_OK; *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 = storage_try_set_u8(STORE_NAMESPACE, key, (uint8_t)meter_type);
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) 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; return err;
} }
err = nvs_set_u8(handle, key, (uint8_t)meter_type); (void)storage_try_flush();
if (err == ESP_OK) ESP_LOGD(TAG, "Saved meter type %d to storage key '%s/%s'", (int)meter_type, STORE_NAMESPACE, key);
{ return 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;
} }
/** /**
* @brief Initializes the meter manager system. * @brief Inicializa o sistema de meter manager.
*
* 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.
*/ */
esp_err_t meter_manager_init(void) 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) esp_err_t err;
// err = meter_manager_evse_init();
// if (err != ESP_OK) return err;
// Initialize GRID meter // Initialize GRID meter
err = meter_manager_grid_init(); err = meter_manager_grid_init();
if (err != ESP_OK) if (err != ESP_OK)
return err; return err;
// Register handler for custom network events // Regista handler para eventos de rede
ESP_LOGI(TAG, "Registering network event handler"); /*
return esp_event_handler_register(NETWORK_EVENTS, ESP_LOGD(TAG, "Registering network event handler");
ESP_EVENT_ANY_ID, err = esp_event_handler_register(
meter_manager_network_event_handler, NETWORK_EVENTS,
NULL); 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 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 // Start GRID meter
err = meter_manager_grid_start(); return meter_manager_grid_start();
if (err != ESP_OK)
return err;
return ESP_OK;
} }
/**
* @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 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 // Stop GRID meter
err = meter_manager_grid_stop(); return meter_manager_grid_stop();
if (err != ESP_OK)
return err;
return ESP_OK;
} }
// ---------- EVSE ---------- // ---------- EVSE ----------
esp_err_t meter_manager_evse_init() 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) if (err != ESP_OK)
return err; 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) switch (meter_evse_type)
{ {
@@ -213,6 +240,8 @@ esp_err_t meter_manager_evse_init()
return meter_dds661_init(); return meter_dds661_init();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_init(); return meter_ea777_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_init(); return meter_zigbee_init();
@@ -242,6 +271,8 @@ esp_err_t meter_manager_evse_start()
return meter_dds661_start(); return meter_dds661_start();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_start(); return meter_ea777_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_start(); return meter_zigbee_start();
@@ -278,6 +309,9 @@ esp_err_t meter_manager_evse_stop(void)
case METER_TYPE_EA777: case METER_TYPE_EA777:
meter_ea777_stop(); meter_ea777_stop();
break; break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
break;
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
meter_zigbee_stop(); meter_zigbee_stop();
@@ -292,13 +326,13 @@ esp_err_t meter_manager_evse_stop(void)
esp_err_t meter_manager_grid_init() 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) if (err != ESP_OK)
return err; 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: case METER_TYPE_NONE:
return ESP_OK; return ESP_OK;
@@ -316,6 +350,8 @@ esp_err_t meter_manager_grid_init()
return meter_dds661_init(); return meter_dds661_init();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_init(); return meter_ea777_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_init(); return meter_zigbee_init();
@@ -334,7 +370,7 @@ esp_err_t meter_manager_grid_start()
case METER_TYPE_ADE7758: case METER_TYPE_ADE7758:
return meter_ade7758_start(); return meter_ade7758_start();
case METER_TYPE_ORNO513: case METER_TYPE_ORNO513:
return meter_orno513_start(); // corrigido return meter_orno513_start();
case METER_TYPE_ORNO516: case METER_TYPE_ORNO516:
return meter_orno516_start(); return meter_orno516_start();
case METER_TYPE_ORNO526: case METER_TYPE_ORNO526:
@@ -345,6 +381,8 @@ esp_err_t meter_manager_grid_start()
return meter_dds661_start(); return meter_dds661_start();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_start(); return meter_ea777_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_start(); return meter_zigbee_start();
@@ -364,7 +402,7 @@ esp_err_t meter_manager_grid_stop(void)
meter_ade7758_stop(); meter_ade7758_stop();
break; break;
case METER_TYPE_ORNO513: case METER_TYPE_ORNO513:
meter_orno513_stop(); // corrigido meter_orno513_stop();
break; break;
case METER_TYPE_ORNO516: case METER_TYPE_ORNO516:
meter_orno516_stop(); meter_orno516_stop();
@@ -381,6 +419,9 @@ esp_err_t meter_manager_grid_stop(void)
case METER_TYPE_EA777: case METER_TYPE_EA777:
meter_ea777_stop(); meter_ea777_stop();
break; break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
break;
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
meter_zigbee_stop(); meter_zigbee_stop();
@@ -391,18 +432,64 @@ esp_err_t meter_manager_grid_stop(void)
return ESP_OK; return ESP_OK;
} }
// ---------- Utilidades ---------- // ---------- Utilidades / setters com evento ----------
esp_err_t meter_manager_evse_set_model(meter_type_t meter_type) 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; 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 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; 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) bool meter_manager_evse_is_enabled(void)
@@ -410,15 +497,8 @@ bool meter_manager_evse_is_enabled(void)
return meter_manager_evse_get_model() != METER_TYPE_NONE; return meter_manager_evse_get_model() != METER_TYPE_NONE;
} }
meter_type_t meter_manager_evse_get_model(void) 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; }
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) const char *meter_type_to_str(meter_type_t type)
{ {
@@ -444,6 +524,8 @@ const char *meter_type_to_str(meter_type_t type)
return "TRIF-ZIGBEE"; return "TRIF-ZIGBEE";
case METER_TYPE_EA777: case METER_TYPE_EA777:
return "EA-777"; return "EA-777";
case METER_TYPE_DTS024M:
return "DTS-024M";
default: default:
return "NENHUM"; return "NENHUM";
} }
@@ -453,6 +535,7 @@ meter_type_t string_to_meter_type(const char *str)
{ {
if (!str) if (!str)
return METER_TYPE_NONE; return METER_TYPE_NONE;
if (strcmp(str, "IC ADE") == 0) if (strcmp(str, "IC ADE") == 0)
return METER_TYPE_ADE7758; return METER_TYPE_ADE7758;
if (strcmp(str, "ORNO-513") == 0) if (strcmp(str, "ORNO-513") == 0)
@@ -471,5 +554,8 @@ meter_type_t string_to_meter_type(const char *str)
return METER_TYPE_TRIF_ZIGBEE; return METER_TYPE_TRIF_ZIGBEE;
if (strcmp(str, "EA-777") == 0) if (strcmp(str, "EA-777") == 0)
return METER_TYPE_EA777; return METER_TYPE_EA777;
if (strcmp(str, "DTS-024M") == 0)
return METER_TYPE_DTS024M;
return METER_TYPE_NONE; return METER_TYPE_NONE;
} }

View File

@@ -5,4 +5,4 @@ set(srcs
idf_component_register(SRCS "${srcs}" idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash esp_netif esp_wifi mdns esp_event) PRIV_REQUIRES nvs_flash esp_netif esp_wifi storage_service mdns esp_event)

View File

@@ -18,7 +18,7 @@
typedef struct typedef struct
{ {
char ssid[32]; char ssid[33];
int rssi; int rssi;
bool auth; bool auth;
} wifi_scan_ap_t; } wifi_scan_ap_t;

View File

@@ -11,22 +11,24 @@
#include "freertos/event_groups.h" #include "freertos/event_groups.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_err.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "esp_mac.h" #include "esp_mac.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "mdns.h" #include "mdns.h"
#include "network_events.h" #include "network_events.h"
#include "network.h" #include "network.h"
// NEW:
#include "storage_service.h"
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Config // Config
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#define AP_SSID "plx-%02x%02x%02x" // SSID do AP (usa 3 bytes do MAC) #define AP_SSID "plx-%02x%02x%02x"
#define MDNS_SSID "plx%02x" // hostname mDNS (usa 2 bytes do MAC) #define MDNS_SSID "plx%02x"
#define NVS_NAMESPACE "wifi" #define NVS_NAMESPACE "wifi"
#define NVS_ENABLED "enabled" #define NVS_ENABLED "enabled"
@@ -35,60 +37,106 @@
// Comprimentos com terminador // Comprimentos com terminador
#define SSID_MAX_LEN 32 #define SSID_MAX_LEN 32
#define PASS_MAX_LEN 64 // 63 chars + '\0' #define PASS_MAX_LEN 64
#define SSID_BUF_SZ (SSID_MAX_LEN + 1) // 33 #define SSID_BUF_SZ (SSID_MAX_LEN + 1)
#define PASS_BUF_SZ (PASS_MAX_LEN + 1) // 65 #define PASS_BUF_SZ (PASS_MAX_LEN + 1)
static const char *TAG = "wifi"; static const char *TAG = "wifi";
// Storage timeouts
#define STORE_TO pdMS_TO_TICKS(800)
#define STORE_FLUSH_TO pdMS_TO_TICKS(2000)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Estado global // Estado global
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
static nvs_handle_t nvs;
static esp_netif_t *sta_netif; static esp_netif_t *sta_netif;
static esp_netif_t *ap_netif; static esp_netif_t *ap_netif;
EventGroupHandle_t wifi_event_group; 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 int s_retry_count = 0;
static const int s_retry_max = 7; 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 store_flush_best_effort(void)
static esp_err_t nvs_get_str_safe(nvs_handle_t h, const char *key, char *out, size_t out_sz) {
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) if (!out || out_sz == 0)
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
out[0] = '\0'; out[0] = '\0';
size_t need = 0; // buffer grande (sem heap): usa o máximo definido no storage_service
esp_err_t err = nvs_get_str(h, key, NULL, &need); char tmp[STORAGE_MAX_VALUE_BYTES + 1];
if (err == ESP_ERR_NVS_NOT_FOUND) memset(tmp, 0, sizeof(tmp));
return ESP_OK;
if (err != ESP_OK)
return err;
if (need == 0) esp_err_t e = storage_get_str_sync(ns, key, tmp, sizeof(tmp), STORE_TO);
return ESP_OK; // vazio if (e == ESP_ERR_NOT_FOUND)
if (need > out_sz)
{ {
// Truncar de forma segura out[0] = '\0';
char *tmp = (char *)malloc(need); return ESP_OK;
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;
} }
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) 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) if (event_base == WIFI_EVENT)
{ {
switch (event_id) switch (event_id)
@@ -125,6 +175,16 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
esp_wifi_connect(); esp_wifi_connect();
break; break;
} }
case WIFI_EVENT_STA_CONNECTED:
{
ESP_LOGI(TAG, "STA associated (L2 connected)");
esp_event_post(NETWORK_EVENTS,
NETWORK_EVENT_STA_CONNECTED,
NULL,
0,
portMAX_DELAY);
break;
}
case WIFI_EVENT_STA_DISCONNECTED: case WIFI_EVENT_STA_DISCONNECTED:
{ {
xEventGroupClearBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); xEventGroupClearBits(wifi_event_group, WIFI_STA_CONNECTED_BIT);
@@ -133,7 +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; wifi_event_sta_disconnected_t *ev = (wifi_event_sta_disconnected_t *)event_data;
ESP_LOGW(TAG, "STA disconnected, reason=%d", ev ? ev->reason : -1); ESP_LOGW(TAG, "STA disconnected, reason=%d", ev ? ev->reason : -1);
// NÃO bloquear o event loop com vTaskDelay esp_event_post(NETWORK_EVENTS,
NETWORK_EVENT_STA_DISCONNECTED,
ev,
ev ? sizeof(*ev) : 0,
portMAX_DELAY);
if (s_retry_count < s_retry_max) if (s_retry_count < s_retry_max)
{ {
esp_err_t err = esp_wifi_connect(); esp_err_t err = esp_wifi_connect();
@@ -144,7 +209,7 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
} }
else else
{ {
ESP_LOGW(TAG, "esp_wifi_connect failed (%d)", err); ESP_LOGW(TAG, "esp_wifi_connect failed (%s)", esp_err_to_name(err));
} }
} }
else else
@@ -166,14 +231,21 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
xEventGroupClearBits(wifi_event_group, WIFI_STA_DISCONNECTED_BIT); xEventGroupClearBits(wifi_event_group, WIFI_STA_DISCONNECTED_BIT);
xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT);
s_retry_count = 0; s_retry_count = 0;
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) else if (event_id == IP_EVENT_STA_LOST_IP)
{ {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data; ESP_LOGW(TAG, "STA lost IP");
ESP_LOGI(TAG, "WiFi STA got ip6: " IPV6STR, IPV62STR(event->ip6_info.ip)); esp_event_post(NETWORK_EVENTS,
xEventGroupClearBits(wifi_event_group, WIFI_STA_DISCONNECTED_BIT); NETWORK_EVENT_STA_LOST_IP,
xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT); NULL,
s_retry_count = 0; 0,
portMAX_DELAY);
} }
} }
} }
@@ -192,28 +264,21 @@ static void sta_set_config(void)
wifi_config.sta.pmf_cfg.capable = true; wifi_config.sta.pmf_cfg.capable = true;
wifi_config.sta.pmf_cfg.required = false; wifi_config.sta.pmf_cfg.required = false;
// buffers "seguros" vindos da NVS (estes sim têm terminador) char ssid_buf[SSID_BUF_SZ] = {0};
char ssid_buf[SSID_BUF_SZ] = {0}; // 33 (32 + '\0') char pass_buf[PASS_BUF_SZ] = {0};
char pass_buf[PASS_BUF_SZ] = {0}; // 65 (64 + '\0')
wifi_get_ssid(ssid_buf); wifi_get_ssid(ssid_buf);
wifi_get_password(pass_buf); wifi_get_password(pass_buf);
// Copiar **sem** terminador para os campos do esp_wifi (32/64 bytes) size_t ssid_len = strnlen(ssid_buf, SSID_MAX_LEN);
// SSID: max 32
size_t ssid_len = strnlen(ssid_buf, SSID_MAX_LEN); // até 32
memcpy(wifi_config.sta.ssid, ssid_buf, ssid_len); memcpy(wifi_config.sta.ssid, ssid_buf, ssid_len);
// Password WPA/WPA2: 8..63 chars (campo tem 64 bytes) size_t pass_len = strnlen(pass_buf, 63);
// (se usares rede aberta, pass_len pode ser 0)
size_t pass_len = strnlen(pass_buf, 63); // até 63
memcpy(wifi_config.sta.password, pass_buf, pass_len); memcpy(wifi_config.sta.password, pass_buf, pass_len);
if (pass_len <= 63) if (pass_len <= 63)
{
wifi_config.sta.password[pass_len] = '\0'; wifi_config.sta.password[pass_len] = '\0';
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 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) static void ap_set_config(void)
@@ -222,14 +287,13 @@ static void ap_set_config(void)
wifi_config_t wifi_ap_config = {0}; wifi_config_t wifi_ap_config = {0};
wifi_ap_config.ap.max_connection = 1; 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]; uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_AP, mac); esp_wifi_get_mac(WIFI_IF_AP, mac);
snprintf((char *)wifi_ap_config.ap.ssid, sizeof(wifi_ap_config.ap.ssid), 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_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config));
} }
@@ -248,7 +312,7 @@ static void sta_try_start(void)
esp_err_t e = esp_wifi_start(); esp_err_t e = esp_wifi_start();
if (e != ESP_OK && e != ESP_ERR_WIFI_CONN) 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); xEventGroupSetBits(wifi_event_group, WIFI_STA_MODE_BIT);
} }
@@ -261,8 +325,10 @@ void wifi_ini(void)
{ {
ESP_LOGI(TAG, "Wifi init"); ESP_LOGI(TAG, "Wifi init");
// Abre NVS (assume que nvs_flash_init() já foi chamado no boot geral) // garante storage pronto (não assume NVS handle aberto aqui)
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs)); 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(); wifi_event_group = xEventGroupCreate();
@@ -275,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(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)); 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]; uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac); esp_wifi_get_mac(WIFI_IF_STA, mac);
char chargeid[16]; char chargeid[16];
// ex.: "plx00"
snprintf(chargeid, sizeof(chargeid), MDNS_SSID, 0); 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(sta_netif, chargeid));
ESP_ERROR_CHECK(esp_netif_set_hostname(ap_netif, chargeid)); ESP_ERROR_CHECK(esp_netif_set_hostname(ap_netif, chargeid));
@@ -300,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); ESP_LOGI(TAG, "wifi_set_config(enabled=%d)", enabled);
// Validação (quando habilitar STA) // Validação
if (enabled) if (enabled)
{ {
// SSID 1..32
if (ssid && (strlen(ssid) == 0 || strlen(ssid) > SSID_MAX_LEN)) if (ssid && (strlen(ssid) == 0 || strlen(ssid) > SSID_MAX_LEN))
{ {
ESP_LOGE(TAG, "SSID out of range"); ESP_LOGE(TAG, "SSID out of range");
@@ -311,15 +373,15 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
} }
if (!ssid) if (!ssid)
{ {
size_t len = 0; char cur_ssid[SSID_BUF_SZ] = {0};
esp_err_t e = nvs_get_str(nvs, NVS_SSID, NULL, &len); wifi_get_ssid(cur_ssid);
if (e != ESP_OK || len <= 1) if (strlen(cur_ssid) == 0)
{ {
ESP_LOGE(TAG, "Required SSID"); ESP_LOGE(TAG, "Required SSID");
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
} }
// Password: 8..63 (se não for vazia). Aceita "" para rede open (caso uses).
if (password) if (password)
{ {
size_t lp = strlen(password); size_t lp = strlen(password);
@@ -331,13 +393,27 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
} }
} }
// Persiste no NVS // Persiste via storage_service
ESP_ERROR_CHECK(nvs_set_u8(nvs, NVS_ENABLED, enabled)); esp_err_t err = store_set_u8_best_effort(NVS_NAMESPACE, NVS_ENABLED, enabled ? 1 : 0);
if (err != ESP_OK)
return err;
if (ssid) 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) 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 // Reinicia modo
ESP_LOGI(TAG, "Stopping AP/STA"); ESP_LOGI(TAG, "Stopping AP/STA");
@@ -345,7 +421,7 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
esp_err_t e = esp_wifi_stop(); esp_err_t e = esp_wifi_stop();
if (e != ESP_OK && e != ESP_ERR_WIFI_NOT_INIT && e != ESP_ERR_WIFI_STOP_STATE) 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(); sta_try_start();
@@ -367,21 +443,21 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps)
esp_err_t err = esp_wifi_scan_start(NULL, true); esp_err_t err = esp_wifi_scan_start(NULL, true);
if (err != ESP_OK) 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; return 0;
} }
err = esp_wifi_scan_get_ap_records(&number, ap_info); err = esp_wifi_scan_get_ap_records(&number, ap_info);
if (err != ESP_OK) 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; return 0;
} }
err = esp_wifi_scan_get_ap_num(&ap_count); err = esp_wifi_scan_get_ap_num(&ap_count);
if (err != ESP_OK) 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; return 0;
} }
@@ -389,8 +465,7 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps)
for (int i = 0; (i < WIFI_SCAN_SCAN_LIST_SIZE) && (i < ap_count); i++) 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);
snprintf(scan_aps[i].ssid, SSID_BUF_SZ, "%s", (const char *)ap_info[i].ssid);
scan_aps[i].rssi = ap_info[i].rssi; scan_aps[i].rssi = ap_info[i].rssi;
scan_aps[i].auth = ap_info[i].authmode != WIFI_AUTH_OPEN; scan_aps[i].auth = ap_info[i].authmode != WIFI_AUTH_OPEN;
} }
@@ -401,29 +476,29 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps)
bool wifi_get_enabled(void) bool wifi_get_enabled(void)
{ {
uint8_t value = 0; uint8_t value = 0;
esp_err_t e = nvs_get_u8(nvs, NVS_ENABLED, &value); esp_err_t e = storage_get_u8_sync(NVS_NAMESPACE, NVS_ENABLED, &value, STORE_TO);
if (e == ESP_ERR_NVS_NOT_FOUND) if (e == ESP_ERR_NOT_FOUND)
return false; return false;
if (e != ESP_OK) 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 false;
} }
return value; return (value != 0);
} }
void wifi_get_ssid(char *value) void wifi_get_ssid(char *value)
{ {
if (!value) if (!value)
return; 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) void wifi_get_password(char *value)
{ {
if (!value) if (!value)
return; 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) void wifi_ap_start(void)
@@ -439,7 +514,7 @@ void wifi_ap_start(void)
esp_err_t e = esp_wifi_start(); esp_err_t e = esp_wifi_start();
if (e != ESP_OK && e != ESP_ERR_WIFI_CONN) 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); xEventGroupSetBits(wifi_event_group, WIFI_AP_MODE_BIT);

Some files were not shown because too many files have changed in this diff Show More