chore: snapshot before shared RS485 bus integration

This commit is contained in:
2026-07-22 15:36:04 +01:00
parent ef02e5c5f5
commit ea5cbcab2a
167 changed files with 5749 additions and 1128 deletions

0
.gitignore vendored Executable file → Normal file
View File

0
CMakeLists.txt Executable file → Normal file
View File

0
LICENSE.md Executable file → Normal file
View File

0
README.md Executable file → Normal file
View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -22,6 +22,8 @@
#include "loadbalancer_events.h"
#include "ocpp_events.h"
#include "scheduler_events.h"
#include "evse_link_events.h"
static const char *TAG = "EVSE_Manager";
@@ -61,6 +63,72 @@ bool evse_sched_is_allowed(void)
return v;
}
static void on_evse_link_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_LINK_EVENTS || id != LINK_EVENT_CURRENT_LIMIT_APPLIED || data == NULL)
return;
uint16_t current = *(const uint16_t *)data;
ESP_LOGI(TAG, "[EVSE-LINK] current limit applied: %uA", current);
if (current == 0)
{
bool prev_auth = evse_state_get_authorized();
portENTER_CRITICAL(&s_mgr_mux);
lb_paused = true;
lb_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_mgr_mux);
if (prev_auth)
{
ESP_LOGI(TAG, "[EVSE-LINK] limit=0A → paused by master");
evse_state_set_authorized(false);
}
return;
}
evse_set_runtime_charging_current(current);
bool was_paused;
bool prev_auth;
bool local_auth_enabled;
portENTER_CRITICAL(&s_mgr_mux);
was_paused = lb_paused;
prev_auth = lb_prev_authorized;
local_auth_enabled = auth_enabled;
lb_paused = false;
lb_prev_authorized = false;
portEXIT_CRITICAL(&s_mgr_mux);
if (was_paused)
{
bool can_resume =
(evse_get_error() == 0) &&
evse_config_is_available() &&
evse_config_is_enabled() &&
evse_sched_is_allowed() &&
!evse_is_limit_reached();
if (!can_resume)
{
ESP_LOGW(TAG, "[EVSE-LINK] limit=%uA → cannot resume", current);
return;
}
if (!local_auth_enabled || prev_auth)
{
ESP_LOGI(TAG, "[EVSE-LINK] limit=%uA → resuming after master pause", current);
evse_state_set_authorized(true);
}
}
}
static void evse_manager_handle_auth_on_tick(void)
{
bool sched_allowed = evse_sched_is_allowed();
@@ -423,6 +491,10 @@ void evse_manager_init(void)
ESP_ERROR_CHECK(esp_event_handler_register(LOADBALANCER_EVENTS, ESP_EVENT_ANY_ID, &on_loadbalancer_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(OCPP_EVENTS, ESP_EVENT_ANY_ID, &on_ocpp_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(SCHED_EVENTS, ESP_EVENT_ANY_ID, &on_sched_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(EVSE_LINK_EVENTS,
ESP_EVENT_ANY_ID,
&on_evse_link_event,
NULL));
ESP_LOGI(TAG, "EVSE Manager inicializado.");

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

@@ -10,6 +10,7 @@
#include "evse_pilot.h"
#include "adc121s021_dma.h"
#include "adc.h"
#include "board_config.h"
#define PILOT_PWM_TIMER LEDC_TIMER_0
@@ -27,7 +28,8 @@
// Percentagem para descartar extremos superior/inferior (ruído)
#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior
// ADC referência
// Referência usada pelo ADC121S021 externo.
// No ADC interno, o valor já é convertido para mV por adc_cali_raw_to_voltage().
#define ADC121_VREF_MV 3300
#define ADC121_MAX 4095
@@ -41,24 +43,100 @@ typedef enum {
static pilot_mode_t s_mode = PILOT_MODE_DC_LOW;
static uint32_t last_pwm_duty = 0;
static bool s_internal_adc_configured = false;
// ---------------------
// Helpers internos
// ---------------------
static int adc_raw_to_mv(uint16_t raw)
static int adc121_raw_to_mv(uint16_t raw)
{
return (int)((raw * ADC121_VREF_MV) / ADC121_MAX);
}
static int compare_uint16(const void *a, const void *b)
static int compare_int(const void *a, const void *b)
{
uint16_t va = *(const uint16_t *)a;
uint16_t vb = *(const uint16_t *)b;
int va = *(const int *)a;
int vb = *(const int *)b;
if (va < vb) return -1;
if (va > vb) return 1;
return 0;
}
static bool pilot_adc_read_mv(int *mv)
{
if (!mv)
{
return false;
}
if (board_config.pilot_adc_source == BOARD_CONFIG_PILOT_ADC_INTERNAL_ESP32)
{
if (!adc_handle || !adc_cali_handle)
{
ESP_LOGE(TAG, "ADC interno não inicializado/calibrado");
return false;
}
int raw = 0;
esp_err_t err;
adc_lock();
err = adc_oneshot_read(adc_handle, board_config.pilot_adc_channel, &raw);
if (err == ESP_OK)
{
err = adc_cali_raw_to_voltage(adc_cali_handle, raw, mv);
}
adc_unlock();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Erro ao ler/converter ADC interno do pilot: %s", esp_err_to_name(err));
return false;
}
return true;
}
uint16_t raw = 0;
if (!adc121s021_dma_get_sample(&raw))
{
return false;
}
*mv = adc121_raw_to_mv(raw);
return true;
}
static void pilot_adc_init(void)
{
if (board_config.pilot_adc_source == BOARD_CONFIG_PILOT_ADC_INTERNAL_ESP32)
{
if (!adc_handle)
{
adc_init();
}
if (!s_internal_adc_configured)
{
adc_oneshot_chan_cfg_t config = {
.bitwidth = ADC_BITWIDTH_DEFAULT,
.atten = ADC_ATTEN_DB_12
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle,
board_config.pilot_adc_channel,
&config));
s_internal_adc_configured = true;
}
ESP_LOGI(TAG, "Pilot ADC: interno ESP32 ADC1_CH%d",
(int)board_config.pilot_adc_channel);
return;
}
ESP_LOGI(TAG, "Pilot ADC: ADC121S021 externo via SPI");
adc121s021_dma_init();
}
// ---------------------
// Inicialização PWM + ADC
// ---------------------
@@ -91,8 +169,8 @@ void pilot_init(void)
s_mode = PILOT_MODE_DC_LOW;
last_pwm_duty = 0;
// Inicializa driver do ADC121S021
adc121s021_dma_init();
// Inicializa o backend de leitura do pilot configurado na board.
pilot_adc_init();
}
// ---------------------
@@ -165,17 +243,17 @@ void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12)
{
ESP_LOGD(TAG, "pilot_measure");
uint16_t samples[NUM_PILOT_SAMPLES];
int samples_mv[NUM_PILOT_SAMPLES];
int collected = 0;
int attempts = 0;
while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS)
{
uint16_t adc_sample;
int sample_mv = 0;
if (adc121s021_dma_get_sample(&adc_sample))
if (pilot_adc_read_mv(&sample_mv))
{
samples[collected++] = adc_sample;
samples_mv[collected++] = sample_mv;
esp_rom_delay_us(PILOT_SAMPLE_DELAY_US);
}
else
@@ -194,7 +272,7 @@ void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12)
}
// Ordena as amostras para eliminar extremos (ruído/espúrios)
qsort(samples, collected, sizeof(uint16_t), compare_uint16);
qsort(samples_mv, collected, sizeof(int), compare_int);
int k = (collected * PILOT_EXTREME_PERCENT) / 100;
if (k < 2) k = 2; // garante margem mínima
@@ -207,11 +285,8 @@ void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12)
if (high_index >= collected) high_index = collected - 1;
if (high_index <= low_index) high_index = low_index;
uint16_t low_raw = samples[low_index];
uint16_t high_raw = samples[high_index];
int high_mv = adc_raw_to_mv(high_raw);
int low_mv = adc_raw_to_mv(low_raw);
int low_mv = samples_mv[low_index];
int high_mv = samples_mv[high_index];
// Determina o nível positivo (+12, +9, +6, +3 ou <3 V)
if (high_mv >= board_config.pilot_down_threshold_12)

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -5,21 +5,36 @@
#include <stdbool.h>
#include "driver/uart.h"
// UART instance and configuration
#define UART_PORT UART_NUM_2
// UART baud/buffer configuration
#define UART_BAUDRATE 9600
#define UART_RX_BUF_SIZE 256
// GPIO pin assignments for RS-485 UART
// Ajuste conforme seu hardware
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// Select EVSE-Link physical layer at compile time.
// 0 = production RS485: UART2 GPIO17/16 + RTS/DE GPIO2
// 1 = emergency/internal UART TTL: UART1 GPIO21/22, no RS485/RTS
#ifndef EVSE_LINK_USE_UART_TTL
#define EVSE_LINK_USE_UART_TTL 0
#endif
#define TX_PIN MB_UART_TXD
#define RX_PIN MB_UART_RXD
#define RTS_PIN MB_UART_RTS
#if EVSE_LINK_USE_UART_TTL
#define UART_PORT UART_NUM_1
#define TX_PIN 21
#define RX_PIN 22
#define RTS_PIN UART_PIN_NO_CHANGE
#define EVSE_LINK_UART_MODE UART_MODE_UART
#define EVSE_LINK_PHY_NAME "UART_TTL"
#else
#define UART_PORT UART_NUM_2
#define TX_PIN 17
#define RX_PIN 16
#define RTS_PIN 2
#define EVSE_LINK_UART_MODE UART_MODE_RS485_HALF_DUPLEX
#define EVSE_LINK_PHY_NAME "RS485"
#endif
// Frame delimiters
#define MAGIC_START 0x7E
#define MAGIC_END 0x7F

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

