Compare commits
5 Commits
v1.0.0
...
pilot-adc-
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a803fcef1 | |||
| ef02e5c5f5 | |||
| d0d431daf2 | |||
| 286028b6a8 | |||
| 023644a887 |
0
.gitignore
vendored
Executable file → Normal file
0
.gitignore
vendored
Executable file → Normal file
0
CMakeLists.txt
Executable file → Normal file
0
CMakeLists.txt
Executable file → Normal file
0
LICENSE.md
Executable file → Normal file
0
LICENSE.md
Executable file → Normal file
209
README.md
Executable file → Normal file
209
README.md
Executable file → Normal file
@@ -1,85 +1,180 @@
|
||||

|
||||
# 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:
|
||||
|
||||

|
||||
[](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
|
||||
|
||||

|
||||
- 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.
|
||||
|
||||

|
||||
### 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)
|
||||
|
||||

|
||||
### 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.
|
||||
|
||||

|
||||
### 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
|
||||
|
||||

|
||||
- `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):
|
||||
|
||||

|
||||
```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
0
cfg/custom/board.cfg
Executable file → Normal file
114
cfg/esp32devkitc/board.cfg
Executable file → Normal file
114
cfg/esp32devkitc/board.cfg
Executable file → Normal 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
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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
|
||||
# ------------------------------------------------------------
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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=
|
||||
SOCKET_LOCK_DETECTION_GPIO=
|
||||
|
||||
0
cfg/esp32s2da/board.cfg
Executable file → Normal file
0
cfg/esp32s2da/board.cfg
Executable file → Normal file
4
components/auth/CMakeLists.txt
Executable file → Normal file
4
components/auth/CMakeLists.txt
Executable file → Normal 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
0
components/auth/idf_component.yml
Executable file → Normal file
0
components/auth/include/auth.h
Executable file → Normal file
0
components/auth/include/auth.h
Executable file → Normal file
0
components/auth/include/wiegand.h
Executable file → Normal file
0
components/auth/include/wiegand.h
Executable file → Normal file
0
components/auth/include/wiegand_reader.h
Executable file → Normal file
0
components/auth/include/wiegand_reader.h
Executable file → Normal file
363
components/auth/src/auth.c
Executable file → Normal file
363
components/auth/src/auth.c
Executable file → Normal 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
0
components/auth/src/auth_types.c
Executable file → Normal file
0
components/auth/src/wiegand.c
Executable file → Normal file
0
components/auth/src/wiegand.c
Executable file → Normal file
5
components/auth/src/wiegand_reader.c
Executable file → Normal file
5
components/auth/src/wiegand_reader.c
Executable file → Normal 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
2
components/buzzer/CMakeLists.txt
Executable file → Normal 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
0
components/buzzer/idf_component.yml
Executable file → Normal file
12
components/buzzer/include/buzzer.h
Executable file → Normal file
12
components/buzzer/include/buzzer.h
Executable file → Normal 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.
|
||||
@@ -63,4 +63,4 @@ void buzzer_set_duty_percent(uint8_t pct);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
307
components/buzzer/src/buzzer.c
Executable file → Normal file
307
components/buzzer/src/buzzer.c
Executable file → Normal 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, <) == 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 (in_quiet_hours() && !is_critical_pattern(id))
|
||||
continue;
|
||||
|
||||
// Anti-spam global (NÃO aplicar a padrões críticos)
|
||||
if (!is_critical_pattern(id))
|
||||
{
|
||||
if (!(id == BUZZER_PATTERN_FAULT || id == BUZZER_PATTERN_CARD_DENIED))
|
||||
{
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if ((now - s_last_play_tick) < s_min_gap_ticks)
|
||||
continue;
|
||||
}
|
||||
s_last_play_tick = now;
|
||||
}
|
||||
|
||||
// Anti-spam global
|
||||
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
2
components/config/CMakeLists.txt
Executable file → Normal 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
25
components/config/board_config.c
Executable file → Normal 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
10
components/config/include/board_config.h
Executable file → Normal 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
4
components/evse/CMakeLists.txt
Executable file → Normal 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
0
components/evse/evse_api.c
Executable file → Normal file
506
components/evse/evse_config.c
Executable file → Normal file
506
components/evse/evse_config.c
Executable file → Normal 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);
|
||||
|
||||
// Socket outlet
|
||||
err = nvs_get_u8(nvs, "socket_outlet", &u8);
|
||||
socket_outlet = (err == ESP_OK && u8) && board_config.proximity;
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
nvs_set_u8(nvs, "socket_outlet", socket_outlet);
|
||||
needs_commit = true;
|
||||
}
|
||||
|
||||
// RCM
|
||||
err = nvs_get_u8(nvs, "rcm", &u8);
|
||||
rcm = (err == ESP_OK && u8) && board_config.rcm;
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
nvs_set_u8(nvs, "rcm", rcm);
|
||||
needs_commit = true;
|
||||
}
|
||||
|
||||
// Temp threshold
|
||||
err = nvs_get_u8(nvs, "temp_threshold", &u8);
|
||||
temp_threshold = (err == ESP_OK && u8 >= 40 && u8 <= 80) ? u8 : 60;
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
nvs_set_u8(nvs, "temp_threshold", temp_threshold);
|
||||
needs_commit = true;
|
||||
}
|
||||
|
||||
// Optional limits
|
||||
if (nvs_get_u32(nvs, "def_cons_lim", &u32) == ESP_OK)
|
||||
evse_set_consumption_limit(u32);
|
||||
|
||||
if (nvs_get_u32(nvs, "def_ch_time_lim", &u32) == ESP_OK)
|
||||
evse_set_charging_time_limit(u32);
|
||||
|
||||
if (nvs_get_u16(nvs, "def_un_pwr_lim", &u16) == ESP_OK)
|
||||
evse_set_under_power_limit(u16);
|
||||
|
||||
// Availability (persist)
|
||||
if (nvs_get_u8(nvs, "available", &u8_bool) == ESP_OK && u8_bool <= 1)
|
||||
{
|
||||
is_available = (u8_bool != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
is_available = true; // default
|
||||
nvs_set_u8(nvs, "available", (uint8_t)is_available);
|
||||
needs_commit = true;
|
||||
ESP_LOGW(TAG, "Missing 'available' -> default=true (persisted).");
|
||||
}
|
||||
|
||||
// Enabled (persist)
|
||||
if (nvs_get_u8(nvs, "enabled", &u8_bool) == ESP_OK && u8_bool <= 1)
|
||||
{
|
||||
is_enabled = (u8_bool != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
is_enabled = true; // default
|
||||
nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled);
|
||||
needs_commit = true;
|
||||
ESP_LOGW(TAG, "Missing 'enabled' -> default=true (persisted).");
|
||||
}
|
||||
|
||||
if (needs_commit)
|
||||
{
|
||||
err = nvs_commit(nvs);
|
||||
if (err == ESP_OK)
|
||||
if (wanted && !board_config.proximity)
|
||||
{
|
||||
ESP_LOGD(TAG, "Configuration committed to NVS.");
|
||||
// NVS dizia 1, mas HW não suporta -> runtime false e persistir 0
|
||||
socket_outlet = false;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "socket_outlet", 0, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist socket_outlet=0 (capability mismatch): %s",
|
||||
esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "socket_outlet requested but HW has no proximity -> forcing false (sync persisted)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to commit configuration to NVS: %s", esp_err_to_name(err));
|
||||
socket_outlet = wanted;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
socket_outlet = false;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "socket_outlet", 0, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist socket_outlet default=0: %s", esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Missing/invalid socket_outlet (%s) -> default=false (sync persisted).",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// RCM (persisted) + capability gate
|
||||
// -----------------------------------------
|
||||
err = storage_get_u8_sync(NVS_NAMESPACE, "rcm", &u8, rd_to);
|
||||
if (err == ESP_OK && u8 <= 1)
|
||||
{
|
||||
bool wanted = (u8 != 0);
|
||||
|
||||
if (wanted && !board_config.rcm)
|
||||
{
|
||||
rcm = false;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist rcm=0 (capability mismatch): %s",
|
||||
esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "rcm requested but HW has no RCM -> forcing false (sync persisted)");
|
||||
}
|
||||
else
|
||||
{
|
||||
rcm = wanted;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rcm = false;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist rcm default=0: %s", esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Missing/invalid rcm (%s) -> default=false (sync persisted).",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Temp threshold (persisted)
|
||||
// -----------------------------------------
|
||||
err = storage_get_u8_sync(NVS_NAMESPACE, "temp_threshold", &u8, rd_to);
|
||||
if (err == ESP_OK && u8 >= 40 && u8 <= 80)
|
||||
{
|
||||
temp_threshold = u8;
|
||||
}
|
||||
else
|
||||
{
|
||||
temp_threshold = 60;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "temp_threshold", temp_threshold, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist temp_threshold=%u: %s",
|
||||
(unsigned)temp_threshold, esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Invalid/missing temp_threshold (%s) -> default=60 (sync persisted).",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Availability (persisted) [0/1]
|
||||
// -----------------------------------------
|
||||
err = storage_get_u8_sync(NVS_NAMESPACE, "available", &u8, rd_to);
|
||||
if (err == ESP_OK && u8 <= 1)
|
||||
{
|
||||
is_available = (u8 != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
is_available = true;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "available", 1, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist available=1: %s", esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Missing/invalid 'available' (%s) -> default=true (sync persisted).",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Enabled (persisted) [0/1]
|
||||
// -----------------------------------------
|
||||
err = storage_get_u8_sync(NVS_NAMESPACE, "enabled", &u8, rd_to);
|
||||
if (err == ESP_OK && u8 <= 1)
|
||||
{
|
||||
is_enabled = (u8 != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
is_enabled = true;
|
||||
|
||||
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "enabled", 1, wr_to);
|
||||
if (se != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist enabled=1: %s", esp_err_to_name(se));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Missing/invalid 'enabled' (%s) -> default=true (sync persisted).",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Flush explícito no boot:
|
||||
// - ajuda a garantir commit determinístico antes do resto do sistema avançar
|
||||
// - mantém-se útil mesmo com setters sync se o teu storage ainda estiver com debounce interno
|
||||
esp_err_t fe = storage_flush_sync(wr_to);
|
||||
if (fe != ESP_OK)
|
||||
ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe));
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Charging current getters/setters
|
||||
// ========================
|
||||
uint8_t evse_get_max_charging_current(void)
|
||||
{
|
||||
return max_charging_current;
|
||||
}
|
||||
uint8_t evse_get_max_charging_current(void) { return max_charging_current; }
|
||||
|
||||
esp_err_t evse_set_max_charging_current(uint8_t value)
|
||||
{
|
||||
if (value < MIN_CHARGING_CURRENT_LIMIT || value > MAX_CHARGING_CURRENT_LIMIT)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
max_charging_current = value;
|
||||
evse_set_runtime_charging_current(value);
|
||||
nvs_set_u8(nvs, "max_chrg_curr", value);
|
||||
return nvs_commit(nvs);
|
||||
}
|
||||
|
||||
uint16_t evse_get_charging_current(void)
|
||||
{
|
||||
return charging_current;
|
||||
}
|
||||
uint16_t evse_get_charging_current(void) { return charging_current; }
|
||||
|
||||
esp_err_t evse_set_charging_current(uint16_t value)
|
||||
{
|
||||
if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current))
|
||||
if (value < MIN_CHARGING_CURRENT_LIMIT || value > max_charging_current)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
|
||||
if (value == charging_current)
|
||||
{
|
||||
evse_set_runtime_charging_current(value);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
charging_current = value;
|
||||
nvs_set_u16(nvs, "def_chrg_curr", value);
|
||||
return nvs_commit(nvs);
|
||||
}
|
||||
|
||||
uint16_t evse_get_default_charging_current(void)
|
||||
{
|
||||
uint16_t value;
|
||||
if (nvs_get_u16(nvs, "def_chrg_curr", &value) == ESP_OK)
|
||||
return value;
|
||||
return charging_current;
|
||||
}
|
||||
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_chrg_curr", value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
// Em runtime, isto pode falhar por fila cheia. RAM fica correta; persistência é best-effort.
|
||||
ESP_LOGE(TAG, "Failed to persist def_chrg_curr async=%u: %s", (unsigned)value, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t evse_set_default_charging_current(uint16_t value)
|
||||
{
|
||||
if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current))
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
nvs_set_u16(nvs, "def_chrg_curr", value);
|
||||
return nvs_commit(nvs);
|
||||
evse_set_runtime_charging_current(value);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ========================
|
||||
@@ -218,135 +292,119 @@ esp_err_t evse_set_default_charging_current(uint16_t value)
|
||||
void evse_set_runtime_charging_current(uint16_t value)
|
||||
{
|
||||
if (value > max_charging_current)
|
||||
{
|
||||
value = max_charging_current;
|
||||
}
|
||||
else if (value < MIN_CHARGING_CURRENT_LIMIT)
|
||||
{
|
||||
value = MIN_CHARGING_CURRENT_LIMIT;
|
||||
}
|
||||
|
||||
charging_current_runtime = value;
|
||||
|
||||
ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime);
|
||||
|
||||
evse_config_event_data_t evt = {
|
||||
.charging = evse_state_is_charging(evse_get_state()),
|
||||
.hw_max_current = (float)evse_get_max_charging_current(),
|
||||
.runtime_current = (float)evse_get_runtime_charging_current(),
|
||||
.timestamp_us = esp_timer_get_time()};
|
||||
|
||||
esp_event_post(EVSE_EVENTS,
|
||||
EVSE_EVENT_CONFIG_UPDATED,
|
||||
&evt,
|
||||
sizeof(evt),
|
||||
portMAX_DELAY);
|
||||
}
|
||||
|
||||
uint16_t evse_get_runtime_charging_current(void)
|
||||
{
|
||||
return charging_current_runtime;
|
||||
}
|
||||
uint16_t evse_get_runtime_charging_current(void) { return charging_current_runtime; }
|
||||
|
||||
// ========================
|
||||
// Socket outlet
|
||||
// ========================
|
||||
bool evse_get_socket_outlet(void)
|
||||
{
|
||||
return socket_outlet;
|
||||
}
|
||||
bool evse_get_socket_outlet(void) { return socket_outlet; }
|
||||
|
||||
esp_err_t evse_set_socket_outlet(bool value)
|
||||
{
|
||||
if (value && !board_config.proximity)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
if (value == socket_outlet)
|
||||
return ESP_OK;
|
||||
|
||||
socket_outlet = value;
|
||||
nvs_set_u8(nvs, "socket_outlet", value);
|
||||
return nvs_commit(nvs);
|
||||
|
||||
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "socket_outlet", (uint8_t)value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist socket_outlet async=%u: %s", (unsigned)value, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// RCM
|
||||
// ========================
|
||||
bool evse_is_rcm(void)
|
||||
{
|
||||
return rcm;
|
||||
}
|
||||
bool evse_is_rcm(void) { return rcm; }
|
||||
|
||||
esp_err_t evse_set_rcm(bool value)
|
||||
{
|
||||
if (value && !board_config.rcm)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
if (value == rcm)
|
||||
return ESP_OK;
|
||||
|
||||
rcm = value;
|
||||
nvs_set_u8(nvs, "rcm", value);
|
||||
return nvs_commit(nvs);
|
||||
|
||||
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "rcm", (uint8_t)value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist rcm async=%u: %s", (unsigned)value, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Temperature
|
||||
// ========================
|
||||
uint8_t evse_get_temp_threshold(void)
|
||||
{
|
||||
return temp_threshold;
|
||||
}
|
||||
uint8_t evse_get_temp_threshold(void) { return temp_threshold; }
|
||||
|
||||
esp_err_t evse_set_temp_threshold(uint8_t value)
|
||||
{
|
||||
if (value < 40 || value > 80)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
if (value == temp_threshold)
|
||||
return ESP_OK;
|
||||
|
||||
temp_threshold = value;
|
||||
nvs_set_u8(nvs, "temp_threshold", value);
|
||||
return nvs_commit(nvs);
|
||||
|
||||
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "temp_threshold", value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist temp_threshold async=%u: %s", (unsigned)value, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Availability
|
||||
// ========================
|
||||
bool evse_config_is_available(void)
|
||||
{
|
||||
return is_available;
|
||||
}
|
||||
bool evse_config_is_available(void) { return is_available; }
|
||||
|
||||
void evse_config_set_available(bool available)
|
||||
{
|
||||
is_available = available ? true : false;
|
||||
bool newv = available;
|
||||
if (newv == is_available)
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_set_u8(nvs, "available", (uint8_t)is_available);
|
||||
if (err == ESP_OK)
|
||||
err = nvs_commit(nvs);
|
||||
is_available = newv;
|
||||
|
||||
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "available", (uint8_t)is_available);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist 'available': %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
evse_available_event_data_t e = {
|
||||
.available = is_available,
|
||||
.timestamp_us = esp_timer_get_time()};
|
||||
esp_event_post(EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
|
||||
ESP_LOGE(TAG, "Failed to persist 'available' async=%u: %s", (unsigned)is_available, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Enable/Disable
|
||||
// ========================
|
||||
bool evse_config_is_enabled(void)
|
||||
{
|
||||
return is_enabled;
|
||||
}
|
||||
bool evse_config_is_enabled(void) { return is_enabled; }
|
||||
|
||||
void evse_config_set_enabled(bool enabled)
|
||||
{
|
||||
is_enabled = enabled ? true : false;
|
||||
bool newv = enabled;
|
||||
if (newv == is_enabled)
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled);
|
||||
if (err == ESP_OK)
|
||||
err = nvs_commit(nvs);
|
||||
is_enabled = newv;
|
||||
|
||||
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "enabled", (uint8_t)is_enabled);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist 'enabled': %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
evse_enable_event_data_t e = {
|
||||
.enabled = is_enabled,
|
||||
.timestamp_us = esp_timer_get_time()};
|
||||
esp_event_post(EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
|
||||
ESP_LOGE(TAG, "Failed to persist 'enabled' async=%u: %s", (unsigned)is_enabled, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
53
components/evse/evse_core.c
Executable file → Normal file
53
components/evse/evse_core.c
Executable file → Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
369
components/evse/evse_error.c
Executable file → Normal file
369
components/evse/evse_error.c
Executable file → Normal 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)
|
||||
// Se existem erros reais, o visível segue imediatamente
|
||||
if (raw_bits != 0)
|
||||
{
|
||||
bool first_time = false;
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
if (!(error_bits & EVSE_ERR_PILOT_FAULT_BIT))
|
||||
{
|
||||
error_cleared = false;
|
||||
error_bits |= EVSE_ERR_PILOT_FAULT_BIT;
|
||||
first_time = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (first_time)
|
||||
{
|
||||
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pilot voltou a nível válido → limpa erro de pilot fault
|
||||
evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT);
|
||||
}
|
||||
|
||||
// 2) Falta de -12V durante PWM (C ou D)
|
||||
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v)
|
||||
{
|
||||
bool first_time = false;
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
if (!(error_bits & EVSE_ERR_DIODE_SHORT_BIT))
|
||||
{
|
||||
error_cleared = false;
|
||||
error_bits |= EVSE_ERR_DIODE_SHORT_BIT;
|
||||
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000);
|
||||
first_time = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (first_time)
|
||||
{
|
||||
ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Se já não estamos em C/D sem -12V, limpa o erro de diodo curto
|
||||
evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_temperature_check(void)
|
||||
{
|
||||
float temp_c = ntc_temp_sensor();
|
||||
uint8_t threshold = evse_get_temp_threshold();
|
||||
|
||||
ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C",
|
||||
temp_c, threshold);
|
||||
|
||||
// Temperatura inválida -> erro de sensor
|
||||
if (temp_c < -40.0f || temp_c > 150.0f)
|
||||
{
|
||||
bool first_time = false;
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
if (!(error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT))
|
||||
{
|
||||
error_cleared = false;
|
||||
error_bits |= EVSE_ERR_TEMPERATURE_FAULT_BIT;
|
||||
first_time = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (first_time)
|
||||
{
|
||||
ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado");
|
||||
}
|
||||
visible_bits = raw_bits;
|
||||
clear_deadline = 0;
|
||||
error_cleared = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Leitura válida -> limpa erro de sensor
|
||||
evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT);
|
||||
|
||||
// Temperatura máxima
|
||||
if (temp_c >= threshold)
|
||||
// raw_bits == 0
|
||||
if (visible_bits == 0)
|
||||
{
|
||||
bool first_time = false;
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
if (!(error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT))
|
||||
{
|
||||
error_cleared = false;
|
||||
error_bits |= EVSE_ERR_TEMPERATURE_HIGH_BIT;
|
||||
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000);
|
||||
first_time = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (first_time)
|
||||
{
|
||||
ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C",
|
||||
temp_c, threshold);
|
||||
}
|
||||
clear_deadline = 0;
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
// Ainda há erro visível (holdoff). Arma deadline 1x.
|
||||
if (clear_deadline == 0)
|
||||
{
|
||||
evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT);
|
||||
clear_deadline = now + pdMS_TO_TICKS(EVSE_ERROR_COOLDOWN_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Expirou -> limpar finalmente
|
||||
if ((int32_t)(now - clear_deadline) >= 0)
|
||||
{
|
||||
visible_bits = 0;
|
||||
clear_deadline = 0;
|
||||
error_cleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// API pública
|
||||
// ----------------------------------------------------
|
||||
void evse_error_init(void)
|
||||
{
|
||||
uint32_t old_vis, new_vis, changed;
|
||||
bool post = false;
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
|
||||
old_vis = visible_bits;
|
||||
|
||||
raw_bits = 0;
|
||||
visible_bits = 0;
|
||||
clear_deadline = 0;
|
||||
error_cleared = false;
|
||||
|
||||
new_vis = visible_bits;
|
||||
changed = old_vis ^ new_vis;
|
||||
post = (changed != 0);
|
||||
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (post)
|
||||
{
|
||||
evse_error_post_event(new_vis, changed);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t evse_get_error(void)
|
||||
{
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
uint32_t val = error_bits;
|
||||
uint32_t val = visible_bits;
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
return val;
|
||||
}
|
||||
|
||||
bool evse_error_is_active(void)
|
||||
{
|
||||
return evse_get_error() != 0;
|
||||
}
|
||||
|
||||
uint32_t evse_error_get_bits(void)
|
||||
{
|
||||
return evse_get_error();
|
||||
}
|
||||
|
||||
bool evse_error_cleared_flag(void)
|
||||
{
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
@@ -169,61 +164,147 @@ void evse_error_reset_flag(void)
|
||||
|
||||
void evse_error_set(uint32_t bitmask)
|
||||
{
|
||||
uint32_t old_vis, new_vis, changed;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
|
||||
error_cleared = false;
|
||||
error_bits |= bitmask;
|
||||
old_vis = visible_bits;
|
||||
|
||||
if (bitmask & EVSE_ERR_AUTO_CLEAR_BITS)
|
||||
{
|
||||
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); // 60s
|
||||
}
|
||||
raw_bits |= bitmask;
|
||||
// se aparece qualquer erro, o "cleared" deixa de ser verdade
|
||||
error_cleared = false;
|
||||
|
||||
reconcile_visible_locked(now);
|
||||
|
||||
new_vis = visible_bits;
|
||||
changed = old_vis ^ new_vis;
|
||||
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (changed != 0)
|
||||
{
|
||||
evse_error_post_event(new_vis, changed);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_error_clear(uint32_t bitmask)
|
||||
{
|
||||
uint32_t old_vis, new_vis, changed;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
|
||||
bool had_error = (error_bits != 0);
|
||||
error_bits &= ~bitmask;
|
||||
old_vis = visible_bits;
|
||||
|
||||
if (had_error && error_bits == 0)
|
||||
{
|
||||
error_cleared = true;
|
||||
}
|
||||
raw_bits &= ~bitmask;
|
||||
|
||||
// ✅ Aqui é onde o “60s depois do erro desaparecer” é armado:
|
||||
// quando raw_bits chega a 0, reconcile arma clear_deadline (uma vez)
|
||||
reconcile_visible_locked(now);
|
||||
|
||||
new_vis = visible_bits;
|
||||
changed = old_vis ^ new_vis;
|
||||
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (changed != 0)
|
||||
{
|
||||
evse_error_post_event(new_vis, changed);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_error_tick(void)
|
||||
{
|
||||
uint32_t old_vis, new_vis, changed;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
|
||||
portENTER_CRITICAL(&error_mux);
|
||||
|
||||
if ((error_bits & EVSE_ERR_AUTO_CLEAR_BITS) &&
|
||||
auto_clear_timeout != 0 &&
|
||||
xTaskGetTickCount() >= auto_clear_timeout)
|
||||
{
|
||||
error_bits &= ~EVSE_ERR_AUTO_CLEAR_BITS;
|
||||
|
||||
if (error_bits == 0)
|
||||
{
|
||||
error_cleared = true;
|
||||
}
|
||||
|
||||
auto_clear_timeout = 0;
|
||||
}
|
||||
old_vis = visible_bits;
|
||||
reconcile_visible_locked(now);
|
||||
new_vis = visible_bits;
|
||||
changed = old_vis ^ new_vis;
|
||||
|
||||
portEXIT_CRITICAL(&error_mux);
|
||||
|
||||
if (changed != 0)
|
||||
{
|
||||
evse_error_post_event(new_vis, changed);
|
||||
}
|
||||
}
|
||||
|
||||
bool evse_error_is_active(void)
|
||||
// ----------------------------------------------------
|
||||
// Checks (raw -> set/clear)
|
||||
// ----------------------------------------------------
|
||||
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v)
|
||||
{
|
||||
return evse_get_error() != 0;
|
||||
ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s",
|
||||
pilot_voltage, is_n12v ? "true" : "false");
|
||||
|
||||
// 1) Falha elétrica geral no pilot
|
||||
if (pilot_voltage == PILOT_VOLTAGE_1)
|
||||
{
|
||||
if (!raw_has_bit(EVSE_ERR_PILOT_FAULT_BIT))
|
||||
{
|
||||
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)");
|
||||
}
|
||||
evse_error_set(EVSE_ERR_PILOT_FAULT_BIT);
|
||||
}
|
||||
else
|
||||
{
|
||||
evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT);
|
||||
}
|
||||
|
||||
// 2) Falta de -12V durante PWM (C ou D)
|
||||
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v)
|
||||
{
|
||||
if (!raw_has_bit(EVSE_ERR_DIODE_SHORT_BIT))
|
||||
{
|
||||
ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)");
|
||||
}
|
||||
evse_error_set(EVSE_ERR_DIODE_SHORT_BIT);
|
||||
}
|
||||
else
|
||||
{
|
||||
evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t evse_error_get_bits(void)
|
||||
void evse_temperature_check(void)
|
||||
{
|
||||
return evse_get_error();
|
||||
float temp_c = ntc_temp_sensor();
|
||||
uint8_t threshold = evse_get_temp_threshold();
|
||||
|
||||
ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C",
|
||||
temp_c, threshold);
|
||||
|
||||
// Temperatura inválida -> erro de sensor
|
||||
if (temp_c < -40.0f || temp_c > 150.0f)
|
||||
{
|
||||
if (!raw_has_bit(EVSE_ERR_TEMPERATURE_FAULT_BIT))
|
||||
{
|
||||
ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado");
|
||||
}
|
||||
evse_error_set(EVSE_ERR_TEMPERATURE_FAULT_BIT);
|
||||
return;
|
||||
}
|
||||
|
||||
// Leitura válida -> limpa erro de sensor
|
||||
evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT);
|
||||
|
||||
// Temperatura máxima
|
||||
if (temp_c >= threshold)
|
||||
{
|
||||
if (!raw_has_bit(EVSE_ERR_TEMPERATURE_HIGH_BIT))
|
||||
{
|
||||
ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C",
|
||||
temp_c, threshold);
|
||||
}
|
||||
evse_error_set(EVSE_ERR_TEMPERATURE_HIGH_BIT);
|
||||
}
|
||||
else
|
||||
{
|
||||
evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
0
components/evse/evse_events.c
Executable file → Normal file
0
components/evse/evse_events.c
Executable file → Normal file
129
components/evse/evse_fsm.c
Executable file → Normal file
129
components/evse/evse_fsm.c
Executable file → Normal 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!");
|
||||
}
|
||||
ac_relay_set_state(false);
|
||||
else
|
||||
{
|
||||
ac_relay_set_state(false);
|
||||
}
|
||||
|
||||
// Em erro, garantir pilot OFF (não PWM / não +12V)
|
||||
pilot_set_level(true);
|
||||
|
||||
if (board_config.socket_lock && socket_outlet)
|
||||
{
|
||||
socket_lock_set_locked(false);
|
||||
@@ -55,14 +60,16 @@ static void update_outputs(evse_state_t state)
|
||||
return;
|
||||
}
|
||||
|
||||
// Fluxo normal
|
||||
switch (state)
|
||||
{
|
||||
case EVSE_STATE_A:
|
||||
case EVSE_STATE_E:
|
||||
case EVSE_STATE_F:
|
||||
ac_relay_set_state(false);
|
||||
|
||||
// A → pilot alto (+12V), E/F → pilot OFF
|
||||
pilot_set_level(state == EVSE_STATE_A);
|
||||
|
||||
if (board_config.socket_lock && socket_outlet)
|
||||
{
|
||||
socket_lock_set_locked(false);
|
||||
@@ -72,66 +79,77 @@ static void update_outputs(evse_state_t state)
|
||||
case EVSE_STATE_B1:
|
||||
pilot_set_level(true);
|
||||
ac_relay_set_state(false);
|
||||
|
||||
if (board_config.socket_lock && socket_outlet)
|
||||
{
|
||||
socket_lock_set_locked(true);
|
||||
}
|
||||
|
||||
if (rcm_test())
|
||||
{
|
||||
// ESP_LOGI(TAG, "RCM self test passed");
|
||||
}
|
||||
else
|
||||
{
|
||||
// ESP_LOGW(TAG, "RCM self test failed");
|
||||
}
|
||||
(void)rcm_test();
|
||||
break;
|
||||
|
||||
case EVSE_STATE_B2:
|
||||
pilot_set_amps(MIN(current, cable_max_current));
|
||||
ac_relay_set_state(false);
|
||||
|
||||
if (board_config.socket_lock && socket_outlet)
|
||||
{
|
||||
socket_lock_set_locked(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case EVSE_STATE_C1:
|
||||
case EVSE_STATE_D1:
|
||||
{
|
||||
pilot_set_amps(MIN(current, cable_max_current));
|
||||
ac_relay_set_state(false);
|
||||
c1_d1_waiting = true;
|
||||
c1_d1_relay_to = xTaskGetTickCount() + pdMS_TO_TICKS(6000);
|
||||
|
||||
if (board_config.socket_lock && socket_outlet)
|
||||
{
|
||||
socket_lock_set_locked(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case EVSE_STATE_C2:
|
||||
case EVSE_STATE_D2:
|
||||
pilot_set_amps(MIN(current, cable_max_current));
|
||||
ac_relay_set_state(true);
|
||||
|
||||
if (board_config.socket_lock && socket_outlet)
|
||||
{
|
||||
socket_lock_set_locked(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// FSM principal
|
||||
/**
|
||||
* @brief Máquina de estados principal do EVSE (IEC 61851).
|
||||
*/
|
||||
void evse_fsm_process(
|
||||
pilot_voltage_t pilot_voltage,
|
||||
bool authorized,
|
||||
bool available,
|
||||
bool enabled)
|
||||
{
|
||||
// Proteção total: erro força F sempre!
|
||||
if (evse_get_error() != 0)
|
||||
// 1) Erros globais: dominam qualquer outra lógica
|
||||
uint32_t err_bits = evse_get_error();
|
||||
if (err_bits != 0)
|
||||
{
|
||||
if (evse_get_state() != EVSE_STATE_F)
|
||||
evse_state_t forced_state =
|
||||
(err_bits & EVSE_ERR_PILOT_FAULT_BIT) ? EVSE_STATE_E : EVSE_STATE_F;
|
||||
|
||||
if (evse_get_state() != forced_state)
|
||||
{
|
||||
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado FAULT (F)");
|
||||
evse_set_state(EVSE_STATE_F);
|
||||
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado %s",
|
||||
evse_state_to_str(forced_state));
|
||||
evse_set_state(forced_state);
|
||||
}
|
||||
update_outputs(EVSE_STATE_F);
|
||||
|
||||
update_outputs(forced_state);
|
||||
return;
|
||||
}
|
||||
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
evse_state_t prev = evse_get_state();
|
||||
evse_state_t curr = prev;
|
||||
evse_state_t curr = evse_get_state();
|
||||
|
||||
switch (curr)
|
||||
{
|
||||
@@ -153,17 +171,25 @@ void evse_fsm_process(
|
||||
evse_set_state(EVSE_STATE_F);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (pilot_voltage)
|
||||
{
|
||||
case PILOT_VOLTAGE_12:
|
||||
evse_set_state(EVSE_STATE_A);
|
||||
break;
|
||||
|
||||
case PILOT_VOLTAGE_9:
|
||||
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
|
||||
break;
|
||||
|
||||
case PILOT_VOLTAGE_6:
|
||||
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
|
||||
break;
|
||||
|
||||
case PILOT_VOLTAGE_3:
|
||||
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -171,52 +197,59 @@ void evse_fsm_process(
|
||||
|
||||
case EVSE_STATE_C1:
|
||||
case EVSE_STATE_D1:
|
||||
if (c1_d1_waiting && now >= c1_d1_relay_to)
|
||||
{
|
||||
ac_relay_set_state(false);
|
||||
c1_d1_waiting = false;
|
||||
if (!available)
|
||||
{
|
||||
evse_set_state(EVSE_STATE_F);
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((fallthrough));
|
||||
|
||||
case EVSE_STATE_C2:
|
||||
case EVSE_STATE_D2:
|
||||
if (!enabled || !available)
|
||||
if (!available)
|
||||
{
|
||||
evse_set_state((curr == EVSE_STATE_D2 || curr == EVSE_STATE_D1)
|
||||
? EVSE_STATE_D1
|
||||
: EVSE_STATE_C1);
|
||||
evse_set_state(EVSE_STATE_F);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
if (curr == EVSE_STATE_C2)
|
||||
{
|
||||
evse_set_state(EVSE_STATE_C1);
|
||||
}
|
||||
else if (curr == EVSE_STATE_D2)
|
||||
{
|
||||
evse_set_state(EVSE_STATE_D1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (pilot_voltage)
|
||||
{
|
||||
case PILOT_VOLTAGE_6:
|
||||
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
|
||||
break;
|
||||
|
||||
case PILOT_VOLTAGE_3:
|
||||
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
|
||||
break;
|
||||
|
||||
case PILOT_VOLTAGE_9:
|
||||
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
|
||||
break;
|
||||
|
||||
case PILOT_VOLTAGE_12:
|
||||
evse_set_state(EVSE_STATE_A);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EVSE_STATE_E:
|
||||
// Estado elétrico grave: só reset manual
|
||||
// ✅ Agora recupera como F: se disponível e sem erro -> volta a A
|
||||
if (available && evse_get_error() == 0)
|
||||
{
|
||||
evse_set_state(EVSE_STATE_A);
|
||||
}
|
||||
break;
|
||||
|
||||
case EVSE_STATE_F:
|
||||
// Fault: só sai se disponível e sem erro
|
||||
if (available && evse_get_error() == 0)
|
||||
{
|
||||
evse_set_state(EVSE_STATE_A);
|
||||
|
||||
0
components/evse/evse_hardware.c
Executable file → Normal file
0
components/evse/evse_hardware.c
Executable file → Normal file
262
components/evse/evse_limits.c
Executable file → Normal file
262
components/evse/evse_limits.c
Executable file → Normal 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; // Wh
|
||||
static uint32_t charging_time_limit = 0; // seconds
|
||||
static uint16_t under_power_limit = 0; // W
|
||||
|
||||
static bool limit_reached = false;
|
||||
static uint32_t consumption_limit = 0; // Energy limit in Wh
|
||||
static uint32_t charging_time_limit = 0; // Time limit in seconds
|
||||
static uint16_t under_power_limit = 0; // Minimum acceptable power in W
|
||||
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
|
||||
static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(1000); }
|
||||
|
||||
// ========================
|
||||
// Limit status flag
|
||||
// ========================
|
||||
// ---------------------------------
|
||||
// Init + defaults
|
||||
// ---------------------------------
|
||||
esp_err_t evse_limits_init(void)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed");
|
||||
ESP_LOGI(TAG, "EVSE limits init OK (storage-backed)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void evse_limits_check_defaults(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
bool needs_flush = false;
|
||||
|
||||
uint32_t u32 = 0;
|
||||
uint16_t u16 = 0;
|
||||
|
||||
ESP_LOGD(TAG, "Checking default limits...");
|
||||
|
||||
// Consumption limit (Wh) default = 0 (disabled)
|
||||
err = storage_get_u32_sync(NVS_NAMESPACE, "def_cons_lim", &u32, BOOT_TO());
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
consumption_limit = u32;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
}
|
||||
else
|
||||
{
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
consumption_limit = 0;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
|
||||
(void)storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", 0);
|
||||
needs_flush = true;
|
||||
ESP_LOGW(TAG, "Missing def_cons_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Charging time limit (s) default = 0 (disabled)
|
||||
err = storage_get_u32_sync(NVS_NAMESPACE, "def_ch_time_lim", &u32, BOOT_TO());
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
charging_time_limit = u32;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
}
|
||||
else
|
||||
{
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
charging_time_limit = 0;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
|
||||
(void)storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", 0);
|
||||
needs_flush = true;
|
||||
ESP_LOGW(TAG, "Missing def_ch_time_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Under-power limit (W) default = 0 (disabled)
|
||||
err = storage_get_u16_sync(NVS_NAMESPACE, "def_un_pwr_lim", &u16, BOOT_TO());
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
under_power_limit = u16;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
}
|
||||
else
|
||||
{
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
under_power_limit = 0;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
|
||||
(void)storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", 0);
|
||||
needs_flush = true;
|
||||
ESP_LOGW(TAG, "Missing def_un_pwr_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
if (needs_flush)
|
||||
{
|
||||
esp_err_t fe = storage_flush_sync(TO_TICKS_MS(2000));
|
||||
if (fe != ESP_OK)
|
||||
ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe));
|
||||
else
|
||||
ESP_LOGD(TAG, "Defaults committed (flush).");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Limit reached flag
|
||||
// ---------------------------------
|
||||
bool evse_get_limit_reached(void)
|
||||
{
|
||||
bool val;
|
||||
@@ -50,10 +139,9 @@ bool evse_is_limit_reached(void)
|
||||
return evse_get_limit_reached();
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Runtime limit accessors
|
||||
// ========================
|
||||
|
||||
// ---------------------------------
|
||||
// Consumption limit
|
||||
// ---------------------------------
|
||||
uint32_t evse_get_consumption_limit(void)
|
||||
{
|
||||
uint32_t val;
|
||||
@@ -78,30 +166,18 @@ void evse_set_consumption_limit(uint32_t value)
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
|
||||
if (err == ESP_OK)
|
||||
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
err = nvs_set_u32(h, "def_cons_lim", value);
|
||||
if (err == ESP_OK)
|
||||
err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE("EVSE_LIMITS",
|
||||
"Failed to persist consumption limit (%" PRIu32 " Wh): %s",
|
||||
value, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE("EVSE_LIMITS",
|
||||
"Failed to open NVS for consumption limit: %s",
|
||||
esp_err_to_name(err));
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to persist consumption limit (%" PRIu32 " Wh): %s",
|
||||
value, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Charging time limit
|
||||
// ---------------------------------
|
||||
uint32_t evse_get_charging_time_limit(void)
|
||||
{
|
||||
uint32_t val;
|
||||
@@ -126,30 +202,18 @@ void evse_set_charging_time_limit(uint32_t value)
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
|
||||
if (err == ESP_OK)
|
||||
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
err = nvs_set_u32(h, "def_ch_time_lim", value);
|
||||
if (err == ESP_OK)
|
||||
err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE("EVSE_LIMITS",
|
||||
"Failed to persist charging time limit (%" PRIu32 " s): %s",
|
||||
value, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE("EVSE_LIMITS",
|
||||
"Failed to open NVS for charging time limit: %s",
|
||||
esp_err_to_name(err));
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to persist charging time limit (%" PRIu32 " s): %s",
|
||||
value, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Under-power limit
|
||||
// ---------------------------------
|
||||
uint16_t evse_get_under_power_limit(void)
|
||||
{
|
||||
uint16_t val;
|
||||
@@ -174,82 +238,64 @@ void evse_set_under_power_limit(uint16_t value)
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
|
||||
if (err == ESP_OK)
|
||||
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
err = nvs_set_u16(h, "def_un_pwr_lim", value);
|
||||
if (err == ESP_OK)
|
||||
err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE("EVSE_LIMITS",
|
||||
"Failed to persist under-power limit (%" PRIu32 " W): %s",
|
||||
(uint32_t)value, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE("EVSE_LIMITS",
|
||||
"Failed to open NVS for under-power limit: %s",
|
||||
esp_err_to_name(err));
|
||||
ESP_LOGE(TAG,
|
||||
"Failed to persist under-power limit (%" PRIu32 " W): %s",
|
||||
(uint32_t)value, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Limit checking logic
|
||||
// ========================
|
||||
|
||||
// ---------------------------------
|
||||
// Runtime check
|
||||
// ---------------------------------
|
||||
void evse_limits_check(void)
|
||||
{
|
||||
// Só faz sentido durante carregamento
|
||||
// Só faz sentido quando há energia ativa (C2/D2)
|
||||
if (!evse_state_is_charging(evse_get_state()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
evse_session_t sess;
|
||||
if (!evse_session_get(&sess) || !sess.is_current)
|
||||
{
|
||||
// Sem sessão ativa → nada a fazer
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t cons_lim;
|
||||
uint32_t time_lim;
|
||||
uint16_t unp_lim;
|
||||
|
||||
portENTER_CRITICAL(&evse_mux);
|
||||
cons_lim = consumption_limit;
|
||||
time_lim = charging_time_limit;
|
||||
unp_lim = under_power_limit;
|
||||
portEXIT_CRITICAL(&evse_mux);
|
||||
|
||||
bool reached = false;
|
||||
|
||||
// 1) Limite de energia (Wh)
|
||||
if (consumption_limit > 0 && sess.energy_wh >= consumption_limit)
|
||||
if (cons_lim > 0 && sess.energy_wh >= cons_lim)
|
||||
{
|
||||
ESP_LOGW("EVSE_LIMITS",
|
||||
"Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
|
||||
sess.energy_wh, consumption_limit);
|
||||
ESP_LOGW(TAG, "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
|
||||
sess.energy_wh, cons_lim);
|
||||
reached = true;
|
||||
}
|
||||
|
||||
// 2) Limite de tempo (s)
|
||||
if (charging_time_limit > 0 && sess.duration_s >= charging_time_limit)
|
||||
if (time_lim > 0 && sess.duration_s >= time_lim)
|
||||
{
|
||||
ESP_LOGW("EVSE_LIMITS",
|
||||
"Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
|
||||
sess.duration_s, charging_time_limit);
|
||||
ESP_LOGW(TAG, "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
|
||||
sess.duration_s, time_lim);
|
||||
reached = true;
|
||||
}
|
||||
|
||||
// 3) Under-power (potência instantânea)
|
||||
uint32_t inst_power = evse_meter_get_instant_power();
|
||||
if (under_power_limit > 0 && inst_power < under_power_limit)
|
||||
int32_t p = evse_meter_get_instant_power();
|
||||
uint32_t inst_power = (p > 0) ? (uint32_t)p : 0;
|
||||
|
||||
if (unp_lim > 0 && inst_power < (uint32_t)unp_lim)
|
||||
{
|
||||
ESP_LOGW("EVSE_LIMITS",
|
||||
"Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
|
||||
(uint32_t)inst_power,
|
||||
(uint32_t)under_power_limit);
|
||||
ESP_LOGW(TAG, "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
|
||||
inst_power, (uint32_t)unp_lim);
|
||||
reached = true;
|
||||
}
|
||||
|
||||
if (reached)
|
||||
{
|
||||
evse_set_limit_reached(true);
|
||||
}
|
||||
}
|
||||
|
||||
358
components/evse/evse_manager.c
Executable file → Normal file
358
components/evse/evse_manager.c
Executable file → Normal 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())
|
||||
bool limit_hit = evse_is_limit_reached();
|
||||
bool can_operate = evse_config_is_available() && evse_config_is_enabled();
|
||||
|
||||
if ((has_error || limit_hit || !sched_allowed || !can_operate || local_lb_paused) &&
|
||||
evse_state_get_authorized())
|
||||
{
|
||||
evse_state_set_authorized(true);
|
||||
ESP_LOGI(TAG, "Authentication disabled → forced authorization (within schedule).");
|
||||
lb_clear_pause_state();
|
||||
ESP_LOGI(TAG,
|
||||
"[OPEN] blocking (err=%d limit=%d sched=%d operate=%d lb_paused=%d) → revoking authorization.",
|
||||
(int)has_error, (int)limit_hit, (int)sched_allowed, (int)can_operate, (int)local_lb_paused);
|
||||
evse_state_set_authorized(false);
|
||||
}
|
||||
|
||||
// Fora da janela, garantir que não fica autorizado
|
||||
if (!sched_allowed && evse_state_get_authorized())
|
||||
if (!local_lb_paused && sched_allowed && can_operate &&
|
||||
!has_error && !limit_hit &&
|
||||
!evse_state_get_authorized())
|
||||
{
|
||||
ESP_LOGI(TAG, "[SCHED] window closed (OPEN mode) → revoking authorization.");
|
||||
evse_state_set_authorized(false);
|
||||
evse_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:
|
||||
ESP_LOGI(TAG, "[OCPP] 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 ===
|
||||
|
||||
301
components/evse/evse_pilot.c
Executable file → Normal file
301
components/evse/evse_pilot.c
Executable file → Normal 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,155 +10,309 @@
|
||||
|
||||
#include "evse_pilot.h"
|
||||
#include "adc121s021_dma.h"
|
||||
#include "adc.h"
|
||||
#include "board_config.h"
|
||||
|
||||
#define PILOT_PWM_TIMER LEDC_TIMER_0
|
||||
#define PILOT_PWM_CHANNEL LEDC_CHANNEL_0
|
||||
#define PILOT_PWM_SPEED_MODE LEDC_LOW_SPEED_MODE
|
||||
#define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT
|
||||
#define PILOT_PWM_MAX_DUTY 1023
|
||||
#define PILOT_PWM_TIMER LEDC_TIMER_0
|
||||
#define PILOT_PWM_CHANNEL LEDC_CHANNEL_0
|
||||
#define PILOT_PWM_SPEED_MODE LEDC_LOW_SPEED_MODE
|
||||
#define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT
|
||||
#define PILOT_PWM_MAX_DUTY 1023
|
||||
|
||||
#define NUM_PILOT_SAMPLES 100
|
||||
#define MAX_SAMPLE_ATTEMPTS 1000
|
||||
#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior
|
||||
// --- Configuração de amostragem do Pilot ---
|
||||
#define NUM_PILOT_SAMPLES 100
|
||||
#define MAX_SAMPLE_ATTEMPTS 1000
|
||||
|
||||
#define ADC121_VREF_MV 3300
|
||||
#define ADC121_MAX 4095
|
||||
#define PILOT_SAMPLE_DELAY_US 10
|
||||
|
||||
// Percentagem para descartar extremos superior/inferior (ruído)
|
||||
#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void pilot_init(void)
|
||||
static int compare_int(const void *a, const void *b)
|
||||
{
|
||||
ledc_timer_config_t ledc_timer = {
|
||||
.speed_mode = PILOT_PWM_SPEED_MODE,
|
||||
.timer_num = PILOT_PWM_TIMER,
|
||||
.duty_resolution = PILOT_PWM_DUTY_RES,
|
||||
.freq_hz = 1000,
|
||||
.clk_cfg = LEDC_AUTO_CLK
|
||||
};
|
||||
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
|
||||
int va = *(const int *)a;
|
||||
int vb = *(const int *)b;
|
||||
if (va < vb) return -1;
|
||||
if (va > vb) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ledc_channel_config_t ledc_channel = {
|
||||
.speed_mode = PILOT_PWM_SPEED_MODE,
|
||||
.channel = PILOT_PWM_CHANNEL,
|
||||
.timer_sel = PILOT_PWM_TIMER,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.gpio_num = board_config.pilot_pwm_gpio,
|
||||
.duty = 0,
|
||||
.hpoint = 0
|
||||
};
|
||||
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
|
||||
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0));
|
||||
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();
|
||||
}
|
||||
|
||||
void pilot_set_level(bool level)
|
||||
// ---------------------
|
||||
// Inicialização PWM + ADC
|
||||
// ---------------------
|
||||
void pilot_init(void)
|
||||
{
|
||||
if (last_pilot_level == level) return;
|
||||
last_pilot_level = level;
|
||||
// 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, // 1 kHz (IEC 61851)
|
||||
.clk_cfg = LEDC_AUTO_CLK
|
||||
};
|
||||
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
|
||||
|
||||
ESP_LOGI(TAG, "Set level %d", level);
|
||||
ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, level ? 1 : 0);
|
||||
// Canal do PWM no pino configurado em board_config
|
||||
ledc_channel_config_t ledc_channel = {
|
||||
.speed_mode = PILOT_PWM_SPEED_MODE,
|
||||
.channel = PILOT_PWM_CHANNEL,
|
||||
.timer_sel = PILOT_PWM_TIMER,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.gpio_num = board_config.pilot_pwm_gpio,
|
||||
.duty = 0,
|
||||
.hpoint = 0
|
||||
};
|
||||
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// Controlo do modo do Pilot
|
||||
// ---------------------
|
||||
void pilot_set_level(bool high)
|
||||
{
|
||||
pilot_mode_t target = high ? PILOT_MODE_DC_HIGH : PILOT_MODE_DC_LOW;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
233
components/evse/evse_state.c
Executable file → Normal file
233
components/evse/evse_state.c
Executable file → Normal 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_C1:
|
||||
case EVSE_STATE_C2: return EVSE_STATE_EVENT_CHARGING;
|
||||
case EVSE_STATE_E:
|
||||
case EVSE_STATE_F: return EVSE_STATE_EVENT_FAULT;
|
||||
default: return EVSE_STATE_EVENT_IDLE;
|
||||
case EVSE_STATE_B1:
|
||||
case EVSE_STATE_B2:
|
||||
return EVSE_STATE_EVENT_WAITING;
|
||||
|
||||
case EVSE_STATE_C1:
|
||||
case EVSE_STATE_C2:
|
||||
case EVSE_STATE_D1:
|
||||
case EVSE_STATE_D2:
|
||||
return EVSE_STATE_EVENT_CHARGING;
|
||||
|
||||
case EVSE_STATE_E:
|
||||
case EVSE_STATE_F:
|
||||
return EVSE_STATE_EVENT_FAULT;
|
||||
|
||||
default:
|
||||
return EVSE_STATE_EVENT_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Public API
|
||||
// =========================
|
||||
static void post_evse_event(evse_event_id_t id, const void *data, size_t len)
|
||||
{
|
||||
esp_err_t err = esp_event_post(
|
||||
EVSE_EVENTS,
|
||||
id,
|
||||
data,
|
||||
len,
|
||||
portMAX_DELAY);
|
||||
|
||||
void evse_set_state(evse_state_t new_state) {
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "esp_event_post(id=%d) failed: %s", (int)id, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
bool evse_state_is_charging(evse_state_t state)
|
||||
{
|
||||
// “charging” == energia efetiva (relé ON)
|
||||
return (state == EVSE_STATE_C2 || state == EVSE_STATE_D2);
|
||||
}
|
||||
|
||||
bool evse_state_is_power_flowing(evse_state_t state)
|
||||
{
|
||||
return evse_state_is_charging(state);
|
||||
}
|
||||
|
||||
bool evse_state_is_requesting(evse_state_t state)
|
||||
{
|
||||
// EV pediu carga mas o relé ainda está OFF
|
||||
return (state == EVSE_STATE_C1 || state == EVSE_STATE_D1);
|
||||
}
|
||||
|
||||
bool evse_state_is_plugged(evse_state_t state)
|
||||
{
|
||||
return state == EVSE_STATE_B1 || state == EVSE_STATE_B2 ||
|
||||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
|
||||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2;
|
||||
}
|
||||
|
||||
bool evse_state_is_session(evse_state_t state)
|
||||
{
|
||||
// Sessão lógica: “autorizado/pronto” ou “a pedir/a fornecer energia”
|
||||
return (state == EVSE_STATE_B2 ||
|
||||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
|
||||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2);
|
||||
}
|
||||
|
||||
void evse_set_state(evse_state_t new_state)
|
||||
{
|
||||
bool changed = false;
|
||||
evse_state_t prev_state;
|
||||
bool start_session = false;
|
||||
bool end_session = false;
|
||||
bool end_session = false;
|
||||
|
||||
// 1) Detecta transição de estado dentro da região crítica
|
||||
portENTER_CRITICAL(&state_mux);
|
||||
prev_state = current_state;
|
||||
if (new_state != current_state) {
|
||||
// se entrou em charging pela primeira vez
|
||||
if (evse_state_is_charging(new_state) && !evse_state_is_charging(prev_state)) {
|
||||
start_session = true;
|
||||
}
|
||||
// se saiu de charging para qualquer outro
|
||||
else if (!evse_state_is_charging(new_state) && evse_state_is_charging(prev_state)) {
|
||||
end_session = true;
|
||||
}
|
||||
current_state = new_state;
|
||||
changed = true;
|
||||
}
|
||||
prev_state = current_state;
|
||||
|
||||
if (new_state != current_state)
|
||||
{
|
||||
|
||||
// Sessão começa quando entra em energia (relé ON)
|
||||
if (evse_state_is_power_flowing(new_state) && !evse_state_is_power_flowing(prev_state))
|
||||
{
|
||||
start_session = true;
|
||||
}
|
||||
// Sessão termina quando sai de energia
|
||||
else if (!evse_state_is_power_flowing(new_state) && evse_state_is_power_flowing(prev_state))
|
||||
{
|
||||
end_session = true;
|
||||
}
|
||||
|
||||
current_state = new_state;
|
||||
changed = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&state_mux);
|
||||
|
||||
// 2) Executa start/end de sessão FORA da região crítica, evitando logs/alloc dentro dela
|
||||
if (start_session) {
|
||||
// Fora da região crítica
|
||||
if (start_session)
|
||||
{
|
||||
evse_session_start();
|
||||
}
|
||||
if (end_session) {
|
||||
if (end_session)
|
||||
{
|
||||
evse_session_end();
|
||||
}
|
||||
|
||||
// 3) Se mudou o estado, faz log e dispara evento
|
||||
if (changed) {
|
||||
const char *prev_str = evse_state_to_str(prev_state);
|
||||
const char *curr_str = evse_state_to_str(new_state);
|
||||
ESP_LOGI(TAG, "State changed: %s → %s", prev_str, curr_str);
|
||||
if (changed)
|
||||
{
|
||||
ESP_LOGI(TAG, "State changed: %s → %s",
|
||||
evse_state_to_str(prev_state),
|
||||
evse_state_to_str(new_state));
|
||||
|
||||
evse_state_event_data_t evt = {
|
||||
.state = map_state_to_event(new_state)
|
||||
};
|
||||
esp_event_post(EVSE_EVENTS,
|
||||
EVSE_EVENT_STATE_CHANGED,
|
||||
&evt,
|
||||
sizeof(evt),
|
||||
portMAX_DELAY);
|
||||
.state = map_state_to_event(new_state)};
|
||||
post_evse_event(EVSE_EVENT_STATE_CHANGED, &evt, sizeof(evt));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
evse_state_t evse_get_state(void) {
|
||||
evse_state_t evse_get_state(void)
|
||||
{
|
||||
portENTER_CRITICAL(&state_mux);
|
||||
evse_state_t s = current_state;
|
||||
portEXIT_CRITICAL(&state_mux);
|
||||
return s;
|
||||
}
|
||||
|
||||
const char* evse_state_to_str(evse_state_t state) {
|
||||
switch (state) {
|
||||
case EVSE_STATE_A: return "A - EV Not Connected (12V)";
|
||||
case EVSE_STATE_B1: return "B1 - EV Connected (9V, Not Authorized)";
|
||||
case EVSE_STATE_B2: return "B2 - EV Connected (9V, Authorized and Ready)";
|
||||
case EVSE_STATE_C1: return "C1 - Charging Requested (6V, Relay Off)";
|
||||
case EVSE_STATE_C2: return "C2 - Charging Active (6V, Relay On)";
|
||||
case EVSE_STATE_D1: return "D1 - Ventilation Required (3V, Relay Off)";
|
||||
case EVSE_STATE_D2: return "D2 - Ventilation Active (3V, Relay On)";
|
||||
case EVSE_STATE_E: return "E - Error: Control Pilot Shorted to Ground (0V)";
|
||||
case EVSE_STATE_F: return "F - Fault: EVSE Unavailable or No Pilot Signal";
|
||||
default: return "Unknown State";
|
||||
const char *evse_state_to_str(evse_state_t state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case EVSE_STATE_A:
|
||||
return "A - EV Not Connected (12V)";
|
||||
case EVSE_STATE_B1:
|
||||
return "B1 - EV Connected (9V, Not Authorized)";
|
||||
case EVSE_STATE_B2:
|
||||
return "B2 - EV Connected (9V, Authorized and Ready)";
|
||||
case EVSE_STATE_C1:
|
||||
return "C1 - Charging Requested (6V, Relay Off)";
|
||||
case EVSE_STATE_C2:
|
||||
return "C2 - Charging Active (6V, Relay On)";
|
||||
case EVSE_STATE_D1:
|
||||
return "D1 - Ventilation Required (3V, Relay Off)";
|
||||
case EVSE_STATE_D2:
|
||||
return "D2 - Ventilation Active (3V, Relay On)";
|
||||
case EVSE_STATE_E:
|
||||
return "E - Error: Control Pilot Shorted to Ground (0V)";
|
||||
case EVSE_STATE_F:
|
||||
return "F - Fault: EVSE Unavailable or No Pilot Signal";
|
||||
default:
|
||||
return "Unknown State";
|
||||
}
|
||||
}
|
||||
|
||||
void evse_state_init(void) {
|
||||
void evse_state_init(void)
|
||||
{
|
||||
portENTER_CRITICAL(&state_mux);
|
||||
current_state = EVSE_STATE_A;
|
||||
is_authorized = true;
|
||||
is_authorized = false;
|
||||
portEXIT_CRITICAL(&state_mux);
|
||||
|
||||
ESP_LOGI("EVSE_STATE", "Initialized in state: %s", evse_state_to_str(current_state));
|
||||
ESP_LOGI(TAG, "Initialized in state: %s", evse_state_to_str(current_state));
|
||||
|
||||
evse_state_event_data_t evt = {
|
||||
.state = map_state_to_event(current_state)
|
||||
};
|
||||
esp_event_post(EVSE_EVENTS, EVSE_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY);
|
||||
.state = map_state_to_event(current_state)};
|
||||
post_evse_event(EVSE_EVENT_INIT, &evt, sizeof(evt));
|
||||
}
|
||||
|
||||
void evse_state_tick(void) {
|
||||
// Placeholder for future state logic
|
||||
void evse_state_tick(void)
|
||||
{
|
||||
// placeholder
|
||||
}
|
||||
|
||||
bool evse_state_is_charging(evse_state_t state) {
|
||||
return state == EVSE_STATE_C1 || state == EVSE_STATE_C2;
|
||||
}
|
||||
|
||||
bool evse_state_is_plugged(evse_state_t state) {
|
||||
return state == EVSE_STATE_B1 || state == EVSE_STATE_B2 ||
|
||||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
|
||||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2;
|
||||
}
|
||||
|
||||
bool evse_state_is_session(evse_state_t state) {
|
||||
return state == EVSE_STATE_B2 || state == EVSE_STATE_C1 || state == EVSE_STATE_C2;
|
||||
}
|
||||
|
||||
void evse_state_set_authorized(bool authorized) {
|
||||
void evse_state_set_authorized(bool authorized)
|
||||
{
|
||||
portENTER_CRITICAL(&state_mux);
|
||||
is_authorized = authorized;
|
||||
portEXIT_CRITICAL(&state_mux);
|
||||
}
|
||||
|
||||
bool evse_state_get_authorized(void) {
|
||||
bool evse_state_get_authorized(void)
|
||||
{
|
||||
portENTER_CRITICAL(&state_mux);
|
||||
bool result = is_authorized;
|
||||
portEXIT_CRITICAL(&state_mux);
|
||||
|
||||
0
components/evse/include/evse_api.h
Executable file → Normal file
0
components/evse/include/evse_api.h
Executable file → Normal file
17
components/evse/include/evse_config.h
Executable file → Normal file
17
components/evse/include/evse_config.h
Executable file → Normal 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);
|
||||
|
||||
36
components/evse/include/evse_error.h
Executable file → Normal file
36
components/evse/include/evse_error.h
Executable file → Normal file
@@ -1,4 +1,3 @@
|
||||
// === Início de: components/evse/include/evse_error.h ===
|
||||
#ifndef EVSE_ERROR_H
|
||||
#define EVSE_ERROR_H
|
||||
|
||||
@@ -6,21 +5,23 @@
|
||||
#include <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)
|
||||
#define EVSE_ERR_LOCK_FAULT_BIT (1 << 1)
|
||||
#define EVSE_ERR_UNLOCK_FAULT_BIT (1 << 2)
|
||||
#define EVSE_ERR_RCM_SELFTEST_FAULT_BIT (1 << 3)
|
||||
#define EVSE_ERR_RCM_TRIGGERED_BIT (1 << 4)
|
||||
#define EVSE_ERR_TEMPERATURE_HIGH_BIT (1 << 5)
|
||||
#define EVSE_ERR_PILOT_FAULT_BIT (1 << 6)
|
||||
#define EVSE_ERR_TEMPERATURE_FAULT_BIT (1 << 7)
|
||||
#define EVSE_ERR_DIODE_SHORT_BIT (1 << 0)
|
||||
#define EVSE_ERR_LOCK_FAULT_BIT (1 << 1)
|
||||
#define EVSE_ERR_UNLOCK_FAULT_BIT (1 << 2)
|
||||
#define EVSE_ERR_RCM_SELFTEST_FAULT_BIT (1 << 3)
|
||||
#define EVSE_ERR_RCM_TRIGGERED_BIT (1 << 4)
|
||||
#define EVSE_ERR_TEMPERATURE_HIGH_BIT (1 << 5)
|
||||
#define EVSE_ERR_PILOT_FAULT_BIT (1 << 6)
|
||||
#define EVSE_ERR_TEMPERATURE_FAULT_BIT (1 << 7)
|
||||
|
||||
// Inicialização do módulo de erros
|
||||
void evse_error_init(void);
|
||||
@@ -30,7 +31,7 @@ void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v);
|
||||
void evse_temperature_check(void);
|
||||
void evse_error_tick(void);
|
||||
|
||||
// Leitura e controle de erros
|
||||
// Leitura e controle de erros (estado "visível" com holdoff)
|
||||
uint32_t evse_get_error(void);
|
||||
void evse_error_set(uint32_t bitmask);
|
||||
void evse_error_clear(uint32_t bitmask);
|
||||
@@ -40,12 +41,9 @@ uint32_t evse_error_get_bits(void);
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Semântica sticky: flag "todos erros limpos"
|
||||
// (fica true quando o erro visível chega a 0; pode ser útil para UI/logs)
|
||||
// ----------------------------------------------------
|
||||
// Fica true quando TODOS os erros são limpos.
|
||||
// Volta a false assim que qualquer erro novo aparece.
|
||||
// Permanece true até o consumidor limpar explicitamente.
|
||||
bool evse_error_cleared_flag(void);
|
||||
void evse_error_reset_flag(void);
|
||||
|
||||
#endif // EVSE_ERROR_H
|
||||
// === Fim de: components/evse/include/evse_error.h ===
|
||||
|
||||
41
components/evse/include/evse_events.h
Executable file → Normal file
41
components/evse/include/evse_events.h
Executable file → Normal 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
0
components/evse/include/evse_fsm.h
Executable file → Normal file
0
components/evse/include/evse_hardware.h
Executable file → Normal file
0
components/evse/include/evse_hardware.h
Executable file → Normal file
0
components/evse/include/evse_limits.h
Executable file → Normal file
0
components/evse/include/evse_limits.h
Executable file → Normal file
0
components/evse/include/evse_manager.h
Executable file → Normal file
0
components/evse/include/evse_manager.h
Executable file → Normal file
74
components/evse/include/evse_pilot.h
Executable file → Normal file
74
components/evse/include/evse_pilot.h
Executable file → Normal file
@@ -2,65 +2,43 @@
|
||||
#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_t;
|
||||
typedef enum
|
||||
{
|
||||
PILOT_VOLTAGE_12,
|
||||
PILOT_VOLTAGE_9,
|
||||
PILOT_VOLTAGE_6,
|
||||
PILOT_VOLTAGE_3,
|
||||
PILOT_VOLTAGE_1
|
||||
} pilot_voltage_t;
|
||||
|
||||
/**
|
||||
* @brief Inicializa o driver do sinal Pilot
|
||||
*/
|
||||
void pilot_init(void);
|
||||
void pilot_init(void);
|
||||
|
||||
/**
|
||||
* @brief Define o nível do Pilot: +12V ou -12V
|
||||
*
|
||||
* @param level true = +12V, false = -12V
|
||||
*/
|
||||
void pilot_set_level(bool level);
|
||||
/**
|
||||
* @brief Define o pilot em modo DC.
|
||||
*
|
||||
* @param high true = nível alto (+12V)
|
||||
* false = nível baixo (-12V)
|
||||
*/
|
||||
void pilot_set_level(bool high);
|
||||
|
||||
/**
|
||||
* @brief Ativa o PWM do Pilot com corrente limitada
|
||||
*
|
||||
* @param amps Corrente em ampères (ex: 16 = 16A)
|
||||
*/
|
||||
void pilot_set_amps(uint16_t amps);
|
||||
void pilot_set_amps(uint16_t amps);
|
||||
|
||||
/**
|
||||
* @brief Mede o nível de tensão do Pilot e detecta -12V
|
||||
*
|
||||
* @param up_voltage Valor categórico da tensão positiva
|
||||
* @param down_voltage_n12 true se o nível negativo atingir -12V
|
||||
*/
|
||||
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12);
|
||||
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12);
|
||||
|
||||
/**
|
||||
* @brief Retorna o estado lógico atual do Pilot (nível alto = +12V)
|
||||
*
|
||||
* @return true se nível atual for +12V, false se for -12V
|
||||
*/
|
||||
bool pilot_get_state(void);
|
||||
bool pilot_get_state(void);
|
||||
|
||||
/**
|
||||
* @brief Cache interno opcional dos níveis de tensão reais do Pilot
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t high_mv; ///< Pico positivo medido (mV)
|
||||
uint16_t low_mv; ///< Pico negativo medido (mV)
|
||||
} pilot_voltage_cache_t;
|
||||
typedef struct
|
||||
{
|
||||
uint16_t high_mv;
|
||||
uint16_t low_mv;
|
||||
} pilot_voltage_cache_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -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
|
||||
uint32_t avg_power_w; ///< average power in W
|
||||
bool is_current; ///< true if session still in progress
|
||||
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
|
||||
|
||||
124
components/evse/include/evse_state.h
Executable file → Normal file
124
components/evse/include/evse_state.h
Executable file → Normal file
@@ -6,90 +6,68 @@
|
||||
#include "evse_events.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// ============================
|
||||
// EVSE Pilot Signal States
|
||||
// ============================
|
||||
typedef enum
|
||||
{
|
||||
EVSE_STATE_A, // EV Not Connected (12V)
|
||||
EVSE_STATE_B1, // EV Connected (9V, Not Authorized)
|
||||
EVSE_STATE_B2, // EV Connected (9V, Authorized and Ready)
|
||||
EVSE_STATE_C1, // Charging Requested (6V, Relay Off)
|
||||
EVSE_STATE_C2, // Charging Active (6V, Relay On)
|
||||
EVSE_STATE_D1, // Ventilation Required (3V, Relay Off)
|
||||
EVSE_STATE_D2, // Ventilation Active (3V, Relay On)
|
||||
EVSE_STATE_E, // Error: Pilot Short to Ground (0V)
|
||||
EVSE_STATE_F // Fault: No Pilot or EVSE Unavailable
|
||||
} evse_state_t;
|
||||
|
||||
typedef enum {
|
||||
EVSE_STATE_A, // EV Not Connected (12V)
|
||||
EVSE_STATE_B1, // EV Connected (9V, Not Authorized)
|
||||
EVSE_STATE_B2, // EV Connected (9V, Authorized and Ready)
|
||||
EVSE_STATE_C1, // Charging Requested (6V, Relay Off)
|
||||
EVSE_STATE_C2, // Charging Active (6V, Relay On)
|
||||
EVSE_STATE_D1, // Ventilation Required (3V, Relay Off)
|
||||
EVSE_STATE_D2, // Ventilation Active (3V, Relay On)
|
||||
EVSE_STATE_E, // Error: Pilot Short to Ground (0V)
|
||||
EVSE_STATE_F // Fault: No Pilot or EVSE Unavailable
|
||||
} evse_state_t;
|
||||
// Initialization
|
||||
void evse_state_init(void);
|
||||
void evse_state_tick(void);
|
||||
|
||||
// ============================
|
||||
// Initialization
|
||||
// ============================
|
||||
// State Access & Control
|
||||
evse_state_t evse_get_state(void);
|
||||
void evse_set_state(evse_state_t state);
|
||||
const char *evse_state_to_str(evse_state_t state);
|
||||
|
||||
/**
|
||||
* @brief Initializes the EVSE state machine and default state.
|
||||
*/
|
||||
void evse_state_init(void);
|
||||
// ---------------------------
|
||||
// State Evaluation Helpers
|
||||
// ---------------------------
|
||||
|
||||
/**
|
||||
* @brief Periodic tick for state handling (optional hook).
|
||||
*/
|
||||
void evse_state_tick(void);
|
||||
/**
|
||||
* @brief True se existe uma sessão "lógica" ativa (carro ligado e autorizado/pronto ou a carregar).
|
||||
* Inclui B2, C1/C2, D1/D2.
|
||||
*/
|
||||
bool evse_state_is_session(evse_state_t state);
|
||||
|
||||
// ============================
|
||||
// State Access & Control
|
||||
// ============================
|
||||
/**
|
||||
* @brief True se o EVSE está a fornecer energia (relé ON).
|
||||
* Estados com energia: C2 e D2.
|
||||
*
|
||||
* Nota: isto substitui a antiga interpretação “C1/C2”.
|
||||
*/
|
||||
bool evse_state_is_charging(evse_state_t state);
|
||||
|
||||
/**
|
||||
* @brief Returns the current EVSE state.
|
||||
*/
|
||||
evse_state_t evse_get_state(void);
|
||||
/**
|
||||
* @brief True se o EV pediu carga mas o relé ainda está OFF (C1/D1).
|
||||
*/
|
||||
bool evse_state_is_requesting(evse_state_t state);
|
||||
|
||||
/**
|
||||
* @brief Sets the current EVSE state and emits a change event if needed.
|
||||
*/
|
||||
void evse_set_state(evse_state_t state);
|
||||
/**
|
||||
* @brief True se há fluxo de energia (alias explícito para charging).
|
||||
*/
|
||||
bool evse_state_is_power_flowing(evse_state_t state);
|
||||
|
||||
/**
|
||||
* @brief Converts the state enum into a human-readable string.
|
||||
*/
|
||||
const char* evse_state_to_str(evse_state_t state);
|
||||
/**
|
||||
* @brief True se o EV está fisicamente ligado (B1 e além).
|
||||
*/
|
||||
bool evse_state_is_plugged(evse_state_t state);
|
||||
|
||||
// ============================
|
||||
// State Evaluation Helpers
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* @brief True if EV is in an active session (B2, C1, C2).
|
||||
*/
|
||||
bool evse_state_is_session(evse_state_t state);
|
||||
|
||||
/**
|
||||
* @brief True if EV is actively charging (C1, C2).
|
||||
*/
|
||||
bool evse_state_is_charging(evse_state_t state);
|
||||
|
||||
/**
|
||||
* @brief True if EV is physically plugged in (B1 and beyond).
|
||||
*/
|
||||
bool evse_state_is_plugged(evse_state_t state);
|
||||
|
||||
// ============================
|
||||
// Authorization Control
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* @brief Sets whether the EV is authorized to charge.
|
||||
*/
|
||||
void evse_state_set_authorized(bool authorized);
|
||||
|
||||
/**
|
||||
* @brief Gets whether the EV is currently authorized.
|
||||
*/
|
||||
bool evse_state_get_authorized(void);
|
||||
// Authorization Control
|
||||
void evse_state_set_authorized(bool authorized);
|
||||
bool evse_state_get_authorized(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
4
components/evse_link/CMakeLists.txt
Executable file → Normal file
4
components/evse_link/CMakeLists.txt
Executable file → Normal 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
3
components/evse_link/include/evse_link.h
Executable file → Normal 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 ===
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -5,24 +5,39 @@
|
||||
#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
|
||||
#define MAGIC_START 0x7E
|
||||
#define MAGIC_END 0x7F
|
||||
|
||||
// Maximum payload (excluding sequence byte)
|
||||
#define EVSE_LINK_MAX_PAYLOAD 254
|
||||
|
||||
169
components/evse_link/src/evse_link.c
Executable file → Normal file
169
components/evse_link/src/evse_link.c
Executable file → Normal 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)
|
||||
{
|
||||
load_link_config();
|
||||
if (s_evse_link_inited)
|
||||
{
|
||||
ESP_LOGW(TAG, "evse_link_init called twice; ignoring");
|
||||
return;
|
||||
}
|
||||
s_evse_link_inited = true;
|
||||
|
||||
esp_err_t se = storage_service_init();
|
||||
if (se == ESP_OK)
|
||||
load_link_config();
|
||||
else
|
||||
ESP_LOGE(TAG, "storage_service_init failed: %s (defaults in RAM)", esp_err_to_name(se));
|
||||
|
||||
ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d",
|
||||
_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);
|
||||
if (xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL) != pdPASS)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create evse_link_rx task");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) delegate to master or slave
|
||||
if (_mode == EVSE_LINK_MODE_MASTER)
|
||||
{
|
||||
evse_link_master_init();
|
||||
}
|
||||
else
|
||||
{
|
||||
evse_link_slave_init();
|
||||
}
|
||||
}
|
||||
|
||||
// Send a frame (delegates to framing module)
|
||||
bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len)
|
||||
{
|
||||
if (!evse_link_is_enabled())
|
||||
return false;
|
||||
|
||||
uint8_t src = evse_link_get_self_id();
|
||||
return evse_link_framing_send(dest, src, payload, len);
|
||||
}
|
||||
|
||||
// Receive byte (delegates to framing module)
|
||||
void evse_link_recv_byte(uint8_t byte)
|
||||
{
|
||||
evse_link_framing_recv_byte(byte);
|
||||
|
||||
@@ -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,118 +31,261 @@ 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) {
|
||||
crc = (uint8_t)((crc << 1) ^ 0x07);
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
const int64_t now = esp_timer_get_time();
|
||||
if (*last_us == 0 || (now - *last_us) > interval_us)
|
||||
{
|
||||
*last_us = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// CRC-8 (poly 0x07), MSB-first, init=0x00
|
||||
static uint8_t crc8_update(uint8_t crc, uint8_t data)
|
||||
{
|
||||
crc ^= data;
|
||||
for (uint8_t b = 0; b < 8; ++b)
|
||||
{
|
||||
if (crc & 0x80)
|
||||
crc = (uint8_t)((crc << 1) ^ 0x07);
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
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,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.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,
|
||||
@@ -133,17 +297,46 @@ void evse_link_framing_recv_byte(uint8_t b)
|
||||
ST_WAIT_END
|
||||
} rx_state = ST_WAIT_START;
|
||||
|
||||
static uint8_t rx_dest;
|
||||
static uint8_t rx_src;
|
||||
static uint8_t rx_len; // inclui SEQ + payload
|
||||
static uint8_t rx_seq;
|
||||
static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD];
|
||||
static uint8_t rx_pos;
|
||||
static uint8_t rx_crc;
|
||||
static uint8_t rx_dest;
|
||||
static uint8_t rx_src;
|
||||
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) {
|
||||
{
|
||||
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 >= (uint8_t)(rx_len - 1)) { // payload completo
|
||||
|
||||
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 {
|
||||
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X",
|
||||
expected, rx_crc);
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
#define CMD_POLL 0x01
|
||||
#define CMD_HEARTBEAT 0x02
|
||||
#define CMD_HEARTBEAT_ACK 0x09
|
||||
// Link commands (opcode no payload[0])
|
||||
#define CMD_POLL 0x01
|
||||
#define CMD_HEARTBEAT 0x02
|
||||
#define CMD_CONFIG_BROADCAST 0x03
|
||||
#define CMD_SET_CURRENT 0x08
|
||||
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização a slave
|
||||
#define CMD_SET_CURRENT 0x08
|
||||
#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) ]
|
||||
#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
|
||||
// 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, ... ]
|
||||
#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,
|
||||
LOADBALANCER_EVENT_SLAVE_STATUS,
|
||||
&status, sizeof(status), portMAX_DELAY);
|
||||
(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
|
||||
poll_timer.timer = xTimerCreate("poll_tmr",
|
||||
poll_timer.interval,
|
||||
pdTRUE, NULL,
|
||||
poll_timer_cb);
|
||||
xTimerStart(poll_timer.timer, 0);
|
||||
if (poll_timer.timer == NULL)
|
||||
{
|
||||
poll_timer.timer = xTimerCreate("poll_tmr",
|
||||
poll_timer.interval,
|
||||
pdTRUE, NULL,
|
||||
poll_timer_cb);
|
||||
if (poll_timer.timer)
|
||||
(void)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 (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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 CMD_POLL 0x01
|
||||
#define CMD_HEARTBEAT 0x02 // not used by slave
|
||||
#define CMD_CONFIG_BROADCAST 0x03
|
||||
#define CMD_SET_CURRENT 0x08
|
||||
#define CMD_HEARTBEAT_ACK 0x09
|
||||
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização
|
||||
#define MASTER_ID 0x01
|
||||
|
||||
// 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
|
||||
// Commands (opcode no payload[0])
|
||||
#define CMD_POLL 0x01
|
||||
#define CMD_HEARTBEAT 0x02
|
||||
#define CMD_CONFIG_BROADCAST 0x03
|
||||
#define CMD_SET_CURRENT 0x08
|
||||
#define CMD_HEARTBEAT_ACK 0x09
|
||||
#define CMD_AUTH_GRANTED 0x0A
|
||||
// #define CMD_RESUME 0x0B // se existir
|
||||
|
||||
// Timing
|
||||
#define FALLBACK_TIMEOUT_MS 120000
|
||||
// lengths (INCLUI opcode)
|
||||
#define LEN_SET_CURRENT 3
|
||||
|
||||
#define FALLBACK_TIMEOUT_MS 180000
|
||||
|
||||
// --- Política de fallback ---
|
||||
// 1 = mais seguro: PAUSE (revoga autorização)
|
||||
// 0 = força corrente mínima (pode continuar a carregar dependendo do core)
|
||||
#ifndef EVSE_LINK_FALLBACK_PAUSE
|
||||
#define EVSE_LINK_FALLBACK_PAUSE 1
|
||||
#endif
|
||||
|
||||
// --- Confirmações via heartbeat (deferidas) ---
|
||||
#define HB_REQ_QUEUE_LEN 8
|
||||
|
||||
typedef enum
|
||||
{
|
||||
HB_REQ_SEND = 1,
|
||||
} hb_req_t;
|
||||
|
||||
static TimerHandle_t fallback_timer = NULL;
|
||||
static bool safe_mode = false;
|
||||
static TaskHandle_t hb_task_handle = NULL;
|
||||
|
||||
// --- 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();
|
||||
// Task para enviar heartbeat sem bloquear RX callback
|
||||
static TaskHandle_t hb_sender_task_handle = NULL;
|
||||
static QueueHandle_t hb_req_q = NULL;
|
||||
|
||||
ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA",
|
||||
charging, hw_max, runtime);
|
||||
// "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);
|
||||
|
||||
uint16_t hw_max = evse_get_max_charging_current();
|
||||
uint16_t runtime = evse_get_runtime_charging_current();
|
||||
|
||||
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,
|
||||
&s, sizeof(amps), portMAX_DELAY);
|
||||
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,
|
||||
&s,
|
||||
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,
|
||||
&s, sizeof(amps), portMAX_DELAY);
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_HEARTBEAT_ACK:
|
||||
ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src);
|
||||
if (fallback_timer) {
|
||||
xTimerReset(fallback_timer, 0);
|
||||
if (safe_mode) {
|
||||
safe_mode = false;
|
||||
uint16_t current = evse_get_runtime_charging_current();
|
||||
evse_set_runtime_charging_current(current);
|
||||
ESP_LOGI(TAG, "Exiting safe mode, restoring %uA", current);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
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,
|
||||
LINK_EVENT_REMOTE_AUTH_GRANTED,
|
||||
&ev,
|
||||
sizeof(ev),
|
||||
portMAX_DELAY);
|
||||
// AUTH_GRANTED não deve sair de safe-mode automaticamente
|
||||
ESP_LOGI(TAG, "AUTH_GRANTED from 0x%02X tag='%s'", src, ev.tag);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
esp_err_t err = esp_event_post(EVSE_LINK_EVENTS,
|
||||
LINK_EVENT_REMOTE_AUTH_GRANTED,
|
||||
&ev, sizeof(ev),
|
||||
portMAX_DELAY);
|
||||
if (err != ESP_OK)
|
||||
ESP_LOGE(TAG, "Failed to post LINK_EVENT_REMOTE_AUTH_GRANTED: %s", esp_err_to_name(err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
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");
|
||||
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
|
||||
esp_event_post(EVSE_LINK_EVENTS,
|
||||
LINK_EVENT_SLAVE_OFFLINE,
|
||||
NULL, 0, portMAX_DELAY);
|
||||
}
|
||||
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);
|
||||
#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);
|
||||
|
||||
// fallback timer
|
||||
fallback_timer = xTimerCreate("fallback_tmr",
|
||||
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS),
|
||||
pdFALSE, NULL,
|
||||
fallback_timer_cb);
|
||||
if (fallback_timer) {
|
||||
xTimerStart(fallback_timer, 0);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// react to EVSE state changes
|
||||
ESP_ERROR_CHECK(
|
||||
esp_event_handler_register(
|
||||
EVSE_EVENTS,
|
||||
EVSE_EVENT_STATE_CHANGED,
|
||||
evse_event_handler,
|
||||
NULL
|
||||
)
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// === Fim de: components/evse_link/src/evse_link_slave.c ===
|
||||
if (fallback_timer == NULL)
|
||||
{
|
||||
fallback_timer = xTimerCreate("fallback_tmr",
|
||||
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS),
|
||||
pdFALSE, NULL,
|
||||
fallback_timer_cb);
|
||||
if (fallback_timer)
|
||||
(void)xTimerStart(fallback_timer, 0);
|
||||
else
|
||||
ESP_LOGE(TAG, "Failed to create fallback timer");
|
||||
}
|
||||
else
|
||||
{
|
||||
(void)xTimerReset(fallback_timer, 0);
|
||||
}
|
||||
|
||||
if (!evse_handler_registered)
|
||||
{
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(
|
||||
EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED,
|
||||
evse_event_handler, NULL));
|
||||
evse_handler_registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
10
components/hmi_link/CMakeLists.txt
Normal file
10
components/hmi_link/CMakeLists.txt
Normal 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
|
||||
)
|
||||
26
components/hmi_link/include/hmi_link.h
Normal file
26
components/hmi_link/include/hmi_link.h
Normal 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
|
||||
406
components/hmi_link/src/hmi_link.c
Normal file
406
components/hmi_link/src/hmi_link.c
Normal 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, &s, "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
0
components/led/CMakeLists.txt
Executable file → Normal file
0
components/led/include/led.h
Executable file → Normal file
0
components/led/include/led.h
Executable file → Normal file
0
components/led/include/ledc_driver.h
Executable file → Normal file
0
components/led/include/ledc_driver.h
Executable file → Normal file
6
components/led/src/led.c
Executable file → Normal file
6
components/led/src/led.c
Executable file → Normal 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
0
components/led/src/ledc_driver.c
Executable file → Normal file
3
components/loadbalancer/CMakeLists.txt
Executable file → Normal file
3
components/loadbalancer/CMakeLists.txt
Executable file → Normal 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)
|
||||
|
||||
42
components/loadbalancer/include/grid_limiter.h
Normal file
42
components/loadbalancer/include/grid_limiter.h
Normal 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
0
components/loadbalancer/include/input_filter.h
Executable file → Normal file
57
components/loadbalancer/include/loadbalancer.h
Executable file → Normal file
57
components/loadbalancer/include/loadbalancer.h
Executable file → Normal file
@@ -9,36 +9,47 @@ 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
|
||||
*/
|
||||
uint8_t load_balancing_get_max_grid_current(void);
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
bool enabled;
|
||||
int64_t timestamp_us;
|
||||
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;
|
||||
int64_t timestamp_us;
|
||||
} loadbalancer_master_limit_event_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t slave_id;
|
||||
/*
|
||||
* SLAVE: includes slave_id.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
uint8_t slave_id;
|
||||
uint16_t max_current;
|
||||
int64_t timestamp_us;
|
||||
int64_t timestamp_us;
|
||||
} loadbalancer_slave_limit_event_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t slave_id; // ID do slave que reportou
|
||||
bool charging; // Status de carregamento
|
||||
float hw_max_current; // Limite máximo de corrente do hardware informado
|
||||
float runtime_current; // Corrente atual de carregamento (A)
|
||||
int64_t timestamp_us; // Momento em que o status foi coletado
|
||||
} loadbalancer_slave_status_event_t;
|
||||
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
|
||||
40
components/loadbalancer/include/pv_optimizer.h
Normal file
40
components/loadbalancer/include/pv_optimizer.h
Normal 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_ */
|
||||
123
components/loadbalancer/src/grid_limiter.c
Normal file
123
components/loadbalancer/src/grid_limiter.c
Normal 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
0
components/loadbalancer/src/input_filter.c
Executable file → Normal file
1750
components/loadbalancer/src/loadbalancer.c
Executable file → Normal file
1750
components/loadbalancer/src/loadbalancer.c
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
165
components/loadbalancer/src/pv_optimizer.c
Normal file
165
components/loadbalancer/src/pv_optimizer.c
Normal 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;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
set(srcs
|
||||
"src/logger.c"
|
||||
"src/output_buffer.c"
|
||||
)
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "include")
|
||||
@@ -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_ */
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
49
components/meter_manager/CMakeLists.txt
Executable file → Normal 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
|
||||
)
|
||||
|
||||
0
components/meter_manager/driver/meter_ade7758/ade7758.c
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/ade7758.c
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/ade7758.h
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/ade7758.h
Executable file → Normal file
2
components/meter_manager/driver/meter_ade7758/meter_ade7758.c
Executable file → Normal file
2
components/meter_manager/driver/meter_ade7758/meter_ade7758.c
Executable file → Normal 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));
|
||||
}
|
||||
|
||||
0
components/meter_manager/driver/meter_ade7758/meter_ade7758.h
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/meter_ade7758.h
Executable file → Normal file
671
components/meter_manager/driver/meter_modbus/meter_dds661.c
Executable file
671
components/meter_manager/driver/meter_modbus/meter_dds661.c
Executable 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);
|
||||
}
|
||||
58
components/meter_manager/driver/meter_modbus/meter_dds661.h
Executable file
58
components/meter_manager/driver/meter_modbus/meter_dds661.h
Executable 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
|
||||
760
components/meter_manager/driver/meter_modbus/meter_dds665.c
Normal file
760
components/meter_manager/driver/meter_modbus/meter_dds665.c
Normal 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);
|
||||
}
|
||||
58
components/meter_manager/driver/meter_modbus/meter_dds665.h
Normal file
58
components/meter_manager/driver/meter_modbus/meter_dds665.h
Normal 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
|
||||
542
components/meter_manager/driver/meter_modbus/meter_dts024m.c
Normal file
542
components/meter_manager/driver/meter_modbus/meter_dts024m.c
Normal 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");
|
||||
}
|
||||
22
components/meter_manager/driver/meter_orno/meter_dts6619.h → components/meter_manager/driver/meter_modbus/meter_dts024m.h
Executable file → Normal file
22
components/meter_manager/driver/meter_orno/meter_dts6619.h → components/meter_manager/driver/meter_modbus/meter_dts024m.h
Executable file → Normal 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_ */
|
||||
2
components/meter_manager/driver/meter_orno/meter_dts6619.c → components/meter_manager/driver/meter_modbus/meter_dts6619.c
Executable file → Normal file
2
components/meter_manager/driver/meter_orno/meter_dts6619.c → components/meter_manager/driver/meter_modbus/meter_dts6619.c
Executable file → Normal 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));
|
||||
5
components/meter_manager/driver/meter_orno/meter_ea777.h → components/meter_manager/driver/meter_modbus/meter_dts6619.h
Executable file → Normal file
5
components/meter_manager/driver/meter_orno/meter_ea777.h → components/meter_manager/driver/meter_modbus/meter_dts6619.h
Executable file → Normal file
@@ -7,14 +7,14 @@
|
||||
|
||||
/**
|
||||
* @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores).
|
||||
*
|
||||
*
|
||||
* @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro.
|
||||
*/
|
||||
esp_err_t meter_dts6619_init(void);
|
||||
|
||||
/**
|
||||
* @brief Inicia a tarefa de leitura de dados do medidor DTS6619.
|
||||
*
|
||||
*
|
||||
* @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro.
|
||||
*/
|
||||
esp_err_t meter_dts6619_start(void);
|
||||
@@ -24,7 +24,6 @@ esp_err_t meter_dts6619_start(void);
|
||||
*/
|
||||
void meter_dts6619_stop(void);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
761
components/meter_manager/driver/meter_modbus/meter_ea777.c
Executable file
761
components/meter_manager/driver/meter_modbus/meter_ea777.c
Executable 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);
|
||||
}
|
||||
61
components/meter_manager/driver/meter_modbus/meter_ea777.h
Executable file
61
components/meter_manager/driver/meter_modbus/meter_ea777.h
Executable 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_ */
|
||||
0
components/meter_manager/driver/meter_orno/meter_orno.h → components/meter_manager/driver/meter_modbus/meter_orno.h
Executable file → Normal file
0
components/meter_manager/driver/meter_orno/meter_orno.h → components/meter_manager/driver/meter_modbus/meter_orno.h
Executable file → Normal file
5
components/meter_manager/driver/meter_orno/meter_orno513.c → components/meter_manager/driver/meter_modbus/meter_orno513.c
Executable file → Normal file
5
components/meter_manager/driver/meter_orno/meter_orno513.c → components/meter_manager/driver/meter_modbus/meter_orno513.c
Executable file → Normal 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);
|
||||
0
components/meter_manager/driver/meter_orno/meter_orno513.h → components/meter_manager/driver/meter_modbus/meter_orno513.h
Executable file → Normal file
0
components/meter_manager/driver/meter_orno/meter_orno513.h → components/meter_manager/driver/meter_modbus/meter_orno513.h
Executable file → Normal file
2
components/meter_manager/driver/meter_orno/meter_orno516.c → components/meter_manager/driver/meter_modbus/meter_orno516.c
Executable file → Normal file
2
components/meter_manager/driver/meter_orno/meter_orno516.c → components/meter_manager/driver/meter_modbus/meter_orno516.c
Executable file → Normal 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));
|
||||
0
components/meter_manager/driver/meter_orno/meter_orno516.h → components/meter_manager/driver/meter_modbus/meter_orno516.h
Executable file → Normal file
0
components/meter_manager/driver/meter_orno/meter_orno516.h → components/meter_manager/driver/meter_modbus/meter_orno516.h
Executable file → Normal file
612
components/meter_manager/driver/meter_modbus/meter_orno526.c
Normal file
612
components/meter_manager/driver/meter_modbus/meter_orno526.c
Normal 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();
|
||||
}
|
||||
58
components/meter_manager/driver/meter_modbus/meter_orno526.h
Normal file
58
components/meter_manager/driver/meter_modbus/meter_orno526.h
Normal 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
Reference in New Issue
Block a user