evse_link feature

This commit is contained in:
2025-08-05 16:55:11 +01:00
parent bd587a10c0
commit 0d0dc5b129
35 changed files with 4353 additions and 2257 deletions

View File

@@ -0,0 +1,16 @@
set(srcs
"src/evse_link_master.c"
"src/evse_link_slave.c"
"src/evse_link_events.c"
"src/evse_link_framing.c"
"src/evse_link.c"
)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES driver esp_timer nvs_flash
REQUIRES config evse loadbalancer)

View File

@@ -0,0 +1,45 @@
#ifndef EVSE_LINK_H_
#define EVSE_LINK_H_
#include <stdbool.h>
#include <stdint.h>
// Operation mode: slave or master
typedef enum {
EVSE_LINK_MODE_SLAVE = 0,
EVSE_LINK_MODE_MASTER = 1
} evse_link_mode_t;
// Callback invoked when a complete frame is received:
// src: device address of sender (0255)
// dest: device address of receiver (0255 or 0xFF broadcast)
// payload: pointer to received data buffer (command + data)
// len: length of payload (0255)
typedef void (*evse_link_rx_cb_t)(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len);
// Initializes the EVSE-Link component
void evse_link_init(void);
// Sends a framed payload to `dest` with length `len`.
// The source address is automatically set from configuration.
// Returns true on successful enqueue/transmit.
bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len);
// Feeds a received byte into the framing parser.
void evse_link_recv_byte(uint8_t byte);
// Registers a callback to receive complete frames.
void evse_link_register_rx_cb(evse_link_rx_cb_t cb);
// Runtime configuration getters/setters
void evse_link_set_mode(evse_link_mode_t mode);
evse_link_mode_t evse_link_get_mode(void);
void evse_link_set_self_id(uint8_t id);
uint8_t evse_link_get_self_id(void);
void evse_link_set_enabled(bool enabled);
bool evse_link_is_enabled(void);
#endif // EVSE_LINK_H_

View File

@@ -0,0 +1,16 @@
#ifndef EVSE_LINK_EVENTS_H_
#define EVSE_LINK_EVENTS_H_
#include "esp_event.h"
ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS);
typedef enum {
LINK_EVENT_FRAME_RECEIVED, // qualquer frame válido
LINK_EVENT_SLAVE_ONLINE, // heartbeat recebido primeira vez
LINK_EVENT_SLAVE_OFFLINE, // sem heartbeat no timeout
LINK_EVENT_MASTER_POLL_SENT, // opcional: poll enviado pelo master
LINK_EVENT_CURRENT_LIMIT_APPLIED,
LINK_EVENT_SLAVE_CONFIG_UPDATED // <- NOVO evento
} evse_link_event_t;
#endif // EVSE_LINK_EVENTS_H_

View File

@@ -0,0 +1,42 @@
#ifndef EVSE_LINK_FRAMING_H_
#define EVSE_LINK_FRAMING_H_
#include <stdint.h>
#include <stdbool.h>
#include "driver/uart.h"
// UART configuration
#define UART_PORT UART_NUM_2
#define UART_BAUDRATE 115200
#define UART_RX_BUF_SIZE 256
// GPIO pin assignments for UART
#define TX_PIN 21 // GPIO21 -> RX on other board
#define RX_PIN 22 // GPIO22 -> TX on other board
// Frame delimiters
#define MAGIC_START 0x7E
#define MAGIC_END 0x7F
// Maximum payload (excluding sequence byte)
#define EVSE_LINK_MAX_PAYLOAD 254
// Callback type for when a full frame is received
typedef void (*evse_link_frame_cb_t)(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len);
// Initialize framing module (mutex, UART driver, etc.)
void evse_link_framing_init(void);
// Send a framed payload to `dest` with length `len`
// Includes source address in the header
bool evse_link_framing_send(uint8_t dest, uint8_t src,
const uint8_t *payload, uint8_t len);
// Feed a received byte into the framing parser
void evse_link_framing_recv_byte(uint8_t byte);
// Register a callback for complete frames
void evse_link_framing_register_cb(evse_link_frame_cb_t cb);
#endif // EVSE_LINK_FRAMING_H_

View File

