From 2a803fcef16d76dedd639fb68cb097b574501f5a Mon Sep 17 00:00:00 2001 From: PlxEV Date: Wed, 22 Jul 2026 15:36:04 +0100 Subject: [PATCH] chore: snapshot before shared RS485 bus integration --- .gitignore | 0 CMakeLists.txt | 0 LICENSE.md | 0 README.md | 0 cfg/custom/board.cfg | 0 cfg/esp32devkitc/board.cfg | 114 ++- cfg/esp32s2da/board.cfg | 0 components/auth/CMakeLists.txt | 0 components/auth/idf_component.yml | 0 components/auth/include/auth.h | 0 components/auth/include/wiegand.h | 0 components/auth/include/wiegand_reader.h | 0 components/auth/src/auth.c | 0 components/auth/src/auth_types.c | 0 components/auth/src/wiegand.c | 0 components/auth/src/wiegand_reader.c | 0 components/buzzer/CMakeLists.txt | 0 components/buzzer/idf_component.yml | 0 components/buzzer/include/buzzer.h | 0 components/buzzer/src/buzzer.c | 0 components/config/CMakeLists.txt | 0 components/config/board_config.c | 25 +- components/config/include/board_config.h | 10 + components/evse/CMakeLists.txt | 2 +- components/evse/evse_api.c | 0 components/evse/evse_config.c | 0 components/evse/evse_core.c | 0 components/evse/evse_error.c | 0 components/evse/evse_events.c | 0 components/evse/evse_fsm.c | 0 components/evse/evse_hardware.c | 0 components/evse/evse_limits.c | 0 components/evse/evse_manager.c | 72 ++ components/evse/evse_pilot.c | 109 ++- components/evse/evse_state.c | 0 components/evse/include/evse_api.h | 0 components/evse/include/evse_config.h | 0 components/evse/include/evse_error.h | 0 components/evse/include/evse_events.h | 0 components/evse/include/evse_fsm.h | 0 components/evse/include/evse_hardware.h | 0 components/evse/include/evse_limits.h | 0 components/evse/include/evse_manager.h | 0 components/evse/include/evse_pilot.h | 0 components/evse/include/evse_state.h | 0 components/evse_link/CMakeLists.txt | 0 components/evse_link/include/evse_link.h | 0 .../evse_link/include/evse_link_framing.h | 35 +- components/evse_link/src/evse_link.c | 0 components/evse_link/src/evse_link_framing.c | 21 +- components/evse_link/src/evse_link_slave.c | 100 +-- components/hmi_link/CMakeLists.txt | 10 + components/hmi_link/include/hmi_link.h | 26 + components/hmi_link/src/hmi_link.c | 406 ++++++++++ components/led/CMakeLists.txt | 0 components/led/include/led.h | 0 components/led/include/ledc_driver.h | 0 components/led/src/led.c | 4 +- components/led/src/ledc_driver.c | 0 components/loadbalancer/CMakeLists.txt | 0 .../loadbalancer/include/grid_limiter.h | 0 .../loadbalancer/include/input_filter.h | 0 .../loadbalancer/include/loadbalancer.h | 21 + .../include/loadbalancer_events.h | 64 +- .../loadbalancer/include/pv_optimizer.h | 0 components/loadbalancer/src/grid_limiter.c | 0 components/loadbalancer/src/input_filter.c | 0 components/loadbalancer/src/loadbalancer.c | 620 +++++++++++--- components/loadbalancer/src/pv_optimizer.c | 0 components/meter_manager/CMakeLists.txt | 1 + .../driver/meter_ade7758/ade7758.c | 0 .../driver/meter_ade7758/ade7758.h | 0 .../driver/meter_ade7758/meter_ade7758.c | 0 .../driver/meter_ade7758/meter_ade7758.h | 0 .../driver/meter_modbus/meter_dds661.c | 678 ++++++++++++---- .../driver/meter_modbus/meter_dds661.h | 35 +- .../driver/meter_modbus/meter_dds665.c | 760 ++++++++++++++++++ .../driver/meter_modbus/meter_dds665.h | 58 ++ .../driver/meter_modbus/meter_dts024m.c | 0 .../driver/meter_modbus/meter_dts024m.h | 0 .../driver/meter_modbus/meter_dts6619.c | 0 .../driver/meter_modbus/meter_dts6619.h | 0 .../driver/meter_modbus/meter_ea777.c | 757 ++++++++++++----- .../driver/meter_modbus/meter_ea777.h | 42 +- .../driver/meter_modbus/meter_orno.h | 0 .../driver/meter_modbus/meter_orno513.c | 0 .../driver/meter_modbus/meter_orno513.h | 0 .../driver/meter_modbus/meter_orno516.c | 0 .../driver/meter_modbus/meter_orno516.h | 0 .../driver/meter_modbus/meter_orno526.c | 642 +++++++++++---- .../driver/meter_modbus/meter_orno526.h | 39 +- .../driver/meter_modbus/modbus_params.c | 0 .../driver/meter_modbus/modbus_params.h | 0 .../driver/meter_zigbee/meter_zigbee.c | 11 +- .../driver/meter_zigbee/meter_zigbee.h | 0 .../meter_manager/include/meter_manager.h | 11 + components/meter_manager/src/meter_manager.c | 200 +++-- components/network/CMakeLists.txt | 0 components/network/include/network.h | 0 components/network/src/network.c | 0 components/ocpp/CMakeLists.txt | 0 components/ocpp/include/ocpp.h | 0 components/ocpp/src/ocpp.c | 63 +- components/ocpp/src/ocpp_events.c | 0 components/peripherals/CMakeLists.txt | 0 components/peripherals/include/ac_relay.h | 0 components/peripherals/include/adc.h | 3 + .../peripherals/include/adc121s021_dma.h | 0 components/peripherals/include/lm75a.h | 0 components/peripherals/include/ntc_sensor.h | 0 components/peripherals/include/peripherals.h | 0 components/peripherals/include/proximity.h | 0 components/peripherals/include/rcm.h | 0 components/peripherals/include/socket_lock.h | 0 components/peripherals/include/temp_sensor.h | 0 components/peripherals/src/ac_relay.c | 0 components/peripherals/src/adc.c | 34 + components/peripherals/src/adc121s021_dma.c | 0 components/peripherals/src/lm75a.c | 0 components/peripherals/src/ntc_sensor.c | 8 +- components/peripherals/src/peripherals.c | 0 components/peripherals/src/proximity.c | 19 +- components/peripherals/src/rcm.c | 0 components/peripherals/src/socket_lock.c | 0 components/peripherals/src/temp_sensor.c | 0 components/protocols/CMakeLists.txt | 0 components/protocols/include/mqtt.h | 17 +- components/protocols/include/protocols.h | 0 components/protocols/src/mqtt.c | 252 +++--- components/protocols/src/protocols.c | 0 components/rest_api/CMakeLists.txt | 2 +- components/rest_api/include/auth_api.h | 0 .../rest_api/include/evse_settings_api.h | 0 components/rest_api/include/network_api.h | 0 components/rest_api/include/ocpp_api.h | 0 .../rest_api/include/power_settings_api.h | 23 + components/rest_api/include/rest_main.h | 0 .../rest_api/include/scheduler_settings_api.h | 0 components/rest_api/src/auth_api.c | 0 components/rest_api/src/evse_settings_api.c | 73 +- .../rest_api/src/loadbalancing_settings_api.c | 698 ++++++++++++++-- components/rest_api/src/network_api.c | 0 components/rest_api/src/ocpp_api.c | 0 components/rest_api/src/power_settings_api.c | 692 ++++++++++++++++ components/rest_api/src/rest_main.c | 4 +- .../rest_api/src/scheduler_settings_api.c | 0 .../webfolder/assets/index-BSlegejp.js | 51 ++ .../webfolder/assets/index-CBPnRb7F.js | 51 -- ...{index-5y1tcufv.css => index-vUXH8POW.css} | 2 +- components/rest_api/webfolder/index.html | 4 +- components/scheduler/CMakeLists.txt | 0 components/scheduler/idf_component.yml | 0 components/scheduler/include/scheduler.h | 0 components/scheduler/src/scheduler.c | 0 components/scheduler/src/scheduler_types.c | 0 components/spi_bus_manager/CMakeLists.txt | 0 .../spi_bus_manager/include/spi_bus_manager.h | 0 .../spi_bus_manager/src/spi_bus_manager.c | 0 components/storage_service/CMakeLists.txt | 0 .../storage_service/include/storage_service.h | 0 .../storage_service/src/storage_service.c | 0 main/CMakeLists.txt | 0 main/Kconfig.projbuild | 0 main/component.mk | 0 main/idf_component.yml | 0 main/main.c | 5 +- partitions.csv | 3 +- 167 files changed, 5749 insertions(+), 1128 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 CMakeLists.txt mode change 100755 => 100644 LICENSE.md mode change 100755 => 100644 README.md mode change 100755 => 100644 cfg/custom/board.cfg mode change 100755 => 100644 cfg/esp32devkitc/board.cfg mode change 100755 => 100644 cfg/esp32s2da/board.cfg mode change 100755 => 100644 components/auth/CMakeLists.txt mode change 100755 => 100644 components/auth/idf_component.yml mode change 100755 => 100644 components/auth/include/auth.h mode change 100755 => 100644 components/auth/include/wiegand.h mode change 100755 => 100644 components/auth/include/wiegand_reader.h mode change 100755 => 100644 components/auth/src/auth.c mode change 100755 => 100644 components/auth/src/auth_types.c mode change 100755 => 100644 components/auth/src/wiegand.c mode change 100755 => 100644 components/auth/src/wiegand_reader.c mode change 100755 => 100644 components/buzzer/CMakeLists.txt mode change 100755 => 100644 components/buzzer/idf_component.yml mode change 100755 => 100644 components/buzzer/include/buzzer.h mode change 100755 => 100644 components/buzzer/src/buzzer.c mode change 100755 => 100644 components/config/CMakeLists.txt mode change 100755 => 100644 components/config/board_config.c mode change 100755 => 100644 components/config/include/board_config.h mode change 100755 => 100644 components/evse/CMakeLists.txt mode change 100755 => 100644 components/evse/evse_api.c mode change 100755 => 100644 components/evse/evse_config.c mode change 100755 => 100644 components/evse/evse_core.c mode change 100755 => 100644 components/evse/evse_error.c mode change 100755 => 100644 components/evse/evse_events.c mode change 100755 => 100644 components/evse/evse_fsm.c mode change 100755 => 100644 components/evse/evse_hardware.c mode change 100755 => 100644 components/evse/evse_limits.c mode change 100755 => 100644 components/evse/evse_manager.c mode change 100755 => 100644 components/evse/evse_pilot.c mode change 100755 => 100644 components/evse/evse_state.c mode change 100755 => 100644 components/evse/include/evse_api.h mode change 100755 => 100644 components/evse/include/evse_config.h mode change 100755 => 100644 components/evse/include/evse_error.h mode change 100755 => 100644 components/evse/include/evse_events.h mode change 100755 => 100644 components/evse/include/evse_fsm.h mode change 100755 => 100644 components/evse/include/evse_hardware.h mode change 100755 => 100644 components/evse/include/evse_limits.h mode change 100755 => 100644 components/evse/include/evse_manager.h mode change 100755 => 100644 components/evse/include/evse_pilot.h mode change 100755 => 100644 components/evse/include/evse_state.h mode change 100755 => 100644 components/evse_link/CMakeLists.txt mode change 100755 => 100644 components/evse_link/include/evse_link.h mode change 100755 => 100644 components/evse_link/src/evse_link.c create mode 100644 components/hmi_link/CMakeLists.txt create mode 100644 components/hmi_link/include/hmi_link.h create mode 100644 components/hmi_link/src/hmi_link.c mode change 100755 => 100644 components/led/CMakeLists.txt mode change 100755 => 100644 components/led/include/led.h mode change 100755 => 100644 components/led/include/ledc_driver.h mode change 100755 => 100644 components/led/src/led.c mode change 100755 => 100644 components/led/src/ledc_driver.c mode change 100755 => 100644 components/loadbalancer/CMakeLists.txt mode change 100755 => 100644 components/loadbalancer/include/grid_limiter.h mode change 100755 => 100644 components/loadbalancer/include/input_filter.h mode change 100755 => 100644 components/loadbalancer/include/loadbalancer.h mode change 100755 => 100644 components/loadbalancer/include/pv_optimizer.h mode change 100755 => 100644 components/loadbalancer/src/grid_limiter.c mode change 100755 => 100644 components/loadbalancer/src/input_filter.c mode change 100755 => 100644 components/loadbalancer/src/loadbalancer.c mode change 100755 => 100644 components/loadbalancer/src/pv_optimizer.c mode change 100755 => 100644 components/meter_manager/CMakeLists.txt mode change 100755 => 100644 components/meter_manager/driver/meter_ade7758/ade7758.c mode change 100755 => 100644 components/meter_manager/driver/meter_ade7758/ade7758.h mode change 100755 => 100644 components/meter_manager/driver/meter_ade7758/meter_ade7758.c mode change 100755 => 100644 components/meter_manager/driver/meter_ade7758/meter_ade7758.h create mode 100644 components/meter_manager/driver/meter_modbus/meter_dds665.c create mode 100644 components/meter_manager/driver/meter_modbus/meter_dds665.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_dts024m.c mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_dts024m.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_dts6619.c mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_dts6619.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno513.c mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno513.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno516.c mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno516.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno526.c mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/meter_orno526.h mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/modbus_params.c mode change 100755 => 100644 components/meter_manager/driver/meter_modbus/modbus_params.h mode change 100755 => 100644 components/meter_manager/driver/meter_zigbee/meter_zigbee.c mode change 100755 => 100644 components/meter_manager/driver/meter_zigbee/meter_zigbee.h mode change 100755 => 100644 components/meter_manager/include/meter_manager.h mode change 100755 => 100644 components/network/CMakeLists.txt mode change 100755 => 100644 components/network/include/network.h mode change 100755 => 100644 components/network/src/network.c mode change 100755 => 100644 components/ocpp/CMakeLists.txt mode change 100755 => 100644 components/ocpp/include/ocpp.h mode change 100755 => 100644 components/ocpp/src/ocpp.c mode change 100755 => 100644 components/ocpp/src/ocpp_events.c mode change 100755 => 100644 components/peripherals/CMakeLists.txt mode change 100755 => 100644 components/peripherals/include/ac_relay.h mode change 100755 => 100644 components/peripherals/include/adc.h mode change 100755 => 100644 components/peripherals/include/adc121s021_dma.h mode change 100755 => 100644 components/peripherals/include/lm75a.h mode change 100755 => 100644 components/peripherals/include/ntc_sensor.h mode change 100755 => 100644 components/peripherals/include/peripherals.h mode change 100755 => 100644 components/peripherals/include/proximity.h mode change 100755 => 100644 components/peripherals/include/rcm.h mode change 100755 => 100644 components/peripherals/include/socket_lock.h mode change 100755 => 100644 components/peripherals/include/temp_sensor.h mode change 100755 => 100644 components/peripherals/src/ac_relay.c mode change 100755 => 100644 components/peripherals/src/adc.c mode change 100755 => 100644 components/peripherals/src/adc121s021_dma.c mode change 100755 => 100644 components/peripherals/src/lm75a.c mode change 100755 => 100644 components/peripherals/src/ntc_sensor.c mode change 100755 => 100644 components/peripherals/src/peripherals.c mode change 100755 => 100644 components/peripherals/src/proximity.c mode change 100755 => 100644 components/peripherals/src/rcm.c mode change 100755 => 100644 components/peripherals/src/socket_lock.c mode change 100755 => 100644 components/peripherals/src/temp_sensor.c mode change 100755 => 100644 components/protocols/CMakeLists.txt mode change 100755 => 100644 components/protocols/include/mqtt.h mode change 100755 => 100644 components/protocols/include/protocols.h mode change 100755 => 100644 components/protocols/src/mqtt.c mode change 100755 => 100644 components/protocols/src/protocols.c mode change 100755 => 100644 components/rest_api/CMakeLists.txt mode change 100755 => 100644 components/rest_api/include/auth_api.h mode change 100755 => 100644 components/rest_api/include/evse_settings_api.h mode change 100755 => 100644 components/rest_api/include/network_api.h mode change 100755 => 100644 components/rest_api/include/ocpp_api.h create mode 100644 components/rest_api/include/power_settings_api.h mode change 100755 => 100644 components/rest_api/include/rest_main.h mode change 100755 => 100644 components/rest_api/include/scheduler_settings_api.h mode change 100755 => 100644 components/rest_api/src/auth_api.c mode change 100755 => 100644 components/rest_api/src/evse_settings_api.c mode change 100755 => 100644 components/rest_api/src/network_api.c mode change 100755 => 100644 components/rest_api/src/ocpp_api.c create mode 100644 components/rest_api/src/power_settings_api.c mode change 100755 => 100644 components/rest_api/src/rest_main.c mode change 100755 => 100644 components/rest_api/src/scheduler_settings_api.c create mode 100644 components/rest_api/webfolder/assets/index-BSlegejp.js delete mode 100644 components/rest_api/webfolder/assets/index-CBPnRb7F.js rename components/rest_api/webfolder/assets/{index-5y1tcufv.css => index-vUXH8POW.css} (56%) mode change 100755 => 100644 components/scheduler/CMakeLists.txt mode change 100755 => 100644 components/scheduler/idf_component.yml mode change 100755 => 100644 components/scheduler/include/scheduler.h mode change 100755 => 100644 components/scheduler/src/scheduler.c mode change 100755 => 100644 components/scheduler/src/scheduler_types.c mode change 100755 => 100644 components/spi_bus_manager/CMakeLists.txt mode change 100755 => 100644 components/spi_bus_manager/include/spi_bus_manager.h mode change 100755 => 100644 components/spi_bus_manager/src/spi_bus_manager.c mode change 100755 => 100644 components/storage_service/CMakeLists.txt mode change 100755 => 100644 components/storage_service/include/storage_service.h mode change 100755 => 100644 components/storage_service/src/storage_service.c mode change 100755 => 100644 main/CMakeLists.txt mode change 100755 => 100644 main/Kconfig.projbuild mode change 100755 => 100644 main/component.mk mode change 100755 => 100644 main/idf_component.yml mode change 100755 => 100644 main/main.c mode change 100755 => 100644 partitions.csv diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/CMakeLists.txt b/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/LICENSE.md b/LICENSE.md old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/cfg/custom/board.cfg b/cfg/custom/board.cfg old mode 100755 new mode 100644 diff --git a/cfg/esp32devkitc/board.cfg b/cfg/esp32devkitc/board.cfg old mode 100755 new mode 100644 index 6e8037c..95e0a63 --- a/cfg/esp32devkitc/board.cfg +++ b/cfg/esp32devkitc/board.cfg @@ -1,32 +1,115 @@ +# ChargeFlow board.cfg +# ------------------------------------------------------------ +# Este ficheiro suporta duas variantes de hardware: +# +# 1) Board antiga -> Pilot no ADC interno do ESP32 +# 2) Board recente -> Pilot no ADC externo ADC121S021 +# +# Para mudar de board, alterar apenas a secção "SELEÇÃO DE PERFIL". +# As restantes opções são comuns às duas boards. +# +# IMPORTANTE: +# - Não deixar duas linhas iguais ativas ao mesmo tempo. +# - Linhas começadas por # são comentários. +# - Na board com ADC externo, PILOT_ADC_SOURCE pode ser omitido, +# porque o firmware usa ADC121S021 por defeito. +# ------------------------------------------------------------ + + +# ============================================================ +# SELEÇÃO DE PERFIL +# ============================================================ + +# ------------------------------------------------------------ +# PERFIL ATIVO: Board antiga com ADC interno ESP32 +# ------------------------------------------------------------ +#led_red_GPIO=13 +#BUZZER_GPIO=21 +#PILOT_ADC_SOURCE=internal + +# ------------------------------------------------------------ +# PERFIL ALTERNATIVO: Board recente com ADC externo ADC121S021 +# Para usar esta board: +# 1. Comentar as 3 linhas do perfil ativo acima +# 2. Descomentar as 3 linhas abaixo +# ------------------------------------------------------------ +led_red_GPIO=26 +BUZZER_GPIO=27 +PILOT_ADC_SOURCE=adc121 + + +# ============================================================ +# CONFIGURAÇÃO COMUM +# ============================================================ + DEVICE_NAME=ChargeFlow -#LEDs + +# ------------------------------------------------------------ +# LEDs +# ------------------------------------------------------------ led_blue=y led_blue_GPIO=14 + led_red=y -led_red_GPIO=26 +# led_red_GPIO é definido na SELEÇÃO DE PERFIL + led_green=y led_green_GPIO=12 -#BUZZER -BUZZER=y -BUZZER_GPIO=27 -#Button +# ------------------------------------------------------------ +# BUZZER +# ------------------------------------------------------------ +BUZZER=y +# BUZZER_GPIO é definido na SELEÇÃO DE PERFIL + + +# ------------------------------------------------------------ +# Botão Wi-Fi +# ------------------------------------------------------------ BUTTON_WIFI_GPIO=32 -#Pilot + +# ------------------------------------------------------------ +# Pilot / Control Pilot +# ------------------------------------------------------------ +# PILOT_ADC_SOURCE é definido na SELEÇÃO DE PERFIL. +# +# Valores possíveis: +# internal -> usa ADC interno do ESP32 +# adc121 -> usa ADC externo ADC121S021 +# +# Board antiga: +# PILOT_ADC_SOURCE=internal +# +# Board recente: +# PILOT_ADC_SOURCE=adc121 +# ou omitir a linha, porque o default do firmware é ADC121S021. +# +# Nota: +# PILOT_ADC_CHANNEL é canal ADC, não GPIO. +# Em ESP32 clássico, ADC1_CH6 normalmente corresponde ao GPIO34. +# ------------------------------------------------------------ PILOT_PWM_GPIO=33 PILOT_ADC_CHANNEL=6 + +# Thresholds em mV no pino ADC/saída do circuito de medição PILOT_DOWN_THRESHOLD_12=3000 PILOT_DOWN_THRESHOLD_9=2600 PILOT_DOWN_THRESHOLD_6=2200 PILOT_DOWN_THRESHOLD_3=1950 PILOT_DOWN_THRESHOLD_N12=500 -#Proximity + +# ------------------------------------------------------------ +# Proximity +# ------------------------------------------------------------ PROXIMITY=y + +# Em ESP32 clássico, ADC1_CH3 normalmente corresponde ao GPIO39. PROXIMITY_ADC_CHANNEL=3 + PROXIMITY_DOWN_THRESHOLD_8=2450 PROXIMITY_DOWN_THRESHOLD_10=2050 PROXIMITY_DOWN_THRESHOLD_13=1650 @@ -34,11 +117,20 @@ PROXIMITY_DOWN_THRESHOLD_20=820 PROXIMITY_DOWN_THRESHOLD_25=610 PROXIMITY_DOWN_THRESHOLD_32=430 -#AC relay + +# ------------------------------------------------------------ +# Relé AC +# ------------------------------------------------------------ AC_RELAY_GPIO=25 -#Cable lock + +# ------------------------------------------------------------ +# Cable lock +# ------------------------------------------------------------ SOCKET_LOCK=n + +# Como SOCKET_LOCK=n, estes GPIOs podem ficar vazios. +# Se SOCKET_LOCK=y, preencher sempre valores válidos. SOCKET_LOCK_A_GPIO= SOCKET_LOCK_B_GPIO= -SOCKET_LOCK_DETECTION_GPIO= \ No newline at end of file +SOCKET_LOCK_DETECTION_GPIO= diff --git a/cfg/esp32s2da/board.cfg b/cfg/esp32s2da/board.cfg old mode 100755 new mode 100644 diff --git a/components/auth/CMakeLists.txt b/components/auth/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/auth/idf_component.yml b/components/auth/idf_component.yml old mode 100755 new mode 100644 diff --git a/components/auth/include/auth.h b/components/auth/include/auth.h old mode 100755 new mode 100644 diff --git a/components/auth/include/wiegand.h b/components/auth/include/wiegand.h old mode 100755 new mode 100644 diff --git a/components/auth/include/wiegand_reader.h b/components/auth/include/wiegand_reader.h old mode 100755 new mode 100644 diff --git a/components/auth/src/auth.c b/components/auth/src/auth.c old mode 100755 new mode 100644 diff --git a/components/auth/src/auth_types.c b/components/auth/src/auth_types.c old mode 100755 new mode 100644 diff --git a/components/auth/src/wiegand.c b/components/auth/src/wiegand.c old mode 100755 new mode 100644 diff --git a/components/auth/src/wiegand_reader.c b/components/auth/src/wiegand_reader.c old mode 100755 new mode 100644 diff --git a/components/buzzer/CMakeLists.txt b/components/buzzer/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/buzzer/idf_component.yml b/components/buzzer/idf_component.yml old mode 100755 new mode 100644 diff --git a/components/buzzer/include/buzzer.h b/components/buzzer/include/buzzer.h old mode 100755 new mode 100644 diff --git a/components/buzzer/src/buzzer.c b/components/buzzer/src/buzzer.c old mode 100755 new mode 100644 diff --git a/components/config/CMakeLists.txt b/components/config/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/config/board_config.c b/components/config/board_config.c old mode 100755 new mode 100644 index a5b4b53..1d4264f --- a/components/config/board_config.c +++ b/components/config/board_config.c @@ -1,4 +1,5 @@ #include +#include #include #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); diff --git a/components/config/include/board_config.h b/components/config/include/board_config.h old mode 100755 new mode 100644 index 99fe001..e5a4856 --- a/components/config/include/board_config.h +++ b/components/config/include/board_config.h @@ -1,6 +1,9 @@ #ifndef BOARD_CONFIG_H_ #define BOARD_CONFIG_H_ +#include +#include + #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; diff --git a/components/evse/CMakeLists.txt b/components/evse/CMakeLists.txt old mode 100755 new mode 100644 index 1a4453b..f5a6cef --- a/components/evse/CMakeLists.txt +++ b/components/evse/CMakeLists.txt @@ -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 ) \ No newline at end of file diff --git a/components/evse/evse_api.c b/components/evse/evse_api.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_config.c b/components/evse/evse_config.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_core.c b/components/evse/evse_core.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_error.c b/components/evse/evse_error.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_events.c b/components/evse/evse_events.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_fsm.c b/components/evse/evse_fsm.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_hardware.c b/components/evse/evse_hardware.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_limits.c b/components/evse/evse_limits.c old mode 100755 new mode 100644 diff --git a/components/evse/evse_manager.c b/components/evse/evse_manager.c old mode 100755 new mode 100644 index 1132f8c..115b381 --- a/components/evse/evse_manager.c +++ b/components/evse/evse_manager.c @@ -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."); diff --git a/components/evse/evse_pilot.c b/components/evse/evse_pilot.c old mode 100755 new mode 100644 index d70fcac..b821724 --- a/components/evse/evse_pilot.c +++ b/components/evse/evse_pilot.c @@ -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) diff --git a/components/evse/evse_state.c b/components/evse/evse_state.c old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_api.h b/components/evse/include/evse_api.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_config.h b/components/evse/include/evse_config.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_error.h b/components/evse/include/evse_error.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_events.h b/components/evse/include/evse_events.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_fsm.h b/components/evse/include/evse_fsm.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_hardware.h b/components/evse/include/evse_hardware.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_limits.h b/components/evse/include/evse_limits.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_manager.h b/components/evse/include/evse_manager.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_pilot.h b/components/evse/include/evse_pilot.h old mode 100755 new mode 100644 diff --git a/components/evse/include/evse_state.h b/components/evse/include/evse_state.h old mode 100755 new mode 100644 diff --git a/components/evse_link/CMakeLists.txt b/components/evse_link/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/evse_link/include/evse_link.h b/components/evse_link/include/evse_link.h old mode 100755 new mode 100644 diff --git a/components/evse_link/include/evse_link_framing.h b/components/evse_link/include/evse_link_framing.h index 718159c..4285a68 100644 --- a/components/evse_link/include/evse_link_framing.h +++ b/components/evse_link/include/evse_link_framing.h @@ -5,21 +5,36 @@ #include #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 diff --git a/components/evse_link/src/evse_link.c b/components/evse_link/src/evse_link.c old mode 100755 new mode 100644 diff --git a/components/evse_link/src/evse_link_framing.c b/components/evse_link/src/evse_link_framing.c index d32eda0..d03ed76 100644 --- a/components/evse_link/src/evse_link_framing.c +++ b/components/evse_link/src/evse_link_framing.c @@ -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; } diff --git a/components/evse_link/src/evse_link_slave.c b/components/evse_link/src/evse_link_slave.c index a8b523d..f47ce85 100644 --- a/components/evse_link/src/evse_link_slave.c +++ b/components/evse_link/src/evse_link_slave.c @@ -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); diff --git a/components/hmi_link/CMakeLists.txt b/components/hmi_link/CMakeLists.txt new file mode 100644 index 0000000..5d66c3a --- /dev/null +++ b/components/hmi_link/CMakeLists.txt @@ -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 +) diff --git a/components/hmi_link/include/hmi_link.h b/components/hmi_link/include/hmi_link.h new file mode 100644 index 0000000..a7bdf5b --- /dev/null +++ b/components/hmi_link/include/hmi_link.h @@ -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 diff --git a/components/hmi_link/src/hmi_link.c b/components/hmi_link/src/hmi_link.c new file mode 100644 index 0000000..aeb9508 --- /dev/null +++ b/components/hmi_link/src/hmi_link.c @@ -0,0 +1,406 @@ +#include "hmi_link.h" + +#include +#include +#include +#include + +#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; +} diff --git a/components/led/CMakeLists.txt b/components/led/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/led/include/led.h b/components/led/include/led.h old mode 100755 new mode 100644 diff --git a/components/led/include/ledc_driver.h b/components/led/include/ledc_driver.h old mode 100755 new mode 100644 diff --git a/components/led/src/led.c b/components/led/src/led.c old mode 100755 new mode 100644 index 97187d5..57ca936 --- a/components/led/src/led.c +++ b/components/led/src/led.c @@ -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: diff --git a/components/led/src/ledc_driver.c b/components/led/src/ledc_driver.c old mode 100755 new mode 100644 diff --git a/components/loadbalancer/CMakeLists.txt b/components/loadbalancer/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/loadbalancer/include/grid_limiter.h b/components/loadbalancer/include/grid_limiter.h old mode 100755 new mode 100644 diff --git a/components/loadbalancer/include/input_filter.h b/components/loadbalancer/include/input_filter.h old mode 100755 new mode 100644 diff --git a/components/loadbalancer/include/loadbalancer.h b/components/loadbalancer/include/loadbalancer.h old mode 100755 new mode 100644 index 67dfecf..2e6637d --- a/components/loadbalancer/include/loadbalancer.h +++ b/components/loadbalancer/include/loadbalancer.h @@ -9,11 +9,32 @@ extern "C" { #include #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); diff --git a/components/loadbalancer/include/loadbalancer_events.h b/components/loadbalancer/include/loadbalancer_events.h index 913a097..8d900bd 100644 --- a/components/loadbalancer/include/loadbalancer_events.h +++ b/components/loadbalancer/include/loadbalancer_events.h @@ -2,9 +2,9 @@ #pragma once #include "esp_event.h" -#include + #include -#include "esp_timer.h" +#include #ifdef __cplusplus extern "C" { @@ -12,49 +12,59 @@ extern "C" { ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS); -typedef enum { +typedef enum +{ LOADBALANCER_EVENT_INIT = 0, LOADBALANCER_EVENT_STATE_CHANGED, - LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT, - // IMPORTANT: eventos separados e payloads diferentes + /* + * Current limit events. + * + * MASTER and SLAVE use different payloads intentionally. + */ LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT, LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT, + /* + * Status received from slave connectors. + */ LOADBALANCER_EVENT_SLAVE_STATUS } loadbalancer_event_id_t; -typedef struct { - bool enabled; - int64_t timestamp_us; +typedef struct +{ + bool enabled; + int64_t timestamp_us; } loadbalancer_state_event_t; -typedef struct { - float limit; - int64_t timestamp_us; -} loadbalancer_global_limit_event_t; - -// MASTER: NÃO tem slave_id -typedef struct { +/* + * MASTER: no slave_id. + */ +typedef struct +{ uint16_t max_current; - int64_t timestamp_us; + int64_t timestamp_us; } loadbalancer_master_limit_event_t; -// SLAVE: tem slave_id -typedef struct { - uint8_t slave_id; +/* + * SLAVE: includes slave_id. + */ +typedef struct +{ + uint8_t slave_id; uint16_t max_current; - int64_t timestamp_us; + int64_t timestamp_us; } loadbalancer_slave_limit_event_t; -typedef struct { - uint8_t slave_id; - bool charging; - float hw_max_current; - float runtime_current; - int64_t timestamp_us; +typedef struct +{ + uint8_t slave_id; + bool charging; + float hw_max_current; + float runtime_current; + int64_t timestamp_us; } loadbalancer_slave_status_event_t; #ifdef __cplusplus } -#endif +#endif \ No newline at end of file diff --git a/components/loadbalancer/include/pv_optimizer.h b/components/loadbalancer/include/pv_optimizer.h old mode 100755 new mode 100644 diff --git a/components/loadbalancer/src/grid_limiter.c b/components/loadbalancer/src/grid_limiter.c old mode 100755 new mode 100644 diff --git a/components/loadbalancer/src/input_filter.c b/components/loadbalancer/src/input_filter.c old mode 100755 new mode 100644 diff --git a/components/loadbalancer/src/loadbalancer.c b/components/loadbalancer/src/loadbalancer.c old mode 100755 new mode 100644 index 4493c89..07e24f5 --- a/components/loadbalancer/src/loadbalancer.c +++ b/components/loadbalancer/src/loadbalancer.c @@ -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, diff --git a/components/loadbalancer/src/pv_optimizer.c b/components/loadbalancer/src/pv_optimizer.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/CMakeLists.txt b/components/meter_manager/CMakeLists.txt old mode 100755 new mode 100644 index 0260ea5..be0b0f9 --- a/components/meter_manager/CMakeLists.txt +++ b/components/meter_manager/CMakeLists.txt @@ -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 diff --git a/components/meter_manager/driver/meter_ade7758/ade7758.c b/components/meter_manager/driver/meter_ade7758/ade7758.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_ade7758/ade7758.h b/components/meter_manager/driver/meter_ade7758/ade7758.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_ade7758/meter_ade7758.c b/components/meter_manager/driver/meter_ade7758/meter_ade7758.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_ade7758/meter_ade7758.h b/components/meter_manager/driver/meter_ade7758/meter_ade7758.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_dds661.c b/components/meter_manager/driver/meter_modbus/meter_dds661.c index 3a9c051..919ca4a 100755 --- a/components/meter_manager/driver/meter_modbus/meter_dds661.c +++ b/components/meter_manager/driver/meter_modbus/meter_dds661.c @@ -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 #include #include @@ -22,19 +33,27 @@ #define MB_UART_RXD 16 #define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 +// ======= DDS661 no mesmo bus ======= +#define DDS661_GRID_SLAVE_ID 1 +#define DDS661_EVSE_SLAVE_ID 2 + #define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS) #define POLL_INTERVAL (120 / portTICK_PERIOD_MS) +#define DDS661_LOCK_TIMEOUT pdMS_TO_TICKS(10000) // ======= Helpers típicos do teu projeto ======= #define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1)) #define STR(x) ((const char *)(x)) #define OPTS(min, max, step) {.opt1 = min, .opt2 = max, .opt3 = step} +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) // ======= Estado ======= static bool is_initialized = false; +static volatile bool task_should_run = false; static TaskHandle_t meter_task = NULL; +static SemaphoreHandle_t dds661_lock = NULL; -// ======= CIDs (sequenciais) ======= +// ======= CIDs locais por meter ======= enum { CID_VOLTAGE = 0, @@ -46,6 +65,36 @@ enum CID_COUNT }; +typedef enum +{ + DDS661_SLOT_GRID = 0, + DDS661_SLOT_EVSE, + DDS661_MAX_INSTANCES +} dds661_slot_t; + +typedef struct +{ + bool registered; + const char *source; // "GRID" ou "EVSE" + uint8_t slave_id; // 1 ou 2 + uint16_t cid_base; // atribuído dinamicamente em dds661_rebuild_descriptors_locked() +} dds661_instance_t; + +static dds661_instance_t dds661_instances[DDS661_MAX_INSTANCES] = { + [DDS661_SLOT_GRID] = { + .registered = false, + .source = "GRID", + .slave_id = DDS661_GRID_SLAVE_ID, + .cid_base = 0, + }, + [DDS661_SLOT_EVSE] = { + .registered = false, + .source = "EVSE", + .slave_id = DDS661_EVSE_SLAVE_ID, + .cid_base = 0, + }, +}; + // ======= Mapa de registradores (Input Registers; FC=0x04) ======= // Endereços típicos para DDS-661 (float32): #define REG_VOLTAGE 0x0000 // V (float32) @@ -55,8 +104,8 @@ enum #define REG_FREQUENCY 0x0036 // Hz (float32) #define REG_E_ACTIVE_KWH 0x0100 // kWh (float32) -// ======= Tabela de parâmetros (Data Dictionary) ======= -const mb_parameter_descriptor_t device_parameters_dds661[] = { +// ======= Template de parâmetros para um DDS661 ======= +static const mb_parameter_descriptor_t dds661_param_template[CID_COUNT] = { {CID_VOLTAGE, "Voltage", "V", 1, MB_PARAM_INPUT, REG_VOLTAGE, 2, HOLD_OFFSET(l1_voltage), PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0, 300, 0.1), PAR_PERMS_READ}, @@ -82,8 +131,30 @@ const mb_parameter_descriptor_t device_parameters_dds661[] = { PARAM_TYPE_FLOAT_CDAB, 4, OPTS(0, 1000000, 0.01), PAR_PERMS_READ}, }; -const uint16_t num_device_parameters_dds661 = - sizeof(device_parameters_dds661) / sizeof(device_parameters_dds661[0]); +// O esp-modbus exige cid e param_key únicos no Data Dictionary. +// Como GRID e EVSE usam o mesmo mapa de registos, o param_key precisa de prefixo por instância. +static const char *const dds661_param_keys[DDS661_MAX_INSTANCES][CID_COUNT] = { + [DDS661_SLOT_GRID] = { + "GRID Voltage", + "GRID Current", + "GRID Active Power", + "GRID Power Factor", + "GRID Frequency", + "GRID Total Active Energy", + }, + [DDS661_SLOT_EVSE] = { + "EVSE Voltage", + "EVSE Current", + "EVSE Active Power", + "EVSE Power Factor", + "EVSE Frequency", + "EVSE Total Active Energy", + }, +}; + +static mb_parameter_descriptor_t device_parameters_dds661[DDS661_MAX_INSTANCES * CID_COUNT]; +static uint16_t num_device_parameters_dds661 = 0; +static bool dds661_descriptor_dirty = true; // ======= Ponteiro para buffer destino ======= static void *get_param_ptr(const mb_parameter_descriptor_t *param) @@ -93,125 +164,80 @@ static void *get_param_ptr(const mb_parameter_descriptor_t *param) return ((uint8_t *)&holding_reg_params + param->param_offset - 1); } -// ======= Tarefa de aquisição ======= -static void serial_mdb_task(void *param) +static bool dds661_has_registered_instance_locked(void) { - esp_err_t err; - const mb_parameter_descriptor_t *desc = NULL; - - // Valores lidos - float v = 0.0f; // V - float i = 0.0f; // A - float pf = 0.0f; // - - float hz = 0.0f; // Hz - float e_kwh = 0.0f; // kWh - float p_kw = 0.0f; // kW - - // Buffers para o evento - float voltage[3] = {0}; - float current[3] = {0}; - int watt[3] = {0}; - - while (1) + for (uint8_t i = 0; i < DDS661_MAX_INSTANCES; ++i) { - for (uint16_t cid = 0; cid < num_device_parameters_dds661; cid++) - { - err = mbc_master_get_cid_info(cid, &desc); - if (err != ESP_OK || !desc) - { - ESP_LOGE(TAG, "get_cid_info(%u) failed: %s", cid, esp_err_to_name(err)); - continue; - } - - void *data_ptr = get_param_ptr(desc); - if (!data_ptr) - { - ESP_LOGE(TAG, "CID %u (%s): null data_ptr", cid, desc->param_key); - continue; - } - - uint8_t type = 0; - err = mbc_master_get_parameter(cid, (char *)desc->param_key, (uint8_t *)data_ptr, &type); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "CID %u (%s) read failed: %s", cid, desc->param_key, esp_err_to_name(err)); - vTaskDelay(POLL_INTERVAL); - continue; - } - - // Dump dos bytes recebidos (4 bytes do float bruto) - uint8_t raw[4]; - memcpy(raw, data_ptr, 4); - ESP_LOGD(TAG, "CID %u (%s) raw bytes: %02X %02X %02X %02X", - cid, desc->param_key, raw[0], raw[1], raw[2], raw[3]); - - float val = 0.0f; - - val = *(float *)data_ptr; - - ESP_LOGD(TAG, "%s: %.3f %s", desc->param_key, val, desc->param_units); - - switch (cid) - { - case CID_VOLTAGE: - v = val; - voltage[0] = v; - break; - case CID_CURRENT: - i = val; - current[0] = i; - break; - case CID_POWER_FACTOR: - pf = val; - break; - case CID_FREQUENCY: - hz = val; - break; - case CID_ACTIVE_POWER_KW: - { - p_kw = val; - float p_w = p_kw * 1000.0f; - int pwi = (int)lrintf(p_w); - watt[0] = pwi; - watt[1] = pwi; - watt[2] = pwi; - break; - } - case CID_TOTAL_ACTIVE_ENERGY_KWH: - e_kwh = val; - break; - default: - break; - } - - vTaskDelay(POLL_INTERVAL); - } - - meter_event_data_t evt = { - .frequency = hz, - .power_factor = pf, - .total_energy = e_kwh, - .source = "GRID", - }; - memcpy(evt.vrms, voltage, sizeof(evt.vrms)); - memcpy(evt.irms, current, sizeof(evt.irms)); - memcpy(evt.watt, watt, sizeof(evt.watt)); - - esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); - vTaskDelay(UPDATE_INTERVAL); + if (dds661_instances[i].registered) + return true; } + return false; } -// ======= API pública ======= -esp_err_t meter_dds661_init(void) +static esp_err_t dds661_rebuild_descriptors_locked(void) { - if (is_initialized) + num_device_parameters_dds661 = 0; + dds661_descriptor_dirty = true; + + for (uint8_t inst_idx = 0; inst_idx < DDS661_MAX_INSTANCES; ++inst_idx) { - ESP_LOGW(TAG, "meter_dds661 already initialized"); - return ESP_ERR_INVALID_STATE; + dds661_instance_t *inst = &dds661_instances[inst_idx]; + if (!inst->registered) + continue; + + // Mantém os CIDs contíguos no descriptor ativo. + // Isto evita problemas em versões do esp-modbus que tratam CID como índice. + inst->cid_base = num_device_parameters_dds661; + + for (uint16_t local_cid = 0; local_cid < CID_COUNT; ++local_cid) + { + mb_parameter_descriptor_t *dst = &device_parameters_dds661[num_device_parameters_dds661++]; + *dst = dds661_param_template[local_cid]; + dst->cid = inst->cid_base + local_cid; + dst->param_key = dds661_param_keys[inst_idx][local_cid]; + dst->mb_slave_addr = inst->slave_id; + } } - ESP_LOGI(TAG, "meter_dds661_init"); + dds661_descriptor_dirty = true; + + ESP_LOGI(TAG, "DDS661 descriptor table prepared: %u parameters", num_device_parameters_dds661); + return ESP_OK; +} + +static esp_err_t dds661_apply_descriptors_locked(void) +{ + if (num_device_parameters_dds661 == 0) + return ESP_ERR_INVALID_STATE; + + if (!dds661_descriptor_dirty) + return ESP_OK; + + esp_err_t err = mbc_master_set_descriptor(device_parameters_dds661, num_device_parameters_dds661); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", esp_err_to_name(err)); + return err; + } + + dds661_descriptor_dirty = false; + ESP_LOGI(TAG, "DDS661 descriptor table applied: %u parameters", num_device_parameters_dds661); + return ESP_OK; +} + +static esp_err_t dds661_master_init_once(void) +{ + if (is_initialized) + return ESP_OK; + + if (!dds661_lock) + { + dds661_lock = xSemaphoreCreateMutex(); + if (!dds661_lock) + return ESP_ERR_NO_MEM; + } + + ESP_LOGI(TAG, "meter_dds661 Modbus master init"); mb_communication_info_t comm = { .port = MB_PORT_NUM, @@ -221,34 +247,359 @@ esp_err_t meter_dds661_init(void) }; void *handler = NULL; - ESP_ERROR_CHECK(mbc_master_init(MB_PORT_SERIAL_MASTER, &handler)); - ESP_ERROR_CHECK(mbc_master_setup(&comm)); + esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler); + if (err != ESP_OK) + return err; + + err = mbc_master_setup(&comm); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } // Pinos e parâmetros básicos - ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE)); - ESP_ERROR_CHECK(uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS)); - ESP_ERROR_CHECK(uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0)); - ESP_ERROR_CHECK(uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1)); + err = uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } - // >>> IMPORTANTE: start antes do set_mode <<< - ESP_ERROR_CHECK(mbc_master_start()); + err = uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + err = uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + err = uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + // IMPORTANTE: start antes do set_mode + err = mbc_master_start(); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } // Só agora muda para RS485 half duplex - ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); + err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } - // (opcional) logs de debug Modbus + // Logs de debug Modbus. Reduz para ESP_LOG_INFO/ESP_LOG_WARN em produção se necessário. esp_log_level_set("MB_CONTROLLER_MASTER", ESP_LOG_DEBUG); esp_log_level_set("MB_PORT_COMMON", ESP_LOG_DEBUG); esp_log_level_set("MB_SERIAL_MASTER", ESP_LOG_DEBUG); vTaskDelay(pdMS_TO_TICKS(5)); - ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_dds661, num_device_parameters_dds661)); - is_initialized = true; return ESP_OK; } +static esp_err_t dds661_register_instance(dds661_slot_t slot) +{ + if (slot >= DDS661_MAX_INSTANCES) + return ESP_ERR_INVALID_ARG; + + esp_err_t err = dds661_master_init_once(); + if (err != ESP_OK) + return err; + + if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) != pdTRUE) + return ESP_ERR_TIMEOUT; + + dds661_instances[slot].registered = true; + ESP_LOGI(TAG, "DDS661 %s registered on Modbus slave ID %u", + dds661_instances[slot].source, + dds661_instances[slot].slave_id); + + err = dds661_rebuild_descriptors_locked(); + xSemaphoreGive(dds661_lock); + + return err; +} + +static void dds661_shutdown_if_idle(void) +{ + bool any_registered = false; + + if (dds661_lock && xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) == pdTRUE) + { + any_registered = dds661_has_registered_instance_locked(); + xSemaphoreGive(dds661_lock); + } + + if (any_registered || !is_initialized) + return; + + ESP_LOGI(TAG, "No DDS661 instances registered; stopping shared Modbus master"); + + task_should_run = false; + for (int i = 0; i < 20 && meter_task != NULL; ++i) + { + vTaskDelay(pdMS_TO_TICKS(10)); + } + + if (meter_task != NULL) + { + ESP_LOGW(TAG, "DDS661 task did not exit in time; deleting it"); + vTaskDelete(meter_task); + meter_task = NULL; + } + + esp_err_t err = mbc_master_destroy(); + if (err != ESP_OK) + ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err)); + + if (uart_is_driver_installed(MB_PORT_NUM)) + uart_driver_delete(MB_PORT_NUM); + + is_initialized = false; + num_device_parameters_dds661 = 0; + dds661_descriptor_dirty = true; +} + +static void dds661_unregister_instance(dds661_slot_t slot) +{ + if (slot >= DDS661_MAX_INSTANCES) + return; + + if (!is_initialized || !dds661_lock) + return; + + if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) == pdTRUE) + { + ESP_LOGI(TAG, "DDS661 %s unregistered", dds661_instances[slot].source); + dds661_instances[slot].registered = false; + (void)dds661_rebuild_descriptors_locked(); + xSemaphoreGive(dds661_lock); + } + + dds661_shutdown_if_idle(); +} + +static void dds661_read_instance_locked(const dds661_instance_t *inst) +{ + esp_err_t err; + const mb_parameter_descriptor_t *desc = NULL; + + float v = 0.0f; // V + float i = 0.0f; // A + float pf = 0.0f; // - + float hz = 0.0f; // Hz + float e_kwh = 0.0f; // kWh + float p_kw = 0.0f; // kW + + float voltage[3] = {0}; + float current[3] = {0}; + int32_t watt[3] = {0}; + + bool got_any_value = false; + + for (uint16_t local_cid = 0; local_cid < CID_COUNT; local_cid++) + { + const uint16_t cid = inst->cid_base + local_cid; + + err = mbc_master_get_cid_info(cid, &desc); + if (err != ESP_OK || !desc) + { + ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", inst->source, cid, esp_err_to_name(err)); + continue; + } + + void *data_ptr = get_param_ptr(desc); + if (!data_ptr) + { + ESP_LOGE(TAG, "%s CID %u (%s): null data_ptr", inst->source, cid, desc->param_key); + continue; + } + + uint8_t type = 0; + err = mbc_master_get_parameter(cid, (char *)desc->param_key, (uint8_t *)data_ptr, &type); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s", + inst->source, cid, desc->param_key, esp_err_to_name(err)); + vTaskDelay(POLL_INTERVAL); + continue; + } + + uint8_t raw[4]; + memcpy(raw, data_ptr, sizeof(raw)); + ESP_LOGD(TAG, "%s CID %u (%s) raw bytes: %02X %02X %02X %02X", + inst->source, cid, desc->param_key, raw[0], raw[1], raw[2], raw[3]); + + const float val = *(float *)data_ptr; + got_any_value = true; + + ESP_LOGD(TAG, "%s %s: %.3f %s", inst->source, desc->param_key, val, desc->param_units); + + switch (local_cid) + { + case CID_VOLTAGE: + v = val; + voltage[0] = v; + break; + case CID_CURRENT: + i = val; + current[0] = i; + break; + case CID_POWER_FACTOR: + pf = val; + break; + case CID_FREQUENCY: + hz = val; + break; + case CID_ACTIVE_POWER_KW: + { + p_kw = val; + + /* + * DDS661 bidirecional: + * p_w > 0 => importação + * p_w < 0 => exportação + * + * Alguns modelos devolvem W apesar do registo estar nomeado como kW. + * Outros podem devolver kW. + * + * Não usar abs() no valor final, apenas na deteção de escala. + */ + float p_w = p_kw; + + const float apparent_w = fabsf(v * i); + + if (apparent_w > 1.0f && fabsf(p_w) < (apparent_w * 0.2f)) + { + // Exemplo: -0.723 kW -> -723 W + p_w *= 1000.0f; + } + + watt[0] = (int32_t)lrintf(p_w); + watt[1] = 0; + watt[2] = 0; + break; + } + case CID_TOTAL_ACTIVE_ENERGY_KWH: + e_kwh = val; + break; + default: + break; + } + + vTaskDelay(POLL_INTERVAL); + } + + if (!got_any_value) + { + ESP_LOGW(TAG, "%s no valid DDS661 values read in this cycle", inst->source); + return; + } + + meter_event_data_t evt = { + .source = inst->source, + .frequency = hz, + .power_factor = pf, + .total_energy = e_kwh, + .watt_total = watt[0] + watt[1] + watt[2], + .timestamp_us = esp_timer_get_time(), + }; + memcpy(evt.vrms, voltage, sizeof(evt.vrms)); + memcpy(evt.irms, current, sizeof(evt.irms)); + memcpy(evt.watt, watt, sizeof(evt.watt)); + + const int32_t p_total_w = watt[0] + watt[1] + watt[2]; + + ESP_LOGI(TAG, + "%s DDS661 event: V=%.1fV I=%.3fA P=%ldW E=%.3fkWh Hz=%.2f PF=%.3f", + inst->source, + voltage[0], + current[0], + (long)p_total_w, + e_kwh, + hz, + pf); + + esp_err_t post_err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); + if (post_err != ESP_OK) + ESP_LOGW(TAG, "%s failed to post meter event: %s", inst->source, esp_err_to_name(post_err)); +} + +// ======= Tarefa de aquisição ======= +static void serial_mdb_task(void *param) +{ + (void)param; + + while (task_should_run) + { + if (!is_initialized || !dds661_lock) + { + vTaskDelay(UPDATE_INTERVAL); + continue; + } + + bool had_instance = false; + + for (uint8_t slot = 0; slot < DDS661_MAX_INSTANCES && task_should_run; ++slot) + { + if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) != pdTRUE) + { + ESP_LOGW(TAG, "DDS661 task timeout waiting lock"); + continue; + } + + dds661_instance_t inst = dds661_instances[slot]; + if (inst.registered) + { + had_instance = true; + dds661_read_instance_locked(&inst); + } + + xSemaphoreGive(dds661_lock); + } + + vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500)); + } + + ESP_LOGI(TAG, "DDS661 task stopped"); + meter_task = NULL; + vTaskDelete(NULL); +} + +// ======= API pública ======= +// Compatibilidade: chamada antiga inicializa DDS661 como GRID/ID 1. +esp_err_t meter_dds661_init(void) +{ + return meter_dds661_grid_init(); +} + +esp_err_t meter_dds661_grid_init(void) +{ + return dds661_register_instance(DDS661_SLOT_GRID); +} + +esp_err_t meter_dds661_evse_init(void) +{ + return dds661_register_instance(DDS661_SLOT_EVSE); +} + esp_err_t meter_dds661_start(void) { if (!is_initialized) @@ -257,33 +608,64 @@ esp_err_t meter_dds661_start(void) return ESP_ERR_INVALID_STATE; } + if (!dds661_lock) + return ESP_ERR_INVALID_STATE; + + if (xSemaphoreTake(dds661_lock, DDS661_LOCK_TIMEOUT) != pdTRUE) + return ESP_ERR_TIMEOUT; + + const bool has_instance = dds661_has_registered_instance_locked(); + esp_err_t desc_err = ESP_OK; + if (has_instance) + desc_err = dds661_apply_descriptors_locked(); + xSemaphoreGive(dds661_lock); + + if (!has_instance) + { + ESP_LOGW(TAG, "meter_dds661 start ignored: no instances registered"); + return ESP_ERR_INVALID_STATE; + } + + if (desc_err != ESP_OK) + return desc_err; + if (meter_task == NULL) { - xTaskCreate(serial_mdb_task, "meter_dds661_task", 4096, NULL, 3, &meter_task); - ESP_LOGI(TAG, "meter_dds661 task started"); + task_should_run = true; + BaseType_t ok = xTaskCreate(serial_mdb_task, "meter_dds661_task", 4096, NULL, 3, &meter_task); + if (ok != pdPASS) + { + task_should_run = false; + meter_task = NULL; + return ESP_ERR_NO_MEM; + } + ESP_LOGI(TAG, "meter_dds661 shared task started"); } return ESP_OK; } +esp_err_t meter_dds661_grid_start(void) +{ + return meter_dds661_start(); +} + +esp_err_t meter_dds661_evse_start(void) +{ + return meter_dds661_start(); +} + void meter_dds661_stop(void) { - if (!is_initialized) - { - ESP_LOGW(TAG, "meter_dds661 not initialized"); - return; - } - ESP_LOGI(TAG, "Stopping meter_dds661"); - - // 1) Destrói o master primeiro - esp_err_t err = mbc_master_destroy(); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err)); - } - - // 2) Depois solta a UART - uart_driver_delete(MB_PORT_NUM); - - is_initialized = false; + meter_dds661_grid_stop(); +} + +void meter_dds661_grid_stop(void) +{ + dds661_unregister_instance(DDS661_SLOT_GRID); +} + +void meter_dds661_evse_stop(void) +{ + dds661_unregister_instance(DDS661_SLOT_EVSE); } diff --git a/components/meter_manager/driver/meter_modbus/meter_dds661.h b/components/meter_manager/driver/meter_modbus/meter_dds661.h index 3f4376d..4a829d3 100755 --- a/components/meter_manager/driver/meter_modbus/meter_dds661.h +++ b/components/meter_manager/driver/meter_modbus/meter_dds661.h @@ -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 } diff --git a/components/meter_manager/driver/meter_modbus/meter_dds665.c b/components/meter_manager/driver/meter_modbus/meter_dds665.c new file mode 100644 index 0000000..862b98a --- /dev/null +++ b/components/meter_manager/driver/meter_modbus/meter_dds665.c @@ -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 +#include +#include +#include +#include + +#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); +} diff --git a/components/meter_manager/driver/meter_modbus/meter_dds665.h b/components/meter_manager/driver/meter_modbus/meter_dds665.h new file mode 100644 index 0000000..22ad3ba --- /dev/null +++ b/components/meter_manager/driver/meter_modbus/meter_dds665.h @@ -0,0 +1,58 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#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 diff --git a/components/meter_manager/driver/meter_modbus/meter_dts024m.c b/components/meter_manager/driver/meter_modbus/meter_dts024m.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_dts024m.h b/components/meter_manager/driver/meter_modbus/meter_dts024m.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_dts6619.c b/components/meter_manager/driver/meter_modbus/meter_dts6619.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_dts6619.h b/components/meter_manager/driver/meter_modbus/meter_dts6619.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_ea777.c b/components/meter_manager/driver/meter_modbus/meter_ea777.c index d946fc3..1a1eb34 100755 --- a/components/meter_manager/driver/meter_modbus/meter_ea777.c +++ b/components/meter_manager/driver/meter_modbus/meter_ea777.c @@ -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 #include -#include "meter_ea777.h" +#include + +#include #define TAG "serial_mdb_ea777" @@ -20,17 +34,30 @@ #define MB_UART_RXD 16 #define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485 +// ===== EA777 no mesmo bus ===== +#define EA777_GRID_SLAVE_ID 1 +#define EA777_EVSE_SLAVE_ID 2 + // ===== Timings ===== #define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS) #define POLL_INTERVAL (200 / portTICK_PERIOD_MS) +#define EA777_LOCK_TIMEOUT pdMS_TO_TICKS(10000) // ===== Helpers ===== #define STR(fieldname) ((const char *)(fieldname)) #define OPTS(min_val, max_val, step_val) {.opt1 = min_val, .opt2 = max_val, .opt3 = step_val} +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) // ===== Estado ===== static bool is_initialized = false; +static volatile bool task_should_run = false; static TaskHandle_t meter_task = NULL; +static SemaphoreHandle_t ea777_lock = NULL; + +static inline uint32_t ea777_swap_words_u32(uint32_t x) +{ + return ((x & 0xFFFFu) << 16) | ((x >> 16) & 0xFFFFu); +} // ============================================================================ // ============ MAPA DE REGISTROS EA777 (Holding 0x03) ======================== @@ -45,7 +72,6 @@ static TaskHandle_t meter_task = NULL; #define EA777_L3CURRENT 0x0005 // Potência ativa total (W) #define EA777_TOTAL_ACTIVE_P 0x0007 -// (se quiser por fase, pode usar 0x0008/0x0009/0x000A) // Fator de potência por fase (0.001) #define EA777_PF_L1 0x0014 #define EA777_PF_L2 0x0015 @@ -56,7 +82,7 @@ static TaskHandle_t meter_task = NULL; #define EA777_TOTAL_ACTIVE_E 0x001D // ============================================================================ -// ============ CIDs ============ +// ============ CIDs locais por meter ============ enum { CID_EA777_L1_VOLTAGE = 0, @@ -71,11 +97,42 @@ enum CID_EA777_PF_L3, CID_EA777_FREQUENCY, CID_EA777_TOTAL_ACTIVE_E, + CID_EA777_COUNT, }; -// ======= Descritores (Holding registers) ======= +typedef enum +{ + EA777_SLOT_GRID = 0, + EA777_SLOT_EVSE, + EA777_MAX_INSTANCES +} ea777_slot_t; + +typedef struct +{ + bool registered; + const char *source; // "GRID" ou "EVSE" + uint8_t slave_id; // 1 ou 2 + uint16_t cid_base; // atribuído dinamicamente em ea777_rebuild_descriptors_locked() +} ea777_instance_t; + +static ea777_instance_t ea777_instances[EA777_MAX_INSTANCES] = { + [EA777_SLOT_GRID] = { + .registered = false, + .source = "GRID", + .slave_id = EA777_GRID_SLAVE_ID, + .cid_base = 0, + }, + [EA777_SLOT_EVSE] = { + .registered = false, + .source = "EVSE", + .slave_id = EA777_EVSE_SLAVE_ID, + .cid_base = 0, + }, +}; + +// ======= Template de descritores (Holding registers) ======= // Nota: param_offset = 0 -> não usamos holding_reg_params_t aqui. -const mb_parameter_descriptor_t device_parameters_ea777[] = { +static const mb_parameter_descriptor_t ea777_param_template[CID_EA777_COUNT] = { // Tensões (0.1 V) {CID_EA777_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1, MB_PARAM_HOLDING, EA777_L1VOLTAGE, 1, @@ -131,18 +188,278 @@ const mb_parameter_descriptor_t device_parameters_ea777[] = { 0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ}, }; -const uint16_t num_device_parameters_ea777 = - sizeof(device_parameters_ea777) / sizeof(device_parameters_ea777[0]); +// O esp-modbus exige cid e param_key únicos no Data Dictionary. +// Como GRID e EVSE usam o mesmo mapa de registos, o param_key precisa de prefixo por instância. +static const char *const ea777_param_keys[EA777_MAX_INSTANCES][CID_EA777_COUNT] = { + [EA777_SLOT_GRID] = { + "GRID L1 Voltage", + "GRID L2 Voltage", + "GRID L3 Voltage", + "GRID L1 Current", + "GRID L2 Current", + "GRID L3 Current", + "GRID Total Active Power", + "GRID L1 PF", + "GRID L2 PF", + "GRID L3 PF", + "GRID Frequency", + "GRID Total Active Energy", + }, + [EA777_SLOT_EVSE] = { + "EVSE L1 Voltage", + "EVSE L2 Voltage", + "EVSE L3 Voltage", + "EVSE L1 Current", + "EVSE L2 Current", + "EVSE L3 Current", + "EVSE Total Active Power", + "EVSE L1 PF", + "EVSE L2 PF", + "EVSE L3 PF", + "EVSE Frequency", + "EVSE Total Active Energy", + }, +}; -// ===== Post do evento de medição ===== -static void meter_ea777_post_event(float *voltage, float *current, int *power_w, - float freq_hz, float pf_avg, float total_kwh) +static mb_parameter_descriptor_t device_parameters_ea777[EA777_MAX_INSTANCES * CID_EA777_COUNT]; +static uint16_t num_device_parameters_ea777 = 0; +static bool ea777_descriptor_dirty = true; + +static bool ea777_has_registered_instance_locked(void) +{ + for (uint8_t i = 0; i < EA777_MAX_INSTANCES; ++i) + { + if (ea777_instances[i].registered) + return true; + } + return false; +} + +static esp_err_t ea777_rebuild_descriptors_locked(void) +{ + num_device_parameters_ea777 = 0; + ea777_descriptor_dirty = true; + + for (uint8_t inst_idx = 0; inst_idx < EA777_MAX_INSTANCES; ++inst_idx) + { + ea777_instance_t *inst = &ea777_instances[inst_idx]; + if (!inst->registered) + continue; + + // Mantém os CIDs contíguos no descriptor ativo. + // Isto evita problemas em versões do esp-modbus que tratam CID como índice. + inst->cid_base = num_device_parameters_ea777; + + for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; ++local_cid) + { + mb_parameter_descriptor_t *dst = &device_parameters_ea777[num_device_parameters_ea777++]; + *dst = ea777_param_template[local_cid]; + dst->cid = inst->cid_base + local_cid; + dst->param_key = ea777_param_keys[inst_idx][local_cid]; + dst->mb_slave_addr = inst->slave_id; + } + } + + ea777_descriptor_dirty = true; + + ESP_LOGI(TAG, "EA777 descriptor table prepared: %u parameters", num_device_parameters_ea777); + return ESP_OK; +} + +static esp_err_t ea777_apply_descriptors_locked(void) +{ + if (num_device_parameters_ea777 == 0) + return ESP_ERR_INVALID_STATE; + + if (!ea777_descriptor_dirty) + return ESP_OK; + + esp_err_t err = mbc_master_set_descriptor(device_parameters_ea777, num_device_parameters_ea777); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", esp_err_to_name(err)); + return err; + } + + ea777_descriptor_dirty = false; + ESP_LOGI(TAG, "EA777 descriptor table applied: %u parameters", num_device_parameters_ea777); + return ESP_OK; +} + +static esp_err_t ea777_master_init_once(void) +{ + if (is_initialized) + return ESP_OK; + + if (!ea777_lock) + { + ea777_lock = xSemaphoreCreateMutex(); + if (!ea777_lock) + return ESP_ERR_NO_MEM; + } + + if (uart_is_driver_installed(MB_PORT_NUM)) + { + uart_driver_delete(MB_PORT_NUM); + ESP_LOGI(TAG, "UART driver deleted before EA777 init"); + } + + (void)mbc_master_destroy(); + + ESP_LOGI(TAG, "meter_ea777 Modbus master init"); + + mb_communication_info_t comm = { + .port = MB_PORT_NUM, + .mode = MB_MODE_RTU, + .baudrate = MB_DEV_SPEED, + .parity = UART_PARITY_EVEN, + }; + + void *handler = NULL; + esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler); + if (err != ESP_OK) + return err; + + err = mbc_master_setup(&comm); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + // RS-485 half duplex + err = uart_set_pin(MB_PORT_NUM, + MB_UART_TXD, MB_UART_RXD, + MB_UART_RTS, UART_PIN_NO_CHANGE); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + err = mbc_master_start(); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX); + if (err != ESP_OK) + { + (void)mbc_master_destroy(); + return err; + } + + vTaskDelay(pdMS_TO_TICKS(50)); + + is_initialized = true; + ESP_LOGI(TAG, "EA777 Modbus master initialized (9600 8E1, Holding Reg 0x03)"); + return ESP_OK; +} + +static esp_err_t ea777_register_instance(ea777_slot_t slot) +{ + if (slot >= EA777_MAX_INSTANCES) + return ESP_ERR_INVALID_ARG; + + esp_err_t err = ea777_master_init_once(); + if (err != ESP_OK) + return err; + + if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE) + return ESP_ERR_TIMEOUT; + + ea777_instances[slot].registered = true; + ESP_LOGI(TAG, "EA777 %s registered on Modbus slave ID %u", + ea777_instances[slot].source, + ea777_instances[slot].slave_id); + + err = ea777_rebuild_descriptors_locked(); + xSemaphoreGive(ea777_lock); + + return err; +} + +static void ea777_shutdown_if_idle(void) +{ + bool any_registered = false; + + if (ea777_lock && xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE) + { + any_registered = ea777_has_registered_instance_locked(); + xSemaphoreGive(ea777_lock); + } + + if (any_registered || !is_initialized) + return; + + ESP_LOGI(TAG, "No EA777 instances registered; stopping shared Modbus master"); + + task_should_run = false; + for (int i = 0; i < 20 && meter_task != NULL; ++i) + { + vTaskDelay(pdMS_TO_TICKS(10)); + } + + if (meter_task != NULL) + { + ESP_LOGW(TAG, "EA777 task did not exit in time; deleting it"); + vTaskDelete(meter_task); + meter_task = NULL; + } + + esp_err_t err = mbc_master_destroy(); + if (err != ESP_OK) + ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err)); + + if (uart_is_driver_installed(MB_PORT_NUM)) + { + uart_driver_delete(MB_PORT_NUM); + ESP_LOGI(TAG, "UART driver deleted"); + } + + is_initialized = false; + num_device_parameters_ea777 = 0; + ea777_descriptor_dirty = true; +} + +static void ea777_unregister_instance(ea777_slot_t slot) +{ + if (slot >= EA777_MAX_INSTANCES) + return; + + if (!is_initialized || !ea777_lock) + return; + + if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) == pdTRUE) + { + ESP_LOGI(TAG, "EA777 %s unregistered", ea777_instances[slot].source); + ea777_instances[slot].registered = false; + (void)ea777_rebuild_descriptors_locked(); + xSemaphoreGive(ea777_lock); + } + + ea777_shutdown_if_idle(); +} + +static void meter_ea777_post_event(const ea777_instance_t *inst, + const float *voltage, + const float *current, + const int32_t *power_w, + int32_t total_power_w, + float freq_hz, + float pf_avg, + float total_kwh) { meter_event_data_t evt = { - .source = "GRID", + .source = inst->source, .frequency = freq_hz, .power_factor = pf_avg, - .total_energy = total_kwh}; + .total_energy = total_kwh, + .watt_total = total_power_w, + .timestamp_us = esp_timer_get_time(), + }; memcpy(evt.vrms, voltage, sizeof(evt.vrms)); memcpy(evt.irms, current, sizeof(evt.irms)); @@ -152,12 +469,11 @@ static void meter_ea777_post_event(float *voltage, float *current, int *power_w, &evt, sizeof(evt), portMAX_DELAY); if (err != ESP_OK) { - ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "%s falha ao emitir evento: %s", inst->source, esp_err_to_name(err)); } } -// ===== Task de polling ===== -static void serial_mdb_ea777_task(void *param) +static void ea777_read_instance_locked(const ea777_instance_t *inst) { esp_err_t err; const mb_parameter_descriptor_t *desc = NULL; @@ -167,214 +483,279 @@ static void serial_mdb_ea777_task(void *param) float pf[3] = {0}; float freq = 0.0f; float total_kwh = 0.0f; + int32_t total_active_power_w = 0; + bool got_any_value = false; - // pequeno settle antes da 1ª leitura - vTaskDelay(pdMS_TO_TICKS(200)); - - while (1) + for (uint16_t local_cid = 0; local_cid < CID_EA777_COUNT; local_cid++) { - for (uint16_t cid = 0; cid < num_device_parameters_ea777; cid++) + const uint16_t cid = inst->cid_base + local_cid; + + err = mbc_master_get_cid_info(cid, &desc); + if (err != ESP_OK || !desc) { - err = mbc_master_get_cid_info(cid, &desc); - if (err != ESP_OK || !desc) - { - continue; - } + ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", inst->source, cid, esp_err_to_name(err)); + continue; + } - uint8_t type = 0; - uint16_t raw16 = 0; - uint32_t raw32 = 0; + uint8_t type = 0; + uint16_t raw16 = 0; + uint32_t raw32 = 0; - void *value_ptr = (cid == CID_EA777_TOTAL_ACTIVE_E) ? (void *)&raw32 : (void *)&raw16; + void *value_ptr = (local_cid == CID_EA777_TOTAL_ACTIVE_E) ? (void *)&raw32 : (void *)&raw16; - // 1 retry simples em caso de timeout + // 1 retry simples em caso de timeout + err = mbc_master_get_parameter(cid, + (char *)desc->param_key, + (uint8_t *)value_ptr, + &type); + if (err == ESP_ERR_TIMEOUT) + { + vTaskDelay(pdMS_TO_TICKS(60)); err = mbc_master_get_parameter(cid, (char *)desc->param_key, (uint8_t *)value_ptr, &type); - if (err == ESP_ERR_TIMEOUT) - { - vTaskDelay(pdMS_TO_TICKS(60)); - err = mbc_master_get_parameter(cid, - (char *)desc->param_key, - (uint8_t *)value_ptr, - &type); - } - - if (err == ESP_OK) - { - switch (cid) - { - case CID_EA777_L1_VOLTAGE: - v[0] = ((float)raw16) * 0.1f; - break; - case CID_EA777_L2_VOLTAGE: - v[1] = ((float)raw16) * 0.1f; - break; - case CID_EA777_L3_VOLTAGE: - v[2] = ((float)raw16) * 0.1f; - break; - - case CID_EA777_L1_CURRENT: - i[0] = ((float)raw16) * 0.01f; - break; - case CID_EA777_L2_CURRENT: - i[1] = ((float)raw16) * 0.01f; - break; - case CID_EA777_L3_CURRENT: - i[2] = ((float)raw16) * 0.01f; - break; - - case CID_EA777_TOTAL_ACTIVE_P: - // guarda se quiser usar em debug; para o evento usamos - // aproximação por fase abaixo - // (poderia ser passado direto em power_w[0..2] também) - break; - - case CID_EA777_PF_L1: - pf[0] = ((float)raw16) * 0.001f; - break; - case CID_EA777_PF_L2: - pf[1] = ((float)raw16) * 0.001f; - break; - case CID_EA777_PF_L3: - pf[2] = ((float)raw16) * 0.001f; - break; - - case CID_EA777_FREQUENCY: - freq = ((float)raw16) * 0.01f; - break; - - case CID_EA777_TOTAL_ACTIVE_E: - total_kwh = ((float)raw32) * 0.01f; - break; - - default: - break; - } - - ESP_LOGD(TAG, "%s (cid=%u) -> raw16=%u raw32=%u", - desc->param_key, cid, - (unsigned int)raw16, - (unsigned int)raw32); - } - else - { - ESP_LOGE(TAG, "CID %u (%s) read failed: %s", - cid, desc->param_key, esp_err_to_name(err)); - } - - vTaskDelay(POLL_INTERVAL); } - // Potência por fase aproximada: P = V * I * PF - int p_int[3] = { - (int)(v[0] * i[0] * pf[0]), - (int)(v[1] * i[1] * pf[1]), - (int)(v[2] * i[2] * pf[2]), - }; - - // PF médio simples (ignora zeros) - float pf_sum = 0.0f; - int pf_cnt = 0; - for (int k = 0; k < 3; ++k) + if (err == ESP_OK) { - if (pf[k] != 0.0f) - { - pf_sum += pf[k]; - pf_cnt++; - } - } - float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f); + got_any_value = true; - meter_ea777_post_event(v, i, p_int, freq, pf_avg, total_kwh); - vTaskDelay(UPDATE_INTERVAL); + switch (local_cid) + { + case CID_EA777_L1_VOLTAGE: + v[0] = ((float)raw16) * 0.1f; + break; + case CID_EA777_L2_VOLTAGE: + v[1] = ((float)raw16) * 0.1f; + break; + case CID_EA777_L3_VOLTAGE: + v[2] = ((float)raw16) * 0.1f; + break; + + case CID_EA777_L1_CURRENT: + i[0] = ((float)raw16) * 0.01f; + break; + case CID_EA777_L2_CURRENT: + i[1] = ((float)raw16) * 0.01f; + break; + case CID_EA777_L3_CURRENT: + i[2] = ((float)raw16) * 0.01f; + break; + + case CID_EA777_TOTAL_ACTIVE_P: + total_active_power_w = (int32_t)raw16; + break; + + case CID_EA777_PF_L1: + pf[0] = ((float)raw16) * 0.001f; + break; + case CID_EA777_PF_L2: + pf[1] = ((float)raw16) * 0.001f; + break; + case CID_EA777_PF_L3: + pf[2] = ((float)raw16) * 0.001f; + break; + + case CID_EA777_FREQUENCY: + freq = ((float)raw16) * 0.01f; + break; + + case CID_EA777_TOTAL_ACTIVE_E: + { + uint32_t raw_e = ea777_swap_words_u32(raw32); + total_kwh = ((float)raw_e) * 0.01f; + + ESP_LOGI(TAG, + "%s EA777 energy raw=0x%08" PRIX32 + " swapped=0x%08" PRIX32 " => %.3f kWh", + inst->source, + raw32, + raw_e, + (double)total_kwh); + break; + } + + default: + break; + } + + ESP_LOGD(TAG, "%s %s (cid=%u) -> raw16=%u raw32=%u", + inst->source, desc->param_key, cid, + (unsigned int)raw16, + (unsigned int)raw32); + } + else + { + ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s", + inst->source, cid, desc->param_key, esp_err_to_name(err)); + } + + vTaskDelay(POLL_INTERVAL); } + + if (!got_any_value) + { + ESP_LOGW(TAG, "%s no valid EA777 values read in this cycle", inst->source); + return; + } + + // Potência por fase aproximada: P = V * I * PF. + // O EA777 também fornece potência ativa total; esta é usada em watt_total quando existe. + int32_t p_int[3] = { + (int32_t)lrintf(v[0] * i[0] * pf[0]), + (int32_t)lrintf(v[1] * i[1] * pf[1]), + (int32_t)lrintf(v[2] * i[2] * pf[2]), + }; + + const int32_t p_sum = p_int[0] + p_int[1] + p_int[2]; + const int32_t watt_total = (total_active_power_w != 0) ? total_active_power_w : p_sum; + + // PF médio simples (ignora zeros) + float pf_sum = 0.0f; + int pf_cnt = 0; + for (int k = 0; k < 3; ++k) + { + if (pf[k] != 0.0f) + { + pf_sum += pf[k]; + pf_cnt++; + } + } + float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f); + + meter_ea777_post_event(inst, v, i, p_int, watt_total, freq, pf_avg, total_kwh); } +// ===== Task de polling partilhada ===== +static void serial_mdb_ea777_task(void *param) +{ + (void)param; + + // pequeno settle antes da 1ª leitura + vTaskDelay(pdMS_TO_TICKS(200)); + + while (task_should_run) + { + if (!is_initialized || !ea777_lock) + { + vTaskDelay(UPDATE_INTERVAL); + continue; + } + + bool had_instance = false; + + for (uint8_t slot = 0; slot < EA777_MAX_INSTANCES && task_should_run; ++slot) + { + if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE) + { + ESP_LOGW(TAG, "EA777 task timeout waiting lock"); + continue; + } + + ea777_instance_t inst = ea777_instances[slot]; + if (inst.registered) + { + had_instance = true; + ea777_read_instance_locked(&inst); + } + + xSemaphoreGive(ea777_lock); + } + + vTaskDelay(had_instance ? UPDATE_INTERVAL : pdMS_TO_TICKS(500)); + } + + ESP_LOGI(TAG, "EA777 task stopped"); + meter_task = NULL; + vTaskDelete(NULL); +} + +// ===== API pública ===== +// Compatibilidade: chamada antiga inicializa EA777 como GRID/ID 1. esp_err_t meter_ea777_init(void) { - if (is_initialized) - { - ESP_LOGW(TAG, "Already initialized"); - return ESP_ERR_INVALID_STATE; - } + return meter_ea777_grid_init(); +} - if (uart_is_driver_installed(MB_PORT_NUM)) - { - uart_driver_delete(MB_PORT_NUM); - ESP_LOGI(TAG, "UART driver deleted"); - } +esp_err_t meter_ea777_grid_init(void) +{ + return ea777_register_instance(EA777_SLOT_GRID); +} - (void)mbc_master_destroy(); - - mb_communication_info_t comm = { - .port = MB_PORT_NUM, - .mode = MB_MODE_RTU, - .baudrate = MB_DEV_SPEED, - .parity = UART_PARITY_EVEN}; - - void *handler = NULL; - ESP_ERROR_CHECK(mbc_master_init(MB_PORT_SERIAL_MASTER, &handler)); - ESP_ERROR_CHECK(mbc_master_setup(&comm)); - - // >>> RS-485 half duplex - ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, - MB_UART_TXD, MB_UART_RXD, - MB_UART_RTS, UART_PIN_NO_CHANGE)); - - ESP_ERROR_CHECK(mbc_master_start()); - ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); - vTaskDelay(pdMS_TO_TICKS(50)); - - ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777, - num_device_parameters_ea777)); - - is_initialized = true; - ESP_LOGI(TAG, "EA777 Modbus master initialized (9600 8E1, Holding Reg 0x03)"); - return ESP_OK; +esp_err_t meter_ea777_evse_init(void) +{ + return ea777_register_instance(EA777_SLOT_EVSE); } esp_err_t meter_ea777_start(void) { if (!is_initialized) { - ESP_LOGE(TAG, "Not initialized"); + ESP_LOGE(TAG, "meter_ea777 not initialized"); return ESP_ERR_INVALID_STATE; } + + if (!ea777_lock) + return ESP_ERR_INVALID_STATE; + + if (xSemaphoreTake(ea777_lock, EA777_LOCK_TIMEOUT) != pdTRUE) + return ESP_ERR_TIMEOUT; + + const bool has_instance = ea777_has_registered_instance_locked(); + esp_err_t desc_err = ESP_OK; + if (has_instance) + desc_err = ea777_apply_descriptors_locked(); + xSemaphoreGive(ea777_lock); + + if (!has_instance) + { + ESP_LOGW(TAG, "meter_ea777 start ignored: no instances registered"); + return ESP_ERR_INVALID_STATE; + } + + if (desc_err != ESP_OK) + return desc_err; + if (meter_task == NULL) { - xTaskCreate(serial_mdb_ea777_task, - "meter_ea777_task", - 4096, NULL, 3, &meter_task); - ESP_LOGI(TAG, "EA777 task started"); + task_should_run = true; + BaseType_t ok = xTaskCreate(serial_mdb_ea777_task, + "meter_ea777_task", + 4096, NULL, 3, &meter_task); + if (ok != pdPASS) + { + task_should_run = false; + meter_task = NULL; + return ESP_ERR_NO_MEM; + } + ESP_LOGI(TAG, "meter_ea777 shared task started"); } + return ESP_OK; } +esp_err_t meter_ea777_grid_start(void) +{ + return meter_ea777_start(); +} + +esp_err_t meter_ea777_evse_start(void) +{ + return meter_ea777_start(); +} + void meter_ea777_stop(void) { - if (!is_initialized) - { - ESP_LOGW(TAG, "Not initialized, skipping stop"); - return; - } - - if (meter_task) - { - vTaskDelete(meter_task); - meter_task = NULL; - ESP_LOGI(TAG, "EA777 task stopped"); - } - - (void)mbc_master_destroy(); - - if (uart_is_driver_installed(MB_PORT_NUM)) - { - uart_driver_delete(MB_PORT_NUM); - ESP_LOGI(TAG, "UART driver deleted"); - } - - is_initialized = false; - ESP_LOGI(TAG, "Meter EA777 cleaned up"); + meter_ea777_grid_stop(); +} + +void meter_ea777_grid_stop(void) +{ + ea777_unregister_instance(EA777_SLOT_GRID); +} + +void meter_ea777_evse_stop(void) +{ + ea777_unregister_instance(EA777_SLOT_EVSE); } diff --git a/components/meter_manager/driver/meter_modbus/meter_ea777.h b/components/meter_manager/driver/meter_modbus/meter_ea777.h index 8fc9e2f..ecfb645 100755 --- a/components/meter_manager/driver/meter_modbus/meter_ea777.h +++ b/components/meter_manager/driver/meter_modbus/meter_ea777.h @@ -10,26 +10,52 @@ extern "C" { #endif /** - * @brief Inicializa o driver do medidor EA777 (UART RS485, Modbus, registradores). - * - * @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro. + * @brief Inicializa o driver EA777 em modo compatível antigo: GRID / slave ID 1. */ esp_err_t meter_ea777_init(void); /** - * @brief Inicia a tarefa de leitura de dados do medidor EA777. - * - * @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro. + * @brief Regista EA777 como meter GRID no slave ID 1. + */ +esp_err_t meter_ea777_grid_init(void); + +/** + * @brief Regista EA777 como meter EVSE no slave ID 2. + */ +esp_err_t meter_ea777_evse_init(void); + +/** + * @brief Inicia a task partilhada de leitura EA777. */ esp_err_t meter_ea777_start(void); /** - * @brief Para a tarefa de leitura e limpa os dados internos do medidor EA777. + * @brief Inicia a task partilhada de leitura EA777 para GRID. + */ +esp_err_t meter_ea777_grid_start(void); + +/** + * @brief Inicia a task partilhada de leitura EA777 para EVSE. + */ +esp_err_t meter_ea777_evse_start(void); + +/** + * @brief Para/remover EA777 GRID em modo compatível antigo. */ void meter_ea777_stop(void); +/** + * @brief Remove EA777 GRID; só destrói o Modbus master se não houver EVSE registado. + */ +void meter_ea777_grid_stop(void); + +/** + * @brief Remove EA777 EVSE; só destrói o Modbus master se não houver GRID registado. + */ +void meter_ea777_evse_stop(void); + #ifdef __cplusplus } #endif -#endif /* METER_EA777_H_ */ \ No newline at end of file +#endif /* METER_EA777_H_ */ diff --git a/components/meter_manager/driver/meter_modbus/meter_orno.h b/components/meter_manager/driver/meter_modbus/meter_orno.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_orno513.c b/components/meter_manager/driver/meter_modbus/meter_orno513.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_orno513.h b/components/meter_manager/driver/meter_modbus/meter_orno513.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_orno516.c b/components/meter_manager/driver/meter_modbus/meter_orno516.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_orno516.h b/components/meter_manager/driver/meter_modbus/meter_orno516.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/meter_orno526.c b/components/meter_manager/driver/meter_modbus/meter_orno526.c old mode 100755 new mode 100644 index b8d6e09..ee3a321 --- a/components/meter_manager/driver/meter_modbus/meter_orno526.c +++ b/components/meter_manager/driver/meter_modbus/meter_orno526.c @@ -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 +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + #include +#include +#include +#include #define TAG "serial_mdb_orno526" +// ===== UART / Modbus ===== #define MB_PORT_NUM 2 #define MB_DEV_SPEED 9600 #define MB_UART_TXD 17 #define MB_UART_RXD 16 #define MB_UART_RTS 2 -#define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS) -#define POLL_INTERVAL (100 / portTICK_PERIOD_MS) +#define ORNO526_SLAVE_ID 1 -#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1)) -#define STR(x) ((const char *)(x)) -#define OPTS(min, max, step) {.opt1 = min, .opt2 = max, .opt3 = step} +// ===== Temporizações ===== +#define UPDATE_INTERVAL pdMS_TO_TICKS(3000) +#define POLL_INTERVAL pdMS_TO_TICKS(100) +#define STOP_WAIT_STEP pdMS_TO_TICKS(10) +#define STOP_WAIT_ITERATIONS 200 -// State flag +#define OPTS(minimum, maximum, step_value) \ + {.opt1 = (minimum), .opt2 = (maximum), .opt3 = (step_value)} +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +// ===== Estado ===== static bool is_initialized = false; +static volatile bool task_should_run = false; static TaskHandle_t meter_task = NULL; +static const char *meter_source = "GRID"; -// CID enums -enum +// ===== CIDs ===== +typedef enum { CID_ACTIVE_ENERGY = 0, CID_REACTIVE_ENERGY, @@ -35,276 +73,540 @@ enum CID_REACTIVE_POWER, CID_L1_CURRENT, CID_L1_VOLTAGE, - CID_FREQUENCY + CID_FREQUENCY, + CID_POWER_FACTOR, + CID_COUNT +} orno526_cid_t; + +#define ORNO526_REQUIRED_MASK \ + ((1UL << CID_L1_VOLTAGE) | \ + (1UL << CID_L1_CURRENT) | \ + (1UL << CID_ACTIVE_POWER)) + +// ===== Endereços Modbus ===== +#define REG_L1_VOLTAGE 0x0100U +#define REG_L1_CURRENT 0x0102U +#define REG_ACTIVE_POWER 0x0104U +#define REG_APPARENT_POWER 0x0106U +#define REG_REACTIVE_POWER 0x0108U +#define REG_FREQUENCY 0x010AU +#define REG_POWER_FACTOR 0x010BU +#define REG_FORWARD_ACTIVE_ENERGY 0x010EU +#define REG_TOTAL_REACTIVE_ENERGY 0x0140U + +// A documentação mostra ABCD no fio. Tal como validado noutros drivers deste +// projeto, o tipo CDAB do esp-modbus faz a conversão correta para o host ESP32. +#ifndef ORNO526_I32_PARAM_TYPE +#define ORNO526_I32_PARAM_TYPE PARAM_TYPE_I32_CDAB +#endif + +// param_offset fica a zero porque cada leitura usa um buffer local alinhado. +static const mb_parameter_descriptor_t device_parameters_orno526[CID_COUNT] = { + {CID_ACTIVE_ENERGY, "Forward Active Energy", "kWh", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_FORWARD_ACTIVE_ENERGY, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 2147483647, 1), PAR_PERMS_READ}, + + {CID_REACTIVE_ENERGY, "Total Reactive Energy", "kvarh", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_TOTAL_REACTIVE_ENERGY, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 2147483647, 1), PAR_PERMS_READ}, + + {CID_ACTIVE_POWER, "Active Power", "W", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_ACTIVE_POWER, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ}, + + {CID_APPARENT_POWER, "Apparent Power", "VA", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_APPARENT_POWER, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 100000, 1), PAR_PERMS_READ}, + + {CID_REACTIVE_POWER, "Reactive Power", "var", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_REACTIVE_POWER, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ}, + + {CID_L1_CURRENT, "L1 Current", "A", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_L1_CURRENT, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ}, + + {CID_L1_VOLTAGE, "L1 Voltage", "V", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_L1_VOLTAGE, 2, 0, + ORNO526_I32_PARAM_TYPE, 4, OPTS(0, 300000, 1), PAR_PERMS_READ}, + + {CID_FREQUENCY, "Frequency", "Hz", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_FREQUENCY, 1, 0, + PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ}, + + {CID_POWER_FACTOR, "Power Factor", "", ORNO526_SLAVE_ID, + MB_PARAM_INPUT, REG_POWER_FACTOR, 1, 0, + PARAM_TYPE_U16, 2, OPTS(0, 1000, 1), PAR_PERMS_READ}, }; -// Register addresses -#define TOTALFACTIVE 0x010E -#define TOTALRACTIVE 0x0118 -#define ACTIVEPOWER 0x0104 -#define APPARENTPOWER 0x0106 -#define REACTIVEPOWER 0x0108 -#define L1CURRENT 0x0102 -#define L1VOLTAGE 0x0100 -#define FREQUENCY 0x010A - -const mb_parameter_descriptor_t device_parameters_orno526[] = { - - {CID_ACTIVE_ENERGY, "Active Energy", "kWh", 1, - MB_PARAM_INPUT, TOTALFACTIVE, 2, HOLD_OFFSET(active_energy), - PARAM_TYPE_I32_CDAB, 4, OPTS(0, 100000, 1), PAR_PERMS_READ}, - - {CID_REACTIVE_ENERGY, "Reactive Energy", "kWh", 1, - MB_PARAM_INPUT, TOTALRACTIVE, 2, HOLD_OFFSET(reactive_energy), - PARAM_TYPE_I32_CDAB, 4, OPTS(0, 100000, 1), PAR_PERMS_READ}, - - {CID_ACTIVE_POWER, "Active Power", "W", 1, - MB_PARAM_INPUT, ACTIVEPOWER, 2, HOLD_OFFSET(active_power), - PARAM_TYPE_I32_CDAB, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ}, - - {CID_APPARENT_POWER, "Apparent Power", "VA", 1, - MB_PARAM_INPUT, APPARENTPOWER, 2, HOLD_OFFSET(apparent_power), - PARAM_TYPE_I32_CDAB, 4, OPTS(0, 100000, 1), PAR_PERMS_READ}, - - {CID_REACTIVE_POWER, "Reactive Power", "VAR", 1, - MB_PARAM_INPUT, REACTIVEPOWER, 2, HOLD_OFFSET(reactive_power), - PARAM_TYPE_I32_CDAB, 4, OPTS(-100000, 100000, 1), PAR_PERMS_READ}, - - {CID_L1_CURRENT, "L1 Current", "A", 1, - MB_PARAM_INPUT, L1CURRENT, 2, HOLD_OFFSET(l1_current), - PARAM_TYPE_I32_CDAB, 4, OPTS(0, 100, 0.1), PAR_PERMS_READ}, - - {CID_L1_VOLTAGE, "L1 Voltage", "V", 1, - MB_PARAM_INPUT, L1VOLTAGE, 2, HOLD_OFFSET(l1_voltage), - PARAM_TYPE_I32_CDAB, 4, OPTS(0, 300, 0.1), PAR_PERMS_READ}, - - {CID_FREQUENCY, "Frequency", "Hz", 1, - MB_PARAM_INPUT, FREQUENCY, 1, HOLD_OFFSET(frequency), - PARAM_TYPE_I32_CDAB, 2, OPTS(0, 1000, 0.1), PAR_PERMS_READ} - -}; - -const uint16_t num_device_parameters_orno526 = sizeof(device_parameters_orno526) / sizeof(device_parameters_orno526[0]); - -static void *get_param_ptr(const mb_parameter_descriptor_t *param) +static bool orno526_is_16bit_cid(uint16_t cid) { - if (!param || param->param_offset == 0) - return NULL; - return ((uint8_t *)&holding_reg_params + param->param_offset - 1); + return cid == CID_FREQUENCY || cid == CID_POWER_FACTOR; } -static inline float scale_for_cid(uint16_t cid) +static bool orno526_value_is_valid(uint16_t cid, float value) +{ + if (!isfinite(value)) + return false; + + switch (cid) + { + case CID_ACTIVE_ENERGY: + case CID_REACTIVE_ENERGY: + return value >= 0.0f && value <= 10000000.0f; + + case CID_ACTIVE_POWER: + case CID_REACTIVE_POWER: + return value >= -100000.0f && value <= 100000.0f; + + case CID_APPARENT_POWER: + return value >= 0.0f && value <= 100000.0f; + + case CID_L1_CURRENT: + return value >= -100.0f && value <= 100.0f; + + case CID_L1_VOLTAGE: + return value >= 0.0f && value <= 300.0f; + + case CID_FREQUENCY: + return value >= 0.0f && value <= 100.0f; + + case CID_POWER_FACTOR: + return value >= 0.0f && value <= 1.1f; + + default: + return false; + } +} + +static float orno526_scale_i32(uint16_t cid, int32_t raw) { switch (cid) { case CID_L1_VOLTAGE: case CID_L1_CURRENT: - return 1000.0f; // V/A = raw / 1000 + return (float)raw / 1000.0f; + + case CID_ACTIVE_ENERGY: + case CID_REACTIVE_ENERGY: + return (float)raw / 100.0f; + case CID_ACTIVE_POWER: case CID_APPARENT_POWER: case CID_REACTIVE_POWER: - return 1.0f; // W/VA/var = raw - case CID_ACTIVE_ENERGY: - case CID_REACTIVE_ENERGY: - return 100.0f; // kWh = raw / 100 - case CID_FREQUENCY: - return 10.0f; // Hz = raw / 10 default: - return 1.0f; + return (float)raw; } } + +static float orno526_scale_u16(uint16_t cid, uint16_t raw) +{ + switch (cid) + { + case CID_FREQUENCY: + return (float)raw / 10.0f; + + case CID_POWER_FACTOR: + return (float)raw / 1000.0f; + + default: + return (float)raw; + } +} + static void serial_mdb_task(void *param) { - esp_err_t err; - const mb_parameter_descriptor_t *desc = NULL; + (void)param; - float voltage[3] = {0}; - float current[3] = {0}; - int watt[3] = {0}; - float energy = 0.0f; - float frequency_hz = 0.0f; // <- armazenar frequência lida (0x010A) - - while (1) + while (task_should_run) { - for (uint16_t cid = 0; cid < num_device_parameters_orno526; cid++) + float voltage[3] = {0.0f, 0.0f, 0.0f}; + float current[3] = {0.0f, 0.0f, 0.0f}; + int32_t watt[3] = {0, 0, 0}; + + float frequency_hz = 0.0f; + float power_factor = 0.0f; + float active_energy_kwh = 0.0f; + float reactive_energy_kvarh = 0.0f; + float apparent_power_va = 0.0f; + float reactive_power_var = 0.0f; + + uint32_t valid_mask = 0U; + + for (uint16_t cid = 0; cid < CID_COUNT && task_should_run; ++cid) { - err = mbc_master_get_cid_info(cid, &desc); + const mb_parameter_descriptor_t *desc = NULL; + esp_err_t err = mbc_master_get_cid_info(cid, &desc); if (err != ESP_OK || !desc) { - ESP_LOGE(TAG, "mbc_master_get_cid_info(%u) failed: %s", cid, esp_err_to_name(err)); + ESP_LOGE(TAG, "%s get_cid_info(%u) failed: %s", + meter_source, (unsigned)cid, esp_err_to_name(err)); continue; } - void *data_ptr = get_param_ptr(desc); - if (!data_ptr) - { - ESP_LOGE(TAG, "CID %u (%s): null data_ptr", cid, desc->param_key); - continue; - } + int32_t raw_i32 = 0; + uint16_t raw_u16 = 0; + void *data_ptr = orno526_is_16bit_cid(cid) + ? (void *)&raw_u16 + : (void *)&raw_i32; uint8_t type = 0; - err = mbc_master_get_parameter(cid, (char *)desc->param_key, (uint8_t *)data_ptr, &type); - if (err == ESP_OK) + err = mbc_master_get_parameter(cid, + (char *)desc->param_key, + (uint8_t *)data_ptr, + &type); + if (err != ESP_OK) { - float val = 0.0f; + ESP_LOGE(TAG, "%s CID %u (%s) read failed: %s", + meter_source, + (unsigned)cid, + desc->param_key, + esp_err_to_name(err)); + vTaskDelay(POLL_INTERVAL); + continue; + } - if (cid == CID_FREQUENCY) + const float value = orno526_is_16bit_cid(cid) + ? orno526_scale_u16(cid, raw_u16) + : orno526_scale_i32(cid, raw_i32); + + if (!orno526_value_is_valid(cid, value)) + { + if (orno526_is_16bit_cid(cid)) { - // Frequência é U16 (1 registo), escala = /10.0 - uint16_t raw16 = *(uint16_t *)data_ptr; - val = raw16 / 10.0f; - frequency_hz = val; + ESP_LOGW(TAG, + "%s %s invalid: raw=0x%04X value=%.6f", + meter_source, + desc->param_key, + (unsigned)raw_u16, + (double)value); } else { - // Demais CIDs são I32_CDAB (2 registos) - int32_t raw32 = *(int32_t *)data_ptr; - float scale = scale_for_cid(cid); - val = raw32 / scale; + ESP_LOGW(TAG, + "%s %s invalid: raw=%ld (0x%08lX) value=%.6f", + meter_source, + desc->param_key, + (long)raw_i32, + (unsigned long)(uint32_t)raw_i32, + (double)value); } - ESP_LOGI(TAG, "%s: %.3f %s", desc->param_key, val, desc->param_units); - - switch (cid) - { - case CID_L1_VOLTAGE: - voltage[0] = val; - break; - case CID_L1_CURRENT: - current[0] = val; - break; - - case CID_ACTIVE_POWER: - watt[0] = (int)lrintf(val); - watt[1] = watt[2] = watt[0]; - break; - - case CID_ACTIVE_ENERGY: - energy = val; // já em kWh (raw/100) - break; - - // CID_FREQUENCY já atualiza 'frequency_hz' acima - default: - break; - } + vTaskDelay(POLL_INTERVAL); + continue; } - else + + valid_mask |= (1UL << cid); + + ESP_LOGD(TAG, "%s %s: %.3f %s", + meter_source, + desc->param_key, + (double)value, + desc->param_units); + + switch (cid) { - ESP_LOGE(TAG, "CID %u (%s) read failed: %s", cid, desc->param_key, esp_err_to_name(err)); + case CID_ACTIVE_ENERGY: + active_energy_kwh = value; + break; + + case CID_REACTIVE_ENERGY: + reactive_energy_kvarh = value; + break; + + case CID_ACTIVE_POWER: + watt[0] = (int32_t)lrintf(value); + break; + + case CID_APPARENT_POWER: + apparent_power_va = value; + break; + + case CID_REACTIVE_POWER: + reactive_power_var = value; + break; + + case CID_L1_CURRENT: + // irms deve ser não negativo; o sentido é representado pela potência. + current[0] = fabsf(value); + break; + + case CID_L1_VOLTAGE: + voltage[0] = value; + break; + + case CID_FREQUENCY: + frequency_hz = value; + break; + + case CID_POWER_FACTOR: + power_factor = value; + break; + + default: + break; } vTaskDelay(POLL_INTERVAL); } + if (!task_should_run) + break; + + if ((valid_mask & ORNO526_REQUIRED_MASK) != ORNO526_REQUIRED_MASK) + { + ESP_LOGW(TAG, + "%s incomplete OR-WE-526 sample, mask=0x%03lX", + meter_source, + (unsigned long)valid_mask); + vTaskDelay(UPDATE_INTERVAL); + continue; + } + meter_event_data_t evt = { - .frequency = frequency_hz, // agora preenchido - .power_factor = 0.0f, // (adicione PF se quiser ler 0x010B) - .total_energy = energy, - .source = "GRID", + .source = meter_source, + .watt_total = watt[0], + .frequency = frequency_hz, + .power_factor = power_factor, + .total_energy = active_energy_kwh, + .timestamp_us = esp_timer_get_time(), }; memcpy(evt.vrms, voltage, sizeof(evt.vrms)); memcpy(evt.irms, current, sizeof(evt.irms)); memcpy(evt.watt, watt, sizeof(evt.watt)); - esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY); + ESP_LOGI(TAG, + "%s OR-WE-526 event: V=%.3fV I=%.3fA P=%ldW " + "S=%.0fVA Q=%.0fvar E=%.2fkWh Er=%.2fkvarh " + "Hz=%.2f PF=%.3f mask=0x%03lX", + meter_source, + (double)voltage[0], + (double)current[0], + (long)evt.watt_total, + (double)apparent_power_va, + (double)reactive_power_var, + (double)active_energy_kwh, + (double)reactive_energy_kvarh, + (double)frequency_hz, + (double)power_factor, + (unsigned long)valid_mask); + + esp_err_t post_err = esp_event_post(METER_EVENT, + METER_EVENT_DATA_READY, + &evt, + sizeof(evt), + portMAX_DELAY); + if (post_err != ESP_OK) + { + ESP_LOGW(TAG, "%s failed to post meter event: %s", + meter_source, esp_err_to_name(post_err)); + } + vTaskDelay(UPDATE_INTERVAL); } + + ESP_LOGI(TAG, "OR-WE-526 task stopped"); + meter_task = NULL; + vTaskDelete(NULL); } -esp_err_t meter_orno526_init(void) +static esp_err_t meter_orno526_init_common(const char *source) { + if (!source) + return ESP_ERR_INVALID_ARG; + if (is_initialized) { - ESP_LOGW(TAG, "meter_orno526 already initialized"); + if (strcmp(meter_source, source) == 0) + return ESP_OK; + + ESP_LOGE(TAG, + "OR-WE-526 already initialized as %s; cannot reinitialize as %s", + meter_source, + source); return ESP_ERR_INVALID_STATE; } - ESP_LOGI(TAG, "meter_orno526_init"); + meter_source = source; + ESP_LOGI(TAG, "meter_orno526 init as %s", meter_source); - // ORNO costuma vir 9600, 8E1. Se o teu estiver 8E2, troca os stop bits mais abaixo. mb_communication_info_t comm = { .port = MB_PORT_NUM, .mode = MB_MODE_RTU, - .baudrate = MB_DEV_SPEED, // 9600 - .parity = UART_PARITY_DISABLE, // 8E1 por padrão + .baudrate = MB_DEV_SPEED, + .parity = UART_PARITY_DISABLE, // Validado: 9600 8N1 }; void *handler = NULL; esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler); if (err != ESP_OK) { - ESP_LOGE(TAG, "mbc_master_init failed"); + ESP_LOGE(TAG, "mbc_master_init failed: %s", esp_err_to_name(err)); return err; } - ESP_ERROR_CHECK(mbc_master_setup(&comm)); + err = mbc_master_setup(&comm); + if (err != ESP_OK) + goto fail_destroy_master; - // Pinos RS-485 (TX, RX, RTS=DE/RE). CTS não usado. - ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE)); + err = uart_set_pin(MB_PORT_NUM, + MB_UART_TXD, + MB_UART_RXD, + MB_UART_RTS, + UART_PIN_NO_CHANGE); + if (err != ESP_OK) + goto fail_destroy_master; - // Garanta 8 bits de dados e sem flow-control. - ESP_ERROR_CHECK(uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS)); - ESP_ERROR_CHECK(uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0)); + err = uart_set_word_length(MB_PORT_NUM, UART_DATA_8_BITS); + if (err != ESP_OK) + goto fail_destroy_master; - // Stop bits: a maioria usa 1. Se continuar a dar INVALID_RESPONSE, teste 2. - ESP_ERROR_CHECK(uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1)); - // Alternativa, se o medidor estiver configurado p/ 2 stop bits: - // ESP_ERROR_CHECK(uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_2)); + err = uart_set_hw_flow_ctrl(MB_PORT_NUM, UART_HW_FLOWCTRL_DISABLE, 0); + if (err != ESP_OK) + goto fail_destroy_master; - ESP_ERROR_CHECK(mbc_master_start()); - ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); + err = uart_set_stop_bits(MB_PORT_NUM, UART_STOP_BITS_1); + if (err != ESP_OK) + goto fail_destroy_master; - // (Opcional) Logs detalhados para ver TX/RX/frames durante debug: + err = mbc_master_start(); + if (err != ESP_OK) + goto fail_destroy_master; + + // O driver UART é instalado por mbc_master_start(); só depois ativamos RS485. + err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX); + if (err != ESP_OK) + goto fail_destroy_master; + + vTaskDelay(pdMS_TO_TICKS(20)); + + err = mbc_master_set_descriptor(device_parameters_orno526, + ARRAY_SIZE(device_parameters_orno526)); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "mbc_master_set_descriptor failed: %s", + esp_err_to_name(err)); + goto fail_destroy_master; + } + + // Reduz para INFO/WARN em produção, se necessário. esp_log_level_set("MB_CONTROLLER_MASTER", ESP_LOG_DEBUG); esp_log_level_set("MB_PORT_COMMON", ESP_LOG_DEBUG); esp_log_level_set("MB_SERIAL_MASTER", ESP_LOG_DEBUG); - vTaskDelay(pdMS_TO_TICKS(5)); - - ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_orno526, num_device_parameters_orno526)); - is_initialized = true; return ESP_OK; + +fail_destroy_master: + ESP_LOGE(TAG, "OR-WE-526 initialization failed: %s", esp_err_to_name(err)); + (void)mbc_master_destroy(); + if (uart_is_driver_installed(MB_PORT_NUM)) + (void)uart_driver_delete(MB_PORT_NUM); + return err; +} + +esp_err_t meter_orno526_init(void) +{ + return meter_orno526_grid_init(); +} + +esp_err_t meter_orno526_grid_init(void) +{ + return meter_orno526_init_common("GRID"); +} + +esp_err_t meter_orno526_evse_init(void) +{ + return meter_orno526_init_common("EVSE"); } esp_err_t meter_orno526_start(void) { - - ESP_LOGI(TAG, "meter_orno526_start"); - if (!is_initialized) { ESP_LOGE(TAG, "meter_orno526 not initialized"); return ESP_ERR_INVALID_STATE; } - if (meter_task == NULL) + if (meter_task != NULL) + return ESP_OK; + + task_should_run = true; + BaseType_t ok = xTaskCreate(serial_mdb_task, + "meter_orno526_task", + 4096, + NULL, + 3, + &meter_task); + if (ok != pdPASS) { - xTaskCreate(serial_mdb_task, "meter_orno526_task", 4096, NULL, 3, &meter_task); - ESP_LOGI(TAG, "meter_orno526 task started"); + task_should_run = false; + meter_task = NULL; + ESP_LOGE(TAG, "Failed to create OR-WE-526 task"); + return ESP_ERR_NO_MEM; } + ESP_LOGI(TAG, "OR-WE-526 %s task started", meter_source); return ESP_OK; } +esp_err_t meter_orno526_grid_start(void) +{ + return meter_orno526_start(); +} + +esp_err_t meter_orno526_evse_start(void) +{ + return meter_orno526_start(); +} + void meter_orno526_stop(void) { if (!is_initialized) - { - ESP_LOGW(TAG, "meter_orno526 not initialized"); return; + + ESP_LOGI(TAG, "Stopping OR-WE-526 %s", meter_source); + + task_should_run = false; + + for (int i = 0; i < STOP_WAIT_ITERATIONS && meter_task != NULL; ++i) + vTaskDelay(STOP_WAIT_STEP); + + if (meter_task != NULL) + { + ESP_LOGW(TAG, "OR-WE-526 task did not stop in time; deleting it"); + vTaskDelete(meter_task); + meter_task = NULL; } - ESP_LOGI(TAG, "Stopping meter_orno526"); - - uart_driver_delete(MB_PORT_NUM); - esp_err_t err = mbc_master_destroy(); if (err != ESP_OK) { - ESP_LOGW(TAG, "mbc_master_destroy() returned %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "mbc_master_destroy returned %s", + esp_err_to_name(err)); + } + + if (uart_is_driver_installed(MB_PORT_NUM)) + { + err = uart_driver_delete(MB_PORT_NUM); + if (err != ESP_OK) + { + ESP_LOGW(TAG, "uart_driver_delete returned %s", + esp_err_to_name(err)); + } } is_initialized = false; + meter_source = "GRID"; +} + +void meter_orno526_grid_stop(void) +{ + meter_orno526_stop(); +} + +void meter_orno526_evse_stop(void) +{ + meter_orno526_stop(); } diff --git a/components/meter_manager/driver/meter_modbus/meter_orno526.h b/components/meter_manager/driver/meter_modbus/meter_orno526.h old mode 100755 new mode 100644 index a9533c8..7ac9a9b --- a/components/meter_manager/driver/meter_modbus/meter_orno526.h +++ b/components/meter_manager/driver/meter_modbus/meter_orno526.h @@ -4,25 +4,54 @@ extern "C" { #endif -#include -#include #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 } diff --git a/components/meter_manager/driver/meter_modbus/modbus_params.c b/components/meter_manager/driver/meter_modbus/modbus_params.c old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_modbus/modbus_params.h b/components/meter_manager/driver/meter_modbus/modbus_params.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/driver/meter_zigbee/meter_zigbee.c b/components/meter_manager/driver/meter_zigbee/meter_zigbee.c old mode 100755 new mode 100644 index d7d505e..e701a6a --- a/components/meter_manager/driver/meter_zigbee/meter_zigbee.c +++ b/components/meter_manager/driver/meter_zigbee/meter_zigbee.c @@ -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)); diff --git a/components/meter_manager/driver/meter_zigbee/meter_zigbee.h b/components/meter_manager/driver/meter_zigbee/meter_zigbee.h old mode 100755 new mode 100644 diff --git a/components/meter_manager/include/meter_manager.h b/components/meter_manager/include/meter_manager.h old mode 100755 new mode 100644 index ed6458f..4536008 --- a/components/meter_manager/include/meter_manager.h +++ b/components/meter_manager/include/meter_manager.h @@ -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 */ diff --git a/components/meter_manager/src/meter_manager.c b/components/meter_manager/src/meter_manager.c index bf8e256..61d2654 100755 --- a/components/meter_manager/src/meter_manager.c +++ b/components/meter_manager/src/meter_manager.c @@ -7,9 +7,10 @@ #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" +#include "meter_dts024m.h" #include @@ -156,60 +157,131 @@ static esp_err_t write_meter_model_to_storage(const char *key, meter_type_t mete return ESP_OK; } +static bool meter_type_uses_rs485_modbus(meter_type_t type) +{ + switch (type) + { + case METER_TYPE_ORNO513: + case METER_TYPE_ORNO516: + case METER_TYPE_ORNO526: + case METER_TYPE_DTS6619: + case METER_TYPE_DDS661: + case METER_TYPE_DDS665: + case METER_TYPE_EA777: + case METER_TYPE_DTS024M: + return true; + default: + return false; + } +} + +static bool meter_type_supports_dual_same_bus(meter_type_t type) +{ + return type == METER_TYPE_DDS661 || type == METER_TYPE_DDS665 || type == METER_TYPE_EA777; +} + +static esp_err_t validate_meter_model_pair(meter_type_t grid_type, meter_type_t evse_type) +{ + if (grid_type == METER_TYPE_NONE || evse_type == METER_TYPE_NONE) + return ESP_OK; + + const bool grid_is_modbus = meter_type_uses_rs485_modbus(grid_type); + const bool evse_is_modbus = meter_type_uses_rs485_modbus(evse_type); + + if (grid_is_modbus && evse_is_modbus) + { + if (grid_type == evse_type && meter_type_supports_dual_same_bus(grid_type)) + return ESP_OK; + + ESP_LOGE( + TAG, + "Unsupported RS485 meter pair: GRID=%s EVSE=%s. " + "Supported dual Modbus pairs are " + "DDS-661+DDS-661, DDS-665+DDS-665 or EA-777+EA-777.", + meter_type_to_str(grid_type), + meter_type_to_str(evse_type)); + return ESP_ERR_NOT_SUPPORTED; + } + + return ESP_OK; +} + /** * @brief Inicializa o sistema de meter manager. */ + esp_err_t meter_manager_init(void) { - // garantir storage pronto - esp_err_t s = storage_service_init(); - if (s != ESP_OK) - ESP_LOGE(TAG, "storage_service_init failed: %s", esp_err_to_name(s)); + esp_err_t err = storage_service_init(); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "storage_service_init failed: %s", + esp_err_to_name(err)); + return err; + } - esp_err_t err; + err = load_or_init_meter_model( + STORE_GRID_MODEL, + &meter_grid_type); + if (err != ESP_OK) + return err; + + err = load_or_init_meter_model( + STORE_EVSE_MODEL, + &meter_evse_type); + if (err != ESP_OK) + return err; + + err = validate_meter_model_pair( + meter_grid_type, + meter_evse_type); + if (err != ESP_OK) + return err; - // Initialize GRID meter err = meter_manager_grid_init(); if (err != ESP_OK) return err; - // Regista handler para eventos de rede - /* - ESP_LOGD(TAG, "Registering network event handler"); - err = esp_event_handler_register( - NETWORK_EVENTS, - ESP_EVENT_ANY_ID, - meter_manager_network_event_handler, - NULL); - */ - + err = meter_manager_evse_init(); if (err != ESP_OK) + { + (void)meter_manager_grid_stop(); return err; + } - // Emite um evento inicial de configuração meter_config_event_t ev = { - .grid_type = meter_manager_grid_get_model(), - .evse_type = meter_manager_evse_get_model(), - .timestamp_us = esp_timer_get_time()}; - esp_event_post(METER_EVENT, - METER_EVENT_CONFIG_UPDATED, - &ev, - sizeof(ev), - 0); + .grid_type = meter_grid_type, + .evse_type = meter_evse_type, + .timestamp_us = esp_timer_get_time(), + }; + + (void)esp_event_post( + METER_EVENT, + METER_EVENT_CONFIG_UPDATED, + &ev, + sizeof(ev), + 0); return ESP_OK; } esp_err_t meter_manager_start(void) { - // Start GRID meter - return meter_manager_grid_start(); + esp_err_t err = meter_manager_grid_start(); + if (err != ESP_OK) + return err; + + return meter_manager_evse_start(); } esp_err_t meter_manager_stop(void) { - // Stop GRID meter - return meter_manager_grid_stop(); + esp_err_t evse_err = meter_manager_evse_stop(); + esp_err_t grid_err = meter_manager_grid_stop(); + + if (evse_err != ESP_OK) + return evse_err; + return grid_err; } // ---------- EVSE ---------- @@ -237,11 +309,13 @@ 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(); - case METER_TYPE_DTS024M: - return meter_dts024m_init(); + return meter_ea777_evse_init(); + case METER_TYPE_DTS024M: + return meter_dts024m_init(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_init(); @@ -268,11 +342,13 @@ 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(); - case METER_TYPE_DTS024M: - return meter_dts024m_start(); + return meter_ea777_evse_start(); + case METER_TYPE_DTS024M: + return meter_dts024m_start(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_start(); @@ -304,13 +380,16 @@ 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(); + case METER_TYPE_DTS024M: + meter_dts024m_stop(); break; case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: @@ -347,11 +426,13 @@ 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(); - case METER_TYPE_DTS024M: - return meter_dts024m_init(); + return meter_ea777_grid_init(); + case METER_TYPE_DTS024M: + return meter_dts024m_init(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_init(); @@ -378,11 +459,13 @@ 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(); - case METER_TYPE_DTS024M: - return meter_dts024m_start(); + return meter_ea777_grid_start(); + case METER_TYPE_DTS024M: + return meter_dts024m_start(); case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: return meter_zigbee_start(); @@ -414,13 +497,16 @@ 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(); + case METER_TYPE_DTS024M: + meter_dts024m_stop(); break; case METER_TYPE_MONO_ZIGBEE: case METER_TYPE_TRIF_ZIGBEE: @@ -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,13 +636,15 @@ 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) return METER_TYPE_TRIF_ZIGBEE; if (strcmp(str, "EA-777") == 0) return METER_TYPE_EA777; - if (strcmp(str, "DTS-024M") == 0) + if (strcmp(str, "DTS-024M") == 0) return METER_TYPE_DTS024M; return METER_TYPE_NONE; diff --git a/components/network/CMakeLists.txt b/components/network/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/network/include/network.h b/components/network/include/network.h old mode 100755 new mode 100644 diff --git a/components/network/src/network.c b/components/network/src/network.c old mode 100755 new mode 100644 diff --git a/components/ocpp/CMakeLists.txt b/components/ocpp/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/ocpp/include/ocpp.h b/components/ocpp/include/ocpp.h old mode 100755 new mode 100644 diff --git a/components/ocpp/src/ocpp.c b/components/ocpp/src/ocpp.c old mode 100755 new mode 100644 index a48e363..aa908a0 --- a/components/ocpp/src/ocpp.c +++ b/components/ocpp/src/ocpp.c @@ -55,7 +55,7 @@ static const char *TAG = "ocpp"; * - If it publishes in Wh, keep 0 */ #ifndef METER_TOTAL_ENERGY_IS_KWH -#define METER_TOTAL_ENERGY_IS_KWH 0 +#define METER_TOTAL_ENERGY_IS_KWH 1 #endif // Bounded timeout for esp_event_post from loop/callback paths @@ -318,7 +318,7 @@ void ocpp_get_server(char *value /* out, size>=64 */) storage_init_once(); - esp_err_t e = storage_get_str_sync(NVS_NAMESPACE, NVS_OCPP_SERVER, value, 64, STORE_TO); + esp_err_t e = storage_get_str_sync(NVS_NAMESPACE, NVS_OCPP_SERVER, value, 256, STORE_TO); if (e == ESP_ERR_NOT_FOUND) { value[0] = '\0'; @@ -414,15 +414,11 @@ void ocpp_set_charge_id(char *value) static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, void *event_data) { (void)arg; - (void)base; - (void)id; - if (s_stopping || !enabled || g_ocpp_conn == NULL) + if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_VERIFY || event_data == NULL) return; const auth_tag_verify_event_t *rq = (const auth_tag_verify_event_t *)event_data; - if (!rq) - return; char idtag[AUTH_TAG_MAX_LEN]; if (rq->tag[0] == '\0') @@ -438,6 +434,13 @@ static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, vo ESP_LOGI(TAG, "AUTH_EVENT_TAG_VERIFY: tag=%s req_id=%u", idtag, (unsigned)rq->req_id); + if (s_stopping || !enabled || g_ocpp_conn == NULL) + { + ESP_LOGW(TAG, "OCPP RFID verification unavailable -> rejecting tag=%s", idtag); + (void)esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, OCPP_EVENT_POST_TO); + return; + } + if (ocpp_isTransactionActive()) { ESP_LOGI(TAG, "Transaction active -> ocpp_endTransaction(\"%s\")", idtag); @@ -519,6 +522,8 @@ static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *d { (void)arg; + ESP_LOGI(TAG, "[OCPP METER EVSE] "); + if (base != METER_EVENT || id != METER_EVENT_DATA_READY || !data) return; @@ -530,12 +535,38 @@ static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *d if (!evt->source || strcmp(evt->source, "EVSE") != 0) return; - int32_t sum_w = (int32_t)evt->watt[0] + (int32_t)evt->watt[1] + (int32_t)evt->watt[2]; - float avg_v = (evt->vrms[0] + evt->vrms[1] + evt->vrms[2]) / 3.0f; + const int32_t phase_sum_w = (int32_t)evt->watt[0] + (int32_t)evt->watt[1] + (int32_t)evt->watt[2]; + int32_t sum_w = (evt->watt_total != 0) ? evt->watt_total : phase_sum_w; + + float v_sum = 0.0f; + int v_count = 0; + for (int i = 0; i < 3; ++i) + { + if (isfinite(evt->vrms[i]) && evt->vrms[i] > 1.0f) + { + v_sum += evt->vrms[i]; + v_count++; + } + } + float avg_v = (v_count > 0) ? (v_sum / (float)v_count) : 0.0f; + float sum_i = evt->irms[0] + evt->irms[1] + evt->irms[2]; float total_wh = sanitize_energy_wh(energy_to_wh(evt->total_energy)); + ESP_LOGI(TAG, + "[OCPP METER EVSE] " + "V=[%.1f %.1f %.1f] avg=%.1fV | " + "I=[%.3f %.3f %.3f] sum=%.3fA | " + "W=[%ld %ld %ld] total=%ldW | " + "E=%.3fkWh / %.1fWh | " + "Hz=%.2f PF=%.3f", + evt->vrms[0], evt->vrms[1], evt->vrms[2], avg_v, + evt->irms[0], evt->irms[1], evt->irms[2], sum_i, + (long)evt->watt[0], (long)evt->watt[1], (long)evt->watt[2], (long)sum_w, + evt->total_energy, total_wh, + evt->frequency, evt->power_factor); + portENTER_CRITICAL(&s_meter_mux); memcpy(s_meter.vrms, evt->vrms, sizeof(s_meter.vrms)); memcpy(s_meter.irms, evt->irms, sizeof(s_meter.irms)); @@ -845,7 +876,7 @@ void ocpp_start(void) return; } - char serverstr[64] = {0}; + char serverstr[256] = {0}; ocpp_get_server(serverstr); if (serverstr[0] == '\0') { @@ -926,12 +957,24 @@ void ocpp_start(void) ocpp_setEnergyMeterInput(&setEnergyInput); + ocpp_addMeterValueInputFloat(&setPowerMeterInput, + "Power.Active.Import", "W", NULL, NULL); + + /* opcional, mas para certificação eu deixaria desligado inicialmente: + ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL); + ocpp_addMeterValueInputFloat(&getCurrentOffered, "Current.Offered", "A", NULL, NULL); + ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL); + ocpp_addMeterValueInputFloat(&setTemperatureInput, "Temperature", "Celsius", NULL, NULL); + */ + + /* ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL); ocpp_addMeterValueInputFloat(&getCurrentOffered, "Current.Offered", "A", NULL, NULL); ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL); ocpp_addMeterValueInputFloat(&setTemperatureInput, "Temperature", "Celsius", NULL, NULL); ocpp_addMeterValueInputFloat(&setPowerMeterInput, "Power.Active.Import", "W", NULL, NULL); ocpp_addMeterValueInputFloat(&setEnergyMeterInput, "Energy.Active.Import.Register", "Wh", NULL, NULL); + */ ocpp_addErrorCodeInput(&addErrorCodeInput); diff --git a/components/ocpp/src/ocpp_events.c b/components/ocpp/src/ocpp_events.c old mode 100755 new mode 100644 diff --git a/components/peripherals/CMakeLists.txt b/components/peripherals/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/peripherals/include/ac_relay.h b/components/peripherals/include/ac_relay.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/adc.h b/components/peripherals/include/adc.h old mode 100755 new mode 100644 index 13c49b2..0592139 --- a/components/peripherals/include/adc.h +++ b/components/peripherals/include/adc.h @@ -11,4 +11,7 @@ extern adc_cali_handle_t adc_cali_handle; void adc_init(void); +void adc_lock(void); +void adc_unlock(void); + #endif /* ADC_H_ */ \ No newline at end of file diff --git a/components/peripherals/include/adc121s021_dma.h b/components/peripherals/include/adc121s021_dma.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/lm75a.h b/components/peripherals/include/lm75a.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/ntc_sensor.h b/components/peripherals/include/ntc_sensor.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/peripherals.h b/components/peripherals/include/peripherals.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/proximity.h b/components/peripherals/include/proximity.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/rcm.h b/components/peripherals/include/rcm.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/socket_lock.h b/components/peripherals/include/socket_lock.h old mode 100755 new mode 100644 diff --git a/components/peripherals/include/temp_sensor.h b/components/peripherals/include/temp_sensor.h old mode 100755 new mode 100644 diff --git a/components/peripherals/src/ac_relay.c b/components/peripherals/src/ac_relay.c old mode 100755 new mode 100644 diff --git a/components/peripherals/src/adc.c b/components/peripherals/src/adc.c old mode 100755 new mode 100644 index 8378f7d..1745af7 --- a/components/peripherals/src/adc.c +++ b/components/peripherals/src/adc.c @@ -1,5 +1,7 @@ #include "adc.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" const static char* TAG = "adc"; @@ -7,8 +9,40 @@ adc_oneshot_unit_handle_t adc_handle; adc_cali_handle_t adc_cali_handle; +static SemaphoreHandle_t s_adc_mutex; + +static void adc_mutex_init(void) +{ + if (!s_adc_mutex) + { + s_adc_mutex = xSemaphoreCreateMutex(); + ESP_ERROR_CHECK(s_adc_mutex ? ESP_OK : ESP_ERR_NO_MEM); + } +} + +void adc_lock(void) +{ + adc_mutex_init(); + xSemaphoreTake(s_adc_mutex, portMAX_DELAY); +} + +void adc_unlock(void) +{ + if (s_adc_mutex) + { + xSemaphoreGive(s_adc_mutex); + } +} + void adc_init(void) { + adc_mutex_init(); + if (adc_handle) + { + ESP_LOGD(TAG, "ADC já inicializado"); + return; + } + adc_oneshot_unit_init_cfg_t conf = { .unit_id = ADC_UNIT_1 }; diff --git a/components/peripherals/src/adc121s021_dma.c b/components/peripherals/src/adc121s021_dma.c old mode 100755 new mode 100644 diff --git a/components/peripherals/src/lm75a.c b/components/peripherals/src/lm75a.c old mode 100755 new mode 100644 diff --git a/components/peripherals/src/ntc_sensor.c b/components/peripherals/src/ntc_sensor.c old mode 100755 new mode 100644 index d7e5d48..5b415ed --- a/components/peripherals/src/ntc_sensor.c +++ b/components/peripherals/src/ntc_sensor.c @@ -19,7 +19,11 @@ static void ntc_sensor_task_func(void *param) float t; while (true) { - if (ntc_dev_get_temperature(ntc, &t) == ESP_OK) + adc_lock(); + esp_err_t err = ntc_dev_get_temperature(ntc, &t); + adc_unlock(); + + if (err == ESP_OK) { portENTER_CRITICAL(&temp_mux); temp = t; @@ -59,7 +63,9 @@ void ntc_sensor_init(void) // Leitura inicial para log float t0 = 0.0f; + adc_lock(); esp_err_t err = ntc_dev_get_temperature(ntc, &t0); + adc_unlock(); if (err == ESP_OK) { portENTER_CRITICAL(&temp_mux); diff --git a/components/peripherals/src/peripherals.c b/components/peripherals/src/peripherals.c old mode 100755 new mode 100644 diff --git a/components/peripherals/src/proximity.c b/components/peripherals/src/proximity.c old mode 100755 new mode 100644 index c49bee6..bea70d3 --- a/components/peripherals/src/proximity.c +++ b/components/peripherals/src/proximity.c @@ -19,9 +19,22 @@ void proximity_init(void) uint8_t proximity_get_max_current(void) { - int voltage; - adc_oneshot_read(adc_handle, board_config.proximity_adc_channel, &voltage); - adc_cali_raw_to_voltage(adc_cali_handle, voltage, &voltage); + int raw = 0; + int voltage = 0; + + adc_lock(); + esp_err_t err = adc_oneshot_read(adc_handle, board_config.proximity_adc_channel, &raw); + if (err == ESP_OK) + { + err = adc_cali_raw_to_voltage(adc_cali_handle, raw, &voltage); + } + adc_unlock(); + + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Erro ao ler/converter ADC proximity: %s", esp_err_to_name(err)); + return 32; + } ESP_LOGI(TAG, "Measured: %dmV", voltage); diff --git a/components/peripherals/src/rcm.c b/components/peripherals/src/rcm.c old mode 100755 new mode 100644 diff --git a/components/peripherals/src/socket_lock.c b/components/peripherals/src/socket_lock.c old mode 100755 new mode 100644 diff --git a/components/peripherals/src/temp_sensor.c b/components/peripherals/src/temp_sensor.c old mode 100755 new mode 100644 diff --git a/components/protocols/CMakeLists.txt b/components/protocols/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/protocols/include/mqtt.h b/components/protocols/include/mqtt.h old mode 100755 new mode 100644 index 76106bf..369f9b5 --- a/components/protocols/include/mqtt.h +++ b/components/protocols/include/mqtt.h @@ -16,30 +16,17 @@ extern "C" #define MQTT_USERNAME_MAX_LEN 32 #define MQTT_PASSWORD_MAX_LEN 64 - // ============================= - // Limit modes (cmd/evse/limit) - // ============================= - typedef enum - { - LIMIT_MODE_OFF = 0, ///< sem limite, usa max do hardware - LIMIT_MODE_MANUAL, ///< corrente fixa definida pelo utilizador - LIMIT_MODE_LOADBALANCER, ///< limita pelo contador da instalação - LIMIT_MODE_SOLAR ///< só com excedente FV - } limit_mode_t; - /** * @brief Inicializa o módulo MQTT. * * - Carrega configuração da NVS * - Regista handlers de eventos (AUTH, SCHED, LOADBALANCER, METER, EVSE, NETWORK) * - Cria a task de telemetria periódica - * - Aplica o modo de limite persistido - * * Não inicia a ligação ao broker imediatamente; isso acontece quando existir IP * (NETWORK_EVENT_STA_GOT_IP) e `enabled == true`. * - * IMPORTANTE: chamar DEPOIS de loadbalancer_init() para o restauro do modo - * de limite funcionar correctamente. + * IMPORTANTE: chamar DEPOIS de loadbalancer_init() para o estado publicado + * refletir a política local já restaurada. */ esp_err_t mqtt_init(void); diff --git a/components/protocols/include/protocols.h b/components/protocols/include/protocols.h old mode 100755 new mode 100644 diff --git a/components/protocols/src/mqtt.c b/components/protocols/src/mqtt.c old mode 100755 new mode 100644 index b540142..f69d649 --- a/components/protocols/src/mqtt.c +++ b/components/protocols/src/mqtt.c @@ -55,7 +55,6 @@ static const char *TAG = "mqtt"; #define MQTT_KEY_USER "user" #define MQTT_KEY_PASS "pass" #define MQTT_KEY_PERIOD "period" -#define MQTT_KEY_LIMITMODE "limitmode" #define MQTT_HOST_MAX_LEN 64 #define MQTT_TOPIC_MAX_LEN 64 @@ -73,7 +72,6 @@ static char s_mqtt_user[MQTT_USER_MAX_LEN] = {0}; static char s_mqtt_pass[MQTT_PASS_MAX_LEN] = {0}; static uint16_t s_mqtt_periodicity = 30; -static limit_mode_t s_limit_mode = LIMIT_MODE_OFF; static char s_client_id[MQTT_CLIENT_ID_MAX_LEN] = {0}; static bool s_session_was_active = false; @@ -93,10 +91,6 @@ static void mqtt_limit_publish_state(void); static void mqtt_session_publish_state(void); static void mqtt_system_publish_heartbeat(void); -static void mqtt_apply_limit_mode(limit_mode_t mode); -static const char *limit_mode_to_str(limit_mode_t m); -static bool limit_mode_from_str(const char *s, limit_mode_t *out); - static void mqtt_subscribe_all(void); static void mqtt_dispatch_command(const char *topic, const char *data); @@ -209,25 +203,12 @@ static void mqtt_load_config_from_storage(void) } } - { - uint8_t u8 = 0; - err = storage_get_u8_sync(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, &u8, TO_TICKS_MS(500)); - if (err == ESP_OK && u8 <= LIMIT_MODE_SOLAR) - s_limit_mode = (limit_mode_t)u8; - else - { - s_limit_mode = LIMIT_MODE_OFF; - (void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, 0); - needs_flush = true; - } - } - if (needs_flush) (void)storage_flush_async(); - ESP_LOGI(TAG, "Config MQTT (storage): enabled=%d host='%s' topic='%s' period=%u user='%s' limitmode=%s", + ESP_LOGI(TAG, "Config MQTT (storage): enabled=%d host='%s' topic='%s' period=%u user='%s'", s_mqtt_enabled, s_mqtt_host, s_mqtt_base_topic, - (unsigned)s_mqtt_periodicity, s_mqtt_user, limit_mode_to_str(s_limit_mode)); + (unsigned)s_mqtt_periodicity, s_mqtt_user); } static esp_err_t mqtt_save_config_to_storage(void) @@ -262,10 +243,6 @@ static esp_err_t mqtt_save_config_to_storage(void) if (e != ESP_OK) err = e; - e = storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, (uint8_t)s_limit_mode); - if (e != ESP_OK) - err = e; - (void)storage_flush_async(); if (err != ESP_OK) @@ -310,85 +287,6 @@ void mqtt_get_password(char *password) uint16_t mqtt_get_periodicity(void) { return s_mqtt_periodicity; } -static const char *limit_mode_to_str(limit_mode_t m) -{ - switch (m) - { - case LIMIT_MODE_OFF: - return "off"; - case LIMIT_MODE_MANUAL: - return "manual"; - case LIMIT_MODE_LOADBALANCER: - return "loadbalancer"; - case LIMIT_MODE_SOLAR: - return "solar"; - default: - return "off"; - } -} - -static bool limit_mode_from_str(const char *s, limit_mode_t *out) -{ - if (!s || !out) - return false; - if (strcmp(s, "off") == 0) - { - *out = LIMIT_MODE_OFF; - return true; - } - if (strcmp(s, "manual") == 0) - { - *out = LIMIT_MODE_MANUAL; - return true; - } - if (strcmp(s, "loadbalancer") == 0) - { - *out = LIMIT_MODE_LOADBALANCER; - return true; - } - if (strcmp(s, "solar") == 0) - { - *out = LIMIT_MODE_SOLAR; - return true; - } - return false; -} - -static void mqtt_apply_limit_mode(limit_mode_t mode) -{ - switch (mode) - { - case LIMIT_MODE_OFF: - loadbalancer_pv_set_enabled(false); - loadbalancer_grid_set_enabled(false); - loadbalancer_set_enabled(false); - evse_set_charging_current(evse_get_max_charging_current()); - break; - - case LIMIT_MODE_MANUAL: - loadbalancer_pv_set_enabled(false); - loadbalancer_grid_set_enabled(false); - loadbalancer_set_enabled(false); - break; - - case LIMIT_MODE_LOADBALANCER: - loadbalancer_pv_set_enabled(false); - loadbalancer_grid_set_enabled(true); - loadbalancer_set_enabled(true); - break; - - case LIMIT_MODE_SOLAR: - loadbalancer_grid_set_enabled(false); - loadbalancer_pv_set_enabled(true); - loadbalancer_set_enabled(true); - break; - } - - s_limit_mode = mode; - (void)storage_set_u8_async(MQTT_NVS_NAMESPACE, MQTT_KEY_LIMITMODE, (uint8_t)mode); - (void)storage_flush_async(); -} - static void mqtt_build_topic(char *out, size_t out_len, const char *suffix) { const char *base = (s_mqtt_base_topic[0] != '\0') ? s_mqtt_base_topic : "evse"; @@ -715,7 +613,7 @@ static void mqtt_limit_publish_state(void) if (!root) return; - cJSON_AddStringToObject(root, "mode", limit_mode_to_str(s_limit_mode)); + cJSON_AddStringToObject(root, "mode", loadbalancer_limit_mode_to_str(loadbalancer_get_limit_mode())); cJSON_AddNumberToObject(root, "requestedCurrentA", (int)evse_get_charging_current()); cJSON_AddNumberToObject(root, "effectiveCurrentA", (int)evse_get_runtime_charging_current()); cJSON_AddNumberToObject(root, "hardwareMaxA", (int)evse_get_max_charging_current()); @@ -824,7 +722,6 @@ static void mqtt_loadbalancer_event_handler(void *arg, esp_event_base_t base, in { case LOADBALANCER_EVENT_INIT: case LOADBALANCER_EVENT_STATE_CHANGED: - case LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT: mqtt_loadbalancer_publish_state(); mqtt_limit_publish_state(); break; @@ -1158,8 +1055,8 @@ static void handle_cmd_evse_limit(const char *payload) return; } - limit_mode_t mode; - if (!limit_mode_from_str(j_mode->valuestring, &mode)) + loadbalancer_limit_mode_t mode; + if (!loadbalancer_limit_mode_from_str(j_mode->valuestring, &mode)) { ESP_LOGW(TAG, "evse/limit: unknown mode '%s'", j_mode->valuestring); cJSON_Delete(json); @@ -1168,7 +1065,7 @@ static void handle_cmd_evse_limit(const char *payload) switch (mode) { - case LIMIT_MODE_MANUAL: + case LOADBALANCER_LIMIT_MODE_MANUAL: { cJSON *j_curr = cJSON_GetObjectItem(json, "requestedCurrentA"); if (!j_curr) @@ -1183,62 +1080,172 @@ static void handle_cmd_evse_limit(const char *payload) int a = j_curr->valueint; uint8_t hw_max = evse_get_max_charging_current(); + if (a < MIN_CHARGING_CURRENT_LIMIT || a > hw_max) { - ESP_LOGW(TAG, "evse/limit manual: currentA %d out of range [%d,%u]", - a, MIN_CHARGING_CURRENT_LIMIT, (unsigned)hw_max); + ESP_LOGW(TAG, + "evse/limit manual: currentA %d out of range [%d,%u]", + a, + MIN_CHARGING_CURRENT_LIMIT, + (unsigned)hw_max); + cJSON_Delete(json); + return; + } + + esp_err_t e = evse_set_charging_current((uint16_t)a); + if (e != ESP_OK) + { + ESP_LOGW(TAG, + "evse_set_charging_current failed: %s", + esp_err_to_name(e)); cJSON_Delete(json); return; } - evse_set_charging_current((uint16_t)a); break; } - case LIMIT_MODE_LOADBALANCER: + case LOADBALANCER_LIMIT_MODE_GRID: { cJSON *j = cJSON_GetObjectItem(json, "gridMaxImportA"); + if (cJSON_IsNumber(j)) { int a = j->valueint; + if (a < 6 || a > 100) { - ESP_LOGW(TAG, "evse/limit loadbalancer: gridMaxImportA %d out of range", a); + ESP_LOGW(TAG, + "evse/limit grid: gridMaxImportA %d out of range", + a); cJSON_Delete(json); return; } + esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)a); if (e != ESP_OK) - ESP_LOGW(TAG, "loadbalancer_grid_set_max_import_a failed: %s", esp_err_to_name(e)); + { + ESP_LOGW(TAG, + "loadbalancer_grid_set_max_import_a failed: %s", + esp_err_to_name(e)); + cJSON_Delete(json); + return; + } } + break; } - case LIMIT_MODE_SOLAR: + case LOADBALANCER_LIMIT_MODE_SOLAR: { cJSON *j = cJSON_GetObjectItem(json, "maxImportW"); + if (cJSON_IsNumber(j)) { int w = j->valueint; + if (w < 0) { - ESP_LOGW(TAG, "evse/limit solar: maxImportW %d invalid", w); + ESP_LOGW(TAG, + "evse/limit solar: maxImportW %d invalid", + w); cJSON_Delete(json); return; } + esp_err_t e = loadbalancer_pv_set_max_import_w(w); if (e != ESP_OK) - ESP_LOGW(TAG, "loadbalancer_pv_set_max_import_w failed: %s", esp_err_to_name(e)); + { + ESP_LOGW(TAG, + "loadbalancer_pv_set_max_import_w failed: %s", + esp_err_to_name(e)); + cJSON_Delete(json); + return; + } } + break; } - case LIMIT_MODE_OFF: + case LOADBALANCER_LIMIT_MODE_GRID_SOLAR: + { + /* + * grid_solar = solar optimizer + grid import cap. + * + * Legacy MQTT flat payload: + * { + * "mode": "grid_solar", + * "gridMaxImportA": 30, + * "maxImportW": 0 + * } + */ + + cJSON *j_grid = cJSON_GetObjectItem(json, "gridMaxImportA"); + if (cJSON_IsNumber(j_grid)) + { + int a = j_grid->valueint; + + if (a < 6 || a > 100) + { + ESP_LOGW(TAG, + "evse/limit grid_solar: gridMaxImportA %d out of range", + a); + cJSON_Delete(json); + return; + } + + esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)a); + if (e != ESP_OK) + { + ESP_LOGW(TAG, + "loadbalancer_grid_set_max_import_a failed: %s", + esp_err_to_name(e)); + cJSON_Delete(json); + return; + } + } + + cJSON *j_pv = cJSON_GetObjectItem(json, "maxImportW"); + if (cJSON_IsNumber(j_pv)) + { + int w = j_pv->valueint; + + if (w < 0) + { + ESP_LOGW(TAG, + "evse/limit grid_solar: maxImportW %d invalid", + w); + cJSON_Delete(json); + return; + } + + esp_err_t e = loadbalancer_pv_set_max_import_w(w); + if (e != ESP_OK) + { + ESP_LOGW(TAG, + "loadbalancer_pv_set_max_import_w failed: %s", + esp_err_to_name(e)); + cJSON_Delete(json); + return; + } + } + break; } - ESP_LOGI(TAG, "MQTT: applying limit mode = %s", limit_mode_to_str(mode)); - mqtt_apply_limit_mode(mode); + case LOADBALANCER_LIMIT_MODE_OFF: + break; + } + + ESP_LOGI(TAG, + "MQTT: requested limit mode = %s", + loadbalancer_limit_mode_to_str(mode)); + + esp_err_t mode_err = loadbalancer_set_limit_mode(mode); + if (mode_err != ESP_OK) + ESP_LOGW(TAG, + "loadbalancer_set_limit_mode failed: %s", + esp_err_to_name(mode_err)); cJSON_Delete(json); mqtt_loadbalancer_publish_state(); @@ -1543,7 +1550,8 @@ esp_err_t mqtt_init(void) xTaskCreate(mqtt_telemetry_task, "mqtt_telemetry_task", 4096, NULL, 3, &s_telemetry_task); } - mqtt_apply_limit_mode(s_limit_mode); + ESP_LOGI(TAG, "MQTT init: loadbalancer owns limit mode; MQTT will only apply broker commands"); + return ESP_OK; } diff --git a/components/protocols/src/protocols.c b/components/protocols/src/protocols.c old mode 100755 new mode 100644 diff --git a/components/rest_api/CMakeLists.txt b/components/rest_api/CMakeLists.txt old mode 100755 new mode 100644 index a1f0345..d0fa168 --- a/components/rest_api/CMakeLists.txt +++ b/components/rest_api/CMakeLists.txt @@ -5,7 +5,7 @@ set(srcs "src/auth_api.c" "src/network_api.c" "src/meters_settings_api.c" - "src/loadbalancing_settings_api.c" + "src/power_settings_api.c" "src/evse_link_config_api.c" "src/dashboard_api.c" "src/scheduler_settings_api.c" diff --git a/components/rest_api/include/auth_api.h b/components/rest_api/include/auth_api.h old mode 100755 new mode 100644 diff --git a/components/rest_api/include/evse_settings_api.h b/components/rest_api/include/evse_settings_api.h old mode 100755 new mode 100644 diff --git a/components/rest_api/include/network_api.h b/components/rest_api/include/network_api.h old mode 100755 new mode 100644 diff --git a/components/rest_api/include/ocpp_api.h b/components/rest_api/include/ocpp_api.h old mode 100755 new mode 100644 diff --git a/components/rest_api/include/power_settings_api.h b/components/rest_api/include/power_settings_api.h new file mode 100644 index 0000000..04ac878 --- /dev/null +++ b/components/rest_api/include/power_settings_api.h @@ -0,0 +1,23 @@ +#ifndef POWER_SETTINGS_API_H_ +#define POWER_SETTINGS_API_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_http_server.h" + +/** + * @brief Register power configuration REST handlers. + * + * Endpoint: + * GET /api/v1/config/power + * POST /api/v1/config/power + */ +void register_power_settings_handlers(httpd_handle_t server, void *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* POWER_SETTINGS_API_H_ */ diff --git a/components/rest_api/include/rest_main.h b/components/rest_api/include/rest_main.h old mode 100755 new mode 100644 diff --git a/components/rest_api/include/scheduler_settings_api.h b/components/rest_api/include/scheduler_settings_api.h old mode 100755 new mode 100644 diff --git a/components/rest_api/src/auth_api.c b/components/rest_api/src/auth_api.c old mode 100755 new mode 100644 diff --git a/components/rest_api/src/evse_settings_api.c b/components/rest_api/src/evse_settings_api.c old mode 100755 new mode 100644 index c4e656d..c9524a3 --- a/components/rest_api/src/evse_settings_api.c +++ b/components/rest_api/src/evse_settings_api.c @@ -8,61 +8,93 @@ #include "esp_log.h" #include "cJSON.h" +#include + static const char *TAG = "evse_settings_api"; -static esp_err_t config_settings_get_handler(httpd_req_t *req) { +static esp_err_t config_settings_get_handler(httpd_req_t *req) +{ httpd_resp_set_type(req, "application/json"); cJSON *config = cJSON_CreateObject(); + if (!config) + { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem"); + return ESP_FAIL; + } + + /* + * currentLimit deixou de pertencer a /config/settings. + * A corrente manual/static é configurada em /api/v1/config/power: + * + * { + * "mode": "manual", + * "manual": { "currentA": 16 } + * } + * + * maxCurrentLimit fica aqui como informação read-only para a UI. + */ cJSON_AddNumberToObject(config, "maxCurrentLimit", evse_get_max_charging_current()); - cJSON_AddNumberToObject(config, "currentLimit", evse_get_charging_current()); cJSON_AddNumberToObject(config, "powerLimit", 0); cJSON_AddNumberToObject(config, "energyLimit", evse_get_consumption_limit()); cJSON_AddNumberToObject(config, "chargingTimeLimit", evse_get_charging_time_limit()); cJSON_AddNumberToObject(config, "temperatureLimit", evse_get_temp_threshold()); cJSON *security = cJSON_CreateObject(); - cJSON_AddBoolToObject(security, "earthFault", false); - cJSON_AddBoolToObject(security, "rcm", false); - cJSON_AddItemToObject(config, "security", security); + if (security) + { + cJSON_AddBoolToObject(security, "earthFault", false); + cJSON_AddBoolToObject(security, "rcm", false); + cJSON_AddItemToObject(config, "security", security); + } char *json_str = cJSON_PrintUnformatted(config); httpd_resp_sendstr(req, json_str ? json_str : "{}"); - if (json_str) free(json_str); + if (json_str) + free(json_str); cJSON_Delete(config); return ESP_OK; } -static esp_err_t config_settings_post_handler(httpd_req_t *req) { +static esp_err_t config_settings_post_handler(httpd_req_t *req) +{ char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (len <= 0) { + if (len <= 0) + { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } + buf[len] = '\0'; + cJSON *json = cJSON_Parse(buf); - if (!json) { + if (!json) + { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } - cJSON *current = cJSON_GetObjectItem(json, "currentLimit"); - if (cJSON_IsNumber(current)) { - int value = current->valueint; - int hw_max = evse_get_max_charging_current(); - if (value < 0) value = 0; - if (value > hw_max) value = hw_max; - evse_set_charging_current(value); + /* + * currentLimit é intencionalmente ignorado neste endpoint. + * A UI nova não o envia. Se algum cliente antigo o enviar, + * não altera a corrente para evitar duas fontes de verdade. + */ + if (cJSON_GetObjectItem(json, "currentLimit")) + { + ESP_LOGW(TAG, "Ignoring legacy currentLimit in /config/settings; use /config/power instead"); } cJSON *temp = cJSON_GetObjectItem(json, "temperatureLimit"); - if (cJSON_IsNumber(temp)) { + if (cJSON_IsNumber(temp)) + { evse_set_temp_threshold(temp->valueint); } - // Campos aceites por compatibilidade de contrato v1. - // Nesta sprint são ignorados caso o firmware ainda não os suporte internamente. + /* + * Campos aceites por compatibilidade de contrato v1. + * Continuam ignorados se o firmware ainda não os suportar internamente. + */ (void)cJSON_GetObjectItem(json, "powerLimit"); (void)cJSON_GetObjectItem(json, "energyLimit"); (void)cJSON_GetObjectItem(json, "chargingTimeLimit"); @@ -73,7 +105,8 @@ static esp_err_t config_settings_post_handler(httpd_req_t *req) { return ESP_OK; } -void register_evse_settings_handlers(httpd_handle_t server, void *ctx) { +void register_evse_settings_handlers(httpd_handle_t server, void *ctx) +{ httpd_uri_t get_uri = { .uri = "/api/v1/config/settings", .method = HTTP_GET, diff --git a/components/rest_api/src/loadbalancing_settings_api.c b/components/rest_api/src/loadbalancing_settings_api.c index b0ee0f6..901c339 100644 --- a/components/rest_api/src/loadbalancing_settings_api.c +++ b/components/rest_api/src/loadbalancing_settings_api.c @@ -8,15 +8,47 @@ #include #include +#include +#include static const char *TAG = "loadbalancing_settings_api"; -// limites simples +/* + * REST API contract + * + * GET /api/v1/config/loadbalancing + * + * { + * "mode": "off" | "manual" | "grid" | "solar", + * "gridLimit": { + * "enabled": true, + * "maxImportA": 30 + * }, + * "solar": { + * "maxImportW": 0 + * } + * } + * + * Notes: + * - "mode" is the source of truth. + * - There is no top-level "enabled"; it is derived from mode != "off". + * - There is no "solar.enabled"; it is derived from mode == "solar". + * - There is no "solar.useGridLimit"; use gridLimit.enabled instead. + * - In mode "solar", gridLimit.enabled=true means solar with grid import cap. + * - In mode "off" or "manual", gridLimit/solar values are kept as saved config. + * + * Compatibility: + * - POST still accepts legacy "enabled". + * - POST still accepts legacy "pv" as alias for "solar". + */ + #define MIN_GRID_A 6 #define MAX_GRID_A 100 #define MIN_PV_W 0 -#define MAX_PV_W 100000 // ajusta se quiseres +#define MAX_PV_W 100000 + +#define REQ_BODY_MAX 768 static esp_err_t send_json(httpd_req_t *req, cJSON *root) { @@ -25,7 +57,9 @@ static esp_err_t send_json(httpd_req_t *req, cJSON *root) char *json_str = cJSON_PrintUnformatted(root); if (!json_str) { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON encode failed"); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "JSON encode failed"); return ESP_FAIL; } @@ -34,38 +68,11 @@ static esp_err_t send_json(httpd_req_t *req, cJSON *root) return err; } -// GET -> payload novo -static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) -{ - cJSON *root = cJSON_CreateObject(); - if (!root) - { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem"); - return ESP_FAIL; - } - - cJSON_AddBoolToObject(root, "enabled", loadbalancer_is_enabled()); - - cJSON *grid = cJSON_CreateObject(); - cJSON_AddItemToObject(root, "gridLimit", grid); - cJSON_AddBoolToObject(grid, "enabled", loadbalancer_grid_is_enabled()); - cJSON_AddNumberToObject(grid, "maxImportA", loadbalancer_grid_get_max_import_a()); - - cJSON *pv = cJSON_CreateObject(); - cJSON_AddItemToObject(root, "pv", pv); - cJSON_AddBoolToObject(pv, "enabled", loadbalancer_pv_is_enabled()); - cJSON_AddNumberToObject(pv, "maxImportW", loadbalancer_pv_get_max_import_w()); - - esp_err_t err = send_json(req, root); - cJSON_Delete(root); - return err; -} - -// lê body de forma robusta (httpd_req_recv pode devolver parcial) static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz) { if (req->content_len <= 0) return ESP_FAIL; + if ((size_t)req->content_len >= buf_sz) return ESP_ERR_NO_MEM; @@ -77,6 +84,7 @@ static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz) int r = httpd_req_recv(req, buf + off, remaining); if (r <= 0) return ESP_FAIL; + off += r; remaining -= r; } @@ -85,31 +93,223 @@ static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz) return ESP_OK; } -// POST -> updates parciais aceites +static bool mode_is_enabled(loadbalancer_limit_mode_t mode) +{ + return mode != LOADBALANCER_LIMIT_MODE_OFF; +} + +static const char *rest_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"; + + default: + return "off"; + } +} + +static bool rest_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) + { + *out = LOADBALANCER_LIMIT_MODE_GRID; + return true; + } + + if (strcmp(str, "solar") == 0) + { + *out = LOADBALANCER_LIMIT_MODE_SOLAR; + return true; + } + + /* + * Aliases legacy. + */ + if (strcmp(str, "pv") == 0) + { + *out = LOADBALANCER_LIMIT_MODE_SOLAR; + return true; + } + + if (strcmp(str, "loadbalancer") == 0) + { + *out = LOADBALANCER_LIMIT_MODE_GRID; + return true; + } + + /* + * Fallback para parser do módulo, caso tenha aliases adicionais. + */ + return loadbalancer_limit_mode_from_str(str, out); +} + +/* + * Compatibilidade com payload antigo: + * + * { + * "enabled": true, + * "gridLimit": { "enabled": true }, + * "pv": { "enabled": false } + * } + */ +static loadbalancer_limit_mode_t derive_mode_from_legacy(bool enabled, + bool grid_enabled, + bool solar_enabled) +{ + if (!enabled) + return LOADBALANCER_LIMIT_MODE_OFF; + + /* + * SOLAR tem prioridade. + * Em modo SOLAR, gridLimit.enabled pode continuar true + * para limitar a importação da rede. + */ + if (solar_enabled) + return LOADBALANCER_LIMIT_MODE_SOLAR; + + if (grid_enabled) + return LOADBALANCER_LIMIT_MODE_GRID; + + /* + * Compatibilidade com UI antiga: + * se vier só {"enabled": true}, assume GRID. + */ + return LOADBALANCER_LIMIT_MODE_GRID; +} + +// GET -> payload novo e limpo, baseado em mode +static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) + { + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + loadbalancer_limit_mode_t mode = loadbalancer_get_limit_mode(); + + /* + * Campo principal. + */ + cJSON_AddStringToObject(root, "mode", rest_limit_mode_to_str(mode)); + + /* + * Configuração do limite de importação da rede. + * + * Em mode=grid: + * gridLimit.enabled deve estar true. + * + * Em mode=solar: + * gridLimit.enabled=true significa solar com limite máximo + * de importação da rede. + */ + cJSON *grid = cJSON_CreateObject(); + if (!grid) + { + cJSON_Delete(root); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + cJSON_AddItemToObject(root, "gridLimit", grid); + cJSON_AddBoolToObject(grid, + "enabled", + loadbalancer_grid_is_enabled()); + cJSON_AddNumberToObject(grid, + "maxImportA", + loadbalancer_grid_get_max_import_a()); + + /* + * Novo bloco REST: solar. + * Internamente ainda usa funções pv_*. + */ + cJSON *solar = cJSON_CreateObject(); + if (!solar) + { + cJSON_Delete(root); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + cJSON_AddItemToObject(root, "solar", solar); + cJSON_AddNumberToObject(solar, + "maxImportW", + loadbalancer_pv_get_max_import_w()); + + esp_err_t err = send_json(req, root); + cJSON_Delete(root); + return err; +} + +// POST -> updates parciais aceites, mas "mode" é o campo principal static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { if (req->content_len <= 0) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Empty body"); return ESP_FAIL; } - if (req->content_len >= 512) + if (req->content_len >= REQ_BODY_MAX) { - httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large"); + httpd_resp_send_err(req, + HTTPD_413_CONTENT_TOO_LARGE, + "Body too large"); return ESP_FAIL; } - char buf[512]; + char buf[REQ_BODY_MAX]; + esp_err_t rb = read_body(req, buf, sizeof(buf)); if (rb == ESP_ERR_NO_MEM) { - httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large"); + httpd_resp_send_err(req, + HTTPD_413_CONTENT_TOO_LARGE, + "Body too large"); return ESP_FAIL; } + if (rb != ESP_OK) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read body"); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Failed to read body"); return ESP_FAIL; } @@ -118,81 +318,419 @@ static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) cJSON *json = cJSON_Parse(buf); if (!json) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Invalid JSON"); return ESP_FAIL; } - // enabled (top-level) - cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled"); - if (enabled_item && cJSON_IsBool(enabled_item)) + if (!cJSON_IsObject(json)) { - loadbalancer_set_enabled(cJSON_IsTrue(enabled_item)); + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "JSON root must be object"); + return ESP_FAIL; } - // gridLimit - cJSON *grid = cJSON_GetObjectItem(json, "gridLimit"); - if (grid && cJSON_IsObject(grid)) + /* + * Base = estado atual. + * Isto permite updates parciais sem limpar configuração. + */ + loadbalancer_limit_mode_t next_mode = loadbalancer_get_limit_mode(); + + bool next_grid_enabled = loadbalancer_grid_is_enabled(); + uint8_t next_grid_max_a = loadbalancer_grid_get_max_import_a(); + + /* + * Internamente o módulo ainda usa pv_*. + * Na REST nova chamamos "solar". + */ + bool next_solar_enabled = loadbalancer_pv_is_enabled(); + int32_t next_solar_max_w = loadbalancer_pv_get_max_import_w(); + + bool mode_provided = false; + + /* + * Campos legacy só são usados para derivar mode se "mode" + * não vier no payload. + */ + bool legacy_operational_hint = false; + bool legacy_enabled = mode_is_enabled(next_mode); + + /* + * Campo novo/principal: + * + * { + * "mode": "off" | "manual" | "grid" | "solar" + * } + */ + cJSON *mode_item = cJSON_GetObjectItem(json, "mode"); + if (mode_item) { - cJSON *g_en = cJSON_GetObjectItem(grid, "enabled"); - if (g_en && cJSON_IsBool(g_en)) + if (!cJSON_IsString(mode_item) || !mode_item->valuestring) { - loadbalancer_grid_set_enabled(cJSON_IsTrue(g_en)); + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "mode must be string"); + return ESP_FAIL; + } + + if (!rest_limit_mode_from_str(mode_item->valuestring, &next_mode)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Invalid mode"); + return ESP_FAIL; + } + + mode_provided = true; + } + + /* + * Campo legacy: + * + * { + * "enabled": true + * } + * + * Não é emitido pelo GET novo. + * Só é aceite para compatibilidade. + */ + cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled"); + if (enabled_item) + { + if (!cJSON_IsBool(enabled_item)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "enabled must be boolean"); + return ESP_FAIL; + } + + legacy_enabled = cJSON_IsTrue(enabled_item); + legacy_operational_hint = true; + } + + /* + * gridLimit + */ + cJSON *grid = cJSON_GetObjectItem(json, "gridLimit"); + if (grid) + { + if (!cJSON_IsObject(grid)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "gridLimit must be object"); + return ESP_FAIL; + } + + cJSON *g_en = cJSON_GetObjectItem(grid, "enabled"); + if (g_en) + { + if (!cJSON_IsBool(g_en)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "gridLimit.enabled must be boolean"); + return ESP_FAIL; + } + + next_grid_enabled = cJSON_IsTrue(g_en); + legacy_operational_hint = true; } cJSON *g_maxA = cJSON_GetObjectItem(grid, "maxImportA"); - if (g_maxA && cJSON_IsNumber(g_maxA)) + if (g_maxA) { + if (!cJSON_IsNumber(g_maxA)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "gridLimit.maxImportA must be number"); + return ESP_FAIL; + } + int maxA = (int)g_maxA->valuedouble; if (maxA < MIN_GRID_A || maxA > MAX_GRID_A) { cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "gridLimit.maxImportA must be 6-100"); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "gridLimit.maxImportA must be 6-100"); return ESP_FAIL; } - esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)maxA); - if (e != ESP_OK) - { - cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set gridLimit.maxImportA"); - return ESP_FAIL; - } + next_grid_max_a = (uint8_t)maxA; } } - // pv - cJSON *pv = cJSON_GetObjectItem(json, "pv"); - if (pv && cJSON_IsObject(pv)) + /* + * Bloco novo: solar + * + * { + * "solar": { + * "maxImportW": 0 + * } + * } + * + * Compatibilidade: + * ainda aceita "pv" como alias antigo de "solar". + */ + cJSON *solar = cJSON_GetObjectItem(json, "solar"); + if (!solar) + solar = cJSON_GetObjectItem(json, "pv"); + + if (solar) { - cJSON *p_en = cJSON_GetObjectItem(pv, "enabled"); - if (p_en && cJSON_IsBool(p_en)) + if (!cJSON_IsObject(solar)) { - loadbalancer_pv_set_enabled(cJSON_IsTrue(p_en)); + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar must be object"); + return ESP_FAIL; } - cJSON *p_maxW = cJSON_GetObjectItem(pv, "maxImportW"); - if (p_maxW && cJSON_IsNumber(p_maxW)) + /* + * Campo legacy: + * + * { + * "solar": { "enabled": true } + * } + * + * Não é emitido pelo GET novo. + * No contrato novo, quem ativa solar é: + * + * { + * "mode": "solar" + * } + */ + cJSON *s_en = cJSON_GetObjectItem(solar, "enabled"); + if (s_en) { - int32_t maxW = (int32_t)p_maxW->valuedouble; + if (!cJSON_IsBool(s_en)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar.enabled must be boolean"); + return ESP_FAIL; + } + + next_solar_enabled = cJSON_IsTrue(s_en); + legacy_operational_hint = true; + } + + /* + * Campo legacy removido do GET: + * + * { + * "solar": { "useGridLimit": true } + * } + * + * Continua aceite no POST e mapeia para gridLimit.enabled. + */ + cJSON *s_use_grid = cJSON_GetObjectItem(solar, "useGridLimit"); + if (s_use_grid) + { + if (!cJSON_IsBool(s_use_grid)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar.useGridLimit must be boolean"); + return ESP_FAIL; + } + + next_grid_enabled = cJSON_IsTrue(s_use_grid); + } + + cJSON *s_maxW = cJSON_GetObjectItem(solar, "maxImportW"); + if (s_maxW) + { + if (!cJSON_IsNumber(s_maxW)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar.maxImportW must be number"); + return ESP_FAIL; + } + + int32_t maxW = (int32_t)s_maxW->valuedouble; if (maxW < MIN_PV_W || maxW > MAX_PV_W) { cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "pv.maxImportW out of range"); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar.maxImportW out of range"); return ESP_FAIL; } - esp_err_t e = loadbalancer_pv_set_max_import_w(maxW); - if (e != ESP_OK) - { - cJSON_Delete(json); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set pv.maxImportW"); - return ESP_FAIL; - } + next_solar_max_w = maxW; } } + /* + * Se o payload não trouxe "mode", aceitamos a semântica antiga + * e derivamos o modo a partir de enabled/grid/pv. + * + * Se o payload só alterou maxImportA ou maxImportW, + * mantemos o modo atual. + */ + if (!mode_provided && legacy_operational_hint) + { + next_mode = derive_mode_from_legacy(legacy_enabled, + next_grid_enabled, + next_solar_enabled); + } + + /* + * Normalização. + * + * "mode" é a fonte de verdade. + * gridLimit e solar são configuração auxiliar. + */ + switch (next_mode) + { + case LOADBALANCER_LIMIT_MODE_OFF: + /* + * OFF não apaga gridLimit nem solar. + * Apenas o modo operacional fica OFF. + */ + break; + + case LOADBALANCER_LIMIT_MODE_MANUAL: + /* + * MANUAL não usa algoritmo dinâmico. + * Não apagar configuração grid/solar. + */ + break; + + case LOADBALANCER_LIMIT_MODE_GRID: + /* + * GRID implica grid limiter ativo. + * Não precisa de solar ativo. + */ + next_grid_enabled = true; + next_solar_enabled = false; + break; + + case LOADBALANCER_LIMIT_MODE_SOLAR: + /* + * SOLAR implica solar ativo. + * Não forçar grid=false. + * + * Em modo solar: + * gridLimit.enabled=true + * significa: + * solar com limite máximo de importação da rede. + */ + next_solar_enabled = true; + break; + + default: + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Invalid mode"); + return ESP_FAIL; + } + + esp_err_t e; + + /* + * 1) Parâmetros. + */ + e = loadbalancer_grid_set_max_import_a(next_grid_max_a); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set gridLimit.maxImportA: %s", + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set gridLimit.maxImportA"); + return ESP_FAIL; + } + + e = loadbalancer_pv_set_max_import_w(next_solar_max_w); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set solar.maxImportW: %s", + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set solar.maxImportW"); + return ESP_FAIL; + } + + /* + * 2) Configuração auxiliar. + * + * Estes setters guardam opções auxiliares. + * O estado operacional principal continua a ser "mode". + */ + loadbalancer_grid_set_enabled(next_grid_enabled); + loadbalancer_pv_set_enabled(next_solar_enabled); + + /* + * 3) Estado operacional principal. + * + * Isto resolve o problema após reset: + * o REST passa a gravar o mesmo campo que o loadbalancer_load_config() + * usa no boot. + */ + e = loadbalancer_set_limit_mode(next_mode); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set loadbalancer mode: %s", + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set loadbalancer mode"); + return ESP_FAIL; + } + + /* + * 4) Garantir persistência antes de responder OK. + */ + e = loadbalancer_flush_config(2000); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to persist loadbalancer config: %s", + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to persist loadbalancer config"); + return ESP_FAIL; + } + + ESP_LOGW(TAG, + "Loadbalancing saved: mode=%s gridEnabled=%d maxA=%u solarEnabled=%d solarMaxW=%ld", + rest_limit_mode_to_str(next_mode), + next_grid_enabled, + (unsigned)next_grid_max_a, + next_solar_enabled, + (long)next_solar_max_w); + cJSON_Delete(json); httpd_resp_sendstr(req, "OK"); return ESP_OK; @@ -204,13 +742,17 @@ void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) .uri = "/api/v1/config/loadbalancing", .method = HTTP_GET, .handler = loadbalancing_config_get_handler, - .user_ctx = ctx}; + .user_ctx = ctx + }; + httpd_register_uri_handler(server, &get_uri); httpd_uri_t post_uri = { .uri = "/api/v1/config/loadbalancing", .method = HTTP_POST, .handler = loadbalancing_config_post_handler, - .user_ctx = ctx}; + .user_ctx = ctx + }; + httpd_register_uri_handler(server, &post_uri); -} +} \ No newline at end of file diff --git a/components/rest_api/src/network_api.c b/components/rest_api/src/network_api.c old mode 100755 new mode 100644 diff --git a/components/rest_api/src/ocpp_api.c b/components/rest_api/src/ocpp_api.c old mode 100755 new mode 100644 diff --git a/components/rest_api/src/power_settings_api.c b/components/rest_api/src/power_settings_api.c new file mode 100644 index 0000000..a13179b --- /dev/null +++ b/components/rest_api/src/power_settings_api.c @@ -0,0 +1,692 @@ +#include "power_settings_api.h" + +#include "loadbalancer.h" +#include "evse_config.h" + +#include "esp_err.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "cJSON.h" + +#include +#include +#include +#include + +static const char *TAG = "power_settings_api"; + +/* + * REST API contract + * + * Endpoint: + * GET /api/v1/config/power + * POST /api/v1/config/power + * + * JSON: + * + * { + * "mode": "off" | "manual" | "grid" | "solar" | "grid_solar", + * "manual": { + * "currentA": 16 + * }, + * "grid": { + * "maxImportA": 30 + * }, + * "solar": { + * "maxImportW": 0 + * } + * } + * + * Notes: + * - "mode" is the source of truth. + * - There is no top-level "enabled". + * - There is no "solar.enabled". + * - There is no "solar.gridCap" in the clean API. + * - "manual.currentA" is the static/default charging current. + * - "grid.maxImportA" is the maximum grid import current. + * - "solar.maxImportW" is the maximum allowed import while in solar mode. + * + * Mode behavior: + * - off: + * no dynamic power policy is active. + * + * - manual: + * no dynamic power policy is active; EVSE uses manual.currentA. + * + * - grid: + * grid import limiter is active and uses grid.maxImportA. + * + * - solar: + * solar/surplus optimizer is active and uses solar.maxImportW. + * The grid limiter is not active. + * + * - grid_solar: + * solar/surplus optimizer is active and also respects grid.maxImportA. + * + * Compatibility: + * - POST still accepts legacy solar.gridCap. If mode=solar and gridCap=true, + * it is converted to mode=grid_solar. + */ + +#define MIN_MANUAL_A 6 + +#define MIN_GRID_A 6 +#define MAX_GRID_A 100 + +#define MIN_SOLAR_W 0 +#define MAX_SOLAR_W 100000 + +#define REQ_BODY_MAX 768 + +static esp_err_t send_json(httpd_req_t *req, cJSON *root) +{ + httpd_resp_set_type(req, "application/json"); + + char *json_str = cJSON_PrintUnformatted(root); + if (!json_str) + { + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "JSON encode failed"); + return ESP_FAIL; + } + + esp_err_t err = httpd_resp_sendstr(req, json_str); + free(json_str); + return err; +} + +static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz) +{ + if (req->content_len <= 0) + return ESP_FAIL; + + if ((size_t)req->content_len >= buf_sz) + return ESP_ERR_NO_MEM; + + int remaining = req->content_len; + int off = 0; + + while (remaining > 0) + { + int r = httpd_req_recv(req, buf + off, remaining); + if (r <= 0) + return ESP_FAIL; + + off += r; + remaining -= r; + } + + buf[off] = '\0'; + return ESP_OK; +} + +static const char *power_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"; + } +} + +static bool power_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 parse_object_if_present(cJSON *root, + const char *name, + cJSON **out) +{ + cJSON *obj = cJSON_GetObjectItem(root, name); + if (!obj) + { + *out = NULL; + return ESP_OK; + } + + if (!cJSON_IsObject(obj)) + return ESP_ERR_INVALID_ARG; + + *out = obj; + return ESP_OK; +} + +static esp_err_t parse_int_field_if_present(cJSON *obj, + const char *name, + int32_t min_value, + int32_t max_value, + int32_t *out, + bool *provided) +{ + if (provided) + *provided = false; + + if (!obj) + return ESP_OK; + + cJSON *item = cJSON_GetObjectItem(obj, name); + if (!item) + return ESP_OK; + + if (!cJSON_IsNumber(item)) + return ESP_ERR_INVALID_ARG; + + int32_t value = (int32_t)item->valuedouble; + + if (value < min_value || value > max_value) + return ESP_ERR_INVALID_ARG; + + *out = value; + + if (provided) + *provided = true; + + return ESP_OK; +} + +static esp_err_t power_config_get_handler(httpd_req_t *req) +{ + cJSON *root = cJSON_CreateObject(); + if (!root) + { + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + loadbalancer_limit_mode_t mode = loadbalancer_get_limit_mode(); + + cJSON_AddStringToObject(root, "mode", power_mode_to_str(mode)); + + cJSON *manual = cJSON_CreateObject(); + if (!manual) + { + cJSON_Delete(root); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + cJSON_AddItemToObject(root, "manual", manual); + cJSON_AddNumberToObject(manual, + "currentA", + (int)evse_get_charging_current()); + + cJSON *grid = cJSON_CreateObject(); + if (!grid) + { + cJSON_Delete(root); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + cJSON_AddItemToObject(root, "grid", grid); + cJSON_AddNumberToObject(grid, + "maxImportA", + (int)loadbalancer_grid_get_max_import_a()); + + cJSON *solar = cJSON_CreateObject(); + if (!solar) + { + cJSON_Delete(root); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "No mem"); + return ESP_FAIL; + } + + cJSON_AddItemToObject(root, "solar", solar); + cJSON_AddNumberToObject(solar, + "maxImportW", + (int)loadbalancer_pv_get_max_import_w()); + + esp_err_t err = send_json(req, root); + cJSON_Delete(root); + return err; +} + +static esp_err_t power_config_post_handler(httpd_req_t *req) +{ + if (req->content_len <= 0) + { + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Empty body"); + return ESP_FAIL; + } + + if (req->content_len >= REQ_BODY_MAX) + { + httpd_resp_send_err(req, + HTTPD_413_CONTENT_TOO_LARGE, + "Body too large"); + return ESP_FAIL; + } + + char buf[REQ_BODY_MAX]; + + esp_err_t rb = read_body(req, buf, sizeof(buf)); + if (rb == ESP_ERR_NO_MEM) + { + httpd_resp_send_err(req, + HTTPD_413_CONTENT_TOO_LARGE, + "Body too large"); + return ESP_FAIL; + } + + if (rb != ESP_OK) + { + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Failed to read body"); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "POST: %s", buf); + + cJSON *json = cJSON_Parse(buf); + if (!json) + { + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Invalid JSON"); + return ESP_FAIL; + } + + if (!cJSON_IsObject(json)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "JSON root must be object"); + return ESP_FAIL; + } + + /* + * Base = current runtime/config state. + * This allows partial updates without clearing other values. + */ + loadbalancer_limit_mode_t next_mode = loadbalancer_get_limit_mode(); + + uint16_t next_manual_current_a = evse_get_charging_current(); + uint8_t next_grid_max_a = loadbalancer_grid_get_max_import_a(); + int32_t next_solar_max_w = loadbalancer_pv_get_max_import_w(); + + /* + * mode + */ + cJSON *mode_item = cJSON_GetObjectItem(json, "mode"); + if (mode_item) + { + if (!cJSON_IsString(mode_item) || !mode_item->valuestring) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "mode must be string"); + return ESP_FAIL; + } + + if (!power_mode_from_str(mode_item->valuestring, &next_mode)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Invalid mode"); + return ESP_FAIL; + } + } + + /* + * manual + */ + cJSON *manual = NULL; + esp_err_t e = parse_object_if_present(json, "manual", &manual); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "manual must be object"); + return ESP_FAIL; + } + + if (manual) + { + int32_t current_a = (int32_t)next_manual_current_a; + bool provided = false; + + e = parse_int_field_if_present(manual, + "currentA", + MIN_MANUAL_A, + evse_get_max_charging_current(), + ¤t_a, + &provided); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "manual.currentA out of range"); + return ESP_FAIL; + } + + if (provided) + next_manual_current_a = (uint16_t)current_a; + } + + /* + * grid + */ + cJSON *grid = NULL; + e = parse_object_if_present(json, "grid", &grid); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "grid must be object"); + return ESP_FAIL; + } + + if (grid) + { + int32_t max_import_a = (int32_t)next_grid_max_a; + bool provided = false; + + e = parse_int_field_if_present(grid, + "maxImportA", + MIN_GRID_A, + MAX_GRID_A, + &max_import_a, + &provided); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "grid.maxImportA must be 6-100"); + return ESP_FAIL; + } + + if (provided) + next_grid_max_a = (uint8_t)max_import_a; + } + + /* + * solar + */ + cJSON *solar = NULL; + e = parse_object_if_present(json, "solar", &solar); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar must be object"); + return ESP_FAIL; + } + + if (solar) + { + int32_t max_import_w = next_solar_max_w; + bool provided = false; + + e = parse_int_field_if_present(solar, + "maxImportW", + MIN_SOLAR_W, + MAX_SOLAR_W, + &max_import_w, + &provided); + if (e != ESP_OK) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar.maxImportW out of range"); + return ESP_FAIL; + } + + if (provided) + next_solar_max_w = max_import_w; + + /* + * Legacy compatibility only: + * + * Old clients may still send: + * "mode": "solar", + * "solar": { "gridCap": true } + * + * The clean API uses: + * "mode": "grid_solar" + * + * If gridCap is present, convert the mode accordingly. + */ + cJSON *legacy_grid_cap = cJSON_GetObjectItem(solar, "gridCap"); + if (legacy_grid_cap) + { + if (!cJSON_IsBool(legacy_grid_cap)) + { + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "solar.gridCap must be boolean"); + return ESP_FAIL; + } + + if (next_mode == LOADBALANCER_LIMIT_MODE_SOLAR && + cJSON_IsTrue(legacy_grid_cap)) + { + next_mode = LOADBALANCER_LIMIT_MODE_GRID_SOLAR; + } + else if (next_mode == LOADBALANCER_LIMIT_MODE_GRID_SOLAR && + !cJSON_IsTrue(legacy_grid_cap)) + { + next_mode = LOADBALANCER_LIMIT_MODE_SOLAR; + } + } + } + + /* + * Normalize auxiliary flags from the selected mode. + * + * mode is the source of truth. + */ + bool apply_grid_limiter = false; + bool apply_solar_optimizer = false; + + switch (next_mode) + { + case LOADBALANCER_LIMIT_MODE_OFF: + apply_grid_limiter = false; + apply_solar_optimizer = false; + break; + + case LOADBALANCER_LIMIT_MODE_MANUAL: + apply_grid_limiter = false; + apply_solar_optimizer = false; + break; + + case LOADBALANCER_LIMIT_MODE_GRID: + apply_grid_limiter = true; + apply_solar_optimizer = false; + break; + + case LOADBALANCER_LIMIT_MODE_SOLAR: + apply_grid_limiter = false; + apply_solar_optimizer = true; + break; + + case LOADBALANCER_LIMIT_MODE_GRID_SOLAR: + apply_grid_limiter = true; + apply_solar_optimizer = true; + break; + + default: + cJSON_Delete(json); + httpd_resp_send_err(req, + HTTPD_400_BAD_REQUEST, + "Invalid mode"); + return ESP_FAIL; + } + + /* + * 1) Static/manual current. + */ + e = evse_set_charging_current(next_manual_current_a); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set manual.currentA=%u: %s", + (unsigned)next_manual_current_a, + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set manual.currentA"); + return ESP_FAIL; + } + + /* + * 2) Loadbalancer parameters. + */ + e = loadbalancer_grid_set_max_import_a(next_grid_max_a); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set grid.maxImportA=%u: %s", + (unsigned)next_grid_max_a, + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set grid.maxImportA"); + return ESP_FAIL; + } + + e = loadbalancer_pv_set_max_import_w(next_solar_max_w); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set solar.maxImportW=%ld: %s", + (long)next_solar_max_w, + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set solar.maxImportW"); + return ESP_FAIL; + } + + /* + * 3) Auxiliary flags derived from mode. + */ + loadbalancer_grid_set_enabled(apply_grid_limiter); + loadbalancer_pv_set_enabled(apply_solar_optimizer); + + /* + * 4) Operational mode. + */ + e = loadbalancer_set_limit_mode(next_mode); + if (e != ESP_OK) + { + cJSON_Delete(json); + ESP_LOGE(TAG, + "Failed to set mode=%s: %s", + power_mode_to_str(next_mode), + esp_err_to_name(e)); + httpd_resp_send_err(req, + HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to set power mode"); + return ESP_FAIL; + } + + ESP_LOGW(TAG, + "Power config saved: mode=%s manual=%uA gridMax=%uA solarMax=%ldW", + power_mode_to_str(next_mode), + (unsigned)next_manual_current_a, + (unsigned)next_grid_max_a, + (long)next_solar_max_w); + + cJSON_Delete(json); + httpd_resp_sendstr(req, "OK"); + return ESP_OK; +} + +void register_power_settings_handlers(httpd_handle_t server, void *ctx) +{ + httpd_uri_t get_uri = { + .uri = "/api/v1/config/power", + .method = HTTP_GET, + .handler = power_config_get_handler, + .user_ctx = ctx}; + + httpd_register_uri_handler(server, &get_uri); + + httpd_uri_t post_uri = { + .uri = "/api/v1/config/power", + .method = HTTP_POST, + .handler = power_config_post_handler, + .user_ctx = ctx}; + + httpd_register_uri_handler(server, &post_uri); +} diff --git a/components/rest_api/src/rest_main.c b/components/rest_api/src/rest_main.c old mode 100755 new mode 100644 index 9e74548..fd305ec --- a/components/rest_api/src/rest_main.c +++ b/components/rest_api/src/rest_main.c @@ -1,7 +1,7 @@ #include "rest_main.h" #include "evse_settings_api.h" #include "meters_settings_api.h" -#include "loadbalancing_settings_api.h" +#include "power_settings_api.h" #include "evse_link_config_api.h" #include "network_api.h" #include "ocpp_api.h" @@ -59,7 +59,7 @@ esp_err_t rest_server_init(const char *base_path) register_auth_handlers(server, ctx); register_dashboard_handlers(server, ctx); register_meters_settings_handlers(server, ctx); - register_loadbalancing_settings_handlers(server, ctx); + register_power_settings_handlers(server, ctx); register_link_config_handlers(server, ctx); register_meters_data_handlers(server, ctx); register_scheduler_settings_handlers(server, ctx); diff --git a/components/rest_api/src/scheduler_settings_api.c b/components/rest_api/src/scheduler_settings_api.c old mode 100755 new mode 100644 diff --git a/components/rest_api/webfolder/assets/index-BSlegejp.js b/components/rest_api/webfolder/assets/index-BSlegejp.js new file mode 100644 index 0000000..09bb51e --- /dev/null +++ b/components/rest_api/webfolder/assets/index-BSlegejp.js @@ -0,0 +1,51 @@ +(function(){const f=document.createElement("link").relList;if(f&&f.supports&&f.supports("modulepreload"))return;for(const m of document.querySelectorAll('link[rel="modulepreload"]'))s(m);new MutationObserver(m=>{for(const g of m)if(g.type==="childList")for(const p of g.addedNodes)p.tagName==="LINK"&&p.rel==="modulepreload"&&s(p)}).observe(document,{childList:!0,subtree:!0});function d(m){const g={};return m.integrity&&(g.integrity=m.integrity),m.referrerPolicy&&(g.referrerPolicy=m.referrerPolicy),m.crossOrigin==="use-credentials"?g.credentials="include":m.crossOrigin==="anonymous"?g.credentials="omit":g.credentials="same-origin",g}function s(m){if(m.ep)return;m.ep=!0;const g=d(m);fetch(m.href,g)}})();function Nm(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var wr={exports:{}},Yn={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Wd;function fg(){if(Wd)return Yn;Wd=1;var i=Symbol.for("react.transitional.element"),f=Symbol.for("react.fragment");function d(s,m,g){var p=null;if(g!==void 0&&(p=""+g),m.key!==void 0&&(p=""+m.key),"key"in m){g={};for(var R in m)R!=="key"&&(g[R]=m[R])}else g=m;return m=g.ref,{$$typeof:i,type:s,key:p,ref:m!==void 0?m:null,props:g}}return Yn.Fragment=f,Yn.jsx=d,Yn.jsxs=d,Yn}var Fd;function og(){return Fd||(Fd=1,wr.exports=fg()),wr.exports}var c=og(),Lr={exports:{}},ie={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Id;function dg(){if(Id)return ie;Id=1;var i=Symbol.for("react.transitional.element"),f=Symbol.for("react.portal"),d=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),m=Symbol.for("react.profiler"),g=Symbol.for("react.consumer"),p=Symbol.for("react.context"),R=Symbol.for("react.forward_ref"),v=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),N=Symbol.for("react.lazy"),O=Symbol.iterator;function j(x){return x===null||typeof x!="object"?null:(x=O&&x[O]||x["@@iterator"],typeof x=="function"?x:null)}var B={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},L=Object.assign,G={};function Z(x,q,$){this.props=x,this.context=q,this.refs=G,this.updater=$||B}Z.prototype.isReactComponent={},Z.prototype.setState=function(x,q){if(typeof x!="object"&&typeof x!="function"&&x!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,x,q,"setState")},Z.prototype.forceUpdate=function(x){this.updater.enqueueForceUpdate(this,x,"forceUpdate")};function Y(){}Y.prototype=Z.prototype;function Q(x,q,$){this.props=x,this.context=q,this.refs=G,this.updater=$||B}var C=Q.prototype=new Y;C.constructor=Q,L(C,Z.prototype),C.isPureReactComponent=!0;var X=Array.isArray,V={H:null,A:null,T:null,S:null,V:null},ce=Object.prototype.hasOwnProperty;function U(x,q,$,J,P,me){return $=me.ref,{$$typeof:i,type:x,key:q,ref:$!==void 0?$:null,props:me}}function te(x,q){return U(x.type,q,void 0,void 0,void 0,x.props)}function K(x){return typeof x=="object"&&x!==null&&x.$$typeof===i}function ne(x){var q={"=":"=0",":":"=2"};return"$"+x.replace(/[=:]/g,function($){return q[$]})}var De=/\/+/g;function Oe(x,q){return typeof x=="object"&&x!==null&&x.key!=null?ne(""+x.key):q.toString(36)}function Ut(){}function tt(x){switch(x.status){case"fulfilled":return x.value;case"rejected":throw x.reason;default:switch(typeof x.status=="string"?x.then(Ut,Ut):(x.status="pending",x.then(function(q){x.status==="pending"&&(x.status="fulfilled",x.value=q)},function(q){x.status==="pending"&&(x.status="rejected",x.reason=q)})),x.status){case"fulfilled":return x.value;case"rejected":throw x.reason}}throw x}function Xe(x,q,$,J,P){var me=typeof x;(me==="undefined"||me==="boolean")&&(x=null);var ue=!1;if(x===null)ue=!0;else switch(me){case"bigint":case"string":case"number":ue=!0;break;case"object":switch(x.$$typeof){case i:case f:ue=!0;break;case N:return ue=x._init,Xe(ue(x._payload),q,$,J,P)}}if(ue)return P=P(x),ue=J===""?"."+Oe(x,0):J,X(P)?($="",ue!=null&&($=ue.replace(De,"$&/")+"/"),Xe(P,q,$,"",function(cl){return cl})):P!=null&&(K(P)&&(P=te(P,$+(P.key==null||x&&x.key===P.key?"":(""+P.key).replace(De,"$&/")+"/")+ue)),q.push(P)),1;ue=0;var st=J===""?".":J+":";if(X(x))for(var Ne=0;Ne>>1,x=z[pe];if(0>>1;pem(J,I))Pm(me,J)?(z[pe]=me,z[P]=I,pe=P):(z[pe]=J,z[$]=I,pe=$);else if(Pm(me,I))z[pe]=me,z[P]=I,pe=P;else break e}}return k}function m(z,k){var I=z.sortIndex-k.sortIndex;return I!==0?I:z.id-k.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var g=performance;i.unstable_now=function(){return g.now()}}else{var p=Date,R=p.now();i.unstable_now=function(){return p.now()-R}}var v=[],h=[],N=1,O=null,j=3,B=!1,L=!1,G=!1,Z=!1,Y=typeof setTimeout=="function"?setTimeout:null,Q=typeof clearTimeout=="function"?clearTimeout:null,C=typeof setImmediate<"u"?setImmediate:null;function X(z){for(var k=d(h);k!==null;){if(k.callback===null)s(h);else if(k.startTime<=z)s(h),k.sortIndex=k.expirationTime,f(v,k);else break;k=d(h)}}function V(z){if(G=!1,X(z),!L)if(d(v)!==null)L=!0,ce||(ce=!0,Oe());else{var k=d(h);k!==null&&Xe(V,k.startTime-z)}}var ce=!1,U=-1,te=5,K=-1;function ne(){return Z?!0:!(i.unstable_now()-Kz&&ne());){var pe=O.callback;if(typeof pe=="function"){O.callback=null,j=O.priorityLevel;var x=pe(O.expirationTime<=z);if(z=i.unstable_now(),typeof x=="function"){O.callback=x,X(z),k=!0;break t}O===d(v)&&s(v),X(z)}else s(v);O=d(v)}if(O!==null)k=!0;else{var q=d(h);q!==null&&Xe(V,q.startTime-z),k=!1}}break e}finally{O=null,j=I,B=!1}k=void 0}}finally{k?Oe():ce=!1}}}var Oe;if(typeof C=="function")Oe=function(){C(De)};else if(typeof MessageChannel<"u"){var Ut=new MessageChannel,tt=Ut.port2;Ut.port1.onmessage=De,Oe=function(){tt.postMessage(null)}}else Oe=function(){Y(De,0)};function Xe(z,k){U=Y(function(){z(i.unstable_now())},k)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(z){z.callback=null},i.unstable_forceFrameRate=function(z){0>z||125pe?(z.sortIndex=I,f(h,z),d(v)===null&&z===d(h)&&(G?(Q(U),U=-1):G=!0,Xe(V,I-pe))):(z.sortIndex=x,f(v,z),L||B||(L=!0,ce||(ce=!0,Oe()))),z},i.unstable_shouldYield=ne,i.unstable_wrapCallback=function(z){var k=j;return function(){var I=j;j=k;try{return z.apply(this,arguments)}finally{j=I}}}}(qr)),qr}var tm;function gg(){return tm||(tm=1,Br.exports=hg()),Br.exports}var Yr={exports:{}},Pe={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var lm;function yg(){if(lm)return Pe;lm=1;var i=Jr();function f(v){var h="https://react.dev/errors/"+v;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(f){console.error(f)}}return i(),Yr.exports=yg(),Yr.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var nm;function pg(){if(nm)return Gn;nm=1;var i=gg(),f=Jr(),d=vg();function s(e){var t="https://react.dev/errors/"+e;if(1x||(e.current=pe[x],pe[x]=null,x--)}function J(e,t){x++,pe[x]=e.current,e.current=t}var P=q(null),me=q(null),ue=q(null),st=q(null);function Ne(e,t){switch(J(ue,t),J(me,e),J(P,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Nd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Nd(t),e=Ad(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}$(P),J(P,e)}function cl(){$(P),$(me),$(ue)}function bi(e){e.memoizedState!==null&&J(st,e);var t=P.current,l=Ad(t,e.type);t!==l&&(J(me,e),J(P,l))}function Jn(e){me.current===e&&($(P),$(me)),st.current===e&&($(st),wn._currentValue=I)}var Si=Object.prototype.hasOwnProperty,Ei=i.unstable_scheduleCallback,ji=i.unstable_cancelCallback,Xm=i.unstable_shouldYield,Qm=i.unstable_requestPaint,wt=i.unstable_now,Vm=i.unstable_getCurrentPriorityLevel,ls=i.unstable_ImmediatePriority,as=i.unstable_UserBlockingPriority,$n=i.unstable_NormalPriority,Zm=i.unstable_LowPriority,ns=i.unstable_IdlePriority,Km=i.log,km=i.unstable_setDisableYieldValue,Xa=null,ft=null;function rl(e){if(typeof Km=="function"&&km(e),ft&&typeof ft.setStrictMode=="function")try{ft.setStrictMode(Xa,e)}catch{}}var ot=Math.clz32?Math.clz32:Wm,Jm=Math.log,$m=Math.LN2;function Wm(e){return e>>>=0,e===0?32:31-(Jm(e)/$m|0)|0}var Wn=256,Fn=4194304;function wl(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function In(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,r=e.pingedLanes;e=e.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=wl(a):(r&=o,r!==0?n=wl(r):l||(l=o&~e,l!==0&&(n=wl(l))))):(o=a&~u,o!==0?n=wl(o):r!==0?n=wl(r):l||(l=a&~e,l!==0&&(n=wl(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function Qa(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Fm(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function us(){var e=Wn;return Wn<<=1,(Wn&4194048)===0&&(Wn=256),e}function is(){var e=Fn;return Fn<<=1,(Fn&62914560)===0&&(Fn=4194304),e}function Ni(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function Va(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Im(e,t,l,a,n,u){var r=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var o=e.entanglements,y=e.expirationTimes,T=e.hiddenUpdates;for(l=r&~l;0)":-1n||y[a]!==T[n]){var _=` +`+y[a].replace(" at new "," at ");return e.displayName&&_.includes("")&&(_=_.replace("",e.displayName)),_}while(1<=a&&0<=n);break}}}finally{Oi=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?na(l):""}function nh(e){switch(e.tag){case 26:case 27:case 5:return na(e.type);case 16:return na("Lazy");case 13:return na("Suspense");case 19:return na("SuspenseList");case 0:case 15:return zi(e.type,!1);case 11:return zi(e.type.render,!1);case 1:return zi(e.type,!0);case 31:return na("Activity");default:return""}}function ys(e){try{var t="";do t+=nh(e),e=e.return;while(e);return t}catch(l){return` +Error generating stack: `+l.message+` +`+l.stack}}function xt(e){switch(typeof e){case"bigint":case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function vs(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function uh(e){var t=vs(e)?"checked":"value",l=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),a=""+e[t];if(!e.hasOwnProperty(t)&&typeof l<"u"&&typeof l.get=="function"&&typeof l.set=="function"){var n=l.get,u=l.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return n.call(this)},set:function(r){a=""+r,u.call(this,r)}}),Object.defineProperty(e,t,{enumerable:l.enumerable}),{getValue:function(){return a},setValue:function(r){a=""+r},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function tu(e){e._valueTracker||(e._valueTracker=uh(e))}function ps(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var l=t.getValue(),a="";return e&&(a=vs(e)?e.checked?"true":"false":e.value),e=a,e!==l?(t.setValue(e),!0):!1}function lu(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var ih=/[\n"\\]/g;function bt(e){return e.replace(ih,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Ci(e,t,l,a,n,u,r,o){e.name="",r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"?e.type=r:e.removeAttribute("type"),t!=null?r==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+xt(t)):e.value!==""+xt(t)&&(e.value=""+xt(t)):r!=="submit"&&r!=="reset"||e.removeAttribute("value"),t!=null?_i(e,r,xt(t)):l!=null?_i(e,r,xt(l)):a!=null&&e.removeAttribute("value"),n==null&&u!=null&&(e.defaultChecked=!!u),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?e.name=""+xt(o):e.removeAttribute("name")}function xs(e,t,l,a,n,u,r,o){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(e.type=u),t!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||t!=null))return;l=l!=null?""+xt(l):"",t=t!=null?""+xt(t):l,o||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=o?e.checked:!!a,e.defaultChecked=!!a,r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"&&(e.name=r)}function _i(e,t,l){t==="number"&&lu(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function ua(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Bi=!1;if(Vt)try{var Ja={};Object.defineProperty(Ja,"passive",{get:function(){Bi=!0}}),window.addEventListener("test",Ja,Ja),window.removeEventListener("test",Ja,Ja)}catch{Bi=!1}var fl=null,qi=null,nu=null;function Ts(){if(nu)return nu;var e,t=qi,l=t.length,a,n="value"in fl?fl.value:fl.textContent,u=n.length;for(e=0;e=Fa),Cs=" ",_s=!1;function Us(e,t){switch(e){case"keyup":return Uh.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ws(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var sa=!1;function Lh(e,t){switch(e){case"compositionend":return ws(t);case"keypress":return t.which!==32?null:(_s=!0,Cs);case"textInput":return e=t.data,e===Cs&&_s?null:e;default:return null}}function Hh(e,t){if(sa)return e==="compositionend"||!Vi&&Us(e,t)?(e=Ts(),nu=qi=fl=null,sa=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=Qs(l)}}function Zs(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Zs(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ks(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=lu(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=lu(e.document)}return t}function ki(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Zh=Vt&&"documentMode"in document&&11>=document.documentMode,fa=null,Ji=null,tn=null,$i=!1;function ks(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;$i||fa==null||fa!==lu(a)||(a=fa,"selectionStart"in a&&ki(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),tn&&en(tn,a)||(tn=a,a=Ju(Ji,"onSelect"),0>=r,n-=r,Kt=1<<32-ot(t)+n|l<u?u:8;var r=z.T,o={};z.T=o,Uc(e,!1,t,l);try{var y=n(),T=z.S;if(T!==null&&T(o,y),y!==null&&typeof y=="object"&&typeof y.then=="function"){var _=e0(y,a);vn(e,t,_,vt(e))}else vn(e,t,a,vt(e))}catch(H){vn(e,t,{then:function(){},status:"rejected",reason:H},vt())}finally{k.p=u,z.T=r}}function u0(){}function Cc(e,t,l,a){if(e.tag!==5)throw Error(s(476));var n=$f(e).queue;Jf(e,n,t,I,l===null?u0:function(){return Wf(e),l(a)})}function $f(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:I,baseState:I,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Wt,lastRenderedState:I},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Wt,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Wf(e){var t=$f(e).next.queue;vn(e,t,{},vt())}function _c(){return Ie(wn)}function Ff(){return He().memoizedState}function If(){return He().memoizedState}function i0(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=vt();e=ml(l);var a=hl(t,e,l);a!==null&&(pt(a,t,l),on(a,t,l)),t={cache:sc()},e.payload=t;return}t=t.return}}function c0(e,t,l){var a=vt();l={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null},Mu(e)?eo(t,l):(l=Pi(e,t,l,a),l!==null&&(pt(l,e,a),to(l,t,a)))}function Pf(e,t,l){var a=vt();vn(e,t,l,a)}function vn(e,t,l,a){var n={lane:a,revertLane:0,action:l,hasEagerState:!1,eagerState:null,next:null};if(Mu(e))eo(t,n);else{var u=e.alternate;if(e.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var r=t.lastRenderedState,o=u(r,l);if(n.hasEagerState=!0,n.eagerState=o,dt(o,r))return ou(e,t,n,0),je===null&&fu(),!1}catch{}finally{}if(l=Pi(e,t,n,a),l!==null)return pt(l,e,a),to(l,t,a),!0}return!1}function Uc(e,t,l,a){if(a={lane:2,revertLane:dr(),action:a,hasEagerState:!1,eagerState:null,next:null},Mu(e)){if(t)throw Error(s(479))}else t=Pi(e,l,a,2),t!==null&&pt(t,e,2)}function Mu(e){var t=e.alternate;return e===re||t!==null&&t===re}function eo(e,t){ba=Eu=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function to(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,rs(e,l)}}var Du={readContext:Ie,use:Nu,useCallback:_e,useContext:_e,useEffect:_e,useImperativeHandle:_e,useLayoutEffect:_e,useInsertionEffect:_e,useMemo:_e,useReducer:_e,useRef:_e,useState:_e,useDebugValue:_e,useDeferredValue:_e,useTransition:_e,useSyncExternalStore:_e,useId:_e,useHostTransitionStatus:_e,useFormState:_e,useActionState:_e,useOptimistic:_e,useMemoCache:_e,useCacheRefresh:_e},lo={readContext:Ie,use:Nu,useCallback:function(e,t){return ut().memoizedState=[e,t===void 0?null:t],e},useContext:Ie,useEffect:qf,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Ru(4194308,4,Qf.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Ru(4194308,4,e,t)},useInsertionEffect:function(e,t){Ru(4,2,e,t)},useMemo:function(e,t){var l=ut();t=t===void 0?null:t;var a=e();if(Jl){rl(!0);try{e()}finally{rl(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=ut();if(l!==void 0){var n=l(t);if(Jl){rl(!0);try{l(t)}finally{rl(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=c0.bind(null,re,e),[a.memoizedState,e]},useRef:function(e){var t=ut();return e={current:e},t.memoizedState=e},useState:function(e){e=Mc(e);var t=e.queue,l=Pf.bind(null,re,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:Oc,useDeferredValue:function(e,t){var l=ut();return zc(l,e,t)},useTransition:function(){var e=Mc(!1);return e=Jf.bind(null,re,e.queue,!0,!1),ut().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=re,n=ut();if(ge){if(l===void 0)throw Error(s(407));l=l()}else{if(l=t(),je===null)throw Error(s(349));(de&124)!==0||jf(a,t,l)}n.memoizedState=l;var u={value:l,getSnapshot:t};return n.queue=u,qf(Af.bind(null,a,u,e),[e]),a.flags|=2048,Ea(9,Tu(),Nf.bind(null,a,u,l,t),null),l},useId:function(){var e=ut(),t=je.identifierPrefix;if(ge){var l=kt,a=Kt;l=(a&~(1<<32-ot(a)-1)).toString(32)+l,t="«"+t+"R"+l,l=ju++,0le?(Ke=F,F=null):Ke=F.sibling;var he=M(E,F,A[le],w);if(he===null){F===null&&(F=Ke);break}e&&F&&he.alternate===null&&t(E,F),b=u(he,b,le),se===null?W=he:se.sibling=he,se=he,F=Ke}if(le===A.length)return l(E,F),ge&&Xl(E,le),W;if(F===null){for(;lele?(Ke=F,F=null):Ke=F.sibling;var zl=M(E,F,he.value,w);if(zl===null){F===null&&(F=Ke);break}e&&F&&zl.alternate===null&&t(E,F),b=u(zl,b,le),se===null?W=zl:se.sibling=zl,se=zl,F=Ke}if(he.done)return l(E,F),ge&&Xl(E,le),W;if(F===null){for(;!he.done;le++,he=A.next())he=H(E,he.value,w),he!==null&&(b=u(he,b,le),se===null?W=he:se.sibling=he,se=he);return ge&&Xl(E,le),W}for(F=a(F);!he.done;le++,he=A.next())he=D(F,E,le,he.value,w),he!==null&&(e&&he.alternate!==null&&F.delete(he.key===null?le:he.key),b=u(he,b,le),se===null?W=he:se.sibling=he,se=he);return e&&F.forEach(function(sg){return t(E,sg)}),ge&&Xl(E,le),W}function Se(E,b,A,w){if(typeof A=="object"&&A!==null&&A.type===L&&A.key===null&&(A=A.props.children),typeof A=="object"&&A!==null){switch(A.$$typeof){case j:e:{for(var W=A.key;b!==null;){if(b.key===W){if(W=A.type,W===L){if(b.tag===7){l(E,b.sibling),w=n(b,A.props.children),w.return=E,E=w;break e}}else if(b.elementType===W||typeof W=="object"&&W!==null&&W.$$typeof===te&&no(W)===b.type){l(E,b.sibling),w=n(b,A.props),xn(w,A),w.return=E,E=w;break e}l(E,b);break}else t(E,b);b=b.sibling}A.type===L?(w=Yl(A.props.children,E.mode,w,A.key),w.return=E,E=w):(w=mu(A.type,A.key,A.props,null,E.mode,w),xn(w,A),w.return=E,E=w)}return r(E);case B:e:{for(W=A.key;b!==null;){if(b.key===W)if(b.tag===4&&b.stateNode.containerInfo===A.containerInfo&&b.stateNode.implementation===A.implementation){l(E,b.sibling),w=n(b,A.children||[]),w.return=E,E=w;break e}else{l(E,b);break}else t(E,b);b=b.sibling}w=lc(A,E.mode,w),w.return=E,E=w}return r(E);case te:return W=A._init,A=W(A._payload),Se(E,b,A,w)}if(Xe(A))return ae(E,b,A,w);if(Oe(A)){if(W=Oe(A),typeof W!="function")throw Error(s(150));return A=W.call(A),ee(E,b,A,w)}if(typeof A.then=="function")return Se(E,b,Ou(A),w);if(A.$$typeof===C)return Se(E,b,vu(E,A),w);zu(E,A)}return typeof A=="string"&&A!==""||typeof A=="number"||typeof A=="bigint"?(A=""+A,b!==null&&b.tag===6?(l(E,b.sibling),w=n(b,A),w.return=E,E=w):(l(E,b),w=tc(A,E.mode,w),w.return=E,E=w),r(E)):l(E,b)}return function(E,b,A,w){try{pn=0;var W=Se(E,b,A,w);return ja=null,W}catch(F){if(F===sn||F===xu)throw F;var se=mt(29,F,null,E.mode);return se.lanes=w,se.return=E,se}finally{}}}var Na=uo(!0),io=uo(!1),At=q(null),Ht=null;function yl(e){var t=e.alternate;J(Ye,Ye.current&1),J(At,e),Ht===null&&(t===null||xa.current!==null||t.memoizedState!==null)&&(Ht=e)}function co(e){if(e.tag===22){if(J(Ye,Ye.current),J(At,e),Ht===null){var t=e.alternate;t!==null&&t.memoizedState!==null&&(Ht=e)}}else vl()}function vl(){J(Ye,Ye.current),J(At,At.current)}function Ft(e){$(At),Ht===e&&(Ht=null),$(Ye)}var Ye=q(0);function Cu(e){for(var t=e;t!==null;){if(t.tag===13){var l=t.memoizedState;if(l!==null&&(l=l.dehydrated,l===null||l.data==="$?"||Nr(l)))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&128)!==0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}function wc(e,t,l,a){t=e.memoizedState,l=l(a,t),l=l==null?t:N({},t,l),e.memoizedState=l,e.lanes===0&&(e.updateQueue.baseState=l)}var Lc={enqueueSetState:function(e,t,l){e=e._reactInternals;var a=vt(),n=ml(a);n.payload=t,l!=null&&(n.callback=l),t=hl(e,n,a),t!==null&&(pt(t,e,a),on(t,e,a))},enqueueReplaceState:function(e,t,l){e=e._reactInternals;var a=vt(),n=ml(a);n.tag=1,n.payload=t,l!=null&&(n.callback=l),t=hl(e,n,a),t!==null&&(pt(t,e,a),on(t,e,a))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var l=vt(),a=ml(l);a.tag=2,t!=null&&(a.callback=t),t=hl(e,a,l),t!==null&&(pt(t,e,l),on(t,e,l))}};function ro(e,t,l,a,n,u,r){return e=e.stateNode,typeof e.shouldComponentUpdate=="function"?e.shouldComponentUpdate(a,u,r):t.prototype&&t.prototype.isPureReactComponent?!en(l,a)||!en(n,u):!0}function so(e,t,l,a){e=t.state,typeof t.componentWillReceiveProps=="function"&&t.componentWillReceiveProps(l,a),typeof t.UNSAFE_componentWillReceiveProps=="function"&&t.UNSAFE_componentWillReceiveProps(l,a),t.state!==e&&Lc.enqueueReplaceState(t,t.state,null)}function $l(e,t){var l=t;if("ref"in t){l={};for(var a in t)a!=="ref"&&(l[a]=t[a])}if(e=e.defaultProps){l===t&&(l=N({},l));for(var n in e)l[n]===void 0&&(l[n]=e[n])}return l}var _u=typeof reportError=="function"?reportError:function(e){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof e=="object"&&e!==null&&typeof e.message=="string"?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",e);return}console.error(e)};function fo(e){_u(e)}function oo(e){console.error(e)}function mo(e){_u(e)}function Uu(e,t){try{var l=e.onUncaughtError;l(t.value,{componentStack:t.stack})}catch(a){setTimeout(function(){throw a})}}function ho(e,t,l){try{var a=e.onCaughtError;a(l.value,{componentStack:l.stack,errorBoundary:t.tag===1?t.stateNode:null})}catch(n){setTimeout(function(){throw n})}}function Hc(e,t,l){return l=ml(l),l.tag=3,l.payload={element:null},l.callback=function(){Uu(e,t)},l}function go(e){return e=ml(e),e.tag=3,e}function yo(e,t,l,a){var n=l.type.getDerivedStateFromError;if(typeof n=="function"){var u=a.value;e.payload=function(){return n(u)},e.callback=function(){ho(t,l,a)}}var r=l.stateNode;r!==null&&typeof r.componentDidCatch=="function"&&(e.callback=function(){ho(t,l,a),typeof n!="function"&&(jl===null?jl=new Set([this]):jl.add(this));var o=a.stack;this.componentDidCatch(a.value,{componentStack:o!==null?o:""})})}function s0(e,t,l,a,n){if(l.flags|=32768,a!==null&&typeof a=="object"&&typeof a.then=="function"){if(t=l.alternate,t!==null&&un(t,l,n,!0),l=At.current,l!==null){switch(l.tag){case 13:return Ht===null?cr():l.alternate===null&&Ce===0&&(Ce=3),l.flags&=-257,l.flags|=65536,l.lanes=n,a===dc?l.flags|=16384:(t=l.updateQueue,t===null?l.updateQueue=new Set([a]):t.add(a),sr(e,a,n)),!1;case 22:return l.flags|=65536,a===dc?l.flags|=16384:(t=l.updateQueue,t===null?(t={transitions:null,markerInstances:null,retryQueue:new Set([a])},l.updateQueue=t):(l=t.retryQueue,l===null?t.retryQueue=new Set([a]):l.add(a)),sr(e,a,n)),!1}throw Error(s(435,l.tag))}return sr(e,a,n),cr(),!1}if(ge)return t=At.current,t!==null?((t.flags&65536)===0&&(t.flags|=256),t.flags|=65536,t.lanes=n,a!==uc&&(e=Error(s(422),{cause:a}),nn(St(e,l)))):(a!==uc&&(t=Error(s(423),{cause:a}),nn(St(t,l))),e=e.current.alternate,e.flags|=65536,n&=-n,e.lanes|=n,a=St(a,l),n=Hc(e.stateNode,a,n),gc(e,n),Ce!==4&&(Ce=2)),!1;var u=Error(s(520),{cause:a});if(u=St(u,l),Tn===null?Tn=[u]:Tn.push(u),Ce!==4&&(Ce=2),t===null)return!0;a=St(a,l),l=t;do{switch(l.tag){case 3:return l.flags|=65536,e=n&-n,l.lanes|=e,e=Hc(l.stateNode,a,e),gc(l,e),!1;case 1:if(t=l.type,u=l.stateNode,(l.flags&128)===0&&(typeof t.getDerivedStateFromError=="function"||u!==null&&typeof u.componentDidCatch=="function"&&(jl===null||!jl.has(u))))return l.flags|=65536,n&=-n,l.lanes|=n,n=go(n),yo(n,e,l,a),gc(l,n),!1}l=l.return}while(l!==null);return!1}var vo=Error(s(461)),Ve=!1;function Je(e,t,l,a){t.child=e===null?io(t,null,l,a):Na(t,e.child,l,a)}function po(e,t,l,a,n){l=l.render;var u=t.ref;if("ref"in a){var r={};for(var o in a)o!=="ref"&&(r[o]=a[o])}else r=a;return Kl(t),a=bc(e,t,l,r,u,n),o=Sc(),e!==null&&!Ve?(Ec(e,t,n),It(e,t,n)):(ge&&o&&ac(t),t.flags|=1,Je(e,t,a,n),t.child)}function xo(e,t,l,a,n){if(e===null){var u=l.type;return typeof u=="function"&&!ec(u)&&u.defaultProps===void 0&&l.compare===null?(t.tag=15,t.type=u,bo(e,t,u,a,n)):(e=mu(l.type,null,a,t,t.mode,n),e.ref=t.ref,e.return=t,t.child=e)}if(u=e.child,!Zc(e,n)){var r=u.memoizedProps;if(l=l.compare,l=l!==null?l:en,l(r,a)&&e.ref===t.ref)return It(e,t,n)}return t.flags|=1,e=Zt(u,a),e.ref=t.ref,e.return=t,t.child=e}function bo(e,t,l,a,n){if(e!==null){var u=e.memoizedProps;if(en(u,a)&&e.ref===t.ref)if(Ve=!1,t.pendingProps=a=u,Zc(e,n))(e.flags&131072)!==0&&(Ve=!0);else return t.lanes=e.lanes,It(e,t,n)}return Bc(e,t,l,a,n)}function So(e,t,l){var a=t.pendingProps,n=a.children,u=e!==null?e.memoizedState:null;if(a.mode==="hidden"){if((t.flags&128)!==0){if(a=u!==null?u.baseLanes|l:l,e!==null){for(n=t.child=e.child,u=0;n!==null;)u=u|n.lanes|n.childLanes,n=n.sibling;t.childLanes=u&~a}else t.childLanes=0,t.child=null;return Eo(e,t,a,l)}if((l&536870912)!==0)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&pu(t,u!==null?u.cachePool:null),u!==null?xf(t,u):vc(),co(t);else return t.lanes=t.childLanes=536870912,Eo(e,t,u!==null?u.baseLanes|l:l,l)}else u!==null?(pu(t,u.cachePool),xf(t,u),vl(),t.memoizedState=null):(e!==null&&pu(t,null),vc(),vl());return Je(e,t,n,l),t.child}function Eo(e,t,l,a){var n=oc();return n=n===null?null:{parent:qe._currentValue,pool:n},t.memoizedState={baseLanes:l,cachePool:n},e!==null&&pu(t,null),vc(),co(t),e!==null&&un(e,t,a,!0),null}function wu(e,t){var l=t.ref;if(l===null)e!==null&&e.ref!==null&&(t.flags|=4194816);else{if(typeof l!="function"&&typeof l!="object")throw Error(s(284));(e===null||e.ref!==l)&&(t.flags|=4194816)}}function Bc(e,t,l,a,n){return Kl(t),l=bc(e,t,l,a,void 0,n),a=Sc(),e!==null&&!Ve?(Ec(e,t,n),It(e,t,n)):(ge&&a&&ac(t),t.flags|=1,Je(e,t,l,n),t.child)}function jo(e,t,l,a,n,u){return Kl(t),t.updateQueue=null,l=Sf(t,a,l,n),bf(e),a=Sc(),e!==null&&!Ve?(Ec(e,t,u),It(e,t,u)):(ge&&a&&ac(t),t.flags|=1,Je(e,t,l,u),t.child)}function No(e,t,l,a,n){if(Kl(t),t.stateNode===null){var u=ha,r=l.contextType;typeof r=="object"&&r!==null&&(u=Ie(r)),u=new l(a,u),t.memoizedState=u.state!==null&&u.state!==void 0?u.state:null,u.updater=Lc,t.stateNode=u,u._reactInternals=t,u=t.stateNode,u.props=a,u.state=t.memoizedState,u.refs={},mc(t),r=l.contextType,u.context=typeof r=="object"&&r!==null?Ie(r):ha,u.state=t.memoizedState,r=l.getDerivedStateFromProps,typeof r=="function"&&(wc(t,l,r,a),u.state=t.memoizedState),typeof l.getDerivedStateFromProps=="function"||typeof u.getSnapshotBeforeUpdate=="function"||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(r=u.state,typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount(),r!==u.state&&Lc.enqueueReplaceState(u,u.state,null),mn(t,a,u,n),dn(),u.state=t.memoizedState),typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!0}else if(e===null){u=t.stateNode;var o=t.memoizedProps,y=$l(l,o);u.props=y;var T=u.context,_=l.contextType;r=ha,typeof _=="object"&&_!==null&&(r=Ie(_));var H=l.getDerivedStateFromProps;_=typeof H=="function"||typeof u.getSnapshotBeforeUpdate=="function",o=t.pendingProps!==o,_||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(o||T!==r)&&so(t,u,a,r),dl=!1;var M=t.memoizedState;u.state=M,mn(t,a,u,n),dn(),T=t.memoizedState,o||M!==T||dl?(typeof H=="function"&&(wc(t,l,H,a),T=t.memoizedState),(y=dl||ro(t,l,y,a,M,T,r))?(_||typeof u.UNSAFE_componentWillMount!="function"&&typeof u.componentWillMount!="function"||(typeof u.componentWillMount=="function"&&u.componentWillMount(),typeof u.UNSAFE_componentWillMount=="function"&&u.UNSAFE_componentWillMount()),typeof u.componentDidMount=="function"&&(t.flags|=4194308)):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=a,t.memoizedState=T),u.props=a,u.state=T,u.context=r,a=y):(typeof u.componentDidMount=="function"&&(t.flags|=4194308),a=!1)}else{u=t.stateNode,hc(e,t),r=t.memoizedProps,_=$l(l,r),u.props=_,H=t.pendingProps,M=u.context,T=l.contextType,y=ha,typeof T=="object"&&T!==null&&(y=Ie(T)),o=l.getDerivedStateFromProps,(T=typeof o=="function"||typeof u.getSnapshotBeforeUpdate=="function")||typeof u.UNSAFE_componentWillReceiveProps!="function"&&typeof u.componentWillReceiveProps!="function"||(r!==H||M!==y)&&so(t,u,a,y),dl=!1,M=t.memoizedState,u.state=M,mn(t,a,u,n),dn();var D=t.memoizedState;r!==H||M!==D||dl||e!==null&&e.dependencies!==null&&yu(e.dependencies)?(typeof o=="function"&&(wc(t,l,o,a),D=t.memoizedState),(_=dl||ro(t,l,_,a,M,D,y)||e!==null&&e.dependencies!==null&&yu(e.dependencies))?(T||typeof u.UNSAFE_componentWillUpdate!="function"&&typeof u.componentWillUpdate!="function"||(typeof u.componentWillUpdate=="function"&&u.componentWillUpdate(a,D,y),typeof u.UNSAFE_componentWillUpdate=="function"&&u.UNSAFE_componentWillUpdate(a,D,y)),typeof u.componentDidUpdate=="function"&&(t.flags|=4),typeof u.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof u.componentDidUpdate!="function"||r===e.memoizedProps&&M===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||r===e.memoizedProps&&M===e.memoizedState||(t.flags|=1024),t.memoizedProps=a,t.memoizedState=D),u.props=a,u.state=D,u.context=y,a=_):(typeof u.componentDidUpdate!="function"||r===e.memoizedProps&&M===e.memoizedState||(t.flags|=4),typeof u.getSnapshotBeforeUpdate!="function"||r===e.memoizedProps&&M===e.memoizedState||(t.flags|=1024),a=!1)}return u=a,wu(e,t),a=(t.flags&128)!==0,u||a?(u=t.stateNode,l=a&&typeof l.getDerivedStateFromError!="function"?null:u.render(),t.flags|=1,e!==null&&a?(t.child=Na(t,e.child,null,n),t.child=Na(t,null,l,n)):Je(e,t,l,n),t.memoizedState=u.state,e=t.child):e=It(e,t,n),e}function Ao(e,t,l,a){return an(),t.flags|=256,Je(e,t,l,a),t.child}var qc={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Yc(e){return{baseLanes:e,cachePool:of()}}function Gc(e,t,l){return e=e!==null?e.childLanes&~l:0,t&&(e|=Tt),e}function To(e,t,l){var a=t.pendingProps,n=!1,u=(t.flags&128)!==0,r;if((r=u)||(r=e!==null&&e.memoizedState===null?!1:(Ye.current&2)!==0),r&&(n=!0,t.flags&=-129),r=(t.flags&32)!==0,t.flags&=-33,e===null){if(ge){if(n?yl(t):vl(),ge){var o=ze,y;if(y=o){e:{for(y=o,o=Lt;y.nodeType!==8;){if(!o){o=null;break e}if(y=Ct(y.nextSibling),y===null){o=null;break e}}o=y}o!==null?(t.memoizedState={dehydrated:o,treeContext:Gl!==null?{id:Kt,overflow:kt}:null,retryLane:536870912,hydrationErrors:null},y=mt(18,null,null,0),y.stateNode=o,y.return=t,t.child=y,lt=t,ze=null,y=!0):y=!1}y||Vl(t)}if(o=t.memoizedState,o!==null&&(o=o.dehydrated,o!==null))return Nr(o)?t.lanes=32:t.lanes=536870912,null;Ft(t)}return o=a.children,a=a.fallback,n?(vl(),n=t.mode,o=Lu({mode:"hidden",children:o},n),a=Yl(a,n,l,null),o.return=t,a.return=t,o.sibling=a,t.child=o,n=t.child,n.memoizedState=Yc(l),n.childLanes=Gc(e,r,l),t.memoizedState=qc,a):(yl(t),Xc(t,o))}if(y=e.memoizedState,y!==null&&(o=y.dehydrated,o!==null)){if(u)t.flags&256?(yl(t),t.flags&=-257,t=Qc(e,t,l)):t.memoizedState!==null?(vl(),t.child=e.child,t.flags|=128,t=null):(vl(),n=a.fallback,o=t.mode,a=Lu({mode:"visible",children:a.children},o),n=Yl(n,o,l,null),n.flags|=2,a.return=t,n.return=t,a.sibling=n,t.child=a,Na(t,e.child,null,l),a=t.child,a.memoizedState=Yc(l),a.childLanes=Gc(e,r,l),t.memoizedState=qc,t=n);else if(yl(t),Nr(o)){if(r=o.nextSibling&&o.nextSibling.dataset,r)var T=r.dgst;r=T,a=Error(s(419)),a.stack="",a.digest=r,nn({value:a,source:null,stack:null}),t=Qc(e,t,l)}else if(Ve||un(e,t,l,!1),r=(l&e.childLanes)!==0,Ve||r){if(r=je,r!==null&&(a=l&-l,a=(a&42)!==0?1:Ai(a),a=(a&(r.suspendedLanes|l))!==0?0:a,a!==0&&a!==y.retryLane))throw y.retryLane=a,ma(e,a),pt(r,e,a),vo;o.data==="$?"||cr(),t=Qc(e,t,l)}else o.data==="$?"?(t.flags|=192,t.child=e.child,t=null):(e=y.treeContext,ze=Ct(o.nextSibling),lt=t,ge=!0,Ql=null,Lt=!1,e!==null&&(jt[Nt++]=Kt,jt[Nt++]=kt,jt[Nt++]=Gl,Kt=e.id,kt=e.overflow,Gl=t),t=Xc(t,a.children),t.flags|=4096);return t}return n?(vl(),n=a.fallback,o=t.mode,y=e.child,T=y.sibling,a=Zt(y,{mode:"hidden",children:a.children}),a.subtreeFlags=y.subtreeFlags&65011712,T!==null?n=Zt(T,n):(n=Yl(n,o,l,null),n.flags|=2),n.return=t,a.return=t,a.sibling=n,t.child=a,a=n,n=t.child,o=e.child.memoizedState,o===null?o=Yc(l):(y=o.cachePool,y!==null?(T=qe._currentValue,y=y.parent!==T?{parent:T,pool:T}:y):y=of(),o={baseLanes:o.baseLanes|l,cachePool:y}),n.memoizedState=o,n.childLanes=Gc(e,r,l),t.memoizedState=qc,a):(yl(t),l=e.child,e=l.sibling,l=Zt(l,{mode:"visible",children:a.children}),l.return=t,l.sibling=null,e!==null&&(r=t.deletions,r===null?(t.deletions=[e],t.flags|=16):r.push(e)),t.child=l,t.memoizedState=null,l)}function Xc(e,t){return t=Lu({mode:"visible",children:t},e.mode),t.return=e,e.child=t}function Lu(e,t){return e=mt(22,e,null,t),e.lanes=0,e.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},e}function Qc(e,t,l){return Na(t,e.child,null,l),e=Xc(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Ro(e,t,l){e.lanes|=t;var a=e.alternate;a!==null&&(a.lanes|=t),cc(e.return,t,l)}function Vc(e,t,l,a,n){var u=e.memoizedState;u===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:a,tail:l,tailMode:n}:(u.isBackwards=t,u.rendering=null,u.renderingStartTime=0,u.last=a,u.tail=l,u.tailMode=n)}function Mo(e,t,l){var a=t.pendingProps,n=a.revealOrder,u=a.tail;if(Je(e,t,a.children,l),a=Ye.current,(a&2)!==0)a=a&1|2,t.flags|=128;else{if(e!==null&&(e.flags&128)!==0)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Ro(e,l,t);else if(e.tag===19)Ro(e,l,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}a&=1}switch(J(Ye,a),n){case"forwards":for(l=t.child,n=null;l!==null;)e=l.alternate,e!==null&&Cu(e)===null&&(n=l),l=l.sibling;l=n,l===null?(n=t.child,t.child=null):(n=l.sibling,l.sibling=null),Vc(t,!1,n,l,u);break;case"backwards":for(l=null,n=t.child,t.child=null;n!==null;){if(e=n.alternate,e!==null&&Cu(e)===null){t.child=n;break}e=n.sibling,n.sibling=l,l=n,n=e}Vc(t,!0,l,null,u);break;case"together":Vc(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function It(e,t,l){if(e!==null&&(t.dependencies=e.dependencies),El|=t.lanes,(l&t.childLanes)===0)if(e!==null){if(un(e,t,l,!1),(l&t.childLanes)===0)return null}else return null;if(e!==null&&t.child!==e.child)throw Error(s(153));if(t.child!==null){for(e=t.child,l=Zt(e,e.pendingProps),t.child=l,l.return=t;e.sibling!==null;)e=e.sibling,l=l.sibling=Zt(e,e.pendingProps),l.return=t;l.sibling=null}return t.child}function Zc(e,t){return(e.lanes&t)!==0?!0:(e=e.dependencies,!!(e!==null&&yu(e)))}function f0(e,t,l){switch(t.tag){case 3:Ne(t,t.stateNode.containerInfo),ol(t,qe,e.memoizedState.cache),an();break;case 27:case 5:bi(t);break;case 4:Ne(t,t.stateNode.containerInfo);break;case 10:ol(t,t.type,t.memoizedProps.value);break;case 13:var a=t.memoizedState;if(a!==null)return a.dehydrated!==null?(yl(t),t.flags|=128,null):(l&t.child.childLanes)!==0?To(e,t,l):(yl(t),e=It(e,t,l),e!==null?e.sibling:null);yl(t);break;case 19:var n=(e.flags&128)!==0;if(a=(l&t.childLanes)!==0,a||(un(e,t,l,!1),a=(l&t.childLanes)!==0),n){if(a)return Mo(e,t,l);t.flags|=128}if(n=t.memoizedState,n!==null&&(n.rendering=null,n.tail=null,n.lastEffect=null),J(Ye,Ye.current),a)break;return null;case 22:case 23:return t.lanes=0,So(e,t,l);case 24:ol(t,qe,e.memoizedState.cache)}return It(e,t,l)}function Do(e,t,l){if(e!==null)if(e.memoizedProps!==t.pendingProps)Ve=!0;else{if(!Zc(e,l)&&(t.flags&128)===0)return Ve=!1,f0(e,t,l);Ve=(e.flags&131072)!==0}else Ve=!1,ge&&(t.flags&1048576)!==0&&af(t,gu,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var a=t.elementType,n=a._init;if(a=n(a._payload),t.type=a,typeof a=="function")ec(a)?(e=$l(a,e),t.tag=1,t=No(null,t,a,e,l)):(t.tag=0,t=Bc(null,t,a,e,l));else{if(a!=null){if(n=a.$$typeof,n===X){t.tag=11,t=po(null,t,a,e,l);break e}else if(n===U){t.tag=14,t=xo(null,t,a,e,l);break e}}throw t=tt(a)||a,Error(s(306,t,""))}}return t;case 0:return Bc(e,t,t.type,t.pendingProps,l);case 1:return a=t.type,n=$l(a,t.pendingProps),No(e,t,a,n,l);case 3:e:{if(Ne(t,t.stateNode.containerInfo),e===null)throw Error(s(387));a=t.pendingProps;var u=t.memoizedState;n=u.element,hc(e,t),mn(t,a,null,l);var r=t.memoizedState;if(a=r.cache,ol(t,qe,a),a!==u.cache&&rc(t,[qe],l,!0),dn(),a=r.element,u.isDehydrated)if(u={element:a,isDehydrated:!1,cache:r.cache},t.updateQueue.baseState=u,t.memoizedState=u,t.flags&256){t=Ao(e,t,a,l);break e}else if(a!==n){n=St(Error(s(424)),t),nn(n),t=Ao(e,t,a,l);break e}else{switch(e=t.stateNode.containerInfo,e.nodeType){case 9:e=e.body;break;default:e=e.nodeName==="HTML"?e.ownerDocument.body:e}for(ze=Ct(e.firstChild),lt=t,ge=!0,Ql=null,Lt=!0,l=io(t,null,a,l),t.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling}else{if(an(),a===n){t=It(e,t,l);break e}Je(e,t,a,l)}t=t.child}return t;case 26:return wu(e,t),e===null?(l=_d(t.type,null,t.pendingProps,null))?t.memoizedState=l:ge||(l=t.type,e=t.pendingProps,a=Wu(ue.current).createElement(l),a[Fe]=t,a[at]=e,We(a,l,e),Qe(a),t.stateNode=a):t.memoizedState=_d(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return bi(t),e===null&&ge&&(a=t.stateNode=Od(t.type,t.pendingProps,ue.current),lt=t,Lt=!0,n=ze,Tl(t.type)?(Ar=n,ze=Ct(a.firstChild)):ze=n),Je(e,t,t.pendingProps.children,l),wu(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&ge&&((n=a=ze)&&(a=B0(a,t.type,t.pendingProps,Lt),a!==null?(t.stateNode=a,lt=t,ze=Ct(a.firstChild),Lt=!1,n=!0):n=!1),n||Vl(t)),bi(t),n=t.type,u=t.pendingProps,r=e!==null?e.memoizedProps:null,a=u.children,Sr(n,u)?a=null:r!==null&&Sr(n,r)&&(t.flags|=32),t.memoizedState!==null&&(n=bc(e,t,l0,null,null,l),wn._currentValue=n),wu(e,t),Je(e,t,a,l),t.child;case 6:return e===null&&ge&&((e=l=ze)&&(l=q0(l,t.pendingProps,Lt),l!==null?(t.stateNode=l,lt=t,ze=null,e=!0):e=!1),e||Vl(t)),null;case 13:return To(e,t,l);case 4:return Ne(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=Na(t,null,a,l):Je(e,t,a,l),t.child;case 11:return po(e,t,t.type,t.pendingProps,l);case 7:return Je(e,t,t.pendingProps,l),t.child;case 8:return Je(e,t,t.pendingProps.children,l),t.child;case 12:return Je(e,t,t.pendingProps.children,l),t.child;case 10:return a=t.pendingProps,ol(t,t.type,a.value),Je(e,t,a.children,l),t.child;case 9:return n=t.type._context,a=t.pendingProps.children,Kl(t),n=Ie(n),a=a(n),t.flags|=1,Je(e,t,a,l),t.child;case 14:return xo(e,t,t.type,t.pendingProps,l);case 15:return bo(e,t,t.type,t.pendingProps,l);case 19:return Mo(e,t,l);case 31:return a=t.pendingProps,l=t.mode,a={mode:a.mode,children:a.children},e===null?(l=Lu(a,l),l.ref=t.ref,t.child=l,l.return=t,t=l):(l=Zt(e.child,a),l.ref=t.ref,t.child=l,l.return=t,t=l),t;case 22:return So(e,t,l);case 24:return Kl(t),a=Ie(qe),e===null?(n=oc(),n===null&&(n=je,u=sc(),n.pooledCache=u,u.refCount++,u!==null&&(n.pooledCacheLanes|=l),n=u),t.memoizedState={parent:a,cache:n},mc(t),ol(t,qe,n)):((e.lanes&l)!==0&&(hc(e,t),mn(t,null,null,l),dn()),n=e.memoizedState,u=t.memoizedState,n.parent!==a?(n={parent:a,cache:a},t.memoizedState=n,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=n),ol(t,qe,a)):(a=u.cache,ol(t,qe,a),a!==n.cache&&rc(t,[qe],l,!0))),Je(e,t,t.pendingProps.children,l),t.child;case 29:throw t.pendingProps}throw Error(s(156,t.tag))}function Pt(e){e.flags|=4}function Oo(e,t){if(t.type!=="stylesheet"||(t.state.loading&4)!==0)e.flags&=-16777217;else if(e.flags|=16777216,!Bd(t)){if(t=At.current,t!==null&&((de&4194048)===de?Ht!==null:(de&62914560)!==de&&(de&536870912)===0||t!==Ht))throw fn=dc,df;e.flags|=8192}}function Hu(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag!==22?is():536870912,e.lanes|=t,Ma|=t)}function bn(e,t){if(!ge)switch(e.tailMode){case"hidden":t=e.tail;for(var l=null;t!==null;)t.alternate!==null&&(l=t),t=t.sibling;l===null?e.tail=null:l.sibling=null;break;case"collapsed":l=e.tail;for(var a=null;l!==null;)l.alternate!==null&&(a=l),l=l.sibling;a===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:a.sibling=null}}function Te(e){var t=e.alternate!==null&&e.alternate.child===e.child,l=0,a=0;if(t)for(var n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags&65011712,a|=n.flags&65011712,n.return=e,n=n.sibling;else for(n=e.child;n!==null;)l|=n.lanes|n.childLanes,a|=n.subtreeFlags,a|=n.flags,n.return=e,n=n.sibling;return e.subtreeFlags|=a,e.childLanes=l,t}function o0(e,t,l){var a=t.pendingProps;switch(nc(t),t.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Te(t),null;case 1:return Te(t),null;case 3:return l=t.stateNode,a=null,e!==null&&(a=e.memoizedState.cache),t.memoizedState.cache!==a&&(t.flags|=2048),$t(qe),cl(),l.pendingContext&&(l.context=l.pendingContext,l.pendingContext=null),(e===null||e.child===null)&&(ln(t)?Pt(t):e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,cf())),Te(t),null;case 26:return l=t.memoizedState,e===null?(Pt(t),l!==null?(Te(t),Oo(t,l)):(Te(t),t.flags&=-16777217)):l?l!==e.memoizedState?(Pt(t),Te(t),Oo(t,l)):(Te(t),t.flags&=-16777217):(e.memoizedProps!==a&&Pt(t),Te(t),t.flags&=-16777217),null;case 27:Jn(t),l=ue.current;var n=t.type;if(e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Pt(t);else{if(!a){if(t.stateNode===null)throw Error(s(166));return Te(t),null}e=P.current,ln(t)?nf(t):(e=Od(n,a,l),t.stateNode=e,Pt(t))}return Te(t),null;case 5:if(Jn(t),l=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==a&&Pt(t);else{if(!a){if(t.stateNode===null)throw Error(s(166));return Te(t),null}if(e=P.current,ln(t))nf(t);else{switch(n=Wu(ue.current),e){case 1:e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case 2:e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;default:switch(l){case"svg":e=n.createElementNS("http://www.w3.org/2000/svg",l);break;case"math":e=n.createElementNS("http://www.w3.org/1998/Math/MathML",l);break;case"script":e=n.createElement("div"),e.innerHTML=" - + +
diff --git a/components/scheduler/CMakeLists.txt b/components/scheduler/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/scheduler/idf_component.yml b/components/scheduler/idf_component.yml old mode 100755 new mode 100644 diff --git a/components/scheduler/include/scheduler.h b/components/scheduler/include/scheduler.h old mode 100755 new mode 100644 diff --git a/components/scheduler/src/scheduler.c b/components/scheduler/src/scheduler.c old mode 100755 new mode 100644 diff --git a/components/scheduler/src/scheduler_types.c b/components/scheduler/src/scheduler_types.c old mode 100755 new mode 100644 diff --git a/components/spi_bus_manager/CMakeLists.txt b/components/spi_bus_manager/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/spi_bus_manager/include/spi_bus_manager.h b/components/spi_bus_manager/include/spi_bus_manager.h old mode 100755 new mode 100644 diff --git a/components/spi_bus_manager/src/spi_bus_manager.c b/components/spi_bus_manager/src/spi_bus_manager.c old mode 100755 new mode 100644 diff --git a/components/storage_service/CMakeLists.txt b/components/storage_service/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/components/storage_service/include/storage_service.h b/components/storage_service/include/storage_service.h old mode 100755 new mode 100644 diff --git a/components/storage_service/src/storage_service.c b/components/storage_service/src/storage_service.c old mode 100755 new mode 100644 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild old mode 100755 new mode 100644 diff --git a/main/component.mk b/main/component.mk old mode 100755 new mode 100644 diff --git a/main/idf_component.yml b/main/idf_component.yml old mode 100755 new mode 100644 diff --git a/main/main.c b/main/main.c old mode 100755 new mode 100644 index e5cbefd..8356890 --- a/main/main.c +++ b/main/main.c @@ -34,6 +34,8 @@ #include "scheduler.h" #include "storage_service.h" #include "time_service.h" +#include "hmi_link.h" + /* * Modos de controlo do AP: @@ -61,7 +63,7 @@ * Para placa SEM botão: * #define APP_AP_CONTROL_MODE APP_AP_CONTROL_BOOT_WINDOW */ -#define APP_AP_CONTROL_MODE APP_AP_CONTROL_BOOT_WINDOW +#define APP_AP_CONTROL_MODE APP_AP_CONTROL_BUTTON #define AP_BOOT_WINDOW_MS 180000UL // 3 minutos #define AP_CONNECTION_TIMEOUT AP_BOOT_WINDOW_MS @@ -373,6 +375,7 @@ static void init_modules(void) protocols_init(); loadbalancer_init(); evse_link_init(); + //ESP_ERROR_CHECK(hmi_link_init()); } // diff --git a/partitions.csv b/partitions.csv old mode 100755 new mode 100644 index c1b4377..466c120 --- a/partitions.csv +++ b/partitions.csv @@ -3,4 +3,5 @@ nvs, data, nvs, 0x9000, 0xA000, phy_init, data, phy, 0x13000, 0x1000, factory, app, factory, 0x20000, 3500K, cfg, data, spiffs, 0x38B000, 0x4000, -data, data, spiffs, 0x38F000, 0x71000, +mo, data, spiffs, 0x38F000, 0x20000, +data, data, spiffs, 0x3AF000, 0x51000, \ No newline at end of file