View File

@@ -110,10 +110,12 @@ static esp_err_t configure_uart(void)
}
// RS-485 HALF DUPLEX (driver controla RTS automaticamente)
err = uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX);
err = uart_set_mode(UART_PORT, EVSE_LINK_UART_MODE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_mode(RS485_HALF_DUPLEX) failed: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "uart_set_mode(%s) failed: %s",
EVSE_LINK_PHY_NAME,
esp_err_to_name(err));
return err;
}
@@ -122,15 +124,22 @@ static esp_err_t configure_uart(void)
(void)uart_set_rx_timeout(UART_PORT, EVSE_LINK_RX_TIMEOUT);
// Opcional: inverter RTS se hardware exigir
if (EVSE_LINK_RTS_INVERT)
if (!EVSE_LINK_USE_UART_TTL && EVSE_LINK_RTS_INVERT)
{
(void)uart_set_line_inverse(UART_PORT, UART_SIGNAL_RTS_INV);
ESP_LOGW(TAG, "RS485 driver: RTS inverted");
}
ESP_LOGW(TAG, "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);
ESP_LOGW(TAG,
"%s driver enabled: UART%d TX=%d RX=%d RTS=%d baud=%d rx_to=%d rx_thresh=%d",
EVSE_LINK_PHY_NAME,
UART_PORT,
TX_PIN,
RX_PIN,
RTS_PIN,
UART_BAUDRATE,
EVSE_LINK_RX_TIMEOUT,
EVSE_LINK_RX_FULL_THRESH);
return ESP_OK;
}

View File

@@ -6,7 +6,8 @@
// 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.
// 6) SET_CURRENT é delegado ao EVSE_Manager via LINK_EVENT_CURRENT_LIMIT_APPLIED.
// O evse_link_slave não mexe diretamente em autorização/runtime current.
//
// Nota: A enum do evse_state_event_data_t que enviaste está correta para o handler.
@@ -77,10 +78,6 @@ static QueueHandle_t hb_req_q = NULL;
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;
@@ -97,13 +94,26 @@ static size_t bounded_strlen_u8(const uint8_t *s, size_t max_len)
static void send_heartbeat_frame_now(void)
{
bool charging = evse_state_is_charging(evse_get_state());
evse_state_t st = evse_get_state();
/*
* Para o master/loadbalancer, este campo significa "há pedido/demanda
* ativa deste conector", não apenas "relé ligado".
*
* C2/D2 => charging real
* C1/D1 => EV está a pedir carga, mas o relé está OFF por pausa/limite
*
* Isto permite retomar depois de SET_CURRENT=0: em C1 o slave continua
* elegível para o loadbalancer e o master pode voltar a enviar >0A.
*/
bool demand = evse_state_is_charging(st) || evse_state_is_requesting(st);
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
uint8_t hb[] = {
CMD_HEARTBEAT,
charging ? 1 : 0,
demand ? 1 : 0,
(uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8),
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)};
@@ -152,18 +162,6 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
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();
@@ -190,41 +188,6 @@ static void maybe_exit_safe_mode_on_explicit_power_cmd(uint8_t cmd)
}
}
static void apply_pause_by_master(void)
{
bool prev_auth = evse_state_get_authorized();
portENTER_CRITICAL(&s_state_mux);
paused_by_master = true;
paused_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_state_mux);
// Revoga autorização para parar contactor/pilot via core
if (prev_auth)
evse_state_set_authorized(false);
// Mantém runtime num valor seguro (não 0, pois clamp -> 6A).
// O "pause" efetivo é pela autorização=false.
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
}
static void clear_pause_by_master_if_any(void)
{
bool was_paused;
bool prev_auth;
portENTER_CRITICAL(&s_state_mux);
was_paused = paused_by_master;
prev_auth = paused_prev_authorized;
paused_by_master = false;
paused_prev_authorized = false;
portEXIT_CRITICAL(&s_state_mux);
// Se estava autorizado antes do pause, tenta reautorizar
if (was_paused && prev_auth)
evse_state_set_authorized(true);
}
static void on_frame_slave(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len)
{
@@ -290,27 +253,22 @@ static void on_frame_slave(uint8_t src, uint8_t dest,
if (amps == 0)
{
// PAUSE explícito
ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE (src=0x%02X)", src);
apply_pause_by_master();
ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE requested 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);
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);
// confirma sem bloquear RX
request_heartbeat_send();
ESP_LOGI(TAG, "Applied runtime limit: %uA from 0x%02X", (unsigned)amps, src);
ESP_LOGI(TAG, "Forwarding runtime limit: %uA from 0x%02X", (unsigned)amps, src);
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
@@ -397,8 +355,14 @@ static void fallback_timer_cb(TimerHandle_t xTimer)
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();
// Pausar é mais seguro quando o master falha.
// O EVSE_Manager é o dono da autorização/pausa/retoma.
uint16_t zero_amps = 0;
(void)esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_CURRENT_LIMIT_APPLIED,
&zero_amps,
sizeof(zero_amps),
portMAX_DELAY);
#else
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA, forcing %uA). Policy=MIN",
(unsigned)saved_runtime_limit, (unsigned)MIN_CHARGING_CURRENT_LIMIT);

View File

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

View File

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

View File

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

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

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

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

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

@@ -234,7 +234,7 @@ static void led_apply_state_mode(evse_state_event_t state)
{
case EVSE_STATE_EVENT_IDLE:
// IDLE → verde fixo (claro e visível)
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_ON);
led_apply_pattern(LED_ID_GREEN, LED_PATTERN_ON);
break;
case EVSE_STATE_EVENT_WAITING:
@@ -249,7 +249,7 @@ static void led_apply_state_mode(evse_state_event_t state)
case EVSE_STATE_EVENT_FAULT:
// FAULT → vermelho a piscar rápido
led_apply_pattern(LED_ID_BLUE, LED_PATTERN_BLINK_FAST);
led_apply_pattern(LED_ID_RED, LED_PATTERN_BLINK_FAST);
break;
default:

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

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

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

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

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