@@ -0,0 +1,142 @@
// components/evse_link/src/evse_link.c
#include "evse_link.h"
#include "evse_link_framing.h"
#include "driver/uart.h"
#include "nvs.h"
#include "esp_log.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
static const char *TAG = "evse_link";
// NVS keys
#define _NVS_NAMESPACE "evse_link"
#define _NVS_MODE_KEY "mode"
#define _NVS_ID_KEY "self_id"
#define _NVS_ENABLED_KEY "enabled"
// UART parameters
#define UART_PORT UART_NUM_2
#define UART_RX_BUF_SIZE 256
// Runtime config
static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER;
static uint8_t _self_id = 0x01;
static bool _enabled = false;
// Registered Rx callback
static evse_link_rx_cb_t _rx_cb = NULL;
// Forward declarations
extern void evse_link_master_init(void);
extern void evse_link_slave_init(void);
static void framing_rx_cb(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) {
ESP_LOGD(TAG, "framing_rx_cb: src=0x%02X dest=0x%02X len=%u", src, dest, len);
if (_rx_cb) {
_rx_cb(src, dest, payload, len);
}
}
// Register protocol-level Rx callback
void evse_link_register_rx_cb(evse_link_rx_cb_t cb) {
_rx_cb = cb;
}
// Load config from NVS
enum { EV_OK = ESP_OK };
static void load_link_config(void) {
nvs_handle_t handle;
if (nvs_open(_NVS_NAMESPACE, NVS_READONLY, &handle) != EV_OK) {
ESP_LOGW(TAG, "NVS open failed, using defaults");
return;
}
uint8_t mode, id, en;
if (nvs_get_u8(handle, _NVS_MODE_KEY, &mode) == EV_OK &&
(mode == EVSE_LINK_MODE_MASTER || mode == EVSE_LINK_MODE_SLAVE)) {
_mode = (evse_link_mode_t)mode;
}
if (nvs_get_u8(handle, _NVS_ID_KEY, &id) == EV_OK) {
_self_id = id;
}
if (nvs_get_u8(handle, _NVS_ENABLED_KEY, &en) == EV_OK) {
_enabled = (en != 0);
}
nvs_close(handle);
}
// Save config to NVS
static void save_link_config(void) {
nvs_handle_t handle;
if (nvs_open(_NVS_NAMESPACE, NVS_READWRITE, &handle) == EV_OK) {
nvs_set_u8(handle, _NVS_MODE_KEY, (uint8_t)_mode);
nvs_set_u8(handle, _NVS_ID_KEY, _self_id);
nvs_set_u8(handle, _NVS_ENABLED_KEY, _enabled ? 1 : 0);
nvs_commit(handle);
nvs_close(handle);
} else {
ESP_LOGE(TAG, "Failed to save NVS");
}
}
// Getters/setters
void evse_link_set_mode(evse_link_mode_t m) { _mode = m; save_link_config(); }
evse_link_mode_t evse_link_get_mode(void) { return _mode; }
void evse_link_set_self_id(uint8_t id) { _self_id = id; save_link_config(); }
uint8_t evse_link_get_self_id(void) { return _self_id; }
void evse_link_set_enabled(bool en) { _enabled = en; save_link_config(); }
bool evse_link_is_enabled(void) { return _enabled; }
// RX task: reads bytes from UART and feeds framing
static void evse_link_rx_task(void *arg) {
uint8_t buf[UART_RX_BUF_SIZE];
while (true) {
int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000));
if (len > 0) {
for (int i = 0; i < len; ++i) {
evse_link_recv_byte(buf[i]);
}
}
}
}
// Initialize EVSE-Link component
void evse_link_init(void) {
load_link_config();
ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d",
_mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S',
_self_id, _enabled);
if (!_enabled) return;
// 1) framing layer init (sets up mutex, UART driver, etc.)
evse_link_framing_init();
evse_link_framing_register_cb(framing_rx_cb);
// 2) start RX task
xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL);
// 3) delegate to master or slave
if (_mode == EVSE_LINK_MODE_MASTER) {
evse_link_master_init();
} else {
evse_link_slave_init();
}
}
// Send a frame (delegates to framing module)
bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len) {
if (!evse_link_is_enabled()) return false;
uint8_t src = evse_link_get_self_id();
return evse_link_framing_send(dest, src, payload, len);
}
// Receive byte (delegates to framing module)
void evse_link_recv_byte(uint8_t byte) {
evse_link_framing_recv_byte(byte);
}

