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_ #ifndef EVSE_LINK_H_
#define EVSE_LINK_H_ #define EVSE_LINK_H_
@@ -43,3 +44,5 @@ void evse_link_set_enabled(bool enabled);
bool evse_link_is_enabled(void); bool evse_link_is_enabled(void);
#endif // EVSE_LINK_H_ #endif // EVSE_LINK_H_
// === Fim de: components/evse_link/include/evse_link.h ===

View File

@@ -1,31 +1,29 @@
// === Início de: components/evse_link/include/evse_link_events.h ===
#ifndef EVSE_LINK_EVENTS_H_ #ifndef EVSE_LINK_EVENTS_H_
#define EVSE_LINK_EVENTS_H_ #define EVSE_LINK_EVENTS_H_
#include "esp_event.h" #include "esp_event.h"
#include <stdint.h>
// Base de eventos do EVSE-Link
ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS); 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 #define EVSE_LINK_TAG_MAX_LEN 32
// IDs de eventos EVSE-Link
typedef enum { typedef enum {
LINK_EVENT_FRAME_RECEIVED, // qualquer frame válido LINK_EVENT_FRAME_RECEIVED,
LINK_EVENT_SLAVE_ONLINE, // heartbeat recebido primeira vez LINK_EVENT_SLAVE_ONLINE, // payload: evse_link_slave_presence_event_t
LINK_EVENT_SLAVE_OFFLINE, // sem heartbeat no timeout LINK_EVENT_SLAVE_OFFLINE, // payload: evse_link_slave_presence_event_t (master-side) ou NULL (slave-side fallback)
LINK_EVENT_MASTER_POLL_SENT, // opcional: poll enviado pelo master LINK_EVENT_MASTER_POLL_SENT,
LINK_EVENT_CURRENT_LIMIT_APPLIED, LINK_EVENT_CURRENT_LIMIT_APPLIED,
LINK_EVENT_SLAVE_CONFIG_UPDATED, // config atualizada pelo master LINK_EVENT_SLAVE_CONFIG_UPDATED,
LINK_EVENT_REMOTE_AUTH_GRANTED // autorização remota (master -> slave) LINK_EVENT_REMOTE_AUTH_GRANTED
} evse_link_event_t; } evse_link_event_t;
// Payload para LINK_EVENT_REMOTE_AUTH_GRANTED
typedef struct { 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; } 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" #include "driver/uart.h"
// UART instance and configuration // UART instance and configuration
#define UART_PORT UART_NUM_2 // Usa a UART2 #define UART_PORT UART_NUM_2
#define UART_BAUDRATE 115200 #define UART_BAUDRATE 9600
#define UART_RX_BUF_SIZE 256 #define UART_RX_BUF_SIZE 256
// GPIO pin assignments for UART (ajuste conforme o hardware) // GPIO pin assignments for RS-485 UART
#define UART_TXD 17 // TX -> DI do MAX3485 // Ajuste conforme seu hardware
#define UART_RXD 16 // RX -> RO do MAX3485 #define MB_UART_TXD 17
#define UART_RTS 2 // RTS -> DE+RE do MAX3485 #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 MB_UART_TXD
#define TX_PIN UART_TXD #define RX_PIN MB_UART_RXD
#define RX_PIN UART_RXD #define RTS_PIN MB_UART_RTS
#define RTS_PIN UART_RTS
// Frame delimiters // Frame delimiters
#define MAGIC_START 0x7E #define MAGIC_START 0x7E
#define MAGIC_END 0x7F #define MAGIC_END 0x7F
// Maximum payload (excluding sequence byte) // Maximum payload (excluding sequence byte)
#define EVSE_LINK_MAX_PAYLOAD 254 #define EVSE_LINK_MAX_PAYLOAD 254

View File

