5 Commits

Author SHA1 Message Date
2a803fcef1 chore: snapshot before shared RS485 bus integration 2026-07-22 15:36:04 +01:00
ef02e5c5f5 new release 2026-05-15 12:21:17 +01:00
d0d431daf2 new release 2026-05-15 12:20:47 +01:00
286028b6a8 fix evse_link 2026-01-24 16:56:51 +00:00
023644a887 new upgrade 2025-12-21 23:28:26 +00:00
593 changed files with 16888 additions and 9751 deletions

0
.gitignore vendored Executable file → Normal file
View File

0
CMakeLists.txt Executable file → Normal file
View File

0
LICENSE.md Executable file → Normal file
View File

209
README.md Executable file → Normal file
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)
[![License](https://img.shields.io/github/license/dzurikmiroslav/esp32-evse.svg)](LICENSE.md)
- IEC-style EVSE state machine (Control Pilot A/B/C/D)
- Wi-Fi (STA + AP for local configuration)
- REST API served from SPIFFS
- Local authentication and OCPP integration
- Load balancing (master + slaves)
- Scheduler (time windows)
- Audible feedback (buzzer) and RGB LED status
- On-device ring-buffer logger
## Key features
- Hardware abstraction for device design
- Responsive web-interface
- OTA update
- Integrated energy meter
- Energy detection for relay control
- [REST](https://github.com/dzurikmiroslav/esp32-evse/wiki/Rest) API
- MQTT API
- [Modbus](https://github.com/dzurikmiroslav/esp32-evse/wiki/Modbus) (RS485, TCP)
- [Scripting](https://github.com/dzurikmiroslav/esp32-evse/wiki/Script)
- [Nextion HMI](https://github.com/dzurikmiroslav/esp32-evse/wiki/Nextion)
---
### Device definition method
## Features
_One firmware to rule them all._ Not really :-) one per device platform (ESP32, ESP32-S2...).
### Core EVSE
There is no need to compile the firmware for your EVSE design.
Source code ist not hardcoded to GPIOs or other hardware design features.
All code is written in ESP-IDF without additional mapping layer like Arduino.
- EVSE manager (`evse_manager`) coordinating:
- Hardware layer (`evse_hardware`)
- State machine (`evse_state`)
- Error handling (`evse_error`)
- Energy metering (`evse_meter` / `meter_manager`)
- Session tracking (`evse_session`)
- Runs a periodic tick (`evse_manager_tick()`) in its own FreeRTOS task.
- Supports multiple auth modes (OPEN / RFID / OCPP), with scheduling and load-balancer aware logic.
All configuration is written outside firmware in configuration file named _board.cfg_ on dedicated partition.
For example, on following scheme is minimal EVSE circuit with ESP32 devkit.
### Networking & REST
![Minimal circuit](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/minimal-circuit.png)
- Wi-Fi:
- Station mode for normal operation.
- Access point mode for local configuration, enabled by a physical button.
- REST server (`rest_main`) serving from `/data` SPIFFS mount:
- For configuration, status, logs, etc. (exact endpoints depend on your REST implementation).
For this circuit there is _board.cfg_, for more information's see [Wiki](https://github.com/dzurikmiroslav/esp32-evse/wiki/Board-config).
### Button & User Input
- One physical button (configured via `board_config`):
- **Short press** → Starts Wi-Fi AP mode for configuration.
- **Long press (~30s)** → Erases NVS and reboots (factory-like reset).
- Robust handling:
- ISR with software debounce and spinlock.
- Dedicated `user_input_task` that receives button press/release notifications via `xTaskNotify`.
```bash
#Device name
DEVICE_NAME=ESP32 minimal EVSE
#Button
BUTTON_WIFI_GPIO=0
#Pilot
PILOT_PWM_GPIO=33
PILOT_ADC_CHANNEL=7
PILOT_DOWN_THRESHOLD_12=2410
PILOT_DOWN_THRESHOLD_9=2104
PILOT_DOWN_THRESHOLD_6=1797
PILOT_DOWN_THRESHOLD_3=1491
PILOT_DOWN_THRESHOLD_N12=265
#AC relay
AC_RELAY_GPIO=32
```
### Storage
### Web interface
- SPIFFS used for:
- `/cfg` partition: persistent configuration.
- `/data` partition: web assets, runtime data, logs, etc.
- Two separate mounts:
- `cfg_conf` → `/cfg` (label: `cfg`)
- `data_conf` → `/data` (label: `data`)
Fully responsive web interface is accessible local network IP address on port 80.
### LED Subsystem
Dashboard page
- RGB LED driven by LEDC:
- `ledc_driver` abstracts LEDC timer + channels.
- `led` module maps EVSE state & sessions to colors/patterns.
- LED patterns per EVSE state:
- **IDLE** → Green solid.
- **WAITING** (vehicle plugged, not charging) → Blue slow blink.
- **CHARGING** → Blue “breathing” effect.
- **FAULT** → Red fast blink.
- Session effects:
- Distinct visual patterns when a session starts/finishes.
- Uses a one-shot timer and a dedicated effect state machine.
![Dashboard](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-dashboard.png)
### Buzzer
Settings page
- Buzzer with multiple patterns (`buzzer` + `buzzer_events`):
- Plugged/unplugged, card read/denied, AP start, charging, fault, etc.
- Supported modes:
- Active buzzer (ON/OFF).
- Passive buzzer with LEDC PWM (frequency & duty configurable).
- Features:
- Central queue + dedicated `buzzer_task`.
- Quiet hours support (optionally suppress non-critical sounds at night).
- Anti-spam mechanism to avoid excessively frequent beeps.
- Integrated with:
- EVSE events (state changes & faults)
- Auth events (RFID card success/denied/added)
- Network events (AP/STA up)
![Settings](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-settings.png)
### Load Balancer
Mobile dashboard page
- `loadbalancer` component:
- Monitors GRID meter and EVSE meter via `meter_events`.
- Supports one master + up to 255 slaves (connectors array).
- Fair distribution of current with:
- Headroom calculation based on grid limit and measured current.
- Min current guarantees (e.g. 6 A) using a “water-filling” algorithm.
- Session-age based priority (oldest sessions first).
- Per-connector hysteresis and LB suspension/resume flags.
- Publishes limits via `LOADBALANCER_EVENTS`:
- `LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT`
- `LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT`
- Fail-safe behavior:
- If GRID meter data times out, clamps connectors to minimum safe current instead of ramping up.
![Dashboard mobile](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-dashboard-mobile.png)
### Scheduler
## Hardware
- Scheduler component (`scheduler`) emits `SCHED_EVENTS` with `allowed_now` flag:
- EVSE manager revokes authorization when the window closes.
- In OPEN mode, automatic re-authorization only happens when scheduler allows.
### ESP32DevkitC
### OCPP
Dev board with basic functionality, single phase energy meter, RS485. One side pcb, for DIY makers easy to make at home conditions ;-)
- `ocpp` module integration:
- Listens to OCPP events (`OCPP_EVENTS`).
- Handles:
- RemoteStart/Stop
- Authorization results
- ChangeAvailability (operative/inoperative) → mapped into local `enabled` config.
- EVSE manager mediates OCPP decisions with scheduler + load balancer.
[EasyEDA project](https://oshwlab.com/dzurik.miroslav/esp32-devkit-evse)
### Logger
![ESP32DevkitC](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/esp32devkitc.jpg)
- `logger` + `output_buffer` components:
- Central log sink with ring buffer in RAM.
- Thread-safe via FreeRTOS mutex.
- Integrated with ESP log system via `esp_log_set_vprintf(logger_vprintf);`
- Optionally mirrors to UART (controlled via `CONFIG_ESP_CONSOLE_UART`).
- Simple reader API:
- Iterate entries using an index.
- Handy for exposing logs over REST/Web UI.
### ESP32-S2 DIY ALPHA
---
ESP32-S2 based EVSE with advanced functionality, three phase energy meter, RS485, UART, 1WIRE, RCM, socket lock.
## Project Structure (Relevant Parts)
[EasyEDA project](https://oshwlab.com/dzurik.miroslav/esp32s2-diy-evse)
Approximate layout (names may vary slightly in your repo):
![ESP32-S2-DA](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/esp32s2da.jpg)
```text
main/
main.c # System entrypoint, button setup, module init
components/
evse/
evse_manager.c/.h # High-level EVSE orchestration
evse_state.c/.h # State machine & events
evse_error.c/.h # Error handling
evse_hardware.c/.h # Hardware abstraction
evse_session.c/.h # Session metrics
loadbalancer/
src/
loadbalancer.c
loadbalancer_events.c
input_filter.c
include/
loadbalancer.h
loadbalancer_events.h
input_filter.h
buzzer/
src/
buzzer.c
buzzer_events.c
include/
buzzer.h
buzzer_events.h
led/
src/
led.c
ledc_driver.c
include/
led.h
ledc_driver.h
logger/
src/
logger.c
output_buffer.c
include/
logger.h
output_buffer.h
# ... other modules: auth, ocpp, scheduler, meter_manager, evse_link, etc.

0
cfg/custom/board.cfg Executable file → Normal file
View File

106
cfg/esp32devkitc/board.cfg Executable file → Normal file
View File

@@ -1,32 +1,115 @@
# ChargeFlow board.cfg
# ------------------------------------------------------------
# Este ficheiro suporta duas variantes de hardware:
#
# 1) Board antiga -> Pilot no ADC interno do ESP32
# 2) Board recente -> Pilot no ADC externo ADC121S021
#
# Para mudar de board, alterar apenas a secção "SELEÇÃO DE PERFIL".
# As restantes opções são comuns às duas boards.
#
# IMPORTANTE:
# - Não deixar duas linhas iguais ativas ao mesmo tempo.
# - Linhas começadas por # são comentários.
# - Na board com ADC externo, PILOT_ADC_SOURCE pode ser omitido,
# porque o firmware usa ADC121S021 por defeito.
# ------------------------------------------------------------
# ============================================================
# SELEÇÃO DE PERFIL
# ============================================================
# ------------------------------------------------------------
# PERFIL ATIVO: Board antiga com ADC interno ESP32
# ------------------------------------------------------------
#led_red_GPIO=13
#BUZZER_GPIO=21
#PILOT_ADC_SOURCE=internal
# ------------------------------------------------------------
# PERFIL ALTERNATIVO: Board recente com ADC externo ADC121S021
# Para usar esta board:
# 1. Comentar as 3 linhas do perfil ativo acima
# 2. Descomentar as 3 linhas abaixo
# ------------------------------------------------------------
led_red_GPIO=26
BUZZER_GPIO=27
PILOT_ADC_SOURCE=adc121
# ============================================================
# CONFIGURAÇÃO COMUM
# ============================================================
DEVICE_NAME=ChargeFlow
# ------------------------------------------------------------
# LEDs
# ------------------------------------------------------------
led_blue=y
led_blue_GPIO=14
led_red=y
led_red_GPIO=26
# led_red_GPIO é definido na SELEÇÃO DE PERFIL
led_green=y
led_green_GPIO=12
#BUZZER
BUZZER=y
BUZZER_GPIO=27
#Button
# ------------------------------------------------------------
# BUZZER
# ------------------------------------------------------------
BUZZER=y
# BUZZER_GPIO é definido na SELEÇÃO DE PERFIL
# ------------------------------------------------------------
# Botão Wi-Fi
# ------------------------------------------------------------
BUTTON_WIFI_GPIO=32
#Pilot
# ------------------------------------------------------------
# Pilot / Control Pilot
# ------------------------------------------------------------
# PILOT_ADC_SOURCE é definido na SELEÇÃO DE PERFIL.
#
# Valores possíveis:
# internal -> usa ADC interno do ESP32
# adc121 -> usa ADC externo ADC121S021
#
# Board antiga:
# PILOT_ADC_SOURCE=internal
#
# Board recente:
# PILOT_ADC_SOURCE=adc121
# ou omitir a linha, porque o default do firmware é ADC121S021.
#
# Nota:
# PILOT_ADC_CHANNEL é canal ADC, não GPIO.
# Em ESP32 clássico, ADC1_CH6 normalmente corresponde ao GPIO34.
# ------------------------------------------------------------
PILOT_PWM_GPIO=33
PILOT_ADC_CHANNEL=6
# Thresholds em mV no pino ADC/saída do circuito de medição
PILOT_DOWN_THRESHOLD_12=3000
PILOT_DOWN_THRESHOLD_9=2600
PILOT_DOWN_THRESHOLD_6=2200
PILOT_DOWN_THRESHOLD_3=1950
PILOT_DOWN_THRESHOLD_N12=500
# ------------------------------------------------------------
# Proximity
# ------------------------------------------------------------
PROXIMITY=y
# Em ESP32 clássico, ADC1_CH3 normalmente corresponde ao GPIO39.
PROXIMITY_ADC_CHANNEL=3
PROXIMITY_DOWN_THRESHOLD_8=2450
PROXIMITY_DOWN_THRESHOLD_10=2050
PROXIMITY_DOWN_THRESHOLD_13=1650
@@ -34,11 +117,20 @@ PROXIMITY_DOWN_THRESHOLD_20=820
PROXIMITY_DOWN_THRESHOLD_25=610
PROXIMITY_DOWN_THRESHOLD_32=430
#AC relay
# ------------------------------------------------------------
# Relé AC
# ------------------------------------------------------------
AC_RELAY_GPIO=25
# ------------------------------------------------------------
# Cable lock
# ------------------------------------------------------------
SOCKET_LOCK=n
# Como SOCKET_LOCK=n, estes GPIOs podem ficar vazios.
# Se SOCKET_LOCK=y, preencher sempre valores válidos.
SOCKET_LOCK_A_GPIO=
SOCKET_LOCK_B_GPIO=
SOCKET_LOCK_DETECTION_GPIO=

0
cfg/esp32s2da/board.cfg Executable file → Normal file
View File

4
components/auth/CMakeLists.txt Executable file → Normal file
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}"
INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src"
PRIV_REQUIRES nvs_flash driver esp_timer
REQUIRES esp_event evse ocpp evse_link)
PRIV_REQUIRES driver esp_timer
REQUIRES esp_event evse ocpp evse_link storage_service)

0
components/auth/idf_component.yml Executable file → Normal file
View File

0
components/auth/include/auth.h Executable file → Normal file
View File

0
components/auth/include/wiegand.h Executable file → Normal file
View File

0
components/auth/include/wiegand_reader.h Executable file → Normal file
View File

363
components/auth/src/auth.c Executable file → Normal file
View File

@@ -4,19 +4,22 @@
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <esp_err.h>
#include <string.h>
#include <strings.h> // strcasecmp
#include <strings.h>
#include <stdio.h>
#include "wiegand_reader.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "storage_service.h"
#include "evse_link.h"
#include "evse_link_events.h"
#define MAX_TAGS 50
static const char *TAG = "Auth";
/* ===== Estado ===== */
@@ -25,162 +28,16 @@ static bool waiting_for_registration = false;
static char valid_tags[MAX_TAGS][AUTH_TAG_MAX_LEN];
static int tag_count = 0;
static uint32_t s_next_req_id = 1;
static bool s_wiegand_started = false; // controla se o Wiegand já foi iniciado
static bool s_wiegand_started = false;
/* ===== NVS keys ===== */
/* ===== Storage keys ===== */
#define NVS_NAMESPACE "auth"
#define NVS_TAG_PREFIX "tag_"
#define NVS_TAG_COUNT_KEY "count"
#define NVS_MODE_KEY "mode" // uint8_t
/* =========================
* NVS Persistence (tags)
* ========================= */
static void load_tags_from_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "No stored tags in NVS (nvs_open: %s)", esp_err_to_name(err));
return;
}
uint8_t count = 0;
err = nvs_get_u8(handle, NVS_TAG_COUNT_KEY, &count);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "No tag count key in NVS (nvs_get_u8: %s)", esp_err_to_name(err));
nvs_close(handle);
return;
}
tag_count = 0;
for (int i = 0; i < count && i < MAX_TAGS; i++)
{
char key[16];
char tag_buf[AUTH_TAG_MAX_LEN];
size_t len = sizeof(tag_buf);
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
err = nvs_get_str(handle, key, tag_buf, &len);
if (err == ESP_OK)
{
strncpy(valid_tags[tag_count], tag_buf, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++;
}
else
{
ESP_LOGW(TAG, "Failed to load tag %d from NVS (%s)", i, esp_err_to_name(err));
}
}
nvs_close(handle);
ESP_LOGI(TAG, "Loaded %d tags from NVS", tag_count);
}
static void save_tags_to_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS to save tags: %s", esp_err_to_name(err));
return;
}
err = nvs_set_u8(handle, NVS_TAG_COUNT_KEY, tag_count);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_set_u8(count) failed: %s", esp_err_to_name(err));
nvs_close(handle);
return;
}
for (int i = 0; i < tag_count; i++)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
err = nvs_set_str(handle, key, valid_tags[i]);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_set_str(%s) failed: %s", key, esp_err_to_name(err));
nvs_close(handle);
return;
}
}
err = nvs_commit(handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_commit failed when saving tags: %s", esp_err_to_name(err));
nvs_close(handle);
return;
}
nvs_close(handle);
ESP_LOGI(TAG, "Tags saved to NVS (%d tags)", tag_count);
}
/* =========================
* NVS Persistence (mode)
* ========================= */
static void load_mode_from_nvs(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_OK)
{
uint8_t u = (uint8_t)AUTH_MODE_OPEN;
err = nvs_get_u8(h, NVS_MODE_KEY, &u);
if (err == ESP_OK)
{
if (u <= (uint8_t)AUTH_MODE_OCPP_RFID)
s_mode = (auth_mode_t)u;
}
else
{
ESP_LOGW(TAG, "No stored auth mode in NVS (nvs_get_u8: %s). Default OPEN", esp_err_to_name(err));
}
nvs_close(h);
}
else
{
ESP_LOGW(TAG, "No stored auth mode in NVS (nvs_open: %s). Default OPEN", esp_err_to_name(err));
}
ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode));
}
static void save_mode_to_nvs(auth_mode_t mode)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS to save auth mode: %s", esp_err_to_name(err));
return;
}
err = nvs_set_u8(h, NVS_MODE_KEY, (uint8_t)mode);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_set_u8(mode) failed: %s", esp_err_to_name(err));
nvs_close(h);
return;
}
err = nvs_commit(h);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_commit failed when saving mode: %s", esp_err_to_name(err));
nvs_close(h);
return;
}
nvs_close(h);
ESP_LOGI(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode));
}
// timeout para operações sync do storage
#define STORAGE_TO pdMS_TO_TICKS(2000)
/* =========================
* Helpers
@@ -190,37 +47,155 @@ static bool is_tag_valid(const char *tag)
for (int i = 0; i < tag_count; i++)
{
if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0)
{
return true;
}
}
return false;
}
/* =========================
* Storage Persistence (tags)
* ========================= */
static void load_tags_from_storage(void)
{
uint8_t count = 0;
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, &count, STORAGE_TO);
if (err == ESP_ERR_NOT_FOUND)
{
ESP_LOGD(TAG, "No stored tags (count not found)");
tag_count = 0;
return;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Failed to read tag count (%s)", esp_err_to_name(err));
tag_count = 0;
return;
}
tag_count = 0;
for (int i = 0; i < (int)count && i < MAX_TAGS; i++)
{
char key[16];
char tag_buf[AUTH_TAG_MAX_LEN] = {0};
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
err = storage_get_str_sync(NVS_NAMESPACE, key, tag_buf, sizeof(tag_buf), STORAGE_TO);
if (err == ESP_OK)
{
if (tag_buf[0] != '\0')
{
strncpy(valid_tags[tag_count], tag_buf, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++;
}
}
else if (err == ESP_ERR_NOT_FOUND)
{
// pode acontecer se count estiver desfasado; ignora
continue;
}
else
{
ESP_LOGW(TAG, "Failed to load tag %d (%s)", i, esp_err_to_name(err));
}
}
ESP_LOGI(TAG, "Loaded %d tags from storage", tag_count);
}
static void save_tags_to_storage(void)
{
// ler count antigo (para apagar keys antigas se removemos tags)
uint8_t old_count = 0;
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, &old_count, STORAGE_TO);
if (err == ESP_ERR_NOT_FOUND)
old_count = 0;
// grava count + tags
(void)storage_set_u8_async(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, (uint8_t)tag_count);
for (int i = 0; i < tag_count; i++)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
(void)storage_set_str_async(NVS_NAMESPACE, key, valid_tags[i]);
}
// se removemos tags: apagar chaves antigas
if (old_count > (uint8_t)tag_count)
{
for (int i = tag_count; i < (int)old_count && i < MAX_TAGS; i++)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
(void)storage_erase_key_async(NVS_NAMESPACE, key);
}
}
// opcional: forçar commit “já”
(void)storage_flush_async();
ESP_LOGD(TAG, "Tags saved to storage (%d tags)", tag_count);
}
/* =========================
* Storage Persistence (mode)
* ========================= */
static void load_mode_from_storage(void)
{
uint8_t u = (uint8_t)AUTH_MODE_OPEN;
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_MODE_KEY, &u, STORAGE_TO);
if (err == ESP_OK)
{
if (u <= (uint8_t)AUTH_MODE_OCPP_RFID)
s_mode = (auth_mode_t)u;
else
s_mode = AUTH_MODE_OPEN;
}
else if (err == ESP_ERR_NOT_FOUND)
{
s_mode = AUTH_MODE_OPEN;
ESP_LOGD(TAG, "No stored mode -> default OPEN");
}
else
{
s_mode = AUTH_MODE_OPEN;
ESP_LOGW(TAG, "Failed to read mode (%s) -> default OPEN", esp_err_to_name(err));
}
ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode));
}
static void save_mode_to_storage(auth_mode_t mode)
{
(void)storage_set_u8_async(NVS_NAMESPACE, NVS_MODE_KEY, (uint8_t)mode);
(void)storage_flush_async(); // opcional: commit mais rápido
ESP_LOGD(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode));
}
/* =========================
* Bridge: EVSE-Link -> AUTH (remote AUTH_GRANTED no slave)
* ========================= */
static void on_remote_auth_grant(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_LINK_EVENTS || id != LINK_EVENT_REMOTE_AUTH_GRANTED || data == NULL)
{
return;
}
const evse_link_auth_grant_event_t *src = (const evse_link_auth_grant_event_t *)data;
// Só faz sentido em SLAVE; em MASTER este evento não deve aparecer
if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE)
{
return;
}
auth_tag_event_data_t ev = {0};
strncpy(ev.tag, src->tag, AUTH_TAG_MAX_LEN - 1);
ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ev.authorized = true;
ESP_LOGI(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag);
ESP_LOGD(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag);
esp_err_t err = esp_event_post(
AUTH_EVENTS,
@@ -241,8 +216,11 @@ static void on_remote_auth_grant(void *arg, esp_event_base_t base, int32_t id, v
* ========================= */
void auth_init(void)
{
load_mode_from_nvs();
load_tags_from_nvs();
// garantir que o storage service está pronto
ESP_ERROR_CHECK(storage_service_init());
load_mode_from_storage();
load_tags_from_storage();
bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID);
if (need_wiegand)
@@ -256,7 +234,7 @@ void auth_init(void)
ESP_LOGI(TAG, "Mode OPEN: Wiegand not started");
}
// Registar bridge para autorizações remotas vindas do EVSE-Link
// bridge EVSE-Link -> AUTH
{
esp_err_t err = esp_event_handler_register(
EVSE_LINK_EVENTS,
@@ -285,44 +263,34 @@ void auth_set_mode(auth_mode_t mode)
if (mode == s_mode)
{
ESP_LOGI(TAG, "Mode unchanged: %s", auth_mode_to_str(mode));
ESP_LOGD(TAG, "Mode unchanged: %s", auth_mode_to_str(mode));
return;
}
auth_mode_t old = s_mode;
s_mode = mode;
save_mode_to_nvs(mode);
save_mode_to_storage(mode);
bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID);
if (need_wiegand && !s_wiegand_started)
{
ESP_LOGI(TAG, "Mode changed %s -> %s, starting Wiegand",
ESP_LOGD(TAG, "Mode changed %s -> %s, starting Wiegand",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
initWiegand();
s_wiegand_started = true;
}
else if (!need_wiegand && s_wiegand_started)
{
// Aqui poderias implementar um wiegand_deinit() se o driver o expuser.
ESP_LOGI(TAG, "Mode changed %s -> %s, Wiegand remains started (no deinit implemented)",
ESP_LOGD(TAG, "Mode changed %s -> %s, Wiegand remains started (no deinit implemented)",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
}
else
{
ESP_LOGI(TAG, "Mode changed %s -> %s, no change in Wiegand state",
ESP_LOGD(TAG, "Mode changed %s -> %s, no change in Wiegand state",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
}
if (s_mode == AUTH_MODE_OPEN)
{
ESP_LOGI(TAG, "Mode set to OPEN");
}
else
{
ESP_LOGI(TAG, "Mode set to %s", auth_mode_to_str(s_mode));
}
auth_mode_event_data_t evt = {.mode = s_mode};
esp_event_post(AUTH_EVENTS, AUTH_EVENT_MODE_CHANGED, &evt, sizeof(evt), portMAX_DELAY);
}
@@ -338,15 +306,16 @@ bool auth_add_tag(const char *tag)
return false;
if (tag_count >= MAX_TAGS)
return false;
if (is_tag_valid(tag))
return true; // já existe
return true;
strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++;
save_tags_to_nvs();
ESP_LOGI(TAG, "Tag added: %s", tag);
save_tags_to_storage();
ESP_LOGD(TAG, "Tag added: %s", tag);
return true;
}
@@ -366,8 +335,8 @@ bool auth_remove_tag(const char *tag)
}
tag_count--;
save_tags_to_nvs();
ESP_LOGI(TAG, "Tag removed: %s", tag);
save_tags_to_storage();
ESP_LOGD(TAG, "Tag removed: %s", tag);
return true;
}
}
@@ -383,11 +352,9 @@ bool auth_tag_exists(const char *tag)
void auth_list_tags(void)
{
ESP_LOGI(TAG, "Registered Tags (%d):", tag_count);
ESP_LOGD(TAG, "Registered Tags (%d):", tag_count);
for (int i = 0; i < tag_count; i++)
{
ESP_LOGI(TAG, "- %s", valid_tags[i]);
}
ESP_LOGD(TAG, "- %s", valid_tags[i]);
}
void auth_wait_for_tag_registration(void)
@@ -398,7 +365,7 @@ void auth_wait_for_tag_registration(void)
return;
}
waiting_for_registration = true;
ESP_LOGI(TAG, "Tag registration mode enabled.");
ESP_LOGD(TAG, "Tag registration mode enabled.");
}
void auth_process_tag(const char *tag)
@@ -412,11 +379,8 @@ void auth_process_tag(const char *tag)
switch (s_mode)
{
case AUTH_MODE_OPEN:
{
// Sem verificação; normalmente nem é necessário evento.
ESP_LOGI(TAG, "Mode OPEN: tag=%s (no verification)", tag);
ESP_LOGD(TAG, "Mode OPEN: tag=%s (no verification)", tag);
break;
}
case AUTH_MODE_LOCAL_RFID:
{
@@ -430,9 +394,10 @@ void auth_process_tag(const char *tag)
strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1);
ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ev.authorized = true;
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED,
&ev, sizeof(ev), portMAX_DELAY);
ESP_LOGI(TAG, "Tag registered: %s", tag);
ESP_LOGD(TAG, "Tag registered: %s", tag);
}
else
{
@@ -446,8 +411,9 @@ void auth_process_tag(const char *tag)
ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ev.authorized = is_tag_valid(tag);
ESP_LOGI(TAG, "LOCAL tag %s: %s", tag,
ESP_LOGD(TAG, "LOCAL tag %s: %s", tag,
ev.authorized ? "AUTHORIZED" : "DENIED");
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED,
&ev, sizeof(ev), portMAX_DELAY);
break;
@@ -455,13 +421,14 @@ void auth_process_tag(const char *tag)
case AUTH_MODE_OCPP_RFID:
{
// Não decide localmente. Pede validação ao OCPP.
auth_tag_verify_event_t rq = {0};
strncpy(rq.tag, tag, AUTH_TAG_MAX_LEN - 1);
rq.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
rq.req_id = s_next_req_id++;
ESP_LOGI(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)",
ESP_LOGD(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)",
rq.tag, (unsigned)rq.req_id);
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY,
&rq, sizeof(rq), portMAX_DELAY);
break;

0
components/auth/src/auth_types.c Executable file → Normal file
View File

0
components/auth/src/wiegand.c Executable file → Normal file
View File

5
components/auth/src/wiegand_reader.c Executable file → Normal file
View File

@@ -276,10 +276,7 @@ static void wiegand_sim_task(void *arg)
{
// lista fixa de idTags simuladas
static const char *idtaglist[] = {
"00000041349",
"W2602312345",
"W34ABCDE123",
};
"127239144069"};
const size_t list_size = sizeof(idtaglist) / sizeof(idtaglist[0]);
for (;;)

2
components/buzzer/CMakeLists.txt Executable file → Normal file
View File

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

0
components/buzzer/idf_component.yml Executable file → Normal file
View File

10
components/buzzer/include/buzzer.h Executable file → Normal file
View File

@@ -3,8 +3,7 @@
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "buzzer_events.h" // para buzzer_pattern_id_t
#include "buzzer_events.h" // buzzer_pattern_id_t
#ifdef __cplusplus
extern "C" {
@@ -33,7 +32,7 @@ void buzzer_stop(void);
/**
* @brief Ativa/Desativa globalmente o buzzer (mute).
* Quando desativado, não toca e garante nível/desligado.
* Quando desativado, não toca e garante desligado.
*/
void buzzer_set_enabled(bool enabled);
@@ -52,9 +51,10 @@ void buzzer_set_quiet_hours(bool enabled, uint16_t start_min, uint16_t end_min);
/**
* @brief Ajusta a frequência PWM (somente PASSIVE/LEDC; timer exclusivo recomendado).
* @return ESP_OK em sucesso; erro se modo inválido ou argumento fora do range.
* @param hz Frequência desejada.
* @return Frequência efetiva configurada (Hz). 0 em erro (modo inválido ou argumento fora do range).
*/
esp_err_t buzzer_set_frequency(uint32_t hz);
uint32_t buzzer_set_frequency(uint32_t hz);
/**
* @brief Define o duty cycle em porcentagem (0..100) para PASSIVE/LEDC.

301
components/buzzer/src/buzzer.c Executable file → Normal file
View File

@@ -1,5 +1,6 @@
// buzzer.c
#include "buzzer_events.h"
#include "buzzer.h"
#include "evse_events.h"
#include "auth_events.h"
#include "network_events.h"
@@ -11,27 +12,26 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include <time.h>
#include <string.h>
#include <stddef.h>
// ===================== Configuração padrão =====================
#define CONFIG_BUZZER_GPIO GPIO_NUM_27
// 1 = PASSIVE (PWM), 0 = ACTIVE (on/off)
#define CONFIG_BUZZER_MODE_PASSIVE 0
#define CONFIG_BUZZER_MODE_PASSIVE 1
#define CONFIG_BUZZER_FREQ_HZ 3500
#define CONFIG_BUZZER_DUTY_PCT 70
#define CONFIG_BUZZER_FREQ_HZ 2400
#define CONFIG_BUZZER_DUTY_PCT 40
#define CONFIG_BUZZER_QUEUE_LEN 8
#define CONFIG_BUZZER_TASK_STACK 2048
#define CONFIG_BUZZER_TASK_PRIO (tskIDLE_PRIORITY + 1)
// anti-spam (gap mínimo entre toques)
@@ -41,7 +41,6 @@
// quiet hours start (minutos desde 00:00)
#define CONFIG_BUZZER_QUIET_START_MIN (22 * 60)
// quiet hours end (minutos desde 00:00)
#define CONFIG_BUZZER_QUIET_END_MIN (7 * 60)
@@ -113,6 +112,31 @@ static TaskHandle_t s_buzzer_task = NULL;
static TickType_t s_last_play_tick = 0;
static const TickType_t s_min_gap_ticks = pdMS_TO_TICKS(CONFIG_BUZZER_MIN_GAP_MS);
// STOP / preempt (robusto em dual-core)
static EventGroupHandle_t s_buzzer_eg = NULL;
#define BUZZER_EG_STOP_BIT (1u << 0)
static bool s_inited = false;
static inline bool buzzer_should_stop(void)
{
if (!s_buzzer_eg)
return false;
return (xEventGroupGetBits(s_buzzer_eg) & BUZZER_EG_STOP_BIT) != 0;
}
static inline void buzzer_clear_stop(void)
{
if (s_buzzer_eg)
xEventGroupClearBits(s_buzzer_eg, BUZZER_EG_STOP_BIT);
}
static inline void buzzer_set_stop(void)
{
if (s_buzzer_eg)
xEventGroupSetBits(s_buzzer_eg, BUZZER_EG_STOP_BIT);
}
// ===================== Padrões (afinados) =====================
// NOTA: os enums BUZZER_PATTERN_* vêm de buzzer_events.h
@@ -139,7 +163,8 @@ static const buzzer_step_t pattern_fault[] = {
{450, 0} // bloco 2
};
static const buzzer_pattern_t buzzer_patterns[] = {
// BUG importante corrigido: tamanho explícito = BUZZER_PATTERN_MAX
static const buzzer_pattern_t buzzer_patterns[BUZZER_PATTERN_MAX] = {
[BUZZER_PATTERN_PLUGGED] = {pattern_plugged, sizeof(pattern_plugged) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_UNPLUGGED] = {pattern_unplugged, sizeof(pattern_unplugged) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_CHARGING] = {pattern_charging, sizeof(pattern_charging) / sizeof(buzzer_step_t)},
@@ -161,38 +186,46 @@ static inline uint32_t duty_from_percent(uint8_t pct, ledc_timer_bit_t res)
return (uint32_t)((pct * max) / 100U);
}
static inline void buzzer_set_duty_raw(uint32_t duty)
{
ledc_set_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel, duty);
ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel);
}
static inline void buzzer_set_duty_pct(uint8_t pct)
{
buzzer_set_duty_raw(duty_from_percent(pct, s_buzzer_cfg.duty_resolution));
}
// Fade async (NO_WAIT) para permitir STOP responsivo (checamos por nós)
static inline void buzzer_fade_to_pct_async(uint8_t pct, uint32_t time_ms)
{
uint32_t duty = duty_from_percent(pct, s_buzzer_cfg.duty_resolution);
ledc_set_fade_with_time(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel, duty, time_ms);
ledc_fade_start(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel, LEDC_FADE_NO_WAIT);
}
static inline bool is_pattern_valid(buzzer_pattern_id_t id)
{
return (id > BUZZER_PATTERN_NONE && id < BUZZER_PATTERN_MAX);
}
static void buzzer_on(void)
static inline bool is_critical_pattern(buzzer_pattern_id_t id)
{
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
// Não mudar freq a cada beep; já foi configurada na init.
esp_err_t err1 = ledc_set_duty(s_buzzer_cfg.ledc_speed_mode,
s_buzzer_cfg.ledc_channel,
duty_from_percent(s_buzzer_cfg.duty_percent, s_buzzer_cfg.duty_resolution));
if (err1 != ESP_OK)
ESP_LOGW(TAG, "ledc_set_duty err=%s", esp_err_to_name(err1));
esp_err_t err2 = ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel);
if (err2 != ESP_OK)
ESP_LOGW(TAG, "ledc_update_duty err=%s", esp_err_to_name(err2));
}
else
{
gpio_set_level(s_buzzer_cfg.gpio, 1);
}
return (id == BUZZER_PATTERN_FAULT || id == BUZZER_PATTERN_CARD_DENIED);
}
static void buzzer_off(void)
{
if (!s_inited)
return;
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
esp_err_t err1 = ledc_set_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel, 0);
if (err1 != ESP_OK)
ESP_LOGW(TAG, "ledc_set_duty(0) err=%s", esp_err_to_name(err1));
esp_err_t err2 = ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel);
if (err2 != ESP_OK)
ESP_LOGW(TAG, "ledc_update_duty err=%s", esp_err_to_name(err2));
@@ -209,6 +242,11 @@ static bool in_quiet_hours(void)
return false;
time_t now = time(NULL);
// Se ainda não há tempo real (NTP não acertou), não aplicar quiet hours
if (now < 1700000000) // ~2023-11
return false;
struct tm lt;
if (localtime_r(&now, &lt) == NULL)
return false;
@@ -219,36 +257,103 @@ static bool in_quiet_hours(void)
if (start == end)
return false; // desativado
if (start < end)
{
return (minutes >= start && minutes < end);
}
else
{ // janela cruza meia-noite
return (minutes >= start || minutes < end);
}
return (minutes >= start || minutes < end); // cruza meia-noite
}
// ===================== Execução do padrão (apenas dentro da task) =====================
static void buzzer_execute(buzzer_pattern_id_t pattern_id)
{
if (!is_pattern_valid(pattern_id))
{
ESP_LOGW(TAG, "Invalid buzzer pattern id: %d", pattern_id);
return;
}
const buzzer_pattern_t *pattern = &buzzer_patterns[pattern_id];
if (!pattern->steps || pattern->length == 0)
return;
// Afinar aqui (para “premium”)
const uint32_t FADE_IN_MS = 10;
const uint32_t FADE_OUT_MS = 20;
// Não limpar STOP aqui — se alguém chamou buzzer_stop(), queremos respeitar já.
for (size_t i = 0; i < pattern->length; i++)
{
buzzer_on();
vTaskDelay(pdMS_TO_TICKS(pattern->steps[i].on_ms));
buzzer_off();
if (pattern->steps[i].off_ms > 0)
if (buzzer_should_stop())
break;
uint32_t on_ms = pattern->steps[i].on_ms;
uint32_t off_ms = pattern->steps[i].off_ms;
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
vTaskDelay(pdMS_TO_TICKS(pattern->steps[i].off_ms));
// Sempre começar em 0 para evitar "click"
buzzer_set_duty_pct(0);
uint32_t fi = (on_ms > (FADE_IN_MS + FADE_OUT_MS)) ? FADE_IN_MS : (on_ms / 3);
uint32_t fo = (on_ms > (FADE_IN_MS + FADE_OUT_MS)) ? FADE_OUT_MS : (on_ms / 3);
uint32_t sustain = (on_ms > (fi + fo)) ? (on_ms - fi - fo) : 0;
// Fade in (async) + espera em passos para STOP rápido
if (fi > 0)
buzzer_fade_to_pct_async(s_buzzer_cfg.duty_percent, fi);
for (uint32_t t = 0; t < fi; t += 10)
{
if (buzzer_should_stop())
break;
vTaskDelay(pdMS_TO_TICKS(10));
}
// Sustain
for (uint32_t t = 0; t < sustain; t += 10)
{
if (buzzer_should_stop())
break;
vTaskDelay(pdMS_TO_TICKS(10));
}
if (buzzer_should_stop())
break;
// Fade out (async) + espera em passos
if (fo > 0)
buzzer_fade_to_pct_async(0, fo);
for (uint32_t t = 0; t < fo; t += 10)
{
if (buzzer_should_stop())
break;
vTaskDelay(pdMS_TO_TICKS(10));
}
buzzer_set_duty_pct(0);
}
else
{
// ACTIVE (liga/desliga simples)
gpio_set_level(s_buzzer_cfg.gpio, 1);
for (uint32_t t = 0; t < on_ms; t += 10)
{
if (buzzer_should_stop())
break;
vTaskDelay(pdMS_TO_TICKS(10));
}
gpio_set_level(s_buzzer_cfg.gpio, 0);
}
if (buzzer_should_stop())
break;
for (uint32_t t = 0; t < off_ms; t += 10)
{
if (buzzer_should_stop())
break;
vTaskDelay(pdMS_TO_TICKS(10));
}
}
buzzer_off();
}
// ===================== Task & Fila =====================
@@ -260,30 +365,31 @@ static void buzzer_task(void *arg)
{
if (xQueueReceive(s_buzzer_q, &id, portMAX_DELAY) == pdTRUE)
{
ESP_LOGD(TAG, "dequeue pattern %d", id); // na buzzer_task()
ESP_LOGD(TAG, "dequeue pattern %d", id);
if (!s_buzzer_ctl.enabled)
continue;
// Quiet hours: permitir apenas alertas críticos/negativos
if (in_quiet_hours())
{
if (!(id == BUZZER_PATTERN_FAULT || id == BUZZER_PATTERN_CARD_DENIED))
{
if (in_quiet_hours() && !is_critical_pattern(id))
continue;
}
}
// Anti-spam global
// Anti-spam global (NÃO aplicar a padrões críticos)
if (!is_critical_pattern(id))
{
TickType_t now = xTaskGetTickCount();
if ((now - s_last_play_tick) < s_min_gap_ticks)
{
continue;
}
s_last_play_tick = now;
}
// Se veio STOP pendente, limpamos antes de começar um novo padrão
buzzer_clear_stop();
buzzer_execute(id);
// Se alguém pediu stop durante execução, já paramos; limpamos o bit.
buzzer_clear_stop();
}
}
}
@@ -291,17 +397,32 @@ static void buzzer_task(void *arg)
// ===================== API pública =====================
void buzzer_play(buzzer_pattern_id_t id)
{
ESP_LOGD(TAG, "enqueue pattern %d", id);
ESP_LOGD(TAG, "enqueue pattern %d", id); // dentro de buzzer_play()
if (!is_pattern_valid(id) || s_buzzer_q == NULL)
if (!s_inited || !is_pattern_valid(id) || s_buzzer_q == NULL)
return;
(void)xQueueSend(s_buzzer_q, &id, 0);
// Críticos: preempt + vão para a frente
if (is_critical_pattern(id))
{
buzzer_stop();
if (xQueueSendToFront(s_buzzer_q, &id, pdMS_TO_TICKS(10)) != pdTRUE)
ESP_LOGW(TAG, "buzzer queue full, dropped critical pattern=%d", id);
return;
}
// Não-críticos: pequeno timeout + log se drop
if (xQueueSend(s_buzzer_q, &id, pdMS_TO_TICKS(10)) != pdTRUE)
ESP_LOGW(TAG, "buzzer queue full, dropped pattern=%d", id);
}
void buzzer_stop(void)
{
// Interrompe imediatamente: esvazia fila e desliga
if (!s_inited)
return;
// Interrompe imediatamente: sinaliza stop, esvazia fila e desliga
buzzer_set_stop();
if (s_buzzer_q)
xQueueReset(s_buzzer_q);
buzzer_off();
@@ -328,15 +449,22 @@ void buzzer_set_quiet_hours(bool enabled, uint16_t start_min, uint16_t end_min)
s_buzzer_ctl.quiet_end_min = end_min;
}
// Opcional: mudar tom dinamicamente (apenas se o timer for exclusivo do buzzer)
esp_err_t buzzer_set_frequency(uint32_t hz)
// ledc_set_freq normalmente devolve a frequência efetiva (0 se falhar)
uint32_t buzzer_set_frequency(uint32_t hz)
{
if (!s_inited)
return 0;
if (s_buzzer_cfg.mode != BUZZER_MODE_PASSIVE)
return ESP_ERR_INVALID_STATE;
return 0;
if (hz < 50 || hz > 20000)
return ESP_ERR_INVALID_ARG;
return 0;
s_buzzer_cfg.freq_hz = hz;
return ledc_set_freq(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_timer, hz);
uint32_t eff = ledc_set_freq(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_timer, hz);
if (eff == 0)
ESP_LOGW(TAG, "ledc_set_freq failed (hz=%lu)", (unsigned long)hz);
return eff;
}
void buzzer_set_duty_percent(uint8_t pct)
@@ -382,7 +510,6 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
buzzer_evt.pattern = BUZZER_PATTERN_CHARGING;
break;
case EVSE_STATE_EVENT_FAULT:
// Preempta qualquer beep em andamento
buzzer_stop();
buzzer_evt.pattern = BUZZER_PATTERN_FAULT;
break;
@@ -398,7 +525,7 @@ static void network_event_handler(void *handler_args, esp_event_base_t base, int
if (base != NETWORK_EVENTS)
return;
ESP_LOGI(TAG, "Network event id=%d", (int)id);
ESP_LOGD(TAG, "Network event id=%d", (int)id);
buzzer_event_data_t evt = {0};
@@ -406,12 +533,9 @@ static void network_event_handler(void *handler_args, esp_event_base_t base, int
{
case NETWORK_EVENT_AP_STARTED:
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;
}
@@ -446,6 +570,12 @@ static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
// ===================== Inicialização / Deinit =====================
void buzzer_init(void)
{
if (s_inited)
{
ESP_LOGW(TAG, "buzzer_init called twice");
return;
}
// Config HW
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
@@ -466,6 +596,11 @@ void buzzer_init(void)
.duty = 0,
.hpoint = 0};
ESP_ERROR_CHECK(ledc_channel_config(&ccfg));
// Evita crash se já estiver instalado (init repetido em app maior)
esp_err_t fe = ledc_fade_func_install(0);
if (fe != ESP_OK && fe != ESP_ERR_INVALID_STATE)
ESP_ERROR_CHECK(fe);
}
else
{
@@ -479,14 +614,14 @@ void buzzer_init(void)
gpio_set_level(s_buzzer_cfg.gpio, 0);
}
buzzer_set_quiet_hours(false, 0, 0);
buzzer_set_enabled(true);
buzzer_off();
// STOP event group
s_buzzer_eg = xEventGroupCreate();
configASSERT(s_buzzer_eg);
// Fila + Task
s_buzzer_q = xQueueCreate(CONFIG_BUZZER_QUEUE_LEN, sizeof(buzzer_pattern_id_t));
configASSERT(s_buzzer_q != NULL);
BaseType_t ok = xTaskCreatePinnedToCore(
buzzer_task,
"buzzer_task",
@@ -497,6 +632,13 @@ void buzzer_init(void)
tskNO_AFFINITY);
configASSERT(ok == pdPASS);
// Runtime defaults
buzzer_set_quiet_hours(false, 0, 0);
buzzer_set_enabled(true);
s_inited = true;
buzzer_off();
// Handlers
ESP_ERROR_CHECK(esp_event_handler_register(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, buzzer_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler, NULL));
@@ -514,24 +656,39 @@ void buzzer_init(void)
void buzzer_deinit(void)
{
if (!s_inited)
return;
(void)esp_event_handler_unregister(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, buzzer_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_SAVED, auth_event_handler);
(void)esp_event_handler_unregister(NETWORK_EVENTS, ESP_EVENT_ANY_ID, network_event_handler);
// Para já
buzzer_stop();
// Mata task ANTES de apagar a queue (evita uso-after-free raro)
if (s_buzzer_task)
{
vTaskDelete(s_buzzer_task);
s_buzzer_task = NULL;
}
if (s_buzzer_q)
{
vQueueDelete(s_buzzer_q);
s_buzzer_q = NULL;
}
if (s_buzzer_task)
if (s_buzzer_eg)
{
vTaskDelete(s_buzzer_task);
s_buzzer_task = NULL;
vEventGroupDelete(s_buzzer_eg);
s_buzzer_eg = NULL;
}
s_inited = false;
buzzer_off();
ESP_LOGI(TAG, "Buzzer deinitialized");
}

2
components/config/CMakeLists.txt Executable file → Normal file
View File

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

25
components/config/board_config.c Executable file → Normal file
View File

@@ -1,4 +1,5 @@
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include "esp_system.h"
#include "esp_log.h"
@@ -12,7 +13,27 @@ board_config_t board_config;
bool atob(const char *value)
{
return value[0] == 'y';
return value[0] == 'y' || value[0] == 'Y' || value[0] == '1';
}
static board_config_pilot_adc_source_t pilot_adc_source_from_string(const char *value)
{
if (value == NULL)
{
return BOARD_CONFIG_PILOT_ADC_EXTERNAL_ADC121;
}
if (!strcasecmp(value, "internal") ||
!strcasecmp(value, "esp32") ||
!strcasecmp(value, "adc_internal") ||
!strcasecmp(value, "internal_esp32") ||
!strcasecmp(value, "1") ||
atob(value))
{
return BOARD_CONFIG_PILOT_ADC_INTERNAL_ESP32;
}
return BOARD_CONFIG_PILOT_ADC_EXTERNAL_ADC121;
}
#define SET_CONFIG_VALUE(name, prop, convert_fn) \
@@ -78,6 +99,8 @@ void board_config_load()
SET_CONFIG_VALUE("BUZZER_GPIO", buzzer_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_ADC_SOURCE", pilot_adc_source, pilot_adc_source_from_string);
SET_CONFIG_VALUE("PILOT_ADC_INTERNAL", pilot_adc_source, pilot_adc_source_from_string);
SET_CONFIG_VALUE("PILOT_ADC_CHANNEL", pilot_adc_channel, atoi);
SET_CONFIG_VALUE("PILOT_DOWN_THRESHOLD_12", pilot_down_threshold_12, atoi);
SET_CONFIG_VALUE("PILOT_DOWN_THRESHOLD_9", pilot_down_threshold_9, atoi);

10
components/config/include/board_config.h Executable file → Normal file
View File

@@ -1,6 +1,9 @@
#ifndef BOARD_CONFIG_H_
#define BOARD_CONFIG_H_
#include <stdbool.h>
#include <stdint.h>
#include "hal/adc_types.h"
#include "hal/gpio_types.h"
#include "soc/soc_caps.h"
@@ -19,6 +22,12 @@ typedef enum
BOARD_CONFIG_SERIAL_RS485
} board_config_serial_t;
typedef enum
{
BOARD_CONFIG_PILOT_ADC_EXTERNAL_ADC121 = 0,
BOARD_CONFIG_PILOT_ADC_INTERNAL_ESP32 = 1
} board_config_pilot_adc_source_t;
typedef struct
{
char device_name[32];
@@ -36,6 +45,7 @@ typedef struct
gpio_num_t button_wifi_gpio;
gpio_num_t pilot_pwm_gpio;
board_config_pilot_adc_source_t pilot_adc_source;
adc_channel_t pilot_adc_channel;
uint16_t pilot_down_threshold_12;
uint16_t pilot_down_threshold_9;

4
components/evse/CMakeLists.txt Executable file → Normal file
View File

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

0
components/evse/evse_api.c Executable file → Normal file
View File

484
components/evse/evse_config.c Executable file → Normal file
View File

@@ -1,215 +1,289 @@
#include <inttypes.h> // For PRI macros
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "evse_config.h"
#include "board_config.h"
#include "evse_limits.h"
#include "evse_api.h"
#include "evse_state.h"
#include "esp_log.h"
#include "nvs.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "esp_check.h"
#include "storage_service.h"
static const char *TAG = "evse_config";
static nvs_handle_t nvs;
#define NVS_NAMESPACE "evse_config"
// ========================
// Configurable parameters
// 3 variáveis (semântica simples)
// ========================
static uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
static uint16_t charging_current; // Persisted (NVS)
static uint16_t charging_current_runtime = 0; // Runtime only
static bool socket_outlet;
static bool rcm;
// 1) Hardware (FIXO)
static const uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
// 2) Configurável (persistido)
static uint16_t charging_current = MAX_CHARGING_CURRENT_LIMIT;
// 3) Runtime (RAM)
static uint16_t charging_current_runtime = 0;
// Outros parâmetros (persistidos)
static bool socket_outlet = false;
static bool rcm = false;
static uint8_t temp_threshold = 60;
static bool require_auth;
// Availability / Enable flags
// Availability / Enable flags (persistidos)
static bool is_available = true;
static bool is_enabled = true;
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
// Ajusta conforme o teu boot:
// 1000ms pode ser curto com Wi-Fi/FS/tasks; 2000ms é mais robusto em produto.
static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(2000); }
// ========================
// Initialization
// ========================
esp_err_t evse_config_init(void)
{
ESP_LOGD(TAG, "Initializing NVS configuration...");
return nvs_open("evse", NVS_READWRITE, &nvs);
// garante storage iniciado
ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed");
ESP_LOGI(TAG, "EVSE config init OK (storage-backed)");
return ESP_OK;
}
void evse_check_defaults(void)
{
esp_err_t err;
uint8_t u8;
uint16_t u16;
uint32_t u32;
bool needs_commit = false;
uint8_t u8_bool;
uint8_t u8 = 0;
uint16_t u16 = 0;
ESP_LOGD(TAG, "Checking default parameters...");
// Timeouts: leitura e escrita no boot
const TickType_t rd_to = BOOT_TO();
const TickType_t wr_to = TO_TICKS_MS(2000);
// Max charging current
err = nvs_get_u8(nvs, "max_chrg_curr", &u8);
if (err != ESP_OK || u8 < MIN_CHARGING_CURRENT_LIMIT || u8 > MAX_CHARGING_CURRENT_LIMIT)
{
max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
nvs_set_u8(nvs, "max_chrg_curr", max_charging_current);
needs_commit = true;
ESP_LOGW(TAG, "Invalid or missing max_chrg_curr, resetting to %d", max_charging_current);
}
else
{
max_charging_current = u8;
}
ESP_LOGD(TAG, "Checking default parameters (sync persistence)...");
// -----------------------------------------
// Charging current (default, persisted)
err = nvs_get_u16(nvs, "def_chrg_curr", &u16);
if (err != ESP_OK || u16 < (MIN_CHARGING_CURRENT_LIMIT) || u16 > (max_charging_current))
// -----------------------------------------
err = storage_get_u16_sync(NVS_NAMESPACE, "def_chrg_curr", &u16, rd_to);
if (err != ESP_OK || u16 < MIN_CHARGING_CURRENT_LIMIT || u16 > max_charging_current)
{
charging_current = max_charging_current;
nvs_set_u16(nvs, "def_chrg_curr", charging_current);
needs_commit = true;
ESP_LOGW(TAG, "Invalid or missing def_chrg_curr, resetting to %d", charging_current);
esp_err_t se = storage_set_u16_sync(NVS_NAMESPACE, "def_chrg_curr", charging_current, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist def_chrg_curr=%u: %s",
(unsigned)charging_current, esp_err_to_name(se));
// seguimos com RAM correta; persist pode falhar por flash/partição
}
ESP_LOGW(TAG, "Invalid/missing def_chrg_curr (%s) -> reset to %u (sync persisted)",
esp_err_to_name(err), (unsigned)charging_current);
}
else
{
charging_current = u16;
}
// Runtime charging current inicializado a partir do default persistido
// runtime inicializa a partir do default
charging_current_runtime = charging_current;
ESP_LOGD(TAG, "Runtime charging current initialized to: %d", charging_current_runtime);
ESP_LOGD(TAG, "Runtime charging current initialized from default: %u",
(unsigned)charging_current_runtime);
// Auth required
err = nvs_get_u8(nvs, "require_auth", &u8);
require_auth = (err == ESP_OK && u8 <= 1) ? u8 : false;
if (err != ESP_OK)
// -----------------------------------------
// Socket outlet (persisted) + capability gate
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "socket_outlet", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
nvs_set_u8(nvs, "require_auth", require_auth);
needs_commit = true;
bool wanted = (u8 != 0);
if (wanted && !board_config.proximity)
{
// 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));
}
// Socket outlet
err = nvs_get_u8(nvs, "socket_outlet", &u8);
socket_outlet = (err == ESP_OK && u8) && board_config.proximity;
if (err != ESP_OK)
{
nvs_set_u8(nvs, "socket_outlet", socket_outlet);
needs_commit = true;
}
// RCM
err = nvs_get_u8(nvs, "rcm", &u8);
rcm = (err == ESP_OK && u8) && board_config.rcm;
if (err != ESP_OK)
{
nvs_set_u8(nvs, "rcm", rcm);
needs_commit = true;
}
// Temp threshold
err = nvs_get_u8(nvs, "temp_threshold", &u8);
temp_threshold = (err == ESP_OK && u8 >= 40 && u8 <= 80) ? u8 : 60;
if (err != ESP_OK)
{
nvs_set_u8(nvs, "temp_threshold", temp_threshold);
needs_commit = true;
}
// Optional limits
if (nvs_get_u32(nvs, "def_cons_lim", &u32) == ESP_OK)
evse_set_consumption_limit(u32);
if (nvs_get_u32(nvs, "def_ch_time_lim", &u32) == ESP_OK)
evse_set_charging_time_limit(u32);
if (nvs_get_u16(nvs, "def_un_pwr_lim", &u16) == ESP_OK)
evse_set_under_power_limit(u16);
// Availability (persist)
if (nvs_get_u8(nvs, "available", &u8_bool) == ESP_OK && u8_bool <= 1)
{
is_available = (u8_bool != 0);
ESP_LOGW(TAG, "socket_outlet requested but HW has no proximity -> forcing false (sync persisted)");
}
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).");
socket_outlet = wanted;
}
// 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).");
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));
}
if (needs_commit)
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)
{
err = nvs_commit(nvs);
if (err == ESP_OK)
bool wanted = (u8 != 0);
if (wanted && !board_config.rcm)
{
ESP_LOGD(TAG, "Configuration committed to NVS.");
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
{
ESP_LOGE(TAG, "Failed to commit configuration to NVS: %s", esp_err_to_name(err));
rcm = wanted;
}
}
else
{
rcm = false;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist rcm default=0: %s", esp_err_to_name(se));
}
ESP_LOGW(TAG, "Missing/invalid rcm (%s) -> default=false (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// Temp threshold (persisted)
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "temp_threshold", &u8, rd_to);
if (err == ESP_OK && u8 >= 40 && u8 <= 80)
{
temp_threshold = u8;
}
else
{
temp_threshold = 60;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "temp_threshold", temp_threshold, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist temp_threshold=%u: %s",
(unsigned)temp_threshold, esp_err_to_name(se));
}
ESP_LOGW(TAG, "Invalid/missing temp_threshold (%s) -> default=60 (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// Availability (persisted) [0/1]
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "available", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
is_available = (u8 != 0);
}
else
{
is_available = true;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "available", 1, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist available=1: %s", esp_err_to_name(se));
}
ESP_LOGW(TAG, "Missing/invalid 'available' (%s) -> default=true (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// Enabled (persisted) [0/1]
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "enabled", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
is_enabled = (u8 != 0);
}
else
{
is_enabled = true;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "enabled", 1, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist enabled=1: %s", esp_err_to_name(se));
}
ESP_LOGW(TAG, "Missing/invalid 'enabled' (%s) -> default=true (sync persisted).",
esp_err_to_name(err));
}
// Flush explícito no boot:
// - ajuda a garantir commit determinístico antes do resto do sistema avançar
// - mantém-se útil mesmo com setters sync se o teu storage ainda estiver com debounce interno
esp_err_t fe = storage_flush_sync(wr_to);
if (fe != ESP_OK)
ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe));
}
// ========================
// Charging current getters/setters
// ========================
uint8_t evse_get_max_charging_current(void)
{
return max_charging_current;
}
uint8_t evse_get_max_charging_current(void) { return max_charging_current; }
esp_err_t evse_set_max_charging_current(uint8_t value)
{
if (value < MIN_CHARGING_CURRENT_LIMIT || value > MAX_CHARGING_CURRENT_LIMIT)
return ESP_ERR_INVALID_ARG;
max_charging_current = value;
evse_set_runtime_charging_current(value);
nvs_set_u8(nvs, "max_chrg_curr", value);
return nvs_commit(nvs);
}
uint16_t evse_get_charging_current(void)
{
return charging_current;
}
uint16_t evse_get_charging_current(void) { return charging_current; }
esp_err_t evse_set_charging_current(uint16_t value)
{
if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current))
if (value < MIN_CHARGING_CURRENT_LIMIT || value > max_charging_current)
return ESP_ERR_INVALID_ARG;
if (value == charging_current)
{
evse_set_runtime_charging_current(value);
return ESP_OK;
}
charging_current = value;
nvs_set_u16(nvs, "def_chrg_curr", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_chrg_curr", value);
if (err != ESP_OK)
{
// Em runtime, isto pode falhar por fila cheia. RAM fica correta; persistência é best-effort.
ESP_LOGE(TAG, "Failed to persist def_chrg_curr async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
uint16_t evse_get_default_charging_current(void)
{
uint16_t value;
if (nvs_get_u16(nvs, "def_chrg_curr", &value) == ESP_OK)
return value;
return charging_current;
}
esp_err_t evse_set_default_charging_current(uint16_t value)
{
if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current))
return ESP_ERR_INVALID_ARG;
nvs_set_u16(nvs, "def_chrg_curr", value);
return nvs_commit(nvs);
evse_set_runtime_charging_current(value);
return ESP_OK;
}
// ========================
@@ -218,135 +292,119 @@ esp_err_t evse_set_default_charging_current(uint16_t value)
void evse_set_runtime_charging_current(uint16_t value)
{
if (value > max_charging_current)
{
value = max_charging_current;
}
else if (value < MIN_CHARGING_CURRENT_LIMIT)
{
value = MIN_CHARGING_CURRENT_LIMIT;
}
charging_current_runtime = value;
ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime);
evse_config_event_data_t evt = {
.charging = evse_state_is_charging(evse_get_state()),
.hw_max_current = (float)evse_get_max_charging_current(),
.runtime_current = (float)evse_get_runtime_charging_current(),
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_CONFIG_UPDATED,
&evt,
sizeof(evt),
portMAX_DELAY);
}
uint16_t evse_get_runtime_charging_current(void)
{
return charging_current_runtime;
}
uint16_t evse_get_runtime_charging_current(void) { return charging_current_runtime; }
// ========================
// Socket outlet
// ========================
bool evse_get_socket_outlet(void)
{
return socket_outlet;
}
bool evse_get_socket_outlet(void) { return socket_outlet; }
esp_err_t evse_set_socket_outlet(bool value)
{
if (value && !board_config.proximity)
return ESP_ERR_INVALID_ARG;
if (value == socket_outlet)
return ESP_OK;
socket_outlet = value;
nvs_set_u8(nvs, "socket_outlet", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "socket_outlet", (uint8_t)value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist socket_outlet async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// ========================
// RCM
// ========================
bool evse_is_rcm(void)
{
return rcm;
}
bool evse_is_rcm(void) { return rcm; }
esp_err_t evse_set_rcm(bool value)
{
if (value && !board_config.rcm)
return ESP_ERR_INVALID_ARG;
if (value == rcm)
return ESP_OK;
rcm = value;
nvs_set_u8(nvs, "rcm", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "rcm", (uint8_t)value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist rcm async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// ========================
// Temperature
// ========================
uint8_t evse_get_temp_threshold(void)
{
return temp_threshold;
}
uint8_t evse_get_temp_threshold(void) { return temp_threshold; }
esp_err_t evse_set_temp_threshold(uint8_t value)
{
if (value < 40 || value > 80)
return ESP_ERR_INVALID_ARG;
if (value == temp_threshold)
return ESP_OK;
temp_threshold = value;
nvs_set_u8(nvs, "temp_threshold", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "temp_threshold", value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist temp_threshold async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// ========================
// Availability
// ========================
bool evse_config_is_available(void)
{
return is_available;
}
bool evse_config_is_available(void) { return is_available; }
void evse_config_set_available(bool available)
{
is_available = available ? true : false;
bool newv = available;
if (newv == is_available)
return;
esp_err_t err = nvs_set_u8(nvs, "available", (uint8_t)is_available);
if (err == ESP_OK)
err = nvs_commit(nvs);
is_available = newv;
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "available", (uint8_t)is_available);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist 'available': %s", esp_err_to_name(err));
}
evse_available_event_data_t e = {
.available = is_available,
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
ESP_LOGE(TAG, "Failed to persist 'available' async=%u: %s", (unsigned)is_available, esp_err_to_name(err));
}
// ========================
// Enable/Disable
// ========================
bool evse_config_is_enabled(void)
{
return is_enabled;
}
bool evse_config_is_enabled(void) { return is_enabled; }
void evse_config_set_enabled(bool enabled)
{
is_enabled = enabled ? true : false;
bool newv = enabled;
if (newv == is_enabled)
return;
esp_err_t err = nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled);
if (err == ESP_OK)
err = nvs_commit(nvs);
is_enabled = newv;
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "enabled", (uint8_t)is_enabled);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist 'enabled': %s", esp_err_to_name(err));
}
evse_enable_event_data_t e = {
.enabled = is_enabled,
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
ESP_LOGE(TAG, "Failed to persist 'enabled' async=%u: %s", (unsigned)is_enabled, esp_err_to_name(err));
}

53
components/evse/evse_core.c Executable file → Normal file
View File

@@ -1,3 +1,4 @@
// components/evse/evse_core.c
#include "evse_fsm.h"
#include "evse_error.h"
#include "evse_limits.h"
@@ -14,6 +15,36 @@ static const char *TAG = "evse_core";
static SemaphoreHandle_t mutex;
static evse_state_t last_state = EVSE_STATE_A;
// Filtro simples de histerese no pilot
#define PILOT_STABLE_SAMPLES 2
static pilot_voltage_t s_last_raw = PILOT_VOLTAGE_12;
static pilot_voltage_t s_filtered = PILOT_VOLTAGE_12;
static int s_stable_count = 0;
static pilot_voltage_t filter_pilot_voltage(pilot_voltage_t raw)
{
if (raw == s_last_raw)
{
if (s_stable_count < PILOT_STABLE_SAMPLES)
{
s_stable_count++;
}
}
else
{
s_last_raw = raw;
s_stable_count = 1;
}
if (s_stable_count >= PILOT_STABLE_SAMPLES && raw != s_filtered)
{
s_filtered = raw;
}
return s_filtered;
}
static void evse_process(void);
static void evse_core_task(void *arg);
@@ -32,7 +63,8 @@ void evse_init(void)
evse_fsm_reset();
pilot_set_level(true);
xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 5, NULL);
BaseType_t rc = xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 6, NULL);
configASSERT(rc == pdPASS);
}
static void evse_process(void)
@@ -44,19 +76,27 @@ static void evse_process(void)
xSemaphoreTake(mutex, portMAX_DELAY);
pilot_voltage_t pilot_voltage;
pilot_voltage_t pilot_raw;
bool is_n12v = false;
pilot_measure(&pilot_voltage, &is_n12v);
ESP_LOGD(TAG, "Pilot: %d, -12V: %s", pilot_voltage, is_n12v ? "yes" : "no");
pilot_measure(&pilot_raw, &is_n12v);
pilot_voltage_t pilot_voltage = filter_pilot_voltage(pilot_raw);
ESP_LOGD(TAG, "Pilot(raw=%d, filt=%d), -12V: %s",
pilot_raw, pilot_voltage, is_n12v ? "yes" : "no");
// raw set/clear; erro visível mantém holdoff interno (60s após sumir)
evse_error_check(pilot_voltage, is_n12v);
// ✅ Sem cooldown externo: disponibilidade depende só do erro "visível"
bool available = evse_config_is_available() && (evse_get_error() == 0);
bool enabled = evse_config_is_enabled();
evse_fsm_process(
pilot_voltage,
evse_state_get_authorized(),
evse_config_is_available(),
evse_config_is_enabled());
available,
enabled);
evse_limits_check();
@@ -65,6 +105,7 @@ static void evse_process(void)
if (evse_state_get_authorized())
{
ESP_LOGW(TAG, "Charging limit reached → revoking authorization");
evse_session_clear_authorized_tag();
evse_state_set_authorized(false);
}
}

359
components/evse/evse_error.c Executable file → Normal file
View File

@@ -1,5 +1,4 @@
#include "evse_error.h"
#include "evse_config.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -7,151 +6,147 @@
#include "esp_log.h"
#include "ntc_sensor.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "evse_events.h"
#include "evse_config.h"
static const char *TAG = "evse_error";
// Estado global de erros
static uint32_t error_bits = 0;
static TickType_t auto_clear_timeout = 0;
// ----------------------------------------------------
// Estado interno
// ----------------------------------------------------
// raw_bits = erros “instantâneos” conforme checks (set/clear)
// visible_bits = erros expostos ao resto do sistema (com holdoff)
// clear_deadline = quando pode finalmente limpar visible_bits para 0
static uint32_t raw_bits = 0;
static uint32_t visible_bits = 0;
static TickType_t clear_deadline = 0;
// Sticky flag: "todos erros foram limpos"
// Sticky flag: "todos erros visíveis foram limpos"
static bool error_cleared = false;
// Proteção contra concorrência
static portMUX_TYPE error_mux = portMUX_INITIALIZER_UNLOCKED;
void evse_error_init(void)
// ----------------------------------------------------
// Helper: publicar evento de alteração de erro (visible_bits)
// ----------------------------------------------------
static void evse_error_post_event(uint32_t new_bits, uint32_t changed_mask)
{
evse_error_event_data_t ev = {
.error_bits = new_bits,
.changed_mask = changed_mask,
.timestamp_us = esp_timer_get_time(),
};
esp_err_t err = esp_event_post(
EVSE_EVENTS,
EVSE_EVENT_ERROR_CHANGED,
&ev,
sizeof(ev),
portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao publicar EVSE_EVENT_ERROR_CHANGED: %s",
esp_err_to_name(err));
}
}
// ----------------------------------------------------
// Helpers internos
// ----------------------------------------------------
static bool raw_has_bit(uint32_t bit)
{
bool v;
portENTER_CRITICAL(&error_mux);
error_bits = 0;
auto_clear_timeout = 0;
error_cleared = false;
v = ((raw_bits & bit) != 0);
portEXIT_CRITICAL(&error_mux);
return v;
}
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v)
static void reconcile_visible_locked(TickType_t now)
{
ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s",
pilot_voltage, is_n12v ? "true" : "false");
// 1) Falha elétrica geral no pilot
if (pilot_voltage == PILOT_VOLTAGE_1)
{
bool first_time = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_PILOT_FAULT_BIT))
// Se existem erros reais, o visível segue imediatamente
if (raw_bits != 0)
{
visible_bits = raw_bits;
clear_deadline = 0;
error_cleared = false;
error_bits |= EVSE_ERR_PILOT_FAULT_BIT;
first_time = true;
}
portEXIT_CRITICAL(&error_mux);
if (first_time)
{
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)");
}
}
else
{
// Pilot voltou a nível válido → limpa erro de pilot fault
evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT);
}
// 2) Falta de -12V durante PWM (C ou D)
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v)
{
bool first_time = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_DIODE_SHORT_BIT))
{
error_cleared = false;
error_bits |= EVSE_ERR_DIODE_SHORT_BIT;
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000);
first_time = true;
}
portEXIT_CRITICAL(&error_mux);
if (first_time)
{
ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)");
}
}
else
{
// Se já não estamos em C/D sem -12V, limpa o erro de diodo curto
evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT);
}
}
void evse_temperature_check(void)
{
float temp_c = ntc_temp_sensor();
uint8_t threshold = evse_get_temp_threshold();
ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C",
temp_c, threshold);
// Temperatura inválida -> erro de sensor
if (temp_c < -40.0f || temp_c > 150.0f)
{
bool first_time = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT))
{
error_cleared = false;
error_bits |= EVSE_ERR_TEMPERATURE_FAULT_BIT;
first_time = true;
}
portEXIT_CRITICAL(&error_mux);
if (first_time)
{
ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado");
}
return;
}
// Leitura válida -> limpa erro de sensor
evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT);
// Temperatura máxima
if (temp_c >= threshold)
// raw_bits == 0
if (visible_bits == 0)
{
bool first_time = false;
clear_deadline = 0;
return;
}
// Ainda há erro visível (holdoff). Arma deadline 1x.
if (clear_deadline == 0)
{
clear_deadline = now + pdMS_TO_TICKS(EVSE_ERROR_COOLDOWN_MS);
return;
}
// Expirou -> limpar finalmente
if ((int32_t)(now - clear_deadline) >= 0)
{
visible_bits = 0;
clear_deadline = 0;
error_cleared = true;
}
}
// ----------------------------------------------------
// API pública
// ----------------------------------------------------
void evse_error_init(void)
{
uint32_t old_vis, new_vis, changed;
bool post = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT))
{
old_vis = visible_bits;
raw_bits = 0;
visible_bits = 0;
clear_deadline = 0;
error_cleared = false;
error_bits |= EVSE_ERR_TEMPERATURE_HIGH_BIT;
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000);
first_time = true;
}
new_vis = visible_bits;
changed = old_vis ^ new_vis;
post = (changed != 0);
portEXIT_CRITICAL(&error_mux);
if (first_time)
if (post)
{
ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C",
temp_c, threshold);
}
}
else
{
evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT);
evse_error_post_event(new_vis, changed);
}
}
uint32_t evse_get_error(void)
{
portENTER_CRITICAL(&error_mux);
uint32_t val = error_bits;
uint32_t val = visible_bits;
portEXIT_CRITICAL(&error_mux);
return val;
}
bool evse_error_is_active(void)
{
return evse_get_error() != 0;
}
uint32_t evse_error_get_bits(void)
{
return evse_get_error();
}
bool evse_error_cleared_flag(void)
{
portENTER_CRITICAL(&error_mux);
@@ -169,61 +164,147 @@ void evse_error_reset_flag(void)
void evse_error_set(uint32_t bitmask)
{
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
error_cleared = false;
error_bits |= bitmask;
old_vis = visible_bits;
if (bitmask & EVSE_ERR_AUTO_CLEAR_BITS)
{
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); // 60s
}
raw_bits |= bitmask;
// se aparece qualquer erro, o "cleared" deixa de ser verdade
error_cleared = false;
reconcile_visible_locked(now);
new_vis = visible_bits;
changed = old_vis ^ new_vis;
portEXIT_CRITICAL(&error_mux);
if (changed != 0)
{
evse_error_post_event(new_vis, changed);
}
}
void evse_error_clear(uint32_t bitmask)
{
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
bool had_error = (error_bits != 0);
error_bits &= ~bitmask;
old_vis = visible_bits;
if (had_error && error_bits == 0)
{
error_cleared = true;
}
raw_bits &= ~bitmask;
// ✅ Aqui é onde o “60s depois do erro desaparecer” é armado:
// quando raw_bits chega a 0, reconcile arma clear_deadline (uma vez)
reconcile_visible_locked(now);
new_vis = visible_bits;
changed = old_vis ^ new_vis;
portEXIT_CRITICAL(&error_mux);
if (changed != 0)
{
evse_error_post_event(new_vis, changed);
}
}
void evse_error_tick(void)
{
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
if ((error_bits & EVSE_ERR_AUTO_CLEAR_BITS) &&
auto_clear_timeout != 0 &&
xTaskGetTickCount() >= auto_clear_timeout)
{
error_bits &= ~EVSE_ERR_AUTO_CLEAR_BITS;
if (error_bits == 0)
{
error_cleared = true;
}
auto_clear_timeout = 0;
}
old_vis = visible_bits;
reconcile_visible_locked(now);
new_vis = visible_bits;
changed = old_vis ^ new_vis;
portEXIT_CRITICAL(&error_mux);
if (changed != 0)
{
evse_error_post_event(new_vis, changed);
}
}
bool evse_error_is_active(void)
// ----------------------------------------------------
// Checks (raw -> set/clear)
// ----------------------------------------------------
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v)
{
return evse_get_error() != 0;
ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s",
pilot_voltage, is_n12v ? "true" : "false");
// 1) Falha elétrica geral no pilot
if (pilot_voltage == PILOT_VOLTAGE_1)
{
if (!raw_has_bit(EVSE_ERR_PILOT_FAULT_BIT))
{
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)");
}
evse_error_set(EVSE_ERR_PILOT_FAULT_BIT);
}
else
{
evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT);
}
uint32_t evse_error_get_bits(void)
// 2) Falta de -12V durante PWM (C ou D)
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v)
{
return evse_get_error();
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);
}
}

0
components/evse/evse_events.c Executable file → Normal file
View File

119
components/evse/evse_fsm.c Executable file → Normal file
View File

@@ -1,3 +1,4 @@
// components/evse/evse_fsm.c
#include "evse_fsm.h"
#include "evse_api.h"
#include "evse_pilot.h"
@@ -17,16 +18,14 @@ static const char *TAG = "evse_fsm";
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif
static bool c1_d1_waiting = false;
static TickType_t c1_d1_relay_to = 0;
void evse_fsm_reset(void)
{
evse_set_state(EVSE_STATE_A);
c1_d1_waiting = false;
c1_d1_relay_to = 0;
}
/**
* @brief Atualiza saídas de hardware (pilot, relé, trava) em função do estado lógico.
*/
static void update_outputs(evse_state_t state)
{
const uint16_t current = evse_get_runtime_charging_current();
@@ -38,7 +37,7 @@ static void update_outputs(evse_state_t state)
cable_max_current = proximity_get_max_current();
}
// Segurança: relé sempre off e outputs seguros em caso de erro
// Segurança total: qualquer erro ativo força saída segura
if (evse_get_error() != 0)
{
if (ac_relay_get_state())
@@ -46,8 +45,14 @@ static void update_outputs(evse_state_t state)
ac_relay_set_state(false);
ESP_LOGW(TAG, "ERRO ativo: relé estava ligado, agora desligado por segurança!");
}
else
{
ac_relay_set_state(false);
}
// Em erro, garantir pilot OFF (não PWM / não +12V)
pilot_set_level(true);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(false);
@@ -55,14 +60,16 @@ static void update_outputs(evse_state_t state)
return;
}
// Fluxo normal
switch (state)
{
case EVSE_STATE_A:
case EVSE_STATE_E:
case EVSE_STATE_F:
ac_relay_set_state(false);
// A → pilot alto (+12V), E/F → pilot OFF
pilot_set_level(state == EVSE_STATE_A);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(false);
@@ -72,66 +79,77 @@ static void update_outputs(evse_state_t state)
case EVSE_STATE_B1:
pilot_set_level(true);
ac_relay_set_state(false);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
if (rcm_test())
{
// ESP_LOGI(TAG, "RCM self test passed");
}
else
{
// ESP_LOGW(TAG, "RCM self test failed");
}
(void)rcm_test();
break;
case EVSE_STATE_B2:
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(false);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
break;
case EVSE_STATE_C1:
case EVSE_STATE_D1:
{
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(false);
c1_d1_waiting = true;
c1_d1_relay_to = xTaskGetTickCount() + pdMS_TO_TICKS(6000);
break;
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
break;
case EVSE_STATE_C2:
case EVSE_STATE_D2:
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(true);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
break;
}
}
// FSM principal
/**
* @brief Máquina de estados principal do EVSE (IEC 61851).
*/
void evse_fsm_process(
pilot_voltage_t pilot_voltage,
bool authorized,
bool available,
bool enabled)
{
// Proteção total: erro força F sempre!
if (evse_get_error() != 0)
// 1) Erros globais: dominam qualquer outra lógica
uint32_t err_bits = evse_get_error();
if (err_bits != 0)
{
if (evse_get_state() != EVSE_STATE_F)
evse_state_t forced_state =
(err_bits & EVSE_ERR_PILOT_FAULT_BIT) ? EVSE_STATE_E : EVSE_STATE_F;
if (evse_get_state() != forced_state)
{
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado FAULT (F)");
evse_set_state(EVSE_STATE_F);
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado %s",
evse_state_to_str(forced_state));
evse_set_state(forced_state);
}
update_outputs(EVSE_STATE_F);
update_outputs(forced_state);
return;
}
TickType_t now = xTaskGetTickCount();
evse_state_t prev = evse_get_state();
evse_state_t curr = prev;
evse_state_t curr = evse_get_state();
switch (curr)
{
@@ -153,17 +171,25 @@ void evse_fsm_process(
evse_set_state(EVSE_STATE_F);
break;
}
switch (pilot_voltage)
{
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break;
case PILOT_VOLTAGE_9:
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
break;
case PILOT_VOLTAGE_6:
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
break;
case PILOT_VOLTAGE_3:
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
break;
default:
break;
}
@@ -171,52 +197,59 @@ void evse_fsm_process(
case EVSE_STATE_C1:
case EVSE_STATE_D1:
if (c1_d1_waiting && now >= c1_d1_relay_to)
{
ac_relay_set_state(false);
c1_d1_waiting = false;
case EVSE_STATE_C2:
case EVSE_STATE_D2:
if (!available)
{
evse_set_state(EVSE_STATE_F);
break;
}
}
__attribute__((fallthrough));
case EVSE_STATE_C2:
case EVSE_STATE_D2:
if (!enabled || !available)
if (!enabled)
{
evse_set_state((curr == EVSE_STATE_D2 || curr == EVSE_STATE_D1)
? EVSE_STATE_D1
: EVSE_STATE_C1);
if (curr == EVSE_STATE_C2)
{
evse_set_state(EVSE_STATE_C1);
}
else if (curr == EVSE_STATE_D2)
{
evse_set_state(EVSE_STATE_D1);
}
break;
}
switch (pilot_voltage)
{
case PILOT_VOLTAGE_6:
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
break;
case PILOT_VOLTAGE_3:
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
break;
case PILOT_VOLTAGE_9:
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
break;
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break;
default:
break;
}
break;
case EVSE_STATE_E:
// Estado elétrico grave: só reset manual
// ✅ Agora recupera como F: se disponível e sem erro -> volta a A
if (available && evse_get_error() == 0)
{
evse_set_state(EVSE_STATE_A);
}
break;
case EVSE_STATE_F:
// Fault: só sai se disponível e sem erro
if (available && evse_get_error() == 0)
{
evse_set_state(EVSE_STATE_A);

0
components/evse/evse_hardware.c Executable file → Normal file
View File

244
components/evse/evse_limits.c Executable file → Normal file
View File

@@ -1,34 +1,123 @@
#include <inttypes.h> // for PRIu32
#include <inttypes.h>
#include <stdbool.h>
#include "evse_state.h"
#include "evse_api.h"
#include "evse_limits.h"
#include "evse_meter.h"
#include "evse_session.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "nvs.h"
#include "esp_check.h"
// ========================
// Concurrency protection
// ========================
#include "storage_service.h"
#define NVS_NAMESPACE "evse_limits"
static const char *TAG = "evse_limits";
static portMUX_TYPE evse_mux = portMUX_INITIALIZER_UNLOCKED;
// ========================
// Runtime state (volatile)
// ========================
static bool limit_reached = false;
static uint32_t consumption_limit = 0; // Energy limit in Wh
static uint32_t charging_time_limit = 0; // Time limit in seconds
static uint16_t under_power_limit = 0; // Minimum acceptable power in W
static uint32_t consumption_limit = 0; // Wh
static uint32_t charging_time_limit = 0; // seconds
static uint16_t under_power_limit = 0; // W
// ========================
// Limit status flag
// ========================
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(1000); }
// ---------------------------------
// Init + defaults
// ---------------------------------
esp_err_t evse_limits_init(void)
{
ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed");
ESP_LOGI(TAG, "EVSE limits init OK (storage-backed)");
return ESP_OK;
}
void evse_limits_check_defaults(void)
{
esp_err_t err;
bool needs_flush = false;
uint32_t u32 = 0;
uint16_t u16 = 0;
ESP_LOGD(TAG, "Checking default limits...");
// Consumption limit (Wh) default = 0 (disabled)
err = storage_get_u32_sync(NVS_NAMESPACE, "def_cons_lim", &u32, BOOT_TO());
if (err == ESP_OK)
{
portENTER_CRITICAL(&evse_mux);
consumption_limit = u32;
portEXIT_CRITICAL(&evse_mux);
}
else
{
portENTER_CRITICAL(&evse_mux);
consumption_limit = 0;
portEXIT_CRITICAL(&evse_mux);
(void)storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", 0);
needs_flush = true;
ESP_LOGW(TAG, "Missing def_cons_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
}
// Charging time limit (s) default = 0 (disabled)
err = storage_get_u32_sync(NVS_NAMESPACE, "def_ch_time_lim", &u32, BOOT_TO());
if (err == ESP_OK)
{
portENTER_CRITICAL(&evse_mux);
charging_time_limit = u32;
portEXIT_CRITICAL(&evse_mux);
}
else
{
portENTER_CRITICAL(&evse_mux);
charging_time_limit = 0;
portEXIT_CRITICAL(&evse_mux);
(void)storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", 0);
needs_flush = true;
ESP_LOGW(TAG, "Missing def_ch_time_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
}
// Under-power limit (W) default = 0 (disabled)
err = storage_get_u16_sync(NVS_NAMESPACE, "def_un_pwr_lim", &u16, BOOT_TO());
if (err == ESP_OK)
{
portENTER_CRITICAL(&evse_mux);
under_power_limit = u16;
portEXIT_CRITICAL(&evse_mux);
}
else
{
portENTER_CRITICAL(&evse_mux);
under_power_limit = 0;
portEXIT_CRITICAL(&evse_mux);
(void)storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", 0);
needs_flush = true;
ESP_LOGW(TAG, "Missing def_un_pwr_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
}
if (needs_flush)
{
esp_err_t fe = storage_flush_sync(TO_TICKS_MS(2000));
if (fe != ESP_OK)
ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe));
else
ESP_LOGD(TAG, "Defaults committed (flush).");
}
}
// ---------------------------------
// Limit reached flag
// ---------------------------------
bool evse_get_limit_reached(void)
{
bool val;
@@ -50,10 +139,9 @@ bool evse_is_limit_reached(void)
return evse_get_limit_reached();
}
// ========================
// Runtime limit accessors
// ========================
// ---------------------------------
// Consumption limit
// ---------------------------------
uint32_t evse_get_consumption_limit(void)
{
uint32_t val;
@@ -78,30 +166,18 @@ void evse_set_consumption_limit(uint32_t value)
if (!changed)
return;
nvs_handle_t h;
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
if (err == ESP_OK)
{
err = nvs_set_u32(h, "def_cons_lim", value);
if (err == ESP_OK)
err = nvs_commit(h);
nvs_close(h);
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", value);
if (err != ESP_OK)
{
ESP_LOGE("EVSE_LIMITS",
ESP_LOGE(TAG,
"Failed to persist consumption limit (%" PRIu32 " Wh): %s",
value, esp_err_to_name(err));
}
}
else
{
ESP_LOGE("EVSE_LIMITS",
"Failed to open NVS for consumption limit: %s",
esp_err_to_name(err));
}
}
// ---------------------------------
// Charging time limit
// ---------------------------------
uint32_t evse_get_charging_time_limit(void)
{
uint32_t val;
@@ -126,30 +202,18 @@ void evse_set_charging_time_limit(uint32_t value)
if (!changed)
return;
nvs_handle_t h;
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
if (err == ESP_OK)
{
err = nvs_set_u32(h, "def_ch_time_lim", value);
if (err == ESP_OK)
err = nvs_commit(h);
nvs_close(h);
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", value);
if (err != ESP_OK)
{
ESP_LOGE("EVSE_LIMITS",
ESP_LOGE(TAG,
"Failed to persist charging time limit (%" PRIu32 " s): %s",
value, esp_err_to_name(err));
}
}
else
{
ESP_LOGE("EVSE_LIMITS",
"Failed to open NVS for charging time limit: %s",
esp_err_to_name(err));
}
}
// ---------------------------------
// Under-power limit
// ---------------------------------
uint16_t evse_get_under_power_limit(void)
{
uint16_t val;
@@ -174,82 +238,64 @@ void evse_set_under_power_limit(uint16_t value)
if (!changed)
return;
nvs_handle_t h;
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
if (err == ESP_OK)
{
err = nvs_set_u16(h, "def_un_pwr_lim", value);
if (err == ESP_OK)
err = nvs_commit(h);
nvs_close(h);
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", value);
if (err != ESP_OK)
{
ESP_LOGE("EVSE_LIMITS",
ESP_LOGE(TAG,
"Failed to persist under-power limit (%" PRIu32 " W): %s",
(uint32_t)value, esp_err_to_name(err));
}
}
else
{
ESP_LOGE("EVSE_LIMITS",
"Failed to open NVS for under-power limit: %s",
esp_err_to_name(err));
}
}
// ========================
// Limit checking logic
// ========================
// ---------------------------------
// Runtime check
// ---------------------------------
void evse_limits_check(void)
{
// Só faz sentido durante carregamento
// Só faz sentido quando há energia ativa (C2/D2)
if (!evse_state_is_charging(evse_get_state()))
{
return;
}
evse_session_t sess;
if (!evse_session_get(&sess) || !sess.is_current)
{
// Sem sessão ativa → nada a fazer
return;
}
uint32_t cons_lim;
uint32_t time_lim;
uint16_t unp_lim;
portENTER_CRITICAL(&evse_mux);
cons_lim = consumption_limit;
time_lim = charging_time_limit;
unp_lim = under_power_limit;
portEXIT_CRITICAL(&evse_mux);
bool reached = false;
// 1) Limite de energia (Wh)
if (consumption_limit > 0 && sess.energy_wh >= consumption_limit)
if (cons_lim > 0 && sess.energy_wh >= cons_lim)
{
ESP_LOGW("EVSE_LIMITS",
"Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
sess.energy_wh, consumption_limit);
ESP_LOGW(TAG, "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
sess.energy_wh, cons_lim);
reached = true;
}
// 2) Limite de tempo (s)
if (charging_time_limit > 0 && sess.duration_s >= charging_time_limit)
if (time_lim > 0 && sess.duration_s >= time_lim)
{
ESP_LOGW("EVSE_LIMITS",
"Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
sess.duration_s, charging_time_limit);
ESP_LOGW(TAG, "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
sess.duration_s, time_lim);
reached = true;
}
// 3) Under-power (potência instantânea)
uint32_t inst_power = evse_meter_get_instant_power();
if (under_power_limit > 0 && inst_power < under_power_limit)
int32_t p = evse_meter_get_instant_power();
uint32_t inst_power = (p > 0) ? (uint32_t)p : 0;
if (unp_lim > 0 && inst_power < (uint32_t)unp_lim)
{
ESP_LOGW("EVSE_LIMITS",
"Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
(uint32_t)inst_power,
(uint32_t)under_power_limit);
ESP_LOGW(TAG, "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
inst_power, (uint32_t)unp_lim);
reached = true;
}
if (reached)
{
evse_set_limit_reached(true);
}
}

358
components/evse/evse_manager.c Executable file → Normal file
View File

@@ -1,4 +1,3 @@
// === Início de: components/evse/evse_manager.c ===
#include "evse_manager.h"
#include "evse_state.h"
#include "evse_error.h"
@@ -11,10 +10,10 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_err.h"
#include <string.h>
#include <inttypes.h>
@@ -23,31 +22,38 @@
#include "loadbalancer_events.h"
#include "ocpp_events.h"
#include "scheduler_events.h"
#include "evse_link_events.h"
static const char *TAG = "EVSE_Manager";
static SemaphoreHandle_t evse_mutex;
static volatile bool auth_enabled = false;
// ✅ Proteção para flags partilhadas (event handlers vs task)
static portMUX_TYPE s_mgr_mux = portMUX_INITIALIZER_UNLOCKED;
static bool auth_enabled = false;
static char s_pending_ocpp_tag[AUTH_TAG_MAX_LEN];
// Estado de pausa controlado pelo Load Balancer
static volatile bool lb_paused = false;
static volatile bool lb_prev_authorized = false;
static bool lb_paused = false;
static bool lb_prev_authorized = false;
// Estado de janela do scheduler
static volatile bool s_sched_allowed = true;
static bool s_sched_allowed = true;
static portMUX_TYPE s_sched_mux = portMUX_INITIALIZER_UNLOCKED;
#define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo
// ================= Helpers internos =================
static uint16_t s_sched_current_a = 0;
static void lb_clear_pause_state(void)
{
portENTER_CRITICAL(&s_mgr_mux);
lb_paused = false;
lb_prev_authorized = false;
portEXIT_CRITICAL(&s_mgr_mux);
}
// Exposto para outros módulos (se quiserem saber se o scheduler permite)
bool evse_sched_is_allowed(void)
{
bool v;
@@ -57,52 +63,142 @@ bool evse_sched_is_allowed(void)
return v;
}
static void on_evse_link_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_LINK_EVENTS || id != LINK_EVENT_CURRENT_LIMIT_APPLIED || data == NULL)
return;
uint16_t current = *(const uint16_t *)data;
ESP_LOGI(TAG, "[EVSE-LINK] current limit applied: %uA", current);
if (current == 0)
{
bool prev_auth = evse_state_get_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, "[EVSE-LINK] limit=0A → paused by master");
evse_state_set_authorized(false);
}
return;
}
evse_set_runtime_charging_current(current);
bool was_paused;
bool prev_auth;
bool local_auth_enabled;
portENTER_CRITICAL(&s_mgr_mux);
was_paused = lb_paused;
prev_auth = lb_prev_authorized;
local_auth_enabled = auth_enabled;
lb_paused = false;
lb_prev_authorized = false;
portEXIT_CRITICAL(&s_mgr_mux);
if (was_paused)
{
bool can_resume =
(evse_get_error() == 0) &&
evse_config_is_available() &&
evse_config_is_enabled() &&
evse_sched_is_allowed() &&
!evse_is_limit_reached();
if (!can_resume)
{
ESP_LOGW(TAG, "[EVSE-LINK] limit=%uA → cannot resume", current);
return;
}
if (!local_auth_enabled || prev_auth)
{
ESP_LOGI(TAG, "[EVSE-LINK] limit=%uA → resuming after master pause", current);
evse_state_set_authorized(true);
}
}
}
static void evse_manager_handle_auth_on_tick(void)
{
bool sched_allowed = evse_sched_is_allowed();
uint32_t err_bits = evse_get_error(); // inclui holdoff interno
bool has_error = (err_bits != 0);
if (auth_enabled)
bool local_auth_enabled;
bool local_lb_paused;
portENTER_CRITICAL(&s_mgr_mux);
local_auth_enabled = auth_enabled;
local_lb_paused = lb_paused;
portEXIT_CRITICAL(&s_mgr_mux);
if (local_auth_enabled)
{
// Se o carro foi desconectado, revoga autorização
if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A)
{
ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization.");
evse_session_clear_authorized_tag();
evse_state_set_authorized(false);
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_session_clear_authorized_tag();
evse_state_set_authorized(false);
// Desconexão física invalida qualquer pausa pendente do LB
lb_clear_pause_state();
}
// Em modos RFID/OCPP, o scheduler pode também forçar paragem
if (!sched_allowed && evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed (auth mode) → revoking authorization.");
evse_session_clear_authorized_tag();
evse_state_set_authorized(false);
}
}
else
{
// Modo OPEN: só autoriza se LB e Scheduler permitirem
if (!lb_paused && sched_allowed && !evse_state_get_authorized())
{
evse_state_set_authorized(true);
ESP_LOGI(TAG, "Authentication disabled → forced authorization (within schedule).");
lb_clear_pause_state();
}
bool limit_hit = evse_is_limit_reached();
bool can_operate = evse_config_is_available() && evse_config_is_enabled();
// Fora da janela, garantir que não fica autorizado
if (!sched_allowed && evse_state_get_authorized())
if ((has_error || limit_hit || !sched_allowed || !can_operate || local_lb_paused) &&
evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed (OPEN mode) → revoking authorization.");
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_session_clear_authorized_tag();
evse_state_set_authorized(true);
ESP_LOGI(TAG, "Authentication disabled → forced authorization (schedule ok, no error/limits).");
lb_clear_pause_state();
}
}
}
// ===== Task de ciclo principal =====
static void evse_manager_task(void *arg)
{
(void)arg;
while (true)
{
evse_manager_tick();
@@ -110,56 +206,61 @@ static void evse_manager_task(void *arg)
}
}
// ===== Tratador de eventos de AUTH =====
static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != AUTH_EVENTS || !data)
return;
auth_mode_t g_mode = AUTH_MODE_OPEN;
switch (id)
{
case AUTH_EVENT_TAG_PROCESSED:
{
const auth_tag_event_data_t *evt = (const auth_tag_event_data_t *)data;
ESP_LOGI(TAG, "Tag %s -> %s", evt->tag, evt->authorized ? "AUTHORIZED" : "DENIED");
if (evt->authorized)
{
evse_session_set_authorized_tag(evt->tag);
}
else
{
evse_session_clear_authorized_tag();
}
evse_state_set_authorized(evt->authorized);
// Qualquer alteração explícita de auth invalida pausa do LB
lb_clear_pause_state();
break;
}
case AUTH_EVENT_TAG_VERIFY:
{
const auth_tag_verify_event_t *evt = (const auth_tag_verify_event_t *)data;
ESP_LOGI(TAG, "Tag %s -> pending remote/OCPP verification", evt->tag);
portENTER_CRITICAL(&s_mgr_mux);
strncpy(s_pending_ocpp_tag, evt->tag, AUTH_TAG_MAX_LEN - 1);
s_pending_ocpp_tag[AUTH_TAG_MAX_LEN - 1] = '\0';
portEXIT_CRITICAL(&s_mgr_mux);
break;
}
case AUTH_EVENT_MODE_CHANGED:
case AUTH_EVENT_INIT:
{
const auth_mode_event_data_t *evt = (const auth_mode_event_data_t *)data;
g_mode = evt->mode;
ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(g_mode));
if (g_mode == AUTH_MODE_OPEN)
{
// Em OPEN, a autorização passa a ser gerida por evse_manager_handle_auth_on_tick(),
// que também respeita o scheduler.
evse_state_set_authorized(false); // vai ser forçado no próximo tick se permitido
auth_enabled = false;
}
else
{
evse_state_set_authorized(false);
auth_enabled = true;
}
ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(evt->mode));
// Modo mudou -> qualquer pausa antiga deixa de fazer sentido
portENTER_CRITICAL(&s_mgr_mux);
auth_enabled = (evt->mode != AUTH_MODE_OPEN);
s_pending_ocpp_tag[0] = '\0';
portEXIT_CRITICAL(&s_mgr_mux);
evse_session_clear_authorized_tag();
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
}
}
}
// ===== Tratador de eventos de Load Balancer =====
static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
@@ -181,143 +282,143 @@ static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base
const loadbalancer_master_limit_event_t *evt =
(const loadbalancer_master_limit_event_t *)event_data;
ESP_LOGI(TAG,
"Novo limite de corrente (master): %u A (ts: %lld)",
ESP_LOGI(TAG, "Novo limite de corrente (master): %u A (ts: %lld)",
evt->max_current, (long long)evt->timestamp_us);
if (evt->max_current == 0)
{
// Suspensão por LB (não interessa se é OPEN ou RFID/OCPP)
lb_paused = true;
lb_prev_authorized = evse_state_get_authorized();
bool prev_auth = evse_state_get_authorized();
if (lb_prev_authorized)
portENTER_CRITICAL(&s_mgr_mux);
lb_paused = true;
lb_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_mgr_mux);
if (prev_auth)
{
ESP_LOGI(TAG, "[LB] limit=0A → pausando sessão (authorized=false)");
evse_state_set_authorized(false);
}
else
{
ESP_LOGD(TAG, "[LB] limit=0A → já não estava autorizado");
}
}
else
{
// Ajusta corrente em runtime
evse_set_runtime_charging_current(evt->max_current);
if (lb_paused)
{
lb_paused = false;
bool was_paused;
bool prev_auth;
// Só retomamos se EVSE estiver operacional e scheduler permitir
portENTER_CRITICAL(&s_mgr_mux);
was_paused = lb_paused;
prev_auth = lb_prev_authorized;
portEXIT_CRITICAL(&s_mgr_mux);
if (was_paused)
{
bool can_resume =
(evse_get_error() == 0) &&
evse_config_is_available() &&
evse_config_is_enabled() &&
evse_sched_is_allowed();
evse_sched_is_allowed() &&
!evse_is_limit_reached();
if (!can_resume)
{
ESP_LOGW(TAG,
"[LB] limit=%uA → não retoma automaticamente (erro/indisponível/desabilitado/fora de horário)",
"[LB] limit=%uA → não retoma automaticamente (erro/indisp/desab/fora de horário/limite)",
evt->max_current);
lb_clear_pause_state();
return;
}
if (!auth_enabled)
bool local_auth_enabled;
portENTER_CRITICAL(&s_mgr_mux);
local_auth_enabled = auth_enabled;
lb_paused = false; // já vai tentar retomar
portEXIT_CRITICAL(&s_mgr_mux);
if (!local_auth_enabled)
{
// Modo OPEN: retoma sempre (se dentro da janela do scheduler)
ESP_LOGI(TAG,
"[LB] limit=%uA → modo OPEN, reautorizando (authorized=true)",
evt->max_current);
ESP_LOGI(TAG, "[LB] limit=%uA → modo OPEN, reautorizando", evt->max_current);
evse_state_set_authorized(true);
}
else
{
// RFID/OCPP: só retoma se havia autorização antes da pausa
if (lb_prev_authorized)
if (prev_auth)
{
ESP_LOGI(TAG,
"[LB] limit=%uA → RFID/OCPP, retomando autorização anterior (auto-resume)",
evt->max_current);
ESP_LOGI(TAG, "[LB] limit=%uA → RFID/OCPP, retomando autorização anterior", evt->max_current);
evse_state_set_authorized(true);
}
else
{
ESP_LOGI(TAG,
"[LB] limit=%uA → RFID/OCPP, sem autorização prévia, mantendo estado atual",
evt->max_current);
}
}
// Limpa estado prévio (não reaplicar em pausas futuras)
portENTER_CRITICAL(&s_mgr_mux);
lb_prev_authorized = false;
}
else
{
// Caso normal: apenas ajuste de corrente, sem mexer em auth
ESP_LOGD(TAG,
"[LB] limit=%uA → ajustando corrente runtime (sem mudança de autorização)",
evt->max_current);
portEXIT_CRITICAL(&s_mgr_mux);
}
}
}
}
// ===== Tratador de eventos de OCPP =====
static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != OCPP_EVENTS)
return;
switch (id)
{
case OCPP_EVENT_AUTHORIZED:
{
char tag[AUTH_TAG_MAX_LEN];
portENTER_CRITICAL(&s_mgr_mux);
strncpy(tag, s_pending_ocpp_tag, AUTH_TAG_MAX_LEN - 1);
tag[AUTH_TAG_MAX_LEN - 1] = '\0';
s_pending_ocpp_tag[0] = '\0';
portEXIT_CRITICAL(&s_mgr_mux);
if (tag[0] != '\0')
{
evse_session_set_authorized_tag(tag);
ESP_LOGI(TAG, "[OCPP] Authorized tag=%s", tag);
}
else
{
ESP_LOGI(TAG, "[OCPP] Authorized");
}
evse_state_set_authorized(true);
lb_clear_pause_state();
break;
}
case OCPP_EVENT_AUTH_REJECTED:
ESP_LOGW(TAG, "[OCPP] Authorization rejected");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
case OCPP_EVENT_AUTH_TIMEOUT:
ESP_LOGW(TAG, "[OCPP] Authorization timeout");
case OCPP_EVENT_REMOTE_STOP:
case OCPP_EVENT_STOP_TX:
ESP_LOGW(TAG, "[OCPP] Authorization/Stop");
portENTER_CRITICAL(&s_mgr_mux);
s_pending_ocpp_tag[0] = '\0';
portEXIT_CRITICAL(&s_mgr_mux);
evse_session_clear_authorized_tag();
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
case OCPP_EVENT_REMOTE_START:
ESP_LOGI(TAG, "[OCPP] RemoteStart");
portENTER_CRITICAL(&s_mgr_mux);
s_pending_ocpp_tag[0] = '\0';
portEXIT_CRITICAL(&s_mgr_mux);
evse_session_clear_authorized_tag();
evse_state_set_authorized(true);
lb_clear_pause_state();
break;
case OCPP_EVENT_REMOTE_STOP:
ESP_LOGI(TAG, "[OCPP] RemoteStop");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
case OCPP_EVENT_START_TX:
ESP_LOGI(TAG, "[OCPP] StartTx");
lb_clear_pause_state();
break;
case OCPP_EVENT_STOP_TX:
ESP_LOGI(TAG, "[OCPP] StopTx");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
// ChangeAvailability remoto (operative/inoperative)
case OCPP_EVENT_OPERATIVE_UPDATED:
{
if (!data)
@@ -329,7 +430,6 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
ESP_LOGI(TAG, "[OCPP] OperativeUpdated: operative=%d ts=%lld",
(int)ev->operative, (long long)ev->timestamp_us);
// Mapear operative → enabled local (persiste e emite EVSE_EVENT_ENABLE_UPDATED)
evse_config_set_enabled(ev->operative);
break;
}
@@ -340,14 +440,9 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
}
}
// ===== Tratador de eventos de Scheduler =====
static void on_sched_event(void *arg,
esp_event_base_t base,
int32_t id,
void *data)
static void on_sched_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != SCHED_EVENTS || data == NULL)
return;
@@ -355,31 +450,37 @@ static void on_sched_event(void *arg,
portENTER_CRITICAL(&s_sched_mux);
s_sched_allowed = ev->allowed_now;
s_sched_current_a = ev->current_limit_a;
portEXIT_CRITICAL(&s_sched_mux);
ESP_LOGI(TAG,
"[SCHED] event id=%" PRIi32 " allowed_now=%d",
id, (int)ev->allowed_now);
ESP_LOGI(TAG, "[SCHED] allowed_now=%d current=%uA",
(int)ev->allowed_now, (unsigned)ev->current_limit_a);
// Se a janela fechou, parar sessão (revogar autorização)
if (!ev->allowed_now && evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed → stopping session (authorized=false)");
evse_session_clear_authorized_tag();
evse_state_set_authorized(false);
}
// Se a janela abriu de novo, não auto-reautorizamos aqui.
// Deixamos que o utilizador / OCPP decida iniciar nova sessão.
// (Em modo OPEN, o tick trata disso respeitando o scheduler.)
// ✅ aplica corrente quando permitido
if (ev->allowed_now && ev->current_limit_a > 0)
{
evse_set_runtime_charging_current(ev->current_limit_a);
}
}
// ===== Inicialização =====
void evse_manager_init(void)
{
evse_mutex = xSemaphoreCreateMutex();
configASSERT(evse_mutex != NULL);
evse_config_init();
esp_err_t err = evse_config_init();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init EVSE config NVS: %s", esp_err_to_name(err));
}
evse_error_init();
evse_hardware_init();
evse_state_init();
@@ -390,14 +491,17 @@ void evse_manager_init(void)
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(SCHED_EVENTS, ESP_EVENT_ANY_ID, &on_sched_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(EVSE_LINK_EVENTS,
ESP_EVENT_ANY_ID,
&on_evse_link_event,
NULL));
ESP_LOGI(TAG, "EVSE Manager inicializado.");
BaseType_t rc = xTaskCreate(evse_manager_task, "evse_manager_task", 4096, NULL, 5, NULL);
BaseType_t rc = xTaskCreate(evse_manager_task, "evse_manager_task", 8192, NULL, 4, NULL);
configASSERT(rc == pdPASS);
}
// ===== Main Tick =====
void evse_manager_tick(void)
{
xSemaphoreTake(evse_mutex, portMAX_DELAY);
@@ -412,5 +516,3 @@ void evse_manager_tick(void)
xSemaphoreGive(evse_mutex);
}
// === Fim de: components/evse/evse_manager.c ===

247
components/evse/evse_pilot.c Executable file → Normal file
View File

@@ -1,8 +1,7 @@
// components/evse/evse_pilot.c
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "driver/ledc.h"
#include "esp_err.h"
@@ -11,6 +10,7 @@
#include "evse_pilot.h"
#include "adc121s021_dma.h"
#include "adc.h"
#include "board_config.h"
#define PILOT_PWM_TIMER LEDC_TIMER_0
@@ -19,33 +19,140 @@
#define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT
#define PILOT_PWM_MAX_DUTY 1023
// --- Configuração de amostragem do Pilot ---
#define NUM_PILOT_SAMPLES 100
#define MAX_SAMPLE_ATTEMPTS 1000
#define PILOT_SAMPLE_DELAY_US 10
// Percentagem para descartar extremos superior/inferior (ruído)
#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior
// Referência usada pelo ADC121S021 externo.
// No ADC interno, o valor já é convertido para mV por adc_cali_raw_to_voltage().
#define ADC121_VREF_MV 3300
#define ADC121_MAX 4095
static const char *TAG = "evse_pilot";
static int last_pilot_level = -1;
static uint32_t last_pwm_duty = 0;
typedef enum {
PILOT_MODE_DC_HIGH = 0, // +12V (nível alto)
PILOT_MODE_DC_LOW, // nível baixo / pilot desligado (dependente do hardware)
PILOT_MODE_PWM // PWM ativo
} pilot_mode_t;
static int adc_raw_to_mv(uint16_t raw) {
return (raw * ADC121_VREF_MV) / ADC121_MAX;
static pilot_mode_t s_mode = PILOT_MODE_DC_LOW;
static uint32_t last_pwm_duty = 0;
static bool s_internal_adc_configured = false;
// ---------------------
// Helpers internos
// ---------------------
static int adc121_raw_to_mv(uint16_t raw)
{
return (int)((raw * ADC121_VREF_MV) / ADC121_MAX);
}
static int compare_int(const void *a, const void *b)
{
int va = *(const int *)a;
int vb = *(const int *)b;
if (va < vb) return -1;
if (va > vb) return 1;
return 0;
}
static bool pilot_adc_read_mv(int *mv)
{
if (!mv)
{
return false;
}
if (board_config.pilot_adc_source == BOARD_CONFIG_PILOT_ADC_INTERNAL_ESP32)
{
if (!adc_handle || !adc_cali_handle)
{
ESP_LOGE(TAG, "ADC interno não inicializado/calibrado");
return false;
}
int raw = 0;
esp_err_t err;
adc_lock();
err = adc_oneshot_read(adc_handle, board_config.pilot_adc_channel, &raw);
if (err == ESP_OK)
{
err = adc_cali_raw_to_voltage(adc_cali_handle, raw, mv);
}
adc_unlock();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Erro ao ler/converter ADC interno do pilot: %s", esp_err_to_name(err));
return false;
}
return true;
}
uint16_t raw = 0;
if (!adc121s021_dma_get_sample(&raw))
{
return false;
}
*mv = adc121_raw_to_mv(raw);
return true;
}
static void pilot_adc_init(void)
{
if (board_config.pilot_adc_source == BOARD_CONFIG_PILOT_ADC_INTERNAL_ESP32)
{
if (!adc_handle)
{
adc_init();
}
if (!s_internal_adc_configured)
{
adc_oneshot_chan_cfg_t config = {
.bitwidth = ADC_BITWIDTH_DEFAULT,
.atten = ADC_ATTEN_DB_12
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle,
board_config.pilot_adc_channel,
&config));
s_internal_adc_configured = true;
}
ESP_LOGI(TAG, "Pilot ADC: interno ESP32 ADC1_CH%d",
(int)board_config.pilot_adc_channel);
return;
}
ESP_LOGI(TAG, "Pilot ADC: ADC121S021 externo via SPI");
adc121s021_dma_init();
}
// ---------------------
// Inicialização PWM + ADC
// ---------------------
void pilot_init(void)
{
// Configura timer do PWM do Pilot (1 kHz)
ledc_timer_config_t ledc_timer = {
.speed_mode = PILOT_PWM_SPEED_MODE,
.timer_num = PILOT_PWM_TIMER,
.duty_resolution = PILOT_PWM_DUTY_RES,
.freq_hz = 1000,
.freq_hz = 1000, // 1 kHz (IEC 61851)
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Canal do PWM no pino configurado em board_config
ledc_channel_config_t ledc_channel = {
.speed_mode = PILOT_PWM_SPEED_MODE,
.channel = PILOT_PWM_CHANNEL,
@@ -56,110 +163,156 @@ void pilot_init(void)
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0));
adc121s021_dma_init();
// 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 o backend de leitura do pilot configurado na board.
pilot_adc_init();
}
void pilot_set_level(bool level)
// ---------------------
// Controlo do modo do Pilot
// ---------------------
void pilot_set_level(bool high)
{
if (last_pilot_level == level) return;
last_pilot_level = level;
pilot_mode_t target = high ? PILOT_MODE_DC_HIGH : PILOT_MODE_DC_LOW;
ESP_LOGI(TAG, "Set level %d", level);
ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, level ? 1 : 0);
// Se já estiver no modo DC desejado e sem PWM ativo, ignora
if (s_mode == target && last_pwm_duty == 0) {
return;
}
ESP_LOGI(TAG, "Pilot set DC level: %s", high ? "HIGH(+12V)" : "LOW/OFF");
// Para PWM e fixa o nível idle do GPIO
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, high ? 1 : 0));
s_mode = target;
last_pwm_duty = 0;
}
void pilot_set_amps(uint16_t amps)
{
if (amps < 6 || amps > 80) {
if (amps < 6 || amps > 80)
{
ESP_LOGE(TAG, "Invalid ampere value: %d A (valid: 6–80 A)", amps);
return;
}
uint32_t duty_percent;
if (amps <= 51) {
if (amps <= 51)
{
duty_percent = (amps * 10) / 6;
} else {
}
else
{
duty_percent = (amps * 10) / 25 + 64;
}
if (duty_percent > 100) duty_percent = 100;
uint32_t duty = (PILOT_PWM_MAX_DUTY * duty_percent) / 100;
if (last_pilot_level == 0 && last_pwm_duty == duty) return;
last_pilot_level = 0;
// Se já estiver em PWM com o mesmo duty, ignora
if (s_mode == PILOT_MODE_PWM && last_pwm_duty == duty) {
return;
}
s_mode = PILOT_MODE_PWM;
last_pwm_duty = duty;
ESP_LOGI(TAG, "Pilot set: %d A → %d/%d (≈ %d%% duty)",
ESP_LOGI(TAG, "Pilot set PWM: %d A → %d/%d (≈ %d%% duty)",
amps, (int)duty, PILOT_PWM_MAX_DUTY, (int)duty_percent);
ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty);
ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL);
ESP_ERROR_CHECK(ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty));
ESP_ERROR_CHECK(ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL));
}
bool pilot_get_state(void) {
return (last_pilot_level == 1) && (last_pwm_duty == 0);
}
static int compare_int(const void *a, const void *b) {
return (*(const int *)a - *(const int *)b);
bool pilot_get_state(void)
{
// "Alto" significa DC +12V (estado A). PWM não conta como DC high.
return (s_mode == PILOT_MODE_DC_HIGH);
}
// ---------------------
// Medição do sinal de Pilot (PWM 1 kHz J1772)
// ---------------------
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12)
{
ESP_LOGD(TAG, "pilot_measure");
int samples[NUM_PILOT_SAMPLES];
int collected = 0, attempts = 0;
uint16_t adc_sample = 0;
int samples_mv[NUM_PILOT_SAMPLES];
int collected = 0;
int attempts = 0;
while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS) {
adc_sample = 0;
if (adc121s021_dma_get_sample(&adc_sample)) {
samples[collected++] = adc_sample;
esp_rom_delay_us(10);
} else {
while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS)
{
int sample_mv = 0;
if (pilot_adc_read_mv(&sample_mv))
{
samples_mv[collected++] = sample_mv;
esp_rom_delay_us(PILOT_SAMPLE_DELAY_US);
}
else
{
esp_rom_delay_us(100);
attempts++;
}
}
if (collected < NUM_PILOT_SAMPLES) {
if (collected < NUM_PILOT_SAMPLES)
{
ESP_LOGW(TAG, "Timeout on sample read (%d/%d)", collected, NUM_PILOT_SAMPLES);
*up_voltage = PILOT_VOLTAGE_1;
*down_voltage_n12 = false;
return;
}
qsort(samples, collected, sizeof(int), compare_int);
// Ordena as amostras para eliminar extremos (ruído/espúrios)
qsort(samples_mv, collected, sizeof(int), compare_int);
int k = (collected * PILOT_EXTREME_PERCENT) / 100;
if (k == 0) k = 1;
if (k < 2) k = 2; // garante margem mínima
// descarta k/2 em cada lado (aprox. 10% total, mantendo simetria)
int low_index = k / 2;
int high_index = collected - k + (k / 2);
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;
int low_raw = samples[low_index];
int high_raw = samples[high_index];
int high_mv = adc_raw_to_mv(high_raw);
int low_mv = adc_raw_to_mv(low_raw);
int low_mv = samples_mv[low_index];
int high_mv = samples_mv[high_index];
// Determina o nível positivo (+12, +9, +6, +3 ou <3 V)
if (high_mv >= board_config.pilot_down_threshold_12)
{
*up_voltage = PILOT_VOLTAGE_12;
}
else if (high_mv >= board_config.pilot_down_threshold_9)
{
*up_voltage = PILOT_VOLTAGE_9;
}
else if (high_mv >= board_config.pilot_down_threshold_6)
{
*up_voltage = PILOT_VOLTAGE_6;
}
else if (high_mv >= board_config.pilot_down_threshold_3)
{
*up_voltage = PILOT_VOLTAGE_3;
}
else
{
*up_voltage = PILOT_VOLTAGE_1;
}
// Verifica se o nível negativo atinge -12 V (diodo presente, C/D válidos)
*down_voltage_n12 = (low_mv <= board_config.pilot_down_threshold_n12);
ESP_LOGD(TAG, "Final: up_voltage=%d, down_voltage_n12=%d", *up_voltage, *down_voltage_n12);
ESP_LOGD(TAG, "Final: up_voltage=%d, down_voltage_n12=%d (high=%d mV, low=%d mV)",
*up_voltage, *down_voltage_n12, high_mv, low_mv);
}

View File

@@ -1,4 +1,5 @@
#include <inttypes.h>
#include <string.h>
#include "evse_session.h"
#include "evse_meter.h"
#include "freertos/FreeRTOS.h"
@@ -7,82 +8,157 @@
#include "evse_events.h"
#include "esp_event.h"
#include "evse_limits.h"
#include "esp_timer.h"
#define EVSE_EVENT_POST_TIMEOUT_MS 50
static const char *TAG = "evse_session";
static TickType_t session_start_tick = 0;
static uint32_t watt_seconds = 0;
// Tempo real (microsegundos)
static int64_t session_start_us = 0;
static int64_t last_tick_us = 0;
// Energia integrada com tempo real: soma de (W * us)
static uint64_t watt_microseconds = 0;
static evse_session_t last_session;
static bool last_session_valid = false;
static uint32_t session_counter = 0;
static char authorized_tag[AUTH_TAG_MAX_LEN];
static char current_session_tag[AUTH_TAG_MAX_LEN];
static portMUX_TYPE session_mux = portMUX_INITIALIZER_UNLOCKED;
static void post_session_event(const evse_session_event_data_t *evt)
{
esp_err_t err = esp_event_post(
EVSE_EVENTS,
EVSE_EVENT_SESSION,
evt,
sizeof(*evt),
portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_event_post(EVSE_EVENT_SESSION) failed: %s", esp_err_to_name(err));
}
}
void evse_session_init(void)
{
portENTER_CRITICAL(&session_mux);
session_start_tick = 0;
watt_seconds = 0;
session_start_us = 0;
last_tick_us = 0;
watt_microseconds = 0;
last_session_valid = false;
session_counter = 0;
authorized_tag[0] = '\0';
current_session_tag[0] = '\0';
portEXIT_CRITICAL(&session_mux);
}
void evse_session_set_authorized_tag(const char *tag)
{
portENTER_CRITICAL(&session_mux);
if (tag && tag[0] != '\0')
{
strncpy(authorized_tag, tag, AUTH_TAG_MAX_LEN - 1);
authorized_tag[AUTH_TAG_MAX_LEN - 1] = '\0';
}
else
{
authorized_tag[0] = '\0';
}
portEXIT_CRITICAL(&session_mux);
}
void evse_session_clear_authorized_tag(void)
{
evse_session_set_authorized_tag(NULL);
}
void evse_session_start(void)
{
TickType_t tick = xTaskGetTickCount();
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
session_start_tick = tick;
watt_seconds = 0;
session_start_us = now_us;
last_tick_us = now_us;
watt_microseconds = 0;
session_counter++;
uint32_t id = session_counter;
strncpy(current_session_tag, authorized_tag, AUTH_TAG_MAX_LEN - 1);
current_session_tag[AUTH_TAG_MAX_LEN - 1] = '\0';
char tag[AUTH_TAG_MAX_LEN];
strncpy(tag, current_session_tag, AUTH_TAG_MAX_LEN - 1);
tag[AUTH_TAG_MAX_LEN - 1] = '\0';
portEXIT_CRITICAL(&session_mux);
evse_set_limit_reached(false);
ESP_LOGI(TAG, "Session started at tick %u", (unsigned)tick);
ESP_LOGI(TAG, "Session started (id=%" PRIu32 ", tag=%s) tick=%u us=%" PRId64,
id, tag[0] ? tag : "-", (unsigned)tick, now_us);
evse_session_event_data_t evt = {
.type = EVSE_SESSION_EVENT_STARTED,
.session_id = session_counter,
.session_id = id,
.duration_s = 0,
.energy_wh = 0,
.avg_power_w = 0,
.is_current = true,
};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_SESSION,
&evt,
sizeof(evt),
portMAX_DELAY);
strncpy(evt.tag, tag, AUTH_TAG_MAX_LEN - 1);
evt.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
post_session_event(&evt);
}
void evse_session_end(void)
{
TickType_t start_tick;
uint32_t ws;
int64_t start_us;
uint64_t w_us;
uint32_t id;
char tag[AUTH_TAG_MAX_LEN];
int64_t end_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
if (session_start_tick == 0) {
if (session_start_tick == 0)
{
portEXIT_CRITICAL(&session_mux);
ESP_LOGW(TAG, "evse_session_end called without active session");
return;
}
start_tick = session_start_tick;
ws = watt_seconds;
start_us = session_start_us;
w_us = watt_microseconds;
id = session_counter;
strncpy(tag, current_session_tag, AUTH_TAG_MAX_LEN - 1);
tag[AUTH_TAG_MAX_LEN - 1] = '\0';
session_start_tick = 0;
session_start_us = 0;
last_tick_us = 0;
watt_microseconds = 0;
current_session_tag[0] = '\0';
portEXIT_CRITICAL(&session_mux);
TickType_t now = xTaskGetTickCount();
uint32_t duration_s = (now - start_tick) / configTICK_RATE_HZ;
uint32_t energy_wh = ws / 3600U;
uint32_t avg_power = duration_s > 0 ? ws / duration_s : 0;
uint32_t duration_s = (end_us > start_us) ? (uint32_t)((end_us - start_us) / 1000000LL) : 0;
uint32_t energy_wh = (uint32_t)(w_us / (3600ULL * 1000000ULL));
uint64_t watt_seconds = (uint64_t)(w_us / 1000000ULL);
uint32_t avg_power = (duration_s > 0) ? (uint32_t)(watt_seconds / duration_s) : 0;
portENTER_CRITICAL(&session_mux);
last_session.start_tick = start_tick;
last_session.session_id = id;
strncpy(last_session.tag, tag, AUTH_TAG_MAX_LEN - 1);
last_session.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
last_session.duration_s = duration_s;
last_session.energy_wh = energy_wh;
last_session.avg_power_w = avg_power;
@@ -91,9 +167,9 @@ void evse_session_end(void)
portEXIT_CRITICAL(&session_mux);
ESP_LOGI(TAG,
"Session ended: duration=%" PRIu32 " s, energy=%" PRIu32
" Wh, avg_power=%" PRIu32 " W",
duration_s, energy_wh, avg_power);
"Session ended (id=%" PRIu32 "): duration=%" PRIu32 " s, energy=%" PRIu32
" Wh, avg_power=%" PRIu32 " W, tag=%s",
id, duration_s, energy_wh, avg_power, tag[0] ? tag : "-");
evse_session_event_data_t evt = {
.type = EVSE_SESSION_EVENT_FINISHED,
@@ -103,21 +179,38 @@ void evse_session_end(void)
.avg_power_w = avg_power,
.is_current = false,
};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_SESSION,
&evt,
sizeof(evt),
portMAX_DELAY);
strncpy(evt.tag, tag, AUTH_TAG_MAX_LEN - 1);
evt.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
post_session_event(&evt);
}
void evse_session_tick(void)
{
uint32_t power_w = evse_meter_get_instant_power();
// Potência instantânea pode ser negativa (ruído/overflow de sensor) -> clamp
int p = evse_meter_get_instant_power();
uint32_t power_w = (p > 0) ? (uint32_t)p : 0;
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
if (session_start_tick != 0) {
watt_seconds += power_w;
if (session_start_tick != 0)
{
if (last_tick_us == 0)
{
last_tick_us = now_us;
}
int64_t dt_us = now_us - last_tick_us;
if (dt_us > 0)
{
// Energia incremental: W * us (64-bit)
watt_microseconds += ((uint64_t)power_w * (uint64_t)dt_us);
last_tick_us = now_us;
}
else
{
// relógio não devia andar para trás; ignora
last_tick_us = now_us;
}
}
portEXIT_CRITICAL(&session_mux);
}
@@ -127,28 +220,40 @@ bool evse_session_get(evse_session_t *out)
if (out == NULL)
return false;
TickType_t start;
uint32_t ws;
TickType_t start_tick;
int64_t start_us;
uint64_t w_us;
bool has_current;
evse_session_t last_copy;
bool last_valid;
uint32_t id;
char tag[AUTH_TAG_MAX_LEN];
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
start = session_start_tick;
ws = watt_seconds;
start_tick = session_start_tick;
start_us = session_start_us;
w_us = watt_microseconds;
has_current = (session_start_tick != 0);
id = session_counter;
strncpy(tag, current_session_tag, AUTH_TAG_MAX_LEN - 1);
tag[AUTH_TAG_MAX_LEN - 1] = '\0';
last_copy = last_session;
last_valid = last_session_valid;
portEXIT_CRITICAL(&session_mux);
if (has_current)
{
TickType_t now = xTaskGetTickCount();
uint32_t duration_s = (now - start) / configTICK_RATE_HZ;
uint32_t energy_wh = ws / 3600U;
uint32_t avg_power = duration_s > 0 ? ws / duration_s : 0;
uint32_t duration_s = (now_us > start_us) ? (uint32_t)((now_us - start_us) / 1000000LL) : 0;
uint32_t energy_wh = (uint32_t)(w_us / (3600ULL * 1000000ULL));
uint64_t watt_seconds = (uint64_t)(w_us / 1000000ULL);
uint32_t avg_power = (duration_s > 0) ? (uint32_t)(watt_seconds / duration_s) : 0;
out->start_tick = start;
out->start_tick = start_tick;
out->session_id = id;
strncpy(out->tag, tag, AUTH_TAG_MAX_LEN - 1);
out->tag[AUTH_TAG_MAX_LEN - 1] = '\0';
out->duration_s = duration_s;
out->energy_wh = energy_wh;
out->avg_power_w = avg_power;

209
components/evse/evse_state.c Executable file → Normal file
View File

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

0
components/evse/include/evse_api.h Executable file → Normal file
View File

17
components/evse/include/evse_config.h Executable file → Normal file
View File

@@ -15,11 +15,9 @@ extern "C" {
// Limites Globais (Defines)
// ========================
// Corrente máxima de carregamento (configurável pelo usuário)
#define MIN_CHARGING_CURRENT_LIMIT 6 // A
#define MAX_CHARGING_CURRENT_LIMIT 32 // A
// Corrente via cabo (proximity) — se configurável
#define MIN_CABLE_CURRENT_LIMIT 6 // A
#define MAX_CABLE_CURRENT_LIMIT 63 // A
@@ -31,23 +29,20 @@ extern "C" {
esp_err_t evse_config_init(void);
void evse_check_defaults(void);
// Corrente de carregamento
// Corrente máxima de hardware (fixa)
uint8_t evse_get_max_charging_current(void);
esp_err_t evse_set_max_charging_current(uint8_t value);
// Corrente configurável (persistida) <= max hardware
uint16_t evse_get_charging_current(void);
esp_err_t evse_set_charging_current(uint16_t value);
uint16_t evse_get_default_charging_current(void);
esp_err_t evse_set_default_charging_current(uint16_t value);
// Configuração de socket outlet
bool evse_get_socket_outlet(void);
esp_err_t evse_set_socket_outlet(bool socket_outlet);
// Corrente runtime (RAM) <= max hardware (load balancer pode alterar)
void evse_set_runtime_charging_current(uint16_t value);
uint16_t evse_get_runtime_charging_current(void);
// Socket outlet
bool evse_get_socket_outlet(void);
esp_err_t evse_set_socket_outlet(bool socket_outlet);
// RCM
bool evse_is_rcm(void);

20
components/evse/include/evse_error.h Executable file → Normal file
View File

@@ -1,4 +1,3 @@
// === Início de: components/evse/include/evse_error.h ===
#ifndef EVSE_ERROR_H
#define EVSE_ERROR_H
@@ -6,11 +5,13 @@
#include <stdbool.h>
#include "evse_pilot.h"
// Bits que auto-limpam passado um timeout
#define EVSE_ERR_AUTO_CLEAR_BITS ( \
EVSE_ERR_DIODE_SHORT_BIT | \
EVSE_ERR_TEMPERATURE_HIGH_BIT | \
EVSE_ERR_RCM_TRIGGERED_BIT)
// ----------------------------------------------------
// Holdoff interno pós-erro (sem expor "cooldown" ao resto)
// ----------------------------------------------------
// Após TODOS os erros reais desaparecerem (raw_bits == 0),
// o módulo mantém o erro "visível" durante este tempo.
// Durante este período, evse_get_error() continua != 0.
#define EVSE_ERROR_COOLDOWN_MS 60000
// Error bits
#define EVSE_ERR_DIODE_SHORT_BIT (1 << 0)
@@ -30,7 +31,7 @@ void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v);
void evse_temperature_check(void);
void evse_error_tick(void);
// Leitura e controle de erros
// Leitura e controle de erros (estado "visível" com holdoff)
uint32_t evse_get_error(void);
void evse_error_set(uint32_t bitmask);
void evse_error_clear(uint32_t bitmask);
@@ -40,12 +41,9 @@ uint32_t evse_error_get_bits(void);
// ----------------------------------------------------
// Semântica sticky: flag "todos erros limpos"
// (fica true quando o erro visível chega a 0; pode ser útil para UI/logs)
// ----------------------------------------------------
// Fica true quando TODOS os erros são limpos.
// Volta a false assim que qualquer erro novo aparece.
// Permanece true até o consumidor limpar explicitamente.
bool evse_error_cleared_flag(void);
void evse_error_reset_flag(void);
#endif // EVSE_ERROR_H
// === Fim de: components/evse/include/evse_error.h ===

41
components/evse/include/evse_events.h Executable file → Normal file
View File

@@ -6,6 +6,7 @@
#include <stdbool.h>
#include <stdint.h>
#include "esp_event.h"
#include "auth_types.h"
ESP_EVENT_DECLARE_BASE(EVSE_EVENTS);
@@ -16,6 +17,7 @@ typedef enum {
EVSE_EVENT_ENABLE_UPDATED,
EVSE_EVENT_AVAILABLE_UPDATED,
EVSE_EVENT_SESSION,
EVSE_EVENT_ERROR_CHANGED,
} evse_event_id_t;
// -----------------
@@ -43,35 +45,42 @@ typedef enum {
typedef struct {
evse_session_event_type_t type; ///< STARTED / FINISHED
// campos básicos da sessão, em tipos simples:
uint32_t session_id; ///< opcional, se tiveres um ID
uint32_t duration_s; ///< duração em segundos (0 no STARTED)
uint32_t energy_wh; ///< energia em Wh (0 no STARTED)
uint32_t avg_power_w; ///< potência média em W (0 no STARTED)
uint32_t session_id;
char tag[AUTH_TAG_MAX_LEN];
uint32_t duration_s;
uint32_t energy_wh;
uint32_t avg_power_w;
bool is_current; ///< true se ainda estiver em curso
bool is_current;
} evse_session_event_data_t;
// -----------------
// Eventos de CONFIG
// -----------------
typedef struct {
bool charging; // Estado de carregamento
float hw_max_current; // Corrente máxima suportada pelo hardware
float runtime_current; // Corrente de carregamento em uso
int64_t timestamp_us; // Momento da atualização
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} evse_config_event_data_t;
// Eventos simples e específicos
typedef struct {
bool enabled; // novo estado de enabled
int64_t timestamp_us; // epoch micros
bool enabled;
int64_t timestamp_us;
} evse_enable_event_data_t;
typedef struct {
bool available; // novo estado de available
int64_t timestamp_us; // epoch micros
bool available;
int64_t timestamp_us;
} evse_available_event_data_t;
// -----------------
// Eventos de ERRO
// -----------------
typedef struct {
uint32_t error_bits; ///< estado atual (todos os bits de erro)
uint32_t changed_mask; ///< bits que mudaram nesta notificação
int64_t timestamp_us; ///< esp_timer_get_time()
} evse_error_event_data_t;
#endif // EVSE_EVENTS_H

0
components/evse/include/evse_fsm.h Executable file → Normal file
View File

0
components/evse/include/evse_hardware.h Executable file → Normal file
View File

0
components/evse/include/evse_limits.h Executable file → Normal file
View File

0
components/evse/include/evse_manager.h Executable file → Normal file
View File

52
components/evse/include/evse_pilot.h Executable file → Normal file
View File

@@ -2,64 +2,42 @@
#define PILOT_H_
#ifdef __cplusplus
extern "C" {
extern "C"
{
#endif
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Níveis categóricos de tensão no sinal CP (Control Pilot)
*/
typedef enum
{
PILOT_VOLTAGE_12, ///< Estado A: +12V
PILOT_VOLTAGE_9, ///< Estado B: +9V
PILOT_VOLTAGE_6, ///< Estado C: +6V
PILOT_VOLTAGE_3, ///< Estado D: +3V
PILOT_VOLTAGE_1 ///< Estado E/F: abaixo de 3V
PILOT_VOLTAGE_12,
PILOT_VOLTAGE_9,
PILOT_VOLTAGE_6,
PILOT_VOLTAGE_3,
PILOT_VOLTAGE_1
} pilot_voltage_t;
/**
* @brief Inicializa o driver do sinal Pilot
*/
void pilot_init(void);
/**
* @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);
/**
* @brief Ativa o PWM do Pilot com corrente limitada
*
* @param amps Corrente em ampères (ex: 16 = 16A)
*/
void pilot_set_amps(uint16_t amps);
/**
* @brief Mede o nível de tensão do Pilot e detecta -12V
*
* @param up_voltage Valor categórico da tensão positiva
* @param down_voltage_n12 true se o nível negativo atingir -12V
*/
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12);
/**
* @brief Retorna o estado lógico atual do Pilot (nível alto = +12V)
*
* @return true se nível atual for +12V, false se for -12V
*/
bool pilot_get_state(void);
/**
* @brief Cache interno opcional dos níveis de tensão reais do Pilot
*/
typedef struct {
uint16_t high_mv; ///< Pico positivo medido (mV)
uint16_t low_mv; ///< Pico negativo medido (mV)
typedef struct
{
uint16_t high_mv;
uint16_t low_mv;
} pilot_voltage_cache_t;
#ifdef __cplusplus

View File

@@ -1,53 +1,44 @@
/*
* evse_session.h
* Module to track and retrieve charging session data (current or last completed),
* accumulating energy via periodic tick of instantaneous power.
*/
#ifndef EVSE_SESSION_H
#define EVSE_SESSION_H
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "auth_types.h"
/**
* @brief Charging session statistics
*/
typedef struct {
TickType_t start_tick; ///< tick when session began
uint32_t duration_s; ///< total duration in seconds
uint32_t energy_wh; ///< total energy consumed in Wh
TickType_t start_tick; ///< tick when session began (debug/trace)
uint32_t session_id; ///< monotonically increasing session identifier
char tag[AUTH_TAG_MAX_LEN]; ///< RFID/auth tag associated with this session, empty if none
uint32_t duration_s; ///< total duration in seconds (tempo real)
uint32_t energy_wh; ///< total energy consumed in Wh (tempo real)
uint32_t avg_power_w; ///< average power in W
bool is_current; ///< true if session still in progress
} evse_session_t;
/**
* @brief Initialize the session module
*/
void evse_session_init(void);
/**
* @brief Mark the beginning of a charging session
* @brief Stores the tag that authorized the next charging session.
*
* The tag is copied into the session when evse_session_start() is called.
* Passing NULL or an empty string clears the pending tag.
*/
void evse_session_start(void);
void evse_session_set_authorized_tag(const char *tag);
void evse_session_clear_authorized_tag(void);
/**
* @brief Mark the end of the charging session and store it as "last session"
*/
void evse_session_start(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);
/**
* @brief Retrieve statistics of either the current ongoing session (if any) or
* the last completed session.
* @param out pointer to evse_session_t to be filled
* @return true if there is a current or last session available, false otherwise
*/
bool evse_session_get(evse_session_t *out);
#endif // EVSE_SESSION_H

68
components/evse/include/evse_state.h Executable file → Normal file
View File

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

4
components/evse_link/CMakeLists.txt Executable file → Normal file
View File

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

3
components/evse_link/include/evse_link.h Executable file → Normal file
View File

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

View File

@@ -1,31 +1,29 @@
// === Início de: components/evse_link/include/evse_link_events.h ===
#ifndef EVSE_LINK_EVENTS_H_
#define EVSE_LINK_EVENTS_H_
#include "esp_event.h"
#include <stdint.h>
// Base de eventos do EVSE-Link
ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS);
// Tamanho máximo de tag propagada via EVSE-Link (inclui NUL)
#define EVSE_LINK_TAG_MAX_LEN 32
// IDs de eventos EVSE-Link
typedef enum {
LINK_EVENT_FRAME_RECEIVED, // qualquer frame válido
LINK_EVENT_SLAVE_ONLINE, // heartbeat recebido primeira vez
LINK_EVENT_SLAVE_OFFLINE, // sem heartbeat no timeout
LINK_EVENT_MASTER_POLL_SENT, // opcional: poll enviado pelo master
LINK_EVENT_FRAME_RECEIVED,
LINK_EVENT_SLAVE_ONLINE, // payload: evse_link_slave_presence_event_t
LINK_EVENT_SLAVE_OFFLINE, // payload: evse_link_slave_presence_event_t (master-side) ou NULL (slave-side fallback)
LINK_EVENT_MASTER_POLL_SENT,
LINK_EVENT_CURRENT_LIMIT_APPLIED,
LINK_EVENT_SLAVE_CONFIG_UPDATED, // config atualizada pelo master
LINK_EVENT_REMOTE_AUTH_GRANTED // autorização remota (master -> slave)
LINK_EVENT_SLAVE_CONFIG_UPDATED,
LINK_EVENT_REMOTE_AUTH_GRANTED
} evse_link_event_t;
// Payload para LINK_EVENT_REMOTE_AUTH_GRANTED
typedef struct {
char tag[EVSE_LINK_TAG_MAX_LEN]; // idTag enviada pelo master
char tag[EVSE_LINK_TAG_MAX_LEN];
} evse_link_auth_grant_event_t;
#endif // EVSE_LINK_EVENTS_H_
typedef struct {
uint8_t slave_id;
} evse_link_slave_presence_event_t;
// === Fim de: components/evse_link/include/evse_link_events.h ===
#endif // EVSE_LINK_EVENTS_H_

View File

@@ -5,21 +5,36 @@
#include <stdbool.h>
#include "driver/uart.h"
// UART instance and configuration
#define UART_PORT UART_NUM_2 // Usa a UART2
#define UART_BAUDRATE 115200
// UART baud/buffer configuration
#define UART_BAUDRATE 9600
#define UART_RX_BUF_SIZE 256
// GPIO pin assignments for UART (ajuste conforme o hardware)
#define UART_TXD 17 // TX -> DI do MAX3485
#define UART_RXD 16 // RX -> RO do MAX3485
#define UART_RTS 2 // RTS -> DE+RE do MAX3485
// Select EVSE-Link physical layer at compile time.
// 0 = production RS485: UART2 GPIO17/16 + RTS/DE GPIO2
// 1 = emergency/internal UART TTL: UART1 GPIO21/22, no RS485/RTS
#ifndef EVSE_LINK_USE_UART_TTL
#define EVSE_LINK_USE_UART_TTL 0
#endif
// Conveniência: nomes usados no .c
#define TX_PIN UART_TXD
#define RX_PIN UART_RXD
#define RTS_PIN UART_RTS
#if EVSE_LINK_USE_UART_TTL
#define UART_PORT UART_NUM_1
#define TX_PIN 21
#define RX_PIN 22
#define RTS_PIN UART_PIN_NO_CHANGE
#define EVSE_LINK_UART_MODE UART_MODE_UART
#define EVSE_LINK_PHY_NAME "UART_TTL"
#else
#define UART_PORT UART_NUM_2
#define TX_PIN 17
#define RX_PIN 16
#define RTS_PIN 2
#define EVSE_LINK_UART_MODE UART_MODE_RS485_HALF_DUPLEX
#define EVSE_LINK_PHY_NAME "RS485"
#endif
// Frame delimiters
#define MAGIC_START 0x7E
#define MAGIC_END 0x7F

169
components/evse_link/src/evse_link.c Executable file → Normal file
View File

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

View File

@@ -1,8 +1,29 @@
// components/evse_link/src/evse_link_framing.c
//
// EVSE-Link framing (RS-485 HALF DUPLEX via UART driver)
// - Usa UART_MODE_RS485_HALF_DUPLEX (driver controla RTS => DE//RE)
// - Configura RX timeout + RX full threshold para evitar “len=120”
// - Remove controlo manual de GPIO do RTS
//
// Requisitos:
// - MAX3485 com DE e /RE juntos ligados ao RTS_PIN
// - RTS_PIN definido em evse_link_framing.h (ex.: GPIO2; recomendado mudar no futuro)
//
// Notas:
// - Se o teu hardware inverter a lógica do RTS (raro), define EVSE_LINK_RTS_INVERT=1
#include "evse_link_framing.h"
#include "driver/uart.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
static const char *TAG = "evse_framing";
@@ -10,36 +31,60 @@ static SemaphoreHandle_t tx_mutex = NULL;
static uint8_t seq = 0;
static evse_link_frame_cb_t rx_cb = NULL;
// CRC-8 (polynomial 0x07)
static uint8_t crc8(const uint8_t *data, uint8_t len)
static bool s_framing_inited = false;
// ---- Tunables (fallbacks) ----
#ifndef EVSE_LINK_INTERBYTE_TIMEOUT_US
// Timeout inter-byte: se um frame morrer a meio, reseta o parser
#define EVSE_LINK_INTERBYTE_TIMEOUT_US 5000
#endif
// Rate-limit para warnings (em microsegundos)
#define LOG_RATELIMIT_US 1000000 // 1s
// RX tuning (evita acumular ~120 bytes antes de "acordar")
#ifndef EVSE_LINK_RX_TIMEOUT
// Timeout do UART TOUT feature (em "character times"). 3..10 funciona bem.
#define EVSE_LINK_RX_TIMEOUT 3
#endif
#ifndef EVSE_LINK_RX_FULL_THRESH
// Gera interrupção quando FIFO tem pelo menos N bytes (1..120). 4 é um bom default.
#define EVSE_LINK_RX_FULL_THRESH 1
#endif
#ifndef EVSE_LINK_RTS_INVERT
// Se precisares inverter RTS (muito raro), define para 1 no build.
#define EVSE_LINK_RTS_INVERT 0
#endif
static inline bool log_ratelimit_ok(int64_t *last_us, int64_t interval_us)
{
uint8_t crc = 0;
for (uint8_t i = 0; i < len; ++i) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; ++b) {
if (crc & 0x80) {
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 {
else
crc <<= 1;
}
}
}
return crc;
}
void evse_link_framing_init(void)
static esp_err_t configure_uart(void)
{
// Mutex para proteger TX (framings de várias tasks)
tx_mutex = xSemaphoreCreateMutex();
// Instala driver UART
uart_driver_install(UART_PORT,
UART_RX_BUF_SIZE * 2, // RX buffer
0, // TX buffer (0 = usa buffer interno)
0,
NULL,
0);
uart_config_t cfg = {
.baud_rate = UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
@@ -48,80 +93,199 @@ void evse_link_framing_init(void)
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
uart_param_config(UART_PORT, &cfg);
// Define pinos: TX, RX e RTS (RTS ligado a DE+RE do transceiver RS485)
uart_set_pin(UART_PORT,
TX_PIN, // MB_UART_TXD (ex: GPIO17)
RX_PIN, // MB_UART_RXD (ex: GPIO16)
RTS_PIN, // MB_UART_RTS (ex: GPIO2, DE+RE)
UART_PIN_NO_CHANGE);
esp_err_t err = uart_param_config(UART_PORT, &cfg);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
return err;
}
// Modo RS485 half-duplex: driver controla RTS/DE/RE automaticamente
uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX);
// TX/RX/RTS na UART (RTS controla DE//RE em RS485 half-duplex)
err = uart_set_pin(UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "Framing init: UART%d TX=%d RX=%d RTS(DE/RE)=%d baud=%d",
// RS-485 HALF DUPLEX (driver controla RTS automaticamente)
err = uart_set_mode(UART_PORT, EVSE_LINK_UART_MODE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_mode(%s) failed: %s",
EVSE_LINK_PHY_NAME,
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_USE_UART_TTL && 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,
"%s driver enabled: UART%d TX=%d RX=%d RTS=%d baud=%d rx_to=%d rx_thresh=%d",
EVSE_LINK_PHY_NAME,
UART_PORT,
TX_PIN,
RX_PIN,
RTS_PIN,
UART_BAUDRATE,
EVSE_LINK_RX_TIMEOUT,
EVSE_LINK_RX_FULL_THRESH);
return ESP_OK;
}
void evse_link_framing_init(void)
{
if (s_framing_inited)
{
ESP_LOGI(TAG, "Framing already initialized");
return;
}
if (!tx_mutex)
{
tx_mutex = xSemaphoreCreateMutex();
if (!tx_mutex)
{
ESP_LOGE(TAG, "Failed to create TX mutex");
return;
}
}
// Se o driver já estiver instalado, só reconfigura e aplica RX tuning.
if (uart_is_driver_installed(UART_PORT))
{
esp_err_t err = configure_uart();
if (err == ESP_OK)
{
s_framing_inited = true;
ESP_LOGW(TAG, "UART%d driver already installed -> configured for RS485 HALF DUPLEX", UART_PORT);
(void)uart_flush_input(UART_PORT);
}
else
{
ESP_LOGE(TAG, "Failed to configure already-installed UART%d", UART_PORT);
}
return;
}
// Instala driver UART
esp_err_t err = uart_driver_install(UART_PORT,
UART_RX_BUF_SIZE * 2, // RX buffer (ringbuffer)
0, // TX buffer (não usado)
0, // event queue size
NULL, // event queue
0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return;
}
err = configure_uart();
if (err != ESP_OK)
return;
(void)uart_flush_input(UART_PORT);
s_framing_inited = true;
ESP_LOGI(TAG, "Framing init (RS485 driver): UART%d TX=%d RX=%d RTS=%d baud=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE);
}
bool evse_link_framing_send(uint8_t dest, uint8_t src,
const uint8_t *payload, uint8_t len)
{
if (len > EVSE_LINK_MAX_PAYLOAD) {
ESP_LOGW(TAG, "Payload too large: %u (max=%u)",
len, EVSE_LINK_MAX_PAYLOAD);
if (!s_framing_inited)
{
ESP_LOGE(TAG, "Framing not initialized");
return false;
}
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) {
if (len > EVSE_LINK_MAX_PAYLOAD)
{
ESP_LOGW(TAG, "Payload too large: %u (max=%u)", len, EVSE_LINK_MAX_PAYLOAD);
return false;
}
if (len > 0 && payload == NULL)
{
ESP_LOGW(TAG, "Invalid send: len=%u but payload=NULL", len);
return false;
}
if (!tx_mutex)
{
ESP_LOGE(TAG, "TX mutex is NULL (framing_init not called?)");
return false;
}
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE)
{
ESP_LOGW(TAG, "Failed to take TX mutex");
return false;
}
// Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END
// LEN on wire = SEQ + PAYLOAD (>=1)
uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7];
int idx = 0;
frame[idx++] = MAGIC_START;
frame[idx++] = dest;
frame[idx++] = src;
frame[idx++] = (uint8_t)(len + 1); // LEN = SEQ + payload
frame[idx++] = (uint8_t)(len + 1);
frame[idx++] = seq;
if (len > 0 && payload != NULL) {
if (len > 0)
{
memcpy(&frame[idx], payload, len);
idx += len;
}
// CRC cobre: DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD];
memcpy(crc_input, &frame[1], 3 + 1 + len);
uint8_t crc = crc8(crc_input, (uint8_t)(3 + 1 + len));
frame[idx++] = crc;
// CRC: DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc = 0;
crc = crc8_update(crc, dest);
crc = crc8_update(crc, src);
crc = crc8_update(crc, (uint8_t)(len + 1));
crc = crc8_update(crc, seq);
for (uint8_t i = 0; i < len; ++i)
crc = crc8_update(crc, payload[i]);
frame[idx++] = crc;
frame[idx++] = MAGIC_END;
// Envia frame completo
int written = uart_write_bytes(UART_PORT, (const char *)frame, idx);
if (written != idx) {
if (written != idx)
{
ESP_LOGW(TAG, "uart_write_bytes wrote %d of %d", written, idx);
}
// Aguarda TX terminar (o driver controla DE/RE via RTS)
uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(20));
// Aguarda TX terminar; driver RS485 devolve RTS para RX automaticamente.
(void)uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(700));
xSemaphoreGive(tx_mutex);
ESP_LOGI(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u",
ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u",
dest, src, len, seq);
seq++; // incrementa sequência após envio
seq++;
return true;
}
void evse_link_framing_recv_byte(uint8_t b)
{
// Máquina de estados para parsing do frame
static enum {
ST_WAIT_START = 0,
ST_WAIT_DEST,
@@ -135,15 +299,44 @@ void evse_link_framing_recv_byte(uint8_t b)
static uint8_t rx_dest;
static uint8_t rx_src;
static uint8_t rx_len; // inclui SEQ + payload
static uint8_t rx_len; // inclui SEQ + payload (>=1)
static uint8_t rx_seq;
static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD];
static uint8_t rx_pos;
static uint8_t rx_crc;
switch (rx_state) {
static int64_t s_last_byte_us = 0;
static int64_t s_last_bad_len_log_us = 0;
static int64_t s_last_bad_crc_log_us = 0;
#define RESET_PARSER() \
do \
{ \
rx_state = ST_WAIT_START; \
rx_dest = 0; \
rx_src = 0; \
rx_len = 0; \
rx_seq = 0; \
rx_pos = 0; \
rx_crc = 0; \
} while (0)
const int64_t now_us = esp_timer_get_time();
// Timeout inter-byte: frame morreu a meio -> reseta
if (rx_state != ST_WAIT_START && s_last_byte_us != 0 &&
(now_us - s_last_byte_us) > EVSE_LINK_INTERBYTE_TIMEOUT_US)
{
RESET_PARSER();
}
s_last_byte_us = now_us;
switch (rx_state)
{
case ST_WAIT_START:
if (b == MAGIC_START) {
if (b == MAGIC_START)
{
rx_pos = 0;
rx_state = ST_WAIT_DEST;
}
break;
@@ -159,28 +352,50 @@ void evse_link_framing_recv_byte(uint8_t b)
break;
case ST_WAIT_LEN:
rx_len = b; // LEN = SEQ + payload
rx_len = b;
// rx_len = SEQ + payload => >=1 e <= MAX+1
if (rx_len < 1 || rx_len > (uint8_t)(EVSE_LINK_MAX_PAYLOAD + 1))
{
if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "Invalid LEN=%u (max=%u), dropping frame",
rx_len, (unsigned)(EVSE_LINK_MAX_PAYLOAD + 1));
}
RESET_PARSER();
break;
}
rx_pos = 0;
rx_state = ST_WAIT_SEQ;
break;
case ST_WAIT_SEQ:
rx_seq = b;
if (rx_len > 1) {
rx_state = ST_READING;
} else {
rx_state = ST_WAIT_CRC;
}
rx_state = (rx_len > 1) ? ST_READING : ST_WAIT_CRC;
break;
case ST_READING:
if (rx_pos < EVSE_LINK_MAX_PAYLOAD) {
rx_buf[rx_pos++] = b;
}
if (rx_pos >= (uint8_t)(rx_len - 1)) { // payload completo
rx_state = ST_WAIT_CRC;
{
const uint8_t payload_len = (uint8_t)(rx_len - 1);
if (payload_len > EVSE_LINK_MAX_PAYLOAD)
{
if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "Payload len too big: %u", (unsigned)payload_len);
}
RESET_PARSER();
break;
}
if (rx_pos < EVSE_LINK_MAX_PAYLOAD)
rx_buf[rx_pos++] = b;
if (rx_pos >= payload_len)
rx_state = ST_WAIT_CRC;
break;
}
case ST_WAIT_CRC:
rx_crc = b;
@@ -188,41 +403,44 @@ void evse_link_framing_recv_byte(uint8_t b)
break;
case ST_WAIT_END:
if (b == MAGIC_END) {
// Monta buffer para verificar CRC:
// DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t temp[3 + 1 + EVSE_LINK_MAX_PAYLOAD];
int temp_len = 0;
temp[temp_len++] = rx_dest;
temp[temp_len++] = rx_src;
temp[temp_len++] = rx_len;
temp[temp_len++] = rx_seq;
if (rx_len > 1) {
memcpy(&temp[temp_len], rx_buf, rx_len - 1);
temp_len += rx_len - 1;
}
if (b == MAGIC_END)
{
uint8_t expected = 0;
expected = crc8_update(expected, rx_dest);
expected = crc8_update(expected, rx_src);
expected = crc8_update(expected, rx_len);
expected = crc8_update(expected, rx_seq);
uint8_t expected = crc8(temp, (uint8_t)temp_len);
if (expected == rx_crc) {
uint8_t payload_len = (uint8_t)(rx_len - 1); // exclui SEQ
if (rx_cb) {
const uint8_t payload_len = (uint8_t)(rx_len - 1);
for (uint8_t i = 0; i < payload_len; ++i)
expected = crc8_update(expected, rx_buf[i]);
if (expected == rx_crc)
{
if (rx_cb)
rx_cb(rx_src, rx_dest, rx_buf, payload_len);
}
ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u",
rx_src, rx_dest, payload_len, rx_seq);
} else {
}
else
{
if (log_ratelimit_ok(&s_last_bad_crc_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X",
expected, rx_crc);
}
}
// Em qualquer caso, volta a esperar novo frame
rx_state = ST_WAIT_START;
}
RESET_PARSER();
break;
default:
rx_state = ST_WAIT_START;
RESET_PARSER();
break;
}
#undef RESET_PARSER
}
void evse_link_framing_register_cb(evse_link_frame_cb_t cb)

View File

@@ -1,10 +1,27 @@
// components/evse_link/src/evse_link_master.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX (ACK deferido para task -> não bloqueia RX)
// 2) Dedupe de ACK por slave (evita rajadas se RX vier em chunks / queue acumular)
// 3) Log quando ACK queue enche (antes era silencioso)
// 4) Proteção concorrente no s_presence (int64_t não é atómico no ESP32 32-bit)
// 5) Comentário do poll corrigido + opção de jitter no ACK
//
// NOTA: Mantive o teu comportamento (POLL a cada 10s). Se quiseres, muda para 30s.
#include "evse_link.h"
#include "evse_link_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "esp_random.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
@@ -14,37 +31,180 @@
static const char *TAG = "evse_link_master";
// Link commands
// Link commands (opcode no payload[0])
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização a slave
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A
// payload lengths (exclui byte de opcode)
#define LEN_POLL_REQ 1 // [ CMD_POLL ]
#define LEN_POLL_RESP 9 // [ CMD_POLL, float V(4), float I(4) ]
// payload lengths (INCLUI opcode)
#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, ... ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, amps_lo, amps_hi ]
// Presence monitoring
#define PRESENCE_CHECK_MS 5000
#define SLAVE_OFFLINE_TIMEOUT_MS 180000
// ACK defer
#define ACK_QUEUE_LEN 16
// backoff para espaçar ACK (evita burst)
// antes era 0..15ms; aqui fica ligeiramente mais suave.
#define ACK_BACKOFF_MIN_MS 5
#define ACK_BACKOFF_MAX_MS 35 // jitter 5..35ms
typedef struct
{
int64_t last_seen_us;
bool online;
} slave_presence_t;
static slave_presence_t s_presence[256];
static TimerHandle_t s_presence_timer = NULL;
// Proteção concorrente (int64_t não é atómico no ESP32)
static portMUX_TYPE s_presence_mux = portMUX_INITIALIZER_UNLOCKED;
// polling / heartbeat timers interval
typedef struct
{
TimerHandle_t timer;
TickType_t interval;
} timer_def_t;
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)};
static timer_def_t hb_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)};
// POLL a cada 60s (comentário corrigido)
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(60000)};
static bool s_handlers_registered = false;
// ACK task/queue
static QueueHandle_t s_ack_q = NULL;
static TaskHandle_t s_ack_task = NULL;
// Dedupe: 1 ACK pendente por slave
static bool s_ack_pending[256] = {0};
static void post_presence_event(evse_link_event_t evt, uint8_t slave_id)
{
evse_link_slave_presence_event_t p = {.slave_id = slave_id};
(void)esp_event_post(EVSE_LINK_EVENTS, evt, &p, sizeof(p), portMAX_DELAY);
}
static void mark_slave_seen(uint8_t slave_id)
{
const int64_t now = esp_timer_get_time();
bool was_offline = false;
portENTER_CRITICAL(&s_presence_mux);
slave_presence_t *p = &s_presence[slave_id];
p->last_seen_us = now;
if (!p->online)
{
p->online = true;
was_offline = true;
}
portEXIT_CRITICAL(&s_presence_mux);
if (was_offline)
{
ESP_LOGI(TAG, "Slave 0x%02X ONLINE", slave_id);
post_presence_event(LINK_EVENT_SLAVE_ONLINE, slave_id);
}
}
static void presence_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
const int64_t now = esp_timer_get_time();
const int64_t timeout_us = (int64_t)SLAVE_OFFLINE_TIMEOUT_MS * 1000;
const uint8_t self = evse_link_get_self_id();
for (int i = 0; i < 256; ++i)
{
if ((uint8_t)i == self)
continue;
bool online;
int64_t last_seen;
portENTER_CRITICAL(&s_presence_mux);
online = s_presence[i].online;
last_seen = s_presence[i].last_seen_us;
portEXIT_CRITICAL(&s_presence_mux);
if (!online)
continue;
if (last_seen > 0 && (now - last_seen) > timeout_us)
{
portENTER_CRITICAL(&s_presence_mux);
s_presence[i].online = false;
portEXIT_CRITICAL(&s_presence_mux);
ESP_LOGW(TAG, "Slave 0x%02X OFFLINE (no heartbeat for %d ms)", i, SLAVE_OFFLINE_TIMEOUT_MS);
post_presence_event(LINK_EVENT_SLAVE_OFFLINE, (uint8_t)i);
}
}
}
// Enfileira ACK sem duplicar por slave
static void enqueue_ack(uint8_t slave_id)
{
if (!s_ack_q)
return;
// Dedupe: se já existe ACK pendente para este slave, não enfileira outro
if (s_ack_pending[slave_id])
return;
s_ack_pending[slave_id] = true;
if (xQueueSendToBack(s_ack_q, &slave_id, 0) != pdTRUE)
{
s_ack_pending[slave_id] = false;
ESP_LOGW(TAG, "ACK queue full, dropping ACK for 0x%02X", slave_id);
}
}
// --- ACK task (não bloqueia RX) ---
static void ack_task(void *arg)
{
(void)arg;
for (;;)
{
uint8_t slave_id = 0;
if (xQueueReceive(s_ack_q, &slave_id, portMAX_DELAY) != pdTRUE)
continue;
// libera dedupe
s_ack_pending[slave_id] = false;
// backoff com jitter
uint32_t backoff = ACK_BACKOFF_MIN_MS +
(esp_random() % (ACK_BACKOFF_MAX_MS - ACK_BACKOFF_MIN_MS + 1));
vTaskDelay(pdMS_TO_TICKS(backoff));
uint8_t ack[] = {CMD_HEARTBEAT_ACK};
bool ok = evse_link_send(slave_id, ack, sizeof(ack));
ESP_LOGI(TAG, "CMD_HEARTBEAT_ACK to 0x%02X ok=%d", slave_id, ok);
}
}
// --- Send new limit to slave ---
static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT)
(void)arg;
(void)base;
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT || data == NULL)
return;
const loadbalancer_slave_limit_event_t *evt = data;
const loadbalancer_slave_limit_event_t *evt = (const loadbalancer_slave_limit_event_t *)data;
uint8_t slave_id = evt->slave_id;
uint16_t max_current = evt->max_current;
@@ -52,156 +212,179 @@ static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *dat
CMD_SET_CURRENT,
(uint8_t)(max_current & 0xFF),
(uint8_t)(max_current >> 8)};
evse_link_send(slave_id, buf, sizeof(buf));
ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, max_current);
(void)evse_link_send(slave_id, buf, sizeof(buf));
ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, (unsigned)max_current);
}
// --- Bridge AUTH -> EVSE-Link: enviar AUTH_GRANTED para slaves ---
// --- Bridge AUTH -> EVSE-Link ---
static void on_auth_result(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL) {
(void)arg;
if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL)
return;
}
const auth_tag_event_data_t *ev = (const auth_tag_event_data_t *)data;
if (!ev->authorized) {
if (!ev->authorized)
{
ESP_LOGI(TAG, "Tag %s not authorized, not propagating to slaves", ev->tag);
return;
}
// Construir payload: [ CMD_AUTH_GRANTED, tag..., '\0' ]
uint8_t buf[1 + EVSE_LINK_TAG_MAX_LEN];
buf[0] = CMD_AUTH_GRANTED;
// Copiar tag e garantir NUL
// Copia tag e garante NUL
strncpy((char *)&buf[1], ev->tag, EVSE_LINK_TAG_MAX_LEN - 1);
((char *)&buf[1])[EVSE_LINK_TAG_MAX_LEN - 1] = '\0';
uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1); // opcode + tag + '\0'
// Payload inclui opcode + string + NUL
uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1);
// Neste exemplo: broadcast para todos os slaves (0xFF)
uint8_t dest = 0xFF;
if (!evse_link_send(dest, buf, payload_len)) {
ESP_LOGW(TAG, "Failed to send CMD_AUTH_GRANTED to dest=0x%02X for tag=%s",
dest, (char *)&buf[1]);
} else {
ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED to dest=0x%02X for tag=%s",
dest, (char *)&buf[1]);
}
(void)evse_link_send(0xFF, buf, payload_len);
ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED (broadcast) tag=%s", (char *)&buf[1]);
}
// --- Polling broadcast callback ---
static void poll_timer_cb(TimerHandle_t xTimer)
{
ESP_LOGD(TAG, "Broadcasting CMD_POLL to all slaves");
;
// Optionally post event LINK_EVENT_MASTER_POLL_SENT
}
(void)xTimer;
// --- Heartbeat timeout callback ---
static void hb_timer_cb(TimerHandle_t xTimer)
{
ESP_LOGW(TAG, "Heartbeat timeout: possible slave offline");
// post event LINK_EVENT_SLAVE_OFFLINE ???
uint8_t poll[] = {CMD_POLL};
bool ok = evse_link_send(0xFF, poll, sizeof(poll));
ESP_LOGI(TAG, "POLL send ok=%d", ok);
}
static void on_frame_master(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len)
{
if (len < 1)
const uint8_t self = evse_link_get_self_id();
// ignora eco do próprio master e frames que não são para nós nem broadcast
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
return;
uint8_t cmd = payload[0];
switch (cmd)
{
case CMD_HEARTBEAT:
{
ESP_LOGD(TAG, "HEARTBEAT from 0x%02X: %u bytes", src, len);
if (len != LEN_HEARTBEAT)
{ // CMD + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi
{
ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len);
return;
}
bool charging = payload[1] != 0;
uint16_t hw_max = payload[2] | (payload[3] << 8);
uint16_t runtime = payload[4] | (payload[5] << 8);
ESP_LOGI(TAG, "Heartbeat from 0x%02X: charging=%d hw_max=%uA runtime=%uA",
src, charging, hw_max, runtime);
bool charging = payload[1] != 0;
uint16_t hw_max = (uint16_t)(payload[2] | ((uint16_t)payload[3] << 8));
uint16_t runtime = (uint16_t)(payload[4] | ((uint16_t)payload[5] << 8));
mark_slave_seen(src);
loadbalancer_slave_status_event_t status = {
.slave_id = src,
.charging = charging,
.hw_max_current = (float)hw_max,
.runtime_current = (float)runtime, // corrente real medida no slave
.runtime_current = (float)runtime,
.timestamp_us = esp_timer_get_time()};
esp_event_post(LOADBALANCER_EVENTS,
(void)esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_STATUS,
&status, sizeof(status), portMAX_DELAY);
// Enviar ACK de volta
uint8_t ack[] = {CMD_HEARTBEAT_ACK};
evse_link_send(src, ack, sizeof(ack));
ESP_LOGD(TAG, "Sent HEARTBEAT_ACK to 0x%02X", src);
// ACK deferido e deduplicado
enqueue_ack(src);
break;
}
case CMD_POLL:
ESP_LOGD(TAG, "Received POLL_RESP from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST:
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA",
src, payload[1]);
if (len >= LEN_CONFIG_BROADCAST)
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", src, payload[1]);
else
ESP_LOGW(TAG, "CONFIG_BROADCAST ack short len=%u from 0x%02X", len, src);
break;
default:
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
ESP_LOGD(TAG, "Cmd 0x%02X from 0x%02X (ignored/unknown)", cmd, src);
break;
}
}
// --- Master initialization ---
void evse_link_master_init(void)
{
if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled())
{
return;
}
ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_master);
// register loadbalancer event
ESP_ERROR_CHECK(
esp_event_handler_register(
// Cria queue/task de ACK uma vez
if (s_ack_q == NULL)
{
s_ack_q = xQueueCreate(ACK_QUEUE_LEN, sizeof(uint8_t));
if (!s_ack_q)
{
ESP_LOGE(TAG, "Failed to create ACK queue");
}
else
{
if (xTaskCreate(ack_task, "evse_ack", 4096, NULL, 4, &s_ack_task) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create ACK task");
vQueueDelete(s_ack_q);
s_ack_q = NULL;
s_ack_task = NULL;
}
}
}
if (!s_handlers_registered)
{
s_handlers_registered = true;
ESP_ERROR_CHECK(esp_event_handler_register(
LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
on_new_limit,
NULL));
// escutar resultado do AUTH para propagar autorização aos slaves
ESP_ERROR_CHECK(
esp_event_handler_register(
ESP_ERROR_CHECK(esp_event_handler_register(
AUTH_EVENTS,
AUTH_EVENT_TAG_PROCESSED,
on_auth_result,
NULL));
}
// create and start poll timer
if (poll_timer.timer == NULL)
{
poll_timer.timer = xTimerCreate("poll_tmr",
poll_timer.interval,
pdTRUE, NULL,
poll_timer_cb);
xTimerStart(poll_timer.timer, 0);
// create and start heartbeat monitor timer
hb_timer.timer = xTimerCreate("hb_tmr",
hb_timer.interval,
pdFALSE, NULL,
hb_timer_cb);
xTimerStart(hb_timer.timer, 0);
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,199 +1,441 @@
// === 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) SET_CURRENT é delegado ao EVSE_Manager via LINK_EVENT_CURRENT_LIMIT_APPLIED.
// O evse_link_slave não mexe diretamente em autorização/runtime current.
//
// Nota: A enum do evse_state_event_data_t que enviaste está correta para o handler.
#include "evse_link.h"
#include "evse_link_events.h"
#include "loadbalancer_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/portmacro.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_random.h"
#include "esp_err.h"
#include "evse_events.h"
#include "evse_state.h"
#include "evse_config.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
static const char *TAG = "evse_link_slave";
// Link commands
#define MASTER_ID 0x01
// Commands (opcode no payload[0])
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 // not used by slave
#define CMD_HEARTBEAT 0x02
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização
#define CMD_AUTH_GRANTED 0x0A
// #define CMD_RESUME 0x0B // se existir
// payload lengths (exclui seq byte)
#define LEN_POLL_REQ 1 // [ CMD_POLL ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1 // [ CMD_HEARTBEAT_ACK ]
#define LEN_HEARTBEAT 6 // CMD_HEARTBEAT + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi
// lengths (INCLUI opcode)
#define LEN_SET_CURRENT 3
// Timing
#define FALLBACK_TIMEOUT_MS 120000
#define FALLBACK_TIMEOUT_MS 180000
// --- Política de fallback ---
// 1 = mais seguro: PAUSE (revoga autorização)
// 0 = força corrente mínima (pode continuar a carregar dependendo do core)
#ifndef EVSE_LINK_FALLBACK_PAUSE
#define EVSE_LINK_FALLBACK_PAUSE 1
#endif
// --- Confirmações via heartbeat (deferidas) ---
#define HB_REQ_QUEUE_LEN 8
typedef enum
{
HB_REQ_SEND = 1,
} hb_req_t;
static TimerHandle_t fallback_timer = NULL;
static bool safe_mode = false;
static TaskHandle_t hb_task_handle = NULL;
// Task para enviar heartbeat sem bloquear RX callback
static TaskHandle_t hb_sender_task_handle = NULL;
static QueueHandle_t hb_req_q = NULL;
// "safe mode" (master offline): aplica fallback local e nao sai com POLL/ACK.
// Só sai com comando explícito de potência (SET_CURRENT / RESUME).
static bool safe_mode = false;
static uint16_t saved_runtime_limit = 0; // informativo
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)
{
evse_state_t st = evse_get_state();
/*
* Para o master/loadbalancer, este campo significa "há pedido/demanda
* ativa deste conector", não apenas "relé ligado".
*
* C2/D2 => charging real
* C1/D1 => EV está a pedir carga, mas o relé está OFF por pausa/limite
*
* Isto permite retomar depois de SET_CURRENT=0: em C1 o slave continua
* elegível para o loadbalancer e o master pode voltar a enviar >0A.
*/
bool demand = evse_state_is_charging(st) || evse_state_is_requesting(st);
// --- Helper to send a heartbeat frame ---
static void send_heartbeat_frame(void) {
bool charging = evse_state_is_charging(evse_get_state());
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA",
charging, hw_max, runtime);
uint8_t hb[] = {
CMD_HEARTBEAT,
charging ? 1 : 0,
demand ? 1 : 0,
(uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8),
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)
};
// Broadcast to master (0xFF)
evse_link_send(0xFF, hb, sizeof(hb));
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)};
(void)evse_link_send(MASTER_ID, hb, sizeof(hb)); // UNICAST
ESP_LOGI(TAG, "Send Heartbeat Frame");
}
// pede heartbeat sem bloquear quem chama (RX callback, event handler, etc.)
static void request_heartbeat_send(void)
{
if (hb_req_q)
{
hb_req_t req = HB_REQ_SEND;
// não bloqueia; se encher, apenas ignora (heartbeat periódico já existe)
(void)xQueueSendToBack(hb_req_q, &req, 0);
}
else
{
// fallback: se queue ainda não existe, manda direto
send_heartbeat_frame_now();
}
}
// --- EVSE state change handler ---
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) {
if (base!=EVSE_EVENTS || id!=EVSE_EVENT_STATE_CHANGED || data==NULL) return;
const evse_state_event_data_t *evt = data;
if (evt->state==EVSE_STATE_EVENT_IDLE || evt->state==EVSE_STATE_EVENT_CHARGING) {
send_heartbeat_frame();
static void 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;
// 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 on_frame_slave(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) {
if (dest != evse_link_get_self_id() && dest != 0xFF) return;
if (len < 1) return;
const uint8_t *payload, uint8_t len)
{
const uint8_t self = evse_link_get_self_id();
// Muito verboso em caminho quente; deixa em DEBUG
ESP_LOGD(TAG, "RX frames (src=0x%02X dest=0x%02X len=%u self=0x%02X)", src, dest, len, self);
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
{
ESP_LOGW(TAG, "RX invalid: payload NULL or len<1 (len=%u)", len);
return;
}
// Só aceitar comandos do master
if (src != MASTER_ID)
{
ESP_LOGW(TAG, "RX ignore: non-master src=0x%02X", src);
return;
}
// Qualquer frame válido do master => link vivo (reset do fallback)
if (fallback_timer)
(void)xTimerReset(fallback_timer, 0);
uint8_t cmd = payload[0];
switch (cmd) {
switch (cmd)
{
case CMD_POLL:
ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src);
// Liveness only. Não sai de safe-mode e não restaura limites.
ESP_LOGI(TAG, "CMD_POLL from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST:
ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src);
ESP_LOGI(TAG, "CMD_CONFIG_BROADCAST from 0x%02X", src);
break;
case CMD_SET_CURRENT: {
if (len < LEN_SET_CURRENT) {
ESP_LOGW(TAG, "SET_CURRENT from 0x%02X with invalid length %u", src, len);
case CMD_HEARTBEAT_ACK:
ESP_LOGI(TAG, "HEARTBEAT_ACK from 0x%02X", src);
break;
case CMD_SET_CURRENT:
{
ESP_LOGI(TAG, "SET_CURRENT from 0x%02X", src);
if (len < LEN_SET_CURRENT)
{
ESP_LOGW(TAG, "SET_CURRENT invalid len=%u from 0x%02X", len, src);
break;
}
uint16_t amps = payload[1] | (payload[2] << 8);
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,
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)
{
ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE requested by master");
request_heartbeat_send();
esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps,
sizeof(amps),
portMAX_DELAY);
break;
}
// confirma sem bloquear RX
request_heartbeat_send();
ESP_LOGI(TAG, "Forwarding 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_HEARTBEAT_ACK:
ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src);
if (fallback_timer) {
xTimerReset(fallback_timer, 0);
if (safe_mode) {
safe_mode = false;
uint16_t current = evse_get_runtime_charging_current();
evse_set_runtime_charging_current(current);
ESP_LOGI(TAG, "Exiting safe mode, restoring %uA", current);
}
}
break;
case CMD_AUTH_GRANTED: {
if (len < 2) {
ESP_LOGW(TAG, "CMD_AUTH_GRANTED from 0x%02X with invalid length %u", src, len);
case CMD_AUTH_GRANTED:
{
if (len < 2)
{
ESP_LOGW(TAG, "AUTH_GRANTED invalid len=%u from 0x%02X", len, src);
break;
}
const char *tag = (const char *)&payload[1];
const uint8_t *tag_ptr = &payload[1];
size_t tag_buf_len = (size_t)(len - 1);
size_t tag_len = bounded_strlen_u8(tag_ptr, tag_buf_len);
evse_link_auth_grant_event_t ev = {0};
strncpy(ev.tag, tag, EVSE_LINK_TAG_MAX_LEN - 1);
ev.tag[EVSE_LINK_TAG_MAX_LEN - 1] = '\0';
size_t copy_len = tag_len;
if (copy_len > (EVSE_LINK_TAG_MAX_LEN - 1))
copy_len = EVSE_LINK_TAG_MAX_LEN - 1;
ESP_LOGI(TAG, "Received CMD_AUTH_GRANTED from master 0x%02X, tag='%s'", src, ev.tag);
if (copy_len > 0)
memcpy(ev.tag, tag_ptr, copy_len);
ev.tag[copy_len] = '\0';
esp_err_t err = esp_event_post(
EVSE_LINK_EVENTS,
// 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),
&ev, sizeof(ev),
portMAX_DELAY);
if (err != ESP_OK) {
if (err != ESP_OK)
ESP_LOGE(TAG, "Failed to post LINK_EVENT_REMOTE_AUTH_GRANTED: %s", esp_err_to_name(err));
}
break;
}
default:
ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src);
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
break;
}
}
static void slave_heartbeat_task(void *arg)
{
(void)arg;
const uint32_t period_ms = 60000;
uint8_t id = evse_link_get_self_id();
// --- Periodic heartbeat task ---
static void slave_heartbeat_task(void *arg) {
const TickType_t interval = pdMS_TO_TICKS(10000);
for (;;) {
send_heartbeat_frame();
vTaskDelay(interval);
// desfasamento por ID: 2s, 4s, 6s...
vTaskDelay(pdMS_TO_TICKS((uint32_t)id * 2000));
for (;;)
{
request_heartbeat_send();
// jitter opcional
uint32_t jitter_ms = esp_random() % 201; // 0..200ms
vTaskDelay(pdMS_TO_TICKS(period_ms + jitter_ms));
}
}
// --- Fallback safe mode callback ---
static void fallback_timer_cb(TimerHandle_t xTimer) {
if (!safe_mode) {
static void fallback_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
// entra safe_mode uma vez
bool already_safe;
portENTER_CRITICAL(&s_state_mux);
already_safe = safe_mode;
if (!safe_mode)
safe_mode = true;
ESP_LOGW(TAG, "Fallback timeout: entering safe mode");
portEXIT_CRITICAL(&s_state_mux);
if (already_safe)
return;
saved_runtime_limit = evse_get_runtime_charging_current();
#if EVSE_LINK_FALLBACK_PAUSE
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA). Policy=PAUSE",
(unsigned)saved_runtime_limit);
// Pausar é mais seguro quando o master falha.
// O EVSE_Manager é o dono da autorização/pausa/retoma.
uint16_t zero_amps = 0;
(void)esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_CURRENT_LIMIT_APPLIED,
&zero_amps,
sizeof(zero_amps),
portMAX_DELAY);
#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);
esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_SLAVE_OFFLINE,
#endif
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE,
NULL, 0, portMAX_DELAY);
}
// opcional: manda heartbeat para indicar estado atual
request_heartbeat_send();
}
// --- Slave initialization ---
void evse_link_slave_init(void) {
if (evse_link_get_mode()!=EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) return;
void evse_link_slave_init(void)
{
if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled())
return;
ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_slave);
// start periodic heartbeat
xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 5, NULL);
// cria queue/task do sender (para não mandar UART TX no callback RX)
if (hb_req_q == NULL)
{
hb_req_q = xQueueCreate(HB_REQ_QUEUE_LEN, sizeof(hb_req_t));
if (!hb_req_q)
{
ESP_LOGE(TAG, "Failed to create HB request queue (fallback to direct send)");
}
else
{
if (xTaskCreate(hb_sender_task, "hb_sender", 3072, NULL, 3, &hb_sender_task_handle) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create hb_sender task");
vQueueDelete(hb_req_q);
hb_req_q = NULL;
hb_sender_task_handle = NULL;
}
}
}
// fallback timer
if (hb_task_handle == NULL)
{
if (xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 3, &hb_task_handle) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create slave_heartbeat_task");
hb_task_handle = NULL;
}
}
if (fallback_timer == NULL)
{
fallback_timer = xTimerCreate("fallback_tmr",
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS),
pdFALSE, NULL,
fallback_timer_cb);
if (fallback_timer) {
xTimerStart(fallback_timer, 0);
if (fallback_timer)
(void)xTimerStart(fallback_timer, 0);
else
ESP_LOGE(TAG, "Failed to create fallback timer");
}
else
{
(void)xTimerReset(fallback_timer, 0);
}
// react to EVSE state changes
ESP_ERROR_CHECK(
esp_event_handler_register(
EVSE_EVENTS,
EVSE_EVENT_STATE_CHANGED,
evse_event_handler,
NULL
)
);
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;
}
}
// === Fim de: components/evse_link/src/evse_link_slave.c ===

View File

@@ -0,0 +1,10 @@
set(srcs
"src/hmi_link.c"
)
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS "include"
REQUIRES evse
PRIV_REQUIRES driver esp_timer json
)

View File

@@ -0,0 +1,26 @@
#ifndef HMI_LINK_H
#define HMI_LINK_H
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Start the local HMI UART link.
*
* Default pins:
* EVSE GPIO22 = TX -> LCD RX
* EVSE GPIO21 = RX <- LCD TX
* Baudrate = 115200 8N1
*
* The link uses one JSON object per line ('\n').
*/
esp_err_t hmi_link_init(void);
#ifdef __cplusplus
}
#endif
#endif // HMI_LINK_H

View File

@@ -0,0 +1,406 @@
#include "hmi_link.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cJSON.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "evse_api.h"
#include "evse_config.h"
#include "evse_error.h"
#include "evse_meter.h"
#include "evse_session.h"
#include "evse_state.h"
#ifndef HMI_LINK_UART_PORT
#define HMI_LINK_UART_PORT UART_NUM_1
#endif
#ifndef HMI_LINK_UART_TX_GPIO
#define HMI_LINK_UART_TX_GPIO GPIO_NUM_22
#endif
#ifndef HMI_LINK_UART_RX_GPIO
#define HMI_LINK_UART_RX_GPIO GPIO_NUM_21
#endif
#ifndef HMI_LINK_UART_BAUDRATE
#define HMI_LINK_UART_BAUDRATE 115200
#endif
#ifndef HMI_LINK_STATUS_PERIOD_MS
#define HMI_LINK_STATUS_PERIOD_MS 1000
#endif
#define HMI_LINK_RX_BUF_SIZE 1024
#define HMI_LINK_TX_BUF_SIZE 1024
#define HMI_LINK_LINE_MAX 512
#define HMI_LINK_JSON_MAX 896
#define HMI_LINK_TASK_STACK 4096
#define HMI_LINK_TASK_PRIO 5
static const char *TAG = "hmi_link";
static TaskHandle_t s_task = NULL;
static uint32_t s_seq = 0;
static void hmi_link_write_line(const char *line)
{
if (!line)
return;
uart_write_bytes(HMI_LINK_UART_PORT, line, strlen(line));
uart_write_bytes(HMI_LINK_UART_PORT, "\n", 1);
}
static void hmi_link_send_ack(const char *cmd, bool ok, esp_err_t err)
{
char out[192];
snprintf(out, sizeof(out),
"{\"type\":\"ack\",\"cmd\":\"%s\",\"ok\":%s,\"err\":\"%s\"}",
cmd ? cmd : "",
ok ? "true" : "false",
ok ? "OK" : esp_err_to_name(err));
hmi_link_write_line(out);
}
static void hmi_link_send_error(const char *reason)
{
char out[192];
snprintf(out, sizeof(out),
"{\"type\":\"error\",\"reason\":\"%s\"}",
reason ? reason : "unknown");
hmi_link_write_line(out);
}
static bool json_bool_value(const cJSON *root, const char *name, bool *out)
{
const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, name);
if (cJSON_IsBool(item))
{
*out = cJSON_IsTrue(item);
return true;
}
return false;
}
static bool json_u16_value_any(const cJSON *root, uint16_t *out, const char *a, const char *b, const char *c)
{
const char *names[3] = {a, b, c};
for (size_t i = 0; i < 3; ++i)
{
if (!names[i])
continue;
const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, names[i]);
if (cJSON_IsNumber(item) && item->valuedouble >= 0.0 && item->valuedouble <= 65535.0)
{
*out = (uint16_t)item->valueint;
return true;
}
}
return false;
}
static void hmi_link_send_status(void)
{
int power[EVSE_METER_PHASE_COUNT] = {0};
float voltage[EVSE_METER_PHASE_COUNT] = {0};
float current[EVSE_METER_PHASE_COUNT] = {0};
evse_session_t session = {0};
bool has_session = evse_get_session(&session);
evse_meter_get_power(power);
evse_meter_get_voltage(voltage);
evse_meter_get_current(current);
evse_state_t state = evse_get_state();
uint32_t error_bits = evse_get_error();
uint64_t uptime_ms = (uint64_t)(esp_timer_get_time() / 1000ULL);
char out[HMI_LINK_JSON_MAX];
snprintf(out, sizeof(out),
"{"
"\"type\":\"status\","
"\"seq\":%lu,"
"\"uptimeMs\":%llu,"
"\"state\":\"%s\","
"\"plugged\":%s,"
"\"charging\":%s,"
"\"enabled\":%s,"
"\"available\":%s,"
"\"authorized\":%s,"
"\"limitA\":%u,"
"\"runtimeA\":%u,"
"\"maxA\":%u,"
"\"tempLimitC\":%u,"
"\"powerW\":%d,"
"\"energyWh\":%d,"
"\"error\":%lu,"
"\"v\":[%.1f,%.1f,%.1f],"
"\"i\":[%.3f,%.3f,%.3f],"
"\"p\":[%d,%d,%d],"
"\"session\":{\"present\":%s,\"current\":%s,\"id\":%lu,\"durationS\":%lu,\"energyWh\":%lu,\"avgPowerW\":%lu}"
"}",
(unsigned long)++s_seq,
(unsigned long long)uptime_ms,
evse_state_to_str(state),
evse_state_is_plugged(state) ? "true" : "false",
evse_state_is_charging(state) ? "true" : "false",
evse_config_is_enabled() ? "true" : "false",
evse_config_is_available() ? "true" : "false",
evse_state_get_authorized() ? "true" : "false",
(unsigned)evse_get_charging_current(),
(unsigned)evse_get_runtime_charging_current(),
(unsigned)evse_get_max_charging_current(),
(unsigned)evse_get_temp_threshold(),
evse_meter_get_instant_power(),
evse_meter_get_total_energy(),
(unsigned long)error_bits,
voltage[0], voltage[1], voltage[2],
current[0], current[1], current[2],
power[0], power[1], power[2],
has_session ? "true" : "false",
(has_session && session.is_current) ? "true" : "false",
has_session ? (unsigned long)session.session_id : 0UL,
has_session ? (unsigned long)session.duration_s : 0UL,
has_session ? (unsigned long)session.energy_wh : 0UL,
has_session ? (unsigned long)session.avg_power_w : 0UL);
hmi_link_write_line(out);
}
static void hmi_link_handle_command(const char *line)
{
cJSON *root = cJSON_Parse(line);
if (!root)
{
hmi_link_send_error("invalid_json");
return;
}
const cJSON *cmd_item = cJSON_GetObjectItemCaseSensitive(root, "cmd");
if (!cJSON_IsString(cmd_item) || cmd_item->valuestring == NULL)
{
cJSON_Delete(root);
hmi_link_send_error("missing_cmd");
return;
}
const char *cmd = cmd_item->valuestring;
if (strcmp(cmd, "get") == 0)
{
hmi_link_send_ack(cmd, true, ESP_OK);
hmi_link_send_status();
}
else if (strcmp(cmd, "ping") == 0)
{
char out[128];
snprintf(out, sizeof(out), "{\"type\":\"pong\",\"uptimeMs\":%llu}",
(unsigned long long)(esp_timer_get_time() / 1000ULL));
hmi_link_write_line(out);
}
else if (strcmp(cmd, "set_current") == 0)
{
uint16_t amps = 0;
if (!json_u16_value_any(root, &amps, "a", "currentA", "limitA"))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
esp_err_t err = evse_set_charging_current(amps);
hmi_link_send_ack(cmd, err == ESP_OK, err);
if (err == ESP_OK)
hmi_link_send_status();
}
}
else if (strcmp(cmd, "set_temp_threshold") == 0)
{
uint16_t temp_c = 0;
if (!json_u16_value_any(root, &temp_c, "c", "tempC", "temperatureLimit"))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
esp_err_t err = evse_set_temp_threshold((uint8_t)temp_c);
hmi_link_send_ack(cmd, err == ESP_OK, err);
if (err == ESP_OK)
hmi_link_send_status();
}
}
else if (strcmp(cmd, "set_enabled") == 0)
{
bool enabled = false;
if (!json_bool_value(root, "enabled", &enabled))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
evse_set_enabled(enabled);
hmi_link_send_ack(cmd, true, ESP_OK);
hmi_link_send_status();
}
}
else if (strcmp(cmd, "set_available") == 0)
{
bool available = false;
if (!json_bool_value(root, "available", &available))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
evse_set_available(available);
hmi_link_send_ack(cmd, true, ESP_OK);
hmi_link_send_status();
}
}
else
{
hmi_link_send_ack(cmd, false, ESP_ERR_NOT_SUPPORTED);
}
cJSON_Delete(root);
}
static void hmi_link_task(void *arg)
{
(void)arg;
char line[HMI_LINK_LINE_MAX];
size_t line_len = 0;
uint8_t rx[128];
TickType_t last_status = 0;
hmi_link_write_line("{\"type\":\"hello\",\"name\":\"hmi_link\",\"version\":1}");
hmi_link_send_status();
last_status = xTaskGetTickCount();
while (true)
{
int n = uart_read_bytes(HMI_LINK_UART_PORT, rx, sizeof(rx), pdMS_TO_TICKS(100));
for (int i = 0; i < n; ++i)
{
uint8_t ch = rx[i];
if (ch == '\r')
continue;
if (ch == '\n')
{
if (line_len > 0)
{
line[line_len] = '\0';
hmi_link_handle_command(line);
line_len = 0;
}
continue;
}
if (line_len < (sizeof(line) - 1))
{
line[line_len++] = (char)ch;
}
else
{
line_len = 0;
hmi_link_send_error("line_too_long");
}
}
TickType_t now = xTaskGetTickCount();
if ((now - last_status) >= pdMS_TO_TICKS(HMI_LINK_STATUS_PERIOD_MS))
{
hmi_link_send_status();
last_status = now;
}
}
}
esp_err_t hmi_link_init(void)
{
if (s_task != NULL)
{
ESP_LOGW(TAG, "Already started");
return ESP_OK;
}
const uart_config_t uart_config = {
.baud_rate = HMI_LINK_UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_driver_install(HMI_LINK_UART_PORT,
HMI_LINK_RX_BUF_SIZE,
HMI_LINK_TX_BUF_SIZE,
0,
NULL,
0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return err;
}
err = uart_param_config(HMI_LINK_UART_PORT, &uart_config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
uart_driver_delete(HMI_LINK_UART_PORT);
return err;
}
err = uart_set_pin(HMI_LINK_UART_PORT,
HMI_LINK_UART_TX_GPIO,
HMI_LINK_UART_RX_GPIO,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
uart_driver_delete(HMI_LINK_UART_PORT);
return err;
}
BaseType_t ok = xTaskCreate(hmi_link_task,
"hmi_link",
HMI_LINK_TASK_STACK,
NULL,
HMI_LINK_TASK_PRIO,
&s_task);
if (ok != pdPASS)
{
ESP_LOGE(TAG, "xTaskCreate failed");
s_task = NULL;
uart_driver_delete(HMI_LINK_UART_PORT);
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG,
"Started: UART%d, TX GPIO%d, RX GPIO%d, %d baud",
(int)HMI_LINK_UART_PORT,
(int)HMI_LINK_UART_TX_GPIO,
(int)HMI_LINK_UART_RX_GPIO,
(int)HMI_LINK_UART_BAUDRATE);
return ESP_OK;
}

0
components/led/CMakeLists.txt Executable file → Normal file
View File

0
components/led/include/led.h Executable file → Normal file
View File

0
components/led/include/ledc_driver.h Executable file → Normal file
View File

6
components/led/src/led.c Executable file → Normal file
View File

@@ -13,7 +13,7 @@
#include "evse_state.h"
#include "ledc_driver.h"
#define BLOCK_TIME pdMS_TO_TICKS(10)
#define BLOCK_TIME portMAX_DELAY
static const char *TAG = "led";
@@ -350,7 +350,7 @@ static void evse_led_event_handler(void *arg, esp_event_base_t base, int32_t id,
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
ESP_LOGI(TAG, "EVSE State Changed: state=%d", evt->state);
ESP_LOGD(TAG, "EVSE State Changed: state=%d", evt->state);
// Atualiza o estado base
current_state_mode = evt->state;
@@ -378,7 +378,7 @@ static void evse_session_led_event_handler(void *arg, esp_event_base_t base, int
const evse_session_event_data_t *evt =
(const evse_session_event_data_t *)data;
ESP_LOGI(TAG,
ESP_LOGD(TAG,
"EVSE Session Event: type=%d, id=%" PRIu32
", duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg=%" PRIu32 " W, current=%d",
(int)evt->type,

0
components/led/src/ledc_driver.c Executable file → Normal file
View File

3
components/loadbalancer/CMakeLists.txt Executable file → Normal file
View File

@@ -1,8 +1,7 @@
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}"
INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash
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_ */

0
components/loadbalancer/include/input_filter.h Executable file → Normal file
View File

55
components/loadbalancer/include/loadbalancer.h Executable file → Normal file
View File

@@ -9,35 +9,46 @@ extern "C" {
#include <stdint.h>
#include "esp_err.h"
/**
* @brief Inicializa o módulo de load balancer
* @brief Operational current limiting mode.
*
* This is owned by the loadbalancer/policy layer. MQTT/REST should only
* request a mode change; they should not persist or re-apply their own mode.
*/
typedef enum
{
LOADBALANCER_LIMIT_MODE_OFF = 0,
LOADBALANCER_LIMIT_MODE_MANUAL,
LOADBALANCER_LIMIT_MODE_GRID,
LOADBALANCER_LIMIT_MODE_SOLAR,
LOADBALANCER_LIMIT_MODE_GRID_SOLAR
} loadbalancer_limit_mode_t;
void loadbalancer_init(void);
/**
* @brief Task contínua do algoritmo de balanceamento
*/
void loadbalancer_task(void *param);
/**
* @brief Ativa ou desativa o load balancing
*/
void loadbalancer_set_enabled(bool value);
/**
* @brief Verifica se o load balancing está ativo
*/
void loadbalancer_set_enabled(bool enabled);
bool loadbalancer_is_enabled(void);
/**
* @brief Define a corrente máxima do grid
*/
esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current);
// Operational limit mode
esp_err_t loadbalancer_set_limit_mode(loadbalancer_limit_mode_t mode);
loadbalancer_limit_mode_t loadbalancer_get_limit_mode(void);
const char *loadbalancer_limit_mode_to_str(loadbalancer_limit_mode_t mode);
bool loadbalancer_limit_mode_from_str(const char *str, loadbalancer_limit_mode_t *out);
/**
* @brief Obtém a corrente máxima do grid
*/
// GRID limit (A)
void loadbalancer_grid_set_enabled(bool en);
bool loadbalancer_grid_is_enabled(void);
esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a);
uint8_t loadbalancer_grid_get_max_import_a(void);
// PV optimizer (W)
void loadbalancer_pv_set_enabled(bool en);
bool loadbalancer_pv_is_enabled(void);
esp_err_t loadbalancer_pv_set_max_import_w(int32_t w);
int32_t loadbalancer_pv_get_max_import_w(void);
// Aliases legacy (se quiseres manter chamadas antigas)
esp_err_t load_balancing_set_max_grid_current(uint8_t value);
uint8_t load_balancing_get_max_grid_current(void);
#ifdef __cplusplus

View File

@@ -1,47 +1,70 @@
// components/loadbalancer/include/loadbalancer_events.h
#pragma once
#include "esp_event.h"
#include <stdint.h>
#include <stdbool.h>
#include "esp_timer.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS);
typedef enum {
LOADBALANCER_EVENT_INIT,
typedef enum
{
LOADBALANCER_EVENT_INIT = 0,
LOADBALANCER_EVENT_STATE_CHANGED,
LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT,
/*
* Current limit events.
*
* MASTER and SLAVE use different payloads intentionally.
*/
LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
/*
* Status received from slave connectors.
*/
LOADBALANCER_EVENT_SLAVE_STATUS
} loadbalancer_event_id_t;
typedef struct {
typedef struct
{
bool enabled;
int64_t timestamp_us;
} loadbalancer_state_event_t;
// (opcional)
typedef struct {
float limit;
int64_t timestamp_us;
} loadbalancer_global_limit_event_t;
typedef struct {
uint8_t slave_id;
/*
* MASTER: no slave_id.
*/
typedef struct
{
uint16_t max_current;
int64_t timestamp_us;
} loadbalancer_master_limit_event_t;
typedef struct {
/*
* SLAVE: includes slave_id.
*/
typedef struct
{
uint8_t slave_id;
uint16_t max_current;
int64_t timestamp_us;
} loadbalancer_slave_limit_event_t;
typedef struct {
uint8_t slave_id; // ID do slave que reportou
bool charging; // Status de carregamento
float hw_max_current; // Limite máximo de corrente do hardware informado
float runtime_current; // Corrente atual de carregamento (A)
int64_t timestamp_us; // Momento em que o status foi coletado
typedef struct
{
uint8_t slave_id;
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} loadbalancer_slave_status_event_t;
#ifdef __cplusplus
}
#endif

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;
}

0
components/loadbalancer/src/input_filter.c Executable file → Normal file
View File

1634
components/loadbalancer/src/loadbalancer.c Executable file → Normal file

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;
}

49
components/meter_manager/CMakeLists.txt Executable file → Normal file
View File

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

View File

View File

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.watt, data->watt, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
}

View File

View File

@@ -0,0 +1,671 @@
// components/meter_manager/driver/meter_modbus/meter_dds661.c
// Driver Modbus RTU para DDS661.
// Suporta 1 ou 2 DDS661 no mesmo bus RS485:
// GRID -> slave ID 1
// EVSE -> slave ID 2
// Usa um único Modbus master e uma única task de leitura.
#include "meter_dds661.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "meter_events.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stddef.h>
#include <string.h>
#include <math.h>
#define TAG "serial_mdb_dds661"
// ======= UART/Modbus config =======
#define MB_PORT_NUM 2
#define MB_DEV_SPEED 9600
// Ajuste os pinos conforme seu hardware (evite GPIO2 para RTS/DE/RE se possível)
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// ======= DDS661 no mesmo bus =======
#define DDS661_GRID_SLAVE_ID 1
#define DDS661_EVSE_SLAVE_ID 2
#define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (120 / portTICK_PERIOD_MS)
#define DDS661_LOCK_TIMEOUT pdMS_TO_TICKS(10000)
// ======= Helpers típicos do teu projeto =======
#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1))
#define STR(x) ((const char *)(x))
#define OPTS(min, max, step) {.opt1 = min, .opt2 = max, .opt3 = step}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// ======= Estado =======
static bool is_initialized = false;
static volatile bool task_should_run = false;
static TaskHandle_t meter_task = NULL;
static SemaphoreHandle_t dds661_lock = NULL;
// ======= CIDs locais por meter =======
enum
{
CID_VOLTAGE = 0,
CID_CURRENT,
CID_ACTIVE_POWER_KW,
CID_POWER_FACTOR,
CID_FREQUENCY,
CID_TOTAL_ACTIVE_ENERGY_KWH,
CID_COUNT
};
typedef enum
{
DDS661_SLOT_GRID = 0,
DDS661_SLOT_EVSE,
DDS661_MAX_INSTANCES
} dds661_slot_t;
typedef struct
{
bool registered;
const char *source; // "GRID" ou "EVSE"
uint8_t slave_id; // 1 ou 2
uint16_t cid_base; // atribuído dinamicamente em dds661_rebuild_descriptors_locked()
} dds661_instance_t;
static dds661_instance_t dds661_instances[DDS661_MAX_INSTANCES] = {
[DDS661_SLOT_GRID] = {
.registered = false,
.source = "GRID",
.slave_id = DDS661_GRID_SLAVE_ID,
.cid_base = 0,
},
[DDS661_SLOT_EVSE] = {
.registered = false,
.source = "EVSE",
.slave_id = DDS661_EVSE_SLAVE_ID,
.cid_base = 0,
},
};
// ======= Mapa de registradores (Input Registers; FC=0x04) =======
// Endereços típicos para DDS-661 (float32):
#define REG_VOLTAGE 0x0000 // V (float32)
#define REG_CURRENT 0x0008 // A (float32)
#define REG_ACTIVE_POWER_KW 0x0012 // kW (float32)
#define REG_POWER_FACTOR 0x002A // PF (float32)
#define REG_FREQUENCY 0x0036 // Hz (float32)
#define REG_E_ACTIVE_KWH 0x0100 // kWh (float32)
// ======= Template de parâmetros para um DDS661 =======
static const mb_parameter_descriptor_t dds661_param_template[CID_COUNT] = {
{CID_VOLTAGE, "Voltage", "V", 1,
MB_PARAM_INPUT, REG_VOLTAGE, 2, HOLD_OFFSET(l1_voltage),
PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0, 300, 0.1), PAR_PERMS_READ},
{CID_CURRENT, "Current", "A", 1,
MB_PARAM_INPUT, REG_CURRENT, 2, HOLD_OFFSET(l1_current),
PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0, 100, 0.1), PAR_PERMS_READ},
{CID_ACTIVE_POWER_KW, "Active Power", "kW", 1,
MB_PARAM_INPUT, REG_ACTIVE_POWER_KW, 2, HOLD_OFFSET(active_power),
PARAM_TYPE_FLOAT_CDAB, 4, OPTS(-100, 100, 0.01), PAR_PERMS_READ},
{CID_POWER_FACTOR, "Power Factor", "", 1,
MB_PARAM_INPUT, REG_POWER_FACTOR, 2, HOLD_OFFSET(power_factor),
PARAM_TYPE_FLOAT_CDAB, 4, OPTS(-1, 1, 0.001), PAR_PERMS_READ},
{CID_FREQUENCY, "Frequency", "Hz", 1,
MB_PARAM_INPUT, REG_FREQUENCY, 2, HOLD_OFFSET(frequency),
PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0, 100, 0.1), PAR_PERMS_READ},
{CID_TOTAL_ACTIVE_ENERGY_KWH, "Total Active Energy", "kWh", 1,
MB_PARAM_INPUT, REG_E_ACTIVE_KWH, 2, HOLD_OFFSET(active_energy),
PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0, 1000000, 0.01), PAR_PERMS_READ},
};
// O esp-modbus exige cid e param_key únicos no Data Dictionary.
// Como GRID e EVSE usam o mesmo mapa de registos, o param_key precisa de prefixo por instância.
static const char *const dds661_param_keys[DDS661_MAX_INSTANCES][CID_COUNT] = {
[DDS661_SLOT_GRID] = {
"GRID Voltage",
"GRID Current",
"GRID Active Power",
"GRID Power Factor",
"GRID Frequency",
"GRID Total Active Energy",
},
[DDS661_SLOT_EVSE] = {
"EVSE Voltage",
"EVSE Current",
"EVSE Active Power",
"EVSE Power Factor",
"EVSE Frequency",
"EVSE Total Active Energy",
},
};
static mb_parameter_descriptor_t device_parameters_dds661[DDS661_MAX_INSTANCES * CID_COUNT];
static uint16_t num_device_parameters_dds661 = 0;
static bool dds661_descriptor_dirty = true;
// ======= Ponteiro para buffer destino =======
static void *get_param_ptr(const mb_parameter_descriptor_t *param)
{
if (!param || param->param_offset == 0)
return NULL;
return ((uint8_t *)&holding_reg_params + param->param_offset - 1);
}
static bool dds661_has_registered_instance_locked(void)
{
for (uint8_t i = 0; i < DDS661_MAX_INSTANCES; ++i)
{
if (dds661_instances[i].registered)
return true;
}
return false;
}
static esp_err_t dds661_rebuild_descriptors_locked(void)
{
num_device_parameters_dds661 = 0;
dds661_descriptor_dirty = true;
for (uint8_t inst_idx = 0; inst_idx < DDS661_MAX_INSTANCES; ++inst_idx)
{
dds661_instance_t *inst = &dds661_instances[inst_idx];
if (!inst->registered)
continue;
// Mantém os CIDs contíguos no descriptor ativo.
// Isto evita problemas em versões do esp-modbus que tratam CID como índice.
inst->cid_base = num_device_parameters_dds661;
for (uint16_t local_cid = 0; local_cid < CID_COUNT; ++local_cid)
{
mb_parameter_descriptor_t *dst = &device_parameters_dds661[num_device_parameters_dds661++];
*dst = dds661_param_template[local_cid];
dst->cid = inst->cid_base + local_cid;
dst->param_key = dds661_param_keys[inst_idx][local_cid];
dst->mb_slave_addr = inst->slave_id;
}
}
dds661_descriptor_dirty = true;
ESP_LOGI(TAG, "DDS661 descriptor table prepared: %u parameters", num_device_parameters_dds661);
return ESP_OK;
}
static esp_err_t dds661_apply_descriptors_locked(void)
{
if (num_device_parameters_dds661 == 0)
return ESP_ERR_INVALID_STATE;
if (!dds661_descriptor_dirty)
return ESP_OK;
esp_err_t err = mbc_master_set_descriptor(device_parameters_dds661, num_device_parameters_dds661);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", esp_err_to_name(err));
return err;
}
dds661_descriptor_dirty = false;
ESP_LOGI(TAG, "DDS661 descriptor table applied: %u parameters", num_device_parameters_dds661);
return ESP_OK;
}
static esp_err_t dds661_master_init_once(void)
{
if (is_initialized)
return ESP_OK;
if (!dds661_lock)
{
dds661_lock = xSemaphoreCreateMutex();
if (!dds661_lock)
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_dds661 Modbus master init");
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_EVEN, // DDS-661: 9600 8E1
};
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;
}
// Pinos e parâmetros básicos
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;
}
err = uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// IMPORTANTE: start antes do set_mode
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// Só agora muda para RS485 half duplex
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// Logs de debug Modbus. Reduz para ESP_LOG_INFO/ESP_LOG_WARN em produção se necessário.
esp_log_level_set("MB_CONTROLLER_MASTER", ESP_LOG_DEBUG);
esp_log_level_set("MB_PORT_COMMON", ESP_LOG_DEBUG);
esp_log_level_set("MB_SERIAL_MASTER", ESP_LOG_DEBUG);
vTaskDelay(pdMS_TO_TICKS(5));
is_initialized = true;
return ESP_OK;
}
static esp_err_t dds661_register_instance(dds661_slot_t slot)
{
if (slot >= DDS661_MAX_INSTANCES)
return ESP_ERR_INVALID_ARG;
esp_err_t err = dds661_master_init_once();
if (err != ESP_OK)
return err;
if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
dds661_instances[slot].registered = true;
ESP_LOGI(TAG, "DDS661 %s registered on Modbus slave ID %u",
dds661_instances[slot].source,
dds661_instances[slot].slave_id);
err = dds661_rebuild_descriptors_locked();
xSemaphoreGive(dds661_lock);
return err;
}
static void dds661_shutdown_if_idle(void)
{
bool any_registered = false;
if (dds661_lock && xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) == pdTRUE)
{
any_registered = dds661_has_registered_instance_locked();
xSemaphoreGive(dds661_lock);
}
if (any_registered || !is_initialized)
return;
ESP_LOGI(TAG, "No DDS661 instances registered; stopping shared Modbus master");
task_should_run = false;
for (int i = 0; i < 20 && meter_task != NULL; ++i)
{
vTaskDelay(pdMS_TO_TICKS(10));
}
if (meter_task != NULL)
{
ESP_LOGW(TAG, "DDS661 task did not exit in time; deleting it");
vTaskDelete(meter_task);
meter_task = NULL;
}
esp_err_t err = mbc_master_destroy();
if (err != ESP_OK)
ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err));
if (uart_is_driver_installed(MB_PORT_NUM))
uart_driver_delete(MB_PORT_NUM);
is_initialized = false;
num_device_parameters_dds661 = 0;
dds661_descriptor_dirty = true;
}
static void dds661_unregister_instance(dds661_slot_t slot)
{
if (slot >= DDS661_MAX_INSTANCES)
return;
if (!is_initialized || !dds661_lock)
return;
if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) == pdTRUE)
{
ESP_LOGI(TAG, "DDS661 %s unregistered", dds661_instances[slot].source);
dds661_instances[slot].registered = false;
(void)dds661_rebuild_descriptors_locked();
xSemaphoreGive(dds661_lock);
}
dds661_shutdown_if_idle();
}
static void dds661_read_instance_locked(const dds661_instance_t *inst)
{
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
float v = 0.0f; // V
float i = 0.0f; // A
float pf = 0.0f; // -
float hz = 0.0f; // Hz
float e_kwh = 0.0f; // kWh
float p_kw = 0.0f; // kW
float voltage[3] = {0};
float current[3] = {0};
int32_t watt[3] = {0};
bool got_any_value = false;
for (uint16_t local_cid = 0; local_cid < CID_COUNT; local_cid++)
{
const uint16_t cid = inst->cid_base + local_cid;
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", inst->source, cid, esp_err_to_name(err));
continue;
}
void *data_ptr = get_param_ptr(desc);
if (!data_ptr)
{
ESP_LOGE(TAG, "%s CID %u (%s): null data_ptr", inst->source, cid, desc->param_key);
continue;
}
uint8_t type = 0;
err = mbc_master_get_parameter(cid, (char *)desc->param_key, (uint8_t *)data_ptr, &type);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
inst->source, cid, desc->param_key, esp_err_to_name(err));
vTaskDelay(POLL_INTERVAL);
continue;
}
uint8_t raw[4];
memcpy(raw, data_ptr, sizeof(raw));
ESP_LOGD(TAG, "%s CID %u (%s) raw bytes: %02X %02X %02X %02X",
inst->source, cid, desc->param_key, raw[0], raw[1], raw[2], raw[3]);
const float val = *(float *)data_ptr;
got_any_value = true;
ESP_LOGD(TAG, "%s %s: %.3f %s", inst->source, desc->param_key, val, desc->param_units);
switch (local_cid)
{
case CID_VOLTAGE:
v = val;
voltage[0] = v;
break;
case CID_CURRENT:
i = val;
current[0] = i;
break;
case CID_POWER_FACTOR:
pf = val;
break;
case CID_FREQUENCY:
hz = val;
break;
case CID_ACTIVE_POWER_KW:
{
p_kw = val;
/*
* DDS661 bidirecional:
* p_w > 0 => importação
* p_w < 0 => exportação
*
* Alguns modelos devolvem W apesar do registo estar nomeado como kW.
* Outros podem devolver kW.
*
* Não usar abs() no valor final, apenas na deteção de escala.
*/
float p_w = p_kw;
const float apparent_w = fabsf(v * i);
if (apparent_w > 1.0f && fabsf(p_w) < (apparent_w * 0.2f))
{
// Exemplo: -0.723 kW -> -723 W
p_w *= 1000.0f;
}
watt[0] = (int32_t)lrintf(p_w);
watt[1] = 0;
watt[2] = 0;
break;
}
case CID_TOTAL_ACTIVE_ENERGY_KWH:
e_kwh = val;
break;
default:
break;
}
vTaskDelay(POLL_INTERVAL);
}
if (!got_any_value)
{
ESP_LOGW(TAG, "%s no valid DDS661 values read in this cycle", inst->source);
return;
}
meter_event_data_t evt = {
.source = inst->source,
.frequency = hz,
.power_factor = pf,
.total_energy = e_kwh,
.watt_total = watt[0] + watt[1] + watt[2],
.timestamp_us = esp_timer_get_time(),
};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
const int32_t p_total_w = watt[0] + watt[1] + watt[2];
ESP_LOGI(TAG,
"%s DDS661 event: V=%.1fV I=%.3fA P=%ldW E=%.3fkWh Hz=%.2f PF=%.3f",
inst->source,
voltage[0],
current[0],
(long)p_total_w,
e_kwh,
hz,
pf);
esp_err_t post_err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
if (post_err != ESP_OK)
ESP_LOGW(TAG, "%s failed to post meter event: %s", inst->source, esp_err_to_name(post_err));
}
// ======= Tarefa de aquisição =======
static void serial_mdb_task(void *param)
{
(void)param;
while (task_should_run)
{
if (!is_initialized || !dds661_lock)
{
vTaskDelay(UPDATE_INTERVAL);
continue;
}
bool had_instance = false;
for (uint8_t slot = 0; slot < DDS661_MAX_INSTANCES && task_should_run; ++slot)
{
if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) != pdTRUE)
{
ESP_LOGW(TAG, "DDS661 task timeout waiting lock");
continue;
}
dds661_instance_t inst = dds661_instances[slot];
if (inst.registered)
{
had_instance = true;
dds661_read_instance_locked(&inst);
}
xSemaphoreGive(dds661_lock);
}
vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500));
}
ESP_LOGI(TAG, "DDS661 task stopped");
meter_task = NULL;
vTaskDelete(NULL);
}
// ======= API pública =======
// Compatibilidade: chamada antiga inicializa DDS661 como GRID/ID 1.
esp_err_t meter_dds661_init(void)
{
return meter_dds661_grid_init();
}
esp_err_t meter_dds661_grid_init(void)
{
return dds661_register_instance(DDS661_SLOT_GRID);
}
esp_err_t meter_dds661_evse_init(void)
{
return dds661_register_instance(DDS661_SLOT_EVSE);
}
esp_err_t meter_dds661_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "meter_dds661 not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!dds661_lock)
return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
const bool has_instance = dds661_has_registered_instance_locked();
esp_err_t desc_err = ESP_OK;
if (has_instance)
desc_err = dds661_apply_descriptors_locked();
xSemaphoreGive(dds661_lock);
if (!has_instance)
{
ESP_LOGW(TAG, "meter_dds661 start ignored: no instances registered");
return ESP_ERR_INVALID_STATE;
}
if (desc_err != ESP_OK)
return desc_err;
if (meter_task == NULL)
{
task_should_run = true;
BaseType_t ok = xTaskCreate(serial_mdb_task, "meter_dds661_task", 4096, NULL, 3, &meter_task);
if (ok != pdPASS)
{
task_should_run = false;
meter_task = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_dds661 shared task started");
}
return ESP_OK;
}
esp_err_t meter_dds661_grid_start(void)
{
return meter_dds661_start();
}
esp_err_t meter_dds661_evse_start(void)
{
return meter_dds661_start();
}
void meter_dds661_stop(void)
{
meter_dds661_grid_stop();
}
void meter_dds661_grid_stop(void)
{
dds661_unregister_instance(DDS661_SLOT_GRID);
}
void meter_dds661_evse_stop(void)
{
dds661_unregister_instance(DDS661_SLOT_EVSE);
}

View File

@@ -0,0 +1,58 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
/**
* @brief Inicializa o driver DDS661 em modo compatível antigo: GRID / slave ID 1.
*/
esp_err_t meter_dds661_init(void);
/**
* @brief Regista DDS661 como meter GRID no slave ID 1.
*/
esp_err_t meter_dds661_grid_init(void);
/**
* @brief Regista DDS661 como meter EVSE no slave ID 2.
*/
esp_err_t meter_dds661_evse_init(void);
/**
* @brief Inicia a task partilhada de leitura DDS661.
*/
esp_err_t meter_dds661_start(void);
/**
* @brief Inicia a task partilhada de leitura DDS661 para GRID.
*/
esp_err_t meter_dds661_grid_start(void);
/**
* @brief Inicia a task partilhada de leitura DDS661 para EVSE.
*/
esp_err_t meter_dds661_evse_start(void);
/**
* @brief Para/remover DDS661 GRID em modo compatível antigo.
*/
void meter_dds661_stop(void);
/**
* @brief Remove DDS661 GRID; só destrói o Modbus master se não houver EVSE registado.
*/
void meter_dds661_grid_stop(void);
/**
* @brief Remove DDS661 EVSE; só destrói o Modbus master se não houver GRID registado.
*/
void meter_dds661_evse_stop(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,760 @@
// components/meter_manager/driver/meter_modbus/meter_dds665.c
// Driver Modbus RTU para DDS665.
// Suporta 1 ou 2 DDS665 no mesmo bus RS485:
// GRID -> slave ID 1
// EVSE -> slave ID 2
// Usa um único Modbus master e uma única task de leitura.
//
// Mapa DDS665 (FC=0x04, Input Registers, float32 IEEE-754):
// 0x0000 Voltage
// 0x0002 Current
// 0x0004 Active power (W)
// 0x0006 Power factor
// 0x0008 Total active energy (kWh)
// 0x000E Frequency (Hz)
#include "meter_dds665.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "meter_events.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#define TAG "serial_mdb_dds665"
// ======= UART/Modbus config =======
#define MB_PORT_NUM 2
#define MB_DEV_SPEED 9600
// Ajustar conforme o hardware. GPIO2 para RTS/DE/RE deve ser evitado quando possível.
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2
// ======= DDS665 no mesmo bus =======
#define DDS665_GRID_SLAVE_ID 1
#define DDS665_EVSE_SLAVE_ID 2
#define UPDATE_INTERVAL pdMS_TO_TICKS(3000)
#define POLL_INTERVAL pdMS_TO_TICKS(120)
#define DDS665_LOCK_TIMEOUT pdMS_TO_TICKS(10000)
/*
* O DDS665 transmite cada float como dois registos Modbus:
* exemplo 43 62 66 66 -> aproximadamente 226,4 V.
*
* Nesta versão do esp-modbus, PARAM_TYPE_FLOAT_CDAB é a transformação
* que converte corretamente essa ordem para o float nativo do ESP32.
* Validado em hardware com DDS665, 9600 8N1.
*/
#ifndef DDS665_FLOAT_PARAM_TYPE
#define DDS665_FLOAT_PARAM_TYPE PARAM_TYPE_FLOAT_CDAB
#endif
#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1U))
#define OPTS(minimum, maximum, step_value) \
{.opt1 = (minimum), .opt2 = (maximum), .opt3 = (step_value)}
// ======= Estado =======
static bool is_initialized = false;
static volatile bool task_should_run = false;
static TaskHandle_t meter_task = NULL;
static SemaphoreHandle_t dds665_lock = NULL;
// ======= CIDs locais por meter =======
enum
{
CID_VOLTAGE = 0,
CID_CURRENT,
CID_ACTIVE_POWER_W,
CID_POWER_FACTOR,
CID_FREQUENCY,
CID_TOTAL_ACTIVE_ENERGY_KWH,
CID_COUNT
};
#define DDS665_REQUIRED_MASK \
((1UL << CID_VOLTAGE) | \
(1UL << CID_CURRENT) | \
(1UL << CID_ACTIVE_POWER_W))
typedef enum
{
DDS665_SLOT_GRID = 0,
DDS665_SLOT_EVSE,
DDS665_MAX_INSTANCES
} dds665_slot_t;
typedef struct
{
bool registered;
const char *source; // "GRID" ou "EVSE"
uint8_t slave_id; // 1 ou 2
uint16_t cid_base; // atribuído em dds665_rebuild_descriptors_locked()
} dds665_instance_t;
static dds665_instance_t dds665_instances[DDS665_MAX_INSTANCES] = {
[DDS665_SLOT_GRID] = {
.registered = false,
.source = "GRID",
.slave_id = DDS665_GRID_SLAVE_ID,
.cid_base = 0,
},
[DDS665_SLOT_EVSE] = {
.registered = false,
.source = "EVSE",
.slave_id = DDS665_EVSE_SLAVE_ID,
.cid_base = 0,
},
};
// ======= Mapa de registradores DDS665 (Input Registers; FC=0x04) =======
#define REG_VOLTAGE 0x0000U // V, float32
#define REG_CURRENT 0x0002U // A, float32
#define REG_ACTIVE_POWER_W 0x0004U // W, float32
#define REG_POWER_FACTOR 0x0006U // cos(phi), float32
#define REG_E_ACTIVE_KWH 0x0008U // kWh, float32
#define REG_FREQUENCY 0x000EU // Hz, float32
// ======= Template de parâmetros para um DDS665 =======
static const mb_parameter_descriptor_t dds665_param_template[CID_COUNT] = {
{CID_VOLTAGE, "Voltage", "V", 1,
MB_PARAM_INPUT, REG_VOLTAGE, 2, HOLD_OFFSET(l1_voltage),
DDS665_FLOAT_PARAM_TYPE, 4, OPTS(0, 300, 0.1), PAR_PERMS_READ},
{CID_CURRENT, "Current", "A", 1,
MB_PARAM_INPUT, REG_CURRENT, 2, HOLD_OFFSET(l1_current),
DDS665_FLOAT_PARAM_TYPE, 4, OPTS(0, 100, 0.1), PAR_PERMS_READ},
{CID_ACTIVE_POWER_W, "Active Power", "W", 1,
MB_PARAM_INPUT, REG_ACTIVE_POWER_W, 2, HOLD_OFFSET(active_power),
DDS665_FLOAT_PARAM_TYPE, 4,
OPTS(-100000, 100000, 1),
PAR_PERMS_READ},
{CID_POWER_FACTOR, "Power Factor", "", 1,
MB_PARAM_INPUT, REG_POWER_FACTOR, 2, HOLD_OFFSET(power_factor),
DDS665_FLOAT_PARAM_TYPE, 4, OPTS(-1, 1, 0.001), PAR_PERMS_READ},
{CID_FREQUENCY, "Frequency", "Hz", 1,
MB_PARAM_INPUT, REG_FREQUENCY, 2, HOLD_OFFSET(frequency),
DDS665_FLOAT_PARAM_TYPE, 4, OPTS(0, 100, 0.1), PAR_PERMS_READ},
{CID_TOTAL_ACTIVE_ENERGY_KWH, "Total Active Energy", "kWh", 1,
MB_PARAM_INPUT, REG_E_ACTIVE_KWH, 2, HOLD_OFFSET(active_energy),
DDS665_FLOAT_PARAM_TYPE, 4, OPTS(0, 1000000, 0.01), PAR_PERMS_READ},
};
// O esp-modbus exige cid e param_key únicos no Data Dictionary.
static const char *const dds665_param_keys[DDS665_MAX_INSTANCES][CID_COUNT] = {
[DDS665_SLOT_GRID] = {
"GRID Voltage",
"GRID Current",
"GRID Active Power",
"GRID Power Factor",
"GRID Frequency",
"GRID Total Active Energy",
},
[DDS665_SLOT_EVSE] = {
"EVSE Voltage",
"EVSE Current",
"EVSE Active Power",
"EVSE Power Factor",
"EVSE Frequency",
"EVSE Total Active Energy",
},
};
static mb_parameter_descriptor_t device_parameters_dds665[DDS665_MAX_INSTANCES * CID_COUNT];
static uint16_t num_device_parameters_dds665 = 0;
static bool dds665_descriptor_dirty = true;
// ======= Ponteiro para buffer destino =======
static void *get_param_ptr(const mb_parameter_descriptor_t *param)
{
if (!param || param->param_offset == 0)
return NULL;
return ((uint8_t *)&holding_reg_params + param->param_offset - 1U);
}
static bool dds665_has_registered_instance_locked(void)
{
for (uint8_t i = 0; i < DDS665_MAX_INSTANCES; ++i)
{
if (dds665_instances[i].registered)
return true;
}
return false;
}
static esp_err_t dds665_rebuild_descriptors_locked(void)
{
num_device_parameters_dds665 = 0;
dds665_descriptor_dirty = true;
for (uint8_t inst_idx = 0; inst_idx < DDS665_MAX_INSTANCES; ++inst_idx)
{
dds665_instance_t *inst = &dds665_instances[inst_idx];
if (!inst->registered)
continue;
// Mantém os CIDs contíguos no descriptor ativo.
inst->cid_base = num_device_parameters_dds665;
for (uint16_t local_cid = 0; local_cid < CID_COUNT; ++local_cid)
{
mb_parameter_descriptor_t *dst =
&device_parameters_dds665[num_device_parameters_dds665++];
*dst = dds665_param_template[local_cid];
dst->cid = inst->cid_base + local_cid;
dst->param_key = dds665_param_keys[inst_idx][local_cid];
dst->mb_slave_addr = inst->slave_id;
}
}
ESP_LOGI(TAG, "DDS665 descriptor table prepared: %u parameters",
(unsigned)num_device_parameters_dds665);
return ESP_OK;
}
static esp_err_t dds665_apply_descriptors_locked(void)
{
if (num_device_parameters_dds665 == 0)
return ESP_ERR_INVALID_STATE;
if (!dds665_descriptor_dirty)
return ESP_OK;
esp_err_t err = mbc_master_set_descriptor(device_parameters_dds665,
num_device_parameters_dds665);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s",
esp_err_to_name(err));
return err;
}
dds665_descriptor_dirty = false;
ESP_LOGI(TAG, "DDS665 descriptor table applied: %u parameters",
(unsigned)num_device_parameters_dds665);
return ESP_OK;
}
static esp_err_t dds665_master_init_once(void)
{
if (is_initialized)
return ESP_OK;
if (!dds665_lock)
{
dds665_lock = xSemaphoreCreateMutex();
if (!dds665_lock)
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_dds665 Modbus master init");
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_DISABLE, // DDS665 testado: 9600 8N1
};
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;
}
err = uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// O controlador Modbus deve arrancar antes da mudança para half-duplex.
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
esp_log_level_set("MB_CONTROLLER_MASTER", ESP_LOG_DEBUG);
esp_log_level_set("MB_PORT_COMMON", ESP_LOG_DEBUG);
esp_log_level_set("MB_SERIAL_MASTER", ESP_LOG_DEBUG);
vTaskDelay(pdMS_TO_TICKS(5));
is_initialized = true;
return ESP_OK;
}
static esp_err_t dds665_register_instance(dds665_slot_t slot)
{
if (slot >= DDS665_MAX_INSTANCES)
return ESP_ERR_INVALID_ARG;
esp_err_t err = dds665_master_init_once();
if (err != ESP_OK)
return err;
if (xSemaphoreTake(dds665_lock, DDS665_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
if (dds665_instances[slot].registered)
{
xSemaphoreGive(dds665_lock);
return ESP_OK;
}
dds665_instances[slot].registered = true;
ESP_LOGI(TAG, "DDS665 %s registered on Modbus slave ID %u",
dds665_instances[slot].source,
(unsigned)dds665_instances[slot].slave_id);
err = dds665_rebuild_descriptors_locked();
xSemaphoreGive(dds665_lock);
return err;
}
static void dds665_shutdown_if_idle(void)
{
bool any_registered = false;
if (dds665_lock &&
xSemaphoreTake(dds665_lock, DDS665_LOCK_TIMEOUT) == pdTRUE)
{
any_registered = dds665_has_registered_instance_locked();
xSemaphoreGive(dds665_lock);
}
if (any_registered || !is_initialized)
return;
ESP_LOGI(TAG, "No DDS665 instances registered; stopping shared Modbus master");
task_should_run = false;
for (int i = 0; i < 20 && meter_task != NULL; ++i)
vTaskDelay(pdMS_TO_TICKS(10));
if (meter_task != NULL)
{
ESP_LOGW(TAG, "DDS665 task did not exit in time; deleting it");
vTaskDelete(meter_task);
meter_task = NULL;
}
esp_err_t err = mbc_master_destroy();
if (err != ESP_OK)
{
ESP_LOGW(TAG, "mbc_master_destroy() returned %s",
esp_err_to_name(err));
}
if (uart_is_driver_installed(MB_PORT_NUM))
uart_driver_delete(MB_PORT_NUM);
is_initialized = false;
num_device_parameters_dds665 = 0;
dds665_descriptor_dirty = true;
}
static void dds665_unregister_instance(dds665_slot_t slot)
{
if (slot >= DDS665_MAX_INSTANCES)
return;
if (!is_initialized || !dds665_lock)
return;
if (xSemaphoreTake(dds665_lock, DDS665_LOCK_TIMEOUT) == pdTRUE)
{
if (dds665_instances[slot].registered)
{
ESP_LOGI(TAG, "DDS665 %s unregistered",
dds665_instances[slot].source);
dds665_instances[slot].registered = false;
(void)dds665_rebuild_descriptors_locked();
// Se a outra instância continuar ativa, reaplica imediatamente
// a tabela compactada para manter cid_base e slave ID coerentes.
if (dds665_has_registered_instance_locked())
{
esp_err_t err = dds665_apply_descriptors_locked();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to reapply DDS665 descriptors: %s",
esp_err_to_name(err));
}
}
}
xSemaphoreGive(dds665_lock);
}
dds665_shutdown_if_idle();
}
static bool dds665_is_valid_value(uint16_t local_cid, float value)
{
if (!isfinite(value))
return false;
switch (local_cid)
{
case CID_VOLTAGE:
return value >= 0.0f && value <= 300.0f;
case CID_CURRENT:
return value >= 0.0f && value <= 100.0f;
case CID_ACTIVE_POWER_W:
return value >= -100000.0f && value <= 100000.0f;
case CID_POWER_FACTOR:
return value >= -1.1f && value <= 1.1f;
case CID_FREQUENCY:
return value >= 0.0f && value <= 100.0f;
case CID_TOTAL_ACTIVE_ENERGY_KWH:
return value >= 0.0f && value <= 1000000.0f;
default:
return false;
}
}
static void dds665_read_instance_locked(const dds665_instance_t *inst)
{
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
float voltage[3] = {0.0f, 0.0f, 0.0f};
float current[3] = {0.0f, 0.0f, 0.0f};
int32_t watt[3] = {0, 0, 0};
float pf = 0.0f;
float hz = 0.0f;
float e_kwh = 0.0f;
uint32_t valid_mask = 0U;
for (uint16_t local_cid = 0; local_cid < CID_COUNT; ++local_cid)
{
const uint16_t cid = inst->cid_base + local_cid;
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s",
inst->source, (unsigned)cid, esp_err_to_name(err));
continue;
}
void *data_ptr = get_param_ptr(desc);
if (!data_ptr)
{
ESP_LOGE(TAG, "%s CID %u (%s): null data_ptr",
inst->source, (unsigned)cid, desc->param_key);
continue;
}
uint8_t type = 0;
err = mbc_master_get_parameter(cid, (char *)desc->param_key,
(uint8_t *)data_ptr, &type);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
inst->source, (unsigned)cid, desc->param_key,
esp_err_to_name(err));
vTaskDelay(POLL_INTERVAL);
continue;
}
uint8_t raw[sizeof(float)];
memcpy(raw, data_ptr, sizeof(raw));
ESP_LOGD(TAG,
"%s CID %u (%s) decoded bytes: %02X %02X %02X %02X",
inst->source, (unsigned)cid, desc->param_key,
raw[0], raw[1], raw[2], raw[3]);
const float value = *(const float *)data_ptr;
if (!dds665_is_valid_value(local_cid, value))
{
ESP_LOGW(TAG, "%s %s invalid value: %.6f",
inst->source, desc->param_key, (double)value);
vTaskDelay(POLL_INTERVAL);
continue;
}
ESP_LOGD(TAG, "%s %s: %.3f %s",
inst->source, desc->param_key, (double)value,
desc->param_units);
valid_mask |= (1UL << local_cid);
switch (local_cid)
{
case CID_VOLTAGE:
voltage[0] = value;
break;
case CID_CURRENT:
current[0] = value;
break;
case CID_ACTIVE_POWER_W:
// Este DDS665 devolve potência ativa diretamente em watts.
watt[0] = (int32_t)lrintf(value);
break;
case CID_POWER_FACTOR:
pf = value;
break;
case CID_FREQUENCY:
hz = value;
break;
case CID_TOTAL_ACTIVE_ENERGY_KWH:
e_kwh = value;
break;
default:
break;
}
vTaskDelay(POLL_INTERVAL);
}
if ((valid_mask & DDS665_REQUIRED_MASK) != DDS665_REQUIRED_MASK)
{
ESP_LOGW(TAG,
"%s incomplete DDS665 sample, mask=0x%02lX",
inst->source,
(unsigned long)valid_mask);
return;
}
meter_event_data_t evt = {
.source = inst->source,
.frequency = hz,
.power_factor = pf,
.total_energy = e_kwh,
.watt_total = watt[0],
.timestamp_us = esp_timer_get_time(),
};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
ESP_LOGI(TAG,
"%s DDS665 event: V=%.1fV I=%.3fA P=%ldW E=%.3fkWh Hz=%.2f PF=%.3f mask=0x%02lX",
inst->source,
(double)voltage[0],
(double)current[0],
(long)evt.watt_total,
(double)e_kwh,
(double)hz,
(double)pf,
(unsigned long)valid_mask);
esp_err_t post_err = esp_event_post(METER_EVENT,
METER_EVENT_DATA_READY,
&evt,
sizeof(evt),
portMAX_DELAY);
if (post_err != ESP_OK)
{
ESP_LOGW(TAG, "%s failed to post meter event: %s",
inst->source, esp_err_to_name(post_err));
}
}
// ======= Tarefa de aquisição =======
static void serial_mdb_task(void *param)
{
(void)param;
while (task_should_run)
{
if (!is_initialized || !dds665_lock)
{
vTaskDelay(UPDATE_INTERVAL);
continue;
}
bool had_instance = false;
for (uint8_t slot = 0;
slot < DDS665_MAX_INSTANCES && task_should_run;
++slot)
{
if (xSemaphoreTake(dds665_lock, DDS665_LOCK_TIMEOUT) != pdTRUE)
{
ESP_LOGW(TAG, "DDS665 task timeout waiting lock");
continue;
}
dds665_instance_t inst = dds665_instances[slot];
if (inst.registered)
{
had_instance = true;
dds665_read_instance_locked(&inst);
}
xSemaphoreGive(dds665_lock);
}
vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500));
}
ESP_LOGI(TAG, "DDS665 task stopped");
meter_task = NULL;
vTaskDelete(NULL);
}
// ======= API pública =======
// Compatibilidade: chamada antiga inicializa DDS665 como GRID/ID 1.
esp_err_t meter_dds665_init(void)
{
return meter_dds665_grid_init();
}
esp_err_t meter_dds665_grid_init(void)
{
return dds665_register_instance(DDS665_SLOT_GRID);
}
esp_err_t meter_dds665_evse_init(void)
{
return dds665_register_instance(DDS665_SLOT_EVSE);
}
esp_err_t meter_dds665_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "meter_dds665 not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!dds665_lock)
return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(dds665_lock, DDS665_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
const bool has_instance = dds665_has_registered_instance_locked();
esp_err_t desc_err = ESP_OK;
if (has_instance)
desc_err = dds665_apply_descriptors_locked();
xSemaphoreGive(dds665_lock);
if (!has_instance)
{
ESP_LOGW(TAG, "meter_dds665 start ignored: no instances registered");
return ESP_ERR_INVALID_STATE;
}
if (desc_err != ESP_OK)
return desc_err;
if (meter_task == NULL)
{
task_should_run = true;
BaseType_t ok = xTaskCreate(serial_mdb_task,
"meter_dds665_task",
4096,
NULL,
3,
&meter_task);
if (ok != pdPASS)
{
task_should_run = false;
meter_task = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_dds665 shared task started");
}
return ESP_OK;
}
esp_err_t meter_dds665_grid_start(void)
{
return meter_dds665_start();
}
esp_err_t meter_dds665_evse_start(void)
{
return meter_dds665_start();
}
void meter_dds665_stop(void)
{
meter_dds665_grid_stop();
}
void meter_dds665_grid_stop(void)
{
dds665_unregister_instance(DDS665_SLOT_GRID);
}
void meter_dds665_evse_stop(void)
{
dds665_unregister_instance(DDS665_SLOT_EVSE);
}

View File

@@ -0,0 +1,58 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
/**
* @brief Inicializa o driver DDS665 em modo compatível antigo: GRID / slave ID 1.
*/
esp_err_t meter_dds665_init(void);
/**
* @brief Regista DDS665 como meter GRID no slave ID 1.
*/
esp_err_t meter_dds665_grid_init(void);
/**
* @brief Regista DDS665 como meter EVSE no slave ID 2.
*/
esp_err_t meter_dds665_evse_init(void);
/**
* @brief Inicia a task partilhada de leitura DDS665.
*/
esp_err_t meter_dds665_start(void);
/**
* @brief Inicia a task partilhada de leitura DDS665 para GRID.
*/
esp_err_t meter_dds665_grid_start(void);
/**
* @brief Inicia a task partilhada de leitura DDS665 para EVSE.
*/
esp_err_t meter_dds665_evse_start(void);
/**
* @brief Para/remove DDS665 GRID em modo compatível antigo.
*/
void meter_dds665_stop(void);
/**
* @brief Remove DDS665 GRID; só destrói o Modbus master se não houver EVSE registado.
*/
void meter_dds665_grid_stop(void);
/**
* @brief Remove DDS665 EVSE; só destrói o Modbus master se não houver GRID registado.
*/
void meter_dds665_evse_stop(void);
#ifdef __cplusplus
}
#endif

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 (two’s complement), (depende do modelo/escala)
#define DTS024M_L2_ACTIVE_P 0x000E
#define DTS024M_L3_ACTIVE_P 0x0010
#define DTS024M_PF_L1 0x001E // I16 (two’s complement), 0.001
#define DTS024M_PF_L2 0x001F
#define DTS024M_PF_L3 0x0020
#define DTS024M_FREQUENCY 0x002A // U16, 0.01 Hz
#define DTS024M_TOTAL_ACTIVE_E 0x0404 // U32, 0.01 kWh (2 regs)
// ============================================================================
// Conversões signed (two’s complement) — porque o projeto não tem PARAM_TYPE_I*
// ============================================================================
static inline int32_t s32_from_u32(uint32_t x)
{
return (x & 0x80000000u) ? (int32_t)(x - 0x100000000ULL) : (int32_t)x;
}
static inline int16_t s16_from_u16(uint16_t x)
{
return (x & 0x8000u) ? (int16_t)(x - 0x10000u) : (int16_t)x;
}
// ============================================================================
// CIDs
// ============================================================================
enum
{
CID_DTS024M_L1_VOLTAGE = 0,
CID_DTS024M_L2_VOLTAGE,
CID_DTS024M_L3_VOLTAGE,
CID_DTS024M_L1_CURRENT,
CID_DTS024M_L2_CURRENT,
CID_DTS024M_L3_CURRENT,
CID_DTS024M_L1_ACTIVE_P,
CID_DTS024M_L2_ACTIVE_P,
CID_DTS024M_L3_ACTIVE_P,
CID_DTS024M_PF_L1,
CID_DTS024M_PF_L2,
CID_DTS024M_PF_L3,
CID_DTS024M_FREQUENCY,
CID_DTS024M_TOTAL_ACTIVE_E,
};
// ============================================================================
// DESCRIPTORS (TEMPLATE) — copiamos para RAM e ajustamos:
// - slave_id
// - base offset (0/1)
// - mb_param_type (HOLDING/INPUT)
// ============================================================================
static const mb_parameter_descriptor_t device_parameters_dts024m_tmpl[] = {
// Tensões (U32 / 2 regs) — 0.01 V
{CID_DTS024M_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L1_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_VOLTAGE, STR("L2 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L2_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_VOLTAGE, STR("L3 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L3_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// Correntes (U32 / 2 regs) — 0.001 A
{CID_DTS024M_L1_CURRENT, STR("L1 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L1_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_CURRENT, STR("L2 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L2_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_CURRENT, STR("L3 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L3_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// Potência ativa por fase (U32 / 2 regs no descriptor; interpretamos como signed I32)
{CID_DTS024M_L1_ACTIVE_P, STR("L1 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L1_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_ACTIVE_P, STR("L2 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L2_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_ACTIVE_P, STR("L3 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L3_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// PF (U16 / 1 reg; interpretamos como signed I16) — 0.001
{CID_DTS024M_PF_L1, STR("L1 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L1, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
{CID_DTS024M_PF_L2, STR("L2 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L2, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
{CID_DTS024M_PF_L3, STR("L3 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L3, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
// Frequência (U16 / 1 reg) — 0.01 Hz
{CID_DTS024M_FREQUENCY, STR("Frequency"), STR("Hz"), 1,
MB_PARAM_HOLDING, DTS024M_FREQUENCY, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Energia ativa total (U32 / 2 regs) — 0.01 kWh
{CID_DTS024M_TOTAL_ACTIVE_E, STR("Total Active Energy"), STR("kWh"), 1,
MB_PARAM_HOLDING, DTS024M_TOTAL_ACTIVE_E, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
};
static mb_parameter_descriptor_t device_parameters_dts024m[ARRAY_SIZE(device_parameters_dts024m_tmpl)];
static const uint16_t num_device_parameters_dts024m = ARRAY_SIZE(device_parameters_dts024m);
static void dts024m_build_descriptors(uint8_t slave_id, uint16_t base_offset, mb_param_type_t area)
{
memcpy(device_parameters_dts024m,
device_parameters_dts024m_tmpl,
sizeof(device_parameters_dts024m));
for (uint16_t i = 0; i < num_device_parameters_dts024m; ++i)
{
device_parameters_dts024m[i].mb_slave_addr = slave_id;
device_parameters_dts024m[i].mb_reg_start =
(uint16_t)(device_parameters_dts024m[i].mb_reg_start + base_offset);
device_parameters_dts024m[i].mb_param_type = area; // HOLDING (FC03) ou INPUT (FC04)
}
}
// ============================================================================
// Modbus master init (fixo) — garante ordem correta (start -> uart_set_mode)
// ============================================================================
static esp_err_t dts024m_master_reinit(uint32_t baud, uart_parity_t parity)
{
if (mb_started)
{
(void)mbc_master_destroy();
mb_started = false;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
}
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = baud,
.parity = parity};
void *handler = NULL;
esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler);
if (err != ESP_OK)
return err;
err = mbc_master_setup(&comm);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// IMPORTANTE: start antes de uart_set_mode (driver UART costuma ser instalado no start)
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
mb_started = true;
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
mb_started = false;
return err;
}
vTaskDelay(pdMS_TO_TICKS(40));
return ESP_OK;
}
// ============================================================================
// Post do evento de medição
// ============================================================================
static void meter_dts024m_post_event(float *voltage, float *current, int *power_w,
float freq_hz, float pf_avg, float total_kwh)
{
meter_event_data_t evt = {
.source = "GRID",
.frequency = freq_hz,
.power_factor = pf_avg,
.total_energy = total_kwh};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
}
}
// ============================================================================
// Task de polling
// ============================================================================
static void serial_mdb_dts024m_task(void *param)
{
(void)param;
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
float v[3] = {0};
float i[3] = {0};
float pf[3] = {0};
float freq = 0.0f;
float total_kwh = 0.0f;
int p_w[3] = {0};
vTaskDelay(pdMS_TO_TICKS(200)); // settle
while (1)
{
for (uint16_t cid = 0; cid < num_device_parameters_dts024m; cid++)
{
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
continue;
}
uint8_t type = 0;
uint16_t raw_u16 = 0;
uint32_t raw_u32 = 0;
void *value_ptr = &raw_u16;
// U32
switch (cid)
{
case CID_DTS024M_L1_VOLTAGE:
case CID_DTS024M_L2_VOLTAGE:
case CID_DTS024M_L3_VOLTAGE:
case CID_DTS024M_L1_CURRENT:
case CID_DTS024M_L2_CURRENT:
case CID_DTS024M_L3_CURRENT:
case CID_DTS024M_L1_ACTIVE_P:
case CID_DTS024M_L2_ACTIVE_P:
case CID_DTS024M_L3_ACTIVE_P:
case CID_DTS024M_TOTAL_ACTIVE_E:
value_ptr = &raw_u32;
break;
default:
value_ptr = &raw_u16;
break;
}
// 1 retry simples em caso de timeout (podes remover se quiseres menos carga)
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
if (err == ESP_ERR_TIMEOUT)
{
vTaskDelay(pdMS_TO_TICKS(60));
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
}
if (err == ESP_OK)
{
switch (cid)
{
// V (0.01V)
case CID_DTS024M_L1_VOLTAGE:
v[0] = ((float)raw_u32) * 0.01f;
break;
case CID_DTS024M_L2_VOLTAGE:
v[1] = ((float)raw_u32) * 0.01f;
break;
case CID_DTS024M_L3_VOLTAGE:
v[2] = ((float)raw_u32) * 0.01f;
break;
// I (0.001A)
case CID_DTS024M_L1_CURRENT:
i[0] = ((float)raw_u32) * 0.001f;
break;
case CID_DTS024M_L2_CURRENT:
i[1] = ((float)raw_u32) * 0.001f;
break;
case CID_DTS024M_L3_CURRENT:
i[2] = ((float)raw_u32) * 0.001f;
break;
// P ativa (two’s complement I32) — atenção: escala depende do modelo
case CID_DTS024M_L1_ACTIVE_P:
p_w[0] = (int)s32_from_u32(raw_u32);
break;
case CID_DTS024M_L2_ACTIVE_P:
p_w[1] = (int)s32_from_u32(raw_u32);
break;
case CID_DTS024M_L3_ACTIVE_P:
p_w[2] = (int)s32_from_u32(raw_u32);
break;
// PF (two’s complement I16; 0.001)
case CID_DTS024M_PF_L1:
pf[0] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
case CID_DTS024M_PF_L2:
pf[1] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
case CID_DTS024M_PF_L3:
pf[2] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
// Freq (0.01Hz)
case CID_DTS024M_FREQUENCY:
freq = ((float)raw_u16) * 0.01f;
break;
// Energia (0.01kWh)
case CID_DTS024M_TOTAL_ACTIVE_E:
total_kwh = ((float)raw_u32) * 0.01f;
break;
default:
break;
}
ESP_LOGD(TAG, "%s (cid=%u) ok (u16=%u u32=%u)",
desc->param_key, cid, (unsigned)raw_u16, (unsigned)raw_u32);
}
else
{
ESP_LOGE(TAG, "CID %u (%s) read failed: %s",
cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
// PF médio simples (ignora zeros)
float pf_sum = 0.0f;
int pf_cnt = 0;
for (int k = 0; k < 3; ++k)
{
if (pf[k] != 0.0f)
{
pf_sum += pf[k];
pf_cnt++;
}
}
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
meter_dts024m_post_event(v, i, p_w, freq, pf_avg, total_kwh);
vTaskDelay(UPDATE_INTERVAL);
}
}
// ============================================================================
// Init / Start / Stop
// ============================================================================
esp_err_t meter_dts024m_init(void)
{
if (is_initialized)
{
ESP_LOGW(TAG, "Already initialized");
return ESP_ERR_INVALID_STATE;
}
// init fixo (produção)
esp_err_t err = dts024m_master_reinit(DTS024M_PROD_BAUD, DTS024M_PROD_PARITY);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "master_reinit failed: %s", esp_err_to_name(err));
return err;
}
// monta descriptors reais com ID/offset/area fixos
dts024m_build_descriptors(DTS024M_PROD_SLAVE_ID, DTS024M_PROD_BASE_OFFSET, DTS024M_PROD_AREA);
// aplica descriptors reais
esp_err_t derr = mbc_master_set_descriptor(device_parameters_dts024m,
num_device_parameters_dts024m);
if (derr != ESP_OK)
{
ESP_LOGE(TAG, "set_descriptor failed: %s", esp_err_to_name(derr));
return derr;
}
is_initialized = true;
ESP_LOGI(TAG, "DTS024M initialized (PROD) baud=%d parity=%d id=%d area=%s base=%d",
DTS024M_PROD_BAUD,
(int)DTS024M_PROD_PARITY,
DTS024M_PROD_SLAVE_ID,
(DTS024M_PROD_AREA == MB_PARAM_HOLDING ? "FC03" : "FC04"),
DTS024M_PROD_BASE_OFFSET);
return ESP_OK;
}
esp_err_t meter_dts024m_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "Not initialized");
return ESP_ERR_INVALID_STATE;
}
if (meter_task == NULL)
{
xTaskCreate(serial_mdb_dts024m_task,
"meter_dts024m_task",
4096, NULL, 3, &meter_task);
ESP_LOGI(TAG, "DTS024M task started");
}
return ESP_OK;
}
void meter_dts024m_stop(void)
{
if (!is_initialized)
{
ESP_LOGW(TAG, "Not initialized, skipping stop");
return;
}
if (meter_task)
{
vTaskDelete(meter_task);
meter_task = NULL;
ESP_LOGI(TAG, "DTS024M task stopped");
}
if (mb_started)
{
(void)mbc_master_destroy();
mb_started = false;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
is_initialized = false;
ESP_LOGI(TAG, "Meter DTS024M cleaned up");
}

View File

@@ -1,5 +1,5 @@
#ifndef METER_EA777_H_
#define METER_EA777_H_
#ifndef METER_DTS024M_H_
#define METER_DTS024M_H_
#include <stdint.h>
#include <stdbool.h>
@@ -10,26 +10,26 @@ extern "C" {
#endif
/**
* @brief Inicializa o driver do medidor EA777 (UART RS485, Modbus, registradores).
* @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_ea777_init(void);
esp_err_t meter_dts024m_init(void);
/**
* @brief Inicia a tarefa de leitura de dados do medidor EA777.
* @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_ea777_start(void);
esp_err_t meter_dts024m_start(void);
/**
* @brief Para a tarefa de leitura e limpa os dados internos do medidor EA777.
* @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS024M.
*/
void meter_ea777_stop(void);
void meter_dts024m_stop(void);
#ifdef __cplusplus
}
#endif
#endif /* METER_EA777_H_ */
#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));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), pdMS_TO_TICKS(10));
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));

View File

@@ -24,7 +24,6 @@ esp_err_t meter_dts6619_start(void);
*/
void meter_dts6619_stop(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,761 @@
// meter_ea777.c — Driver Modbus RTU para EARU EA777 (ESP-IDF)
// Suporta 1 ou 2 EA777 no mesmo bus RS485:
// GRID -> slave ID 1
// EVSE -> slave ID 2
// Usa um único Modbus master e uma única task de leitura.
#include "meter_ea777.h"
#include "meter_events.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stddef.h>
#include <string.h>
#include <math.h>
#include <inttypes.h>
#define TAG "serial_mdb_ea777"
// ===== UART / RS-485 =====
#define MB_PORT_NUM 1
#define MB_DEV_SPEED 9600
// Ajuste os pinos conforme seu hardware (evite GPIO2 para RTS/DE/RE se possível)
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// ===== EA777 no mesmo bus =====
#define EA777_GRID_SLAVE_ID 1
#define EA777_EVSE_SLAVE_ID 2
// ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (200 / portTICK_PERIOD_MS)
#define EA777_LOCK_TIMEOUT pdMS_TO_TICKS(10000)
// ===== 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]))
// ===== Estado =====
static bool is_initialized = false;
static volatile bool task_should_run = false;
static TaskHandle_t meter_task = NULL;
static SemaphoreHandle_t ea777_lock = NULL;
static inline uint32_t ea777_swap_words_u32(uint32_t x)
{
return ((x & 0xFFFFu) << 16) | ((x >> 16) & 0xFFFFu);
}
// ============================================================================
// ============ MAPA DE REGISTROS EA777 (Holding 0x03) ========================
// Endereços zero-based. Tipos reais (engenharia) via fator de escala.
// Tensões (0.1 V)
#define EA777_L1VOLTAGE 0x0000
#define EA777_L2VOLTAGE 0x0001
#define EA777_L3VOLTAGE 0x0002
// Correntes (0.01 A)
#define EA777_L1CURRENT 0x0003
#define EA777_L2CURRENT 0x0004
#define EA777_L3CURRENT 0x0005
// Potência ativa total (W)
#define EA777_TOTAL_ACTIVE_P 0x0007
// Fator de potência por fase (0.001)
#define EA777_PF_L1 0x0014
#define EA777_PF_L2 0x0015
#define EA777_PF_L3 0x0016
// Frequência (0.01 Hz)
#define EA777_FREQUENCY 0x001A
// Energia ativa total (U32 * 0.01 kWh, 2 registradores)
#define EA777_TOTAL_ACTIVE_E 0x001D
// ============================================================================
// ============ CIDs locais por meter ============
enum
{
CID_EA777_L1_VOLTAGE = 0,
CID_EA777_L2_VOLTAGE,
CID_EA777_L3_VOLTAGE,
CID_EA777_L1_CURRENT,
CID_EA777_L2_CURRENT,
CID_EA777_L3_CURRENT,
CID_EA777_TOTAL_ACTIVE_P,
CID_EA777_PF_L1,
CID_EA777_PF_L2,
CID_EA777_PF_L3,
CID_EA777_FREQUENCY,
CID_EA777_TOTAL_ACTIVE_E,
CID_EA777_COUNT,
};
typedef enum
{
EA777_SLOT_GRID = 0,
EA777_SLOT_EVSE,
EA777_MAX_INSTANCES
} ea777_slot_t;
typedef struct
{
bool registered;
const char *source; // "GRID" ou "EVSE"
uint8_t slave_id; // 1 ou 2
uint16_t cid_base; // atribuído dinamicamente em ea777_rebuild_descriptors_locked()
} ea777_instance_t;
static ea777_instance_t ea777_instances[EA777_MAX_INSTANCES] = {
[EA777_SLOT_GRID] = {
.registered = false,
.source = "GRID",
.slave_id = EA777_GRID_SLAVE_ID,
.cid_base = 0,
},
[EA777_SLOT_EVSE] = {
.registered = false,
.source = "EVSE",
.slave_id = EA777_EVSE_SLAVE_ID,
.cid_base = 0,
},
};
// ======= Template de descritores (Holding registers) =======
// Nota: param_offset = 0 -> não usamos holding_reg_params_t aqui.
static const mb_parameter_descriptor_t ea777_param_template[CID_EA777_COUNT] = {
// Tensões (0.1 V)
{CID_EA777_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L1VOLTAGE, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 4000, 1), PAR_PERMS_READ},
{CID_EA777_L2_VOLTAGE, STR("L2 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L2VOLTAGE, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 4000, 1), PAR_PERMS_READ},
{CID_EA777_L3_VOLTAGE, STR("L3 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L3VOLTAGE, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 4000, 1), PAR_PERMS_READ},
// Correntes (0.01 A)
{CID_EA777_L1_CURRENT, STR("L1 Current"), STR("A"), 1,
MB_PARAM_HOLDING, EA777_L1CURRENT, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
{CID_EA777_L2_CURRENT, STR("L2 Current"), STR("A"), 1,
MB_PARAM_HOLDING, EA777_L2CURRENT, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
{CID_EA777_L3_CURRENT, STR("L3 Current"), STR("A"), 1,
MB_PARAM_HOLDING, EA777_L3CURRENT, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Potência ativa total (W)
{CID_EA777_TOTAL_ACTIVE_P, STR("Total Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, EA777_TOTAL_ACTIVE_P, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 60000, 1), PAR_PERMS_READ},
// Fator de potência (0.001)
{CID_EA777_PF_L1, STR("L1 PF"), STR(""), 1,
MB_PARAM_HOLDING, EA777_PF_L1, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
{CID_EA777_PF_L2, STR("L2 PF"), STR(""), 1,
MB_PARAM_HOLDING, EA777_PF_L2, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
{CID_EA777_PF_L3, STR("L3 PF"), STR(""), 1,
MB_PARAM_HOLDING, EA777_PF_L3, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
// Frequência (0.01 Hz)
{CID_EA777_FREQUENCY, STR("Frequency"), STR("Hz"), 1,
MB_PARAM_HOLDING, EA777_FREQUENCY, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Energia ativa total (U32 * 0.01 kWh, 2 regs)
{CID_EA777_TOTAL_ACTIVE_E, STR("Total Active Energy"), STR("kWh"), 1,
MB_PARAM_HOLDING, EA777_TOTAL_ACTIVE_E, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
};
// O esp-modbus exige cid e param_key únicos no Data Dictionary.
// Como GRID e EVSE usam o mesmo mapa de registos, o param_key precisa de prefixo por instância.
static const char *const ea777_param_keys[EA777_MAX_INSTANCES][CID_EA777_COUNT] = {
[EA777_SLOT_GRID] = {
"GRID L1 Voltage",
"GRID L2 Voltage",
"GRID L3 Voltage",
"GRID L1 Current",
"GRID L2 Current",
"GRID L3 Current",
"GRID Total Active Power",
"GRID L1 PF",
"GRID L2 PF",
"GRID L3 PF",
"GRID Frequency",
"GRID Total Active Energy",
},
[EA777_SLOT_EVSE] = {
"EVSE L1 Voltage",
"EVSE L2 Voltage",
"EVSE L3 Voltage",
"EVSE L1 Current",
"EVSE L2 Current",
"EVSE L3 Current",
"EVSE Total Active Power",
"EVSE L1 PF",
"EVSE L2 PF",
"EVSE L3 PF",
"EVSE Frequency",
"EVSE Total Active Energy",
},
};
static mb_parameter_descriptor_t device_parameters_ea777[EA777_MAX_INSTANCES * CID_EA777_COUNT];
static uint16_t num_device_parameters_ea777 = 0;
static bool ea777_descriptor_dirty = true;
static bool ea777_has_registered_instance_locked(void)
{
for (uint8_t i = 0; i < EA777_MAX_INSTANCES; ++i)
{
if (ea777_instances[i].registered)
return true;
}
return false;
}
static esp_err_t ea777_rebuild_descriptors_locked(void)
{
num_device_parameters_ea777 = 0;
ea777_descriptor_dirty = true;
for (uint8_t inst_idx = 0; inst_idx < EA777_MAX_INSTANCES; ++inst_idx)
{
ea777_instance_t *inst = &ea777_instances[inst_idx];
if (!inst->registered)
continue;
// Mantém os CIDs contíguos no descriptor ativo.
// Isto evita problemas em versões do esp-modbus que tratam CID como índice.
inst->cid_base = num_device_parameters_ea777;
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; ++local_cid)
{
mb_parameter_descriptor_t *dst = &device_parameters_ea777[num_device_parameters_ea777++];
*dst = ea777_param_template[local_cid];
dst->cid = inst->cid_base + local_cid;
dst->param_key = ea777_param_keys[inst_idx][local_cid];
dst->mb_slave_addr = inst->slave_id;
}
}
ea777_descriptor_dirty = true;
ESP_LOGI(TAG, "EA777 descriptor table prepared: %u parameters", num_device_parameters_ea777);
return ESP_OK;
}
static esp_err_t ea777_apply_descriptors_locked(void)
{
if (num_device_parameters_ea777 == 0)
return ESP_ERR_INVALID_STATE;
if (!ea777_descriptor_dirty)
return ESP_OK;
esp_err_t err = mbc_master_set_descriptor(device_parameters_ea777, num_device_parameters_ea777);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", esp_err_to_name(err));
return err;
}
ea777_descriptor_dirty = false;
ESP_LOGI(TAG, "EA777 descriptor table applied: %u parameters", num_device_parameters_ea777);
return ESP_OK;
}
static esp_err_t ea777_master_init_once(void)
{
if (is_initialized)
return ESP_OK;
if (!ea777_lock)
{
ea777_lock = xSemaphoreCreateMutex();
if (!ea777_lock)
return ESP_ERR_NO_MEM;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted before EA777 init");
}
(void)mbc_master_destroy();
ESP_LOGI(TAG, "meter_ea777 Modbus master init");
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_EVEN,
};
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;
}
// RS-485 half duplex
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;
}
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
is_initialized = true;
ESP_LOGI(TAG, "EA777 Modbus master initialized (9600 8E1, Holding Reg 0x03)");
return ESP_OK;
}
static esp_err_t ea777_register_instance(ea777_slot_t slot)
{
if (slot >= EA777_MAX_INSTANCES)
return ESP_ERR_INVALID_ARG;
esp_err_t err = ea777_master_init_once();
if (err != ESP_OK)
return err;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
ea777_instances[slot].registered = true;
ESP_LOGI(TAG, "EA777 %s registered on Modbus slave ID %u",
ea777_instances[slot].source,
ea777_instances[slot].slave_id);
err = ea777_rebuild_descriptors_locked();
xSemaphoreGive(ea777_lock);
return err;
}
static void ea777_shutdown_if_idle(void)
{
bool any_registered = false;
if (ea777_lock && xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE)
{
any_registered = ea777_has_registered_instance_locked();
xSemaphoreGive(ea777_lock);
}
if (any_registered || !is_initialized)
return;
ESP_LOGI(TAG, "No EA777 instances registered; stopping shared Modbus master");
task_should_run = false;
for (int i = 0; i < 20 && meter_task != NULL; ++i)
{
vTaskDelay(pdMS_TO_TICKS(10));
}
if (meter_task != NULL)
{
ESP_LOGW(TAG, "EA777 task did not exit in time; deleting it");
vTaskDelete(meter_task);
meter_task = NULL;
}
esp_err_t err = mbc_master_destroy();
if (err != ESP_OK)
ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err));
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
is_initialized = false;
num_device_parameters_ea777 = 0;
ea777_descriptor_dirty = true;
}
static void ea777_unregister_instance(ea777_slot_t slot)
{
if (slot >= EA777_MAX_INSTANCES)
return;
if (!is_initialized || !ea777_lock)
return;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE)
{
ESP_LOGI(TAG, "EA777 %s unregistered", ea777_instances[slot].source);
ea777_instances[slot].registered = false;
(void)ea777_rebuild_descriptors_locked();
xSemaphoreGive(ea777_lock);
}
ea777_shutdown_if_idle();
}
static void meter_ea777_post_event(const ea777_instance_t *inst,
const float *voltage,
const float *current,
const int32_t *power_w,
int32_t total_power_w,
float freq_hz,
float pf_avg,
float total_kwh)
{
meter_event_data_t evt = {
.source = inst->source,
.frequency = freq_hz,
.power_factor = pf_avg,
.total_energy = total_kwh,
.watt_total = total_power_w,
.timestamp_us = esp_timer_get_time(),
};
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, "%s falha ao emitir evento: %s", inst->source, esp_err_to_name(err));
}
}
static void ea777_read_instance_locked(const ea777_instance_t *inst)
{
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;
int32_t total_active_power_w = 0;
bool got_any_value = false;
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; local_cid++)
{
const uint16_t cid = inst->cid_base + local_cid;
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", inst->source, cid, esp_err_to_name(err));
continue;
}
uint8_t type = 0;
uint16_t raw16 = 0;
uint32_t raw32 = 0;
void *value_ptr = (local_cid == CID_EA777_TOTAL_ACTIVE_E) ? (void *)&raw32 : (void *)&raw16;
// 1 retry simples em caso de timeout
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)
{
got_any_value = true;
switch (local_cid)
{
case CID_EA777_L1_VOLTAGE:
v[0] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L2_VOLTAGE:
v[1] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L3_VOLTAGE:
v[2] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L1_CURRENT:
i[0] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L2_CURRENT:
i[1] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L3_CURRENT:
i[2] = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_P:
total_active_power_w = (int32_t)raw16;
break;
case CID_EA777_PF_L1:
pf[0] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L2:
pf[1] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L3:
pf[2] = ((float)raw16) * 0.001f;
break;
case CID_EA777_FREQUENCY:
freq = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_E:
{
uint32_t raw_e = ea777_swap_words_u32(raw32);
total_kwh = ((float)raw_e) * 0.01f;
ESP_LOGI(TAG,
"%s EA777 energy raw=0x%08" PRIX32
" swapped=0x%08" PRIX32 " => %.3f kWh",
inst->source,
raw32,
raw_e,
(double)total_kwh);
break;
}
default:
break;
}
ESP_LOGD(TAG, "%s %s (cid=%u) -> raw16=%u raw32=%u",
inst->source, desc->param_key, cid,
(unsigned int)raw16,
(unsigned int)raw32);
}
else
{
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
inst->source, cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
if (!got_any_value)
{
ESP_LOGW(TAG, "%s no valid EA777 values read in this cycle", inst->source);
return;
}
// Potência por fase aproximada: P = V * I * PF.
// O EA777 também fornece potência ativa total; esta é usada em watt_total quando existe.
int32_t p_int[3] = {
(int32_t)lrintf(v[0] * i[0] * pf[0]),
(int32_t)lrintf(v[1] * i[1] * pf[1]),
(int32_t)lrintf(v[2] * i[2] * pf[2]),
};
const int32_t p_sum = p_int[0] + p_int[1] + p_int[2];
const int32_t watt_total = (total_active_power_w != 0) ? total_active_power_w : p_sum;
// 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_ea777_post_event(inst, v, i, p_int, watt_total, freq, pf_avg, total_kwh);
}
// ===== Task de polling partilhada =====
static void serial_mdb_ea777_task(void *param)
{
(void)param;
// pequeno settle antes da 1ª leitura
vTaskDelay(pdMS_TO_TICKS(200));
while (task_should_run)
{
if (!is_initialized || !ea777_lock)
{
vTaskDelay(UPDATE_INTERVAL);
continue;
}
bool had_instance = false;
for (uint8_t slot = 0; slot < EA777_MAX_INSTANCES && task_should_run; ++slot)
{
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
{
ESP_LOGW(TAG, "EA777 task timeout waiting lock");
continue;
}
ea777_instance_t inst = ea777_instances[slot];
if (inst.registered)
{
had_instance = true;
ea777_read_instance_locked(&inst);
}
xSemaphoreGive(ea777_lock);
}
vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500));
}
ESP_LOGI(TAG, "EA777 task stopped");
meter_task = NULL;
vTaskDelete(NULL);
}
// ===== API pública =====
// Compatibilidade: chamada antiga inicializa EA777 como GRID/ID 1.
esp_err_t meter_ea777_init(void)
{
return meter_ea777_grid_init();
}
esp_err_t meter_ea777_grid_init(void)
{
return ea777_register_instance(EA777_SLOT_GRID);
}
esp_err_t meter_ea777_evse_init(void)
{
return ea777_register_instance(EA777_SLOT_EVSE);
}
esp_err_t meter_ea777_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "meter_ea777 not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!ea777_lock)
return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
const bool has_instance = ea777_has_registered_instance_locked();
esp_err_t desc_err = ESP_OK;
if (has_instance)
desc_err = ea777_apply_descriptors_locked();
xSemaphoreGive(ea777_lock);
if (!has_instance)
{
ESP_LOGW(TAG, "meter_ea777 start ignored: no instances registered");
return ESP_ERR_INVALID_STATE;
}
if (desc_err != ESP_OK)
return desc_err;
if (meter_task == NULL)
{
task_should_run = true;
BaseType_t ok = xTaskCreate(serial_mdb_ea777_task,
"meter_ea777_task",
4096, NULL, 3, &meter_task);
if (ok != pdPASS)
{
task_should_run = false;
meter_task = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_ea777 shared task started");
}
return ESP_OK;
}
esp_err_t meter_ea777_grid_start(void)
{
return meter_ea777_start();
}
esp_err_t meter_ea777_evse_start(void)
{
return meter_ea777_start();
}
void meter_ea777_stop(void)
{
meter_ea777_grid_stop();
}
void meter_ea777_grid_stop(void)
{
ea777_unregister_instance(EA777_SLOT_GRID);
}
void meter_ea777_evse_stop(void)
{
ea777_unregister_instance(EA777_SLOT_EVSE);
}

View File

@@ -0,0 +1,61 @@
#ifndef METER_EA777_H_
#define METER_EA777_H_
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Inicializa o driver EA777 em modo compatível antigo: GRID / slave ID 1.
*/
esp_err_t meter_ea777_init(void);
/**
* @brief Regista EA777 como meter GRID no slave ID 1.
*/
esp_err_t meter_ea777_grid_init(void);
/**
* @brief Regista EA777 como meter EVSE no slave ID 2.
*/
esp_err_t meter_ea777_evse_init(void);
/**
* @brief Inicia a task partilhada de leitura EA777.
*/
esp_err_t meter_ea777_start(void);
/**
* @brief Inicia a task partilhada de leitura EA777 para GRID.
*/
esp_err_t meter_ea777_grid_start(void);
/**
* @brief Inicia a task partilhada de leitura EA777 para EVSE.
*/
esp_err_t meter_ea777_evse_start(void);
/**
* @brief Para/remover EA777 GRID em modo compatível antigo.
*/
void meter_ea777_stop(void);
/**
* @brief Remove EA777 GRID; só destrói o Modbus master se não houver EVSE registado.
*/
void meter_ea777_grid_stop(void);
/**
* @brief Remove EA777 EVSE; só destrói o Modbus master se não houver GRID registado.
*/
void meter_ea777_evse_stop(void);
#ifdef __cplusplus
}
#endif
#endif /* METER_EA777_H_ */

View File

@@ -8,11 +8,12 @@
#define TAG "serial_mdb_orno513"
#define MB_PORT_NUM 2
#define MB_DEV_SPEED 9600
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 5
#define MB_UART_RTS 2
#define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (100 / portTICK_PERIOD_MS)
@@ -129,7 +130,7 @@ static void serial_mdb_task(void *param) {
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
vTaskDelay(UPDATE_INTERVAL);

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));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), pdMS_TO_TICKS(10));
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));

View File

@@ -0,0 +1,612 @@
// components/meter_manager/driver/meter_modbus/meter_orno526.c
// Driver Modbus RTU/RS485 para ORNO OR-WE-526.
//
// Configuração validada em hardware por USB-RS485:
// - Slave ID 1
// - 9600 baud, 8N1
// - FC04 (Input Registers)
// - INT32 transmitido no fio como ABCD; nesta versão do esp-modbus,
// PARAM_TYPE_I32_CDAB produz o inteiro nativo correto no ESP32.
//
// Mapa usado:
// 0x0100 tensão INT32 / 1000 V
// 0x0102 corrente INT32 / 1000 A
// 0x0104 potência ativa INT32 W
// 0x0106 potência aparente INT32 VA
// 0x0108 potência reativa INT32 var
// 0x010A frequência UINT16 / 10 Hz
// 0x010B fator de potência UINT16 / 1000
// 0x010E energia ativa direta INT32 / 100 kWh
// 0x0140 energia reativa total INT32 / 100 kvarh
//
// O OR-WE-526 é monofásico: apenas o índice de fase 0 é preenchido.
#include "meter_orno526.h"
#include "mbcontroller.h"
#include "meter_events.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#define TAG "serial_mdb_orno526"
// ===== UART / Modbus =====
#define MB_PORT_NUM 2
#define MB_DEV_SPEED 9600
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2
#define ORNO526_SLAVE_ID 1
// ===== Temporizações =====
#define UPDATE_INTERVAL pdMS_TO_TICKS(3000)
#define POLL_INTERVAL pdMS_TO_TICKS(100)
#define STOP_WAIT_STEP pdMS_TO_TICKS(10)
#define STOP_WAIT_ITERATIONS 200
#define OPTS(minimum, maximum, step_value) \
{.opt1 = (minimum), .opt2 = (maximum), .opt3 = (step_value)}
#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
// ===== Estado =====
static bool is_initialized = false;
static volatile bool task_should_run = false;
static TaskHandle_t meter_task = NULL;
static const char *meter_source = "GRID";
// ===== CIDs =====
typedef enum
{
CID_ACTIVE_ENERGY = 0,
CID_REACTIVE_ENERGY,
CID_ACTIVE_POWER,
CID_APPARENT_POWER,
CID_REACTIVE_POWER,
CID_L1_CURRENT,
CID_L1_VOLTAGE,
CID_FREQUENCY,
CID_POWER_FACTOR,
CID_COUNT
} orno526_cid_t;
#define ORNO526_REQUIRED_MASK \
((1UL << CID_L1_VOLTAGE) | \
(1UL << CID_L1_CURRENT) | \
(1UL << CID_ACTIVE_POWER))
// ===== Endereços Modbus =====
#define REG_L1_VOLTAGE 0x0100U
#define REG_L1_CURRENT 0x0102U
#define REG_ACTIVE_POWER 0x0104U
#define REG_APPARENT_POWER 0x0106U
#define REG_REACTIVE_POWER 0x0108U
#define REG_FREQUENCY 0x010AU
#define REG_POWER_FACTOR 0x010BU
#define REG_FORWARD_ACTIVE_ENERGY 0x010EU
#define REG_TOTAL_REACTIVE_ENERGY 0x0140U
// A documentação mostra ABCD no fio. Tal como validado noutros drivers deste
// projeto, o tipo CDAB do esp-modbus faz a conversão correta para o host ESP32.
#ifndef ORNO526_I32_PARAM_TYPE
#define ORNO526_I32_PARAM_TYPE PARAM_TYPE_I32_CDAB
#endif
// param_offset fica a zero porque cada leitura usa um buffer local alinhado.
static const mb_parameter_descriptor_t device_parameters_orno526[CID_COUNT] = {
{CID_ACTIVE_ENERGY, "Forward Active Energy", "kWh", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_FORWARD_ACTIVE_ENERGY, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 2147483647, 1), PAR_PERMS_READ},
{CID_REACTIVE_ENERGY, "Total Reactive Energy", "kvarh", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_TOTAL_REACTIVE_ENERGY, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 2147483647, 1), PAR_PERMS_READ},
{CID_ACTIVE_POWER, "Active Power", "W", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_ACTIVE_POWER, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ},
{CID_APPARENT_POWER, "Apparent Power", "VA", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_APPARENT_POWER, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 100000, 1), PAR_PERMS_READ},
{CID_REACTIVE_POWER, "Reactive Power", "var", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_REACTIVE_POWER, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ},
{CID_L1_CURRENT, "L1 Current", "A", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_L1_CURRENT, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ},
{CID_L1_VOLTAGE, "L1 Voltage", "V", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_L1_VOLTAGE, 2, 0,
ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 300000, 1), PAR_PERMS_READ},
{CID_FREQUENCY, "Frequency", "Hz", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_FREQUENCY, 1, 0,
PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
{CID_POWER_FACTOR, "Power Factor", "", ORNO526_SLAVE_ID,
MB_PARAM_INPUT, REG_POWER_FACTOR, 1, 0,
PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ},
};
static bool orno526_is_16bit_cid(uint16_t cid)
{
return cid == CID_FREQUENCY || cid == CID_POWER_FACTOR;
}
static bool orno526_value_is_valid(uint16_t cid, float value)
{
if (!isfinite(value))
return false;
switch (cid)
{
case CID_ACTIVE_ENERGY:
case CID_REACTIVE_ENERGY:
return value >= 0.0f && value <= 10000000.0f;
case CID_ACTIVE_POWER:
case CID_REACTIVE_POWER:
return value >= -100000.0f && value <= 100000.0f;
case CID_APPARENT_POWER:
return value >= 0.0f && value <= 100000.0f;
case CID_L1_CURRENT:
return value >= -100.0f && value <= 100.0f;
case CID_L1_VOLTAGE:
return value >= 0.0f && value <= 300.0f;
case CID_FREQUENCY:
return value >= 0.0f && value <= 100.0f;
case CID_POWER_FACTOR:
return value >= 0.0f && value <= 1.1f;
default:
return false;
}
}
static float orno526_scale_i32(uint16_t cid, int32_t raw)
{
switch (cid)
{
case CID_L1_VOLTAGE:
case CID_L1_CURRENT:
return (float)raw / 1000.0f;
case CID_ACTIVE_ENERGY:
case CID_REACTIVE_ENERGY:
return (float)raw / 100.0f;
case CID_ACTIVE_POWER:
case CID_APPARENT_POWER:
case CID_REACTIVE_POWER:
default:
return (float)raw;
}
}
static float orno526_scale_u16(uint16_t cid, uint16_t raw)
{
switch (cid)
{
case CID_FREQUENCY:
return (float)raw / 10.0f;
case CID_POWER_FACTOR:
return (float)raw / 1000.0f;
default:
return (float)raw;
}
}
static void serial_mdb_task(void *param)
{
(void)param;
while (task_should_run)
{
float voltage[3] = {0.0f, 0.0f, 0.0f};
float current[3] = {0.0f, 0.0f, 0.0f};
int32_t watt[3] = {0, 0, 0};
float frequency_hz = 0.0f;
float power_factor = 0.0f;
float active_energy_kwh = 0.0f;
float reactive_energy_kvarh = 0.0f;
float apparent_power_va = 0.0f;
float reactive_power_var = 0.0f;
uint32_t valid_mask = 0U;
for (uint16_t cid = 0; cid < CID_COUNT && task_should_run; ++cid)
{
const mb_parameter_descriptor_t *desc = NULL;
esp_err_t err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s",
meter_source, (unsigned)cid, esp_err_to_name(err));
continue;
}
int32_t raw_i32 = 0;
uint16_t raw_u16 = 0;
void *data_ptr = orno526_is_16bit_cid(cid)
? (void *)&raw_u16
: (void *)&raw_i32;
uint8_t type = 0;
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)data_ptr,
&type);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
meter_source,
(unsigned)cid,
desc->param_key,
esp_err_to_name(err));
vTaskDelay(POLL_INTERVAL);
continue;
}
const float value = orno526_is_16bit_cid(cid)
? orno526_scale_u16(cid, raw_u16)
: orno526_scale_i32(cid, raw_i32);
if (!orno526_value_is_valid(cid, value))
{
if (orno526_is_16bit_cid(cid))
{
ESP_LOGW(TAG,
"%s %s invalid: raw=0x%04X value=%.6f",
meter_source,
desc->param_key,
(unsigned)raw_u16,
(double)value);
}
else
{
ESP_LOGW(TAG,
"%s %s invalid: raw=%ld (0x%08lX) value=%.6f",
meter_source,
desc->param_key,
(long)raw_i32,
(unsigned long)(uint32_t)raw_i32,
(double)value);
}
vTaskDelay(POLL_INTERVAL);
continue;
}
valid_mask |= (1UL << cid);
ESP_LOGD(TAG, "%s %s: %.3f %s",
meter_source,
desc->param_key,
(double)value,
desc->param_units);
switch (cid)
{
case CID_ACTIVE_ENERGY:
active_energy_kwh = value;
break;
case CID_REACTIVE_ENERGY:
reactive_energy_kvarh = value;
break;
case CID_ACTIVE_POWER:
watt[0] = (int32_t)lrintf(value);
break;
case CID_APPARENT_POWER:
apparent_power_va = value;
break;
case CID_REACTIVE_POWER:
reactive_power_var = value;
break;
case CID_L1_CURRENT:
// irms deve ser não negativo; o sentido é representado pela potência.
current[0] = fabsf(value);
break;
case CID_L1_VOLTAGE:
voltage[0] = value;
break;
case CID_FREQUENCY:
frequency_hz = value;
break;
case CID_POWER_FACTOR:
power_factor = value;
break;
default:
break;
}
vTaskDelay(POLL_INTERVAL);
}
if (!task_should_run)
break;
if ((valid_mask & ORNO526_REQUIRED_MASK) != ORNO526_REQUIRED_MASK)
{
ESP_LOGW(TAG,
"%s incomplete OR-WE-526 sample, mask=0x%03lX",
meter_source,
(unsigned long)valid_mask);
vTaskDelay(UPDATE_INTERVAL);
continue;
}
meter_event_data_t evt = {
.source = meter_source,
.watt_total = watt[0],
.frequency = frequency_hz,
.power_factor = power_factor,
.total_energy = active_energy_kwh,
.timestamp_us = esp_timer_get_time(),
};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
ESP_LOGI(TAG,
"%s OR-WE-526 event: V=%.3fV I=%.3fA P=%ldW "
"S=%.0fVA Q=%.0fvar E=%.2fkWh Er=%.2fkvarh "
"Hz=%.2f PF=%.3f mask=0x%03lX",
meter_source,
(double)voltage[0],
(double)current[0],
(long)evt.watt_total,
(double)apparent_power_va,
(double)reactive_power_var,
(double)active_energy_kwh,
(double)reactive_energy_kvarh,
(double)frequency_hz,
(double)power_factor,
(unsigned long)valid_mask);
esp_err_t post_err = esp_event_post(METER_EVENT,
METER_EVENT_DATA_READY,
&evt,
sizeof(evt),
portMAX_DELAY);
if (post_err != ESP_OK)
{
ESP_LOGW(TAG, "%s failed to post meter event: %s",
meter_source, esp_err_to_name(post_err));
}
vTaskDelay(UPDATE_INTERVAL);
}
ESP_LOGI(TAG, "OR-WE-526 task stopped");
meter_task = NULL;
vTaskDelete(NULL);
}
static esp_err_t meter_orno526_init_common(const char *source)
{
if (!source)
return ESP_ERR_INVALID_ARG;
if (is_initialized)
{
if (strcmp(meter_source, source) == 0)
return ESP_OK;
ESP_LOGE(TAG,
"OR-WE-526 already initialized as %s; cannot reinitialize as %s",
meter_source,
source);
return ESP_ERR_INVALID_STATE;
}
meter_source = source;
ESP_LOGI(TAG, "meter_orno526 init as %s", meter_source);
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_DISABLE, // Validado: 9600 8N1
};
void *handler = NULL;
esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_init failed: %s", esp_err_to_name(err));
return err;
}
err = mbc_master_setup(&comm);
if (err != ESP_OK)
goto fail_destroy_master;
err = uart_set_pin(MB_PORT_NUM,
MB_UART_TXD,
MB_UART_RXD,
MB_UART_RTS,
UART_PIN_NO_CHANGE);
if (err != ESP_OK)
goto fail_destroy_master;
err = uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS);
if (err != ESP_OK)
goto fail_destroy_master;
err = uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0);
if (err != ESP_OK)
goto fail_destroy_master;
err = uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1);
if (err != ESP_OK)
goto fail_destroy_master;
err = mbc_master_start();
if (err != ESP_OK)
goto fail_destroy_master;
// O driver UART é instalado por mbc_master_start(); só depois ativamos RS485.
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
goto fail_destroy_master;
vTaskDelay(pdMS_TO_TICKS(20));
err = mbc_master_set_descriptor(device_parameters_orno526,
ARRAY_SIZE(device_parameters_orno526));
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s",
esp_err_to_name(err));
goto fail_destroy_master;
}
// Reduz para INFO/WARN em produção, se necessário.
esp_log_level_set("MB_CONTROLLER_MASTER", ESP_LOG_DEBUG);
esp_log_level_set("MB_PORT_COMMON", ESP_LOG_DEBUG);
esp_log_level_set("MB_SERIAL_MASTER", ESP_LOG_DEBUG);
is_initialized = true;
return ESP_OK;
fail_destroy_master:
ESP_LOGE(TAG, "OR-WE-526 initialization failed: %s", esp_err_to_name(err));
(void)mbc_master_destroy();
if (uart_is_driver_installed(MB_PORT_NUM))
(void)uart_driver_delete(MB_PORT_NUM);
return err;
}
esp_err_t meter_orno526_init(void)
{
return meter_orno526_grid_init();
}
esp_err_t meter_orno526_grid_init(void)
{
return meter_orno526_init_common("GRID");
}
esp_err_t meter_orno526_evse_init(void)
{
return meter_orno526_init_common("EVSE");
}
esp_err_t meter_orno526_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "meter_orno526 not initialized");
return ESP_ERR_INVALID_STATE;
}
if (meter_task != NULL)
return ESP_OK;
task_should_run = true;
BaseType_t ok = xTaskCreate(serial_mdb_task,
"meter_orno526_task",
4096,
NULL,
3,
&meter_task);
if (ok != pdPASS)
{
task_should_run = false;
meter_task = NULL;
ESP_LOGE(TAG, "Failed to create OR-WE-526 task");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "OR-WE-526 %s task started", meter_source);
return ESP_OK;
}
esp_err_t meter_orno526_grid_start(void)
{
return meter_orno526_start();
}
esp_err_t meter_orno526_evse_start(void)
{
return meter_orno526_start();
}
void meter_orno526_stop(void)
{
if (!is_initialized)
return;
ESP_LOGI(TAG, "Stopping OR-WE-526 %s", meter_source);
task_should_run = false;
for (int i = 0; i < STOP_WAIT_ITERATIONS && meter_task != NULL; ++i)
vTaskDelay(STOP_WAIT_STEP);
if (meter_task != NULL)
{
ESP_LOGW(TAG, "OR-WE-526 task did not stop in time; deleting it");
vTaskDelete(meter_task);
meter_task = NULL;
}
esp_err_t err = mbc_master_destroy();
if (err != ESP_OK)
{
ESP_LOGW(TAG, "mbc_master_destroy returned %s",
esp_err_to_name(err));
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
err = uart_driver_delete(MB_PORT_NUM);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "uart_driver_delete returned %s",
esp_err_to_name(err));
}
}
is_initialized = false;
meter_source = "GRID";
}
void meter_orno526_grid_stop(void)
{
meter_orno526_stop();
}
void meter_orno526_evse_stop(void)
{
meter_orno526_stop();
}

View File

@@ -0,0 +1,58 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "esp_err.h"
/**
* @brief Compatibilidade: inicializa OR-WE-526 como meter GRID (slave ID 1).
*/
esp_err_t meter_orno526_init(void);
/**
* @brief Inicializa OR-WE-526 como meter GRID (slave ID 1).
*/
esp_err_t meter_orno526_grid_init(void);
/**
* @brief Inicializa OR-WE-526 como meter EVSE (slave ID 1).
*
* O driver suporta uma única instância OR-WE-526 de cada vez.
*/
esp_err_t meter_orno526_evse_init(void);
/**
* @brief Inicia a task de aquisição OR-WE-526.
*/
esp_err_t meter_orno526_start(void);
/**
* @brief Alias de start para utilização como GRID.
*/
esp_err_t meter_orno526_grid_start(void);
/**
* @brief Alias de start para utilização como EVSE.
*/
esp_err_t meter_orno526_evse_start(void);
/**
* @brief Para a task e destrói o master Modbus/UART.
*/
void meter_orno526_stop(void);
/**
* @brief Alias de stop para utilização como GRID.
*/
void meter_orno526_grid_stop(void);
/**
* @brief Alias de stop para utilização como EVSE.
*/
void meter_orno526_evse_stop(void);
#ifdef __cplusplus
}
#endif

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