View File

@@ -0,0 +1,4 @@
#include "evse_link_events.h"
// Esta única linha insere o símbolo EVSE_LINK_EVENTS no binário
ESP_EVENT_DEFINE_BASE(EVSE_LINK_EVENTS);

View File

@@ -0,0 +1,168 @@
// components/evse_link_framing/src/evse_link_framing.c
#include "evse_link_framing.h"
#include "driver/uart.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include <string.h>
static const char *TAG = "evse_framing";
static SemaphoreHandle_t tx_mutex;
static uint8_t seq = 0;
static evse_link_frame_cb_t rx_cb = NULL;
// CRC-8 (polynomial 0x07)
static uint8_t crc8(const uint8_t *data, uint8_t len) {
uint8_t crc = 0;
for (uint8_t i = 0; i < len; ++i) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; ++b) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
}
return crc;
}
void evse_link_framing_init(void) {
// Create mutex for TX
tx_mutex = xSemaphoreCreateMutex();
// Install UART driver
uart_driver_install(UART_PORT, UART_RX_BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_PORT, &(uart_config_t){
.baud_rate = UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
});
uart_set_pin(UART_PORT, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
bool evse_link_framing_send(uint8_t dest, uint8_t src,
const uint8_t *payload, uint8_t len) {
if (len > EVSE_LINK_MAX_PAYLOAD) return false;
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) return false;
// Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END
uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7];
int idx = 0;
frame[idx++] = MAGIC_START;
frame[idx++] = dest;
frame[idx++] = src;
frame[idx++] = len + 1; // +1 for SEQ
frame[idx++] = seq;
memcpy(&frame[idx], payload, len);
idx += len;
// CRC covers DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD];
memcpy(crc_input, &frame[1], 3 + 1 + len);
frame[idx++] = crc8(crc_input, 3 + 1 + len);
frame[idx++] = MAGIC_END;
uart_write_bytes(UART_PORT, (const char *)frame, idx);
uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(10));
xSemaphoreGive(tx_mutex);
ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u",
dest, src, len, seq);
seq++; // increment sequence after sending
return true;
}
void evse_link_framing_recv_byte(uint8_t b) {
// State machine for frame parsing
static enum {
ST_WAIT_START,
ST_WAIT_DEST,
ST_WAIT_SRC,
ST_WAIT_LEN,
ST_WAIT_SEQ,
ST_READING,
ST_WAIT_CRC,
ST_WAIT_END
} rx_state = ST_WAIT_START;
static uint8_t rx_dest;
static uint8_t rx_src;
static uint8_t rx_len;
static uint8_t rx_seq;
static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD];
static uint8_t rx_pos;
static uint8_t rx_crc;
switch (rx_state) {
case ST_WAIT_START:
if (b == MAGIC_START) {
rx_state = ST_WAIT_DEST;
}
break;
case ST_WAIT_DEST:
rx_dest = b;
rx_state = ST_WAIT_SRC;
break;
case ST_WAIT_SRC:
rx_src = b;
rx_state = ST_WAIT_LEN;
break;
case ST_WAIT_LEN:
rx_len = b; // includes SEQ + payload
rx_pos = 0;
rx_state = ST_WAIT_SEQ;
break;
case ST_WAIT_SEQ:
rx_seq = b;
rx_state = (rx_len > 1) ? ST_READING : ST_WAIT_CRC;
break;
case ST_READING:
rx_buf[rx_pos++] = b;
if (rx_pos >= (rx_len - 1)) { // all payload bytes read
rx_state = ST_WAIT_CRC;
}
break;
case ST_WAIT_CRC:
rx_crc = b;
rx_state = ST_WAIT_END;
break;
case ST_WAIT_END:
if (b == MAGIC_END) {
// Build data for CRC calculation
uint8_t temp[3 + 1 + EVSE_LINK_MAX_PAYLOAD];
int temp_len = 0;
temp[temp_len++] = rx_dest;
temp[temp_len++] = rx_src;
temp[temp_len++] = rx_len;
temp[temp_len++] = rx_seq;
memcpy(&temp[temp_len], rx_buf, rx_len - 1);
temp_len += rx_len - 1;
uint8_t expected = crc8(temp, temp_len);
if (expected == rx_crc) {
if (rx_cb) {
rx_cb(rx_src, rx_dest, rx_buf, rx_len - 1);
}
ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u",
rx_src, rx_dest, rx_len - 1, rx_seq);
} else {
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X",
expected, rx_crc);
}
}
rx_state = ST_WAIT_START;
break;
}
}
void evse_link_framing_register_cb(evse_link_frame_cb_t cb) {
rx_cb = cb;
}