@@ -1,10 +1,20 @@
// components/evse_link/src/evse_link.c // components/evse_link/src/evse_link.c
//
// Camada de transporte EVSE-Link:
// - carrega config (mode/self_id/enabled)
// - init do framing
// - task RX (UART -> framing)
// - entrega frames completos ao callback registado
//
// NOTA: a logica de protocolo (CMD_POLL / ACK / etc.) deve ficar em
// evse_link_master.c / evse_link_slave.c.
#include "evse_link.h" #include "evse_link.h"
#include "evse_link_framing.h" #include "evse_link_framing.h"
#include "driver/uart.h" #include "driver/uart.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include <stdbool.h> #include <stdbool.h>
@@ -14,104 +24,75 @@
static const char *TAG = "evse_link"; static const char *TAG = "evse_link";
// Storage keys
#define _NVS_NAMESPACE "evse_link" #define _NVS_NAMESPACE "evse_link"
#define _KEY_MODE "mode" #define _KEY_MODE "mode"
#define _KEY_SELF_ID "self_id" #define _KEY_SELF_ID "self_id"
#define _KEY_ENABLED "enabled" #define _KEY_ENABLED "enabled"
// UART parameters
#define UART_PORT UART_NUM_2
#define UART_RX_BUF_SIZE 256
// Runtime config
static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER; static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER;
static uint8_t _self_id = 0x01; static uint8_t _self_id = 0x01;
static bool _enabled = false; static bool _enabled = false;
// Registered Rx callback
static evse_link_rx_cb_t _rx_cb = NULL; static evse_link_rx_cb_t _rx_cb = NULL;
static bool s_evse_link_inited = false;
// Forward declarations
extern void evse_link_master_init(void); extern void evse_link_master_init(void);
extern void evse_link_slave_init(void); extern void evse_link_slave_init(void);
static void framing_rx_cb(uint8_t src, uint8_t dest, static void framing_rx_cb(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) const uint8_t *payload, uint8_t len)
{ {
ESP_LOGD(TAG, "framing_rx_cb: src=0x%02X dest=0x%02X len=%u", src, dest, len);
if (_rx_cb) if (_rx_cb)
_rx_cb(src, dest, payload, len); _rx_cb(src, dest, payload, len);
} }
// Register protocol-level Rx callback
void evse_link_register_rx_cb(evse_link_rx_cb_t cb) void evse_link_register_rx_cb(evse_link_rx_cb_t cb)
{ {
_rx_cb = cb; _rx_cb = cb;
} }
// Load config from storage_service (NVS-backed)
static void load_link_config(void) static void load_link_config(void)
{ {
uint8_t u8 = 0; uint8_t u8 = 0;
// mode
esp_err_t err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_MODE, &u8, pdMS_TO_TICKS(500)); 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)) if (err == ESP_OK && (u8 == (uint8_t)EVSE_LINK_MODE_MASTER || u8 == (uint8_t)EVSE_LINK_MODE_SLAVE))
{
_mode = (evse_link_mode_t)u8; _mode = (evse_link_mode_t)u8;
}
else else
{ {
// default + persist
_mode = EVSE_LINK_MODE_MASTER; _mode = EVSE_LINK_MODE_MASTER;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode); (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode);
ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER (persisted async)", ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER", esp_err_to_name(err));
esp_err_to_name(err));
} }
// self_id
err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_SELF_ID, &u8, pdMS_TO_TICKS(500)); err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_SELF_ID, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK) if (err == ESP_OK)
{
_self_id = u8; _self_id = u8;
}
else else
{ {
_self_id = 0x01; _self_id = 0x01;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id); (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_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X", esp_err_to_name(err), _self_id);
esp_err_to_name(err), _self_id);
} }
// enabled
err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_ENABLED, &u8, pdMS_TO_TICKS(500)); err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_ENABLED, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK && u8 <= 1) if (err == ESP_OK && u8 <= 1)
{
_enabled = (u8 != 0); _enabled = (u8 != 0);
}
else else
{ {
_enabled = false; _enabled = false;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, 0); (void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, 0);
ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false (persisted async)", ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false", esp_err_to_name(err));
esp_err_to_name(err));
} }
} }
// Save config to storage_service (debounced)
static void save_link_config(void) 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_MODE, (uint8_t)_mode);
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id); (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)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) void evse_link_set_mode(evse_link_mode_t m)
{ {
if (m != EVSE_LINK_MODE_MASTER && m != EVSE_LINK_MODE_SLAVE) 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; } bool evse_link_is_enabled(void) { return _enabled; }
// RX task: reads bytes from UART and feeds framing
static void evse_link_rx_task(void *arg) static void evse_link_rx_task(void *arg)
{ {
(void)arg; (void)arg;
ESP_LOGI(TAG, "evse_link_rx_task started");
uint8_t buf[UART_RX_BUF_SIZE]; uint8_t buf[UART_RX_BUF_SIZE];
while (true) while (true)
@@ -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)); int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000));
if (len > 0) if (len > 0)
{ {
ESP_LOGD(TAG, "UART RX: len=%d first=0x%02X last=0x%02X", len, buf[0], buf[len - 1]);
for (int i = 0; i < len; ++i) for (int i = 0; i < len; ++i)
evse_link_recv_byte(buf[i]); evse_link_recv_byte(buf[i]);
} }
} }
} }
// Initialize EVSE-Link component
void evse_link_init(void) void evse_link_init(void)
{ {
// 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(); esp_err_t se = storage_service_init();
if (se != ESP_OK) if (se == ESP_OK)
{
ESP_LOGE(TAG, "storage_service_init failed: %s (using defaults in RAM)", esp_err_to_name(se));
// segue com defaults em RAM
}
else
{
load_link_config(); load_link_config();
} else
ESP_LOGE(TAG, "storage_service_init failed: %s (defaults in RAM)", esp_err_to_name(se));
ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d", ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d",
_mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S', _mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S',
@@ -187,21 +171,21 @@ void evse_link_init(void)
if (!_enabled) if (!_enabled)
return; return;
// 1) framing layer init (sets up mutex, UART driver, etc.)
evse_link_framing_init(); evse_link_framing_init();
evse_link_framing_register_cb(framing_rx_cb); evse_link_framing_register_cb(framing_rx_cb);
// 2) start RX task if (xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL) != pdPASS)
xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 3, NULL); {
ESP_LOGE(TAG, "Failed to create evse_link_rx task");
return;
}
// 3) delegate to master or slave
if (_mode == EVSE_LINK_MODE_MASTER) if (_mode == EVSE_LINK_MODE_MASTER)
evse_link_master_init(); evse_link_master_init();
else else
evse_link_slave_init(); evse_link_slave_init();
} }
// Send a frame (delegates to framing module)
bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len) bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len)
{ {
if (!evse_link_is_enabled()) if (!evse_link_is_enabled())
@@ -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); return evse_link_framing_send(dest, src, payload, len);
} }
// Receive byte (delegates to framing module)
void evse_link_recv_byte(uint8_t byte) void evse_link_recv_byte(uint8_t byte)
{ {
evse_link_framing_recv_byte(byte); evse_link_framing_recv_byte(byte);

View File

@@ -1,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 "evse_link_framing.h"
#include "driver/uart.h" #include "driver/uart.h"
#include "freertos/semphr.h" #include "freertos/semphr.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include <string.h> #include <string.h>
#include <stdint.h>
#include <stdbool.h>
static const char *TAG = "evse_framing"; static const char *TAG = "evse_framing";
@@ -10,118 +31,252 @@ static SemaphoreHandle_t tx_mutex = NULL;
static uint8_t seq = 0; static uint8_t seq = 0;
static evse_link_frame_cb_t rx_cb = NULL; static evse_link_frame_cb_t rx_cb = NULL;
// CRC-8 (polynomial 0x07) static bool s_framing_inited = false;
static uint8_t crc8(const uint8_t *data, uint8_t len)
// ---- 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; const int64_t now = esp_timer_get_time();
for (uint8_t i = 0; i < len; ++i) { if (*last_us == 0 || (now - *last_us) > interval_us)
crc ^= data[i]; {
for (uint8_t b = 0; b < 8; ++b) { *last_us = now;
if (crc & 0x80) { return true;
crc = (uint8_t)((crc << 1) ^ 0x07); }
} else { return false;
crc <<= 1; }
}
} // CRC-8 (poly 0x07), MSB-first, init=0x00
static uint8_t crc8_update(uint8_t crc, uint8_t data)
{
crc ^= data;
for (uint8_t b = 0; b < 8; ++b)
{
if (crc & 0x80)
crc = (uint8_t)((crc << 1) ^ 0x07);
else
crc <<= 1;
} }
return crc; return crc;
} }
void evse_link_framing_init(void) static esp_err_t configure_uart(void)
{ {
// 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 = { uart_config_t cfg = {
.baud_rate = UART_BAUDRATE, .baud_rate = UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS, .data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE, .parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1, .stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT, .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) esp_err_t err = uart_param_config(UART_PORT, &cfg);
uart_set_pin(UART_PORT, if (err != ESP_OK)
TX_PIN, // MB_UART_TXD (ex: GPIO17) {
RX_PIN, // MB_UART_RXD (ex: GPIO16) ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
RTS_PIN, // MB_UART_RTS (ex: GPIO2, DE+RE) return err;
UART_PIN_NO_CHANGE); }
// Modo RS485 half-duplex: driver controla RTS/DE/RE automaticamente // TX/RX/RTS na UART (RTS controla DE//RE em RS485 half-duplex)
uart_set_mode(UART_PORT, UART_MODE_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); UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE);
} }
bool evse_link_framing_send(uint8_t dest, uint8_t src, bool evse_link_framing_send(uint8_t dest, uint8_t src,
const uint8_t *payload, uint8_t len) const uint8_t *payload, uint8_t len)
{ {
if (len > EVSE_LINK_MAX_PAYLOAD) { if (!s_framing_inited)
ESP_LOGW(TAG, "Payload too large: %u (max=%u)", {
len, EVSE_LINK_MAX_PAYLOAD); ESP_LOGE(TAG, "Framing not initialized");
return false; 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"); ESP_LOGW(TAG, "Failed to take TX mutex");
return false; return false;
} }
// Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END // Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END
// LEN on wire = SEQ + PAYLOAD (>=1)
uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7]; uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7];
int idx = 0; int idx = 0;
frame[idx++] = MAGIC_START; frame[idx++] = MAGIC_START;
frame[idx++] = dest; frame[idx++] = dest;
frame[idx++] = src; frame[idx++] = src;
frame[idx++] = (uint8_t)(len + 1); // LEN = SEQ + payload frame[idx++] = (uint8_t)(len + 1);
frame[idx++] = seq; frame[idx++] = seq;
if (len > 0 && payload != NULL) { if (len > 0)
{
memcpy(&frame[idx], payload, len); memcpy(&frame[idx], payload, len);
idx += len; idx += len;
} }
// CRC cobre: DEST + SRC + LEN + SEQ + PAYLOAD // CRC: DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; uint8_t crc = 0;
memcpy(crc_input, &frame[1], 3 + 1 + len); crc = crc8_update(crc, dest);
uint8_t crc = crc8(crc_input, (uint8_t)(3 + 1 + len)); crc = crc8_update(crc, src);
frame[idx++] = crc; crc = crc8_update(crc, (uint8_t)(len + 1));
crc = crc8_update(crc, seq);
for (uint8_t i = 0; i < len; ++i)
crc = crc8_update(crc, payload[i]);
frame[idx++] = crc;
frame[idx++] = MAGIC_END; frame[idx++] = MAGIC_END;
// Envia frame completo
int written = uart_write_bytes(UART_PORT, (const char *)frame, idx); 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); ESP_LOGW(TAG, "uart_write_bytes wrote %d of %d", written, idx);
} }
// Aguarda TX terminar (o driver controla DE/RE via RTS) // Aguarda TX terminar; driver RS485 devolve RTS para RX automaticamente.
uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(20)); (void)uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(700));
xSemaphoreGive(tx_mutex); 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); dest, src, len, seq);
seq++; // incrementa sequência após envio seq++;
return true; return true;
} }
void evse_link_framing_recv_byte(uint8_t b) void evse_link_framing_recv_byte(uint8_t b)
{ {
// Máquina de estados para parsing do frame
static enum { static enum {
ST_WAIT_START = 0, ST_WAIT_START = 0,
ST_WAIT_DEST, ST_WAIT_DEST,
@@ -133,17 +288,46 @@ void evse_link_framing_recv_byte(uint8_t b)
ST_WAIT_END ST_WAIT_END
} rx_state = ST_WAIT_START; } rx_state = ST_WAIT_START;
static uint8_t rx_dest; static uint8_t rx_dest;
static uint8_t rx_src; static uint8_t rx_src;
static uint8_t rx_len; // inclui SEQ + payload static uint8_t rx_len; // inclui SEQ + payload (>=1)
static uint8_t rx_seq; static uint8_t rx_seq;
static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD]; static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD];
static uint8_t rx_pos; static uint8_t rx_pos;
static uint8_t rx_crc; static uint8_t rx_crc;
switch (rx_state) { static int64_t s_last_byte_us = 0;
static int64_t s_last_bad_len_log_us = 0;
static int64_t s_last_bad_crc_log_us = 0;
#define RESET_PARSER() \
do \
{ \
rx_state = ST_WAIT_START; \
rx_dest = 0; \
rx_src = 0; \
rx_len = 0; \
rx_seq = 0; \
rx_pos = 0; \
rx_crc = 0; \
} while (0)
const int64_t now_us = esp_timer_get_time();
// Timeout inter-byte: frame morreu a meio -> reseta
if (rx_state != ST_WAIT_START && s_last_byte_us != 0 &&
(now_us - s_last_byte_us) > EVSE_LINK_INTERBYTE_TIMEOUT_US)
{
RESET_PARSER();
}
s_last_byte_us = now_us;
switch (rx_state)
{
case ST_WAIT_START: case ST_WAIT_START:
if (b == MAGIC_START) { if (b == MAGIC_START)
{
rx_pos = 0;
rx_state = ST_WAIT_DEST; rx_state = ST_WAIT_DEST;
} }
break; break;
@@ -159,28 +343,50 @@ void evse_link_framing_recv_byte(uint8_t b)
break; break;
case ST_WAIT_LEN: 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_pos = 0;
rx_state = ST_WAIT_SEQ; rx_state = ST_WAIT_SEQ;
break; break;
case ST_WAIT_SEQ: case ST_WAIT_SEQ:
rx_seq = b; rx_seq = b;
if (rx_len > 1) { rx_state = (rx_len > 1) ? ST_READING : ST_WAIT_CRC;
rx_state = ST_READING;
} else {
rx_state = ST_WAIT_CRC;
}
break; break;
case ST_READING: 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; 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; rx_state = ST_WAIT_CRC;
}
break; break;
}
case ST_WAIT_CRC: case ST_WAIT_CRC:
rx_crc = b; rx_crc = b;
@@ -188,41 +394,44 @@ void evse_link_framing_recv_byte(uint8_t b)
break; break;
case ST_WAIT_END: case ST_WAIT_END:
if (b == MAGIC_END) { if (b == MAGIC_END)
// Monta buffer para verificar CRC: {
// DEST + SRC + LEN + SEQ + PAYLOAD uint8_t expected = 0;
uint8_t temp[3 + 1 + EVSE_LINK_MAX_PAYLOAD]; expected = crc8_update(expected, rx_dest);
int temp_len = 0; expected = crc8_update(expected, rx_src);
temp[temp_len++] = rx_dest; expected = crc8_update(expected, rx_len);
temp[temp_len++] = rx_src; expected = crc8_update(expected, rx_seq);
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;
}
uint8_t expected = crc8(temp, (uint8_t)temp_len); const uint8_t payload_len = (uint8_t)(rx_len - 1);
if (expected == rx_crc) { for (uint8_t i = 0; i < payload_len; ++i)
uint8_t payload_len = (uint8_t)(rx_len - 1); // exclui SEQ expected = crc8_update(expected, rx_buf[i]);
if (rx_cb) {
if (expected == rx_crc)
{
if (rx_cb)
rx_cb(rx_src, rx_dest, rx_buf, payload_len); 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", ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u",
rx_src, rx_dest, payload_len, rx_seq); rx_src, rx_dest, payload_len, rx_seq);
} else { }
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X", else
expected, rx_crc); {
if (log_ratelimit_ok(&s_last_bad_crc_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X",
expected, rx_crc);
}
} }
} }
// Em qualquer caso, volta a esperar novo frame RESET_PARSER();
rx_state = ST_WAIT_START;
break; break;
default: default:
rx_state = ST_WAIT_START; RESET_PARSER();
break; break;
} }
#undef RESET_PARSER
} }
void evse_link_framing_register_cb(evse_link_frame_cb_t cb) void evse_link_framing_register_cb(evse_link_frame_cb_t cb)

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.h"
#include "evse_link_events.h" #include "evse_link_events.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h" #include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_timer.h"
#include "esp_random.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
@@ -14,37 +31,180 @@
static const char *TAG = "evse_link_master"; static const char *TAG = "evse_link_master";
// Link commands // Link commands (opcode no payload[0])
#define CMD_POLL 0x01 #define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 #define CMD_HEARTBEAT 0x02
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_CONFIG_BROADCAST 0x03 #define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08 #define CMD_SET_CURRENT 0x08
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização a slave #define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A
// payload lengths (exclui byte de opcode) // payload lengths (INCLUI opcode)
#define LEN_POLL_REQ 1 // [ CMD_POLL ] #define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ]
#define LEN_POLL_RESP 9 // [ CMD_POLL, float V(4), float I(4) ] #define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, ... ]
#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ] #define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, amps_lo, amps_hi ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ] // Presence monitoring
#define LEN_HEARTBEAT_ACK 1 #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 typedef struct
{ {
TimerHandle_t timer; TimerHandle_t timer;
TickType_t interval; TickType_t interval;
} timer_def_t; } timer_def_t;
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)};
static timer_def_t hb_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)}; // POLL a cada 60s (comentário corrigido)
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(60000)};
static bool s_handlers_registered = false;
// ACK task/queue
static QueueHandle_t s_ack_q = NULL;
static TaskHandle_t s_ack_task = NULL;
// Dedupe: 1 ACK pendente por slave
static bool s_ack_pending[256] = {0};
static void post_presence_event(evse_link_event_t evt, uint8_t slave_id)
{
evse_link_slave_presence_event_t p = {.slave_id = slave_id};
(void)esp_event_post(EVSE_LINK_EVENTS, evt, &p, sizeof(p), portMAX_DELAY);
}
static void mark_slave_seen(uint8_t slave_id)
{
const int64_t now = esp_timer_get_time();
bool was_offline = false;
portENTER_CRITICAL(&s_presence_mux);
slave_presence_t *p = &s_presence[slave_id];
p->last_seen_us = now;
if (!p->online)
{
p->online = true;
was_offline = true;
}
portEXIT_CRITICAL(&s_presence_mux);
if (was_offline)
{
ESP_LOGI(TAG, "Slave 0x%02X ONLINE", slave_id);
post_presence_event(LINK_EVENT_SLAVE_ONLINE, slave_id);
}
}
static void presence_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
const int64_t now = esp_timer_get_time();
const int64_t timeout_us = (int64_t)SLAVE_OFFLINE_TIMEOUT_MS * 1000;
const uint8_t self = evse_link_get_self_id();
for (int i = 0; i < 256; ++i)
{
if ((uint8_t)i == self)
continue;
bool online;
int64_t last_seen;
portENTER_CRITICAL(&s_presence_mux);
online = s_presence[i].online;
last_seen = s_presence[i].last_seen_us;
portEXIT_CRITICAL(&s_presence_mux);
if (!online)
continue;
if (last_seen > 0 && (now - last_seen) > timeout_us)
{
portENTER_CRITICAL(&s_presence_mux);
s_presence[i].online = false;
portEXIT_CRITICAL(&s_presence_mux);
ESP_LOGW(TAG, "Slave 0x%02X OFFLINE (no heartbeat for %d ms)", i, SLAVE_OFFLINE_TIMEOUT_MS);
post_presence_event(LINK_EVENT_SLAVE_OFFLINE, (uint8_t)i);
}
}
}
// Enfileira ACK sem duplicar por slave
static void enqueue_ack(uint8_t slave_id)
{
if (!s_ack_q)
return;
// Dedupe: se já existe ACK pendente para este slave, não enfileira outro
if (s_ack_pending[slave_id])
return;
s_ack_pending[slave_id] = true;
if (xQueueSendToBack(s_ack_q, &slave_id, 0) != pdTRUE)
{
s_ack_pending[slave_id] = false;
ESP_LOGW(TAG, "ACK queue full, dropping ACK for 0x%02X", slave_id);
}
}
// --- ACK task (não bloqueia RX) ---
static void ack_task(void *arg)
{
(void)arg;
for (;;)
{
uint8_t slave_id = 0;
if (xQueueReceive(s_ack_q, &slave_id, portMAX_DELAY) != pdTRUE)
continue;
// libera dedupe
s_ack_pending[slave_id] = false;
// backoff com jitter
uint32_t backoff = ACK_BACKOFF_MIN_MS +
(esp_random() % (ACK_BACKOFF_MAX_MS - ACK_BACKOFF_MIN_MS + 1));
vTaskDelay(pdMS_TO_TICKS(backoff));
uint8_t ack[] = {CMD_HEARTBEAT_ACK};
bool ok = evse_link_send(slave_id, ack, sizeof(ack));
ESP_LOGI(TAG, "CMD_HEARTBEAT_ACK to 0x%02X ok=%d", slave_id, ok);
}
}
// --- Send new limit to slave --- // --- Send new limit to slave ---
static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *data) static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *data)
{ {
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT) (void)arg;
(void)base;
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT || data == NULL)
return; 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; uint8_t slave_id = evt->slave_id;
uint16_t max_current = evt->max_current; 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, CMD_SET_CURRENT,
(uint8_t)(max_current & 0xFF), (uint8_t)(max_current & 0xFF),
(uint8_t)(max_current >> 8)}; (uint8_t)(max_current >> 8)};
evse_link_send(slave_id, buf, sizeof(buf));
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) 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; return;
}
const auth_tag_event_data_t *ev = (const auth_tag_event_data_t *)data; 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); ESP_LOGI(TAG, "Tag %s not authorized, not propagating to slaves", ev->tag);
return; return;
} }
// Construir payload: [ CMD_AUTH_GRANTED, tag..., '\0' ]
uint8_t buf[1 + EVSE_LINK_TAG_MAX_LEN]; uint8_t buf[1 + EVSE_LINK_TAG_MAX_LEN];
buf[0] = CMD_AUTH_GRANTED; 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); strncpy((char *)&buf[1], ev->tag, EVSE_LINK_TAG_MAX_LEN - 1);
((char *)&buf[1])[EVSE_LINK_TAG_MAX_LEN - 1] = '\0'; ((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) (void)evse_link_send(0xFF, buf, payload_len);
uint8_t dest = 0xFF; ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED (broadcast) tag=%s", (char *)&buf[1]);
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]);
}
} }
// --- Polling broadcast callback --- // --- Polling broadcast callback ---
static void poll_timer_cb(TimerHandle_t xTimer) static void poll_timer_cb(TimerHandle_t xTimer)
{ {
ESP_LOGD(TAG, "Broadcasting CMD_POLL to all slaves"); (void)xTimer;
;
// Optionally post event LINK_EVENT_MASTER_POLL_SENT
}
// --- Heartbeat timeout callback --- uint8_t poll[] = {CMD_POLL};
static void hb_timer_cb(TimerHandle_t xTimer) bool ok = evse_link_send(0xFF, poll, sizeof(poll));
{ ESP_LOGI(TAG, "POLL send ok=%d", ok);
ESP_LOGW(TAG, "Heartbeat timeout: possible slave offline");
// post event LINK_EVENT_SLAVE_OFFLINE ???
} }
static void on_frame_master(uint8_t src, uint8_t dest, static void on_frame_master(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) const uint8_t *payload, uint8_t len)
{ {
if (len < 1) 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; return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
return;
uint8_t cmd = payload[0]; uint8_t cmd = payload[0];
switch (cmd) switch (cmd)
{ {
case CMD_HEARTBEAT: case CMD_HEARTBEAT:
{ {
ESP_LOGD(TAG, "HEARTBEAT from 0x%02X: %u bytes", src, len);
if (len != LEN_HEARTBEAT) 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); ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len);
return; return;
} }
bool charging = payload[1] != 0;
uint16_t hw_max = payload[2] | (payload[3] << 8);
uint16_t runtime = payload[4] | (payload[5] << 8);
ESP_LOGI(TAG, "Heartbeat from 0x%02X: charging=%d hw_max=%uA runtime=%uA", bool charging = payload[1] != 0;
src, charging, hw_max, runtime); uint16_t hw_max = (uint16_t)(payload[2] | ((uint16_t)payload[3] << 8));
uint16_t runtime = (uint16_t)(payload[4] | ((uint16_t)payload[5] << 8));
mark_slave_seen(src);
loadbalancer_slave_status_event_t status = { loadbalancer_slave_status_event_t status = {
.slave_id = src, .slave_id = src,
.charging = charging, .charging = charging,
.hw_max_current = (float)hw_max, .hw_max_current = (float)hw_max,
.runtime_current = (float)runtime, // corrente real medida no slave .runtime_current = (float)runtime,
.timestamp_us = esp_timer_get_time()}; .timestamp_us = esp_timer_get_time()};
esp_event_post(LOADBALANCER_EVENTS, (void)esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_STATUS, LOADBALANCER_EVENT_SLAVE_STATUS,
&status, sizeof(status), portMAX_DELAY); &status, sizeof(status), portMAX_DELAY);
// Enviar ACK de volta // ACK deferido e deduplicado
uint8_t ack[] = {CMD_HEARTBEAT_ACK}; enqueue_ack(src);
evse_link_send(src, ack, sizeof(ack));
ESP_LOGD(TAG, "Sent HEARTBEAT_ACK to 0x%02X", src);
break; break;
} }
case CMD_POLL:
ESP_LOGD(TAG, "Received POLL_RESP from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST: case CMD_CONFIG_BROADCAST:
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", if (len >= LEN_CONFIG_BROADCAST)
src, payload[1]); ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", src, payload[1]);
else
ESP_LOGW(TAG, "CONFIG_BROADCAST ack short len=%u from 0x%02X", len, src);
break; break;
default: default:
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src); ESP_LOGD(TAG, "Cmd 0x%02X from 0x%02X (ignored/unknown)", cmd, src);
break;
} }
} }
// --- Master initialization ---
void evse_link_master_init(void) void evse_link_master_init(void)
{ {
if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled()) if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled())
{
return; return;
}
ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id()); ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_master); evse_link_register_rx_cb(on_frame_master);
// register loadbalancer event // Cria queue/task de ACK uma vez
ESP_ERROR_CHECK( if (s_ack_q == NULL)
esp_event_handler_register( {
s_ack_q = xQueueCreate(ACK_QUEUE_LEN, sizeof(uint8_t));
if (!s_ack_q)
{
ESP_LOGE(TAG, "Failed to create ACK queue");
}
else
{
if (xTaskCreate(ack_task, "evse_ack", 4096, NULL, 4, &s_ack_task) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create ACK task");
vQueueDelete(s_ack_q);
s_ack_q = NULL;
s_ack_task = NULL;
}
}
}
if (!s_handlers_registered)
{
s_handlers_registered = true;
ESP_ERROR_CHECK(esp_event_handler_register(
LOADBALANCER_EVENTS, LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
on_new_limit, on_new_limit,
NULL)); NULL));
// 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_EVENTS,
AUTH_EVENT_TAG_PROCESSED, AUTH_EVENT_TAG_PROCESSED,
on_auth_result, on_auth_result,
NULL)); NULL));
}
// create and start poll timer if (poll_timer.timer == NULL)
poll_timer.timer = xTimerCreate("poll_tmr", {
poll_timer.interval, poll_timer.timer = xTimerCreate("poll_tmr",
pdTRUE, NULL, poll_timer.interval,
poll_timer_cb); pdTRUE, NULL,
xTimerStart(poll_timer.timer, 0); poll_timer_cb);
if (poll_timer.timer)
(void)xTimerStart(poll_timer.timer, 0);
}
// create and start heartbeat monitor timer if (s_presence_timer == NULL)
hb_timer.timer = xTimerCreate("hb_tmr", {
hb_timer.interval, s_presence_timer = xTimerCreate("presence_tmr",
pdFALSE, NULL, pdMS_TO_TICKS(PRESENCE_CHECK_MS),
hb_timer_cb); pdTRUE, NULL,
xTimerStart(hb_timer.timer, 0); 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.h"
#include "evse_link_events.h" #include "evse_link_events.h"
#include "loadbalancer_events.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/timers.h" #include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/portmacro.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_random.h"
#include "esp_err.h"
#include "evse_events.h" #include "evse_events.h"
#include "evse_state.h" #include "evse_state.h"
#include "evse_config.h" #include "evse_config.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
static const char *TAG = "evse_link_slave"; static const char *TAG = "evse_link_slave";
// Link commands #define MASTER_ID 0x01
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 // not used by slave
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização
// payload lengths (exclui seq byte) // Commands (opcode no payload[0])
#define LEN_POLL_REQ 1 // [ CMD_POLL ] #define CMD_POLL 0x01
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ] #define CMD_HEARTBEAT 0x02
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ] #define CMD_CONFIG_BROADCAST 0x03
#define LEN_HEARTBEAT_ACK 1 // [ CMD_HEARTBEAT_ACK ] #define CMD_SET_CURRENT 0x08
#define LEN_HEARTBEAT 6 // CMD_HEARTBEAT + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi #define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A
// #define CMD_RESUME 0x0B // se existir
// Timing // lengths (INCLUI opcode)
#define FALLBACK_TIMEOUT_MS 120000 #define LEN_SET_CURRENT 3
#define FALLBACK_TIMEOUT_MS 180000
// --- Política de fallback ---
// 1 = mais seguro: PAUSE (revoga autorização)
// 0 = força corrente mínima (pode continuar a carregar dependendo do core)
#ifndef EVSE_LINK_FALLBACK_PAUSE
#define EVSE_LINK_FALLBACK_PAUSE 1
#endif
// --- Confirmações via heartbeat (deferidas) ---
#define HB_REQ_QUEUE_LEN 8
typedef enum
{
HB_REQ_SEND = 1,
} hb_req_t;
static TimerHandle_t fallback_timer = NULL; static TimerHandle_t fallback_timer = NULL;
static bool safe_mode = false; static TaskHandle_t hb_task_handle = NULL;
// --- Helper to send a heartbeat frame --- // Task para enviar heartbeat sem bloquear RX callback
static void send_heartbeat_frame(void) { static TaskHandle_t hb_sender_task_handle = NULL;
bool charging = evse_state_is_charging(evse_get_state()); static QueueHandle_t hb_req_q = NULL;
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA", // "safe mode" (master offline): aplica fallback local e nao sai com POLL/ACK.
charging, hw_max, runtime); // Só sai com comando explícito de potência (SET_CURRENT / RESUME).
static bool safe_mode = false;
static uint16_t saved_runtime_limit = 0; // informativo
// "remote pause" (SET_CURRENT=0)
static bool paused_by_master = false;
static bool paused_prev_authorized = false;
static portMUX_TYPE s_state_mux = portMUX_INITIALIZER_UNLOCKED;
static bool evse_handler_registered = false;
static size_t bounded_strlen_u8(const uint8_t *s, size_t max_len)
{
size_t i = 0;
if (!s)
return 0;
while (i < max_len && s[i] != 0)
i++;
return i;
}
static void send_heartbeat_frame_now(void)
{
bool charging = evse_state_is_charging(evse_get_state());
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
uint8_t hb[] = { uint8_t hb[] = {
CMD_HEARTBEAT, CMD_HEARTBEAT,
charging ? 1 : 0, charging ? 1 : 0,
(uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8), (uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8),
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8) (uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)};
};
// Broadcast to master (0xFF) (void)evse_link_send(MASTER_ID, hb, sizeof(hb)); // UNICAST
evse_link_send(0xFF, hb, sizeof(hb)); ESP_LOGI(TAG, "Send Heartbeat Frame");
} }
// pede heartbeat sem bloquear quem chama (RX callback, event handler, etc.)
// --- EVSE state change handler --- static void request_heartbeat_send(void)
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { {
if (base!=EVSE_EVENTS || id!=EVSE_EVENT_STATE_CHANGED || data==NULL) return; if (hb_req_q)
const evse_state_event_data_t *evt = data; {
if (evt->state==EVSE_STATE_EVENT_IDLE || evt->state==EVSE_STATE_EVENT_CHARGING) { hb_req_t req = HB_REQ_SEND;
send_heartbeat_frame(); // não bloqueia; se encher, apenas ignora (heartbeat periódico já existe)
(void)xQueueSendToBack(hb_req_q, &req, 0);
} }
else
{
// fallback: se queue ainda não existe, manda direto
send_heartbeat_frame_now();
}
}
static void hb_sender_task(void *arg)
{
(void)arg;
hb_req_t req;
for (;;)
{
if (xQueueReceive(hb_req_q, &req, portMAX_DELAY) != pdTRUE)
continue;
if (req == HB_REQ_SEND)
{
send_heartbeat_frame_now();
}
}
}
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
return;
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
// Enforce pause: se algo tentar voltar a carregar enquanto paused_by_master, revoga auth.
bool paused;
portENTER_CRITICAL(&s_state_mux);
paused = paused_by_master;
portEXIT_CRITICAL(&s_state_mux);
if (paused && evt->state == EVSE_STATE_EVENT_CHARGING)
{
// Garante que não continua a carregar por clamp/auto-auth
evse_state_set_authorized(false);
}
// Envia heartbeat quando entra em IDLE ou CHARGING (estado relevante)
if (evt->state == EVSE_STATE_EVENT_IDLE || evt->state == EVSE_STATE_EVENT_CHARGING)
request_heartbeat_send();
}
// Sai de safe-mode APENAS com comando explícito (SET_CURRENT / RESUME).
static void maybe_exit_safe_mode_on_explicit_power_cmd(uint8_t cmd)
{
bool in_safe;
portENTER_CRITICAL(&s_state_mux);
in_safe = safe_mode;
portEXIT_CRITICAL(&s_state_mux);
if (!in_safe)
return;
if (cmd == CMD_SET_CURRENT /*|| cmd == CMD_RESUME*/)
{
portENTER_CRITICAL(&s_state_mux);
safe_mode = false;
portEXIT_CRITICAL(&s_state_mux);
ESP_LOGI(TAG, "Exiting safe mode due to explicit cmd 0x%02X", cmd);
}
}
static void apply_pause_by_master(void)
{
bool prev_auth = evse_state_get_authorized();
portENTER_CRITICAL(&s_state_mux);
paused_by_master = true;
paused_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_state_mux);
// Revoga autorização para parar contactor/pilot via core
if (prev_auth)
evse_state_set_authorized(false);
// Mantém runtime num valor seguro (não 0, pois clamp -> 6A).
// O "pause" efetivo é pela autorização=false.
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
}
static void clear_pause_by_master_if_any(void)
{
bool was_paused;
bool prev_auth;
portENTER_CRITICAL(&s_state_mux);
was_paused = paused_by_master;
prev_auth = paused_prev_authorized;
paused_by_master = false;
paused_prev_authorized = false;
portEXIT_CRITICAL(&s_state_mux);
// Se estava autorizado antes do pause, tenta reautorizar
if (was_paused && prev_auth)
evse_state_set_authorized(true);
} }
static void on_frame_slave(uint8_t src, uint8_t dest, static void on_frame_slave(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) { const uint8_t *payload, uint8_t len)
if (dest != evse_link_get_self_id() && dest != 0xFF) return; {
if (len < 1) return; const uint8_t self = evse_link_get_self_id();
// Muito verboso em caminho quente; deixa em DEBUG
ESP_LOGD(TAG, "RX frames (src=0x%02X dest=0x%02X len=%u self=0x%02X)", src, dest, len, self);
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
{
ESP_LOGW(TAG, "RX invalid: payload NULL or len<1 (len=%u)", len);
return;
}
// Só aceitar comandos do master
if (src != MASTER_ID)
{
ESP_LOGW(TAG, "RX ignore: non-master src=0x%02X", src);
return;
}
// Qualquer frame válido do master => link vivo (reset do fallback)
if (fallback_timer)
(void)xTimerReset(fallback_timer, 0);
uint8_t cmd = payload[0]; uint8_t cmd = payload[0];
switch (cmd) {
switch (cmd)
{
case CMD_POLL: case CMD_POLL:
ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src); // Liveness only. Não sai de safe-mode e não restaura limites.
ESP_LOGI(TAG, "CMD_POLL from 0x%02X", src);
break; break;
case CMD_CONFIG_BROADCAST: case CMD_CONFIG_BROADCAST:
ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src); ESP_LOGI(TAG, "CMD_CONFIG_BROADCAST from 0x%02X", src);
break; break;
case CMD_SET_CURRENT: { case CMD_HEARTBEAT_ACK:
if (len < LEN_SET_CURRENT) { ESP_LOGI(TAG, "HEARTBEAT_ACK from 0x%02X", src);
ESP_LOGW(TAG, "SET_CURRENT from 0x%02X with invalid length %u", src, len); 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; 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); 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, // confirma sem bloquear RX
&amps, sizeof(amps), portMAX_DELAY); 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; break;
} }
case CMD_HEARTBEAT_ACK: case CMD_AUTH_GRANTED:
ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src); {
if (fallback_timer) { if (len < 2)
xTimerReset(fallback_timer, 0); {
if (safe_mode) { ESP_LOGW(TAG, "AUTH_GRANTED invalid len=%u from 0x%02X", len, src);
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);
break; 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}; evse_link_auth_grant_event_t ev = {0};
strncpy(ev.tag, tag, EVSE_LINK_TAG_MAX_LEN - 1); size_t copy_len = tag_len;
ev.tag[EVSE_LINK_TAG_MAX_LEN - 1] = '\0'; 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( // AUTH_GRANTED não deve sair de safe-mode automaticamente
EVSE_LINK_EVENTS, ESP_LOGI(TAG, "AUTH_GRANTED from 0x%02X tag='%s'", src, ev.tag);
LINK_EVENT_REMOTE_AUTH_GRANTED,
&ev,
sizeof(ev),
portMAX_DELAY);
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)); ESP_LOGE(TAG, "Failed to post LINK_EVENT_REMOTE_AUTH_GRANTED: %s", esp_err_to_name(err));
}
break; break;
} }
default: default:
ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src); ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
break;
} }
} }
static void slave_heartbeat_task(void *arg)
{
(void)arg;
const uint32_t period_ms = 60000;
uint8_t id = evse_link_get_self_id();
// --- Periodic heartbeat task --- // desfasamento por ID: 2s, 4s, 6s...
static void slave_heartbeat_task(void *arg) { vTaskDelay(pdMS_TO_TICKS((uint32_t)id * 2000));
const TickType_t interval = pdMS_TO_TICKS(10000);
for (;;) { for (;;)
send_heartbeat_frame(); {
vTaskDelay(interval); request_heartbeat_send();
// jitter opcional
uint32_t jitter_ms = esp_random() % 201; // 0..200ms
vTaskDelay(pdMS_TO_TICKS(period_ms + jitter_ms));
} }
} }
// --- Fallback safe mode callback --- static void fallback_timer_cb(TimerHandle_t xTimer)
static void fallback_timer_cb(TimerHandle_t xTimer) { {
if (!safe_mode) { (void)xTimer;
// entra safe_mode uma vez
bool already_safe;
portENTER_CRITICAL(&s_state_mux);
already_safe = safe_mode;
if (!safe_mode)
safe_mode = true; safe_mode = true;
ESP_LOGW(TAG, "Fallback timeout: entering safe mode"); portEXIT_CRITICAL(&s_state_mux);
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
esp_event_post(EVSE_LINK_EVENTS, if (already_safe)
LINK_EVENT_SLAVE_OFFLINE, return;
NULL, 0, portMAX_DELAY);
} saved_runtime_limit = evse_get_runtime_charging_current();
#if EVSE_LINK_FALLBACK_PAUSE
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA). Policy=PAUSE",
(unsigned)saved_runtime_limit);
// pausar é mais seguro quando o master “morre”
apply_pause_by_master();
#else
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA, forcing %uA). Policy=MIN",
(unsigned)saved_runtime_limit, (unsigned)MIN_CHARGING_CURRENT_LIMIT);
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
#endif
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE,
NULL, 0, portMAX_DELAY);
// opcional: manda heartbeat para indicar estado atual
request_heartbeat_send();
} }
// --- Slave initialization --- void evse_link_slave_init(void)
void evse_link_slave_init(void) { {
if (evse_link_get_mode()!=EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) return; if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled())
return;
ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id()); ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_slave); evse_link_register_rx_cb(on_frame_slave);
// start periodic heartbeat // cria queue/task do sender (para não mandar UART TX no callback RX)
xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 5, NULL); if (hb_req_q == NULL)
{
// fallback timer hb_req_q = xQueueCreate(HB_REQ_QUEUE_LEN, sizeof(hb_req_t));
fallback_timer = xTimerCreate("fallback_tmr", if (!hb_req_q)
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS), {
pdFALSE, NULL, ESP_LOGE(TAG, "Failed to create HB request queue (fallback to direct send)");
fallback_timer_cb); }
if (fallback_timer) { else
xTimerStart(fallback_timer, 0); {
if (xTaskCreate(hb_sender_task, "hb_sender", 3072, NULL, 3, &hb_sender_task_handle) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create hb_sender task");
vQueueDelete(hb_req_q);
hb_req_q = NULL;
hb_sender_task_handle = NULL;
}
}
} }
// react to EVSE state changes if (hb_task_handle == NULL)
ESP_ERROR_CHECK( {
esp_event_handler_register( if (xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 3, &hb_task_handle) != pdPASS)
EVSE_EVENTS, {
EVSE_EVENT_STATE_CHANGED, ESP_LOGE(TAG, "Failed to create slave_heartbeat_task");
evse_event_handler, hb_task_handle = NULL;
NULL }
) }
);
}
// === 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;
}
}