@@ -9,11 +9,32 @@ extern "C" {
#include <stdint.h>
#include "esp_err.h"
/**
* @brief Operational current limiting mode.
*
* This is owned by the loadbalancer/policy layer. MQTT/REST should only
* request a mode change; they should not persist or re-apply their own mode.
*/
typedef enum
{
LOADBALANCER_LIMIT_MODE_OFF = 0,
LOADBALANCER_LIMIT_MODE_MANUAL,
LOADBALANCER_LIMIT_MODE_GRID,
LOADBALANCER_LIMIT_MODE_SOLAR,
LOADBALANCER_LIMIT_MODE_GRID_SOLAR
} loadbalancer_limit_mode_t;
void loadbalancer_init(void);
void loadbalancer_set_enabled(bool enabled);
bool loadbalancer_is_enabled(void);
// Operational limit mode
esp_err_t loadbalancer_set_limit_mode(loadbalancer_limit_mode_t mode);
loadbalancer_limit_mode_t loadbalancer_get_limit_mode(void);
const char *loadbalancer_limit_mode_to_str(loadbalancer_limit_mode_t mode);
bool loadbalancer_limit_mode_from_str(const char *str, loadbalancer_limit_mode_t *out);
// GRID limit (A)
void loadbalancer_grid_set_enabled(bool en);
bool loadbalancer_grid_is_enabled(void);

View File

@@ -2,9 +2,9 @@
#pragma once
#include "esp_event.h"
#include <stdint.h>
#include <stdbool.h>
#include "esp_timer.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
@@ -12,47 +12,57 @@ extern "C" {
ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS);
typedef enum {
typedef enum
{
LOADBALANCER_EVENT_INIT = 0,
LOADBALANCER_EVENT_STATE_CHANGED,
LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT,
// IMPORTANT: eventos separados e payloads diferentes
/*
* Current limit events.
*
* MASTER and SLAVE use different payloads intentionally.
*/
LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
/*
* Status received from slave connectors.
*/
LOADBALANCER_EVENT_SLAVE_STATUS
} loadbalancer_event_id_t;
typedef struct {
bool enabled;
int64_t timestamp_us;
typedef struct
{
bool enabled;
int64_t timestamp_us;
} loadbalancer_state_event_t;
typedef struct {
float limit;
int64_t timestamp_us;
} loadbalancer_global_limit_event_t;
// MASTER: NÃO tem slave_id
typedef struct {
/*
* MASTER: no slave_id.
*/
typedef struct
{
uint16_t max_current;
int64_t timestamp_us;
int64_t timestamp_us;
} loadbalancer_master_limit_event_t;
// SLAVE: tem slave_id
typedef struct {
uint8_t slave_id;
/*
* SLAVE: includes slave_id.
*/
typedef struct
{
uint8_t slave_id;
uint16_t max_current;
int64_t timestamp_us;
int64_t timestamp_us;
} loadbalancer_slave_limit_event_t;
typedef struct {
uint8_t slave_id;
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
typedef struct
{
uint8_t slave_id;
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} loadbalancer_slave_status_event_t;
#ifdef __cplusplus

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

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

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

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

@@ -18,6 +18,7 @@
#include "input_filter.h"
#include "meter_events.h"
#include "evse_events.h"
#include "evse_config.h"
#include "storage_service.h"
#include "evse_link_events.h"
@@ -42,7 +43,7 @@ static const char *TAG = "loadbalancer";
#define LB_SUSPEND_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT - 1.0f)
#define LB_RESUME_THRESHOLD (MIN_CHARGING_CURRENT_LIMIT + 1.0f)
#define GRID_METER_TIMEOUT_US (10LL * 1000000LL)
#define GRID_METER_TIMEOUT_US (30LL * 1000000LL)
static bool loadbalancer_enabled = false;
@@ -53,6 +54,7 @@ static uint8_t max_grid_current = MAX_GRID_CURRENT_LIMIT; // mantém nome (já u
// PV
static bool pv_enabled = false;
static int32_t pv_max_import_w = 0; // 0=só PV
static loadbalancer_limit_mode_t limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
// métricas do meter
static float grid_current = 0.0f; // fallback magnitude (max irms filtrado)
@@ -100,6 +102,7 @@ static const int64_t METRICS_TIMEOUT_US = 60LL * 1000000LL;
#define LB_KEY_GRID_MAXA "grid_max_a"
#define LB_KEY_PV_ON "pv_on"
#define LB_KEY_PV_MAXW "pv_max_w" // precisa storage u32/i32
#define LB_KEY_LIMIT_MODE "limit_mode"
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
@@ -286,9 +289,25 @@ static void loadbalancer_meter_event_handler(void *handler_arg, esp_event_base_t
have_grid_evt = true;
ESP_LOGI(TAG, "GRID: I=%.3fA P=%ldW (maxA=%u pv_on=%d pvMaxW=%ld)",
grid_current, (long)grid_watt_total, (unsigned)max_grid_current,
pv_enabled, (long)pv_max_import_w);
if (loadbalancer_enabled)
{
ESP_LOGI(TAG,
"GRID: V=[%.1f %.1f %.1f]V I=[%.3f %.3f %.3f]A maxI=%.3fA P=%ldW (maxA=%u pv_on=%d pvMaxW=%ld)",
evt->vrms[0], evt->vrms[1], evt->vrms[2],
evt->irms[0], evt->irms[1], evt->irms[2],
grid_current,
(long)grid_watt_total,
(unsigned)max_grid_current,
pv_enabled,
(long)pv_max_import_w);
}
else
{
ESP_LOGD(TAG,
"GRID ignored because loadbalancer disabled: maxI=%.3fA P=%ldW",
grid_current,
(long)grid_watt_total);
}
}
else
{
@@ -354,6 +373,179 @@ static void loadbalancer_evse_event_handler(void *handler_arg, esp_event_base_t
}
// --------- Config load/save ---------
const char *loadbalancer_limit_mode_to_str(loadbalancer_limit_mode_t mode)
{
switch (mode)
{
case LOADBALANCER_LIMIT_MODE_OFF:
return "off";
case LOADBALANCER_LIMIT_MODE_MANUAL:
return "manual";
case LOADBALANCER_LIMIT_MODE_GRID:
return "grid";
case LOADBALANCER_LIMIT_MODE_SOLAR:
return "solar";
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
return "grid_solar";
default:
return "off";
}
}
bool loadbalancer_limit_mode_from_str(const char *str, loadbalancer_limit_mode_t *out)
{
if (!str || !out)
return false;
if (strcmp(str, "off") == 0)
{
*out = LOADBALANCER_LIMIT_MODE_OFF;
return true;
}
if (strcmp(str, "manual") == 0)
{
*out = LOADBALANCER_LIMIT_MODE_MANUAL;
return true;
}
if (strcmp(str, "grid") == 0 || strcmp(str, "loadbalancer") == 0)
{
*out = LOADBALANCER_LIMIT_MODE_GRID;
return true;
}
if (strcmp(str, "solar") == 0 || strcmp(str, "pv") == 0)
{
*out = LOADBALANCER_LIMIT_MODE_SOLAR;
return true;
}
if (strcmp(str, "grid_solar") == 0 ||
strcmp(str, "solar_grid") == 0 ||
strcmp(str, "grid_pv") == 0 ||
strcmp(str, "pv_grid") == 0)
{
*out = LOADBALANCER_LIMIT_MODE_GRID_SOLAR;
return true;
}
return false;
}
static esp_err_t persist_limit_mode(loadbalancer_limit_mode_t mode)
{
esp_err_t err = storage_set_u8_sync(LB_NS, LB_KEY_LIMIT_MODE, (uint8_t)mode, TO_TICKS_MS(1000));
if (err == ESP_OK)
err = storage_flush_sync(TO_TICKS_MS(2000));
if (err != ESP_OK)
ESP_LOGW(TAG, "Persist %s=%u failed: %s", LB_KEY_LIMIT_MODE, (unsigned)mode, esp_err_to_name(err));
return err;
}
static void loadbalancer_apply_limit_mode_no_persist(loadbalancer_limit_mode_t mode)
{
switch (mode)
{
case LOADBALANCER_LIMIT_MODE_OFF:
loadbalancer_enabled = false;
grid_limit_enabled = false;
pv_enabled = false;
pv_optimizer_set_enabled(false);
grid_limiter_set_enabled(false);
break;
case LOADBALANCER_LIMIT_MODE_MANUAL:
loadbalancer_enabled = false;
pv_optimizer_set_enabled(false);
grid_limiter_set_enabled(false);
break;
case LOADBALANCER_LIMIT_MODE_GRID:
loadbalancer_enabled = true;
grid_limit_enabled = true;
pv_enabled = false;
grid_limiter_set_enabled(true);
pv_optimizer_set_enabled(false);
break;
case LOADBALANCER_LIMIT_MODE_SOLAR:
loadbalancer_enabled = true;
grid_limit_enabled = false;
pv_enabled = true;
grid_limiter_set_enabled(false);
pv_optimizer_set_enabled(true);
break;
case LOADBALANCER_LIMIT_MODE_GRID_SOLAR:
loadbalancer_enabled = true;
grid_limit_enabled = true;
pv_enabled = true;
grid_limiter_set_enabled(true);
pv_optimizer_set_enabled(true);
break;
default:
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
loadbalancer_enabled = false;
pv_optimizer_set_enabled(false);
grid_limiter_set_enabled(false);
return;
}
limit_mode = mode;
}
esp_err_t loadbalancer_set_limit_mode(loadbalancer_limit_mode_t mode)
{
if (mode > LOADBALANCER_LIMIT_MODE_GRID_SOLAR)
return ESP_ERR_INVALID_ARG;
loadbalancer_apply_limit_mode_no_persist(mode);
(void)storage_set_u8_sync(LB_NS, LB_KEY_ENABLED,
loadbalancer_enabled ? 1 : 0,
TO_TICKS_MS(1000));
(void)storage_set_u8_sync(LB_NS, LB_KEY_GRID_ON,
grid_limit_enabled ? 1 : 0,
TO_TICKS_MS(1000));
(void)storage_set_u8_sync(LB_NS, LB_KEY_PV_ON,
pv_enabled ? 1 : 0,
TO_TICKS_MS(1000));
esp_err_t err = persist_limit_mode(mode);
loadbalancer_state_event_t evt = {
.enabled = loadbalancer_enabled,
.timestamp_us = esp_timer_get_time()};
(void)esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_STATE_CHANGED,
&evt,
sizeof(evt),
portMAX_DELAY);
return err;
}
loadbalancer_limit_mode_t loadbalancer_get_limit_mode(void)
{
return limit_mode;
}
static esp_err_t loadbalancer_load_config(void)
{
esp_err_t se = storage_service_init();
@@ -364,72 +556,318 @@ static esp_err_t loadbalancer_load_config(void)
bool needs_flush = false;
uint8_t temp_u8 = 0;
// enabled
/*
* Defaults seguros em RAM.
*
* Estes valores só são gravados na storage quando a chave não existe
* ou quando existe mas tem valor inválido.
*
* Em erro temporário de leitura, usa-se o default em RAM, mas NÃO se
* sobrescreve a storage.
*/
loadbalancer_enabled = false;
grid_limit_enabled = true;
max_grid_current = MAX_GRID_CURRENT_LIMIT;
pv_enabled = false;
pv_max_import_w = 0;
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
/*
* enabled - legacy.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_ENABLED, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= 1)
{
loadbalancer_enabled = (temp_u8 != 0);
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
loadbalancer_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=0", LB_KEY_ENABLED);
}
else if (err == ESP_OK)
{
loadbalancer_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_ENABLED, 0);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=0",
LB_KEY_ENABLED,
(unsigned)temp_u8);
}
else
{
loadbalancer_enabled = false;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=0, storage not overwritten.",
LB_NS,
LB_KEY_ENABLED,
esp_err_to_name(err));
}
// grid on
/*
* grid_on - legacy/config auxiliar.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_ON, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= 1)
{
grid_limit_enabled = (temp_u8 != 0);
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
grid_limit_enabled = true;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_ON, 1);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=1", LB_KEY_GRID_ON);
}
else if (err == ESP_OK)
{
grid_limit_enabled = true;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_ON, 1);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=1",
LB_KEY_GRID_ON,
(unsigned)temp_u8);
}
else
{
grid_limit_enabled = true;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=1, storage not overwritten.",
LB_NS,
LB_KEY_GRID_ON,
esp_err_to_name(err));
}
// grid maxA
/*
* grid_max_a.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_GRID_MAXA, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 >= MIN_GRID_CURRENT_LIMIT && temp_u8 <= MAX_GRID_CURRENT_LIMIT)
if (err == ESP_OK &&
temp_u8 >= MIN_GRID_CURRENT_LIMIT &&
temp_u8 <= MAX_GRID_CURRENT_LIMIT)
{
max_grid_current = temp_u8;
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
max_grid_current = MAX_GRID_CURRENT_LIMIT;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_MAXA, max_grid_current);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s missing -> default=%u",
LB_KEY_GRID_MAXA,
(unsigned)max_grid_current);
}
else if (err == ESP_OK)
{
max_grid_current = MAX_GRID_CURRENT_LIMIT;
(void)storage_set_u8_async(LB_NS, LB_KEY_GRID_MAXA, max_grid_current);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=%u",
LB_KEY_GRID_MAXA,
(unsigned)temp_u8,
(unsigned)max_grid_current);
}
else
{
max_grid_current = MAX_GRID_CURRENT_LIMIT;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=%u, storage not overwritten.",
LB_NS,
LB_KEY_GRID_MAXA,
esp_err_to_name(err),
(unsigned)max_grid_current);
}
// pv on
/*
* pv_on - legacy/config auxiliar.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_PV_ON, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= 1)
{
pv_enabled = (temp_u8 != 0);
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
pv_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_PV_ON, 0);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=0", LB_KEY_PV_ON);
}
else if (err == ESP_OK)
{
pv_enabled = false;
(void)storage_set_u8_async(LB_NS, LB_KEY_PV_ON, 0);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> default=0",
LB_KEY_PV_ON,
(unsigned)temp_u8);
}
else
{
pv_enabled = false;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=0, storage not overwritten.",
LB_NS,
LB_KEY_PV_ON,
esp_err_to_name(err));
}
// pv maxW (se não tiveres storage_get_u32_sync, adapta para i32/u32 do teu storage_service)
/*
* pv_max_w.
*/
uint32_t temp_u32 = 0;
err = storage_get_u32_sync(LB_NS, LB_KEY_PV_MAXW, &temp_u32, TO_TICKS_MS(800));
if (err == ESP_OK)
{
pv_max_import_w = (int32_t)temp_u32;
else
}
else if (err == ESP_ERR_NOT_FOUND)
{
pv_max_import_w = 0;
(void)storage_set_u32_async(LB_NS, LB_KEY_PV_MAXW, 0);
needs_flush = true;
ESP_LOGW(TAG, "LB config: %s missing -> default=0", LB_KEY_PV_MAXW);
}
else
{
pv_max_import_w = 0;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime default=0, storage not overwritten.",
LB_NS,
LB_KEY_PV_MAXW,
esp_err_to_name(err));
}
/*
* limit_mode - fonte principal do estado operacional.
*
* IMPORTANTE:
* - Se a chave existe e é válida, ela ganha sempre.
* - Só migrar dos booleans antigos quando LB_KEY_LIMIT_MODE não existe.
* - Se a leitura falhar temporariamente, NÃO gravar OFF por cima.
*/
err = storage_get_u8_sync(LB_NS, LB_KEY_LIMIT_MODE, &temp_u8, TO_TICKS_MS(800));
if (err == ESP_OK && temp_u8 <= LOADBALANCER_LIMIT_MODE_GRID_SOLAR)
{
limit_mode = (loadbalancer_limit_mode_t)temp_u8;
}
else if (err == ESP_ERR_NOT_FOUND)
{
/*
* Migração de versões antigas sem limit_mode.
*/
if (loadbalancer_enabled && pv_enabled && grid_limit_enabled)
{
limit_mode = LOADBALANCER_LIMIT_MODE_GRID_SOLAR;
}
else if (loadbalancer_enabled && pv_enabled)
{
limit_mode = LOADBALANCER_LIMIT_MODE_SOLAR;
}
else if (loadbalancer_enabled && grid_limit_enabled)
{
limit_mode = LOADBALANCER_LIMIT_MODE_GRID;
}
else
{
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
}
(void)storage_set_u8_async(LB_NS,
LB_KEY_LIMIT_MODE,
(uint8_t)limit_mode);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s missing -> migrated mode=%s",
LB_KEY_LIMIT_MODE,
loadbalancer_limit_mode_to_str(limit_mode));
}
else if (err == ESP_OK)
{
/*
* Valor existente mas inválido.
* Aqui é aceitável reparar para OFF.
*/
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
(void)storage_set_u8_async(LB_NS,
LB_KEY_LIMIT_MODE,
(uint8_t)limit_mode);
needs_flush = true;
ESP_LOGW(TAG,
"LB config: %s invalid value=%u -> mode=off",
LB_KEY_LIMIT_MODE,
(unsigned)temp_u8);
}
else
{
/*
* Erro temporário de leitura.
* Não sobrescrever storage.
*/
limit_mode = LOADBALANCER_LIMIT_MODE_OFF;
ESP_LOGW(TAG,
"LB config: failed to read %s/%s: %s. Runtime mode=off, storage not overwritten.",
LB_NS,
LB_KEY_LIMIT_MODE,
esp_err_to_name(err));
}
if (needs_flush)
(void)storage_flush_sync(TO_TICKS_MS(2000));
{
esp_err_t fe = storage_flush_sync(TO_TICKS_MS(2000));
if (fe != ESP_OK)
{
ESP_LOGW(TAG,
"LB config: flush defaults/migration failed: %s",
esp_err_to_name(fe));
}
}
// propagar para sub-módulos
pv_optimizer_set_enabled(pv_enabled);
/*
* Propagar parâmetros para sub-módulos antes de aplicar o modo.
*/
(void)pv_optimizer_set_max_import_w(pv_max_import_w);
grid_limiter_set_enabled(grid_limit_enabled);
(void)grid_limiter_set_max_import_a(max_grid_current);
/*
* Aplicar modo operacional sem persistir novamente.
*/
loadbalancer_apply_limit_mode_no_persist(limit_mode);
ESP_LOGW(TAG,
"LB config loaded: mode=%s enabled=%d grid=%d maxA=%u pv=%d pvMaxW=%ld",
loadbalancer_limit_mode_to_str(limit_mode),
loadbalancer_enabled,
grid_limit_enabled,
(unsigned)max_grid_current,
pv_enabled,
(long)pv_max_import_w);
return ESP_OK;
}
@@ -438,17 +876,19 @@ static void persist_u32(const char *key, uint32_t v) { (void)storage_set_u32_asy
void loadbalancer_set_enabled(bool enabled)
{
if (enabled == loadbalancer_enabled)
return;
loadbalancer_enabled = enabled;
persist_u8(LB_KEY_ENABLED, enabled ? 1 : 0);
loadbalancer_state_event_t evt = {.enabled = enabled, .timestamp_us = esp_timer_get_time()};
(void)esp_event_post(LOADBALANCER_EVENTS, LOADBALANCER_EVENT_STATE_CHANGED,
&evt, sizeof(evt), portMAX_DELAY);
if (enabled)
{
/*
* Compatibilidade legacy:
* enabled=true passa a significar modo GRID.
*/
(void)loadbalancer_set_limit_mode(LOADBALANCER_LIMIT_MODE_GRID);
}
else
{
(void)loadbalancer_set_limit_mode(LOADBALANCER_LIMIT_MODE_OFF);
}
}
bool loadbalancer_is_enabled(void) { return loadbalancer_enabled; }
// grid setters/getters
@@ -463,8 +903,13 @@ bool loadbalancer_grid_is_enabled(void) { return grid_limit_enabled; }
esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a)
{
ESP_LOGW(TAG, "SET grid max import: %uA -> %uA",
(unsigned)max_grid_current,
(unsigned)a);
if (a < MIN_GRID_CURRENT_LIMIT || a > MAX_GRID_CURRENT_LIMIT)
return ESP_ERR_INVALID_ARG;
max_grid_current = a;
persist_u8(LB_KEY_GRID_MAXA, a);
return grid_limiter_set_max_import_a(a);
@@ -741,83 +1186,60 @@ static void apply_meter_timeout_policy_locked(int *idxs, int cnt)
static void apply_normal_policy_locked(int *idxs, int cnt, float grid_snapshot)
{
// o teu algoritmo atual (grid-limit em A, por corrente medida)
float available = (float)max_grid_current - grid_snapshot;
if (available < -AVAILABLE_EPS)
{
float factor = 0.0f;
if (grid_snapshot > AVAILABLE_EPS)
factor = ((float)max_grid_current) / grid_snapshot;
if (factor < 0.0f)
factor = 0.0f;
if (factor > 1.0f)
factor = 1.0f;
for (int k = 0; k < cnt; k++)
{
int i = idxs[k];
// baseline = runtime (como tinhas)
float cur = connectors[i].runtime_current;
float hw = connectors[i].hw_max_current;
if (cur < 0.0f)
cur = 0.0f;
if (hw < 0.0f)
hw = 0.0f;
connectors[i].assigned = MIN(cur, hw) * factor;
}
return;
}
// baseline = runtime
for (int k = 0; k < cnt; k++)
{
int i = idxs[k];
float cur = connectors[i].runtime_current;
float hw = connectors[i].hw_max_current;
if (cur < 0.0f)
cur = 0.0f;
if (hw < 0.0f)
hw = 0.0f;
connectors[i].assigned = MIN(cur, hw);
}
if (fabsf(available) <= AVAILABLE_EPS)
/*
* Grid limit em modo normal.
*
* max_grid_current é o limite TOTAL de importação da instalação.
*
* Não podemos usar runtime_current como baseline inicial, porque isso
* permite comandos acima do limite configurado. Exemplo observado:
* maxA=8 e SET_CURRENT=22A/27A.
*
* Fórmula:
*
* budget = ultimo_total_comandado + (max_grid_current - grid_medido)
*
* Exemplos:
* last=0A, grid=0A, max=8A => budget=8A
* last=20A, grid=15A, max=8A => budget=13A
* last=0A, grid=5A, max=8A => budget=3A => abaixo de 6A, fica pausado
*/
if (cnt <= 0)
return;
float remaining = available;
if (grid_snapshot < 0.0f)
grid_snapshot = 0.0f;
// fase 1: subir até MIN para os mais antigos, se possível
for (int k = 0; k < cnt && remaining > 0.0f; k++)
{
int i = idxs[k];
float hw = connectors[i].hw_max_current;
float target = MIN((float)MIN_CHARGING_CURRENT_LIMIT, hw);
const float total_hw_max_a = sum_hw_max_locked(idxs, cnt);
const float last_total_cmd_a = sum_last_limit_locked(idxs, cnt);
if (connectors[i].assigned >= target)
continue;
float budget_total_a =
last_total_cmd_a + ((float)max_grid_current - grid_snapshot);
float delta = target - connectors[i].assigned;
float inc = MIN(delta, remaining);
connectors[i].assigned += inc;
remaining -= inc;
}
if (budget_total_a < 0.0f)
budget_total_a = 0.0f;
// fase 2: suspender os mais recentes que ficaram abaixo do mínimo
for (int k = cnt - 1; k >= 0; k--)
{
int i = idxs[k];
if (connectors[i].assigned >= (float)MIN_CHARGING_CURRENT_LIMIT)
continue;
/*
* Safety clamp:
* Em modo grid-limit, o total comandado nunca deve ultrapassar
* max_grid_current. A fórmula incremental pode subir acima do limite
* quando o meter ainda mede pouca corrente após pausa/retoma.
*/
if (budget_total_a > (float)max_grid_current)
budget_total_a = (float)max_grid_current;
connectors[i].assigned = 0.0f;
connectors[i].suspended_by_lb = true;
}
if (budget_total_a > total_hw_max_a)
budget_total_a = total_hw_max_a;
// fase 3: distribuir extra pelos que estão ativos
if (remaining > AVAILABLE_EPS)
distribute_extra_evenly(idxs, cnt, remaining);
ESP_LOGI(TAG,
"GRID policy: maxA=%u grid=%.2fA lastCmd=%.2fA budget=%.2fA active=%d",
(unsigned)max_grid_current,
grid_snapshot,
last_total_cmd_a,
budget_total_a,
cnt);
apply_budget_policy_locked(idxs, cnt, budget_total_a);
}
static int prepare_pending_limits_locked(const int *idxs,

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

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

@@ -8,6 +8,7 @@ set(srcs
driver/meter_modbus/meter_orno516.c
driver/meter_modbus/meter_dts6619.c
driver/meter_modbus/meter_dds661.c
driver/meter_modbus/meter_dds665.c
driver/meter_modbus/meter_ea777.c
driver/meter_modbus/meter_dts024m.c
driver/meter_modbus/modbus_params.c

View File

View File

View File

View File

View File

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

View File

@@ -9,20 +9,49 @@ extern "C" {
#include "esp_err.h"
/**
* @brief Inicializa o driver do medidor DDS 661 (SPI, mutex, registradores).
* @brief Inicializa o driver DDS661 em modo compatível antigo: GRID / slave ID 1.
*/
esp_err_t meter_dds661_init(void);
/**
* @brief Inicia a tarefa de leitura de dados do medidor DDS 661.
* @brief Regista DDS661 como meter GRID no slave ID 1.
*/
esp_err_t meter_dds661_grid_init(void);
/**
* @brief Regista DDS661 como meter EVSE no slave ID 2.
*/
esp_err_t meter_dds661_evse_init(void);
/**
* @brief Inicia a task partilhada de leitura DDS661.
*/
esp_err_t meter_dds661_start(void);
/**
* @brief Para a tarefa de leitura e limpa os dados internos do medidor DDS 661.
* @brief Inicia a task partilhada de leitura DDS661 para GRID.
*/
esp_err_t meter_dds661_grid_start(void);
/**
* @brief Inicia a task partilhada de leitura DDS661 para EVSE.
*/
esp_err_t meter_dds661_evse_start(void);
/**
* @brief Para/remover DDS661 GRID em modo compatível antigo.
*/
void meter_dds661_stop(void);
/**
* @brief Remove DDS661 GRID; só destrói o Modbus master se não houver EVSE registado.
*/
void meter_dds661_grid_stop(void);
/**
* @brief Remove DDS661 EVSE; só destrói o Modbus master se não houver GRID registado.
*/
void meter_dds661_evse_stop(void);
#ifdef __cplusplus
}

View File

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

View File

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

View File

View File

View File

View File

View File

@@ -1,13 +1,27 @@
// meter_ea777.c — Driver Modbus RTU para EARU EA777 (ESP-IDF)
// Suporta 1 ou 2 EA777 no mesmo bus RS485:
// GRID -> slave ID 1
// EVSE -> slave ID 2
// Usa um único Modbus master e uma única task de leitura.
#include "meter_ea777.h"
#include "meter_events.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stddef.h>
#include <string.h>
#include "meter_ea777.h"
#include <math.h>
#include <inttypes.h>
#define TAG "serial_mdb_ea777"
@@ -20,17 +34,30 @@
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// ===== EA777 no mesmo bus =====
#define EA777_GRID_SLAVE_ID 1
#define EA777_EVSE_SLAVE_ID 2
// ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (200 / portTICK_PERIOD_MS)
#define EA777_LOCK_TIMEOUT pdMS_TO_TICKS(10000)
// ===== Helpers =====
#define STR(fieldname) ((const char *)(fieldname))
#define OPTS(min_val, max_val, step_val) {.opt1 = min_val, .opt2 = max_val, .opt3 = step_val}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// ===== Estado =====
static bool is_initialized = false;
static volatile bool task_should_run = false;
static TaskHandle_t meter_task = NULL;
static SemaphoreHandle_t ea777_lock = NULL;
static inline uint32_t ea777_swap_words_u32(uint32_t x)
{
return ((x & 0xFFFFu) << 16) | ((x >> 16) & 0xFFFFu);
}
// ============================================================================
// ============ MAPA DE REGISTROS EA777 (Holding 0x03) ========================
@@ -45,7 +72,6 @@ static TaskHandle_t meter_task = NULL;
#define EA777_L3CURRENT 0x0005
// Potência ativa total (W)
#define EA777_TOTAL_ACTIVE_P 0x0007
// (se quiser por fase, pode usar 0x0008/0x0009/0x000A)
// Fator de potência por fase (0.001)
#define EA777_PF_L1 0x0014
#define EA777_PF_L2 0x0015
@@ -56,7 +82,7 @@ static TaskHandle_t meter_task = NULL;
#define EA777_TOTAL_ACTIVE_E 0x001D
// ============================================================================
// ============ CIDs ============
// ============ CIDs locais por meter ============
enum
{
CID_EA777_L1_VOLTAGE = 0,
@@ -71,11 +97,42 @@ enum
CID_EA777_PF_L3,
CID_EA777_FREQUENCY,
CID_EA777_TOTAL_ACTIVE_E,
CID_EA777_COUNT,
};
// ======= Descritores (Holding registers) =======
typedef enum
{
EA777_SLOT_GRID = 0,
EA777_SLOT_EVSE,
EA777_MAX_INSTANCES
} ea777_slot_t;
typedef struct
{
bool registered;
const char *source; // "GRID" ou "EVSE"
uint8_t slave_id; // 1 ou 2
uint16_t cid_base; // atribuído dinamicamente em ea777_rebuild_descriptors_locked()
} ea777_instance_t;
static ea777_instance_t ea777_instances[EA777_MAX_INSTANCES] = {
[EA777_SLOT_GRID] = {
.registered = false,
.source = "GRID",
.slave_id = EA777_GRID_SLAVE_ID,
.cid_base = 0,
},
[EA777_SLOT_EVSE] = {
.registered = false,
.source = "EVSE",
.slave_id = EA777_EVSE_SLAVE_ID,
.cid_base = 0,
},
};
// ======= Template de descritores (Holding registers) =======
// Nota: param_offset = 0 -> não usamos holding_reg_params_t aqui.
const mb_parameter_descriptor_t device_parameters_ea777[] = {
static const mb_parameter_descriptor_t ea777_param_template[CID_EA777_COUNT] = {
// Tensões (0.1 V)
{CID_EA777_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, EA777_L1VOLTAGE, 1,
@@ -131,18 +188,278 @@ const mb_parameter_descriptor_t device_parameters_ea777[] = {
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
};
const uint16_t num_device_parameters_ea777 =
sizeof(device_parameters_ea777) / sizeof(device_parameters_ea777[0]);
// O esp-modbus exige cid e param_key únicos no Data Dictionary.
// Como GRID e EVSE usam o mesmo mapa de registos, o param_key precisa de prefixo por instância.
static const char *const ea777_param_keys[EA777_MAX_INSTANCES][CID_EA777_COUNT] = {
[EA777_SLOT_GRID] = {
"GRID L1 Voltage",
"GRID L2 Voltage",
"GRID L3 Voltage",
"GRID L1 Current",
"GRID L2 Current",
"GRID L3 Current",
"GRID Total Active Power",
"GRID L1 PF",
"GRID L2 PF",
"GRID L3 PF",
"GRID Frequency",
"GRID Total Active Energy",
},
[EA777_SLOT_EVSE] = {
"EVSE L1 Voltage",
"EVSE L2 Voltage",
"EVSE L3 Voltage",
"EVSE L1 Current",
"EVSE L2 Current",
"EVSE L3 Current",
"EVSE Total Active Power",
"EVSE L1 PF",
"EVSE L2 PF",
"EVSE L3 PF",
"EVSE Frequency",
"EVSE Total Active Energy",
},
};
// ===== Post do evento de medição =====
static void meter_ea777_post_event(float *voltage, float *current, int *power_w,
float freq_hz, float pf_avg, float total_kwh)
static mb_parameter_descriptor_t device_parameters_ea777[EA777_MAX_INSTANCES * CID_EA777_COUNT];
static uint16_t num_device_parameters_ea777 = 0;
static bool ea777_descriptor_dirty = true;
static bool ea777_has_registered_instance_locked(void)
{
for (uint8_t i = 0; i < EA777_MAX_INSTANCES; ++i)
{
if (ea777_instances[i].registered)
return true;
}
return false;
}
static esp_err_t ea777_rebuild_descriptors_locked(void)
{
num_device_parameters_ea777 = 0;
ea777_descriptor_dirty = true;
for (uint8_t inst_idx = 0; inst_idx < EA777_MAX_INSTANCES; ++inst_idx)
{
ea777_instance_t *inst = &ea777_instances[inst_idx];
if (!inst->registered)
continue;
// Mantém os CIDs contíguos no descriptor ativo.
// Isto evita problemas em versões do esp-modbus que tratam CID como índice.
inst->cid_base = num_device_parameters_ea777;
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; ++local_cid)
{
mb_parameter_descriptor_t *dst = &device_parameters_ea777[num_device_parameters_ea777++];
*dst = ea777_param_template[local_cid];
dst->cid = inst->cid_base + local_cid;
dst->param_key = ea777_param_keys[inst_idx][local_cid];
dst->mb_slave_addr = inst->slave_id;
}
}
ea777_descriptor_dirty = true;
ESP_LOGI(TAG, "EA777 descriptor table prepared: %u parameters", num_device_parameters_ea777);
return ESP_OK;
}
static esp_err_t ea777_apply_descriptors_locked(void)
{
if (num_device_parameters_ea777 == 0)
return ESP_ERR_INVALID_STATE;
if (!ea777_descriptor_dirty)
return ESP_OK;
esp_err_t err = mbc_master_set_descriptor(device_parameters_ea777, num_device_parameters_ea777);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", esp_err_to_name(err));
return err;
}
ea777_descriptor_dirty = false;
ESP_LOGI(TAG, "EA777 descriptor table applied: %u parameters", num_device_parameters_ea777);
return ESP_OK;
}
static esp_err_t ea777_master_init_once(void)
{
if (is_initialized)
return ESP_OK;
if (!ea777_lock)
{
ea777_lock = xSemaphoreCreateMutex();
if (!ea777_lock)
return ESP_ERR_NO_MEM;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted before EA777 init");
}
(void)mbc_master_destroy();
ESP_LOGI(TAG, "meter_ea777 Modbus master init");
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_EVEN,
};
void *handler = NULL;
esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler);
if (err != ESP_OK)
return err;
err = mbc_master_setup(&comm);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// RS-485 half duplex
err = uart_set_pin(MB_PORT_NUM,
MB_UART_TXD, MB_UART_RXD,
MB_UART_RTS, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
is_initialized = true;
ESP_LOGI(TAG, "EA777 Modbus master initialized (9600 8E1, Holding Reg 0x03)");
return ESP_OK;
}
static esp_err_t ea777_register_instance(ea777_slot_t slot)
{
if (slot >= EA777_MAX_INSTANCES)
return ESP_ERR_INVALID_ARG;
esp_err_t err = ea777_master_init_once();
if (err != ESP_OK)
return err;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
ea777_instances[slot].registered = true;
ESP_LOGI(TAG, "EA777 %s registered on Modbus slave ID %u",
ea777_instances[slot].source,
ea777_instances[slot].slave_id);
err = ea777_rebuild_descriptors_locked();
xSemaphoreGive(ea777_lock);
return err;
}
static void ea777_shutdown_if_idle(void)
{
bool any_registered = false;
if (ea777_lock && xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE)
{
any_registered = ea777_has_registered_instance_locked();
xSemaphoreGive(ea777_lock);
}
if (any_registered || !is_initialized)
return;
ESP_LOGI(TAG, "No EA777 instances registered; stopping shared Modbus master");
task_should_run = false;
for (int i = 0; i < 20 && meter_task != NULL; ++i)
{
vTaskDelay(pdMS_TO_TICKS(10));
}
if (meter_task != NULL)
{
ESP_LOGW(TAG, "EA777 task did not exit in time; deleting it");
vTaskDelete(meter_task);
meter_task = NULL;
}
esp_err_t err = mbc_master_destroy();
if (err != ESP_OK)
ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err));
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
is_initialized = false;
num_device_parameters_ea777 = 0;
ea777_descriptor_dirty = true;
}
static void ea777_unregister_instance(ea777_slot_t slot)
{
if (slot >= EA777_MAX_INSTANCES)
return;
if (!is_initialized || !ea777_lock)
return;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE)
{
ESP_LOGI(TAG, "EA777 %s unregistered", ea777_instances[slot].source);
ea777_instances[slot].registered = false;
(void)ea777_rebuild_descriptors_locked();
xSemaphoreGive(ea777_lock);
}
ea777_shutdown_if_idle();
}
static void meter_ea777_post_event(const ea777_instance_t *inst,
const float *voltage,
const float *current,
const int32_t *power_w,
int32_t total_power_w,
float freq_hz,
float pf_avg,
float total_kwh)
{
meter_event_data_t evt = {
.source = "GRID",
.source = inst->source,
.frequency = freq_hz,
.power_factor = pf_avg,
.total_energy = total_kwh};
.total_energy = total_kwh,
.watt_total = total_power_w,
.timestamp_us = esp_timer_get_time(),
};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
@@ -152,12 +469,11 @@ static void meter_ea777_post_event(float *voltage, float *current, int *power_w,
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "%s falha ao emitir evento: %s", inst->source, esp_err_to_name(err));
}
}
// ===== Task de polling =====
static void serial_mdb_ea777_task(void *param)
static void ea777_read_instance_locked(const ea777_instance_t *inst)
{
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
@@ -167,214 +483,279 @@ static void serial_mdb_ea777_task(void *param)
float pf[3] = {0};
float freq = 0.0f;
float total_kwh = 0.0f;
int32_t total_active_power_w = 0;
bool got_any_value = false;
// pequeno settle antes da 1ª leitura
vTaskDelay(pdMS_TO_TICKS(200));
while (1)
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; local_cid++)
{
for (uint16_t cid = 0; cid < num_device_parameters_ea777; cid++)
const uint16_t cid = inst->cid_base + local_cid;
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
continue;
}
ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", inst->source, cid, esp_err_to_name(err));
continue;
}
uint8_t type = 0;
uint16_t raw16 = 0;
uint32_t raw32 = 0;
uint8_t type = 0;
uint16_t raw16 = 0;
uint32_t raw32 = 0;
void *value_ptr = (cid == CID_EA777_TOTAL_ACTIVE_E) ? (void *)&raw32 : (void *)&raw16;
void *value_ptr = (local_cid == CID_EA777_TOTAL_ACTIVE_E) ? (void *)&raw32 : (void *)&raw16;
// 1 retry simples em caso de timeout
// 1 retry simples em caso de timeout
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
if (err == ESP_ERR_TIMEOUT)
{
vTaskDelay(pdMS_TO_TICKS(60));
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
if (err == ESP_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)
{
case CID_EA777_L1_VOLTAGE:
v[0] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L2_VOLTAGE:
v[1] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L3_VOLTAGE:
v[2] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L1_CURRENT:
i[0] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L2_CURRENT:
i[1] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L3_CURRENT:
i[2] = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_P:
// guarda se quiser usar em debug; para o evento usamos
// aproximação por fase abaixo
// (poderia ser passado direto em power_w[0..2] também)
break;
case CID_EA777_PF_L1:
pf[0] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L2:
pf[1] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L3:
pf[2] = ((float)raw16) * 0.001f;
break;
case CID_EA777_FREQUENCY:
freq = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_E:
total_kwh = ((float)raw32) * 0.01f;
break;
default:
break;
}
ESP_LOGD(TAG, "%s (cid=%u) -> raw16=%u raw32=%u",
desc->param_key, cid,
(unsigned int)raw16,
(unsigned int)raw32);
}
else
{
ESP_LOGE(TAG, "CID %u (%s) read failed: %s",
cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
// Potência por fase aproximada: P = V * I * PF
int p_int[3] = {
(int)(v[0] * i[0] * pf[0]),
(int)(v[1] * i[1] * pf[1]),
(int)(v[2] * i[2] * pf[2]),
};
// PF médio simples (ignora zeros)
float pf_sum = 0.0f;
int pf_cnt = 0;
for (int k = 0; k < 3; ++k)
if (err == ESP_OK)
{
if (pf[k] != 0.0f)
{
pf_sum += pf[k];
pf_cnt++;
}
}
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
got_any_value = true;
meter_ea777_post_event(v, i, p_int, freq, pf_avg, total_kwh);
vTaskDelay(UPDATE_INTERVAL);
switch (local_cid)
{
case CID_EA777_L1_VOLTAGE:
v[0] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L2_VOLTAGE:
v[1] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L3_VOLTAGE:
v[2] = ((float)raw16) * 0.1f;
break;
case CID_EA777_L1_CURRENT:
i[0] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L2_CURRENT:
i[1] = ((float)raw16) * 0.01f;
break;
case CID_EA777_L3_CURRENT:
i[2] = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_P:
total_active_power_w = (int32_t)raw16;
break;
case CID_EA777_PF_L1:
pf[0] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L2:
pf[1] = ((float)raw16) * 0.001f;
break;
case CID_EA777_PF_L3:
pf[2] = ((float)raw16) * 0.001f;
break;
case CID_EA777_FREQUENCY:
freq = ((float)raw16) * 0.01f;
break;
case CID_EA777_TOTAL_ACTIVE_E:
{
uint32_t raw_e = ea777_swap_words_u32(raw32);
total_kwh = ((float)raw_e) * 0.01f;
ESP_LOGI(TAG,
"%s EA777 energy raw=0x%08" PRIX32
" swapped=0x%08" PRIX32 " => %.3f kWh",
inst->source,
raw32,
raw_e,
(double)total_kwh);
break;
}
default:
break;
}
ESP_LOGD(TAG, "%s %s (cid=%u) -> raw16=%u raw32=%u",
inst->source, desc->param_key, cid,
(unsigned int)raw16,
(unsigned int)raw32);
}
else
{
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
inst->source, cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
if (!got_any_value)
{
ESP_LOGW(TAG, "%s no valid EA777 values read in this cycle", inst->source);
return;
}
// Potência por fase aproximada: P = V * I * PF.
// O EA777 também fornece potência ativa total; esta é usada em watt_total quando existe.
int32_t p_int[3] = {
(int32_t)lrintf(v[0] * i[0] * pf[0]),
(int32_t)lrintf(v[1] * i[1] * pf[1]),
(int32_t)lrintf(v[2] * i[2] * pf[2]),
};
const int32_t p_sum = p_int[0] + p_int[1] + p_int[2];
const int32_t watt_total = (total_active_power_w != 0) ? total_active_power_w : p_sum;
// PF médio simples (ignora zeros)
float pf_sum = 0.0f;
int pf_cnt = 0;
for (int k = 0; k < 3; ++k)
{
if (pf[k] != 0.0f)
{
pf_sum += pf[k];
pf_cnt++;
}
}
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
meter_ea777_post_event(inst, v, i, p_int, watt_total, freq, pf_avg, total_kwh);
}
// ===== Task de polling partilhada =====
static void serial_mdb_ea777_task(void *param)
{
(void)param;
// pequeno settle antes da 1ª leitura
vTaskDelay(pdMS_TO_TICKS(200));
while (task_should_run)
{
if (!is_initialized || !ea777_lock)
{
vTaskDelay(UPDATE_INTERVAL);
continue;
}
bool had_instance = false;
for (uint8_t slot = 0; slot < EA777_MAX_INSTANCES && task_should_run; ++slot)
{
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
{
ESP_LOGW(TAG, "EA777 task timeout waiting lock");
continue;
}
ea777_instance_t inst = ea777_instances[slot];
if (inst.registered)
{
had_instance = true;
ea777_read_instance_locked(&inst);
}
xSemaphoreGive(ea777_lock);
}
vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500));
}
ESP_LOGI(TAG, "EA777 task stopped");
meter_task = NULL;
vTaskDelete(NULL);
}
// ===== API pública =====
// Compatibilidade: chamada antiga inicializa EA777 como GRID/ID 1.
esp_err_t meter_ea777_init(void)
{
if (is_initialized)
{
ESP_LOGW(TAG, "Already initialized");
return ESP_ERR_INVALID_STATE;
}
return meter_ea777_grid_init();
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
esp_err_t meter_ea777_grid_init(void)
{
return ea777_register_instance(EA777_SLOT_GRID);
}
(void)mbc_master_destroy();
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = MB_DEV_SPEED,
.parity = UART_PARITY_EVEN};
void *handler = NULL;
ESP_ERROR_CHECK(mbc_master_init(MB_PORT_SERIAL_MASTER, &handler));
ESP_ERROR_CHECK(mbc_master_setup(&comm));
// >>> RS-485 half duplex
ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM,
MB_UART_TXD, MB_UART_RXD,
MB_UART_RTS, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(mbc_master_start());
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX));
vTaskDelay(pdMS_TO_TICKS(50));
ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777,
num_device_parameters_ea777));
is_initialized = true;
ESP_LOGI(TAG, "EA777 Modbus master initialized (9600 8E1, Holding Reg 0x03)");
return ESP_OK;
esp_err_t meter_ea777_evse_init(void)
{
return ea777_register_instance(EA777_SLOT_EVSE);
}
esp_err_t meter_ea777_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "Not initialized");
ESP_LOGE(TAG, "meter_ea777 not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!ea777_lock)
return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE)
return ESP_ERR_TIMEOUT;
const bool has_instance = ea777_has_registered_instance_locked();
esp_err_t desc_err = ESP_OK;
if (has_instance)
desc_err = ea777_apply_descriptors_locked();
xSemaphoreGive(ea777_lock);
if (!has_instance)
{
ESP_LOGW(TAG, "meter_ea777 start ignored: no instances registered");
return ESP_ERR_INVALID_STATE;
}
if (desc_err != ESP_OK)
return desc_err;
if (meter_task == NULL)
{
xTaskCreate(serial_mdb_ea777_task,
"meter_ea777_task",
4096, NULL, 3, &meter_task);
ESP_LOGI(TAG, "EA777 task started");
task_should_run = true;
BaseType_t ok = xTaskCreate(serial_mdb_ea777_task,
"meter_ea777_task",
4096, NULL, 3, &meter_task);
if (ok != pdPASS)
{
task_should_run = false;
meter_task = NULL;
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "meter_ea777 shared task started");
}
return ESP_OK;
}
esp_err_t meter_ea777_grid_start(void)
{
return meter_ea777_start();
}
esp_err_t meter_ea777_evse_start(void)
{
return meter_ea777_start();
}
void meter_ea777_stop(void)
{
if (!is_initialized)
{
ESP_LOGW(TAG, "Not initialized, skipping stop");
return;
}
if (meter_task)
{
vTaskDelete(meter_task);
meter_task = NULL;
ESP_LOGI(TAG, "EA777 task stopped");
}
(void)mbc_master_destroy();
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 EA777 cleaned up");
meter_ea777_grid_stop();
}
void meter_ea777_grid_stop(void)
{
ea777_unregister_instance(EA777_SLOT_GRID);
}
void meter_ea777_evse_stop(void)
{
ea777_unregister_instance(EA777_SLOT_EVSE);
}