View File

@@ -0,0 +1,152 @@
// === components/evse_link/src/evse_link_master.c ===
#include "evse_link.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_log.h"
#include "esp_event.h"
#include <stdint.h>
#include <stdbool.h>
#include "loadbalancer_events.h"
static const char *TAG = "evse_link_master";
// Link commands
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
// payload lengths (exclui byte de opcode)
#define LEN_POLL_REQ 1 // [ CMD_POLL ]
#define LEN_POLL_RESP 9 // [ CMD_POLL, float V(4), float I(4) ]
#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1
// polling / heartbeat timers interval
typedef struct {
TimerHandle_t timer;
TickType_t interval;
} timer_def_t;
static timer_def_t poll_timer = { .timer = NULL, .interval = pdMS_TO_TICKS(30000) };
static timer_def_t hb_timer = { .timer = NULL, .interval = pdMS_TO_TICKS(30000) };
// --- Send new limit to slave ---
static void on_new_limit(void* arg, esp_event_base_t base, int32_t id, void* data) {
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT) return;
const loadbalancer_slave_limit_event_t *evt = data;
uint8_t slave_id = evt->slave_id;
uint16_t max_current = evt->max_current;
uint8_t buf[LEN_SET_CURRENT] = {
CMD_SET_CURRENT,
(uint8_t)(max_current & 0xFF),
(uint8_t)(max_current >> 8)
};
evse_link_send(slave_id, buf, sizeof(buf));
ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, max_current);
}
// --- Polling broadcast callback ---
static void poll_timer_cb(TimerHandle_t xTimer) {
ESP_LOGD(TAG, "Broadcasting CMD_POLL to all slaves");;
// Optionally post event LINK_EVENT_MASTER_POLL_SENT
}
// --- Heartbeat timeout callback ---
static void hb_timer_cb(TimerHandle_t xTimer) {
ESP_LOGW(TAG, "Heartbeat timeout: possible slave offline");
// post event LINK_EVENT_SLAVE_OFFLINE ???
}
static void on_frame_master(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) {
if (len < 1) return;
uint8_t cmd = payload[0];
switch (cmd) {
case CMD_HEARTBEAT: {
if (len != 6) { // CMD + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi
ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len);
return;
}
bool charging = payload[1] != 0;
uint16_t hw_max = payload[2] | (payload[3] << 8);
uint16_t runtime = payload[4] | (payload[5] << 8);
ESP_LOGI(TAG, "Heartbeat from 0x%02X: charging=%d hw_max=%uA runtime=%uA",
src, charging, hw_max, runtime);
loadbalancer_slave_status_event_t status = {
.slave_id = src,
.charging = charging,
.hw_max_current = (float)hw_max,
.runtime_current = (float)runtime, // corrente real medida no slave
.timestamp_us = esp_timer_get_time()
};
esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_STATUS,
&status, sizeof(status), portMAX_DELAY);
// Enviar ACK de volta
uint8_t ack[] = { CMD_HEARTBEAT_ACK };
evse_link_send(src, ack, sizeof(ack));
ESP_LOGD(TAG, "Sent HEARTBEAT_ACK to 0x%02X", src);
break;
}
case CMD_POLL:
ESP_LOGD(TAG, "Received POLL_RESP from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST:
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA",
src, payload[1]);
break;
default:
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
}
}
// --- Master initialization ---
void evse_link_master_init(void) {
if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled()) {
return;
}
ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_master);
// register loadbalancer event
ESP_ERROR_CHECK(
esp_event_handler_register(
LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
on_new_limit,
NULL
)
);
// create and start poll timer
poll_timer.timer = xTimerCreate("poll_tmr",
poll_timer.interval,
pdTRUE, NULL,
poll_timer_cb);
xTimerStart(poll_timer.timer, 0);
// create and start heartbeat monitor timer
hb_timer.timer = xTimerCreate("hb_tmr",
hb_timer.interval,
pdFALSE, NULL,
hb_timer_cb);
xTimerStart(hb_timer.timer, 0);
}