View File

@@ -1,5 +1,5 @@
set(srcs set(srcs
"src/input_filter.c" "src/loadbalancer.c" "src/loadbalancer_events.c" "src/input_filter.c" "src/loadbalancer.c" "src/pv_optimizer.c" "src/grid_limiter.c" "src/loadbalancer_events.c"
) )
idf_component_register(SRCS "${srcs}" idf_component_register(SRCS "${srcs}"

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
#ifndef LOGGER_H_
#define LOGGER_H_
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#ifdef __cplusplus
extern "C"
{
#endif
#define LOGGER_SERIAL_BIT BIT0
extern EventGroupHandle_t logger_event_group;
void logger_init(void);
void logger_print(const char *str);
int logger_vprintf(const char *fmt, va_list args);
uint16_t logger_count(void);
// opcional: quantas mensagens foram dropadas por contenção de mutex
uint32_t logger_dropped_count(void);
/**
* ⚠️ API antiga (não recomendada): devolve ponteiro interno.
* Pode ficar inválido se houver novas escritas/rotação.
*/
bool logger_read(uint16_t *index, char **str, uint16_t *len);
/**
* ✅ API recomendada: copia a entrada para buffer do caller (safe).
* out é sempre terminado com '\0' (se out_sz > 0).
*/
bool logger_read_copy(uint16_t *index, char *out, uint16_t out_sz, uint16_t *out_len);
#ifdef __cplusplus
}
#endif
#endif /* LOGGER_H_ */

View File

@@ -1,34 +0,0 @@
#ifndef OUTPUT_BUFFER_H_
#define OUTPUT_BUFFER_H_
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C"
{
#endif
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);
#ifdef __cplusplus
}
#endif
#endif /* OUTPUT_BUFFER_H_ */

View File

@@ -1,192 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.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 // tamanho total do buffer circular
#define MAX_LOG_SIZE 256 // ✅ reduzir stack/CPU; era 512
static SemaphoreHandle_t mutex = NULL;
static output_buffer_t *buffer = NULL;
EventGroupHandle_t logger_event_group = NULL;
// opcional: contador de mensagens dropadas quando mutex está ocupado
static volatile uint32_t s_dropped = 0;
void logger_init(void)
{
// Permitir múltiplas chamadas seguras
if (mutex != NULL)
{
return;
}
mutex = xSemaphoreCreateMutex();
configASSERT(mutex != NULL);
logger_event_group = xEventGroupCreate();
configASSERT(logger_event_group != NULL);
buffer = output_buffer_create(LOG_BUFFER_SIZE);
configASSERT(buffer != NULL);
}
uint16_t logger_count(void)
{
if (!mutex || !buffer)
{
return 0;
}
// ✅ não bloquear para sempre (mas aqui pode bloquear sem stress)
xSemaphoreTake(mutex, portMAX_DELAY);
uint16_t c = buffer->count;
xSemaphoreGive(mutex);
return c;
}
uint32_t logger_dropped_count(void)
{
return s_dropped;
}
void logger_print(const char *str)
{
if (!str || !mutex || !buffer)
{
return;
}
// Limitar comprimento para evitar entradas enormes
size_t len = strlen(str);
if (len > (MAX_LOG_SIZE - 1))
{
len = MAX_LOG_SIZE - 1;
}
// ✅ NÃO bloquear aqui: se o mutex estiver ocupado, dropa
if (xSemaphoreTake(mutex, 0) != pdTRUE)
{
s_dropped++;
return;
}
output_buffer_append_buf(buffer, str, (uint16_t)len);
xEventGroupSetBits(logger_event_group, LOGGER_SERIAL_BIT);
xSemaphoreGive(mutex);
}
int logger_vprintf(const char *fmt, va_list args)
{
char log_buf[MAX_LOG_SIZE];
#ifdef CONFIG_ESP_CONSOLE_UART
// Duplicar va_list para ecoar na UART sem consumir o original
va_list args_copy;
va_copy(args_copy, args);
vprintf(fmt, args_copy);
va_end(args_copy);
#endif
// Se ainda não inicializado, apenas formatar para devolver comprimento
if (!mutex || !buffer)
{
int len = vsnprintf(log_buf, MAX_LOG_SIZE, fmt, args);
if (len < 0)
{
len = 0;
}
else if (len >= MAX_LOG_SIZE)
{
len = MAX_LOG_SIZE - 1;
}
return len;
}
int len = vsnprintf(log_buf, MAX_LOG_SIZE, fmt, args);
if (len < 0)
{
len = 0;
}
else if (len >= MAX_LOG_SIZE)
{
len = MAX_LOG_SIZE - 1;
}
// ✅ NÃO bloquear o sistema (sys_evt/httpd/wifi/etc) por causa de log
if (xSemaphoreTake(mutex, 0) != pdTRUE)
{
s_dropped++;
return len;
}
output_buffer_append_buf(buffer, log_buf, (uint16_t)len);
xEventGroupSetBits(logger_event_group, LOGGER_SERIAL_BIT);
xSemaphoreGive(mutex);
return len;
}
/**
* ⚠️ API antiga: devolve ponteiro interno do buffer.
* Só é segura se o caller COPIAR imediatamente e garantir que não há novas escritas.
* Recomendo usar logger_read_copy().
*/
bool logger_read(uint16_t *index, char **str, uint16_t *len)
{
if (!mutex || !buffer || !index || !str || !len)
{
return false;
}
xSemaphoreTake(mutex, portMAX_DELAY);
bool has_next = output_buffer_read(buffer, index, str, len);
xSemaphoreGive(mutex);
return has_next;
}
// ✅ API segura: copia a linha para buffer do caller (evita ponteiro ficar inválido após rotação)
bool logger_read_copy(uint16_t *index, char *out, uint16_t out_sz, uint16_t *out_len)
{
if (!mutex || !buffer || !index || !out || out_sz == 0)
{
return false;
}
xSemaphoreTake(mutex, portMAX_DELAY);
char *ptr = NULL;
uint16_t len = 0;
bool ok = output_buffer_read(buffer, index, &ptr, &len);
if (!ok)
{
xSemaphoreGive(mutex);
return false;
}
uint16_t n = (len < (out_sz - 1)) ? len : (out_sz - 1);
memcpy(out, ptr, n);
out[n] = '\0';
if (out_len)
{
*out_len = n;
}
xSemaphoreGive(mutex);
return true;
}

View File

@@ -1,217 +0,0 @@
#include <stdlib.h>
#include <string.h>
#include "output_buffer.h"
output_buffer_t *output_buffer_create(uint16_t size)
{
if (size == 0)
{
return NULL;
}
output_buffer_t *buffer = (output_buffer_t *)malloc(sizeof(output_buffer_t));
if (!buffer)
{
return NULL;
}
buffer->data = (uint8_t *)malloc((size_t)size);
if (!buffer->data)
{
free(buffer);
return NULL;
}
buffer->size = size;
buffer->count = 0;
buffer->append = buffer->data;
return buffer;
}
void output_buffer_delete(output_buffer_t *buffer)
{
if (!buffer)
{
return;
}
if (buffer->data)
{
free((void *)buffer->data);
buffer->data = NULL;
}
free((void *)buffer);
}
void output_buffer_append_buf(output_buffer_t *buffer, const char *str, uint16_t len)
{
if (!buffer || !buffer->data || !str || len == 0)
{
return;
}
// Garantir que nunca escrevemos entradas absurdamente grandes
if (len > buffer->size / 2)
{
// Tamanho de entrada demasiado grande para a lógica de rotação;
// corta-a para caber de forma segura.
len = buffer->size / 2;
}
size_t used = (size_t)(buffer->append - buffer->data);
if (used > buffer->size)
{
// Estado incoerente: reset defensivo
buffer->append = buffer->data;
buffer->count = 0;
used = 0;
}
// Se não couber mais esta entrada, rodar o buffer
if (used + sizeof(uint16_t) + len > buffer->size)
{
uint8_t *pos = buffer->data;
uint16_t rotate_count = 0;
uint8_t *end = buffer->data + buffer->size;
// Avança entradas até aproximadamente metade do buffer
while ((pos + sizeof(uint16_t)) < end &&
(size_t)(pos - buffer->data) < buffer->size / 2)
{
uint16_t entry_len;
memcpy(&entry_len, pos, sizeof(uint16_t));
// Sanitizar entry_len para evitar corrupções
if (entry_len == 0 || entry_len > buffer->size)
{
// Corrompido → reset defensivo
pos = buffer->data;
rotate_count = 0;
buffer->count = 0;
break;
}
if (pos + sizeof(uint16_t) + entry_len > end)
{
// Entrada incompleta na cauda → para por aqui
break;
}
pos += sizeof(uint16_t) + entry_len;
rotate_count++;
}
// Compacta o que sobrou para o início
size_t remaining = (size_t)(end - pos);
memmove(buffer->data, pos, remaining);
buffer->count = (buffer->count >= rotate_count) ? (buffer->count - rotate_count) : 0;
buffer->append = buffer->data + remaining;
used = (size_t)(buffer->append - buffer->data);
}
// Escreve [len][dados]
memcpy(buffer->append, &len, sizeof(uint16_t));
buffer->append += sizeof(uint16_t);
memcpy(buffer->append, str, len);
buffer->append += len;
buffer->count++;
}
void output_buffer_append_str(output_buffer_t *buffer, const char *str)
{
if (!buffer || !str)
{
return;
}
size_t len = strlen(str);
if (len == 0)
{
return;
}
// A API pública em logger.c já limita o tamanho, mas aqui fazemos
// um clamp defensivo para o caso de uso direto.
if (len > UINT16_MAX)
{
len = UINT16_MAX;
}
output_buffer_append_buf(buffer, str, (uint16_t)len);
}
bool output_buffer_read(output_buffer_t *buffer, uint16_t *index, char **str, uint16_t *len)
{
if (!buffer || !buffer->data || !index || !str || !len)
{
return false;
}
if (*index > buffer->count)
{
*index = buffer->count;
}
if (*index >= buffer->count)
{
return false;
}
uint8_t *pos = buffer->data;
uint8_t *end = buffer->data + buffer->size;
uint16_t current = 0;
// Avança até à entrada [*index]
while (current < *index)
{
if (pos + sizeof(uint16_t) > end)
{
// Dados corrompidos ou índice fora → fail-safe
return false;
}
uint16_t entry_len;
memcpy(&entry_len, pos, sizeof(uint16_t));
if (entry_len == 0 || entry_len > buffer->size)
{
// Corrompido → aborta
return false;
}
if (pos + sizeof(uint16_t) + entry_len > end)
{
return false;
}
pos += sizeof(uint16_t) + entry_len;
current++;
}
// Agora pos aponta para o len da entrada desejada
if (pos + sizeof(uint16_t) > end)
{
return false;
}
memcpy(len, pos, sizeof(uint16_t));
pos += sizeof(uint16_t);
if (pos + *len > end)
{
return false;
}
*str = (char *)pos;
(*index)++;
return true;
}

View File

@@ -3,13 +3,14 @@
set(srcs set(srcs
driver/meter_ade7758/meter_ade7758.c driver/meter_ade7758/meter_ade7758.c
driver/meter_ade7758/ade7758.c driver/meter_ade7758/ade7758.c
driver/meter_orno/meter_orno513.c driver/meter_modbus/meter_orno513.c
driver/meter_orno/meter_orno526.c driver/meter_modbus/meter_orno526.c
driver/meter_orno/meter_orno516.c driver/meter_modbus/meter_orno516.c
driver/meter_orno/meter_dts6619.c driver/meter_modbus/meter_dts6619.c
driver/meter_orno/meter_dds661.c driver/meter_modbus/meter_dds661.c
driver/meter_orno/meter_ea777.c driver/meter_modbus/meter_ea777.c
driver/meter_orno/modbus_params.c driver/meter_modbus/meter_dts024m.c
driver/meter_modbus/modbus_params.c
driver/meter_zigbee/meter_zigbee.c driver/meter_zigbee/meter_zigbee.c
src/meter_manager.c src/meter_manager.c
src/meter_events.c src/meter_events.c
@@ -18,7 +19,7 @@ set(srcs
set(includes set(includes
include include
driver/meter_ade7758 driver/meter_ade7758
driver/meter_orno driver/meter_modbus
driver/meter_zigbee driver/meter_zigbee
) )

View File

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

View File

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

View File

@@ -7,14 +7,14 @@
/** /**
* @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores). * @brief Inicializa o driver do medidor dts6619 (SPI, mutex, registradores).
* *
* @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro.
*/ */
esp_err_t meter_dts6619_init(void); esp_err_t meter_dts6619_init(void);
/** /**
* @brief Inicia a tarefa de leitura de dados do medidor DTS6619. * @brief Inicia a tarefa de leitura de dados do medidor DTS6619.
* *
* @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro.
*/ */
esp_err_t meter_dts6619_start(void); esp_err_t meter_dts6619_start(void);
@@ -24,7 +24,6 @@ esp_err_t meter_dts6619_start(void);
*/ */
void meter_dts6619_stop(void); void meter_dts6619_stop(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -12,13 +12,13 @@
#define TAG "serial_mdb_ea777" #define TAG "serial_mdb_ea777"
// ===== UART / RS-485 ===== // ===== UART / RS-485 =====
#define MB_PORT_NUM 2 #define MB_PORT_NUM 1
#define MB_DEV_SPEED 9600 #define MB_DEV_SPEED 9600
// Ajuste os pinos conforme seu hardware // Ajuste os pinos conforme seu hardware
#define MB_UART_TXD 17 #define MB_UART_TXD 21
#define MB_UART_RXD 16 #define MB_UART_RXD 22
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 #define MB_UART_RTS UART_PIN_NO_CHANGE // sem DE/RE
// ===== Timings ===== // ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS) #define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
@@ -322,9 +322,10 @@ esp_err_t meter_ea777_init(void)
ESP_ERROR_CHECK(mbc_master_setup(&comm)); ESP_ERROR_CHECK(mbc_master_setup(&comm));
ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM,
MB_UART_TXD, MB_UART_RXD, MB_UART_TXD, MB_UART_RXD,
MB_UART_RTS, UART_PIN_NO_CHANGE)); UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(mbc_master_start()); ESP_ERROR_CHECK(mbc_master_start());
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART));
// ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART));
vTaskDelay(pdMS_TO_TICKS(50)); vTaskDelay(pdMS_TO_TICKS(50));
ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777, ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777,

View File

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

View File

@@ -2,42 +2,53 @@
#define METER_EVENTS_H #define METER_EVENTS_H
#include "esp_event.h" #include "esp_event.h"
#include "meter_manager.h" // Para meter_type_t #include "meter_manager.h" // meter_type_t
#include <stdint.h> // Para int64_t #include <stdint.h> // int32_t, int64_t
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C"
{
#endif #endif
// Base de eventos dos medidores // Base de eventos dos medidores
ESP_EVENT_DECLARE_BASE(METER_EVENT); ESP_EVENT_DECLARE_BASE(METER_EVENT);
// IDs de eventos emitidos por medidores // IDs de eventos emitidos por medidores
typedef enum { typedef enum
METER_EVENT_DATA_READY = 0, {
METER_EVENT_ERROR, METER_EVENT_DATA_READY = 0,
METER_EVENT_STARTED, METER_EVENT_ERROR,
METER_EVENT_STOPPED, METER_EVENT_STARTED,
METER_EVENT_CONFIG_UPDATED // Novo: configuração (grid/evse) atualizada METER_EVENT_STOPPED,
} meter_event_id_t; METER_EVENT_CONFIG_UPDATED
} meter_event_id_t;
// Estrutura de dados enviados com METER_EVENT_DATA_READY // Estrutura de dados enviados com METER_EVENT_DATA_READY
typedef struct { // NOTA: campos não suportados pelo meter devem ficar a 0.
const char *source; // "GRID" ou "EVSE" typedef struct
float vrms[3]; // Tensão por fase {
float irms[3]; // Corrente por fase const char *source; // "GRID" ou "EVSE"
int watt[3]; // Potência ativa por fase
float frequency; // Frequência da rede (Hz)
float power_factor; // Fator de potência
float total_energy; // Energia acumulada (kWh)
} meter_event_data_t;
// Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED float vrms[3]; // V por fase (0 se não existir)
typedef struct { float irms[3]; // A por fase (0 se não existir)
meter_type_t grid_type; // Tipo de contador configurado para o GRID
meter_type_t evse_type; // Tipo de contador configurado para a EVSE int32_t watt[3]; // W por fase (0 se não existir)
int64_t timestamp_us; // Momento da atualização (esp_timer_get_time) int32_t watt_total; // W total ASSINADO: +import / -export (0 se não existir)
} meter_config_event_t;
float frequency; // Hz (0 se não existir)
float power_factor; // (0 se não existir)
float total_energy; // kWh (0 se não existir)
int64_t timestamp_us; // esp_timer_get_time() (0 => consumidor pode usar "now")
} meter_event_data_t;
// Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED
typedef struct
{
meter_type_t grid_type;
meter_type_t evse_type;
int64_t timestamp_us; // esp_timer_get_time()
} meter_config_event_t;
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

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

View File

@@ -9,6 +9,7 @@
#include "meter_dds661.h" #include "meter_dds661.h"
#include "meter_zigbee.h" #include "meter_zigbee.h"
#include "meter_ea777.h" #include "meter_ea777.h"
#include "meter_dts024m.h"
#include <string.h> #include <string.h>
@@ -239,6 +240,8 @@ esp_err_t meter_manager_evse_init()
return meter_dds661_init(); return meter_dds661_init();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_init(); return meter_ea777_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_init(); return meter_zigbee_init();
@@ -268,6 +271,8 @@ esp_err_t meter_manager_evse_start()
return meter_dds661_start(); return meter_dds661_start();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_start(); return meter_ea777_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_start(); return meter_zigbee_start();
@@ -304,6 +309,9 @@ esp_err_t meter_manager_evse_stop(void)
case METER_TYPE_EA777: case METER_TYPE_EA777:
meter_ea777_stop(); meter_ea777_stop();
break; break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
break;
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
meter_zigbee_stop(); meter_zigbee_stop();
@@ -342,6 +350,8 @@ esp_err_t meter_manager_grid_init()
return meter_dds661_init(); return meter_dds661_init();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_init(); return meter_ea777_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_init(); return meter_zigbee_init();
@@ -371,6 +381,8 @@ esp_err_t meter_manager_grid_start()
return meter_dds661_start(); return meter_dds661_start();
case METER_TYPE_EA777: case METER_TYPE_EA777:
return meter_ea777_start(); return meter_ea777_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_start(); return meter_zigbee_start();
@@ -407,6 +419,9 @@ esp_err_t meter_manager_grid_stop(void)
case METER_TYPE_EA777: case METER_TYPE_EA777:
meter_ea777_stop(); meter_ea777_stop();
break; break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
break;
case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE:
meter_zigbee_stop(); meter_zigbee_stop();
@@ -509,6 +524,8 @@ const char *meter_type_to_str(meter_type_t type)
return "TRIF-ZIGBEE"; return "TRIF-ZIGBEE";
case METER_TYPE_EA777: case METER_TYPE_EA777:
return "EA-777"; return "EA-777";
case METER_TYPE_DTS024M:
return "DTS-024M";
default: default:
return "NENHUM"; return "NENHUM";
} }
@@ -537,6 +554,8 @@ meter_type_t string_to_meter_type(const char *str)
return METER_TYPE_TRIF_ZIGBEE; return METER_TYPE_TRIF_ZIGBEE;
if (strcmp(str, "EA-777") == 0) if (strcmp(str, "EA-777") == 0)
return METER_TYPE_EA777; return METER_TYPE_EA777;
if (strcmp(str, "DTS-024M") == 0)
return METER_TYPE_DTS024M;
return METER_TYPE_NONE; return METER_TYPE_NONE;
} }