View File

@@ -10,24 +10,50 @@ extern "C" {
#endif
/**
* @brief Inicializa o driver do medidor EA777 (UART RS485, Modbus, registradores).
*
* @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro.
* @brief Inicializa o driver EA777 em modo compatível antigo: GRID / slave ID 1.
*/
esp_err_t meter_ea777_init(void);
/**
* @brief Inicia a tarefa de leitura de dados do medidor EA777.
*
* @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro.
* @brief Regista EA777 como meter GRID no slave ID 1.
*/
esp_err_t meter_ea777_grid_init(void);
/**
* @brief Regista EA777 como meter EVSE no slave ID 2.
*/
esp_err_t meter_ea777_evse_init(void);
/**
* @brief Inicia a task partilhada de leitura EA777.
*/
esp_err_t meter_ea777_start(void);
/**
* @brief Para a tarefa de leitura e limpa os dados internos do medidor EA777.
* @brief Inicia a task partilhada de leitura EA777 para GRID.
*/
esp_err_t meter_ea777_grid_start(void);
/**
* @brief Inicia a task partilhada de leitura EA777 para EVSE.
*/
esp_err_t meter_ea777_evse_start(void);
/**
* @brief Para/remover EA777 GRID em modo compatível antigo.
*/
void meter_ea777_stop(void);
/**
* @brief Remove EA777 GRID; só destrói o Modbus master se não houver EVSE registado.
*/
void meter_ea777_grid_stop(void);
/**
* @brief Remove EA777 EVSE; só destrói o Modbus master se não houver GRID registado.
*/
void meter_ea777_evse_stop(void);
#ifdef __cplusplus
}
#endif

