chore: snapshot before shared RS485 bus integration
This commit is contained in:
0
.gitignore
vendored
Executable file → Normal file
0
.gitignore
vendored
Executable file → Normal file
0
CMakeLists.txt
Executable file → Normal file
0
CMakeLists.txt
Executable file → Normal file
0
LICENSE.md
Executable file → Normal file
0
LICENSE.md
Executable file → Normal file
0
cfg/custom/board.cfg
Executable file → Normal file
0
cfg/custom/board.cfg
Executable file → Normal file
106
cfg/esp32devkitc/board.cfg
Executable file → Normal file
106
cfg/esp32devkitc/board.cfg
Executable file → Normal 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
|
||||
# ------------------------------------------------------------
|
||||
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=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
|
||||
# ------------------------------------------------------------
|
||||
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
0
cfg/esp32s2da/board.cfg
Executable file → Normal file
0
components/auth/CMakeLists.txt
Executable file → Normal file
0
components/auth/CMakeLists.txt
Executable file → Normal file
0
components/auth/idf_component.yml
Executable file → Normal file
0
components/auth/idf_component.yml
Executable file → Normal file
0
components/auth/include/auth.h
Executable file → Normal file
0
components/auth/include/auth.h
Executable file → Normal file
0
components/auth/include/wiegand.h
Executable file → Normal file
0
components/auth/include/wiegand.h
Executable file → Normal file
0
components/auth/include/wiegand_reader.h
Executable file → Normal file
0
components/auth/include/wiegand_reader.h
Executable file → Normal file
0
components/auth/src/auth.c
Executable file → Normal file
0
components/auth/src/auth.c
Executable file → Normal file
0
components/auth/src/auth_types.c
Executable file → Normal file
0
components/auth/src/auth_types.c
Executable file → Normal file
0
components/auth/src/wiegand.c
Executable file → Normal file
0
components/auth/src/wiegand.c
Executable file → Normal file
0
components/auth/src/wiegand_reader.c
Executable file → Normal file
0
components/auth/src/wiegand_reader.c
Executable file → Normal file
0
components/buzzer/CMakeLists.txt
Executable file → Normal file
0
components/buzzer/CMakeLists.txt
Executable file → Normal file
0
components/buzzer/idf_component.yml
Executable file → Normal file
0
components/buzzer/idf_component.yml
Executable file → Normal file
0
components/buzzer/include/buzzer.h
Executable file → Normal file
0
components/buzzer/include/buzzer.h
Executable file → Normal file
0
components/buzzer/src/buzzer.c
Executable file → Normal file
0
components/buzzer/src/buzzer.c
Executable file → Normal file
0
components/config/CMakeLists.txt
Executable file → Normal file
0
components/config/CMakeLists.txt
Executable file → Normal file
25
components/config/board_config.c
Executable file → Normal file
25
components/config/board_config.c
Executable file → Normal 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
10
components/config/include/board_config.h
Executable file → Normal 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
2
components/evse/CMakeLists.txt
Executable file → Normal 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
0
components/evse/evse_api.c
Executable file → Normal file
0
components/evse/evse_config.c
Executable file → Normal file
0
components/evse/evse_config.c
Executable file → Normal file
0
components/evse/evse_core.c
Executable file → Normal file
0
components/evse/evse_core.c
Executable file → Normal file
0
components/evse/evse_error.c
Executable file → Normal file
0
components/evse/evse_error.c
Executable file → Normal file
0
components/evse/evse_events.c
Executable file → Normal file
0
components/evse/evse_events.c
Executable file → Normal file
0
components/evse/evse_fsm.c
Executable file → Normal file
0
components/evse/evse_fsm.c
Executable file → Normal file
0
components/evse/evse_hardware.c
Executable file → Normal file
0
components/evse/evse_hardware.c
Executable file → Normal file
0
components/evse/evse_limits.c
Executable file → Normal file
0
components/evse/evse_limits.c
Executable file → Normal file
72
components/evse/evse_manager.c
Executable file → Normal file
72
components/evse/evse_manager.c
Executable file → Normal 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
109
components/evse/evse_pilot.c
Executable file → Normal 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
0
components/evse/evse_state.c
Executable file → Normal file
0
components/evse/include/evse_api.h
Executable file → Normal file
0
components/evse/include/evse_api.h
Executable file → Normal file
0
components/evse/include/evse_config.h
Executable file → Normal file
0
components/evse/include/evse_config.h
Executable file → Normal file
0
components/evse/include/evse_error.h
Executable file → Normal file
0
components/evse/include/evse_error.h
Executable file → Normal file
0
components/evse/include/evse_events.h
Executable file → Normal file
0
components/evse/include/evse_events.h
Executable file → Normal file
0
components/evse/include/evse_fsm.h
Executable file → Normal file
0
components/evse/include/evse_fsm.h
Executable file → Normal file
0
components/evse/include/evse_hardware.h
Executable file → Normal file
0
components/evse/include/evse_hardware.h
Executable file → Normal file
0
components/evse/include/evse_limits.h
Executable file → Normal file
0
components/evse/include/evse_limits.h
Executable file → Normal file
0
components/evse/include/evse_manager.h
Executable file → Normal file
0
components/evse/include/evse_manager.h
Executable file → Normal file
0
components/evse/include/evse_pilot.h
Executable file → Normal file
0
components/evse/include/evse_pilot.h
Executable file → Normal file
0
components/evse/include/evse_state.h
Executable file → Normal file
0
components/evse/include/evse_state.h
Executable file → Normal file
0
components/evse_link/CMakeLists.txt
Executable file → Normal file
0
components/evse_link/CMakeLists.txt
Executable file → Normal file
0
components/evse_link/include/evse_link.h
Executable file → Normal file
0
components/evse_link/include/evse_link.h
Executable file → Normal 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
0
components/evse_link/src/evse_link.c
Executable file → Normal 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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
&s, sizeof(amps), portMAX_DELAY);
|
||||
esp_event_post(EVSE_LINK_EVENTS,
|
||||
LINK_EVENT_CURRENT_LIMIT_APPLIED,
|
||||
&s,
|
||||
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,
|
||||
&s, 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);
|
||||
|
||||
10
components/hmi_link/CMakeLists.txt
Normal file
10
components/hmi_link/CMakeLists.txt
Normal 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
|
||||
)
|
||||
26
components/hmi_link/include/hmi_link.h
Normal file
26
components/hmi_link/include/hmi_link.h
Normal 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
|
||||
406
components/hmi_link/src/hmi_link.c
Normal file
406
components/hmi_link/src/hmi_link.c
Normal 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, &s, "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
0
components/led/CMakeLists.txt
Executable file → Normal file
0
components/led/include/led.h
Executable file → Normal file
0
components/led/include/led.h
Executable file → Normal file
0
components/led/include/ledc_driver.h
Executable file → Normal file
0
components/led/include/ledc_driver.h
Executable file → Normal file
4
components/led/src/led.c
Executable file → Normal file
4
components/led/src/led.c
Executable file → Normal 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
0
components/led/src/ledc_driver.c
Executable file → Normal file
0
components/loadbalancer/CMakeLists.txt
Executable file → Normal file
0
components/loadbalancer/CMakeLists.txt
Executable file → Normal file
0
components/loadbalancer/include/grid_limiter.h
Executable file → Normal file
0
components/loadbalancer/include/grid_limiter.h
Executable file → Normal file
0
components/loadbalancer/include/input_filter.h
Executable file → Normal file
0
components/loadbalancer/include/input_filter.h
Executable file → Normal file
21
components/loadbalancer/include/loadbalancer.h
Executable file → Normal file
21
components/loadbalancer/include/loadbalancer.h
Executable file → Normal 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);
|
||||
|
||||
@@ -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,42 +12,52 @@ 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 {
|
||||
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;
|
||||
} loadbalancer_master_limit_event_t;
|
||||
|
||||
// SLAVE: tem slave_id
|
||||
typedef struct {
|
||||
/*
|
||||
* SLAVE: includes slave_id.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
uint8_t slave_id;
|
||||
uint16_t max_current;
|
||||
int64_t timestamp_us;
|
||||
} loadbalancer_slave_limit_event_t;
|
||||
|
||||
typedef struct {
|
||||
typedef struct
|
||||
{
|
||||
uint8_t slave_id;
|
||||
bool charging;
|
||||
float hw_max_current;
|
||||
|
||||
0
components/loadbalancer/include/pv_optimizer.h
Executable file → Normal file
0
components/loadbalancer/include/pv_optimizer.h
Executable file → Normal file
0
components/loadbalancer/src/grid_limiter.c
Executable file → Normal file
0
components/loadbalancer/src/grid_limiter.c
Executable file → Normal file
0
components/loadbalancer/src/input_filter.c
Executable file → Normal file
0
components/loadbalancer/src/input_filter.c
Executable file → Normal file
620
components/loadbalancer/src/loadbalancer.c
Executable file → Normal file
620
components/loadbalancer/src/loadbalancer.c
Executable file → Normal 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
0
components/loadbalancer/src/pv_optimizer.c
Executable file → Normal file
1
components/meter_manager/CMakeLists.txt
Executable file → Normal file
1
components/meter_manager/CMakeLists.txt
Executable file → Normal 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
|
||||
|
||||
0
components/meter_manager/driver/meter_ade7758/ade7758.c
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/ade7758.c
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/ade7758.h
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/ade7758.h
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/meter_ade7758.c
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/meter_ade7758.c
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/meter_ade7758.h
Executable file → Normal file
0
components/meter_manager/driver/meter_ade7758/meter_ade7758.h
Executable file → Normal 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,13 +164,243 @@ 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)
|
||||
{
|
||||
for (uint8_t i = 0; i < DDS661_MAX_INSTANCES; ++i)
|
||||
{
|
||||
if (dds661_instances[i].registered)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static esp_err_t dds661_rebuild_descriptors_locked(void)
|
||||
{
|
||||
num_device_parameters_dds661 = 0;
|
||||
dds661_descriptor_dirty = true;
|
||||
|
||||
for (uint8_t inst_idx = 0; inst_idx < DDS661_MAX_INSTANCES; ++inst_idx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
.mode = MB_MODE_RTU,
|
||||
.baudrate = MB_DEV_SPEED,
|
||||
.parity = UART_PARITY_EVEN, // DDS-661: 9600 8E1
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Pinos e parâmetros básicos
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
(void)mbc_master_destroy();
|
||||
return err;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
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;
|
||||
|
||||
// Valores lidos
|
||||
float v = 0.0f; // V
|
||||
float i = 0.0f; // A
|
||||
float pf = 0.0f; // -
|
||||
@@ -107,26 +408,27 @@ static void serial_mdb_task(void *param)
|
||||
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};
|
||||
int32_t watt[3] = {0};
|
||||
|
||||
while (1)
|
||||
{
|
||||
for (uint16_t cid = 0; cid < num_device_parameters_dds661; cid++)
|
||||
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, "get_cid_info(%u) failed: %s", cid, esp_err_to_name(err));
|
||||
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, "CID %u (%s): null data_ptr", cid, desc->param_key);
|
||||
ESP_LOGE(TAG, "%s CID %u (%s): null data_ptr", inst->source, cid, desc->param_key);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -134,24 +436,23 @@ static void serial_mdb_task(void *param)
|
||||
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));
|
||||
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;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
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]);
|
||||
|
||||
float val = 0.0f;
|
||||
const float val = *(float *)data_ptr;
|
||||
got_any_value = true;
|
||||
|
||||
val = *(float *)data_ptr;
|
||||
ESP_LOGD(TAG, "%s %s: %.3f %s", inst->source, desc->param_key, val, desc->param_units);
|
||||
|
||||
ESP_LOGD(TAG, "%s: %.3f %s", desc->param_key, val, desc->param_units);
|
||||
|
||||
switch (cid)
|
||||
switch (local_cid)
|
||||
{
|
||||
case CID_VOLTAGE:
|
||||
v = val;
|
||||
@@ -170,11 +471,30 @@ static void serial_mdb_task(void *param)
|
||||
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;
|
||||
|
||||
/*
|
||||
* 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:
|
||||
@@ -187,66 +507,97 @@ static void serial_mdb_task(void *param)
|
||||
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,
|
||||
.source = "GRID",
|
||||
.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));
|
||||
|
||||
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
|
||||
vTaskDelay(UPDATE_INTERVAL);
|
||||
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)
|
||||
{
|
||||
if (is_initialized)
|
||||
{
|
||||
ESP_LOGW(TAG, "meter_dds661 already initialized");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
return meter_dds661_grid_init();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "meter_dds661_init");
|
||||
esp_err_t meter_dds661_grid_init(void)
|
||||
{
|
||||
return dds661_register_instance(DDS661_SLOT_GRID);
|
||||
}
|
||||
|
||||
mb_communication_info_t comm = {
|
||||
.port = MB_PORT_NUM,
|
||||
.mode = MB_MODE_RTU,
|
||||
.baudrate = MB_DEV_SPEED,
|
||||
.parity = UART_PARITY_EVEN, // DDS-661: 9600 8E1
|
||||
};
|
||||
|
||||
void *handler = NULL;
|
||||
ESP_ERROR_CHECK(mbc_master_init(MB_PORT_SERIAL_MASTER, &handler));
|
||||
ESP_ERROR_CHECK(mbc_master_setup(&comm));
|
||||
|
||||
// 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));
|
||||
|
||||
// >>> IMPORTANTE: start antes do set_mode <<<
|
||||
ESP_ERROR_CHECK(mbc_master_start());
|
||||
|
||||
// Só agora muda para RS485 half duplex
|
||||
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX));
|
||||
|
||||
// (opcional) logs de debug Modbus
|
||||
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;
|
||||
esp_err_t meter_dds661_evse_init(void)
|
||||
{
|
||||
return dds661_register_instance(DDS661_SLOT_EVSE);
|
||||
}
|
||||
|
||||
esp_err_t meter_dds661_start(void)
|
||||
@@ -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)
|
||||
meter_dds661_grid_stop();
|
||||
}
|
||||
|
||||
void meter_dds661_grid_stop(void)
|
||||
{
|
||||
ESP_LOGW(TAG, "meter_dds661 not initialized");
|
||||
return;
|
||||
dds661_unregister_instance(DDS661_SLOT_GRID);
|
||||
}
|
||||
ESP_LOGI(TAG, "Stopping meter_dds661");
|
||||
|
||||
// 1) Destrói o master primeiro
|
||||
esp_err_t err = mbc_master_destroy();
|
||||
if (err != ESP_OK)
|
||||
void meter_dds661_evse_stop(void)
|
||||
{
|
||||
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;
|
||||
dds661_unregister_instance(DDS661_SLOT_EVSE);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
760
components/meter_manager/driver/meter_modbus/meter_dds665.c
Normal file
760
components/meter_manager/driver/meter_modbus/meter_dds665.c
Normal 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);
|
||||
}
|
||||
58
components/meter_manager/driver/meter_modbus/meter_dds665.h
Normal file
58
components/meter_manager/driver/meter_modbus/meter_dds665.h
Normal 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
|
||||
0
components/meter_manager/driver/meter_modbus/meter_dts024m.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts024m.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts024m.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts024m.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts6619.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts6619.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts6619.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_dts6619.h
Executable file → Normal 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,17 +483,17 @@ 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));
|
||||
for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; local_cid++)
|
||||
{
|
||||
const uint16_t cid = inst->cid_base + local_cid;
|
||||
|
||||
while (1)
|
||||
{
|
||||
for (uint16_t cid = 0; cid < num_device_parameters_ea777; 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;
|
||||
}
|
||||
|
||||
@@ -185,7 +501,7 @@ static void serial_mdb_ea777_task(void *param)
|
||||
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
|
||||
err = mbc_master_get_parameter(cid,
|
||||
@@ -203,7 +519,9 @@ static void serial_mdb_ea777_task(void *param)
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
switch (cid)
|
||||
got_any_value = true;
|
||||
|
||||
switch (local_cid)
|
||||
{
|
||||
case CID_EA777_L1_VOLTAGE:
|
||||
v[0] = ((float)raw16) * 0.1f;
|
||||
@@ -226,9 +544,7 @@ static void serial_mdb_ea777_task(void *param)
|
||||
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)
|
||||
total_active_power_w = (int32_t)raw16;
|
||||
break;
|
||||
|
||||
case CID_EA777_PF_L1:
|
||||
@@ -246,34 +562,55 @@ static void serial_mdb_ea777_task(void *param)
|
||||
break;
|
||||
|
||||
case CID_EA777_TOTAL_ACTIVE_E:
|
||||
total_kwh = ((float)raw32) * 0.01f;
|
||||
{
|
||||
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 (cid=%u) -> raw16=%u raw32=%u",
|
||||
desc->param_key, cid,
|
||||
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, "CID %u (%s) read failed: %s",
|
||||
cid, desc->param_key, esp_err_to_name(err));
|
||||
ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s",
|
||||
inst->source, 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]),
|
||||
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;
|
||||
@@ -287,94 +624,138 @@ static void serial_mdb_ea777_task(void *param)
|
||||
}
|
||||
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
|
||||
|
||||
meter_ea777_post_event(v, i, p_int, freq, pf_avg, total_kwh);
|
||||
vTaskDelay(UPDATE_INTERVAL);
|
||||
}
|
||||
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))
|
||||
esp_err_t meter_ea777_grid_init(void)
|
||||
{
|
||||
uart_driver_delete(MB_PORT_NUM);
|
||||
ESP_LOGI(TAG, "UART driver deleted");
|
||||
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,
|
||||
task_should_run = true;
|
||||
BaseType_t ok = xTaskCreate(serial_mdb_ea777_task,
|
||||
"meter_ea777_task",
|
||||
4096, NULL, 3, &meter_task);
|
||||
ESP_LOGI(TAG, "EA777 task started");
|
||||
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)
|
||||
meter_ea777_grid_stop();
|
||||
}
|
||||
|
||||
void meter_ea777_grid_stop(void)
|
||||
{
|
||||
ESP_LOGW(TAG, "Not initialized, skipping stop");
|
||||
return;
|
||||
ea777_unregister_instance(EA777_SLOT_GRID);
|
||||
}
|
||||
|
||||
if (meter_task)
|
||||
void meter_ea777_evse_stop(void)
|
||||
{
|
||||
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");
|
||||
ea777_unregister_instance(EA777_SLOT_EVSE);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
0
components/meter_manager/driver/meter_modbus/meter_orno.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno513.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno513.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno513.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno513.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno516.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno516.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno516.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/meter_orno516.h
Executable file → Normal file
628
components/meter_manager/driver/meter_modbus/meter_orno526.c
Executable file → Normal file
628
components/meter_manager/driver/meter_modbus/meter_orno526.c
Executable file → Normal 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;
|
||||
(void)param;
|
||||
|
||||
while (task_should_run)
|
||||
{
|
||||
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)
|
||||
{
|
||||
const mb_parameter_descriptor_t *desc = NULL;
|
||||
|
||||
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)
|
||||
{
|
||||
for (uint16_t cid = 0; cid < num_device_parameters_orno526; cid++)
|
||||
{
|
||||
err = mbc_master_get_cid_info(cid, &desc);
|
||||
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))
|
||||
{
|
||||
// Frequência é U16 (1 registo), escala = /10.0
|
||||
uint16_t raw16 = *(uint16_t *)data_ptr;
|
||||
val = raw16 / 10.0f;
|
||||
frequency_hz = val;
|
||||
if (orno526_is_16bit_cid(cid))
|
||||
{
|
||||
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);
|
||||
vTaskDelay(POLL_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
|
||||
valid_mask |= (1UL << cid);
|
||||
|
||||
ESP_LOGD(TAG, "%s %s: %.3f %s",
|
||||
meter_source,
|
||||
desc->param_key,
|
||||
(double)value,
|
||||
desc->param_units);
|
||||
|
||||
switch (cid)
|
||||
{
|
||||
case CID_L1_VOLTAGE:
|
||||
voltage[0] = val;
|
||||
case CID_ACTIVE_ENERGY:
|
||||
active_energy_kwh = value;
|
||||
break;
|
||||
case CID_L1_CURRENT:
|
||||
current[0] = val;
|
||||
|
||||
case CID_REACTIVE_ENERGY:
|
||||
reactive_energy_kvarh = value;
|
||||
break;
|
||||
|
||||
case CID_ACTIVE_POWER:
|
||||
watt[0] = (int)lrintf(val);
|
||||
watt[1] = watt[2] = watt[0];
|
||||
watt[0] = (int32_t)lrintf(value);
|
||||
break;
|
||||
|
||||
case CID_ACTIVE_ENERGY:
|
||||
energy = val; // já em kWh (raw/100)
|
||||
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;
|
||||
|
||||
// CID_FREQUENCY já atualiza 'frequency_hz' acima
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "CID %u (%s) read failed: %s", cid, desc->param_key, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
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);
|
||||
vTaskDelay(UPDATE_INTERVAL);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
esp_err_t meter_orno526_init(void)
|
||||
vTaskDelay(UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "OR-WE-526 task stopped");
|
||||
meter_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
39
components/meter_manager/driver/meter_modbus/meter_orno526.h
Executable file → Normal file
39
components/meter_manager/driver/meter_modbus/meter_orno526.h
Executable file → Normal 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
|
||||
}
|
||||
|
||||
0
components/meter_manager/driver/meter_modbus/modbus_params.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/modbus_params.c
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/modbus_params.h
Executable file → Normal file
0
components/meter_manager/driver/meter_modbus/modbus_params.h
Executable file → Normal file
11
components/meter_manager/driver/meter_zigbee/meter_zigbee.c
Executable file → Normal file
11
components/meter_manager/driver/meter_zigbee/meter_zigbee.c
Executable file → Normal 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));
|
||||
|
||||
0
components/meter_manager/driver/meter_zigbee/meter_zigbee.h
Executable file → Normal file
0
components/meter_manager/driver/meter_zigbee/meter_zigbee.h
Executable file → Normal file
11
components/meter_manager/include/meter_manager.h
Executable file → Normal file
11
components/meter_manager/include/meter_manager.h
Executable file → Normal 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
|
||||
*/
|
||||
|
||||
@@ -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,42 +157,106 @@ 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,
|
||||
.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),
|
||||
@@ -202,14 +267,21 @@ esp_err_t meter_manager_init(void)
|
||||
|
||||
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
0
components/network/CMakeLists.txt
Executable file → Normal file
0
components/network/include/network.h
Executable file → Normal file
0
components/network/include/network.h
Executable file → Normal file
0
components/network/src/network.c
Executable file → Normal file
0
components/network/src/network.c
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user