chore: snapshot before shared RS485 bus integration

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

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,26 +10,52 @@ 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
#endif /* METER_EA777_H_ */
#endif /* METER_EA777_H_ */

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