View File

@@ -170,7 +170,7 @@ static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out,
size_t n = strnlen(tmp, out_sz - 1); size_t n = strnlen(tmp, out_sz - 1);
memcpy(out, tmp, n); memcpy(out, tmp, n);
out[n] = '\0'; out[n] = '\0';
return ESP_OK; return ESP_OK;
} }
@@ -769,6 +769,12 @@ void ocpp_start(void)
return; return;
} }
//chargePointModel: "EPower M1"
//chargePointVendor: "Plixin"
//firmwareVersion: "FW-PLXV1.0"
//chargePointSerialNumber: "SN001"
ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false); ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false);
ocpp_setEvReadyInput(&setEvReadyInput); ocpp_setEvReadyInput(&setEvReadyInput);

View File

@@ -12,7 +12,6 @@ idf_component_register(
vfs vfs
spiffs spiffs
REQUIRES REQUIRES
logger
network network
config config
evse evse

View File

@@ -317,7 +317,7 @@ static void mqtt_publish_raw(const char *subtopic, const char *payload, bool ret
1, 1,
retain ? 1 : 0); retain ? 1 : 0);
ESP_LOGD(TAG, "MQTT publish [%s] (id=%d): %s", topic, msg_id, payload); ESP_LOGI(TAG, "MQTT publish [%s] (id=%d): %s", topic, msg_id, payload);
} }
static void mqtt_publish_json(const char *subtopic, cJSON *obj, bool retain) static void mqtt_publish_json(const char *subtopic, cJSON *obj, bool retain)