View File

@@ -0,0 +1,164 @@
// === components/evse_link/src/evse_link_slave.c ===
#include "evse_link.h"
#include "evse_link_events.h"
#include "loadbalancer_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_log.h"
#include "esp_event.h"
#include "evse_events.h"
#include "evse_state.h"
#include "evse_config.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
static const char *TAG = "evse_link_slave";
// Link commands
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 // not used by slave
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_HEARTBEAT_ACK 0x09
// payload lengths (exclui seq byte)
#define LEN_POLL_REQ 1 // [ CMD_POLL ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1 // [ CMD_HEARTBEAT_ACK ]
#define LEN_HEARTBEAT 6 // CMD_HEARTBEAT + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi
// Timing
#define FALLBACK_TIMEOUT_MS 120000
static TimerHandle_t fallback_timer = NULL;
static bool safe_mode = false;
// --- Helper to send a heartbeat frame ---
static void send_heartbeat_frame(void) {
bool charging = evse_state_is_charging(evse_get_state());
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA",
charging, hw_max, runtime);
uint8_t hb[] = {
CMD_HEARTBEAT,
charging ? 1 : 0,
(uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8),
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)
};
// Broadcast to master (0xFF)
evse_link_send(0xFF, hb, sizeof(hb));
}
// --- EVSE state change handler ---
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) {
if (base!=EVSE_EVENTS || id!=EVSE_EVENT_STATE_CHANGED || data==NULL) return;
const evse_state_event_data_t *evt = data;
if (evt->state==EVSE_STATE_EVENT_IDLE || evt->state==EVSE_STATE_EVENT_CHARGING) {
send_heartbeat_frame();
}
}
static void on_frame_slave(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) {
if (dest != evse_link_get_self_id() && dest != 0xFF) return;
if (len < 1) return;
uint8_t cmd = payload[0];
switch (cmd) {
case CMD_POLL:
ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST:
ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src);
break;
case CMD_SET_CURRENT: {
uint16_t amps = payload[1] | (payload[2] << 8);
evse_set_runtime_charging_current(amps);
ESP_LOGI(TAG, "Applied runtime limit: %uA from master 0x%02X", amps, src);
esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
case CMD_HEARTBEAT_ACK:
ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src);
if (fallback_timer) {
xTimerReset(fallback_timer, 0);
if (safe_mode) {
safe_mode = false;
uint16_t current = evse_get_runtime_charging_current();
evse_set_runtime_charging_current(current);
ESP_LOGI(TAG, "Exiting safe mode, restoring %uA", current);
}
}
break;
default:
ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src);
}
}
// --- Periodic heartbeat task ---
static void slave_heartbeat_task(void *arg) {
const TickType_t interval = pdMS_TO_TICKS(10000);
for (;;) {
send_heartbeat_frame();
vTaskDelay(interval);
}
}
// --- Fallback safe mode callback ---
static void fallback_timer_cb(TimerHandle_t xTimer) {
if (!safe_mode) {
safe_mode = true;
ESP_LOGW(TAG, "Fallback timeout: entering safe mode");
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_SLAVE_OFFLINE,
NULL, 0, portMAX_DELAY);
}
}
// --- Slave initialization ---
void evse_link_slave_init(void) {
if (evse_link_get_mode()!=EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) return;
ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_slave);
// start periodic heartbeat
xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 5, NULL);
// fallback timer
fallback_timer = xTimerCreate("fallback_tmr",
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS),
pdFALSE, NULL,
fallback_timer_cb);
if (fallback_timer) {
xTimerStart(fallback_timer, 0);
}
// react to EVSE state changes
ESP_ERROR_CHECK(
esp_event_handler_register(
EVSE_EVENTS,
EVSE_EVENT_STATE_CHANGED,
evse_event_handler,
NULL
)
);
}