View File

View File

View File

View File

View File

View File

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

View File

@@ -4,25 +4,54 @@
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
/**
* @brief Inicializa o driver do medidor ORNO 526 (SPI, mutex, registradores).
* @brief Compatibilidade: inicializa OR-WE-526 como meter GRID (slave ID 1).
*/
esp_err_t meter_orno526_init(void);
/**
* @brief Inicia a tarefa de leitura de dados do medidor ORNO 526.
* @brief Inicializa OR-WE-526 como meter GRID (slave ID 1).
*/
esp_err_t meter_orno526_grid_init(void);
/**
* @brief Inicializa OR-WE-526 como meter EVSE (slave ID 1).
*
* O driver suporta uma única instância OR-WE-526 de cada vez.
*/
esp_err_t meter_orno526_evse_init(void);
/**
* @brief Inicia a task de aquisição OR-WE-526.
*/
esp_err_t meter_orno526_start(void);
/**
* @brief Para a tarefa de leitura e limpa os dados internos do medidor ORNO 526.
* @brief Alias de start para utilização como GRID.
*/
esp_err_t meter_orno526_grid_start(void);
/**
* @brief Alias de start para utilização como EVSE.
*/
esp_err_t meter_orno526_evse_start(void);
/**
* @brief Para a task e destrói o master Modbus/UART.
*/
void meter_orno526_stop(void);
/**
* @brief Alias de stop para utilização como GRID.
*/
void meter_orno526_grid_stop(void);
/**
* @brief Alias de stop para utilização como EVSE.
*/
void meter_orno526_evse_stop(void);
#ifdef __cplusplus
}