View File

@@ -1,108 +1,216 @@
#include "loadbalancing_settings_api.h" #include "loadbalancing_settings_api.h"
#include "loadbalancer.h" #include "loadbalancer.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_err.h"
#include "esp_http_server.h"
#include "cJSON.h" #include "cJSON.h"
#include <stdlib.h>
#include <string.h>
static const char *TAG = "loadbalancing_settings_api"; static const char *TAG = "loadbalancing_settings_api";
// GET Handler: Retorna configurações atuais de load balancing // limites simples
static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) { #define MIN_GRID_A 6
bool enabled = loadbalancer_is_enabled(); #define MAX_GRID_A 100
uint8_t currentLimit = load_balancing_get_max_grid_current();
ESP_LOGD(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit); #define MIN_PV_W 0
#define MAX_PV_W 100000 // ajusta se quiseres
static esp_err_t send_json(httpd_req_t *req, cJSON *root)
{
httpd_resp_set_type(req, "application/json"); httpd_resp_set_type(req, "application/json");
cJSON *config = cJSON_CreateObject(); char *json_str = cJSON_PrintUnformatted(root);
cJSON_AddBoolToObject(config, "loadBalancingEnabled", enabled); if (!json_str)
cJSON_AddNumberToObject(config, "loadBalancingCurrentLimit", currentLimit); {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON encode failed");
return ESP_FAIL;
}
const char *json_str = cJSON_Print(config); esp_err_t err = httpd_resp_sendstr(req, json_str);
httpd_resp_sendstr(req, json_str); free(json_str);
return err;
}
ESP_LOGD(TAG, "Returned config: %s", json_str); // GET -> payload novo
static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req)
{
cJSON *root = cJSON_CreateObject();
if (!root)
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem");
return ESP_FAIL;
}
free((void *)json_str); cJSON_AddBoolToObject(root, "enabled", loadbalancer_is_enabled());
cJSON_Delete(config);
cJSON *grid = cJSON_CreateObject();
cJSON_AddItemToObject(root, "gridLimit", grid);
cJSON_AddBoolToObject(grid, "enabled", loadbalancer_grid_is_enabled());
cJSON_AddNumberToObject(grid, "maxImportA", loadbalancer_grid_get_max_import_a());
cJSON *pv = cJSON_CreateObject();
cJSON_AddItemToObject(root, "pv", pv);
cJSON_AddBoolToObject(pv, "enabled", loadbalancer_pv_is_enabled());
cJSON_AddNumberToObject(pv, "maxImportW", loadbalancer_pv_get_max_import_w());
esp_err_t err = send_json(req, root);
cJSON_Delete(root);
return err;
}
// lê body de forma robusta (httpd_req_recv pode devolver parcial)
static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz)
{
if (req->content_len <= 0)
return ESP_FAIL;
if ((size_t)req->content_len >= buf_sz)
return ESP_ERR_NO_MEM;
int remaining = req->content_len;
int off = 0;
while (remaining > 0)
{
int r = httpd_req_recv(req, buf + off, remaining);
if (r <= 0)
return ESP_FAIL;
off += r;
remaining -= r;
}
buf[off] = '\0';
return ESP_OK; return ESP_OK;
} }
// POST Handler: Atualiza configurações de load balancing // POST -> updates parciais aceites
static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req)
char buf[512]; {
int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (req->content_len <= 0)
{
if (len <= 0) {
ESP_LOGE(TAG, "Received empty POST body");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body");
return ESP_FAIL; return ESP_FAIL;
} }
buf[len] = '\0'; if (req->content_len >= 512)
ESP_LOGD(TAG, "Received POST data: %s", buf); {
httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large");
return ESP_FAIL;
}
char buf[512];
esp_err_t rb = read_body(req, buf, sizeof(buf));
if (rb == ESP_ERR_NO_MEM)
{
httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large");
return ESP_FAIL;
}
if (rb != ESP_OK)
{
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read body");
return ESP_FAIL;
}
ESP_LOGD(TAG, "POST: %s", buf);
cJSON *json = cJSON_Parse(buf); cJSON *json = cJSON_Parse(buf);
if (!json) { if (!json)
ESP_LOGE(TAG, "Invalid JSON"); {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL; return ESP_FAIL;
} }
// Atualizar estado habilitado // enabled (top-level)
cJSON *enabled_item = cJSON_GetObjectItem(json, "loadBalancingEnabled"); cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled");
if (enabled_item && cJSON_IsBool(enabled_item)) { if (enabled_item && cJSON_IsBool(enabled_item))
bool isEnabled = cJSON_IsTrue(enabled_item); {
loadbalancer_set_enabled(isEnabled); loadbalancer_set_enabled(cJSON_IsTrue(enabled_item));
ESP_LOGD(TAG, "Updated loadBalancingEnabled to: %d", isEnabled);
} }
// Atualizar limite de corrente // gridLimit
cJSON *limit_item = cJSON_GetObjectItem(json, "loadBalancingCurrentLimit"); cJSON *grid = cJSON_GetObjectItem(json, "gridLimit");
if (limit_item && cJSON_IsNumber(limit_item)) { if (grid && cJSON_IsObject(grid))
uint8_t currentLimit = (uint8_t)limit_item->valuedouble; {
cJSON *g_en = cJSON_GetObjectItem(grid, "enabled");
// Validar intervalo if (g_en && cJSON_IsBool(g_en))
if (currentLimit < 6 || currentLimit > 100) { {
ESP_LOGW(TAG, "Rejected invalid currentLimit: %d", currentLimit); loadbalancer_grid_set_enabled(cJSON_IsTrue(g_en));
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid currentLimit (must be 6-100)");
return ESP_FAIL;
} }
esp_err_t err = load_balancing_set_max_grid_current(currentLimit); cJSON *g_maxA = cJSON_GetObjectItem(grid, "maxImportA");
if (err != ESP_OK) { if (g_maxA && cJSON_IsNumber(g_maxA))
ESP_LOGE(TAG, "Failed to save currentLimit: %s", esp_err_to_name(err)); {
cJSON_Delete(json); int maxA = (int)g_maxA->valuedouble;
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save setting");
return ESP_FAIL; if (maxA < MIN_GRID_A || maxA > MAX_GRID_A)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "gridLimit.maxImportA must be 6-100");
return ESP_FAIL;
}
esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)maxA);
if (e != ESP_OK)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set gridLimit.maxImportA");
return ESP_FAIL;
}
}
}
// pv
cJSON *pv = cJSON_GetObjectItem(json, "pv");
if (pv && cJSON_IsObject(pv))
{
cJSON *p_en = cJSON_GetObjectItem(pv, "enabled");
if (p_en && cJSON_IsBool(p_en))
{
loadbalancer_pv_set_enabled(cJSON_IsTrue(p_en));
} }
ESP_LOGD(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit); cJSON *p_maxW = cJSON_GetObjectItem(pv, "maxImportW");
if (p_maxW && cJSON_IsNumber(p_maxW))
{
int32_t maxW = (int32_t)p_maxW->valuedouble;
if (maxW < MIN_PV_W || maxW > MAX_PV_W)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "pv.maxImportW out of range");
return ESP_FAIL;
}
esp_err_t e = loadbalancer_pv_set_max_import_w(maxW);
if (e != ESP_OK)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set pv.maxImportW");
return ESP_FAIL;
}
}
} }
cJSON_Delete(json); cJSON_Delete(json);
httpd_resp_sendstr(req, "Load balancing settings updated successfully"); httpd_resp_sendstr(req, "OK");
return ESP_OK; return ESP_OK;
} }
// Registro dos handlers na API HTTP void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx)
void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) { {
// GET
httpd_uri_t get_uri = { httpd_uri_t get_uri = {
.uri = "/api/v1/config/loadbalancing", .uri = "/api/v1/config/loadbalancing",
.method = HTTP_GET, .method = HTTP_GET,
.handler = loadbalancing_config_get_handler, .handler = loadbalancing_config_get_handler,
.user_ctx = ctx .user_ctx = ctx};
};
httpd_register_uri_handler(server, &get_uri); httpd_register_uri_handler(server, &get_uri);
// POST
httpd_uri_t post_uri = { httpd_uri_t post_uri = {
.uri = "/api/v1/config/loadbalancing", .uri = "/api/v1/config/loadbalancing",
.method = HTTP_POST, .method = HTTP_POST,
.handler = loadbalancing_config_post_handler, .handler = loadbalancing_config_post_handler,
.user_ctx = ctx .user_ctx = ctx};
};
httpd_register_uri_handler(server, &post_uri); httpd_register_uri_handler(server, &post_uri);
} }

