fix evse_link

This commit is contained in:
2026-01-24 16:56:51 +00:00
parent 023644a887
commit 286028b6a8
54 changed files with 4456 additions and 2632 deletions

View File

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

View File

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

View File

@@ -6,23 +6,23 @@
#include "driver/uart.h"
// UART instance and configuration
#define UART_PORT UART_NUM_2 // Usa a UART2
#define UART_BAUDRATE 115200
#define UART_PORT UART_NUM_2
#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
// GPIO pin assignments for RS-485 UART
// Ajuste 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
// Conveniência: nomes usados no .c
#define TX_PIN UART_TXD
#define RX_PIN UART_RXD
#define RTS_PIN UART_RTS
#define TX_PIN MB_UART_TXD
#define RX_PIN MB_UART_RXD
#define RTS_PIN MB_UART_RTS
// 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

View File

@@ -1,10 +1,20 @@
// 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 "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stdbool.h>
@@ -14,104 +24,75 @@
static const char *TAG = "evse_link";
// Storage keys
#define _NVS_NAMESPACE "evse_link"
#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 storage_service (NVS-backed)
static void load_link_config(void)
{
uint8_t u8 = 0;
// mode
esp_err_t err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_MODE, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK && (u8 == (uint8_t)EVSE_LINK_MODE_MASTER || u8 == (uint8_t)EVSE_LINK_MODE_SLAVE))
{
_mode = (evse_link_mode_t)u8;
}
else
{
// default + persist
_mode = EVSE_LINK_MODE_MASTER;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode);
ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER (persisted async)",
esp_err_to_name(err));
ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER", esp_err_to_name(err));
}
// self_id
err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_SELF_ID, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK)
{
_self_id = u8;
}
else
{
_self_id = 0x01;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id);
ESP_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X (persisted async)",
esp_err_to_name(err), _self_id);
ESP_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X", esp_err_to_name(err), _self_id);
}
// enabled
err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_ENABLED, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK && u8 <= 1)
{
_enabled = (u8 != 0);
}
else
{
_enabled = false;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, 0);
ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false (persisted async)",
esp_err_to_name(err));
ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false", esp_err_to_name(err));
}
}
// Save config to storage_service (debounced)
static void save_link_config(void)
{
// Debounced writes: não bloqueia e reduz desgaste
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode);
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id);
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, _enabled ? 1 : 0);
// opcional: se quiseres persistência imediata em configurações “críticas”
// (void)storage_flush_async();
}
// Getters/setters
void evse_link_set_mode(evse_link_mode_t m)
{
if (m != EVSE_LINK_MODE_MASTER && m != EVSE_LINK_MODE_SLAVE)
@@ -148,10 +129,11 @@ void evse_link_set_enabled(bool en)
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)
@@ -159,26 +141,28 @@ static void evse_link_rx_task(void *arg)
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)
{
// garante storage disponível
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)
{
ESP_LOGE(TAG, "storage_service_init failed: %s (using defaults in RAM)", esp_err_to_name(se));
// segue com defaults em RAM
}
else
{
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',
@@ -187,21 +171,21 @@ void evse_link_init(void)
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, 3, 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())
@@ -211,7 +195,6 @@ bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len)
return evse_link_framing_send(dest, src, payload, len);
}
// Receive byte (delegates to framing module)
void evse_link_recv_byte(uint8_t byte)
{
evse_link_framing_recv_byte(byte);

View File

@@ -1,8 +1,29 @@
// components/evse_link/src/evse_link_framing.c
//
// EVSE-Link framing (RS-485 HALF DUPLEX via UART driver)
// - Usa UART_MODE_RS485_HALF_DUPLEX (driver controla RTS => DE//RE)
// - Configura RX timeout + RX full threshold para evitar “len=120”
// - Remove controlo manual de GPIO do RTS
//
// Requisitos:
// - MAX3485 com DE e /RE juntos ligados ao RTS_PIN
// - RTS_PIN definido em evse_link_framing.h (ex.: GPIO2; recomendado mudar no futuro)
//
// Notas:
// - Se o teu hardware inverter a lógica do RTS (raro), define EVSE_LINK_RTS_INVERT=1
#include "evse_link_framing.h"
#include "driver/uart.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
static const char *TAG = "evse_framing";
@@ -10,118 +31,252 @@ 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, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_mode(RS485_HALF_DUPLEX) failed: %s", esp_err_to_name(err));
return err;
}
// Ajustes para RX responsivo (evita "len=120")
(void)uart_set_rx_full_threshold(UART_PORT, EVSE_LINK_RX_FULL_THRESH);
(void)uart_set_rx_timeout(UART_PORT, EVSE_LINK_RX_TIMEOUT);
// Opcional: inverter RTS se hardware exigir
if (EVSE_LINK_RTS_INVERT)
{
(void)uart_set_line_inverse(UART_PORT, UART_SIGNAL_RTS_INV);
ESP_LOGW(TAG, "RS485 driver: RTS inverted");
}
ESP_LOGW(TAG, "RS485 driver enabled: UART%d TX=%d RX=%d RTS=%d baud=%d rx_to=%d rx_thresh=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE,
EVSE_LINK_RX_TIMEOUT, EVSE_LINK_RX_FULL_THRESH);
return ESP_OK;
}
void evse_link_framing_init(void)
{
if (s_framing_inited)
{
ESP_LOGI(TAG, "Framing already initialized");
return;
}
if (!tx_mutex)
{
tx_mutex = xSemaphoreCreateMutex();
if (!tx_mutex)
{
ESP_LOGE(TAG, "Failed to create TX mutex");
return;
}
}
// Se o driver já estiver instalado, só reconfigura e aplica RX tuning.
if (uart_is_driver_installed(UART_PORT))
{
esp_err_t err = configure_uart();
if (err == ESP_OK)
{
s_framing_inited = true;
ESP_LOGW(TAG, "UART%d driver already installed -> configured for RS485 HALF DUPLEX", UART_PORT);
(void)uart_flush_input(UART_PORT);
}
else
{
ESP_LOGE(TAG, "Failed to configure already-installed UART%d", UART_PORT);
}
return;
}
// Instala driver UART
esp_err_t err = uart_driver_install(UART_PORT,
UART_RX_BUF_SIZE * 2, // RX buffer (ringbuffer)
0, // TX buffer (não usado)
0, // event queue size
NULL, // event queue
0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return;
}
err = configure_uart();
if (err != ESP_OK)
return;
(void)uart_flush_input(UART_PORT);
s_framing_inited = true;
ESP_LOGI(TAG, "Framing init (RS485 driver): UART%d TX=%d RX=%d RTS=%d baud=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE);
}
bool evse_link_framing_send(uint8_t dest, uint8_t src,
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 +288,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 +343,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 +394,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)

View File

@@ -1,10 +1,27 @@
// components/evse_link/src/evse_link_master.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX (ACK deferido para task -> não bloqueia RX)
// 2) Dedupe de ACK por slave (evita rajadas se RX vier em chunks / queue acumular)
// 3) Log quando ACK queue enche (antes era silencioso)
// 4) Proteção concorrente no s_presence (int64_t não é atómico no ESP32 32-bit)
// 5) Comentário do poll corrigido + opção de jitter no ACK
//
// NOTA: Mantive o teu comportamento (POLL a cada 10s). Se quiseres, muda para 30s.
#include "evse_link.h"
#include "evse_link_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "esp_random.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
@@ -14,37 +31,180 @@
static const char *TAG = "evse_link_master";
// Link commands
#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");
}
}