View File

View File

View File

@@ -8,6 +8,7 @@
#include "driver/uart.h"
#include "driver/gpio.h"
#include "meter_events.h"
#include "esp_timer.h"
#define TAG "meter_zigbee"
@@ -76,11 +77,19 @@ static inline int32_t tuya_power16_to_signed(uint16_t p)
static void meter_zigbee_post_event(void)
{
int32_t watt_total =
(int32_t)meter_data.watt[PHASE_L1] +
(int32_t)meter_data.watt[PHASE_L2] +
(int32_t)meter_data.watt[PHASE_L3];
meter_event_data_t evt = {
.source = "GRID",
.frequency = meter_data.frequency,
.power_factor = meter_data.power_factor,
.total_energy = meter_data.total_energy};
.total_energy = meter_data.total_energy,
.watt_total = watt_total,
.timestamp_us = esp_timer_get_time(),
};
memcpy(evt.vrms, meter_data.vrms, sizeof(evt.vrms));
memcpy(evt.irms, meter_data.irms, sizeof(evt.irms));

View File

11
components/meter_manager/include/meter_manager.h Executable file → Normal file
View File

@@ -14,6 +14,7 @@ typedef enum {
METER_TYPE_ORNO516, // ORNO-516
METER_TYPE_ORNO526, // ORNO-516
METER_TYPE_DDS661, // DDS-661
METER_TYPE_DDS665, // DDS-665
METER_TYPE_DTS6619, // dts6619
METER_TYPE_MONO_ZIGBEE, // Zigbee single-phase
METER_TYPE_TRIF_ZIGBEE, // Zigbee three-phase
@@ -105,6 +106,16 @@ esp_err_t meter_manager_grid_set_model(meter_type_t meter_type);
*/
meter_type_t meter_manager_grid_get_model(void);
/**
* @brief Atomically sets GRID and EVSE meter types, saves them to storage,
* then reinitializes/restarts the configured meters.
*
* This is preferred when changing both meters from the REST API because it
* avoids transient mixed Modbus masters on the same RS485 bus.
*/
esp_err_t meter_manager_set_models(meter_type_t grid_type, meter_type_t evse_type);
/**
* @brief Utility functions
*/

View File

@@ -7,6 +7,7 @@
#include "meter_orno526.h"
#include "meter_dts6619.h"
#include "meter_dds661.h"
#include "meter_dds665.h"
#include "meter_zigbee.h"
#include "meter_ea777.h"
#include "meter_dts024m.h"
@@ -156,60 +157,131 @@ static esp_err_t write_meter_model_to_storage(const char *key, meter_type_t mete
return ESP_OK;
}
static bool meter_type_uses_rs485_modbus(meter_type_t type)
{
switch (type)
{
case METER_TYPE_ORNO513:
case METER_TYPE_ORNO516:
case METER_TYPE_ORNO526:
case METER_TYPE_DTS6619:
case METER_TYPE_DDS661:
case METER_TYPE_DDS665:
case METER_TYPE_EA777:
case METER_TYPE_DTS024M:
return true;
default:
return false;
}
}
static bool meter_type_supports_dual_same_bus(meter_type_t type)
{
return type == METER_TYPE_DDS661 || type == METER_TYPE_DDS665 || type == METER_TYPE_EA777;
}
static esp_err_t validate_meter_model_pair(meter_type_t grid_type, meter_type_t evse_type)
{
if (grid_type == METER_TYPE_NONE || evse_type == METER_TYPE_NONE)
return ESP_OK;
const bool grid_is_modbus = meter_type_uses_rs485_modbus(grid_type);
const bool evse_is_modbus = meter_type_uses_rs485_modbus(evse_type);
if (grid_is_modbus && evse_is_modbus)
{
if (grid_type == evse_type && meter_type_supports_dual_same_bus(grid_type))
return ESP_OK;
ESP_LOGE(
TAG,
"Unsupported RS485 meter pair: GRID=%s EVSE=%s. "
"Supported dual Modbus pairs are "
"DDS-661+DDS-661, DDS-665+DDS-665 or EA-777+EA-777.",
meter_type_to_str(grid_type),
meter_type_to_str(evse_type));
return ESP_ERR_NOT_SUPPORTED;
}
return ESP_OK;
}
/**
* @brief Inicializa o sistema de meter manager.
*/
esp_err_t meter_manager_init(void)
{
// garantir storage pronto
esp_err_t s = storage_service_init();
if (s != ESP_OK)
ESP_LOGE(TAG, "storage_service_init failed: %s", esp_err_to_name(s));
esp_err_t err = storage_service_init();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "storage_service_init failed: %s",
esp_err_to_name(err));
return err;
}
esp_err_t err;
err = load_or_init_meter_model(
STORE_GRID_MODEL,
&meter_grid_type);
if (err != ESP_OK)
return err;
err = load_or_init_meter_model(
STORE_EVSE_MODEL,
&meter_evse_type);
if (err != ESP_OK)
return err;
err = validate_meter_model_pair(
meter_grid_type,
meter_evse_type);
if (err != ESP_OK)
return err;
// Initialize GRID meter
err = meter_manager_grid_init();
if (err != ESP_OK)
return err;
// Regista handler para eventos de rede
/*
ESP_LOGD(TAG, "Registering network event handler");
err = esp_event_handler_register(
NETWORK_EVENTS,
ESP_EVENT_ANY_ID,
meter_manager_network_event_handler,
NULL);
*/
err = meter_manager_evse_init();
if (err != ESP_OK)
{
(void)meter_manager_grid_stop();
return err;
}
// Emite um evento inicial de configuração
meter_config_event_t ev = {
.grid_type = meter_manager_grid_get_model(),
.evse_type = meter_manager_evse_get_model(),
.timestamp_us = esp_timer_get_time()};
esp_event_post(METER_EVENT,
METER_EVENT_CONFIG_UPDATED,
&ev,
sizeof(ev),
0);
.grid_type = meter_grid_type,
.evse_type = meter_evse_type,
.timestamp_us = esp_timer_get_time(),
};
(void)esp_event_post(
METER_EVENT,
METER_EVENT_CONFIG_UPDATED,
&ev,
sizeof(ev),
0);
return ESP_OK;
}
esp_err_t meter_manager_start(void)
{
// Start GRID meter
return meter_manager_grid_start();
esp_err_t err = meter_manager_grid_start();
if (err != ESP_OK)
return err;
return meter_manager_evse_start();
}
esp_err_t meter_manager_stop(void)
{
// Stop GRID meter
return meter_manager_grid_stop();
esp_err_t evse_err = meter_manager_evse_stop();
esp_err_t grid_err = meter_manager_grid_stop();
if (evse_err != ESP_OK)
return evse_err;
return grid_err;
}
// ---------- EVSE ----------
@@ -237,9 +309,11 @@ esp_err_t meter_manager_evse_init()
case METER_TYPE_DTS6619:
return meter_dts6619_init();
case METER_TYPE_DDS661:
return meter_dds661_init();
return meter_dds661_evse_init();
case METER_TYPE_DDS665:
return meter_dds665_evse_init();
case METER_TYPE_EA777:
return meter_ea777_init();
return meter_ea777_evse_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE:
@@ -268,9 +342,11 @@ esp_err_t meter_manager_evse_start()
case METER_TYPE_DTS6619:
return meter_dts6619_start();
case METER_TYPE_DDS661:
return meter_dds661_start();
return meter_dds661_evse_start();
case METER_TYPE_DDS665:
return meter_dds665_evse_start();
case METER_TYPE_EA777:
return meter_ea777_start();
return meter_ea777_evse_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE:
@@ -304,10 +380,13 @@ esp_err_t meter_manager_evse_stop(void)
meter_dts6619_stop();
break;
case METER_TYPE_DDS661:
meter_dds661_stop();
meter_dds661_evse_stop();
break;
case METER_TYPE_DDS665:
meter_dds665_evse_stop();
break;
case METER_TYPE_EA777:
meter_ea777_stop();
meter_ea777_evse_stop();
break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
@@ -347,9 +426,11 @@ esp_err_t meter_manager_grid_init()
case METER_TYPE_DTS6619:
return meter_dts6619_init();
case METER_TYPE_DDS661:
return meter_dds661_init();
return meter_dds661_grid_init();
case METER_TYPE_DDS665:
return meter_dds665_grid_init();
case METER_TYPE_EA777:
return meter_ea777_init();
return meter_ea777_grid_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE:
@@ -378,9 +459,11 @@ esp_err_t meter_manager_grid_start()
case METER_TYPE_DTS6619:
return meter_dts6619_start();
case METER_TYPE_DDS661:
return meter_dds661_start();
return meter_dds661_grid_start();
case METER_TYPE_DDS665:
return meter_dds665_grid_start();
case METER_TYPE_EA777:
return meter_ea777_start();
return meter_ea777_grid_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE:
@@ -414,10 +497,13 @@ esp_err_t meter_manager_grid_stop(void)
meter_dts6619_stop();
break;
case METER_TYPE_DDS661:
meter_dds661_stop();
meter_dds661_grid_stop();
break;
case METER_TYPE_DDS665:
meter_dds665_grid_stop();
break;
case METER_TYPE_EA777:
meter_ea777_stop();
meter_ea777_grid_stop();
break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
@@ -518,6 +604,8 @@ const char *meter_type_to_str(meter_type_t type)
return "DTS-6619";
case METER_TYPE_DDS661:
return "DDS-661";
case METER_TYPE_DDS665:
return "DDS-665";
case METER_TYPE_MONO_ZIGBEE:
return "MONO-ZIGBEE";
case METER_TYPE_TRIF_ZIGBEE:
@@ -548,6 +636,8 @@ meter_type_t string_to_meter_type(const char *str)
return METER_TYPE_DTS6619;
if (strcmp(str, "DDS-661") == 0)
return METER_TYPE_DDS661;
if (strcmp(str, "DDS-665") == 0)
return METER_TYPE_DDS665;
if (strcmp(str, "MONO-ZIGBEE") == 0)
return METER_TYPE_MONO_ZIGBEE;
if (strcmp(str, "TRIF-ZIGBEE") == 0)

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

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

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

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