View File

@@ -8,16 +8,20 @@
#include "network.h" #include "network.h"
#include "mqtt.h" #include "mqtt.h"
#include <string.h>
#include <stdlib.h>
static const char *TAG = "network_api"; static const char *TAG = "network_api";
typedef struct { typedef struct
{
bool enabled; bool enabled;
char ssid[33]; char ssid[33];
char password[65]; char password[65];
} wifi_task_data_t; } wifi_task_data_t;
static void wifi_apply_config_task(void *param)
static void wifi_apply_config_task(void *param) { {
wifi_task_data_t *data = (wifi_task_data_t *)param; wifi_task_data_t *data = (wifi_task_data_t *)param;
ESP_LOGD("wifi_task", "Applying Wi-Fi config in background task"); ESP_LOGD("wifi_task", "Applying Wi-Fi config in background task");
wifi_set_config(data->enabled, data->ssid, data->password); wifi_set_config(data->enabled, data->ssid, data->password);
@@ -25,12 +29,12 @@ static void wifi_apply_config_task(void *param) {
vTaskDelete(NULL); vTaskDelete(NULL);
} }
static esp_err_t wifi_get_handler(httpd_req_t *req) { static esp_err_t wifi_get_handler(httpd_req_t *req)
{
ESP_LOGD(TAG, "Handling GET /api/v1/config/wifi"); ESP_LOGD(TAG, "Handling GET /api/v1/config/wifi");
httpd_resp_set_type(req, "application/json"); httpd_resp_set_type(req, "application/json");
// Obter dados da NVS via wifi.c
bool enabled = wifi_get_enabled(); bool enabled = wifi_get_enabled();
char ssid[33] = {0}; char ssid[33] = {0};
char password[65] = {0}; char password[65] = {0};
@@ -38,78 +42,82 @@ static esp_err_t wifi_get_handler(httpd_req_t *req) {
wifi_get_ssid(ssid); wifi_get_ssid(ssid);
wifi_get_password(password); wifi_get_password(password);
// Criar JSON
cJSON *json = cJSON_CreateObject(); cJSON *json = cJSON_CreateObject();
cJSON_AddBoolToObject(json, "enabled", enabled); cJSON_AddBoolToObject(json, "enabled", enabled);
cJSON_AddStringToObject(json, "ssid", ssid); cJSON_AddStringToObject(json, "ssid", ssid);
cJSON_AddStringToObject(json, "password", password); cJSON_AddStringToObject(json, "password", password);
// Enviar resposta
char *response = cJSON_Print(json); char *response = cJSON_Print(json);
httpd_resp_sendstr(req, response); httpd_resp_sendstr(req, response);
// Limpeza
free(response); free(response);
cJSON_Delete(json); cJSON_Delete(json);
return ESP_OK; return ESP_OK;
} }
static esp_err_t wifi_post_handler(httpd_req_t *req) { static esp_err_t wifi_post_handler(httpd_req_t *req)
{
ESP_LOGD(TAG, "Handling POST /api/v1/config/wifi"); ESP_LOGD(TAG, "Handling POST /api/v1/config/wifi");
char buf[512]; char buf[512];
int len = httpd_req_recv(req, buf, sizeof(buf) - 1); int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (len <= 0) return ESP_FAIL; if (len <= 0)
return ESP_FAIL;
buf[len] = '\0'; buf[len] = '\0';
cJSON *json = cJSON_Parse(buf); cJSON *json = cJSON_Parse(buf);
if (!json) return ESP_FAIL; if (!json)
return ESP_FAIL;
// Valores padrão
bool enabled = false; bool enabled = false;
const char *ssid = NULL; const char *ssid = NULL;
const char *password = NULL; const char *password = NULL;
cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled"); cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled");
if (cJSON_IsBool(j_enabled)) enabled = j_enabled->valueint; if (cJSON_IsBool(j_enabled))
enabled = j_enabled->valueint;
cJSON *j_ssid = cJSON_GetObjectItem(json, "ssid"); cJSON *j_ssid = cJSON_GetObjectItem(json, "ssid");
if (cJSON_IsString(j_ssid)) ssid = j_ssid->valuestring; if (cJSON_IsString(j_ssid))
ssid = j_ssid->valuestring;
cJSON *j_password = cJSON_GetObjectItem(json, "password"); cJSON *j_password = cJSON_GetObjectItem(json, "password");
if (cJSON_IsString(j_password)) password = j_password->valuestring; if (cJSON_IsString(j_password))
password = j_password->valuestring;
// Enviar resposta antes de alterar Wi-Fi // Resposta imediata
httpd_resp_sendstr(req, "Wi-Fi config atualizada com sucesso"); httpd_resp_sendstr(req, "Wi-Fi config atualizada com sucesso");
// Alocar struct para passar para a task wifi_task_data_t *task_data = (wifi_task_data_t *)malloc(sizeof(wifi_task_data_t));
wifi_task_data_t *task_data = malloc(sizeof(wifi_task_data_t)); if (!task_data)
if (!task_data) { {
cJSON_Delete(json); cJSON_Delete(json);
ESP_LOGE(TAG, "Memory allocation failed for Wi-Fi task"); ESP_LOGE(TAG, "Memory allocation failed for Wi-Fi task");
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
task_data->enabled = enabled; task_data->enabled = enabled;
strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid));
strncpy(task_data->password, password ? password : "", sizeof(task_data->password));
// Criar task normal com função C // Copias seguras (garante null-termination)
strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid) - 1);
task_data->ssid[sizeof(task_data->ssid) - 1] = '\0';
strncpy(task_data->password, password ? password : "", sizeof(task_data->password) - 1);
task_data->password[sizeof(task_data->password) - 1] = '\0';
xTaskCreate( xTaskCreate(
wifi_apply_config_task, wifi_apply_config_task,
"wifi_config_task", "wifi_config_task",
4096, 4096,
task_data, task_data,
3, 3,
NULL NULL);
);
cJSON_Delete(json); cJSON_Delete(json);
return ESP_OK; return ESP_OK;
} }
static esp_err_t config_mqtt_get_handler(httpd_req_t *req) static esp_err_t config_mqtt_get_handler(httpd_req_t *req)
{ {
ESP_LOGD(TAG, "Handling GET /api/v1/config/mqtt"); ESP_LOGD(TAG, "Handling GET /api/v1/config/mqtt");
@@ -139,28 +147,28 @@ static esp_err_t config_mqtt_get_handler(httpd_req_t *req)
cJSON *config = cJSON_CreateObject(); cJSON *config = cJSON_CreateObject();
cJSON_AddBoolToObject(config, "enabled", enabled); cJSON_AddBoolToObject(config, "enabled", enabled);
cJSON_AddStringToObject(config, "host", server); cJSON_AddStringToObject(config, "host", server);
cJSON_AddNumberToObject(config, "port", 1883); cJSON_AddNumberToObject(config, "port", 1883); // fixo (se não usas no mqtt_set_config)
cJSON_AddStringToObject(config, "username", username); cJSON_AddStringToObject(config, "username", username);
cJSON_AddStringToObject(config, "password", password); cJSON_AddStringToObject(config, "password", password);
cJSON_AddStringToObject(config, "topic", base_topic); cJSON_AddStringToObject(config, "topic", base_topic);
cJSON_AddNumberToObject(config, "periodicity", periodicity); cJSON_AddNumberToObject(config, "periodicity", periodicity);
const char *config_str = cJSON_Print(config); char *config_str = cJSON_Print(config);
httpd_resp_sendstr(req, config_str); httpd_resp_sendstr(req, config_str);
free((void *)config_str); free(config_str);
cJSON_Delete(config); cJSON_Delete(config);
return ESP_OK; return ESP_OK;
} }
static esp_err_t config_mqtt_post_handler(httpd_req_t *req) static esp_err_t config_mqtt_post_handler(httpd_req_t *req)
{ {
ESP_LOGD(TAG, "Handling POST /api/v1/config/mqtt"); ESP_LOGD(TAG, "Handling POST /api/v1/config/mqtt");
char buf[512]; char buf[512];
int len = httpd_req_recv(req, buf, sizeof(buf) - 1); int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (len <= 0) { if (len <= 0)
{
ESP_LOGE(TAG, "Failed to read request body"); ESP_LOGE(TAG, "Failed to read request body");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request body"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request body");
return ESP_FAIL; return ESP_FAIL;
@@ -169,33 +177,75 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req)
ESP_LOGD(TAG, "Received JSON: %s", buf); ESP_LOGD(TAG, "Received JSON: %s", buf);
cJSON *json = cJSON_Parse(buf); cJSON *json = cJSON_Parse(buf);
if (!json) { if (!json)
{
ESP_LOGE(TAG, "Invalid JSON format"); ESP_LOGE(TAG, "Invalid JSON format");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL; return ESP_FAIL;
} }
bool enabled = false; // --- Ler config atual (para permitir "partial update" e evitar strings vazias)
const char *host = NULL, *topic = NULL, *username = NULL, *password = NULL; bool current_enabled = mqtt_get_enabled();
int periodicity = 30; char current_host[64] = {0};
char current_topic[32] = {0};
char current_user[32] = {0};
char current_pass[64] = {0};
uint16_t current_periodicity = mqtt_get_periodicity();
if (cJSON_IsBool(cJSON_GetObjectItem(json, "enabled"))) mqtt_get_server(current_host);
enabled = cJSON_GetObjectItem(json, "enabled")->valueint; mqtt_get_base_topic(current_topic);
mqtt_get_user(current_user);
mqtt_get_password(current_pass);
bool enabled = current_enabled;
const char *host_in = NULL, *topic_in = NULL, *user_in = NULL, *pass_in = NULL;
int periodicity = (int)current_periodicity;
cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled");
if (cJSON_IsBool(j_enabled))
enabled = j_enabled->valueint;
cJSON *j_host = cJSON_GetObjectItem(json, "host"); cJSON *j_host = cJSON_GetObjectItem(json, "host");
if (cJSON_IsString(j_host)) host = j_host->valuestring; if (cJSON_IsString(j_host))
host_in = j_host->valuestring;
cJSON *j_topic = cJSON_GetObjectItem(json, "topic"); cJSON *j_topic = cJSON_GetObjectItem(json, "topic");
if (cJSON_IsString(j_topic)) topic = j_topic->valuestring; if (cJSON_IsString(j_topic))
topic_in = j_topic->valuestring;
cJSON *j_user = cJSON_GetObjectItem(json, "username"); cJSON *j_user = cJSON_GetObjectItem(json, "username");
if (cJSON_IsString(j_user)) username = j_user->valuestring; if (cJSON_IsString(j_user))
user_in = j_user->valuestring;
cJSON *j_pass = cJSON_GetObjectItem(json, "password"); cJSON *j_pass = cJSON_GetObjectItem(json, "password");
if (cJSON_IsString(j_pass)) password = j_pass->valuestring; if (cJSON_IsString(j_pass))
pass_in = j_pass->valuestring;
cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity"); cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity");
if (cJSON_IsNumber(j_periodicity)) periodicity = j_periodicity->valueint; if (cJSON_IsNumber(j_periodicity))
periodicity = j_periodicity->valueint;
// --- Regras: se vier NULL ou "" mantém o atual; se atual também estiver vazio, usa default
const char *host =
(host_in && host_in[0] != '\0') ? host_in : (current_host[0] != '\0') ? current_host
: "mqtt.plixin.com";
const char *topic =
(topic_in && topic_in[0] != '\0') ? topic_in : (current_topic[0] != '\0') ? current_topic
: "";
const char *username =
(user_in && user_in[0] != '\0') ? user_in : (current_user[0] != '\0') ? current_user
: "";
const char *password =
(pass_in && pass_in[0] != '\0') ? pass_in : (current_pass[0] != '\0') ? current_pass
: "";
if (periodicity <= 0)
periodicity = (int)current_periodicity;
if (periodicity <= 0)
periodicity = 30;
ESP_LOGD(TAG, "Applying MQTT config:"); ESP_LOGD(TAG, "Applying MQTT config:");
ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false"); ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false");
@@ -206,7 +256,8 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req)
ESP_LOGD(TAG, " Periodicity: %d", periodicity); ESP_LOGD(TAG, " Periodicity: %d", periodicity);
esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity); esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity);
if (err != ESP_OK) { if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to apply MQTT config (code %d)", err); ESP_LOGE(TAG, "Failed to apply MQTT config (code %d)", err);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to apply config"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to apply config");
cJSON_Delete(json); cJSON_Delete(json);
@@ -218,40 +269,33 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req)
return ESP_OK; return ESP_OK;
} }
void register_network_handlers(httpd_handle_t server, void *ctx)
{
void register_network_handlers(httpd_handle_t server, void *ctx) {
httpd_uri_t wifi_get = { httpd_uri_t wifi_get = {
.uri = "/api/v1/config/wifi", .uri = "/api/v1/config/wifi",
.method = HTTP_GET, .method = HTTP_GET,
.handler = wifi_get_handler, .handler = wifi_get_handler,
.user_ctx = ctx .user_ctx = ctx};
};
httpd_register_uri_handler(server, &wifi_get); httpd_register_uri_handler(server, &wifi_get);
httpd_uri_t wifi_post = { httpd_uri_t wifi_post = {
.uri = "/api/v1/config/wifi", .uri = "/api/v1/config/wifi",
.method = HTTP_POST, .method = HTTP_POST,
.handler = wifi_post_handler, .handler = wifi_post_handler,
.user_ctx = ctx .user_ctx = ctx};
};
httpd_register_uri_handler(server, &wifi_post); httpd_register_uri_handler(server, &wifi_post);
// URI handler for getting MQTT config
httpd_uri_t config_mqtt_get_uri = { httpd_uri_t config_mqtt_get_uri = {
.uri = "/api/v1/config/mqtt", .uri = "/api/v1/config/mqtt",
.method = HTTP_GET, .method = HTTP_GET,
.handler = config_mqtt_get_handler, .handler = config_mqtt_get_handler,
.user_ctx = ctx .user_ctx = ctx};
};
httpd_register_uri_handler(server, &config_mqtt_get_uri); httpd_register_uri_handler(server, &config_mqtt_get_uri);
// URI handler for posting MQTT config
httpd_uri_t config_mqtt_post_uri = { httpd_uri_t config_mqtt_post_uri = {
.uri = "/api/v1/config/mqtt", .uri = "/api/v1/config/mqtt",
.method = HTTP_POST, .method = HTTP_POST,
.handler = config_mqtt_post_handler, .handler = config_mqtt_post_handler,
.user_ctx = ctx .user_ctx = ctx};
};
httpd_register_uri_handler(server, &config_mqtt_post_uri); httpd_register_uri_handler(server, &config_mqtt_post_uri);
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,8 @@
} }
</style> </style>
<title>ChargeFlow</title> <title>ChargeFlow</title>
<script type="module" crossorigin src="/assets/index-CH8H7Z_T.js"></script> <script type="module" crossorigin src="/assets/index-0q0tbwk5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-SX00HfRO.css"> <link rel="stylesheet" crossorigin href="/assets/index-BIZ-rt0x.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -56,7 +56,7 @@ dependencies:
idf: idf:
source: source:
type: idf type: idf
version: 5.4.3 version: 5.5.2
direct_dependencies: direct_dependencies:
- espressif/cjson - espressif/cjson
- espressif/esp-modbus - espressif/esp-modbus