View File

@@ -1,199 +1,477 @@
// === components/evse_link/src/evse_link_slave.c ===
// components/evse_link/src/evse_link_slave.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX: confirmação (heartbeat) é deferida via queue/task.
// 2) Proteger safe_mode (e flags relacionadas) com mux (evita race entre RX e timer).
// 3) Opção de política no fallback: por default faz PAUSE (mais seguro). Pode ser alterado por macro.
// 4) Reduzir ruído de logs em caminho quente (RX frames em DEBUG).
// 5) Manter semântica: só sai de safe_mode com comando explícito de potência (SET_CURRENT / RESUME).
// 6) Mantém lógica de pause SET_CURRENT=0 e resume quando >0.
//
// Nota: A enum do evse_state_event_data_t que enviaste está correta para o handler.
#include "evse_link.h"
#include "evse_link_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
// "remote pause" (SET_CURRENT=0)
static bool paused_by_master = false;
static bool paused_prev_authorized = false;
static portMUX_TYPE s_state_mux = portMUX_INITIALIZER_UNLOCKED;
static bool evse_handler_registered = false;
static size_t bounded_strlen_u8(const uint8_t *s, size_t max_len)
{
size_t i = 0;
if (!s)
return 0;
while (i < max_len && s[i] != 0)
i++;
return i;
}
static void send_heartbeat_frame_now(void)
{
bool charging = evse_state_is_charging(evse_get_state());
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
uint8_t hb[] = {
CMD_HEARTBEAT,
charging ? 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");
}
// --- 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();
// 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();
}
}
static void hb_sender_task(void *arg)
{
(void)arg;
hb_req_t req;
for (;;)
{
if (xQueueReceive(hb_req_q, &req, portMAX_DELAY) != pdTRUE)
continue;
if (req == HB_REQ_SEND)
{
send_heartbeat_frame_now();
}
}
}
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
return;
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
// Enforce pause: se algo tentar voltar a carregar enquanto paused_by_master, revoga auth.
bool paused;
portENTER_CRITICAL(&s_state_mux);
paused = paused_by_master;
portEXIT_CRITICAL(&s_state_mux);
if (paused && evt->state == EVSE_STATE_EVENT_CHARGING)
{
// Garante que não continua a carregar por clamp/auto-auth
evse_state_set_authorized(false);
}
// Envia heartbeat quando entra em IDLE ou CHARGING (estado relevante)
if (evt->state == EVSE_STATE_EVENT_IDLE || evt->state == EVSE_STATE_EVENT_CHARGING)
request_heartbeat_send();
}
// Sai de safe-mode APENAS com comando explícito (SET_CURRENT / RESUME).
static void maybe_exit_safe_mode_on_explicit_power_cmd(uint8_t cmd)
{
bool in_safe;
portENTER_CRITICAL(&s_state_mux);
in_safe = safe_mode;
portEXIT_CRITICAL(&s_state_mux);
if (!in_safe)
return;
if (cmd == CMD_SET_CURRENT /*|| cmd == CMD_RESUME*/)
{
portENTER_CRITICAL(&s_state_mux);
safe_mode = false;
portEXIT_CRITICAL(&s_state_mux);
ESP_LOGI(TAG, "Exiting safe mode due to explicit cmd 0x%02X", cmd);
}
}
static void apply_pause_by_master(void)
{
bool prev_auth = evse_state_get_authorized();
portENTER_CRITICAL(&s_state_mux);
paused_by_master = true;
paused_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_state_mux);
// Revoga autorização para parar contactor/pilot via core
if (prev_auth)
evse_state_set_authorized(false);
// Mantém runtime num valor seguro (não 0, pois clamp -> 6A).
// O "pause" efetivo é pela autorização=false.
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
}
static void clear_pause_by_master_if_any(void)
{
bool was_paused;
bool prev_auth;
portENTER_CRITICAL(&s_state_mux);
was_paused = paused_by_master;
prev_auth = paused_prev_authorized;
paused_by_master = false;
paused_prev_authorized = false;
portEXIT_CRITICAL(&s_state_mux);
// Se estava autorizado antes do pause, tenta reautorizar
if (was_paused && prev_auth)
evse_state_set_authorized(true);
}
static void on_frame_slave(uint8_t src, uint8_t dest,
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);
uint16_t amps = (uint16_t)(payload[1] | ((uint16_t)payload[2] << 8));
// Comando explícito => pode sair de safe-mode (mesmo se for pause)
maybe_exit_safe_mode_on_explicit_power_cmd(cmd);
if (amps == 0)
{
// PAUSE explícito
ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE (src=0x%02X)", src);
apply_pause_by_master();
// Confirma sem bloquear RX
request_heartbeat_send();
// Publica evento com 0A (semântica: pause)
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
clear_pause_by_master_if_any();
evse_set_runtime_charging_current(amps);
ESP_LOGI(TAG, "Applied runtime limit: %uA from master 0x%02X", amps, src);
esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
// confirma sem bloquear RX
request_heartbeat_send();
ESP_LOGI(TAG, "Applied runtime limit: %uA from 0x%02X", (unsigned)amps, src);
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
case CMD_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 “morre”
apply_pause_by_master();
#else
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA, forcing %uA). Policy=MIN",
(unsigned)saved_runtime_limit, (unsigned)MIN_CHARGING_CURRENT_LIMIT);
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
#endif
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE,
NULL, 0, portMAX_DELAY);
// opcional: manda heartbeat para indicar estado atual
request_heartbeat_send();
}
// --- Slave initialization ---
void evse_link_slave_init(void) {
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;
}
}