View File

@@ -18,7 +18,6 @@
#include "network.h" #include "network.h"
#include "board_config.h" #include "board_config.h"
#include "logger.h"
#include "rest_main.h" #include "rest_main.h"
#include "peripherals.h" #include "peripherals.h"
@@ -274,21 +273,21 @@ static void init_modules(void)
ESP_ERROR_CHECK(storage_service_init()); ESP_ERROR_CHECK(storage_service_init());
peripherals_init(); peripherals_init();
led_init();
wifi_ini(); // garante wifi_event_group inicializado wifi_ini(); // garante wifi_event_group inicializado
buzzer_init(); buzzer_init();
led_init();
ESP_ERROR_CHECK(rest_server_init("/data")); ESP_ERROR_CHECK(rest_server_init("/data"));
evse_manager_init(); evse_manager_init();
evse_init(); evse_init();
auth_init(); auth_init();
loadbalancer_init();
meter_manager_init(); meter_manager_init();
meter_manager_start(); meter_manager_start();
evse_link_init();
ocpp_start(); ocpp_start();
scheduler_init(); scheduler_init();
protocols_init(); protocols_init();
loadbalancer_init();
evse_link_init();
} }
// //
@@ -296,8 +295,6 @@ static void init_modules(void)
// //
void app_main(void) void app_main(void)
{ {
logger_init();
esp_log_set_vprintf(logger_vprintf);
esp_reset_reason_t reason = esp_reset_reason(); esp_reset_reason_t reason = esp_reset_reason();
ESP_LOGI(TAG, "Reset reason: %d", reason); ESP_LOGI(TAG, "Reset reason: %d", reason);

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import os import os
TAMANHO_MAX = 200000 # Limite por arquivo TAMANHO_MAX = 230000 # Limite por arquivo
def coletar_arquivos(diretorios, extensoes=(".c", ".h")): def coletar_arquivos(diretorios, extensoes=(".c", ".h")):
arquivos = [] arquivos = []
@@ -51,9 +51,9 @@ def unir_em_partes(arquivos, prefixo="projeto_parte", limite=TAMANHO_MAX):
print(f"🔹 Arquivos gerados: {parte}") print(f"🔹 Arquivos gerados: {parte}")
def main(): def main():
diretorio_main = "" #"main" diretorio_main = "main"
componentes_escolhidos = [ componentes_escolhidos = [
"storage_service" "ocpp"
] ]
diretorios_componentes = [os.path.join("components", nome) for nome in componentes_escolhidos] diretorios_componentes = [os.path.join("components", nome) for nome in componentes_escolhidos]