Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
if(DEFINED EVEREST_IO_WITH_MQTT AND NOT EVEREST_IO_WITH_MQTT)
|
||||
message(FATAL_ERROR "pionix_chargebridge requires MQTT support in everest::io. "
|
||||
"Set EVEREST_IO_WITH_MQTT=ON or disable EVEREST_BUILD_APPLICATIONS.")
|
||||
endif()
|
||||
|
||||
find_package(ryml QUIET)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
|
||||
add_executable(pionix_chargebridge
|
||||
src/everest_api/api_connector.cpp
|
||||
src/everest_api/evse_bsp_api.cpp
|
||||
src/everest_api/ovm_api.cpp
|
||||
src/everest_api/ev_bsp_api.cpp
|
||||
|
||||
src/firmware_update/sync_fw_updater.cpp
|
||||
|
||||
src/utilities/filesystem.cpp
|
||||
src/utilities/logging.cpp
|
||||
src/utilities/parse_config.cpp
|
||||
src/utilities/print_config.cpp
|
||||
src/utilities/string.cpp
|
||||
src/utilities/symlink.cpp
|
||||
src/utilities/sync_udp_client.cpp
|
||||
src/utilities/type_converters.cpp
|
||||
|
||||
src/can_bridge.cpp
|
||||
src/charge_bridge.cpp
|
||||
src/bsp_bridge.cpp
|
||||
src/gpio_bridge.cpp
|
||||
src/heartbeat_service.cpp
|
||||
src/plc_bridge.cpp
|
||||
src/serial_bridge.cpp
|
||||
src/discovery.cpp
|
||||
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(pionix_chargebridge
|
||||
everest::io
|
||||
everest::everest_api_types
|
||||
nlohmann_json::nlohmann_json
|
||||
ryml::ryml
|
||||
)
|
||||
|
||||
target_include_directories(pionix_chargebridge
|
||||
PRIVATE include
|
||||
PRIVATE shared
|
||||
)
|
||||
|
||||
set(cb_firmware_binary config/firmware/charge-bridge-fw_complete.cbfw)
|
||||
|
||||
add_custom_command(
|
||||
TARGET pionix_chargebridge
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${cb_firmware_binary}"
|
||||
"$<TARGET_FILE_DIR:pionix_chargebridge>/"
|
||||
COMMENT "Copying Pionix ChargeBridge firmware binary..."
|
||||
)
|
||||
|
||||
|
||||
install (TARGETS pionix_chargebridge)
|
||||
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/${cb_firmware_binary}" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge/firmware)
|
||||
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-EVAL.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-EVAL.yaml-example")
|
||||
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-SAT-AC.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-SAT-AC.yaml-example")
|
||||
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-EVAL-EV.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-EVAL-EV.yaml-example")
|
||||
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-EVAL-SIM.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-EVAL-SIM.yaml-example")
|
||||
@@ -0,0 +1,154 @@
|
||||
charge_bridge:
|
||||
name: cb_eval_ev
|
||||
ip: ANY_EV
|
||||
#ip: chargebridge-44b7d0c99629.local
|
||||
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
|
||||
fw_update_on_start: true
|
||||
mdns_name: ""
|
||||
|
||||
heartbeat:
|
||||
interval_s: 1
|
||||
connection_to_s: 10
|
||||
|
||||
safety:
|
||||
pp_mode: "disabled"
|
||||
cp_avg_ms: 10
|
||||
inverted_emergency_input: 0
|
||||
relay_1:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 200
|
||||
feedback_inverted: true
|
||||
# PWM not supported yet
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_2:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 200
|
||||
feedback_inverted: true
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_3:
|
||||
relay_mode: "UserRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
|
||||
can_0:
|
||||
enable: true
|
||||
local: "cb_ev_can"
|
||||
baudrate: 125000
|
||||
|
||||
serial_1:
|
||||
enable: true
|
||||
local: "/dev/cb_ev_uart"
|
||||
baudrate: 115200
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
serial_2:
|
||||
enable: true
|
||||
local: "/dev/cb_ev_rs485"
|
||||
baudrate: 19200
|
||||
stopbits: OneStopBit
|
||||
parity: Even
|
||||
|
||||
plc:
|
||||
enable: true
|
||||
tap: "cb_ev_plc"
|
||||
ip: 172.25.6.1
|
||||
netmask: 255.255.255.0
|
||||
mtu: 1518
|
||||
powersaving_mode: 1
|
||||
|
||||
ev_bsp:
|
||||
enable: true
|
||||
module_id: "ev_bsp_1"
|
||||
mqtt_remote: 127.0.0.1
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
ovm_enabled: false
|
||||
ovm_module_id: "ovm_1"
|
||||
|
||||
evse_bsp:
|
||||
enable: false
|
||||
module_id: "bsp_1"
|
||||
mqtt_remote: 127.0.0.1
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
capabilities:
|
||||
max_current_A_import: 16
|
||||
min_current_A_import: 6
|
||||
max_phase_count_import: 3
|
||||
min_phase_count_import: 3
|
||||
max_current_A_export: 16
|
||||
min_current_A_export: 6
|
||||
max_phase_count_export: 3
|
||||
min_phase_count_export: 3
|
||||
supports_changing_phases_during_charging: false
|
||||
connector_type: "IEC62196Type2Cable"
|
||||
max_plug_temperature_C: 250
|
||||
ovm_enabled: true
|
||||
ovm_module_id: "ovm_1"
|
||||
|
||||
gpio:
|
||||
enable: true
|
||||
interval_s: 1
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_ping_interval_ms: 5000
|
||||
gpio_0:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_1:
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
config: 32767
|
||||
gpio_2:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_3:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_4:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 1000
|
||||
gpio_5:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_6:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_7:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_8:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_9:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
@@ -0,0 +1,153 @@
|
||||
charge_bridge:
|
||||
name: cb_eval
|
||||
ip: ANY_EVSE
|
||||
#ip: chargebridge-44b7d0c99629.local
|
||||
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
|
||||
fw_update_on_start: true
|
||||
mdns_name: ""
|
||||
|
||||
heartbeat:
|
||||
interval_s: 1
|
||||
connection_to_s: 10
|
||||
|
||||
safety:
|
||||
pp_mode: "disabled"
|
||||
cp_avg_ms: 10
|
||||
inverted_emergency_input: 0
|
||||
relay_1:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 200
|
||||
feedback_inverted: true
|
||||
# PWM not supported yet
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_2:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 200
|
||||
feedback_inverted: true
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_3:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: true
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
|
||||
can_0:
|
||||
enable: true
|
||||
local: "cb_can"
|
||||
baudrate: 125000
|
||||
|
||||
serial_1:
|
||||
enable: true
|
||||
local: "/dev/cb_uart"
|
||||
baudrate: 115200
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
serial_2:
|
||||
enable: true
|
||||
local: "/dev/cb_rs485"
|
||||
baudrate: 19200
|
||||
stopbits: OneStopBit
|
||||
parity: Even
|
||||
|
||||
plc:
|
||||
enable: true
|
||||
tap: "cb_plc"
|
||||
ip: 172.25.6.1
|
||||
netmask: 255.255.255.0
|
||||
mtu: 1518
|
||||
powersaving_mode: 1
|
||||
|
||||
ev_bsp:
|
||||
enable: false
|
||||
module_id: "ev_bsp_1"
|
||||
mqtt_remote: 127.0.0.1
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
ovm_enabled: false
|
||||
ovm_module_id: "ovm_1"
|
||||
|
||||
evse_bsp:
|
||||
enable: true
|
||||
module_id: "cb_bsp"
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_ping_interval_ms: 5000
|
||||
ovm_enabled: true
|
||||
ovm_module_id: "cb_ovm"
|
||||
capabilities:
|
||||
max_current_A_import: 16
|
||||
min_current_A_import: 0
|
||||
max_phase_count_import: 3
|
||||
min_phase_count_import: 3
|
||||
max_current_A_export: 16
|
||||
min_current_A_export: 0
|
||||
max_phase_count_export: 3
|
||||
min_phase_count_export: 3
|
||||
supports_changing_phases_during_charging: false
|
||||
connector_type: "IEC62196Type2Cable"
|
||||
|
||||
gpio:
|
||||
enable: true
|
||||
interval_s: 1
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_ping_interval_ms: 5000
|
||||
gpio_0:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_1:
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
config: 32767
|
||||
gpio_2:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_3:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_4:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 1000
|
||||
gpio_5:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_6:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_7:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_8:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_9:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
@@ -0,0 +1,147 @@
|
||||
charge_bridge:
|
||||
name: cb_eval
|
||||
ip: ANY_EVSE
|
||||
#ip: chargebridge-44b7d0c9bcc0.local^
|
||||
#ip: chargebridge-44b7d0c99629.local
|
||||
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
|
||||
fw_update_on_start: true
|
||||
mdns_name: ""
|
||||
|
||||
heartbeat:
|
||||
interval_s: 1
|
||||
connection_to_s: 10
|
||||
|
||||
safety:
|
||||
pp_mode: "disabled"
|
||||
cp_avg_ms: 10
|
||||
inverted_emergency_input: 0
|
||||
relay_1:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: true
|
||||
feedback_delay_ms: 200
|
||||
feedback_inverted: true
|
||||
# PWM not supported yet
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_2:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: true
|
||||
feedback_delay_ms: 200
|
||||
feedback_inverted: true
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_3:
|
||||
relay_mode: "UserRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
|
||||
can_0:
|
||||
enable: true
|
||||
local: "cb_can"
|
||||
baudrate: 125000
|
||||
|
||||
serial_1:
|
||||
enable: true
|
||||
local: "/dev/cb_uart"
|
||||
baudrate: 115200
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
serial_2:
|
||||
enable: true
|
||||
local: "/dev/cb_rs485"
|
||||
baudrate: 19200
|
||||
stopbits: OneStopBit
|
||||
parity: Even
|
||||
|
||||
plc:
|
||||
enable: true
|
||||
tap: "cb_plc"
|
||||
ip: 172.25.6.2
|
||||
netmask: 255.255.255.0
|
||||
mtu: 1518
|
||||
powersaving_mode: 1
|
||||
|
||||
evse_bsp:
|
||||
enable: true
|
||||
module_id: "cb_bsp"
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
mqtt_ping_interval_ms: 5000
|
||||
ovm_enabled: true
|
||||
ovm_module_id: "cb_ovm"
|
||||
capabilities:
|
||||
max_current_A_import: 16
|
||||
min_current_A_import: 0
|
||||
max_phase_count_import: 3
|
||||
min_phase_count_import: 3
|
||||
max_current_A_export: 16
|
||||
min_current_A_export: 0
|
||||
max_phase_count_export: 3
|
||||
min_phase_count_export: 3
|
||||
supports_changing_phases_during_charging: false
|
||||
connector_type: "IEC62196Type2Cable"
|
||||
|
||||
gpio:
|
||||
enable: true
|
||||
interval_s: 1
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
mqtt_ping_interval_ms: 5000
|
||||
gpio_0:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_1:
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
config: 32767
|
||||
gpio_2:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_3:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_4:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 1000
|
||||
gpio_5:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_6:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_7:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_8:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_9:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
@@ -0,0 +1,169 @@
|
||||
charge_bridge:
|
||||
name: cb_sat_ac
|
||||
ip: ANY_EVSE
|
||||
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
|
||||
fw_update_on_start: true
|
||||
mdns_name: ""
|
||||
|
||||
heartbeat:
|
||||
interval_s: 1
|
||||
connection_to_s: 10
|
||||
|
||||
safety:
|
||||
pp_mode: "disabled"
|
||||
cp_avg_ms: 10
|
||||
inverted_emergency_input: 0
|
||||
relay_1:
|
||||
relay_mode: "PowerRelay"
|
||||
# Auxilary contact is connected from the OMRON relay
|
||||
feedback_enabled: true
|
||||
# The Omron relay switches in less than 100ms, use 200ms here
|
||||
feedback_delay_ms: 200
|
||||
# Only for PCB version 1.1, set to false for PCB version 1.2 and up
|
||||
feedback_inverted: true
|
||||
# PWM not supported yet
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_2:
|
||||
# Not connected on PCB
|
||||
relay_mode: "UserRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_3:
|
||||
# Not connected on PCB
|
||||
relay_mode: "UserRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
|
||||
can_0:
|
||||
enable: true
|
||||
local: "cb_can"
|
||||
baudrate: 250000
|
||||
|
||||
serial_1:
|
||||
enable: true
|
||||
local: "/dev/cb_uart"
|
||||
baudrate: 115200
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
serial_2:
|
||||
enable: true
|
||||
local: "/dev/cb_rs485"
|
||||
baudrate: 9600
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
plc:
|
||||
enable: true
|
||||
tap: "cb_plc"
|
||||
ip: 172.25.6.1
|
||||
netmask: 255.255.255.0
|
||||
mtu: 1518
|
||||
powersaving_mode: 1
|
||||
|
||||
evse_bsp:
|
||||
enable: true
|
||||
module_id: "cb_bsp"
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
mqtt_ping_interval_ms: 1000
|
||||
capabilities:
|
||||
max_current_A_import: 16
|
||||
min_current_A_import: 6
|
||||
max_phase_count_import: 3
|
||||
min_phase_count_import: 3
|
||||
max_current_A_export: 16
|
||||
min_current_A_export: 6
|
||||
max_phase_count_export: 3
|
||||
min_phase_count_export: 3
|
||||
supports_changing_phases_during_charging: false
|
||||
connector_type: "IEC62196Type2Cable"
|
||||
ovm_enabled: false
|
||||
ovm_module_id: cb_ovm
|
||||
|
||||
gpio:
|
||||
enable: true
|
||||
interval_s: 1
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_bind: 127.0.0.1
|
||||
mqtt_ping_interval_ms: 1000
|
||||
gpio_0:
|
||||
# RCD.TEST
|
||||
#mode: "Rcd_Selftest_Output"
|
||||
# Self test not fully supported yet
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
# RCD self test duration (ignore emergency input signals for this time after self test)
|
||||
# and show reason of safety decision on host somehow to simplify debugging
|
||||
config: 1000
|
||||
gpio_1:
|
||||
# RCD.ERROR
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
config: 0
|
||||
gpio_2:
|
||||
# MOTOR_1
|
||||
# simple Motor lock with only 2 wires (no feedback contacts)
|
||||
mode: "MotorLock_1"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
# 1000 ms motor drive time for locking/unlocking
|
||||
config: 1000
|
||||
gpio_3:
|
||||
# MOTOR_2
|
||||
# simple Motor lock with only 2 wires (no feedback contacts)
|
||||
mode: "MotorLock_2"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
config: 1000
|
||||
gpio_4:
|
||||
# RCD.PWM
|
||||
# not supported yet
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
config: 0
|
||||
gpio_5:
|
||||
# External GPIO on connector J4 pin 9 (10kOhm I/O)
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
config: 0
|
||||
gpio_6:
|
||||
# External GPIO on connector J4 pin 10 (10kOhm I/O)
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 0
|
||||
gpio_7:
|
||||
# External GPIO on connector J3 pin 11 (10kOhm I/O)
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
config: 0
|
||||
gpio_8:
|
||||
# External GPIO on connector J3 pin 12 (10kOhm I/O)
|
||||
mode: "Input"
|
||||
pulls: "PullDown"
|
||||
mdns: false
|
||||
config: 0
|
||||
gpio_9:
|
||||
# Not connected
|
||||
mode: "Input"
|
||||
pulls: "PullDown"
|
||||
mdns: false
|
||||
config: 0
|
||||
@@ -0,0 +1,143 @@
|
||||
charge_bridge_ip_list : [ "192.168.188.65", "192.168.188.65"]
|
||||
|
||||
charge_bridge:
|
||||
name: cb_##
|
||||
ip: ""
|
||||
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
|
||||
fw_update_on_start: false
|
||||
mdns_name: "pionix_cb_##"
|
||||
|
||||
heartbeat:
|
||||
interval_s: 1
|
||||
connection_to_s: 10
|
||||
|
||||
safety:
|
||||
pp_mode: "disabled"
|
||||
cp_avg_ms: 10
|
||||
relay_1:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_2:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
relay_3:
|
||||
relay_mode: "PowerRelay"
|
||||
feedback_enabled: false
|
||||
feedback_delay_ms: 10
|
||||
feedback_inverted: false
|
||||
pwm_dc: 100
|
||||
pwm_delay_ms: 0
|
||||
switchoff_delay_ms: 10
|
||||
|
||||
can_0:
|
||||
enable: true
|
||||
local: "cb_##_can0"
|
||||
baudrate: 250000
|
||||
|
||||
serial_1:
|
||||
enable: true
|
||||
local: "/dev/cb_##_serial_1"
|
||||
baudrate: 19200
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
serial_2:
|
||||
enable: false
|
||||
local: "/dev/cb_##_serial_2"
|
||||
baudrate: 19200
|
||||
stopbits: OneStopBit
|
||||
parity: None
|
||||
|
||||
plc:
|
||||
enable: false
|
||||
tap: "cb_##_tap0"
|
||||
ip: 172.25.6.1
|
||||
netmask: 255.255.255.0
|
||||
mtu: 1518
|
||||
powersaving_mode: 1
|
||||
|
||||
evse_bsp:
|
||||
enable: false
|
||||
module_id: "bsp_##"
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_ping_interval_ms: 1000
|
||||
capabilities:
|
||||
max_current_A_import: 16
|
||||
min_current_A_import: 6
|
||||
max_phase_count_import: 3
|
||||
min_phase_count_import: 3
|
||||
max_current_A_export: 16
|
||||
min_current_A_export: 6
|
||||
max_phase_count_export: 3
|
||||
min_phase_count_export: 3
|
||||
supports_changing_phases_during_charging: false
|
||||
connector_type: "IEC62196Type2Cable"
|
||||
ovm_enabled: true
|
||||
ovm_module_id: "ovm_1"
|
||||
|
||||
gpio:
|
||||
enable: false
|
||||
interval_s: 4
|
||||
mqtt_remote: "localhost"
|
||||
mqtt_port: 1883
|
||||
mqtt_ping_interval_ms: 1000
|
||||
gpio_0:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_1:
|
||||
mode: "Input"
|
||||
pulls: "NoPull"
|
||||
config: 32767
|
||||
gpio_2:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_3:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_4:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_5:
|
||||
mode: "Output"
|
||||
pulls: "NoPull"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_6:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_7:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_8:
|
||||
mode: "Output"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
gpio_9:
|
||||
mode: "Input"
|
||||
pulls: "PullUp"
|
||||
mdns: false
|
||||
config: 32767
|
||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <charge_bridge/everest_api/api_connector.hpp>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/udp/udp_client.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct bsp_bridge_config {
|
||||
std::string cb;
|
||||
std::string item;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
evse_bsp::everest_api_config api;
|
||||
};
|
||||
|
||||
class bsp_bridge : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
bsp_bridge(bsp_bridge_config const& config);
|
||||
~bsp_bridge() = default;
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
private:
|
||||
void handle_timer_event();
|
||||
|
||||
evse_bsp::api_connector m_api;
|
||||
everest::lib::io::udp::udp_client m_udp;
|
||||
everest::lib::io::event::timer_fd m_timer;
|
||||
bool m_udp_on_error{false};
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <everest/io/can/can_payload.hpp>
|
||||
#include <everest/io/can/socket_can.hpp>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/udp/udp_client.hpp>
|
||||
#include <memory>
|
||||
|
||||
extern "C" struct cb_can_message;
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct can_bridge_config {
|
||||
std::string cb;
|
||||
std::string item;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::string can_device;
|
||||
};
|
||||
|
||||
class can_bridge : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
can_bridge(can_bridge_config const& config);
|
||||
~can_bridge();
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
private:
|
||||
void handle_error_timer();
|
||||
void handle_heartbeat_timer();
|
||||
void send_can_to_udp(cb_can_message const& pl);
|
||||
std::unique_ptr<everest::lib::io::can::socket_can> m_can;
|
||||
everest::lib::io::udp::udp_client m_udp;
|
||||
std::string m_can_device;
|
||||
std::string m_identifier;
|
||||
everest::lib::io::event::timer_fd m_heartbeat_timer;
|
||||
std::chrono::steady_clock::time_point m_last_msg_to_cb;
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <charge_bridge/bsp_bridge.hpp>
|
||||
#include <charge_bridge/can_bridge.hpp>
|
||||
#include <charge_bridge/discovery.hpp>
|
||||
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
|
||||
#include <charge_bridge/gpio_bridge.hpp>
|
||||
#include <charge_bridge/heartbeat_service.hpp>
|
||||
#include <charge_bridge/plc_bridge.hpp>
|
||||
#include <charge_bridge/serial_bridge.hpp>
|
||||
#include <charge_bridge/utilities/symlink.hpp>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/serial/event_pty.hpp>
|
||||
#include <everest/io/tun_tap/tap_client.hpp>
|
||||
#include <everest/util/async/monitor.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct charge_bridge_status {
|
||||
bool is_connected{false};
|
||||
bool discovery_pending{false};
|
||||
};
|
||||
|
||||
struct charge_bridge_config {
|
||||
std::string cb_name;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::optional<can_bridge_config> can0;
|
||||
std::optional<serial_bridge_config> serial1;
|
||||
std::optional<serial_bridge_config> serial2;
|
||||
std::optional<serial_bridge_config> serial3;
|
||||
std::optional<plc_bridge_config> plc;
|
||||
std::optional<bsp_bridge_config> bsp;
|
||||
std::optional<heartbeat_config> heartbeat;
|
||||
std::optional<gpio_config> gpio;
|
||||
firmware_update::fw_update_config firmware;
|
||||
};
|
||||
|
||||
void print_charge_bridge_config(charge_bridge_config const& config);
|
||||
|
||||
class charge_bridge : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
charge_bridge(charge_bridge_config const& config);
|
||||
~charge_bridge();
|
||||
|
||||
bool update_firmware(bool force);
|
||||
|
||||
std::string get_pty_1_slave_path();
|
||||
std::string get_pty_2_slave_path();
|
||||
std::string get_pty_3_slave_path();
|
||||
|
||||
void print_config();
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
void manage(everest::lib::io::event::fd_event_handler& handler, std::atomic_bool const& exit, bool force_update);
|
||||
|
||||
private:
|
||||
void init();
|
||||
void init_discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding);
|
||||
void handle_discovery(std::string const& ip);
|
||||
|
||||
private:
|
||||
std::unique_ptr<can_bridge> m_can_0_client;
|
||||
std::unique_ptr<serial_bridge> m_pty_1;
|
||||
std::unique_ptr<serial_bridge> m_pty_2;
|
||||
std::unique_ptr<serial_bridge> m_pty_3;
|
||||
std::unique_ptr<bsp_bridge> m_bsp;
|
||||
std::unique_ptr<plc_bridge> m_plc;
|
||||
std::unique_ptr<heartbeat_service> m_heartbeat;
|
||||
std::unique_ptr<gpio_bridge> m_gpio;
|
||||
std::unique_ptr<discovery> m_discovery;
|
||||
|
||||
everest::lib::io::event::fd_event_handler* m_event_handler{nullptr};
|
||||
bool m_force_firmware_update{false};
|
||||
everest::lib::util::monitor<charge_bridge_status> m_cb_status;
|
||||
bool m_was_connected{false};
|
||||
bool m_discovery_active{false};
|
||||
|
||||
charge_bridge_config m_config;
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/mdns/mdns.hpp>
|
||||
#include <everest/io/mdns/mdns_client.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
enum class discovery_device_type {
|
||||
CB_EVSE,
|
||||
CB_EV
|
||||
};
|
||||
|
||||
class discovery : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
using discovery_cb = std::function<void(std::string const&)>;
|
||||
|
||||
discovery(discovery_device_type type);
|
||||
discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding);
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
void set_discovery_callback(discovery_cb const& cb);
|
||||
|
||||
private:
|
||||
void add_client(std::string const& interface);
|
||||
void query_registry();
|
||||
|
||||
std::vector<std::unique_ptr<everest::lib::io::mdns::mdns_client>> m_mdns;
|
||||
everest::lib::io::event::timer_fd m_timer;
|
||||
discovery_cb m_on_discover;
|
||||
everest::lib::io::mdns::mDNS_registry m_registry;
|
||||
discovery_device_type m_type;
|
||||
static const std::string discovery_id;
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <charge_bridge/everest_api/ev_bsp_api.hpp>
|
||||
#include <charge_bridge/everest_api/evse_bsp_api.hpp>
|
||||
#include <charge_bridge/everest_api/ovm_api.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/mqtt/mqtt_client.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_manager/API.hpp>
|
||||
#include <everest_api_types/utilities/Topics.hpp>
|
||||
#include <functional>
|
||||
#include <protocol/cb_common.h>
|
||||
#include <protocol/evse_bsp_cb_to_host.h>
|
||||
#include <protocol/evse_bsp_host_to_cb.h>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
namespace API_BSP = everest::lib::API::V1_0::types::evse_board_support;
|
||||
|
||||
struct everest_api_config {
|
||||
std::string mqtt_remote;
|
||||
std::string mqtt_bind;
|
||||
uint16_t mqtt_port;
|
||||
uint32_t mqtt_ping_interval_ms;
|
||||
evse_bsp_config evse;
|
||||
evse_ovm_config ovm;
|
||||
evse_ev_bsp_config ev;
|
||||
};
|
||||
|
||||
class api_connector : public everest::lib::io::event::fd_event_register_interface {
|
||||
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
|
||||
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
|
||||
|
||||
public:
|
||||
api_connector(everest_api_config const& config, std::string const& cb_identifier);
|
||||
void set_cb_tx(tx_ftor const& handler);
|
||||
void set_cb_message(evse_bsp_cb_to_host const& msg);
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
private:
|
||||
void handle_mqtt_connect();
|
||||
void handle_cb_connection_state();
|
||||
bool check_cb_heartbeat();
|
||||
|
||||
std::string m_cb_identifier;
|
||||
everest::lib::io::mqtt::mqtt_client m_mqtt;
|
||||
tx_ftor m_tx;
|
||||
std::chrono::steady_clock::time_point m_last_cb_heartbeat;
|
||||
everest::lib::io::event::timer_fd m_sync_timer;
|
||||
|
||||
std::string m_evse_bsp_receive_topic;
|
||||
std::string m_evse_bsp_send_topic;
|
||||
std::string m_ovm_receive_topic;
|
||||
std::string m_ovm_send_topic;
|
||||
std::string m_ev_bsp_receive_topic;
|
||||
std::string m_ev_bsp_send_topic;
|
||||
bool m_evse_bsp_enabled{false};
|
||||
bool m_ovm_enabled{false};
|
||||
bool m_ev_bsp_enabled{false};
|
||||
bool m_cb_initial_comm_check{true};
|
||||
bool m_cb_connected{false};
|
||||
evse_bsp_host_to_cb m_host_status;
|
||||
|
||||
evse_bsp_api m_evse_bsp;
|
||||
ovm_api m_ovm;
|
||||
ev_bsp_api m_ev_bsp;
|
||||
};
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,101 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest_api_types/ev_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_manager/API.hpp>
|
||||
#include <everest_api_types/generic/API.hpp>
|
||||
#include <everest_api_types/utilities/Topics.hpp>
|
||||
#include <functional>
|
||||
#include <protocol/cb_common.h>
|
||||
#include <protocol/evse_bsp_cb_to_host.h>
|
||||
#include <protocol/evse_bsp_host_to_cb.h>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
namespace API_EVSE_BSP = everest::lib::API::V1_0::types::evse_board_support;
|
||||
namespace API_EV_BSP = everest::lib::API::V1_0::types::ev_board_support;
|
||||
namespace API_EVM = everest::lib::API::V1_0::types::evse_manager;
|
||||
namespace API_GENERIC = everest::lib::API::V1_0::types::generic;
|
||||
// namespace API_OVM = everest::lib::API::V1_0::types::over_voltage_monitor;
|
||||
|
||||
struct evse_ev_bsp_config {
|
||||
bool enabled{false};
|
||||
std::string module_id;
|
||||
};
|
||||
|
||||
class ev_bsp_api : public everest::lib::io::event::fd_event_register_interface {
|
||||
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
|
||||
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
|
||||
using mqtt_ftor = std::function<void(std::string const&, std::string const&)>;
|
||||
|
||||
public:
|
||||
ev_bsp_api(evse_ev_bsp_config const& config, std::string const& cb_identifier, evse_bsp_host_to_cb& host_status);
|
||||
void set_cb_tx(tx_ftor const& handler);
|
||||
void set_cb_message(evse_bsp_cb_to_host const& msg);
|
||||
void set_mqtt_tx(mqtt_ftor const& tx);
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
void dispatch(std::string const& operation, std::string const& payload);
|
||||
|
||||
void raise_comm_fault();
|
||||
void clear_comm_fault();
|
||||
void sync(bool cb_connected);
|
||||
|
||||
private:
|
||||
void tx(evse_bsp_host_to_cb const& msg);
|
||||
|
||||
void send_bsp_event(API_EVSE_BSP::Event data);
|
||||
void send_bsp_measurement(API_EV_BSP::BspMeasurement data);
|
||||
void send_ev_info(API_EVM::EVInfo data);
|
||||
|
||||
void send_raise_error(API_GENERIC::ErrorEnum error, std::string const& subtype, std::string const& msg);
|
||||
void send_clear_error(API_GENERIC::ErrorEnum error, std::string const& subtype);
|
||||
|
||||
void send_communication_check();
|
||||
|
||||
void send_mqtt(std::string const& topic, std::string const& message);
|
||||
|
||||
void send_event(API_EVSE_BSP::Event data);
|
||||
|
||||
void receive_enable(std::string const& payload);
|
||||
void receive_set_cp_state(std::string const& payload);
|
||||
void receive_allow_power_on(std::string const& payload);
|
||||
void receive_diode_fail(std::string const& payload);
|
||||
void receive_set_ac_max_current(std::string const& payload);
|
||||
void receive_set_three_phases(std::string const& payload);
|
||||
void receive_set_rcd_error(std::string const& payload);
|
||||
void receive_heartbeat(std::string const& pl);
|
||||
|
||||
void handle_error(const SafetyErrorFlags& data);
|
||||
void handle_event_cp(std::uint8_t cp);
|
||||
void handle_event_relay(std::uint8_t relay);
|
||||
void handle_bsp_measurement(uint16_t cp, uint8_t pp_1, uint8_t pp2);
|
||||
|
||||
bool check_everest_heartbeat();
|
||||
void handle_everest_connection_state();
|
||||
|
||||
evse_bsp_host_to_cb& host_status;
|
||||
evse_bsp_cb_to_host m_cb_status;
|
||||
|
||||
tx_ftor m_tx;
|
||||
bool m_everest_connected{false};
|
||||
bool m_cb_connected{false};
|
||||
bool m_cb_initial_comm_check{true};
|
||||
bool m_bc_initial_comm_check{true};
|
||||
std::string m_cb_identifier;
|
||||
std::chrono::steady_clock::time_point last_everest_heartbeat;
|
||||
|
||||
mqtt_ftor m_mqtt_tx;
|
||||
std::size_t m_last_hb_id{0};
|
||||
everest::lib::API::V1_0::types::evse_board_support::Event last_cp_event{
|
||||
everest::lib::API::V1_0::types::evse_board_support::Event::Disconnected};
|
||||
};
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_manager/API.hpp>
|
||||
#include <everest_api_types/utilities/Topics.hpp>
|
||||
#include <functional>
|
||||
#include <protocol/cb_common.h>
|
||||
#include <protocol/evse_bsp_cb_to_host.h>
|
||||
#include <protocol/evse_bsp_host_to_cb.h>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
namespace API_BSP = everest::lib::API::V1_0::types::evse_board_support;
|
||||
namespace API_EVM = everest::lib::API::V1_0::types::evse_manager;
|
||||
|
||||
struct evse_bsp_config {
|
||||
std::string module_id;
|
||||
bool enabled{false};
|
||||
API_BSP::HardwareCapabilities capabilities;
|
||||
};
|
||||
|
||||
class evse_bsp_api : public everest::lib::io::event::fd_event_register_interface {
|
||||
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
|
||||
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
|
||||
using mqtt_ftor = std::function<void(std::string const&, std::string const&)>;
|
||||
|
||||
public:
|
||||
evse_bsp_api(evse_bsp_config const& config, std::string const& cb_identifier, evse_bsp_host_to_cb& host_status);
|
||||
void set_cb_tx(tx_ftor const& handler);
|
||||
void set_cb_message(evse_bsp_cb_to_host const& msg);
|
||||
void set_mqtt_tx(mqtt_ftor const& tx);
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
void dispatch(std::string const& operation, std::string const& payload);
|
||||
|
||||
void raise_comm_fault();
|
||||
void clear_comm_fault();
|
||||
void sync(bool cb_connected);
|
||||
|
||||
private:
|
||||
void tx(evse_bsp_host_to_cb const& msg);
|
||||
|
||||
void handle_event_cp(std::uint8_t cp);
|
||||
void handle_event_relay(std::uint8_t relay);
|
||||
void handle_error(const SafetyErrorFlags& data);
|
||||
void handle_pp_type1(std::uint8_t data);
|
||||
void handle_pp_type2(std::uint8_t data);
|
||||
void handle_stop_button(std::uint8_t data);
|
||||
|
||||
void send_event(API_BSP::Event data);
|
||||
void send_ac_nr_of_phases(std::uint8_t data);
|
||||
void send_capabilities();
|
||||
void send_ac_pp_amapcity(API_BSP::Ampacity data);
|
||||
void send_request_stop_transaction(API_EVM::StopTransactionReason data);
|
||||
void send_rcd_current(std::uint8_t data);
|
||||
void send_raise_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg);
|
||||
void send_clear_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg);
|
||||
void send_communication_check();
|
||||
void send_reply_reset(std::string const& replyTo);
|
||||
|
||||
void send_mqtt(std::string const& topic, std::string const& message);
|
||||
|
||||
void receive_enable(std::string const& payload);
|
||||
void receive_pwm_on(std::string const& payload);
|
||||
void receive_cp_state_X1(std::string const& payload);
|
||||
void receive_cp_state_F(std::string const& payload);
|
||||
void receive_allow_power_on(std::string const& payload);
|
||||
void receive_ac_switch_three_phases_while_charging(std::string const& payload);
|
||||
void receive_ac_overcurrent_limit(std::string const& payload);
|
||||
void receive_lock();
|
||||
void receive_unlock();
|
||||
void receive_self_test(std::string const& payload);
|
||||
void receive_request_reset(std::string const& payload);
|
||||
void receive_heartbeat(std::string const& pl);
|
||||
|
||||
bool check_everest_heartbeat();
|
||||
void handle_everest_connection_state();
|
||||
|
||||
evse_bsp_host_to_cb& host_status;
|
||||
evse_bsp_cb_to_host cb_status;
|
||||
|
||||
tx_ftor m_tx;
|
||||
everest::lib::io::event::timer_fd m_capabilities_timer;
|
||||
API_BSP::HardwareCapabilities m_capabilities;
|
||||
bool m_enabled{false};
|
||||
bool everest_connected{false};
|
||||
bool m_cb_connected{false};
|
||||
bool m_bc_initial_comm_check{true};
|
||||
std::string m_cb_identifier;
|
||||
std::chrono::steady_clock::time_point last_everest_heartbeat;
|
||||
|
||||
mqtt_ftor m_mqtt_tx;
|
||||
std::size_t m_last_hb_id{0};
|
||||
};
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest_api_types/evse_manager/API.hpp>
|
||||
#include <everest_api_types/over_voltage_monitor/API.hpp>
|
||||
#include <everest_api_types/utilities/Topics.hpp>
|
||||
#include <functional>
|
||||
#include <protocol/cb_common.h>
|
||||
#include <protocol/evse_bsp_cb_to_host.h>
|
||||
#include <protocol/evse_bsp_host_to_cb.h>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
namespace API_OVM = everest::lib::API::V1_0::types::over_voltage_monitor;
|
||||
|
||||
struct evse_ovm_config {
|
||||
bool enabled{false};
|
||||
std::string module_id;
|
||||
};
|
||||
|
||||
class ovm_api : public everest::lib::io::event::fd_event_register_interface {
|
||||
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
|
||||
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
|
||||
using mqtt_ftor = std::function<void(std::string const&, std::string const&)>;
|
||||
|
||||
public:
|
||||
ovm_api(evse_ovm_config const& config, std::string const& cb_identifier, evse_bsp_host_to_cb& host_status);
|
||||
void set_cb_tx(tx_ftor const& handler);
|
||||
void set_cb_message(evse_bsp_cb_to_host const& msg);
|
||||
void set_mqtt_tx(mqtt_ftor const& tx);
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
void dispatch(std::string const& operation, std::string const& payload);
|
||||
|
||||
void raise_comm_fault();
|
||||
void clear_comm_fault();
|
||||
void sync(bool cb_connected);
|
||||
|
||||
private:
|
||||
void tx(evse_bsp_host_to_cb const& msg);
|
||||
|
||||
void send_voltage_measurement_V(double data);
|
||||
void send_raise_error(API_OVM::ErrorEnum error, std::string const& subtype, std::string const& msg,
|
||||
API_OVM::ErrorSeverityEnum severity);
|
||||
void send_clear_error(API_OVM::ErrorEnum error, std::string const& subtype);
|
||||
void send_communication_check();
|
||||
|
||||
void send_mqtt(std::string const& topic, std::string const& message);
|
||||
|
||||
void handle_dc_hv_ov_emergency(bool high);
|
||||
void handle_dc_hv_ov_error(bool high);
|
||||
void handle_cp_state(CpState state);
|
||||
|
||||
void receive_set_limits(std::string const& payload);
|
||||
void receive_start();
|
||||
void receive_stop();
|
||||
void receive_reset_over_voltage_error();
|
||||
void receive_heartbeat(std::string const& pl);
|
||||
|
||||
bool check_everest_heartbeat();
|
||||
void handle_everest_connection_state();
|
||||
|
||||
evse_bsp_host_to_cb& host_status;
|
||||
evse_bsp_cb_to_host m_cb_status;
|
||||
|
||||
tx_ftor m_tx;
|
||||
bool m_everest_connected{false};
|
||||
bool m_cb_connected{false};
|
||||
bool m_cb_initial_comm_check{true};
|
||||
bool m_bc_initial_comm_check{true};
|
||||
std::string m_cb_identifier;
|
||||
std::chrono::steady_clock::time_point last_everest_heartbeat;
|
||||
|
||||
API_OVM::OverVoltageLimits m_limits{0, 0};
|
||||
mqtt_ftor m_mqtt_tx;
|
||||
std::size_t m_last_hb_id{0};
|
||||
};
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <charge_bridge/utilities/filesystem.hpp>
|
||||
#include <charge_bridge/utilities/sync_udp_client.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace charge_bridge::firmware_update {
|
||||
|
||||
struct fw_update_config {
|
||||
std::string cb;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::string fw_path;
|
||||
bool fw_update_on_start;
|
||||
};
|
||||
|
||||
class sync_fw_updater {
|
||||
public:
|
||||
sync_fw_updater(fw_update_config const& config);
|
||||
~sync_fw_updater() = default;
|
||||
|
||||
std::optional<std::string> get_fw_version();
|
||||
bool switch_bank();
|
||||
bool ping();
|
||||
bool upload_fw();
|
||||
|
||||
void print_fw_version();
|
||||
bool print_switch_bank();
|
||||
bool quick_check_connection();
|
||||
bool check_connection();
|
||||
bool check_if_correct_fw_installed();
|
||||
|
||||
private:
|
||||
bool check_reply(utilities::sync_udp_client::reply const& val);
|
||||
|
||||
bool upload_firmware();
|
||||
|
||||
bool upload_init(const fs::path& file_path, std::uint32_t& offset,
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader& hdr);
|
||||
bool upload_transfer(const fs::path& file_path, std::uint16_t& sector, std::uint32_t offset,
|
||||
std::uint32_t& total_bytes);
|
||||
bool upload_finish(const fs::path& file_path, std::uint32_t total_bytes,
|
||||
const charge_bridge::filesystem_utils::CryptSignedHeader& hdr);
|
||||
|
||||
everest::lib::io::udp::udp_payload make_fw_chunk(std::uint16_t sector, std::uint8_t last_chunk,
|
||||
std::vector<std::uint8_t> const& data);
|
||||
|
||||
utilities::sync_udp_client m_udp;
|
||||
fw_update_config m_config;
|
||||
static const std::uint32_t app_udp_sector_size;
|
||||
static const std::uint16_t sub_chunk_size;
|
||||
};
|
||||
} // namespace charge_bridge::firmware_update
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include "everest/io/mqtt/mosquitto_cpp.hpp"
|
||||
#include <array>
|
||||
#include <everest/io/can/can_payload.hpp>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/mqtt/mqtt_client.hpp>
|
||||
#include <everest/io/udp/udp_client.hpp>
|
||||
#include <protocol/cb_management.h>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct gpio_config {
|
||||
std::string cb;
|
||||
std::string item;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::uint16_t interval_s;
|
||||
std::string mqtt_remote;
|
||||
std::string mqtt_bind;
|
||||
std::uint16_t mqtt_port;
|
||||
std::uint32_t mqtt_ping_interval_ms;
|
||||
};
|
||||
|
||||
class gpio_bridge : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
gpio_bridge(gpio_config const& config);
|
||||
~gpio_bridge();
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
private:
|
||||
void handle_error_timer();
|
||||
void handle_heartbeat_timer();
|
||||
void handle_udp_rx(everest::lib::io::udp::udp_payload const& payload);
|
||||
void dispatch(everest::lib::io::mqtt::mqtt_client::message const& data);
|
||||
void send_mqtt(std::string const& topic, std::string const& message);
|
||||
void send_udp();
|
||||
|
||||
everest::lib::io::udp::udp_client m_udp;
|
||||
bool m_udp_on_error{false};
|
||||
everest::lib::io::event::timer_fd m_heartbeat_timer;
|
||||
std::chrono::steady_clock::time_point last_heartbeat;
|
||||
CbManagementPacket<CbGpioPacket> m_message;
|
||||
std::string m_identifier;
|
||||
bool m_mqtt_on_error{false};
|
||||
everest::lib::io::mqtt::mqtt_client m_mqtt;
|
||||
std::string m_receive_topic;
|
||||
std::string m_send_topic;
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include "protocol/cb_management.h"
|
||||
#include <chrono>
|
||||
#include <everest/io/can/can_payload.hpp>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/udp/udp_client.hpp>
|
||||
#include <memory>
|
||||
#include <protocol/cb_config.h>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct heartbeat_config {
|
||||
std::string cb;
|
||||
std::string item;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::uint16_t interval_s;
|
||||
std::uint16_t connection_to_s;
|
||||
CbConfig cb_config;
|
||||
};
|
||||
|
||||
class heartbeat_service : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
heartbeat_service(heartbeat_config const& config, std::function<void(bool)> const& publish_connection_status);
|
||||
~heartbeat_service();
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
private:
|
||||
void handle_error_timer();
|
||||
void handle_heartbeat_timer();
|
||||
void handle_udp_rx(everest::lib::io::udp::udp_payload const& payload);
|
||||
|
||||
everest::lib::io::udp::udp_client m_udp;
|
||||
bool m_udp_on_error{false};
|
||||
everest::lib::io::event::timer_fd m_heartbeat_timer;
|
||||
std::string m_identifier;
|
||||
CbManagementPacket<CbConfig> m_config_message;
|
||||
std::chrono::steady_clock::time_point m_last_heartbeat_reply;
|
||||
bool m_cb_connected{false};
|
||||
bool m_inital_cb_commcheck{true};
|
||||
std::chrono::milliseconds m_heartbeat_interval;
|
||||
std::chrono::milliseconds m_connection_to;
|
||||
std::function<void(bool)> m_publish_connection_status;
|
||||
std::uint32_t m_mcu_timestamp{0};
|
||||
int m_mcu_reset_count{0};
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/tun_tap/tap_client.hpp>
|
||||
#include <everest/io/udp/udp_client.hpp>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct plc_bridge_config {
|
||||
std::string cb;
|
||||
std::string item;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::string plc_tap;
|
||||
std::string plc_ip;
|
||||
std::string plc_netmaks;
|
||||
int plc_mtu;
|
||||
};
|
||||
|
||||
class plc_bridge : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
plc_bridge(plc_bridge_config const& config);
|
||||
~plc_bridge() = default;
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
|
||||
private:
|
||||
void handle_timer_event();
|
||||
|
||||
everest::lib::io::tun_tap::tap_client m_tap;
|
||||
everest::lib::io::udp::udp_client m_udp;
|
||||
everest::lib::io::event::timer_fd m_timer;
|
||||
bool m_udp_on_error{false};
|
||||
bool m_tap_on_error{false};
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <charge_bridge/utilities/symlink.hpp>
|
||||
#include <everest/io/event/fd_event_register_interface.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <everest/io/serial/event_pty.hpp>
|
||||
#include <everest/io/tcp/tcp_client.hpp>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
struct serial_bridge_config {
|
||||
std::string cb;
|
||||
std::string item;
|
||||
std::uint16_t cb_port;
|
||||
std::string cb_remote;
|
||||
std::string serial_device;
|
||||
};
|
||||
|
||||
class serial_bridge : public everest::lib::io::event::fd_event_register_interface {
|
||||
public:
|
||||
serial_bridge(serial_bridge_config const& config);
|
||||
~serial_bridge() = default;
|
||||
|
||||
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
|
||||
std::string get_slave_path();
|
||||
|
||||
private:
|
||||
void reset_tcp();
|
||||
|
||||
everest::lib::io::serial::event_pty m_pty;
|
||||
everest::lib::io::tcp::tcp_client m_tcp;
|
||||
utilities::symlink m_symlink;
|
||||
int m_tcp_last_error_id = -1;
|
||||
};
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace charge_bridge::filesystem_utils {
|
||||
|
||||
bool read_from_file_partial(const fs::path& file_path, const std::size_t byte_count, std::string& out_data);
|
||||
|
||||
bool read_from_file(const fs::path& file_path, std::string& out_data);
|
||||
|
||||
/// @brief Process the file in chunks with the provided function. If the process function
|
||||
/// returns false, this function will also immediately return
|
||||
/// @return True if the file was properly opened false otherwise
|
||||
bool process_file(const fs::path& file_path, std::size_t buffer_size,
|
||||
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func);
|
||||
|
||||
bool process_file(std::ifstream& file, std::size_t buffer_size,
|
||||
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func);
|
||||
|
||||
struct CryptSignedHeader {
|
||||
std::string firmware_version; // max 32 bytes long string describing the fw version
|
||||
std::uint8_t sig_len = 0;
|
||||
std::vector<std::uint8_t> signature; // length = sig_len
|
||||
std::uint8_t num_sectors = 0; // global one-byte value
|
||||
std::array<std::uint8_t, 16> iv{}; // 16-byte IV from file #2
|
||||
};
|
||||
|
||||
bool read_crypt_signed_header(const fs::path& path, CryptSignedHeader& hdr, std::uint32_t& image_offset);
|
||||
|
||||
} // namespace charge_bridge::filesystem_utils
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
std::ostream& print_error(std::string const& device, std::string const& unit, int status);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <charge_bridge/charge_bridge.hpp>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
std::vector<charge_bridge_config> parse_config_multi(std::string const& config_file);
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
// Converts a struct to raw bytes
|
||||
template <typename T> static inline void struct_to_vector(const T& data_struct, std::vector<std::uint8_t>& buffer) {
|
||||
static constexpr auto struct_size = sizeof(T);
|
||||
|
||||
buffer.resize(struct_size);
|
||||
std::memcpy(buffer.data(), &data_struct, struct_size);
|
||||
}
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <protocol/cb_config.h>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
std::string to_string(CbCanBaudrate value);
|
||||
std::string to_string(CbUartBaudrate value);
|
||||
std::string to_string(CbUartParity value);
|
||||
std::string to_string(CbUartStopbits value);
|
||||
std::string to_string(CbUartConfig const& value);
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
bool string_starts_with(std::string_view const& str, std::string_view const& pattern);
|
||||
bool string_ends_with(std::string const& str, std::string const& pattern);
|
||||
|
||||
std::string string_after_pattern(std::string_view const& str, std::string_view const& pattern);
|
||||
std::string& replace_all_in_place(std::string& source, std::string const& placeholder, std::string const& substitute);
|
||||
std::string replace_all(std::string const& source, std::string const& placeholder, std::string const& substitute);
|
||||
|
||||
std::set<std::string> csv_to_set(std::string const& str);
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
class symlink {
|
||||
public:
|
||||
symlink(std::string const& src, std::string const& tar);
|
||||
symlink();
|
||||
bool set_link(std::string const& src, std::string const& tar);
|
||||
bool del_link();
|
||||
~symlink();
|
||||
|
||||
private:
|
||||
std::string m_tar;
|
||||
};
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include "everest/io/udp/udp_payload.hpp"
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/udp/udp_socket.hpp>
|
||||
#include <optional>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
class sync_udp_client {
|
||||
public:
|
||||
using udp_payload = everest::lib::io::udp::udp_payload;
|
||||
using reply = std::optional<udp_payload>;
|
||||
sync_udp_client(std::string const& remote, std::uint16_t port);
|
||||
sync_udp_client(std::string const& remote, std::uint16_t port, std::uint16_t retries, std::uint16_t timeout_ms);
|
||||
reply request_reply(udp_payload const& payload);
|
||||
reply request_reply(udp_payload const& payload, std::uint16_t timeout_ms, std::uint16_t retries);
|
||||
bool tx(udp_payload const& payload);
|
||||
reply rx();
|
||||
reply rx(std::uint16_t timeout_ms);
|
||||
bool is_open();
|
||||
|
||||
private:
|
||||
void init(std::string const& remote, std::uint16_t port);
|
||||
void clear_socket();
|
||||
|
||||
std::uint16_t m_retries;
|
||||
std::uint16_t m_timeout_ms;
|
||||
everest::lib::io::udp::udp_client_socket m_udp;
|
||||
everest::lib::io::event::fd_event_handler m_handler;
|
||||
};
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <protocol/cb_config.h>
|
||||
#include <protocol/cb_management.h>
|
||||
#include <ryml.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
class yml_node_error {
|
||||
public:
|
||||
yml_node_error(c4::yml::ConstNodeRef node);
|
||||
yml_node_error(c4::yml::ConstNodeRef node, std::string const& msg);
|
||||
|
||||
c4::yml::ConstNodeRef m_node;
|
||||
std::string m_msg;
|
||||
};
|
||||
|
||||
namespace EXT_API = everest::lib::API;
|
||||
namespace EXT_API_BSP = EXT_API::V1_0::types::evse_board_support;
|
||||
|
||||
bool decode_CbGpioMode(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs);
|
||||
bool decode_CbGpioPulls(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs);
|
||||
bool decode_CbUartBaudrate(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs);
|
||||
bool decode_CbUartStopbits(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs);
|
||||
bool decode_CbUartParity(c4::yml::ConstNodeRef const& node, CbUartParity& rhs);
|
||||
bool decode_CbCanBaudrate(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs);
|
||||
bool decode_CbRelayMode(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs);
|
||||
bool decode_CbSafetyMode(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs);
|
||||
bool decode_RelayConfig(c4::yml::ConstNodeRef const& node, RelayConfig& rhs);
|
||||
bool decode_SafetyConfig(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs);
|
||||
bool decode_CbGpioConfig(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs);
|
||||
bool decode_CbUartConfig(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs);
|
||||
bool decode_CbCanConfig(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs);
|
||||
bool decode_CbNetworkConfig(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs);
|
||||
bool decode_Connector_type(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs);
|
||||
bool decode_HardwareCapabilities(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs);
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartParity& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, RelayConfig& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs);
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs);
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
120
tools/EVerest-main/applications/pionix_chargebridge/main.cpp
Normal file
120
tools/EVerest-main/applications/pionix_chargebridge/main.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "charge_bridge/charge_bridge.hpp"
|
||||
#include "charge_bridge/utilities/string.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <charge_bridge/utilities/parse_config.hpp>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/event/timer_fd.hpp>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
|
||||
using namespace everest::lib::io::event;
|
||||
using namespace everest::lib::API::V1_0::types;
|
||||
using namespace charge_bridge;
|
||||
|
||||
enum class mode {
|
||||
error,
|
||||
connector,
|
||||
update,
|
||||
update_only,
|
||||
};
|
||||
|
||||
mode parse_args(int argc, char* argv[], std::vector<std::string>& config_files) {
|
||||
// clang-format off
|
||||
auto print_msg = []() {
|
||||
std::cout << "\nUSAGE: \n";
|
||||
std::cout << "pionix_chargebridge [--update][--update_only] {config_file [config_file_2 ....]} \n";
|
||||
std::cout << "\n";
|
||||
std::cout << "--update use this flag to execute an update at start and continue operation after\n";
|
||||
std::cout << "--update_only use this flag to execute an update and stop the application after\n";
|
||||
std::cout << "config_file use this configuration file\n";
|
||||
std::cout << "config_file_x add more configuration files for each additional ChargeBridge group\n";
|
||||
std::cout << "\n";
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
auto mode = mode::connector;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string current_arg = argv[i];
|
||||
if (current_arg == "--update_only") {
|
||||
mode = mode::update_only;
|
||||
} else if (current_arg == "--update") {
|
||||
mode = mode::update;
|
||||
} else if (utilities::string_starts_with(current_arg, "--")) {
|
||||
mode = mode::error;
|
||||
break;
|
||||
} else {
|
||||
config_files.push_back(current_arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (config_files.size() == 0) {
|
||||
mode = mode::error;
|
||||
}
|
||||
|
||||
if (mode == mode::error) {
|
||||
print_msg();
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
std::atomic<bool> g_run_application(true);
|
||||
void signal_handler(int signum) {
|
||||
std::cout << "\nSignal " << signum << " received. Initiating graceful shutdown." << std::endl;
|
||||
g_run_application = false;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "PIONIX ChargeBridge (C) 2025-2026\n" << std::endl;
|
||||
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGHUP, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
|
||||
std::vector<std::string> config_files;
|
||||
std::vector<charge_bridge_config> cb_configs;
|
||||
std::vector<std::unique_ptr<::charge_bridge::charge_bridge>> cb_handler;
|
||||
|
||||
auto mode_of_operation = parse_args(argc, argv, config_files);
|
||||
if (mode_of_operation == mode::error) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
fd_event_handler ev_handler;
|
||||
|
||||
std::set<std::string> cb_ids_in_use;
|
||||
|
||||
for (auto const& elem : config_files) {
|
||||
auto config_list = utilities::parse_config_multi(elem);
|
||||
if (config_list.empty()) {
|
||||
g_run_application.store(false);
|
||||
break;
|
||||
}
|
||||
for (auto const& config : config_list) {
|
||||
print_charge_bridge_config(config);
|
||||
if (cb_ids_in_use.count(config.cb_name) > 0) {
|
||||
std::cerr << "Duplicate charge_bridge::name '" << config.cb_name << "'" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
cb_ids_in_use.insert(config.cb_name);
|
||||
cb_handler.push_back(std::make_unique<::charge_bridge::charge_bridge>(config));
|
||||
auto& cb = *cb_handler.rbegin();
|
||||
|
||||
if (mode_of_operation == mode::update_only) {
|
||||
cb->update_firmware(true);
|
||||
}
|
||||
|
||||
auto force_update = mode_of_operation == mode::update;
|
||||
cb->manage(ev_handler, g_run_application, force_update);
|
||||
}
|
||||
}
|
||||
|
||||
ev_handler.run(g_run_application);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Chargebridge Protocol
|
||||
|
||||
Contains the header definitions for the raw C structs that will be used for comms between the Linux system and the
|
||||
development board. Will also include various assertions and size/bounds checks to determine the that the client
|
||||
systems are compatible and have the same memory layout.
|
||||
|
||||
The headers are C compliant for both C and C++ user code.
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cb_platform.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* Enum definitions */
|
||||
typedef enum _CanErrorState {
|
||||
CanErrorState_Error_Active = 0,
|
||||
CanErrorState_Error_Passive = 1,
|
||||
CanErrorState_ForceSize = 0xFFFFFFFF,
|
||||
} CanErrorState;
|
||||
|
||||
typedef enum _CanBitrate {
|
||||
CanBitrate_125kbps = 0,
|
||||
CanBitrate_250kbps = 1,
|
||||
CanBitrate_500kbps = 2,
|
||||
CanBitrate_1000kbps = 3,
|
||||
CanBitrate_ForceSize = 0xFFFFFFFF,
|
||||
} CanBitrate;
|
||||
|
||||
typedef enum _CanFDBitrate {
|
||||
CanFDBitrate_1MBps = 0,
|
||||
CanFDBitrate_2MBps = 1,
|
||||
CanFDBitrate_3MBps = 2,
|
||||
CanFDBitrate_4MBps = 3,
|
||||
CanFDBitrate_5MBps = 4,
|
||||
CanFDBitrate_6MBps = 5,
|
||||
CanFDBitrate_7MBps = 6,
|
||||
CanFDBitrate_8MBps = 7,
|
||||
CanFDBitrate_ForceSize = 0xFFFFFFFF,
|
||||
} CanFDBitrate;
|
||||
|
||||
typedef enum _CanFlags {
|
||||
CanFlags_EFF = 1,
|
||||
CanFlags_RTR = 1 << 1,
|
||||
CanFlags_ERR = 1 << 2,
|
||||
} CanFlags;
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _CanStatistics {
|
||||
// tx: direction is from host to bus
|
||||
// rx: direction is from bus to host
|
||||
uint32_t frames_tx;
|
||||
uint32_t frames_rx;
|
||||
uint32_t event_rx_buf_full;
|
||||
uint32_t event_tx_buf_full;
|
||||
} CanStatistics;
|
||||
|
||||
typedef enum _CanPacketType : uint8_t{
|
||||
CanPacketType_Regular = 0,
|
||||
CanPacketType_Keep_Alive = 1,
|
||||
} CanPacketType;
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK cb_can_message {
|
||||
uint8_t version;
|
||||
CanPacketType packet_type; // 0: regular CAN packet, 1: dummy keep-alive packet
|
||||
CanErrorState error_state;
|
||||
CanStatistics statistics;
|
||||
CanBitrate bitrate;
|
||||
CanFDBitrate fd_bitrate; /* integer in MBit (1-8) */
|
||||
uint8_t can_flags; // EFF, RTR, ERR
|
||||
uint32_t can_id;
|
||||
|
||||
/* dlc 0..8: standard CAN frame with up to 8 bytes
|
||||
* FDCAN dlc:
|
||||
* 9: 12 bytes
|
||||
* 10: 16 bytes
|
||||
* 11: 20 bytes
|
||||
* 12: 24 bytes
|
||||
* 13: 32 bytes
|
||||
* 14: 48 bytes
|
||||
* 15: 64 bytes
|
||||
*/
|
||||
uint8_t dlc;
|
||||
|
||||
// Note: in UDP transmission, data bytes at the end may be omitted in the message.
|
||||
// Always check dlc first before accessing the data
|
||||
uint8_t data[64];
|
||||
};
|
||||
|
||||
#define cb_can_message_set_zero \
|
||||
{0, CanPacketType_Regular, CanErrorState_Error_Active, {0, 0, 0, 0}, CanBitrate_125kbps, CanFDBitrate_1MBps, 0, 0, \
|
||||
0, {0, 0, 0, 0, 0, 0, 0, 0}};
|
||||
|
||||
#include "test/cb_can_message_test.h"
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cb_platform.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// Structs
|
||||
|
||||
typedef union _SafetyErrorFlags {
|
||||
struct _flags {
|
||||
uint32_t cp_not_state_c : 1;
|
||||
uint32_t pwm_not_enabled : 1;
|
||||
uint32_t pp_invalid : 1;
|
||||
uint32_t plug_temperature_too_high : 1;
|
||||
uint32_t internal_temperature_too_high : 1;
|
||||
uint32_t emergency_input_latched : 1;
|
||||
uint32_t relay_health_latched : 1;
|
||||
uint32_t vdd_3v3_out_of_range : 1;
|
||||
uint32_t vdd_core_out_of_range : 1;
|
||||
uint32_t vdd_12V_out_of_range : 1;
|
||||
uint32_t vdd_N12V_out_of_range : 1;
|
||||
uint32_t vdd_refint_out_of_range : 1;
|
||||
uint32_t external_allow_power_on : 1;
|
||||
uint32_t config_mem_error : 1;
|
||||
uint32_t dc_hv_ov_emergency : 1;
|
||||
uint32_t dc_hv_ov_error : 1;
|
||||
uint32_t reserved : 17;
|
||||
} flags;
|
||||
uint32_t raw;
|
||||
} SafetyErrorFlags;
|
||||
|
||||
|
||||
typedef enum _CpState : uint8_t {
|
||||
CpState_A,
|
||||
CpState_B,
|
||||
CpState_C,
|
||||
CpState_D,
|
||||
CpState_E,
|
||||
CpState_F,
|
||||
CpState_DF,
|
||||
CpState_INVALID
|
||||
} CpState;
|
||||
@@ -0,0 +1,127 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cb_platform.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#define CB_NUMBER_OF_GPIOS 10
|
||||
#define CB_NUMBER_OF_UARTS 3
|
||||
|
||||
// enums
|
||||
|
||||
typedef enum _CbGpioMode : uint8_t {
|
||||
CBG_Input = 0x00,
|
||||
CBG_Output = 0x01,
|
||||
CBG_Pwm_Input = 0x02,
|
||||
CBG_Pwm_Output = 0x03,
|
||||
CBG_RS485_2_DE = 0x04,
|
||||
CBG_Rcd_Selftest_Output = 0x05,
|
||||
CBG_Rcd_Error_Input= 0x06,
|
||||
CBG_Rcd_PWM_Input= 0x07,
|
||||
CBG_MotorLock_1 = 0x08,
|
||||
CBG_MotorLock_2 = 0x09,
|
||||
} CbGpioMode;
|
||||
|
||||
typedef enum _CbRelayMode : uint8_t {
|
||||
CBR_PowerRelay = 0x00, CBR_UserRelay = 0x01,
|
||||
} CbRelayMode;
|
||||
|
||||
typedef enum _CbGpioPulls : uint8_t {
|
||||
CBGP_NoPull = 0x00, CBGP_PullUp = 0x01, CBGP_PullDown = 0x02,
|
||||
} CbGpioPulls;
|
||||
|
||||
typedef enum _CbUartBaudrate : uint8_t {
|
||||
CBUBR_9600 = 0x00,
|
||||
CBUBR_19200 = 0x01,
|
||||
CBUBR_38400 = 0x02,
|
||||
CBUBR_57600 = 0x03,
|
||||
CBUBR_115200 = 0x04,
|
||||
CBUBR_230400 = 0x05,
|
||||
CBUBR_250000 = 0x06,
|
||||
CBUBR_460800 = 0x07,
|
||||
CBUBR_500000 = 0x08,
|
||||
CBUBR_1000000 = 0x09,
|
||||
CBUBR_2000000 = 0x0A,
|
||||
CBUBR_3000000 = 0x0B,
|
||||
CBUBR_4000000 = 0x0C,
|
||||
CBUBR_6000000 = 0x0D,
|
||||
CBUBR_8000000 = 0x0E,
|
||||
CBUBR_10000000 = 0x0F,
|
||||
} CbUartBaudrate;
|
||||
|
||||
typedef enum _CbUartStopbits : uint8_t {
|
||||
CBUS_OneStopBit = 0x00, CBUS_TwoStopBits = 0x01,
|
||||
} CbUartStopbits;
|
||||
|
||||
typedef enum _CbUartParity : uint8_t {
|
||||
CBUP_None = 0x00, CBUP_Odd = 0x01, CBUP_Even = 0x02,
|
||||
} CbUartParity;
|
||||
|
||||
typedef enum _CbCanBaudrate : uint8_t {
|
||||
CBCBR_125000 = 0x00,
|
||||
CBCBR_250000 = 0x01,
|
||||
CBCBR_500000 = 0x02,
|
||||
CBCBR_1000000 = 0x03,
|
||||
} CbCanBaudrate;
|
||||
|
||||
typedef enum _CbSafetyMode : uint8_t {
|
||||
CBSM_disabled = 0x00, CBSM_US = 0x01, CBSM_EU = 0x02,
|
||||
|
||||
} CbSafetyMode;
|
||||
|
||||
// Structs
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _relay_config {
|
||||
CbRelayMode relay_mode;
|
||||
uint8_t feedback_enabled; // 0: feedback unused, 1: feedback expected
|
||||
uint16_t feedback_delay_ms; // After switching, wait for this delay before evaluating feedback pin
|
||||
uint8_t feedback_inverted; // 0: feedback normal (mirror contact, high when relay is off), 1: inverted
|
||||
uint8_t pwm_dc; // 100: Do not use PWM. 1-99: Set PWM Duty cycle after delay
|
||||
uint16_t pwm_delay_ms; // Delay in ms after which the PWM starts
|
||||
uint16_t switchoff_delay_ms; // Delay before switching relay off. Can be used to set a small delay between EMG_OUT
|
||||
// and relays off [SR-SL-2]
|
||||
} RelayConfig;
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _safety_config {
|
||||
CbSafetyMode pp_mode; // set to 0: disabled 1: US type 1, 2: EU type 2
|
||||
uint8_t cp_avg_ms; // default is 10ms / pulses
|
||||
RelayConfig relays[3]; // Config for the 3 relay I/Os
|
||||
uint8_t inverted_emergency_input; // 0: normal operation, 1: emergency input is inverted
|
||||
uint8_t temperature_limit_pt1000_C; // Temperature limit for the PT1000 inputs. Relays will switch off if temperature is above the limit. Setting this to 0 will disable the feature.
|
||||
} SafetyConfig;
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _CbGpioConfig {
|
||||
CbGpioMode mode;
|
||||
CbGpioPulls pulls;
|
||||
uint8_t strap_option_mdns_naming; // sample as bit for mdns id;
|
||||
uint16_t mode_config; // Config value for the mode, e.g. frequency of PWM
|
||||
} CbGpioConfig;
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _CbUartConfig {
|
||||
CbUartBaudrate baudrate;
|
||||
CbUartStopbits stopbits;
|
||||
CbUartParity parity;
|
||||
} CbUartConfig;
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _CbCanConfig {
|
||||
CbCanBaudrate baudrate;
|
||||
} CbCanConfig;
|
||||
|
||||
typedef struct CB_COMPILER_ATTR_PACK _CbNetworkConfig {
|
||||
char mdns_name[20]; // custom MDNS name
|
||||
} CbNetworkConfig;
|
||||
|
||||
// Final complete config struct
|
||||
|
||||
#define CB_CONFIG_VERSION 0
|
||||
typedef struct CB_COMPILER_ATTR_PACK _cb_config {
|
||||
uint32_t config_version;
|
||||
SafetyConfig safety;
|
||||
CbGpioConfig gpios[CB_NUMBER_OF_GPIOS];
|
||||
CbUartConfig uarts[CB_NUMBER_OF_UARTS];
|
||||
CbCanConfig can;
|
||||
CbNetworkConfig network;
|
||||
uint8_t plc_powersaving_mode;
|
||||
} CbConfig;
|
||||
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* \file Common structs used between linux and the STM32 dev board. Data
|
||||
* will be sent raw over the UDP socket, with the sender using
|
||||
* \ref send(struct, sizeof(CbStruct))
|
||||
* and the receiver using
|
||||
* \ref receive(raw_bytes, sizeof(CbStruct))
|
||||
* CbStruct *struct = reinterpret_cast<CbStruct *>(raw_bytes);
|
||||
*
|
||||
* Notes:
|
||||
* 1) After V1 structs will not be able to remove fields, only add fields
|
||||
* after the existing fields
|
||||
* 2) There can be problems with variable length structs, for example
|
||||
* CbFirmwarePacket that can have a payload with lengths 0-1024
|
||||
*
|
||||
* Test files are added at the end that check the sizes of the structs
|
||||
* at compile time, to determine the fact that they are consistent
|
||||
* across platforms.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "cb_config.h"
|
||||
|
||||
#ifndef __cplusplus
|
||||
#error "This header is C++ only"
|
||||
#endif
|
||||
|
||||
enum class AppUDPResponse : uint32_t {
|
||||
AUR_Ok = 0x500D500D, AUR_Bad = 0xBADBAD00,
|
||||
};
|
||||
|
||||
enum class CbType : uint8_t {
|
||||
CCS_EVSE = 0, CCS_EV = 1,
|
||||
};
|
||||
|
||||
/*
|
||||
* What type of message is on the socket
|
||||
*/
|
||||
enum class CbStructType : uint16_t {
|
||||
|
||||
// track IP with timeout and port
|
||||
// Housekeepig, heartbeat/config (safety_config, serial port(fixed)/CAN bitrate, gpio config, mdns module name), fw version, softreset,
|
||||
CST_HostToCb_Heartbeat = 1,
|
||||
CST_CbToHost_Heartbeat = 2,
|
||||
|
||||
// track IP with timeout and port
|
||||
// GPIO client
|
||||
CST_HostToCb_Gpio = 3,
|
||||
CST_CbToHost_Gpio = 4,
|
||||
|
||||
// FW update
|
||||
CST_CbFirmwareReply = 0xFFF9,
|
||||
CST_CbFirmwareStart = 0xFFFA,
|
||||
CST_CbFirmwarePacket = 0xFFFB,
|
||||
CST_CbFirmwareFinish = 0xFFFC,
|
||||
CST_CbFirmwareUpdateCancel = 0xFFFD,
|
||||
CST_CbFirmwarePing = 0xFFFE,
|
||||
CST_CbFirmwareGetVersion = 0xFFFF,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
This container message is used for all generic module management packets
|
||||
*/
|
||||
|
||||
|
||||
template<typename T>
|
||||
struct CB_COMPILER_ATTR_PACK CbManagementPacket {
|
||||
CbStructType type;
|
||||
T data;
|
||||
};
|
||||
|
||||
template<> struct CB_COMPILER_ATTR_PACK CbManagementPacket<void> {
|
||||
CbStructType type;
|
||||
};
|
||||
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbGpioPacket {
|
||||
uint8_t number_of_gpios; // Just to check compatibility
|
||||
uint16_t gpio_values[CB_NUMBER_OF_GPIOS]; // Actual value, 0: low, 1: high, or duty cycle for PWM
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbHeartbeatPacket {
|
||||
CbConfig module_config;
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbHeartbeatReplyPacket {
|
||||
int32_t cp_hi_mV;
|
||||
int32_t cp_lo_mV;
|
||||
int32_t vdd_core;
|
||||
int32_t vdd_3v3;
|
||||
int32_t vdd_refint;
|
||||
int32_t vdd_12V;
|
||||
int32_t vdd_N12V;
|
||||
int32_t pp_mOhm;
|
||||
int32_t pp_voltage_mV;
|
||||
uint8_t relay_state_feedback[3];
|
||||
int16_t temperature_mcu_C;
|
||||
int16_t temperature_pcb_C;
|
||||
int16_t temperature_modem_C;
|
||||
int16_t temperature_PT1000_C[2];
|
||||
int32_t uptime_ms;
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbFirmwareStart {
|
||||
uint8_t is_secure_fw :1;
|
||||
uint8_t requires_crc_verification :1;
|
||||
uint8_t requires_sha256_verification :1;
|
||||
uint8_t requires_signature_verification :1;
|
||||
uint8_t requires_decryption :1;
|
||||
|
||||
// IV in case we require decryption
|
||||
uint8_t iv[16];
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbFirmwarePacket {
|
||||
// If it is the last packet sent, used when we need to take care of
|
||||
// the padding bytes in case of decryption or other operations
|
||||
uint8_t last_packet :1;
|
||||
|
||||
uint16_t sector;
|
||||
uint16_t data_len;
|
||||
uint8_t data[1024];
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbFirmwareEnd {
|
||||
uint32_t firmware_len;
|
||||
|
||||
// Signature for the firmware in the faw format
|
||||
uint8_t fw_signature[128];
|
||||
uint8_t fw_signature_len;
|
||||
uint8_t watermark_secure_end;
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbFirmwareUpdateCancel {
|
||||
uint8_t dummy;
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbFirmwarePing {
|
||||
uint8_t dummy;
|
||||
};
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK CbFirmwareGetVersion {
|
||||
uint8_t dummy;
|
||||
};
|
||||
|
||||
#include "test/cb_management_test.h"
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* \file compiler utilities and Pionix defines for
|
||||
* cross-compiling
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#if defined(__cplusplus)
|
||||
#define CB_STATIC_ASSERT(cond, msg) static_assert(cond, msg)
|
||||
#else
|
||||
#define CB_STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
|
||||
#endif
|
||||
|
||||
#define CB_COMPILER_ATTR_PACK __attribute__((packed))
|
||||
|
||||
// Should be < MTU (defined as #define NX_DRIVER_ETHERNET_MTU 1514)
|
||||
#define CB_MAX_UDP_PACKET_SIZE (1024 + 256)
|
||||
// -128 since we might want some non-struct metadata
|
||||
#define CB_MAX_CB_STRUCT_SIZE (CB_MAX_UDP_PACKET_SIZE - 128)
|
||||
|
||||
#define CB_MAX_STRING_SIZE 64
|
||||
|
||||
#define cb_string(name) int8_t name[CB_MAX_STRING_SIZE]
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVSE_BSP_CB_TO_HOST_H
|
||||
#define EVSE_BSP_CB_TO_HOST_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "cb_platform.h"
|
||||
#include "cb_common.h"
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK evse_bsp_cb_to_host {
|
||||
// add version number as first uint16 ????
|
||||
// potentially unused, ignore for now
|
||||
uint8_t reset_reason;
|
||||
CpState cp_state;
|
||||
uint8_t relay_state;
|
||||
SafetyErrorFlags error_flags;
|
||||
uint8_t pp_state_type1;
|
||||
uint8_t pp_state_type2;
|
||||
uint8_t lock_state;
|
||||
uint32_t hv_mV;
|
||||
// still define handling set for
|
||||
uint8_t stop_charging;
|
||||
uint16_t cp_duty_cycle;
|
||||
};
|
||||
|
||||
/* Enum definitions */
|
||||
typedef enum _RelayState {
|
||||
RelayState_Open = 0,
|
||||
RelayState_Closed = 1
|
||||
} RelaiseState;
|
||||
|
||||
typedef enum _ResetReason {
|
||||
ResetReason_USER = 0,
|
||||
ResetReason_WATCHDOG = 1
|
||||
} ResetReason;
|
||||
|
||||
typedef enum _PpState_Type2 {
|
||||
PpState_Type2_STATE_NC = 0,
|
||||
PpState_Type2_STATE_13A = 1,
|
||||
PpState_Type2_STATE_20A = 2,
|
||||
PpState_Type2_STATE_32A = 3,
|
||||
PpState_Type2_STATE_70A = 4,
|
||||
PpState_Type2_STATE_FAULT = 5
|
||||
} PpState_Type2;
|
||||
|
||||
typedef enum _PpState_Type1 {
|
||||
PpState_Type1_STATE_NotConnected,
|
||||
PpState_Type1_STATE_Connected_Button_Pressed,
|
||||
PpState_Type1_STATE_Connected,
|
||||
PpState_Type1_STATE_Invalid
|
||||
} PpState_Type1;
|
||||
|
||||
typedef enum _LockState {
|
||||
LockState_UNDEFINED = 0,
|
||||
LockState_UNLOCKED = 1,
|
||||
LockState_LOCKED = 2
|
||||
} LockState;
|
||||
|
||||
#include "test/evse_bsp_cb_to_host_test.h"
|
||||
|
||||
#endif // EVSE_BSP_CB_TO_HOST_H
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVSE_BSP_HOST_TO_CB_H
|
||||
#define EVSE_BSP_HOST_TO_CB_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "cb_platform.h"
|
||||
|
||||
struct CB_COMPILER_ATTR_PACK evse_bsp_host_to_cb {
|
||||
uint8_t connector_lock; /* 0: unlock, otherwise: lock */
|
||||
uint32_t pwm_duty_cycle; /* in 0.01 %, 0 = State F, 10000 = X1 */
|
||||
uint8_t allow_power_on; /* 0 false, true otherwise */
|
||||
uint8_t reset; /* 0 false, true otherwise */
|
||||
uint8_t ovm_enable; /* 0 disabled, 1: enabled */
|
||||
uint8_t ovm_reset_errors; /* 0 leave errors untouched, 1: clear error bits for OVM */
|
||||
uint32_t ovm_limit_emergency_mV; /* 9ms limit in mV */
|
||||
uint32_t ovm_limit_error_mV; /* 400ms limit in mV */
|
||||
CpState ev_set_cp_state; /* Set CP state (EV side only) */
|
||||
uint8_t ev_set_diodefault; /* Set/Clear DF state (EV side only) */
|
||||
};
|
||||
|
||||
#include "test/evse_bsp_host_to_cb_test.h"
|
||||
|
||||
#endif // EVSE_BSP_H
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "protocol/cb_can_message.h"
|
||||
#include "protocol/cb_platform.h"
|
||||
CB_STATIC_ASSERT(sizeof(CanErrorState) == 4, "CanErrorState data size!!");
|
||||
CB_STATIC_ASSERT(sizeof(CanBitrate) == 4, "CanBitrate data size!!");
|
||||
CB_STATIC_ASSERT(sizeof(CanFDBitrate) == 4, "CanFDBitrate data size!!");
|
||||
CB_STATIC_ASSERT(sizeof(CanStatistics) == 4+4+4+4, "CanStatistics data size!!");
|
||||
CB_STATIC_ASSERT(sizeof(struct cb_can_message) == 1+4+16+4+4+1+4+1+64+1, "cb_can_message type size!!");
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* \file Test utilities used to determine that the used types
|
||||
* have the same sizes independent of the platforms that we
|
||||
* are using. Added to make sure that reinterpret_cast or other
|
||||
* types of cast will yield the same types across platform
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
CB_STATIC_ASSERT(sizeof(AppUDPResponse) == 4, "Wrong AppUDPReponse type size!");
|
||||
CB_STATIC_ASSERT(sizeof(CbType) == 1, "Wrong CB type size!");
|
||||
CB_STATIC_ASSERT(sizeof(CbStructType) == 2, "Wrong CB type size!");
|
||||
CB_STATIC_ASSERT((sizeof(CbFirmwareStart) == 16 + 1 && sizeof(CbFirmwareStart) <= CB_MAX_CB_STRUCT_SIZE),
|
||||
"Wrong CB type size!");
|
||||
CB_STATIC_ASSERT((sizeof(CbFirmwarePacket) == 1 + 2 + 2 + 1024 && sizeof(CbFirmwarePacket) <= CB_MAX_CB_STRUCT_SIZE),
|
||||
"Wrong CB type size!");
|
||||
CB_STATIC_ASSERT((sizeof(CbFirmwareEnd) == 4 + 1 + (128 + 1) && sizeof(CbFirmwareEnd) <= CB_MAX_CB_STRUCT_SIZE),
|
||||
"Wrong CB type size!");
|
||||
CB_STATIC_ASSERT((sizeof(CbHeartbeatPacket) == 119 && sizeof(CbHeartbeatPacket) <= CB_MAX_CB_STRUCT_SIZE),
|
||||
"Wrong CB type size!");
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
CB_STATIC_ASSERT(sizeof(struct evse_bsp_cb_to_host)== 11+4+4+2, "Wrong evse_bsp_cb_to_host size!!!");
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
CB_STATIC_ASSERT (sizeof(struct evse_bsp_host_to_cb) == 7+9+1+1+1, "Wrong evse_bsp_host_to_cb size!");
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/bsp_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <cstring>
|
||||
#include <everest/io/udp/udp_payload.hpp>
|
||||
#include <iostream>
|
||||
#include <protocol/evse_bsp_cb_to_host.h>
|
||||
#include <protocol/evse_bsp_host_to_cb.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
}
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
bsp_bridge::bsp_bridge(bsp_bridge_config const& config) :
|
||||
m_api(config.api, config.cb + "/" + config.item), m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(5s);
|
||||
|
||||
m_api.set_cb_tx([this](auto& data) {
|
||||
everest::lib::io::udp::udp_payload pl;
|
||||
pl.set_message(&data, sizeof(data));
|
||||
m_udp.tx(pl);
|
||||
});
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) {
|
||||
evse_bsp_cb_to_host msg;
|
||||
std::memcpy(&msg, data.buffer.data(), data.size());
|
||||
m_api.set_cb_message(msg);
|
||||
});
|
||||
|
||||
auto identifier = config.cb + "/" + config.item;
|
||||
m_udp.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "BSP/UDP", id) << msg << std::endl;
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
}
|
||||
|
||||
void bsp_bridge::handle_timer_event() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool bsp_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.register_event_handler(&m_api) && result;
|
||||
result = handler.register_event_handler(&m_udp) && result;
|
||||
result = handler.register_event_handler(&m_timer, [this](auto&) { handle_timer_event(); }) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool bsp_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.unregister_event_handler(&m_api) && result;
|
||||
result = handler.unregister_event_handler(&m_udp) && result;
|
||||
result = handler.unregister_event_handler(&m_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,162 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/can_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/netlink/vcan_netlink_manager.hpp>
|
||||
#include <memory>
|
||||
#include <protocol/cb_can_message.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
}
|
||||
|
||||
namespace charge_bridge {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
void msg_cb_to_host(cb_can_message const& src, everest::lib::io::can::socket_can::ClientPayloadT& tar) {
|
||||
tar.set_can_id_with_flags(src.can_id, src.can_flags & CanFlags_EFF, src.can_flags & CanFlags_RTR,
|
||||
src.can_flags & CanFlags_ERR);
|
||||
tar.len8_dlc = 0;
|
||||
tar.payload.resize(src.dlc);
|
||||
std::memcpy(tar.payload.data(), src.data, src.dlc);
|
||||
}
|
||||
|
||||
void msg_host_to_cb(everest::lib::io::can::socket_can::ClientPayloadT const& src, cb_can_message& tar) {
|
||||
tar = cb_can_message_set_zero;
|
||||
tar.can_id = src.get_can_id();
|
||||
tar.can_flags = 0;
|
||||
if (src.eff_flag()) {
|
||||
tar.can_flags |= CanFlags_EFF;
|
||||
}
|
||||
if (src.rtr_flag()) {
|
||||
tar.can_flags |= CanFlags_RTR;
|
||||
}
|
||||
if (src.err_flag()) {
|
||||
tar.can_flags |= CanFlags_ERR;
|
||||
}
|
||||
tar.dlc = std::min<uint8_t>(src.payload.size(), sizeof(tar.data));
|
||||
std::memcpy(tar.data, src.payload.data(), src.payload.size());
|
||||
}
|
||||
|
||||
bool is_data_msg([[maybe_unused]] cb_can_message const& msg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
can_bridge::can_bridge(can_bridge_config const& config) :
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
|
||||
m_can_device(config.can_device),
|
||||
m_last_msg_to_cb(std::chrono::steady_clock::time_point()) {
|
||||
|
||||
auto& manager = everest::lib::io::netlink::vcan_netlink_manager::Instance();
|
||||
auto success = manager.create(config.can_device) && manager.bring_up(config.can_device);
|
||||
if (success) {
|
||||
m_can = std::make_unique<everest::lib::io::can::socket_can>(config.can_device);
|
||||
} else {
|
||||
manager.destroy(config.can_device);
|
||||
success = manager.create(config.can_device) && manager.bring_up(config.can_device);
|
||||
if (success) {
|
||||
m_can = std::make_unique<everest::lib::io::can::socket_can>(config.can_device);
|
||||
} else {
|
||||
manager.destroy(config.can_device);
|
||||
throw std::runtime_error("Failed to setup virtual CAN device: " + config.can_device);
|
||||
}
|
||||
}
|
||||
|
||||
m_can->set_rx_handler([this](auto const& data, auto&) {
|
||||
everest::lib::io::udp::udp_client::ClientPayloadT pl;
|
||||
cb_can_message msg;
|
||||
msg_host_to_cb(data, msg);
|
||||
send_can_to_udp(msg);
|
||||
});
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) {
|
||||
everest::lib::io::can::socket_can::ClientPayloadT pl;
|
||||
cb_can_message msg;
|
||||
std::memcpy(&msg, data.buffer.data(), sizeof(cb_can_message));
|
||||
|
||||
msg_cb_to_host(msg, pl);
|
||||
if (is_data_msg(msg)) {
|
||||
m_can->tx(pl);
|
||||
}
|
||||
});
|
||||
|
||||
m_identifier = config.cb + "/" + config.item;
|
||||
m_can->set_error_handler([this](auto id, auto const& msg) {
|
||||
utilities::print_error(m_identifier, "CAN/HW", id) << msg << std::endl;
|
||||
if (id not_eq 0) {
|
||||
// This is a smart pointer!! Using .reset() would delete the obj!
|
||||
m_can->reset();
|
||||
}
|
||||
});
|
||||
|
||||
m_udp.set_error_handler([this](auto id, auto const& msg) {
|
||||
utilities::print_error(m_identifier, "CAN/UDP", id) << msg << std::endl;
|
||||
if (id not_eq 0) {
|
||||
m_udp.reset();
|
||||
}
|
||||
});
|
||||
|
||||
m_heartbeat_timer.set_timeout(10s);
|
||||
}
|
||||
|
||||
can_bridge::~can_bridge() {
|
||||
auto& manager = everest::lib::io::netlink::vcan_netlink_manager::Instance();
|
||||
if (m_can) {
|
||||
m_can.reset();
|
||||
manager.destroy(m_can_device);
|
||||
}
|
||||
}
|
||||
|
||||
bool can_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.register_event_handler(m_can.get());
|
||||
result = handler.register_event_handler(&m_udp) && result;
|
||||
result = handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); }) && result;
|
||||
|
||||
if (result) {
|
||||
handler.add_action([this]() { handle_heartbeat_timer(); });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool can_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.unregister_event_handler(m_can.get());
|
||||
result = handler.unregister_event_handler(&m_udp) && result;
|
||||
result = handler.unregister_event_handler(&m_heartbeat_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void can_bridge::send_can_to_udp(cb_can_message const& msg) {
|
||||
everest::lib::io::udp::udp_client::ClientPayloadT udp_pl;
|
||||
udp_pl.buffer.resize(sizeof(cb_can_message));
|
||||
std::memcpy(udp_pl.buffer.data(), &msg, sizeof(cb_can_message));
|
||||
m_udp.tx(udp_pl);
|
||||
m_last_msg_to_cb = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void can_bridge::handle_heartbeat_timer() {
|
||||
if (m_udp.on_error()) {
|
||||
// If the connection is not available, retry soon and invalidate last hearbeat
|
||||
m_heartbeat_timer.set_timeout(250ms);
|
||||
m_last_msg_to_cb = std::chrono::steady_clock::time_point();
|
||||
return;
|
||||
} else {
|
||||
// otherwise go back to regular interval
|
||||
m_heartbeat_timer.set_timeout(10s);
|
||||
}
|
||||
auto delta = std::chrono::steady_clock::now() - m_last_msg_to_cb;
|
||||
if (delta > 10s) {
|
||||
cb_can_message msg = cb_can_message_set_zero;
|
||||
msg.packet_type = CanPacketType_Keep_Alive;
|
||||
send_can_to_udp(msg);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,388 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "protocol/cb_config.h"
|
||||
#include <charge_bridge/charge_bridge.hpp>
|
||||
#include <charge_bridge/discovery.hpp>
|
||||
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
|
||||
#include <charge_bridge/gpio_bridge.hpp>
|
||||
#include <charge_bridge/heartbeat_service.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/print_config.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <charge_bridge/utilities/sync_udp_client.hpp>
|
||||
#include <everest/io/event/fd_event_sync_interface.hpp>
|
||||
#include <everest/io/netlink/vcan_netlink_manager.hpp>
|
||||
#include <everest/util/misc/bind.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
namespace {
|
||||
std::pair<bool, std::set<std::string>> make_interface_list(std::string const& str, std::string const& pattern) {
|
||||
if (str == pattern) {
|
||||
return {false, {}};
|
||||
};
|
||||
auto raw = utilities::string_after_pattern(str, pattern).substr(1);
|
||||
if (raw.size() <= 2) {
|
||||
return {false, {}};
|
||||
}
|
||||
auto exclude = raw.substr(0, 1) == "!";
|
||||
auto items = utilities::csv_to_set(raw.substr(exclude ? 1 : 0));
|
||||
for (auto const& elem : items) {
|
||||
std::cout << elem << ", ";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
return {exclude, items};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
charge_bridge::charge_bridge(charge_bridge_config const& config) : m_config(config) {
|
||||
if (utilities::string_starts_with(config.cb_remote, "ANY_EVSE")) {
|
||||
auto params = make_interface_list(config.cb_remote, "ANY_EVSE");
|
||||
init_discovery(discovery_device_type::CB_EVSE, params.second, params.first);
|
||||
} else if (utilities::string_starts_with(config.cb_remote, "ANY_EV")) {
|
||||
auto params = make_interface_list(config.cb_remote, "ANY_EV");
|
||||
init_discovery(discovery_device_type::CB_EV, params.second, params.first);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
void charge_bridge::init_discovery(discovery_device_type type, std::set<std::string> const& interfaces,
|
||||
bool excluding) {
|
||||
using namespace everest::lib::util;
|
||||
utilities::print_error(m_config.cb_name, "DISCOVERY", -1) << "Discovery pending" << std::endl;
|
||||
|
||||
m_discovery = std::make_unique<discovery>(type, interfaces, excluding);
|
||||
m_discovery->set_discovery_callback(bind_obj(&charge_bridge::handle_discovery, this));
|
||||
{
|
||||
auto handle = m_cb_status.handle();
|
||||
handle->discovery_pending = true;
|
||||
}
|
||||
m_cb_status.notify_one();
|
||||
}
|
||||
|
||||
void charge_bridge::handle_discovery(std::string const& ip) {
|
||||
utilities::print_error(m_config.cb_name, "DISCOVERY", 0) << "Discovered at: " + ip << std::endl;
|
||||
|
||||
m_config.cb_remote = ip;
|
||||
if (m_config.can0) {
|
||||
m_config.can0->cb_remote = ip;
|
||||
}
|
||||
if (m_config.serial1) {
|
||||
m_config.serial1->cb_remote = ip;
|
||||
}
|
||||
if (m_config.serial2) {
|
||||
m_config.serial2->cb_remote = ip;
|
||||
}
|
||||
if (m_config.serial3) {
|
||||
m_config.serial3->cb_remote = ip;
|
||||
}
|
||||
if (m_config.plc) {
|
||||
m_config.plc->cb_remote = ip;
|
||||
}
|
||||
if (m_config.bsp) {
|
||||
m_config.bsp->cb_remote = ip;
|
||||
}
|
||||
if (m_config.heartbeat) {
|
||||
m_config.heartbeat->cb_remote = ip;
|
||||
}
|
||||
if (m_config.gpio) {
|
||||
m_config.gpio->cb_remote = ip;
|
||||
}
|
||||
m_config.firmware.cb_remote = ip;
|
||||
|
||||
m_event_handler->add_action([this]() {
|
||||
std::unique_ptr<discovery> tmp;
|
||||
std::swap(m_discovery, tmp);
|
||||
|
||||
init();
|
||||
{
|
||||
auto handle = m_cb_status.handle();
|
||||
handle->discovery_pending = false;
|
||||
}
|
||||
m_cb_status.notify_one();
|
||||
});
|
||||
}
|
||||
|
||||
void charge_bridge::init() {
|
||||
if (m_config.can0.has_value()) {
|
||||
m_can_0_client = std::make_unique<can_bridge>(m_config.can0.value());
|
||||
}
|
||||
if (m_config.serial1.has_value()) {
|
||||
m_pty_1 = std::make_unique<serial_bridge>(m_config.serial1.value());
|
||||
}
|
||||
if (m_config.serial2.has_value()) {
|
||||
m_pty_2 = std::make_unique<serial_bridge>(m_config.serial2.value());
|
||||
}
|
||||
if (m_config.serial3.has_value()) {
|
||||
m_pty_3 = std::make_unique<serial_bridge>(m_config.serial3.value());
|
||||
}
|
||||
if (m_config.plc.has_value()) {
|
||||
m_plc = std::make_unique<plc_bridge>(m_config.plc.value());
|
||||
}
|
||||
if (m_config.bsp.has_value()) {
|
||||
m_bsp = std::make_unique<bsp_bridge>(m_config.bsp.value());
|
||||
}
|
||||
if (m_config.heartbeat.has_value()) {
|
||||
m_heartbeat = std::make_unique<heartbeat_service>(m_config.heartbeat.value(), [this](bool connected) {
|
||||
{
|
||||
auto handle = m_cb_status.handle();
|
||||
handle->is_connected = connected;
|
||||
}
|
||||
m_cb_status.notify_one();
|
||||
});
|
||||
}
|
||||
if (m_config.gpio.has_value()) {
|
||||
m_gpio = std::make_unique<gpio_bridge>(m_config.gpio.value());
|
||||
}
|
||||
}
|
||||
|
||||
charge_bridge::~charge_bridge() {
|
||||
m_cb_status.notify_one();
|
||||
}
|
||||
|
||||
void charge_bridge::manage(everest::lib::io::event::fd_event_handler& handler, std::atomic_bool const& run,
|
||||
bool force_update) {
|
||||
using namespace std::chrono_literals;
|
||||
m_event_handler = &handler;
|
||||
m_force_firmware_update = force_update;
|
||||
|
||||
auto action = [this](bool is_connected, bool discovery_pending, int& error_count) {
|
||||
if (discovery_pending) {
|
||||
if (m_discovery_active) {
|
||||
return;
|
||||
}
|
||||
m_discovery_active = true;
|
||||
m_event_handler->add_action([this]() { register_events(*m_event_handler); });
|
||||
return;
|
||||
}
|
||||
if (m_was_connected and not is_connected) {
|
||||
if (error_count > 1) {
|
||||
m_event_handler->add_action([this]() { unregister_events(*m_event_handler); });
|
||||
m_was_connected = false;
|
||||
} else {
|
||||
error_count++;
|
||||
}
|
||||
}
|
||||
if (not m_was_connected) {
|
||||
if (update_firmware(m_force_firmware_update)) {
|
||||
m_event_handler->add_action([this]() { register_events(*m_event_handler); });
|
||||
m_was_connected = true;
|
||||
error_count = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::thread manager([&run, action, this]() {
|
||||
auto handle = m_cb_status.handle();
|
||||
bool last_is_connected = handle->is_connected;
|
||||
bool last_discovery_pending = handle->discovery_pending;
|
||||
int error_count = 0;
|
||||
auto condition = [&] {
|
||||
if (handle->is_connected not_eq last_is_connected) {
|
||||
return true;
|
||||
}
|
||||
if (handle->discovery_pending not_eq last_discovery_pending) {
|
||||
return true;
|
||||
}
|
||||
if (not run.load()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (run.load()) {
|
||||
action(handle->is_connected, handle->discovery_pending, error_count);
|
||||
handle.wait_for(condition, 10s);
|
||||
last_is_connected = handle->is_connected;
|
||||
last_discovery_pending = handle->discovery_pending;
|
||||
}
|
||||
});
|
||||
manager.detach();
|
||||
}
|
||||
|
||||
bool charge_bridge::update_firmware(bool force) {
|
||||
firmware_update::sync_fw_updater updater(m_config.firmware);
|
||||
auto is_connected = updater.quick_check_connection();
|
||||
if (not is_connected) {
|
||||
return false;
|
||||
}
|
||||
updater.print_fw_version();
|
||||
|
||||
auto do_update = force or (m_config.firmware.fw_update_on_start and not updater.check_if_correct_fw_installed());
|
||||
|
||||
if (not do_update) {
|
||||
return true;
|
||||
}
|
||||
auto result = updater.upload_fw() && updater.check_connection();
|
||||
if (not result) {
|
||||
std::cout << "Error: could not install correct firmware version" << std::endl;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string charge_bridge::get_pty_1_slave_path() {
|
||||
if (m_pty_1) {
|
||||
return m_pty_1->get_slave_path();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string charge_bridge::get_pty_2_slave_path() {
|
||||
if (m_pty_2) {
|
||||
return m_pty_2->get_slave_path();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string charge_bridge::get_pty_3_slave_path() {
|
||||
if (m_pty_3) {
|
||||
return m_pty_3->get_slave_path();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool charge_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_can_0_client) {
|
||||
result = handler.register_event_handler(m_can_0_client.get()) && result;
|
||||
}
|
||||
if (m_pty_1) {
|
||||
result = handler.register_event_handler(m_pty_1.get()) && result;
|
||||
}
|
||||
if (m_pty_2) {
|
||||
result = handler.register_event_handler(m_pty_2.get()) && result;
|
||||
}
|
||||
if (m_pty_3) {
|
||||
result = handler.register_event_handler(m_pty_3.get()) && result;
|
||||
}
|
||||
if (m_bsp) {
|
||||
result = handler.register_event_handler(m_bsp.get()) && result;
|
||||
}
|
||||
if (m_plc) {
|
||||
result = handler.register_event_handler(m_plc.get()) && result;
|
||||
}
|
||||
if (m_heartbeat) {
|
||||
result = handler.register_event_handler(m_heartbeat.get()) && result;
|
||||
}
|
||||
if (m_gpio) {
|
||||
result = handler.register_event_handler(m_gpio.get()) && result;
|
||||
}
|
||||
if (m_discovery) {
|
||||
result = handler.register_event_handler(m_discovery.get()) && result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
bool charge_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_can_0_client) {
|
||||
result = handler.unregister_event_handler(m_can_0_client.get()) && result;
|
||||
}
|
||||
if (m_pty_1) {
|
||||
result = handler.unregister_event_handler(m_pty_1.get()) && result;
|
||||
}
|
||||
if (m_pty_2) {
|
||||
result = handler.unregister_event_handler(m_pty_2.get()) && result;
|
||||
}
|
||||
if (m_pty_3) {
|
||||
result = handler.unregister_event_handler(m_pty_3.get()) && result;
|
||||
}
|
||||
if (m_bsp) {
|
||||
result = handler.unregister_event_handler(m_bsp.get()) && result;
|
||||
}
|
||||
if (m_plc) {
|
||||
result = handler.unregister_event_handler(m_plc.get()) && result;
|
||||
}
|
||||
if (m_heartbeat) {
|
||||
result = handler.unregister_event_handler(m_heartbeat.get()) && result;
|
||||
}
|
||||
if (m_gpio) {
|
||||
result = handler.unregister_event_handler(m_gpio.get()) && result;
|
||||
}
|
||||
if (m_discovery) {
|
||||
result = handler.unregister_event_handler(m_discovery.get()) && result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void charge_bridge::print_config() {
|
||||
print_charge_bridge_config(m_config);
|
||||
}
|
||||
|
||||
void print_charge_bridge_config(charge_bridge_config const& c) {
|
||||
using namespace utilities;
|
||||
std::cout << "ChargeBridge: " << c.cb_name << std::endl;
|
||||
std::cout << " * remote: " << c.cb_remote << std::endl;
|
||||
if (c.serial1) {
|
||||
std::cout << " * serial 1: " << c.serial1->serial_device;
|
||||
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 1) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[0]);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.serial2) {
|
||||
std::cout << " * serial 2: " << c.serial2->serial_device;
|
||||
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 2) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[1]);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.serial3) {
|
||||
std::cout << " * serial 3: " << c.serial3->serial_device;
|
||||
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 3) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[2]);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.can0) {
|
||||
std::cout << " * can 0: " << c.can0->can_device;
|
||||
if (c.heartbeat.has_value()) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.can.baudrate) << "bps" << std::endl;
|
||||
}
|
||||
}
|
||||
if (c.plc) {
|
||||
std::cout << " * plc: " << c.plc->plc_tap << std::flush;
|
||||
std::cout << " " << c.cb_remote << ":" << c.plc->cb_port;
|
||||
std::cout << " adress " << c.plc->plc_ip;
|
||||
std::cout << " netmask " << c.plc->plc_netmaks;
|
||||
std::cout << " MTU " << c.plc->plc_mtu << std::endl;
|
||||
}
|
||||
if (c.bsp) {
|
||||
if (c.bsp->api.evse.enabled) {
|
||||
std::cout << " * evse_bsp: ";
|
||||
} else if (c.bsp->api.ev.enabled) {
|
||||
std::cout << " * ev_bsp: ";
|
||||
}
|
||||
std::cout << c.bsp->cb_remote << ":" << c.bsp->cb_port;
|
||||
std::cout << " module " << c.bsp->api.evse.module_id;
|
||||
std::cout << " MQTT " << c.bsp->api.mqtt_remote << ":" << c.bsp->api.mqtt_port;
|
||||
if (not c.bsp->api.mqtt_bind.empty()) {
|
||||
std::cout << " on " << c.bsp->api.mqtt_bind;
|
||||
}
|
||||
std::cout << " ping " << c.bsp->api.mqtt_ping_interval_ms << "ms";
|
||||
if (c.bsp->api.ovm.enabled) {
|
||||
std::cout << " OVM module " << c.bsp->api.ovm.module_id;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.heartbeat) {
|
||||
std::cout << " * heartbeat: " << c.cb_remote << ":" << c.cb_port;
|
||||
std::cout << " heartbeat interval " << c.heartbeat->interval_s << "s" << std::endl;
|
||||
}
|
||||
if (c.gpio) {
|
||||
std::cout << " * gpio: " << c.cb_remote << ":" << c.cb_port;
|
||||
std::cout << " MQTT " << c.gpio->mqtt_remote << ":" << c.gpio->mqtt_port;
|
||||
if (not c.gpio->mqtt_bind.empty()) {
|
||||
std::cout << " on " << c.gpio->mqtt_bind;
|
||||
}
|
||||
std::cout << " send interval " << c.gpio->interval_s << "s" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "\n" << std::endl;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,122 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "charge_bridge/utilities/string.hpp"
|
||||
#include <charge_bridge/discovery.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <type_traits>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
namespace {
|
||||
std::string to_string(discovery_device_type val) {
|
||||
switch (val) {
|
||||
case discovery_device_type::CB_EV:
|
||||
return "CB-CCS-EV-LU";
|
||||
|
||||
case discovery_device_type::CB_EVSE:
|
||||
return "CB-CCS-EVSE-LU";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
|
||||
bool is_cb_match(std::string const& board_type, discovery_device_type discriminator) {
|
||||
auto result = board_type == to_string(discriminator);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::string discovery::discovery_id = "_chargebridge._udp.local";
|
||||
|
||||
discovery::discovery(discovery_device_type type) : m_type(type) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(1s);
|
||||
|
||||
for (auto const& item : everest::lib::io::socket::get_all_interaces()) {
|
||||
add_client(item.name);
|
||||
}
|
||||
}
|
||||
|
||||
discovery::discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding) :
|
||||
m_type(type) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(1s);
|
||||
|
||||
for (auto const& item : everest::lib::io::socket::get_all_interaces()) {
|
||||
if (not interfaces.empty()) {
|
||||
if (interfaces.count(item.name) == 1 and excluding) {
|
||||
continue;
|
||||
}
|
||||
if (interfaces.count(item.name) == 0 and not excluding) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
std::cout << " using interface: " << item.name << std::endl;
|
||||
add_client(item.name);
|
||||
}
|
||||
}
|
||||
|
||||
void discovery::add_client(std::string const& interface) {
|
||||
auto client = std::make_unique<everest::lib::io::mdns::mdns_client>(interface);
|
||||
client->set_rx_handler([&](auto const& data, auto&) {
|
||||
auto discovery = everest::lib::io::mdns::parse_mdns_packet(data.buffer);
|
||||
if (discovery.has_value()) {
|
||||
if (m_registry.update(discovery.value())) {
|
||||
query_registry();
|
||||
}
|
||||
}
|
||||
});
|
||||
m_mdns.push_back(std::move(client));
|
||||
}
|
||||
|
||||
void discovery::query_registry() {
|
||||
auto obj = m_registry.get();
|
||||
for (auto const& [key, value] : obj) {
|
||||
if (not utilities::string_ends_with(key, discovery_id)) {
|
||||
continue;
|
||||
}
|
||||
if (not value.txt.count("board_type") or not is_cb_match(value.txt.at("board_type"), m_type)) {
|
||||
continue;
|
||||
}
|
||||
if (not m_on_discover) {
|
||||
continue;
|
||||
}
|
||||
m_on_discover(value.ip);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void discovery::set_discovery_callback(discovery_cb const& cb) {
|
||||
m_on_discover = cb;
|
||||
}
|
||||
|
||||
bool discovery::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
for (auto& item : m_mdns) {
|
||||
if (item) {
|
||||
result = handler.register_event_handler(item.get()) && result;
|
||||
}
|
||||
}
|
||||
handler.register_event_handler(&m_timer, [&](auto) {
|
||||
for (auto& item : m_mdns) {
|
||||
item->get_raw_handler()->query(discovery_id);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool discovery::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
for (auto& item : m_mdns) {
|
||||
if (item) {
|
||||
result = handler.unregister_event_handler(item.get()) && result;
|
||||
}
|
||||
}
|
||||
handler.unregister_event_handler(&m_timer);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,214 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "charge_bridge/utilities/string.hpp"
|
||||
#include "everest/io/mqtt/mosquitto_cpp.hpp"
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/api_connector.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
namespace mqtt = everest::lib::io::mqtt;
|
||||
|
||||
namespace {
|
||||
const int mqtt_reconnect_to_ms = 1000;
|
||||
}
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
api_connector::api_connector(everest_api_config const& config, std::string const& cb_identifier) :
|
||||
m_cb_identifier(cb_identifier),
|
||||
m_mqtt(mqtt_reconnect_to_ms),
|
||||
m_evse_bsp(config.evse, cb_identifier, m_host_status),
|
||||
m_ovm(config.ovm, cb_identifier, m_host_status),
|
||||
m_ev_bsp(config.ev, cb_identifier, m_host_status) {
|
||||
|
||||
everest::lib::API::Topics api_topics;
|
||||
m_evse_bsp_enabled = config.evse.enabled;
|
||||
m_ovm_enabled = config.ovm.enabled;
|
||||
m_ev_bsp_enabled = config.ev.enabled;
|
||||
|
||||
if (m_evse_bsp_enabled && m_ev_bsp_enabled) {
|
||||
throw std::runtime_error("Configuration error: Cannot enable EV and EVSE BSP at the same time");
|
||||
}
|
||||
utilities::print_error(m_cb_identifier, "BSP/CB", 0) << "ChargeBridge connected." << std::endl;
|
||||
|
||||
if (m_evse_bsp_enabled) {
|
||||
api_topics.setup(config.evse.module_id, "evse_board_support", 1);
|
||||
m_evse_bsp_receive_topic = api_topics.everest_to_extern("");
|
||||
m_evse_bsp_send_topic = api_topics.extern_to_everest("");
|
||||
m_evse_bsp.set_mqtt_tx(
|
||||
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_evse_bsp_send_topic + topic, payload); });
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
api_topics.setup(config.ovm.module_id, "over_voltage_monitor", 1);
|
||||
m_ovm_receive_topic = api_topics.everest_to_extern("");
|
||||
m_ovm_send_topic = api_topics.extern_to_everest("");
|
||||
m_ovm.set_mqtt_tx(
|
||||
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_ovm_send_topic + topic, payload); });
|
||||
}
|
||||
|
||||
if (m_ev_bsp_enabled) {
|
||||
api_topics.setup(config.ev.module_id, "ev_board_support", 1);
|
||||
m_ev_bsp_receive_topic = api_topics.everest_to_extern("");
|
||||
m_ev_bsp_send_topic = api_topics.extern_to_everest("");
|
||||
m_ev_bsp.set_mqtt_tx(
|
||||
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_ev_bsp_send_topic + topic, payload); });
|
||||
}
|
||||
|
||||
m_mqtt.set_error_handler([this](int code, std::string const& msg) {
|
||||
auto is_error = code == 0 ? 0 : 1;
|
||||
utilities::print_error(m_cb_identifier, "BSP/MQTT", is_error) << msg << std::endl;
|
||||
});
|
||||
|
||||
m_mqtt.set_callback_connect([this, config](auto&, auto, auto, auto const&) { handle_mqtt_connect(); });
|
||||
|
||||
m_mqtt.connect(config.mqtt_bind, config.mqtt_remote, config.mqtt_port, config.mqtt_ping_interval_ms);
|
||||
|
||||
m_sync_timer.set_timeout(1s);
|
||||
|
||||
std::memset(&m_host_status, 0, sizeof(m_host_status));
|
||||
}
|
||||
|
||||
bool api_connector::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_evse_bsp_enabled) {
|
||||
result = handler.register_event_handler(&m_evse_bsp) && result;
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
result = handler.register_event_handler(&m_ovm) && result;
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
result = handler.register_event_handler(&m_ev_bsp) && result;
|
||||
}
|
||||
result = handler.register_event_handler(&m_mqtt) && result;
|
||||
result = handler.register_event_handler(&m_sync_timer, [this](auto&) {
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.sync(m_cb_connected);
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.sync(m_cb_connected);
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.sync(m_cb_connected);
|
||||
}
|
||||
handle_cb_connection_state();
|
||||
}) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool api_connector::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_evse_bsp_enabled) {
|
||||
result = handler.unregister_event_handler(&m_evse_bsp) && result;
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
result = handler.unregister_event_handler(&m_ovm) && result;
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
result = handler.unregister_event_handler(&m_ev_bsp) && result;
|
||||
}
|
||||
result = handler.unregister_event_handler(&m_mqtt) && result;
|
||||
result = handler.unregister_event_handler(&m_sync_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void api_connector::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
m_evse_bsp.set_cb_tx(handler);
|
||||
m_ev_bsp.set_cb_tx(handler);
|
||||
}
|
||||
|
||||
void api_connector::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
m_last_cb_heartbeat = std::chrono::steady_clock::now();
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.set_cb_message(msg);
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.set_cb_message(msg);
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.set_cb_message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
bool api_connector::check_cb_heartbeat() {
|
||||
if (m_last_cb_heartbeat == std::chrono::steady_clock::time_point::max()) {
|
||||
return false;
|
||||
}
|
||||
return std::chrono::steady_clock::now() - m_last_cb_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void api_connector::handle_mqtt_connect() {
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_mqtt.subscribe(m_evse_bsp_receive_topic + "#",
|
||||
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
|
||||
auto operation = utilities::string_after_pattern(topic, m_evse_bsp_receive_topic);
|
||||
if (not operation.empty()) {
|
||||
m_evse_bsp.dispatch(operation, static_cast<std::string>(payload));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_mqtt.subscribe(m_ovm_receive_topic + "#",
|
||||
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
|
||||
auto operation = utilities::string_after_pattern(topic, m_ovm_receive_topic);
|
||||
if (not operation.empty()) {
|
||||
m_ovm.dispatch(operation, static_cast<std::string>(payload));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_mqtt.subscribe(m_ev_bsp_receive_topic + "#",
|
||||
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
|
||||
auto operation = utilities::string_after_pattern(topic, m_ev_bsp_receive_topic);
|
||||
if (not operation.empty()) {
|
||||
m_ev_bsp.dispatch(operation, static_cast<std::string>(payload));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void api_connector::handle_cb_connection_state() {
|
||||
m_tx(m_host_status);
|
||||
auto current = check_cb_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "BSP/CB", 0) << "ChargeBridge connected." << std::endl;
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.clear_comm_fault();
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.clear_comm_fault();
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.clear_comm_fault();
|
||||
}
|
||||
|
||||
} else {
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.raise_comm_fault();
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.raise_comm_fault();
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.raise_comm_fault();
|
||||
}
|
||||
|
||||
utilities::print_error(m_cb_identifier, "BSP/CB", 1) << "Waiting for ChargeBridge...." << std::endl;
|
||||
}
|
||||
};
|
||||
if (m_cb_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_cb_initial_comm_check = false;
|
||||
}
|
||||
if (m_cb_connected != current) {
|
||||
handle_status(not m_cb_connected);
|
||||
}
|
||||
m_cb_connected = current;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,418 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "protocol/cb_common.h"
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/ev_bsp_api.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest_api_types/ev_board_support/codec.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::API::V1_0::types::generic;
|
||||
using namespace everest::lib::API;
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
ev_bsp_api::ev_bsp_api([[maybe_unused]] evse_ev_bsp_config const& config, std::string const& cb_identifier,
|
||||
evse_bsp_host_to_cb& host_status) :
|
||||
host_status(host_status), m_cb_identifier(cb_identifier) {
|
||||
|
||||
last_everest_heartbeat = std::chrono::steady_clock::time_point();
|
||||
}
|
||||
|
||||
void ev_bsp_api::sync(bool cb_connected) {
|
||||
m_cb_connected = cb_connected;
|
||||
handle_everest_connection_state();
|
||||
}
|
||||
|
||||
bool ev_bsp_api::register_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ev_bsp_api::unregister_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ev_bsp_api::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
}
|
||||
|
||||
void ev_bsp_api::tx(evse_bsp_host_to_cb const& msg) {
|
||||
if (m_tx) {
|
||||
m_tx(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::set_mqtt_tx(mqtt_ftor const& handler) {
|
||||
m_mqtt_tx = handler;
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_bsp_event(API_EVSE_BSP::Event data) {
|
||||
API_EVSE_BSP::BspEvent event{data};
|
||||
send_mqtt("bsp_event", serialize(event));
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_bsp_measurement(API_EV_BSP::BspMeasurement data) {
|
||||
API_EV_BSP::BspMeasurement measurement{data};
|
||||
send_mqtt("bsp_measurement", serialize(measurement));
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_event_relay(std::uint8_t relay) {
|
||||
using bc_event = API_EVSE_BSP::Event;
|
||||
bc_event relaise_event;
|
||||
bool relaise_state_valid = true;
|
||||
switch (relay) {
|
||||
case RelaiseState::RelayState_Open:
|
||||
relaise_event = bc_event::PowerOff;
|
||||
break;
|
||||
case RelaiseState::RelayState_Closed:
|
||||
relaise_event = bc_event::PowerOn;
|
||||
break;
|
||||
default:
|
||||
relaise_state_valid = false;
|
||||
}
|
||||
if (relaise_state_valid) {
|
||||
send_bsp_event(relaise_event);
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_event_cp(std::uint8_t cp) {
|
||||
using bc_event = API_EVSE_BSP::Event;
|
||||
bc_event cp_event;
|
||||
bool cp_state_valid = true;
|
||||
switch (cp) {
|
||||
case CpState_A:
|
||||
cp_event = bc_event::A;
|
||||
break;
|
||||
case CpState_B:
|
||||
cp_event = bc_event::B;
|
||||
break;
|
||||
case CpState_C:
|
||||
cp_event = bc_event::C;
|
||||
break;
|
||||
case CpState_D:
|
||||
cp_event = bc_event::D;
|
||||
break;
|
||||
case CpState_E:
|
||||
cp_event = bc_event::Disconnected;
|
||||
break;
|
||||
case CpState_F:
|
||||
cp_event = bc_event::F;
|
||||
break;
|
||||
case CpState_DF:
|
||||
cp_event = bc_event::E;
|
||||
break;
|
||||
case CpState::CpState_INVALID:
|
||||
cp_event = bc_event::E;
|
||||
break;
|
||||
default:
|
||||
cp_state_valid = false;
|
||||
}
|
||||
if (cp_state_valid) {
|
||||
last_cp_event = cp_event;
|
||||
send_bsp_event(cp_event);
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_bsp_measurement(uint16_t cp, [[maybe_unused]] uint8_t pp_1, [[maybe_unused]] uint8_t pp2) {
|
||||
// FIXME implement PP correctly
|
||||
API_EV_BSP::BspMeasurement data;
|
||||
data.cp_pwm_duty_cycle = cp / 65536. * 100.;
|
||||
API_EVSE_BSP::ProximityPilot pp;
|
||||
API_EVSE_BSP::Ampacity amp;
|
||||
amp = API_EVSE_BSP::Ampacity::None;
|
||||
pp.ampacity = amp;
|
||||
data.proximity_pilot = pp;
|
||||
send_bsp_measurement(data);
|
||||
}
|
||||
|
||||
inline static bool operator==(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw == b.raw;
|
||||
}
|
||||
inline static bool operator!=(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw != b.raw;
|
||||
}
|
||||
|
||||
void ev_bsp_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
|
||||
if (m_cb_status.cp_state not_eq msg.cp_state) {
|
||||
handle_event_cp(msg.cp_state);
|
||||
}
|
||||
|
||||
if (m_cb_status.relay_state != msg.relay_state) {
|
||||
handle_event_relay(msg.relay_state);
|
||||
}
|
||||
|
||||
if (m_cb_status.cp_duty_cycle not_eq msg.cp_duty_cycle or m_cb_status.pp_state_type1 not_eq msg.pp_state_type1 or
|
||||
m_cb_status.pp_state_type2 not_eq msg.pp_state_type2) {
|
||||
handle_bsp_measurement(msg.cp_duty_cycle, m_cb_status.pp_state_type1, m_cb_status.pp_state_type2);
|
||||
}
|
||||
|
||||
if (m_cb_status.error_flags not_eq msg.error_flags) {
|
||||
handle_error(msg.error_flags);
|
||||
}
|
||||
|
||||
// This is not supported in EVerest yet but should be added at some point
|
||||
/*
|
||||
if (cb_status.stop_charging not_eq msg.stop_charging) {
|
||||
handle_stop_button(msg.stop_charging);
|
||||
}*/
|
||||
|
||||
// The ev_board_support interface in EVerest does not yet have proper errors defined, so we do not handle errors
|
||||
// here yet
|
||||
/*
|
||||
if (m_cb_status.error_flags not_eq msg.error_flags) {
|
||||
handle_error(msg.error_flags);
|
||||
}*/
|
||||
|
||||
m_cb_status = msg;
|
||||
}
|
||||
|
||||
enum class SafetyErrorMask : std::uint32_t {
|
||||
cp_not_state_c = (1 << 0),
|
||||
pwm_not_enabled = (1 << 1),
|
||||
pp_invalid = (1 << 2),
|
||||
plug_temperature_too_high = (1 << 3),
|
||||
internal_temperature_too_high = (1 << 4),
|
||||
emergency_input_latched = (1 << 5),
|
||||
relay_health_latched = (1 << 6),
|
||||
vdd_3v3_out_of_range = (1 << 7),
|
||||
vdd_core_out_of_range = (1 << 8),
|
||||
vdd_12V_out_of_range = (1 << 9),
|
||||
vdd_N12V_out_of_range = (1 << 10),
|
||||
vdd_refint_out_of_range = (1 << 11),
|
||||
external_allow_power_on = (1 << 12),
|
||||
config_mem_error = (1 << 13),
|
||||
dc_hv_ov = (1 << 14),
|
||||
};
|
||||
|
||||
// Table that maps a mask to our API error + message
|
||||
struct FlagSpec {
|
||||
SafetyErrorMask mask;
|
||||
const char* message;
|
||||
};
|
||||
|
||||
static constexpr FlagSpec error_specs[] = {
|
||||
{SafetyErrorMask::pp_invalid, "PP invalid"},
|
||||
{SafetyErrorMask::plug_temperature_too_high, "Plug temperature too high"},
|
||||
{SafetyErrorMask::internal_temperature_too_high, "ChargeBridge internal over temperature"},
|
||||
{SafetyErrorMask::emergency_input_latched, "Emergency input latched"},
|
||||
{SafetyErrorMask::relay_health_latched, "Relay welded error"},
|
||||
{SafetyErrorMask::vdd_3v3_out_of_range, "Supply voltage 3.3V out of range"},
|
||||
{SafetyErrorMask::vdd_core_out_of_range, "Internal supply core voltage out of range"},
|
||||
{SafetyErrorMask::vdd_12V_out_of_range, "Internal supply 12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_N12V_out_of_range, "Internal supply -12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_refint_out_of_range, "Internal supply VREF voltage out of range"},
|
||||
{SafetyErrorMask::config_mem_error, "Internal config memory error"},
|
||||
{SafetyErrorMask::dc_hv_ov, "DC HV OVM. FIXME: This should be on OVM not EVSE interface"},
|
||||
};
|
||||
|
||||
static constexpr FlagSpec print_warning_specs[] = {
|
||||
{SafetyErrorMask::cp_not_state_c, "CP is not state C"},
|
||||
{SafetyErrorMask::pwm_not_enabled, "PWM not enabled"},
|
||||
{SafetyErrorMask::external_allow_power_on, "Allow power on from EVerest missing"},
|
||||
};
|
||||
|
||||
void ev_bsp_api::handle_error(const SafetyErrorFlags& data) {
|
||||
std::uint32_t next = data.raw; // current raw value
|
||||
std::stringstream log;
|
||||
|
||||
for (const auto& s : print_warning_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& s : error_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
if (m_everest_connected && m_cb_connected) {
|
||||
if (log.str().empty()) {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "Relays can be switched on." << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "Relays off due to:" << log.str() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::dispatch(std::string const& operation, std::string const& payload) {
|
||||
if (operation == "enable") {
|
||||
receive_enable(payload);
|
||||
} else if (operation == "set_cp_state") {
|
||||
receive_set_cp_state(payload);
|
||||
} else if (operation == "allow_power_on") {
|
||||
receive_allow_power_on(payload);
|
||||
} else if (operation == "diode_fail") {
|
||||
receive_diode_fail(payload);
|
||||
} else if (operation == "set_ac_max_current") {
|
||||
receive_set_ac_max_current(payload);
|
||||
} else if (operation == "set_three_phases") {
|
||||
receive_set_three_phases(payload);
|
||||
} else if (operation == "set_rcd_error") {
|
||||
receive_set_rcd_error(payload);
|
||||
} else if (operation == "heartbeat") {
|
||||
receive_heartbeat(payload);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api: RECEIVE invalid operation: " << operation << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::raise_comm_fault() {
|
||||
send_raise_error(API_GENERIC::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
|
||||
}
|
||||
|
||||
void ev_bsp_api::clear_comm_fault() {
|
||||
send_clear_error(API_GENERIC::ErrorEnum::CommunicationFault, "ChargeBridge not available");
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_enable([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
static CpState evcpstate_to_cpstate(API_EV_BSP::EvCpState s) {
|
||||
switch (s) {
|
||||
case API_EV_BSP::EvCpState::A:
|
||||
return CpState::CpState_A;
|
||||
case API_EV_BSP::EvCpState::B:
|
||||
return CpState::CpState_B;
|
||||
case API_EV_BSP::EvCpState::C:
|
||||
return CpState::CpState_C;
|
||||
case API_EV_BSP::EvCpState::D:
|
||||
return CpState::CpState_D;
|
||||
case API_EV_BSP::EvCpState::E:
|
||||
return CpState::CpState_E;
|
||||
default:
|
||||
return CpState::CpState_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_cp_state(std::string const& payload) {
|
||||
API_EV_BSP::EvCpState cp;
|
||||
|
||||
if (everest::lib::API::deserialize(payload, cp)) {
|
||||
host_status.ev_set_cp_state = evcpstate_to_cpstate(cp);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api::receive_set_cp_state: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_allow_power_on(std::string const& payload) {
|
||||
bool on;
|
||||
|
||||
if (everest::lib::API::deserialize(payload, on)) {
|
||||
host_status.allow_power_on = static_cast<std::uint8_t>(on);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api::receive_allow_power_on: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_diode_fail(std::string const& payload) {
|
||||
bool on;
|
||||
|
||||
if (everest::lib::API::deserialize(payload, on)) {
|
||||
host_status.ev_set_diodefault = static_cast<std::uint8_t>(on);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api::receive_diode_fail: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_ac_max_current([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_three_phases([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_rcd_error([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_heartbeat(std::string const& pl) {
|
||||
last_everest_heartbeat = std::chrono::steady_clock::now();
|
||||
std::size_t id = 0;
|
||||
if (deserialize(pl, id)) {
|
||||
auto delta = id - m_last_hb_id;
|
||||
if (delta > 1) {
|
||||
utilities::print_error(m_cb_identifier, "EV_BSP/EVEREST", -1)
|
||||
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
|
||||
}
|
||||
m_last_hb_id = id;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EV_BSP/EVEREST", -1)
|
||||
<< "EVerest invalid heartbeat message: " << pl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_communication_check() {
|
||||
send_mqtt("communication_check", serialize(true));
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
if (m_mqtt_tx) {
|
||||
m_mqtt_tx(topic, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool ev_bsp_api::check_everest_heartbeat() {
|
||||
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_everest_connection_state() {
|
||||
send_communication_check();
|
||||
auto current = check_everest_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "EVerest connected" << std::endl;
|
||||
// re-send last CP state event
|
||||
send_bsp_event(last_cp_event);
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 1) << "Waiting for EVerest..." << std::endl;
|
||||
// unplug CP if EVerest disconnects
|
||||
host_status.ev_set_cp_state = CpState_A;
|
||||
tx(host_status);
|
||||
}
|
||||
};
|
||||
|
||||
if (m_bc_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_bc_initial_comm_check = false;
|
||||
} else if (m_everest_connected != current) {
|
||||
handle_status(not m_everest_connected);
|
||||
}
|
||||
m_everest_connected = current;
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_raise_error(API_GENERIC::ErrorEnum error, std::string const& subtype, std::string const& msg) {
|
||||
API_GENERIC::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
send_mqtt("raise_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_clear_error(API_GENERIC::ErrorEnum error, std::string const& subtype) {
|
||||
API_GENERIC::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
send_mqtt("clear_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,514 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/evse_bsp_api.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <everest_api_types/evse_manager/codec.hpp>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::API::V1_0::types::generic;
|
||||
using namespace everest::lib::API;
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
evse_bsp_api::evse_bsp_api(evse_bsp_config const& config, std::string const& cb_identifier,
|
||||
evse_bsp_host_to_cb& host_status) :
|
||||
host_status(host_status), m_capabilities(config.capabilities), m_cb_identifier(cb_identifier) {
|
||||
|
||||
last_everest_heartbeat = std::chrono::steady_clock::time_point();
|
||||
|
||||
m_capabilities_timer.set_timeout(10s);
|
||||
|
||||
std::memset(&cb_status, 0, sizeof(cb_status));
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
|
||||
void evse_bsp_api::sync(bool cb_connected) {
|
||||
m_cb_connected = cb_connected;
|
||||
handle_everest_connection_state();
|
||||
}
|
||||
|
||||
bool evse_bsp_api::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.register_event_handler(&m_capabilities_timer, [this](auto&) {
|
||||
send_capabilities();
|
||||
});
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
bool evse_bsp_api::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.unregister_event_handler(&m_capabilities_timer);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
void evse_bsp_api::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
}
|
||||
|
||||
void evse_bsp_api::tx(evse_bsp_host_to_cb const& msg) {
|
||||
if (m_tx) {
|
||||
m_tx(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::set_mqtt_tx(mqtt_ftor const& handler) {
|
||||
m_mqtt_tx = handler;
|
||||
}
|
||||
|
||||
inline static bool operator==(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw == b.raw;
|
||||
}
|
||||
inline static bool operator!=(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw != b.raw;
|
||||
}
|
||||
|
||||
void evse_bsp_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
if (cb_status.reset_reason not_eq msg.reset_reason) {
|
||||
}
|
||||
if (cb_status.cp_state not_eq msg.cp_state) {
|
||||
handle_event_cp(msg.cp_state);
|
||||
}
|
||||
if (cb_status.relay_state != msg.relay_state) {
|
||||
handle_event_relay(msg.relay_state);
|
||||
}
|
||||
if (cb_status.error_flags not_eq msg.error_flags) {
|
||||
handle_error(msg.error_flags);
|
||||
}
|
||||
if (cb_status.pp_state_type1 not_eq msg.pp_state_type1) {
|
||||
handle_pp_type1(msg.pp_state_type1);
|
||||
}
|
||||
if (cb_status.pp_state_type2 not_eq msg.pp_state_type2) {
|
||||
handle_pp_type2(msg.pp_state_type2);
|
||||
}
|
||||
if (cb_status.stop_charging not_eq msg.stop_charging) {
|
||||
handle_stop_button(msg.stop_charging);
|
||||
}
|
||||
// cb_status.lock_state is not checked here as it cannot be reported to EVerest.
|
||||
|
||||
cb_status = msg;
|
||||
}
|
||||
|
||||
void evse_bsp_api::dispatch(std::string const& operation, std::string const& payload) {
|
||||
if (operation == "enable") {
|
||||
receive_enable(payload);
|
||||
} else if (operation == "pwm_on") {
|
||||
receive_pwm_on(payload);
|
||||
} else if (operation == "cp_state_X1") {
|
||||
receive_cp_state_X1(payload);
|
||||
} else if (operation == "cp_state_F") {
|
||||
receive_cp_state_F(payload);
|
||||
} else if (operation == "allow_power_on") {
|
||||
receive_allow_power_on(payload);
|
||||
} else if (operation == "ac_switch_three_phases_while_charging") {
|
||||
receive_ac_switch_three_phases_while_charging(payload);
|
||||
} else if (operation == "ac_overcurrent_limit") {
|
||||
receive_ac_overcurrent_limit(payload);
|
||||
} else if (operation == "lock") {
|
||||
receive_lock();
|
||||
} else if (operation == "unlock") {
|
||||
receive_unlock();
|
||||
} else if (operation == "self_test") {
|
||||
receive_self_test(payload);
|
||||
} else if (operation == "reset") {
|
||||
receive_request_reset(payload);
|
||||
} else if (operation == "heartbeat") {
|
||||
receive_heartbeat(payload);
|
||||
} else {
|
||||
std::cerr << "evse_bsp: RECEIVE invalid operation: " << operation << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::raise_comm_fault() {
|
||||
send_raise_error(API_BSP::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
|
||||
}
|
||||
|
||||
void evse_bsp_api::clear_comm_fault() {
|
||||
send_clear_error(API_BSP::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_event_cp(std::uint8_t cp) {
|
||||
using bc_event = API_BSP::Event;
|
||||
bc_event cp_event;
|
||||
bool cp_state_valid = true;
|
||||
switch (cp) {
|
||||
case CpState_A:
|
||||
cp_event = bc_event::A;
|
||||
send_clear_error(API_BSP::ErrorEnum::MREC14PilotFault, "", "");
|
||||
send_clear_error(API_BSP::ErrorEnum::DiodeFault, "", "");
|
||||
break;
|
||||
case CpState_B:
|
||||
cp_event = bc_event::B;
|
||||
break;
|
||||
case CpState_C:
|
||||
cp_event = bc_event::C;
|
||||
break;
|
||||
case CpState_D:
|
||||
cp_event = bc_event::D;
|
||||
break;
|
||||
case CpState_E:
|
||||
cp_event = bc_event::E;
|
||||
break;
|
||||
case CpState_F:
|
||||
cp_event = bc_event::F;
|
||||
break;
|
||||
case CpState_DF:
|
||||
cp_event = bc_event::E;
|
||||
send_raise_error(API_BSP::ErrorEnum::DiodeFault, "", "Diode Fault");
|
||||
break;
|
||||
case CpState::CpState_INVALID:
|
||||
cp_event = bc_event::E;
|
||||
send_raise_error(API_BSP::ErrorEnum::MREC14PilotFault, "", "Pilot Fault");
|
||||
break;
|
||||
default:
|
||||
cp_state_valid = false;
|
||||
}
|
||||
if (cp_state_valid and m_enabled) {
|
||||
send_event(cp_event);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_event_relay(std::uint8_t relay) {
|
||||
using bc_event = API_BSP::Event;
|
||||
bc_event relaise_event;
|
||||
bool relaise_state_valid = true;
|
||||
switch (relay) {
|
||||
case RelaiseState::RelayState_Open:
|
||||
relaise_event = bc_event::PowerOff;
|
||||
break;
|
||||
case RelaiseState::RelayState_Closed:
|
||||
relaise_event = bc_event::PowerOn;
|
||||
break;
|
||||
default:
|
||||
relaise_state_valid = false;
|
||||
}
|
||||
if (relaise_state_valid) {
|
||||
send_event(relaise_event);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_pp_type2(std::uint8_t data) {
|
||||
API_BSP::Ampacity bc_ampacity;
|
||||
bool bc_ampacity_valid = true;
|
||||
switch (data) {
|
||||
case PpState_Type2_STATE_NC:
|
||||
bc_ampacity = API_BSP::Ampacity::None;
|
||||
break;
|
||||
case PpState_Type2_STATE_13A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_13;
|
||||
break;
|
||||
case PpState_Type2_STATE_20A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_20;
|
||||
break;
|
||||
case PpState_Type2_STATE_32A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_32;
|
||||
break;
|
||||
case PpState_Type2_STATE_70A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_63_3ph_70_1ph;
|
||||
break;
|
||||
case PpState_Type2_STATE_FAULT:
|
||||
// Raise error check state
|
||||
bc_ampacity_valid = false;
|
||||
send_raise_error(API_BSP::ErrorEnum::MREC23ProximityFault, "", "Proximity Pilot Fault State");
|
||||
break;
|
||||
default:
|
||||
bc_ampacity_valid = false;
|
||||
}
|
||||
if (bc_ampacity_valid) {
|
||||
send_ac_pp_amapcity(bc_ampacity);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_pp_type1(std::uint8_t data) {
|
||||
// EVerest does not really have support for type 1 PP.
|
||||
// We just send a stop charging if some one presses the button,
|
||||
// for everything else the PP state does not really matter.
|
||||
if (data == PpState_Type1_STATE_Connected_Button_Pressed) {
|
||||
auto reason = API_EVM::StopTransactionReason::EVDisconnected;
|
||||
send_request_stop_transaction(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
// Define bit masks
|
||||
enum class SafetyErrorMask : std::uint32_t {
|
||||
cp_not_state_c = (1 << 0),
|
||||
pwm_not_enabled = (1 << 1),
|
||||
pp_invalid = (1 << 2),
|
||||
plug_temperature_too_high = (1 << 3),
|
||||
internal_temperature_too_high = (1 << 4),
|
||||
emergency_input_latched = (1 << 5),
|
||||
relay_health_latched = (1 << 6),
|
||||
vdd_3v3_out_of_range = (1 << 7),
|
||||
vdd_core_out_of_range = (1 << 8),
|
||||
vdd_12V_out_of_range = (1 << 9),
|
||||
vdd_N12V_out_of_range = (1 << 10),
|
||||
vdd_refint_out_of_range = (1 << 11),
|
||||
external_allow_power_on = (1 << 12),
|
||||
config_mem_error = (1 << 13),
|
||||
dc_hv_ov = (1 << 14),
|
||||
};
|
||||
|
||||
// Table that maps a mask to our API error + message
|
||||
struct FlagSpec {
|
||||
SafetyErrorMask mask;
|
||||
API_BSP::ErrorEnum error;
|
||||
const char* subtype;
|
||||
const char* message;
|
||||
};
|
||||
|
||||
static constexpr FlagSpec error_specs[] = {
|
||||
{SafetyErrorMask::pp_invalid, API_BSP::ErrorEnum::MREC23ProximityFault, "", "PP invalid"},
|
||||
{SafetyErrorMask::plug_temperature_too_high, API_BSP::ErrorEnum::MREC19CableOverTempStop, "",
|
||||
"Plug temperature too high"},
|
||||
{SafetyErrorMask::internal_temperature_too_high, API_BSP::ErrorEnum::VendorError, "INTTEMP",
|
||||
"ChargeBridge internal over temperature"},
|
||||
{SafetyErrorMask::emergency_input_latched, API_BSP::ErrorEnum::VendorError, "EMGINPUT", "Emergency input latched"},
|
||||
{SafetyErrorMask::relay_health_latched, API_BSP::ErrorEnum::VendorError, "RELAYS", "Relay welded error"},
|
||||
{SafetyErrorMask::vdd_3v3_out_of_range, API_BSP::ErrorEnum::VendorError, "3V3", "Supply voltage 3.3V out of range"},
|
||||
{SafetyErrorMask::vdd_core_out_of_range, API_BSP::ErrorEnum::VendorError, "VDDCORE",
|
||||
"Internal supply core voltage out of range"},
|
||||
{SafetyErrorMask::vdd_12V_out_of_range, API_BSP::ErrorEnum::VendorError, "VCC12",
|
||||
"Internal supply 12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_N12V_out_of_range, API_BSP::ErrorEnum::VendorError, "VCCN12",
|
||||
"Internal supply -12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_refint_out_of_range, API_BSP::ErrorEnum::VendorError, "VCCREF",
|
||||
"Internal supply VREF voltage out of range"},
|
||||
{SafetyErrorMask::config_mem_error, API_BSP::ErrorEnum::VendorError, "CONFIGMEM", "Internal config memory error"},
|
||||
{SafetyErrorMask::dc_hv_ov, API_BSP::ErrorEnum::VendorError, "DV_HV",
|
||||
"DC HV OVM. FIXME: This should be on OVM not EVSE interface"},
|
||||
};
|
||||
|
||||
static constexpr FlagSpec print_warning_specs[] = {
|
||||
{SafetyErrorMask::cp_not_state_c, API_BSP::ErrorEnum::VendorWarning, "", "CP is not state C"},
|
||||
{SafetyErrorMask::pwm_not_enabled, API_BSP::ErrorEnum::VendorWarning, "", "PWM not enabled"},
|
||||
{SafetyErrorMask::external_allow_power_on, API_BSP::ErrorEnum::VendorWarning, "",
|
||||
"Allow power on from EVerest missing"},
|
||||
};
|
||||
|
||||
// 4) Edge-driven handler
|
||||
void evse_bsp_api::handle_error(const SafetyErrorFlags& data) {
|
||||
std::uint32_t prev = cb_status.error_flags.raw; // cached raw value from before
|
||||
std::uint32_t next = data.raw; // current raw value
|
||||
|
||||
std::uint32_t became_active = next & ~prev; // rising edges
|
||||
std::uint32_t became_inactive = prev & ~next; // falling edges
|
||||
|
||||
for (const auto& s : error_specs) {
|
||||
if (became_active & static_cast<std::uint32_t>(s.mask)) {
|
||||
send_raise_error(s.error, s.subtype, s.message);
|
||||
}
|
||||
if (became_inactive & static_cast<std::uint32_t>(s.mask)) {
|
||||
send_clear_error(s.error, s.subtype, "");
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream log;
|
||||
|
||||
for (const auto& s : print_warning_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& s : error_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
if (everest_connected && m_cb_connected) {
|
||||
if (log.str().empty()) {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0) << "Relays can be switched on." << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0)
|
||||
<< "Relays off due to:" << log.str() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_stop_button([[maybe_unused]] std::uint8_t data) {
|
||||
auto reason = API_EVM::StopTransactionReason::Local;
|
||||
send_request_stop_transaction(reason);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_enable(std::string const& payload) {
|
||||
if (everest::lib::API::deserialize(payload, m_enabled)) {
|
||||
handle_event_cp(cb_status.cp_state);
|
||||
handle_event_relay(cb_status.relay_state);
|
||||
} else {
|
||||
std::cerr << "evse_bsp_api::receive_enabled: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_pwm_on(std::string const& payload) {
|
||||
double pwm = 0;
|
||||
if (everest::lib::API::deserialize(payload, pwm)) {
|
||||
host_status.pwm_duty_cycle = pwm * 100;
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "evse_bsp_api::receive_pwm_on: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_cp_state_X1([[maybe_unused]] std::string const& payload) {
|
||||
host_status.pwm_duty_cycle = 10001;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_cp_state_F([[maybe_unused]] std::string const& payload) {
|
||||
host_status.pwm_duty_cycle = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_allow_power_on(std::string const& payload) {
|
||||
API_BSP::PowerOnOff obj;
|
||||
if (everest::lib::API::deserialize(payload, obj)) {
|
||||
host_status.allow_power_on = obj.allow_power_on;
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "evse_bsp_api::receive_allow_power_on: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_ac_switch_three_phases_while_charging(std::string const&) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_ac_overcurrent_limit(std::string const&) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_lock() {
|
||||
host_status.connector_lock = 1;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_unlock() {
|
||||
host_status.connector_lock = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_self_test([[maybe_unused]] std::string const& payload) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_request_reset(std::string const&) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_heartbeat(std::string const& pl) {
|
||||
last_everest_heartbeat = std::chrono::steady_clock::now();
|
||||
std::size_t id = 0;
|
||||
if (deserialize(pl, id)) {
|
||||
auto delta = id - m_last_hb_id;
|
||||
if (delta > 1) {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
|
||||
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
|
||||
}
|
||||
m_last_hb_id = id;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
|
||||
<< "EVerest invalid heartbeat message: " << pl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_event(API_BSP::Event data) {
|
||||
API_BSP::BspEvent event{data};
|
||||
send_mqtt("event", serialize(event));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_ac_nr_of_phases(std::uint8_t data) {
|
||||
auto phases = static_cast<int>(data);
|
||||
if (phases > 0 && phases <= 3) {
|
||||
send_mqtt("ac_nr_of_phases", serialize(phases));
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_capabilities() {
|
||||
send_mqtt("capabilities", serialize(m_capabilities));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_ac_pp_amapcity(API_BSP::Ampacity data) {
|
||||
API_BSP::ProximityPilot msg{data};
|
||||
send_mqtt("ac_pp_ampacity", serialize(msg));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_request_stop_transaction(API_EVM::StopTransactionReason data) {
|
||||
API_EVM::StopTransactionRequest request;
|
||||
request.reason = data;
|
||||
send_mqtt("request_stop_transaction", serialize(request));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_rcd_current(std::uint8_t) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_raise_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg) {
|
||||
API_BSP::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
send_mqtt("raise_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_clear_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg) {
|
||||
API_BSP::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
send_mqtt("clear_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_communication_check() {
|
||||
send_mqtt("communication_check", serialize(true));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_reply_reset([[maybe_unused]] std::string const& replyTo) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
m_mqtt_tx(topic, message);
|
||||
}
|
||||
|
||||
bool evse_bsp_api::check_everest_heartbeat() {
|
||||
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_everest_connection_state() {
|
||||
send_communication_check();
|
||||
auto current = check_everest_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0) << "EVerest connected" << std::endl;
|
||||
send_capabilities();
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 1) << "Waiting for EVerest...." << std::endl;
|
||||
host_status.allow_power_on = 0;
|
||||
host_status.pwm_duty_cycle = 65535;
|
||||
tx(host_status);
|
||||
}
|
||||
};
|
||||
|
||||
if (m_bc_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_bc_initial_comm_check = false;
|
||||
} else if (everest_connected != current) {
|
||||
handle_status(not everest_connected);
|
||||
}
|
||||
everest_connected = current;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,226 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "protocol/cb_common.h"
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/ovm_api.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/over_voltage_monitor/API.hpp>
|
||||
#include <everest_api_types/over_voltage_monitor/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::API::V1_0::types::generic;
|
||||
using namespace everest::lib::API;
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
ovm_api::ovm_api([[maybe_unused]] evse_ovm_config const& config, std::string const& cb_identifier,
|
||||
evse_bsp_host_to_cb& host_status) :
|
||||
host_status(host_status), m_cb_identifier(cb_identifier) {
|
||||
|
||||
last_everest_heartbeat = std::chrono::steady_clock::time_point();
|
||||
}
|
||||
|
||||
void ovm_api::sync(bool cb_connected) {
|
||||
m_cb_connected = cb_connected;
|
||||
handle_everest_connection_state();
|
||||
}
|
||||
|
||||
bool ovm_api::register_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ovm_api::unregister_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ovm_api::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
}
|
||||
|
||||
void ovm_api::tx(evse_bsp_host_to_cb const& msg) {
|
||||
if (m_tx) {
|
||||
m_tx(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::set_mqtt_tx(mqtt_ftor const& handler) {
|
||||
m_mqtt_tx = handler;
|
||||
}
|
||||
|
||||
void ovm_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
const double voltage_V = msg.hv_mV * 0.001;
|
||||
send_voltage_measurement_V(voltage_V);
|
||||
|
||||
if (msg.error_flags.flags.dc_hv_ov_emergency not_eq m_cb_status.error_flags.flags.dc_hv_ov_emergency) {
|
||||
handle_dc_hv_ov_emergency(msg.error_flags.flags.dc_hv_ov_emergency not_eq 0);
|
||||
}
|
||||
if (msg.error_flags.flags.dc_hv_ov_error not_eq m_cb_status.error_flags.flags.dc_hv_ov_error) {
|
||||
handle_dc_hv_ov_error(msg.error_flags.flags.dc_hv_ov_error not_eq 0);
|
||||
}
|
||||
|
||||
if (msg.cp_state not_eq m_cb_status.cp_state) {
|
||||
handle_cp_state(static_cast<CpState>(msg.cp_state));
|
||||
}
|
||||
|
||||
m_cb_status = msg;
|
||||
}
|
||||
|
||||
void ovm_api::dispatch(std::string const& operation, std::string const& payload) {
|
||||
if (operation == "set_limits") {
|
||||
receive_set_limits(payload);
|
||||
} else if (operation == "start") {
|
||||
receive_start();
|
||||
} else if (operation == "stop") {
|
||||
receive_stop();
|
||||
} else if (operation == "reset_over_voltage_error") {
|
||||
receive_reset_over_voltage_error();
|
||||
} else if (operation == "heartbeat") {
|
||||
receive_heartbeat(payload);
|
||||
} else {
|
||||
std::cerr << "ovm_api: RECEIVE invalid operation: " << operation << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::raise_comm_fault() {
|
||||
send_raise_error(API_OVM::ErrorEnum::CommunicationFault, "ChargeBridge not available", "",
|
||||
API_OVM::ErrorSeverityEnum::High);
|
||||
}
|
||||
|
||||
void ovm_api::clear_comm_fault() {
|
||||
send_clear_error(API_OVM::ErrorEnum::CommunicationFault, "ChargeBridge not available");
|
||||
}
|
||||
|
||||
void ovm_api::handle_dc_hv_ov_emergency(bool high) {
|
||||
static const std::string subtype = "Emergency";
|
||||
if (high) {
|
||||
send_raise_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype, "", API_OVM::ErrorSeverityEnum::High);
|
||||
} else {
|
||||
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype);
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::handle_dc_hv_ov_error(bool high) {
|
||||
static const std::string subtype = "Error";
|
||||
if (high) {
|
||||
send_raise_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype, "", API_OVM::ErrorSeverityEnum::Medium);
|
||||
} else {
|
||||
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype);
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::handle_cp_state(CpState state) {
|
||||
if (state == CpState_A) {
|
||||
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, "");
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::receive_set_limits(std::string const& payload) {
|
||||
static auto const V_to_mV_factor = 1000;
|
||||
if (everest::lib::API::deserialize(payload, m_limits)) {
|
||||
host_status.ovm_limit_emergency_mV = static_cast<std::uint32_t>(m_limits.emergency_limit_V * V_to_mV_factor);
|
||||
host_status.ovm_limit_error_mV = static_cast<std::uint32_t>(m_limits.error_limit_V * V_to_mV_factor);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ovm_api::receive_set_limits: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::receive_start() {
|
||||
host_status.ovm_enable = 1;
|
||||
host_status.ovm_reset_errors = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void ovm_api::receive_stop() {
|
||||
host_status.ovm_enable = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void ovm_api::receive_reset_over_voltage_error() {
|
||||
host_status.ovm_reset_errors = 1;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void ovm_api::receive_heartbeat(std::string const& pl) {
|
||||
last_everest_heartbeat = std::chrono::steady_clock::now();
|
||||
std::size_t id = 0;
|
||||
if (deserialize(pl, id)) {
|
||||
auto delta = id - m_last_hb_id;
|
||||
if (delta > 1) {
|
||||
utilities::print_error(m_cb_identifier, "OVM/EVEREST", -1)
|
||||
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
|
||||
}
|
||||
m_last_hb_id = id;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
|
||||
<< "EVerest invalid heartbeat message: " << pl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::send_voltage_measurement_V(double data) {
|
||||
send_mqtt("voltage_measurement_V", serialize(data));
|
||||
}
|
||||
|
||||
void ovm_api::send_raise_error(API_OVM::ErrorEnum error, std::string const& subtype, std::string const& msg,
|
||||
API_OVM::ErrorSeverityEnum severity) {
|
||||
API_OVM::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
error_msg.severity = severity;
|
||||
send_mqtt("raise_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void ovm_api::send_clear_error(API_OVM::ErrorEnum error, std::string const& subtype) {
|
||||
API_OVM::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
send_mqtt("clear_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void ovm_api::send_communication_check() {
|
||||
send_mqtt("communication_check", serialize(true));
|
||||
}
|
||||
|
||||
void ovm_api::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
if (m_mqtt_tx) {
|
||||
m_mqtt_tx(topic, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool ovm_api::check_everest_heartbeat() {
|
||||
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void ovm_api::handle_everest_connection_state() {
|
||||
send_communication_check();
|
||||
auto current = check_everest_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "OVM/EVEREST", 0) << "EVerest connected" << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "OVM/EVEREST", 1) << "Waiting for EVerest...." << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
if (m_bc_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_bc_initial_comm_check = false;
|
||||
} else if (m_everest_connected != current) {
|
||||
handle_status(not m_everest_connected);
|
||||
}
|
||||
m_everest_connected = current;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "everest/io/udp/udp_payload.hpp"
|
||||
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
|
||||
#include <charge_bridge/utilities/filesystem.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/platform_utils.hpp>
|
||||
|
||||
#include <protocol/cb_management.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 3000;
|
||||
}
|
||||
|
||||
namespace charge_bridge::firmware_update {
|
||||
|
||||
const std::uint32_t sync_fw_updater::app_udp_sector_size = 0x2000;
|
||||
const std::uint16_t sync_fw_updater::sub_chunk_size = 1024;
|
||||
|
||||
using namespace everest::lib::io::udp;
|
||||
|
||||
static everest::lib::io::udp::udp_payload make_ping_command() {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
|
||||
CbManagementPacket<CbFirmwarePing> packet;
|
||||
packet.type = CbStructType::CST_CbFirmwarePing;
|
||||
packet.data.dummy = 0;
|
||||
utilities::struct_to_vector(packet, payload.buffer);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
static everest::lib::io::udp::udp_payload make_get_version_command() {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
|
||||
CbManagementPacket<CbFirmwareGetVersion> packet;
|
||||
packet.type = CbStructType::CST_CbFirmwareGetVersion;
|
||||
packet.data.dummy = 0;
|
||||
utilities::struct_to_vector(packet, payload.buffer);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
sync_fw_updater::sync_fw_updater(fw_update_config const& config) :
|
||||
m_udp(config.cb_remote, config.cb_port, 3, default_udp_timeout_ms), m_config(config) {
|
||||
}
|
||||
|
||||
std::optional<std::string> sync_fw_updater::get_fw_version() {
|
||||
auto pl = make_get_version_command();
|
||||
|
||||
auto result = m_udp.request_reply(pl);
|
||||
if (not result) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result->buffer[result->buffer.size() - 1] = 0x00; // ensure it is actually a 0 terminated string
|
||||
auto* str_ptr = reinterpret_cast<char*>(result->buffer.data()); // reinterpret for string conversion
|
||||
return std::string(str_ptr + 2); // skip 2 byte header
|
||||
}
|
||||
|
||||
void sync_fw_updater::print_fw_version() {
|
||||
auto result = get_fw_version();
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", not result.has_value())
|
||||
<< "Firmware version " << result.value_or("ERROR") << std::endl;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::check_if_correct_fw_installed() {
|
||||
auto installed_fw = get_fw_version();
|
||||
|
||||
if (not installed_fw.has_value()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader hdr;
|
||||
std::uint32_t offset;
|
||||
if (not read_crypt_signed_header(m_config.fw_path, hdr, offset)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1)
|
||||
<< "Could not read header for file: " << m_config.fw_path << std::endl;
|
||||
return false;
|
||||
}
|
||||
auto available_fw = hdr.firmware_version;
|
||||
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0)
|
||||
<< "Firmware installed: \"" << installed_fw.value() << "\" Firmware available: \"" << available_fw << "\""
|
||||
<< std::endl;
|
||||
|
||||
if (installed_fw.value() == available_fw) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool sync_fw_updater::quick_check_connection() {
|
||||
static const std::uint16_t rr_timeout_ms = 200;
|
||||
static const std::uint16_t rr_retires_ms = 10;
|
||||
|
||||
everest::lib::io::udp::udp_payload pl = make_ping_command();
|
||||
auto result = m_udp.request_reply(pl, rr_timeout_ms, rr_retires_ms).has_value();
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", not result)
|
||||
<< (result ? "ChargeBride Connected" : "No connection to ChargeBridge") << std::endl;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::check_connection() {
|
||||
static const std::uint16_t rr_timeout_ms = 150;
|
||||
static const std::uint16_t rr_retires_ms = 100;
|
||||
|
||||
everest::lib::io::udp::udp_payload pl = make_ping_command();
|
||||
auto result = m_udp.request_reply(pl, rr_timeout_ms, rr_retires_ms).has_value();
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", not result)
|
||||
<< (result ? "ChargeBride Connected" : "No connection to ChargeBridge") << std::endl;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::ping() {
|
||||
everest::lib::io::udp::udp_payload pl = make_ping_command();
|
||||
|
||||
return m_udp.request_reply(pl).has_value();
|
||||
}
|
||||
|
||||
bool sync_fw_updater::check_reply(utilities::sync_udp_client::reply const& val) {
|
||||
if (val && val->size() == (sizeof(AppUDPResponse) + 2)) {
|
||||
AppUDPResponse reply;
|
||||
memcpy(&reply, val->buffer.data() + 2, sizeof(AppUDPResponse));
|
||||
return (reply == AppUDPResponse::AUR_Ok);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_fw() {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0) << "Upload in progress" << std::endl;
|
||||
|
||||
if (not upload_firmware()) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "Upload of firmware image: " << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0) << "Upload completed" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_firmware() {
|
||||
auto path = m_config.fw_path;
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0) << path << std::endl;
|
||||
|
||||
if (not fs::exists(path) || not fs::is_regular_file(path)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "firmware file not found: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t offset;
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader hdr;
|
||||
|
||||
if (not upload_init(path, offset, hdr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t total_bytes = 0;
|
||||
std::uint16_t sector = 0;
|
||||
|
||||
if (not upload_transfer(path, sector, offset, total_bytes)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "Upload failed at sector: " << sector << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (not upload_finish(path, total_bytes, hdr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
# File format for the binary update bundle:
|
||||
# 32 byte header [reserved]
|
||||
# 1 byte length of signature
|
||||
# signature binary
|
||||
# 1 byte NUM_SECTORS: This is the number of secure sectors
|
||||
# 16 byte IV
|
||||
# ... rest of the file is assembled firmware image: secure part...padding...non secure part (encrypted)
|
||||
*/
|
||||
|
||||
bool sync_fw_updater::upload_init(const fs::path& file_path, std::uint32_t& offset,
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader& hdr) {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
|
||||
if (not read_crypt_signed_header(file_path, hdr, offset)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1)
|
||||
<< "Could not read header for file: " << file_path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0)
|
||||
<< "Loaded firmware version file: " << file_path << " Version: " << hdr.firmware_version << std::endl;
|
||||
|
||||
CbManagementPacket<CbFirmwareStart> msg;
|
||||
msg.type = CbStructType::CST_CbFirmwareStart;
|
||||
|
||||
msg.data.is_secure_fw = true;
|
||||
msg.data.requires_crc_verification = true;
|
||||
msg.data.requires_sha256_verification = true;
|
||||
msg.data.requires_signature_verification = true;
|
||||
msg.data.requires_decryption = true;
|
||||
|
||||
// Copy the IV from the header
|
||||
std::memcpy(msg.data.iv, hdr.iv.data(), sizeof(msg.data.iv));
|
||||
|
||||
utilities::struct_to_vector(msg, payload.buffer);
|
||||
auto result = m_udp.request_reply(payload);
|
||||
|
||||
return check_reply(result);
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_transfer(const fs::path& file_path, std::uint16_t& sector, std::uint32_t offset,
|
||||
std::uint32_t& total_bytes) {
|
||||
bool send_failed = false;
|
||||
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip the header
|
||||
file.seekg(offset, std::ios::beg);
|
||||
|
||||
bool processed_file = filesystem_utils::process_file(
|
||||
file, sub_chunk_size, [&](const std::vector<std::uint8_t>& buffer, bool last_chunk) -> bool {
|
||||
total_bytes += buffer.size();
|
||||
|
||||
// Care must be taken when sending this over, since on the
|
||||
// receiving end we must remove the PKCS#7 added bytes
|
||||
auto block = make_fw_chunk(sector, last_chunk, buffer);
|
||||
auto result = m_udp.request_reply(block);
|
||||
|
||||
if (not check_reply(result)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "chunk could not be sent" << std::endl;
|
||||
|
||||
send_failed = true;
|
||||
return true; // Interrupt
|
||||
}
|
||||
|
||||
sector++;
|
||||
|
||||
return false; // Continue
|
||||
});
|
||||
|
||||
return (processed_file) && (send_failed == false);
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_finish([[maybe_unused]] const fs::path& file_path, std::uint32_t total_bytes,
|
||||
const charge_bridge::filesystem_utils::CryptSignedHeader& hdr) {
|
||||
CbManagementPacket<CbFirmwareEnd> fw_check_packet;
|
||||
|
||||
fw_check_packet.type = CbStructType::CST_CbFirmwareFinish;
|
||||
fw_check_packet.data.firmware_len = total_bytes;
|
||||
fw_check_packet.data.watermark_secure_end = hdr.num_sectors;
|
||||
|
||||
if (hdr.sig_len > sizeof(fw_check_packet.data.fw_signature) || hdr.sig_len > hdr.signature.size()) {
|
||||
return false;
|
||||
}
|
||||
memcpy(fw_check_packet.data.fw_signature, hdr.signature.data(), hdr.sig_len);
|
||||
fw_check_packet.data.fw_signature_len = hdr.sig_len;
|
||||
|
||||
udp_payload payload;
|
||||
utilities::struct_to_vector(fw_check_packet, payload.buffer);
|
||||
|
||||
// The final check can be a very slow operation due to the cryptography involved
|
||||
static const std::uint16_t rr_timeout_ms = 10000;
|
||||
static const std::uint16_t rr_retires_ms = 1;
|
||||
auto result = m_udp.request_reply(payload, rr_timeout_ms, rr_retires_ms);
|
||||
|
||||
return check_reply(result);
|
||||
}
|
||||
|
||||
udp_payload sync_fw_updater::make_fw_chunk(std::uint16_t sector, std::uint8_t last_chunk,
|
||||
std::vector<std::uint8_t> const& data) {
|
||||
CbManagementPacket<CbFirmwarePacket> fw_data_packet;
|
||||
fw_data_packet.type = CbStructType::CST_CbFirmwarePacket;
|
||||
fw_data_packet.data.last_packet = last_chunk;
|
||||
fw_data_packet.data.sector = sector;
|
||||
fw_data_packet.data.data_len = data.size();
|
||||
std::memcpy(fw_data_packet.data.data, data.data(), data.size());
|
||||
|
||||
udp_payload result;
|
||||
utilities::struct_to_vector(fw_data_packet, result.buffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::firmware_update
|
||||
@@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <charge_bridge/gpio_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/platform_utils.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/mqtt/mqtt_client.hpp>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <protocol/cb_management.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge {
|
||||
using namespace std::chrono_literals;
|
||||
namespace mqtt = everest::lib::io::mqtt;
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
const int mqtt_reconnect_timeout_ms = 1000;
|
||||
} // namespace
|
||||
|
||||
gpio_bridge::gpio_bridge(gpio_config const& config) :
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
|
||||
m_mqtt(mqtt_reconnect_timeout_ms)
|
||||
|
||||
{
|
||||
m_identifier = config.cb + "/" + config.item;
|
||||
|
||||
m_heartbeat_timer.set_timeout(std::chrono::seconds(config.interval_s));
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) { handle_udp_rx(data); });
|
||||
|
||||
m_udp.set_error_handler([this](auto id, auto const& msg) {
|
||||
utilities::print_error(m_identifier, "GPIO/UDP", id) << msg << std::endl;
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
|
||||
m_receive_topic = "pionix/chargebridge/" + config.cb + "/gpio/output/";
|
||||
m_send_topic = "pionix/chargebridge/" + config.cb + "/gpio/input/";
|
||||
|
||||
m_mqtt.set_error_handler([this, config](int id, std::string const& msg) {
|
||||
utilities::print_error(m_identifier, "GPIO/MQTT", id) << msg << std::endl;
|
||||
m_mqtt_on_error = id not_eq 0;
|
||||
});
|
||||
|
||||
m_mqtt.set_callback_connect([this](auto&, auto, auto, auto const&) {
|
||||
m_mqtt.subscribe(
|
||||
m_receive_topic + "#", [this](auto&, auto const& payload) { dispatch(payload); },
|
||||
everest::lib::io::mqtt::mqtt_client::QoS::at_most_once);
|
||||
});
|
||||
|
||||
m_mqtt.connect(config.mqtt_bind, config.mqtt_remote, config.mqtt_port, config.mqtt_ping_interval_ms);
|
||||
|
||||
m_message.type = CbStructType::CST_HostToCb_Gpio;
|
||||
m_message.data.number_of_gpios = CB_NUMBER_OF_GPIOS;
|
||||
std::memset(m_message.data.gpio_values, 0, sizeof(m_message.data.gpio_values));
|
||||
}
|
||||
|
||||
gpio_bridge::~gpio_bridge() {
|
||||
}
|
||||
|
||||
bool gpio_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.register_event_handler(&m_udp);
|
||||
result = handler.register_event_handler(&m_mqtt) && result;
|
||||
result = handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); }) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool gpio_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.unregister_event_handler(&m_udp);
|
||||
result = handler.unregister_event_handler(&m_mqtt) && result;
|
||||
result = handler.unregister_event_handler(&m_heartbeat_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void gpio_bridge::dispatch(everest::lib::io::mqtt::mqtt_client::message const& data) {
|
||||
auto& topic = data.topic;
|
||||
auto& payload = data.payload;
|
||||
auto operation = utilities::string_after_pattern(topic, m_receive_topic);
|
||||
uint16_t value = 0;
|
||||
int id = 0;
|
||||
|
||||
auto stous = [](std::string const& data) {
|
||||
auto val = stoi(data);
|
||||
if (val < 0 or val > std::numeric_limits<uint16_t>::max()) {
|
||||
throw std::range_error("");
|
||||
}
|
||||
return static_cast<uint16_t>(val);
|
||||
};
|
||||
|
||||
try {
|
||||
value = stous(payload);
|
||||
} catch (...) {
|
||||
std::cout << "INVALID DATA on MQTT for GPIO DATA" << std::endl;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
id = std::stoi(operation);
|
||||
} catch (...) {
|
||||
std::cout << "INVALID DATA on MQTT for GPIO ID" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (id < 0 or id >= CB_NUMBER_OF_GPIOS) {
|
||||
std::cout << "INVALID GPIO ID" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
m_message.data.gpio_values[id] = value;
|
||||
send_udp();
|
||||
}
|
||||
|
||||
void gpio_bridge::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
everest::lib::io::mqtt::mqtt_client::message payload;
|
||||
payload.topic = m_send_topic + topic;
|
||||
payload.payload = message;
|
||||
m_mqtt.publish(payload);
|
||||
}
|
||||
|
||||
void gpio_bridge::send_udp() {
|
||||
if (not m_udp_on_error) {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
utilities::struct_to_vector(m_message, payload.buffer);
|
||||
m_udp.tx(payload);
|
||||
}
|
||||
}
|
||||
|
||||
void gpio_bridge::handle_error_timer() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void gpio_bridge::handle_heartbeat_timer() {
|
||||
send_udp();
|
||||
}
|
||||
|
||||
void gpio_bridge::handle_udp_rx(everest::lib::io::udp::udp_payload const& payload) {
|
||||
CbManagementPacket<CbGpioPacket> data;
|
||||
if (payload.size() == sizeof(data)) {
|
||||
std::memcpy(&data, payload.buffer.data(), sizeof(data));
|
||||
for (std::size_t i = 0; i < sizeof(data.data.gpio_values) / sizeof(data.data.gpio_values[0]); ++i) {
|
||||
send_mqtt(std::to_string(i), std::to_string(data.data.gpio_values[i]));
|
||||
}
|
||||
} else {
|
||||
std::cout << "INVALID DATA SIZE in UDP RX of GPIO: " << payload.size() << " vs " << sizeof(data) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <charge_bridge/heartbeat_service.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/platform_utils.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <protocol/cb_management.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
const std::uint16_t s_to_ms_factor = 1000;
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
heartbeat_service::heartbeat_service(heartbeat_config const& config,
|
||||
std::function<void(bool)> const& publish_connection_status) :
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
|
||||
m_publish_connection_status(publish_connection_status) {
|
||||
m_identifier = config.cb + "/" + config.item;
|
||||
std::memcpy(&m_config_message.data, &config.cb_config, sizeof(CbConfig));
|
||||
m_config_message.type = CbStructType::CST_HostToCb_Heartbeat;
|
||||
m_heartbeat_interval = std::chrono::milliseconds(config.interval_s * s_to_ms_factor);
|
||||
m_connection_to = std::chrono::milliseconds(config.connection_to_s * s_to_ms_factor);
|
||||
m_heartbeat_timer.set_timeout(m_heartbeat_interval);
|
||||
m_last_heartbeat_reply = std::chrono::steady_clock::time_point();
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) { handle_udp_rx(data); });
|
||||
|
||||
m_udp.set_error_handler([this](auto id, auto const& msg) {
|
||||
if (m_inital_cb_commcheck and id == 0) {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 1) << "Waiting for ChargeBridge" << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", id) << msg << std::endl;
|
||||
}
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
}
|
||||
|
||||
heartbeat_service::~heartbeat_service() {
|
||||
}
|
||||
|
||||
bool heartbeat_service::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.register_event_handler(&m_udp) &&
|
||||
handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); });
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
bool heartbeat_service::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.unregister_event_handler(&m_udp) &&
|
||||
handler.unregister_event_handler(&m_heartbeat_timer);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
void heartbeat_service::handle_error_timer() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void heartbeat_service::handle_heartbeat_timer() {
|
||||
if (not m_udp_on_error) {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
utilities::struct_to_vector(m_config_message, payload.buffer);
|
||||
m_udp.tx(payload);
|
||||
}
|
||||
auto timeout = std::chrono::steady_clock::now() - m_last_heartbeat_reply > m_connection_to;
|
||||
if (timeout and m_cb_connected) {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 1) << "ChargeBridge connection lost" << std::endl;
|
||||
m_cb_connected = false;
|
||||
}
|
||||
|
||||
else if (not timeout and not m_cb_connected) {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 0) << "ChargeBridge connected" << std::endl;
|
||||
m_cb_connected = true;
|
||||
}
|
||||
if (m_publish_connection_status) {
|
||||
m_publish_connection_status(m_cb_connected);
|
||||
}
|
||||
}
|
||||
|
||||
void heartbeat_service::handle_udp_rx(everest::lib::io::udp::udp_payload const& payload) {
|
||||
CbManagementPacket<CbHeartbeatReplyPacket> data;
|
||||
if (payload.size() == sizeof(data)) {
|
||||
std::memcpy(&data, payload.buffer.data(), sizeof(data));
|
||||
m_last_heartbeat_reply = std::chrono::steady_clock::now();
|
||||
auto mcu_current = static_cast<uint32_t>(data.data.uptime_ms);
|
||||
if (mcu_current <= m_mcu_timestamp) {
|
||||
m_mcu_reset_count++;
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", -1)
|
||||
<< "ChargeBridge reset count " << m_mcu_reset_count << std::endl;
|
||||
}
|
||||
m_mcu_timestamp = mcu_current;
|
||||
|
||||
// TODO: Once we have the telemetry framework in EVerest, we should publish those values.
|
||||
/*printf(
|
||||
"CP: %.2f/%.2f PP: %i MCU_temp %i degC\nVoltages: 12V: %.2f, -12V: %.2f, ref %.3f, 3.3V: %.3f, core:
|
||||
%.3f\n", data.data.cp_hi_mV / 1000., data.data.cp_lo_mV / 1000., (int)data.data.pp_mOhm / 1000,
|
||||
data.data.temperature_mcu_C, data.data.vdd_12V/1000., data.data.vdd_N12V/1000., data.data.vdd_refint/1000.,
|
||||
data.data.vdd_3v3/1000., data.data.vdd_core/1000.);*/
|
||||
} else {
|
||||
std::cout << "INVALID DATA SIZE in UDP RX of HEARTBEAT: " << payload.size() << " vs " << sizeof(data)
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/plc_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/udp/udp_payload.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
plc_bridge::plc_bridge(plc_bridge_config const& config) :
|
||||
m_tap(config.plc_tap, config.plc_ip, config.plc_netmaks, config.plc_mtu),
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(5s);
|
||||
|
||||
m_tap.set_rx_handler([this](auto const& data, auto&) {
|
||||
everest::lib::io::udp::udp_payload pl;
|
||||
pl.buffer = data;
|
||||
m_udp.tx(pl);
|
||||
});
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) { m_tap.tx(data.buffer); });
|
||||
|
||||
auto identifier = config.cb + "/" + config.item;
|
||||
m_tap.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "PLC/TAP", id) << msg << std::endl;
|
||||
m_tap_on_error = id not_eq 0;
|
||||
});
|
||||
|
||||
m_udp.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "PLC/UDP", id) << msg << std::endl;
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
}
|
||||
|
||||
void plc_bridge::handle_timer_event() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
if (m_tap_on_error) {
|
||||
m_tap.reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool plc_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.register_event_handler(&m_tap) && result;
|
||||
result = handler.register_event_handler(&m_udp) && result;
|
||||
result = handler.register_event_handler(&m_timer, [this](auto) { handle_timer_event(); }) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool plc_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.unregister_event_handler(&m_tap) && result;
|
||||
result = handler.unregister_event_handler(&m_udp) && result;
|
||||
result = handler.unregister_event_handler(&m_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "everest/io/serial/event_pty.hpp"
|
||||
#include <charge_bridge/serial_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <protocol/cb_can_message.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
const std::uint32_t tcp_user_timeout_ms = 4000;
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
serial_bridge::serial_bridge(serial_bridge_config const& config) :
|
||||
m_pty(), m_tcp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto link_ok = m_symlink.set_link(m_pty.get_slave_path(), config.serial_device);
|
||||
if (not link_ok) {
|
||||
throw std::runtime_error("Failed to setup symbolic links for serial ports");
|
||||
}
|
||||
|
||||
m_tcp.set_on_ready_action([this]() {
|
||||
m_tcp.get_raw_handler()->set_keep_alive(3, 1, 1);
|
||||
m_tcp.get_raw_handler()->set_user_timeout(tcp_user_timeout_ms);
|
||||
});
|
||||
|
||||
m_pty.set_data_handler([this](auto const& data, auto&) { m_tcp.tx(data); });
|
||||
|
||||
m_tcp.set_rx_handler([this](auto const& data, auto&) { m_pty.tx(data); });
|
||||
|
||||
auto identifier = config.cb + "/" + config.item;
|
||||
m_pty.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "SERIAL/PTY", id) << msg << std::endl;
|
||||
if (id not_eq 0) {
|
||||
m_pty.reset();
|
||||
}
|
||||
});
|
||||
|
||||
m_tcp.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
if (m_tcp_last_error_id not_eq id) {
|
||||
utilities::print_error(identifier, "SERIAL/TCP", id) << msg << std::endl;
|
||||
m_tcp_last_error_id = id;
|
||||
}
|
||||
if (id not_eq 0) {
|
||||
m_tcp.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void serial_bridge::reset_tcp() {
|
||||
m_tcp.reset();
|
||||
}
|
||||
|
||||
std::string serial_bridge::get_slave_path() {
|
||||
return m_pty.get_slave_path();
|
||||
}
|
||||
|
||||
bool serial_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.register_event_handler(&m_pty) && result;
|
||||
result = handler.register_event_handler(&m_tcp) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool serial_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.unregister_event_handler(&m_pty) && result;
|
||||
result = handler.unregister_event_handler(&m_tcp) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,147 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/filesystem.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
namespace charge_bridge::filesystem_utils {
|
||||
|
||||
bool read_from_file_partial(const fs::path& file_path, const std::size_t byte_count, std::string& out_data) {
|
||||
try {
|
||||
if (fs::is_regular_file(file_path)) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (file.is_open()) {
|
||||
std::vector<char> buffer(byte_count);
|
||||
file.read(buffer.data(), byte_count);
|
||||
|
||||
std::size_t read_bytes = file.gcount();
|
||||
|
||||
if (read_bytes == byte_count) {
|
||||
out_data.assign(buffer.data(), read_bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool read_from_file(const fs::path& file_path, std::string& out_data) {
|
||||
try {
|
||||
if (fs::is_regular_file(file_path)) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (file.is_open()) {
|
||||
out_data = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool process_file(const fs::path& file_path, std::size_t buffer_size,
|
||||
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
return process_file(file, buffer_size, std::move(func));
|
||||
}
|
||||
|
||||
bool process_file(std::ifstream& file, std::size_t buffer_size,
|
||||
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func) {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> buffer(buffer_size);
|
||||
bool interupted = false;
|
||||
|
||||
while (file.read(reinterpret_cast<char*>(buffer.data()), buffer_size)) {
|
||||
interupted = func(buffer, false);
|
||||
|
||||
if (interupted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the remaining bytes
|
||||
if (interupted == false) {
|
||||
std::size_t remaining = file.gcount();
|
||||
|
||||
// Keep only remaining elements
|
||||
buffer.resize(remaining);
|
||||
func(buffer, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns true on success, fills `hdr`, and sets `image_offset` to the byte
|
||||
// position (from start of file) where the firmware image begins.
|
||||
bool read_crypt_signed_header(const fs::path& path, CryptSignedHeader& hdr, std::uint32_t& image_offset) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto read_exact = [&](void* dst, std::size_t n) -> bool {
|
||||
f.read(reinterpret_cast<char*>(dst), static_cast<std::streamsize>(n));
|
||||
return f.good() || (f.eof() && static_cast<std::size_t>(f.gcount()) == n);
|
||||
};
|
||||
|
||||
char firmware_version_str[32];
|
||||
std::memset(firmware_version_str, 0, sizeof(firmware_version_str)); // all zeros
|
||||
// 32-byte reserved header
|
||||
if (!read_exact(firmware_version_str, sizeof(firmware_version_str))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hdr.firmware_version = std::string(firmware_version_str);
|
||||
|
||||
// 1-byte signature length
|
||||
if (!read_exact(&hdr.sig_len, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// L-byte signature
|
||||
hdr.signature.resize(hdr.sig_len);
|
||||
if (hdr.sig_len > 0) {
|
||||
if (!read_exact(hdr.signature.data(), hdr.signature.size())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 1-byte NUM_SECTORS
|
||||
if (!read_exact(&hdr.num_sectors, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 16-byte IV
|
||||
if (!read_exact(hdr.iv.data(), hdr.iv.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Where the firmware image starts:
|
||||
// offset = 32 + 1 + L + 1 + 16
|
||||
image_offset = static_cast<std::uint32_t>(f.tellg());
|
||||
// As a sanity fallback, compute if tellg() failed:
|
||||
// Disabled, since it is always false
|
||||
// if (static_cast<std::streamoff>(image_offset)< 0) {
|
||||
// image_offset = 32u + 1u + static_cast<std::uint64_t>(hdr.sig_len) + 1u + 16u;
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::filesystem_utils
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <iomanip>
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
enum class color {
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
message,
|
||||
unit,
|
||||
standard,
|
||||
terminal,
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& s, color c) {
|
||||
switch (c) {
|
||||
case color::error:
|
||||
s << "\033[31m";
|
||||
break;
|
||||
case color::success:
|
||||
s << "\033[32m";
|
||||
break;
|
||||
case color::warning:
|
||||
s << "\033[33m";
|
||||
break;
|
||||
case color::message:
|
||||
s << "\033[37m";
|
||||
break;
|
||||
case color::unit:
|
||||
s << "\033[1;37m";
|
||||
break;
|
||||
case color::terminal:
|
||||
s << "\033[m";
|
||||
break;
|
||||
case color::standard:
|
||||
default:
|
||||
s << "\033[39;49m";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
std::ostream& print_error(std::string const& device, std::string const& unit, int status) {
|
||||
// clang-format off
|
||||
auto ctrl =
|
||||
status == 0 ? color::success :
|
||||
status == -1 ? color::warning:
|
||||
color::error;
|
||||
std::cout << "[ " << ctrl << std::setw(13) << std::left << unit << color::terminal << " ] "
|
||||
<< color::unit << std::setw(20) << device << color::terminal << " ";
|
||||
if(status not_eq 0){
|
||||
if(status == -1){
|
||||
std::cout << color::standard << "WARNING ";
|
||||
}
|
||||
else{
|
||||
std::cout << color::standard << "ERROR ( " << status << " ) ";
|
||||
}
|
||||
}
|
||||
return std::cout << color::standard;
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,415 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "c4/yml/node.hpp"
|
||||
#include <charge_bridge/utilities/parse_config.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <charge_bridge/utilities/type_converters.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <iostream>
|
||||
|
||||
#include <filesystem>
|
||||
// clang-format off
|
||||
#include <ryml_std.hpp>
|
||||
#include <ryml.hpp>
|
||||
// clang-format on
|
||||
using namespace everest::lib::API::V1_0::types;
|
||||
|
||||
namespace {
|
||||
static const int g_cb_port_management = 6000;
|
||||
static const int g_cb_port_evse_bsp = 6001;
|
||||
static const int g_cb_port_plc = 6002;
|
||||
static const int g_cb_port_can0 = 6003;
|
||||
static const int g_cb_port_serial_1 = 6004;
|
||||
static const int g_cb_port_serial_2 = 6005;
|
||||
static const std::uint16_t default_mqtt_ping_interval_ms = 1000;
|
||||
|
||||
std::string print_yaml_location(ryml::Location const& loc) {
|
||||
std::stringstream error_msg;
|
||||
|
||||
if (loc) {
|
||||
if (not loc.name.empty()) {
|
||||
auto tmp = std::string(loc.name.str, loc.name.len);
|
||||
if (charge_bridge::utilities::string_ends_with(tmp, ".hpp")) {
|
||||
return "";
|
||||
}
|
||||
error_msg << "\n file ";
|
||||
error_msg << tmp;
|
||||
}
|
||||
error_msg << "\n line " << loc.line;
|
||||
if (loc.col) {
|
||||
error_msg << " column " << loc.col;
|
||||
}
|
||||
if (loc.offset) {
|
||||
error_msg << " offset " << loc.offset << "B";
|
||||
}
|
||||
error_msg << "\n";
|
||||
}
|
||||
return error_msg.str();
|
||||
}
|
||||
|
||||
void yaml_error_handler(const char* msg, std::size_t len, ryml::Location loc, void*) {
|
||||
std::stringstream error_msg;
|
||||
error_msg << "YAML parsing error: ";
|
||||
error_msg << print_yaml_location(loc);
|
||||
error_msg.write(msg, len);
|
||||
|
||||
std::cerr << error_msg.str() << std::endl;
|
||||
throw std::runtime_error(error_msg.str());
|
||||
}
|
||||
|
||||
void print_location(ryml::ConstNodeRef node, ryml::Parser& parser) {
|
||||
std::cerr << print_yaml_location(node.location(parser)) << std::endl;
|
||||
}
|
||||
|
||||
void load_yaml_file(const std::string& filename, ryml::Parser* parser, ryml::Tree* t) {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Could not open file: " + filename);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string file_content = buffer.str();
|
||||
|
||||
parse_in_arena(parser, ryml::to_csubstr(filename), ryml::to_csubstr(file_content), t);
|
||||
}
|
||||
|
||||
template <class T> c4::yml::ConstNodeRef decode(c4::yml::ConstNodeRef const& node, T& rhs) {
|
||||
using namespace charge_bridge::utilities;
|
||||
node >> rhs;
|
||||
return node;
|
||||
}
|
||||
|
||||
std::pair<std::string, c4::yml::ConstNodeRef> find_node(c4::yml::NodeRef& config, std::string const& main,
|
||||
std::string const& sub) {
|
||||
auto main_str = ryml::to_csubstr(main);
|
||||
auto node_str = main;
|
||||
c4::yml::ConstNodeRef node;
|
||||
if (not sub.empty()) {
|
||||
node_str = node_str + "::" + sub;
|
||||
auto sub_str = ryml::to_csubstr(sub);
|
||||
node = config.find_child(main_str);
|
||||
if (not node.invalid()) {
|
||||
node = config.find_child(main_str).find_child(sub_str);
|
||||
}
|
||||
} else {
|
||||
node = config[main_str];
|
||||
}
|
||||
return {node_str, node};
|
||||
}
|
||||
|
||||
template <class DataT>
|
||||
bool get_node_impl(c4::yml::ConstNodeRef node, ryml::Parser& parser, std::string const& node_str, DataT& data) {
|
||||
if (node.invalid()) {
|
||||
std::cerr << "Node not found: " << node_str << std::endl;
|
||||
throw std::runtime_error("");
|
||||
}
|
||||
try {
|
||||
decode(node, data);
|
||||
return true;
|
||||
} catch (std::exception const& e) {
|
||||
std::cerr << "Cannot parse config: " << node_str << std::endl;
|
||||
std::cerr << e.what() << std::endl;
|
||||
} catch (charge_bridge::utilities::yml_node_error const& e) {
|
||||
std::cerr << "Error source: \n"
|
||||
<< " parent " << node_str << "\n"
|
||||
<< " data " << e.m_msg << std::flush;
|
||||
print_location(e.m_node, parser);
|
||||
}
|
||||
throw std::runtime_error("");
|
||||
}
|
||||
|
||||
struct RymlCallbackInitializer {
|
||||
RymlCallbackInitializer() {
|
||||
ryml::set_callbacks({nullptr, nullptr, nullptr, yaml_error_handler});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
void parse_config_impl(c4::yml::NodeRef& config, charge_bridge_config& c, std::filesystem::path const& config_path,
|
||||
ryml::Parser& parser) {
|
||||
auto get_node = [&config, &parser](auto& data, std::string const& main, std::string const& sub = "") {
|
||||
auto [node_str, node] = find_node(config, main, sub);
|
||||
get_node_impl(node, parser, node_str, data);
|
||||
};
|
||||
|
||||
auto get_node_or_default = [&get_node, &config](auto& data, std::string const& main, std::string const& sub,
|
||||
auto fallback) {
|
||||
auto [node_str, node] = find_node(config, main, sub);
|
||||
if (node.invalid()) {
|
||||
data = fallback;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
get_node(data, main, sub);
|
||||
} catch (...) {
|
||||
data = fallback;
|
||||
}
|
||||
};
|
||||
|
||||
auto get_block = [&config, &c](std::string const& block, auto& block_cfg, auto const& ftor) {
|
||||
bool enable = false;
|
||||
auto block_str = ryml::to_csubstr(block);
|
||||
if (not config.find_child(block_str).invalid()) {
|
||||
if (config[block_str].find_child("enable").invalid()) {
|
||||
enable = true;
|
||||
} else {
|
||||
decode(config[block_str]["enable"], enable);
|
||||
}
|
||||
}
|
||||
if (enable) {
|
||||
block_cfg.emplace();
|
||||
ftor(*block_cfg, block);
|
||||
block_cfg->cb = c.cb_name;
|
||||
block_cfg->item = block;
|
||||
}
|
||||
};
|
||||
|
||||
get_node(c.cb_name, "charge_bridge", "name");
|
||||
|
||||
get_node(c.cb_remote, "charge_bridge", "ip");
|
||||
|
||||
c.cb_port = g_cb_port_management;
|
||||
|
||||
get_block("can_0", c.can0, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.can_device, main, "local");
|
||||
cfg.cb_port = g_cb_port_can0;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
get_block("serial_1", c.serial1, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.serial_device, main, "local");
|
||||
cfg.cb_port = g_cb_port_serial_1;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
get_block("serial_2", c.serial2, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.serial_device, main, "local");
|
||||
cfg.cb_port = g_cb_port_serial_2;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
// FIXME (JH) serial3 not availabe in first release
|
||||
// get_block("serial_3", c.serial3, [&](auto& cfg, auto const& main) {
|
||||
// get_node(main, "local", cfg.serial_device);
|
||||
// get_node(main, "port", cfg.cb_port);
|
||||
// cfg.cb_remote = c.cb_remote;
|
||||
// });
|
||||
|
||||
get_block("plc", c.plc, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.plc_tap, main, "tap");
|
||||
get_node(cfg.plc_ip, main, "ip");
|
||||
get_node(cfg.plc_netmaks, main, "netmask");
|
||||
get_node(cfg.plc_mtu, main, "mtu");
|
||||
cfg.cb_port = g_cb_port_plc;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
{
|
||||
bool wants_ev = false;
|
||||
bool wants_evse = false;
|
||||
get_node_or_default(wants_ev, "ev_bsp", "enable", false);
|
||||
get_node_or_default(wants_evse, "evse_bsp", "enable", false);
|
||||
if (wants_ev && wants_evse) {
|
||||
std::cerr << "Configuration error: Cannot enable EVSE and EV BSP at the same time" << std::endl;
|
||||
throw std::exception();
|
||||
}
|
||||
}
|
||||
|
||||
get_block("evse_bsp", c.bsp, [&](auto& cfg, auto const& main) {
|
||||
cfg.cb_port = g_cb_port_evse_bsp;
|
||||
cfg.api.evse.enabled = true;
|
||||
get_node(cfg.api.evse.module_id, main, "module_id");
|
||||
get_node(cfg.api.mqtt_remote, main, "mqtt_remote");
|
||||
get_node_or_default(cfg.api.mqtt_bind, main, "mqtt_bind", "");
|
||||
get_node(cfg.api.mqtt_port, main, "mqtt_port");
|
||||
get_node_or_default(cfg.api.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms",
|
||||
default_mqtt_ping_interval_ms);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
get_node(cfg.api.evse.capabilities, main, "capabilities");
|
||||
get_node(cfg.api.ovm.enabled, main, "ovm_enabled");
|
||||
get_node(cfg.api.ovm.module_id, main, "ovm_module_id");
|
||||
});
|
||||
|
||||
if (not c.bsp.has_value()) {
|
||||
get_block("ev_bsp", c.bsp, [&](auto& cfg, auto const& main) {
|
||||
cfg.cb_port = g_cb_port_evse_bsp;
|
||||
cfg.api.ev.enabled = true;
|
||||
get_node(cfg.api.ev.module_id, main, "module_id");
|
||||
get_node(cfg.api.mqtt_remote, main, "mqtt_remote");
|
||||
get_node_or_default(cfg.api.mqtt_bind, main, "mqtt_bind", "");
|
||||
get_node(cfg.api.mqtt_port, main, "mqtt_port");
|
||||
get_node_or_default(cfg.api.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms",
|
||||
default_mqtt_ping_interval_ms);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
get_node(cfg.api.ovm.enabled, main, "ovm_enabled");
|
||||
get_node(cfg.api.ovm.module_id, main, "ovm_module_id");
|
||||
});
|
||||
}
|
||||
|
||||
get_block("gpio", c.gpio, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.interval_s, main, "interval_s");
|
||||
get_node(cfg.mqtt_remote, main, "mqtt_remote");
|
||||
get_node_or_default(cfg.mqtt_bind, main, "mqtt_bind", "");
|
||||
get_node(cfg.mqtt_port, main, "mqtt_port");
|
||||
get_node_or_default(cfg.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms", default_mqtt_ping_interval_ms);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
cfg.cb_port = c.cb_port;
|
||||
});
|
||||
|
||||
get_block("heartbeat", c.heartbeat, [&](auto& cfg, auto const& main) {
|
||||
get_node_or_default(cfg.interval_s, main, "interval_s", 1);
|
||||
get_node_or_default(cfg.connection_to_s, main, "connection_to_s", 3 * cfg.interval_s);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
cfg.cb_port = c.cb_port;
|
||||
get_node(cfg.cb_config.network, "charge_bridge");
|
||||
get_node(cfg.cb_config.safety, "safety");
|
||||
|
||||
std::memset(cfg.cb_config.gpios, 0, CB_NUMBER_OF_GPIOS * sizeof(CbGpioConfig));
|
||||
std::memset(cfg.cb_config.uarts, 0, CB_NUMBER_OF_UARTS * sizeof(CbUartConfig));
|
||||
if (c.serial1) {
|
||||
get_node(cfg.cb_config.uarts[0], "serial_1");
|
||||
}
|
||||
if (c.serial2) {
|
||||
get_node(cfg.cb_config.uarts[1], "serial_2");
|
||||
}
|
||||
// FIXME (JH) serial 3 not available in first release
|
||||
// if (c.serial3) {
|
||||
// get_main_node("serial_3", cfg.cb_config.uarts[2]);
|
||||
// }
|
||||
if (c.gpio) {
|
||||
for (auto i = 0; i < CB_NUMBER_OF_GPIOS; ++i) {
|
||||
get_node(cfg.cb_config.gpios[i], "gpio", "gpio_" + std::to_string(i));
|
||||
}
|
||||
}
|
||||
if (c.can0) {
|
||||
get_node(cfg.cb_config.can, "can_0");
|
||||
}
|
||||
get_node(cfg.cb_config.plc_powersaving_mode, "plc", "powersaving_mode");
|
||||
cfg.cb_config.config_version = CB_CONFIG_VERSION;
|
||||
});
|
||||
|
||||
get_node(c.firmware.fw_path, "charge_bridge", "fw_file");
|
||||
get_node(c.firmware.fw_update_on_start, "charge_bridge", "fw_update_on_start");
|
||||
|
||||
// If the path to the firmware file is relative, make it relative to the config file
|
||||
std::filesystem::path fw_path = c.firmware.fw_path;
|
||||
if (fw_path.is_relative()) {
|
||||
c.firmware.fw_path = config_path.parent_path().append(c.firmware.fw_path);
|
||||
}
|
||||
|
||||
c.firmware.cb_remote = c.cb_remote;
|
||||
c.firmware.cb_port = c.cb_port;
|
||||
c.firmware.cb = c.cb_name;
|
||||
}
|
||||
|
||||
charge_bridge_config set_config_placeholders(charge_bridge_config const& src, charge_bridge_config& result,
|
||||
std::string const& ip, std::size_t index) {
|
||||
auto index_str = std::to_string(index);
|
||||
result = src;
|
||||
auto replace = [index_str](std::string& src) { replace_all_in_place(src, "##", index_str); };
|
||||
|
||||
result.cb_remote = ip;
|
||||
result.firmware.cb_remote = ip;
|
||||
replace(result.cb_name);
|
||||
result.firmware.cb = result.cb_name;
|
||||
if (result.can0.has_value()) {
|
||||
result.can0->cb_remote = ip;
|
||||
result.can0->cb = result.cb_name;
|
||||
replace(result.can0->can_device);
|
||||
}
|
||||
if (result.serial1.has_value()) {
|
||||
result.serial1->cb_remote = ip;
|
||||
result.serial1->cb = result.cb_name;
|
||||
replace(result.serial1->serial_device);
|
||||
}
|
||||
if (result.serial2.has_value()) {
|
||||
result.serial2->cb_remote = ip;
|
||||
result.serial2->cb = result.cb_name;
|
||||
replace(result.serial2->serial_device);
|
||||
}
|
||||
if (result.serial3.has_value()) {
|
||||
result.serial3->cb_remote = ip;
|
||||
result.serial3->cb = result.cb_name;
|
||||
replace(result.serial3->serial_device);
|
||||
}
|
||||
if (result.plc.has_value()) {
|
||||
result.plc->cb_remote = ip;
|
||||
result.plc->cb = result.cb_name;
|
||||
replace(result.plc->plc_tap);
|
||||
}
|
||||
if (result.bsp.has_value()) {
|
||||
result.bsp->cb_remote = ip;
|
||||
result.bsp->cb = result.cb_name;
|
||||
replace(result.bsp->api.evse.module_id);
|
||||
replace(result.bsp->api.ev.module_id);
|
||||
replace(result.bsp->api.ovm.module_id);
|
||||
}
|
||||
if (result.heartbeat.has_value()) {
|
||||
result.heartbeat->cb = result.cb_name;
|
||||
result.heartbeat->cb_remote = ip;
|
||||
}
|
||||
if (result.gpio.has_value()) {
|
||||
result.gpio->cb = result.cb_name;
|
||||
result.gpio->cb_remote = ip;
|
||||
}
|
||||
|
||||
if (result.heartbeat.has_value()) {
|
||||
auto& raw = result.heartbeat->cb_config.network.mdns_name;
|
||||
std::string item = raw;
|
||||
replace(item);
|
||||
auto limit = sizeof(raw);
|
||||
if (item.size() > limit) {
|
||||
item = "cb_" + index_str;
|
||||
std::cout << "WARNING: Replacement for mdns_name is too long. Fallback to '" + item + "'" << std::endl;
|
||||
}
|
||||
std::memset(raw, 0, limit);
|
||||
std::memcpy(raw, item.c_str(), std::min(item.size(), limit));
|
||||
|
||||
result.heartbeat->cb_remote = ip;
|
||||
result.heartbeat->cb = result.cb_name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<charge_bridge_config> parse_config_multi(std::string const& config_file) {
|
||||
const static RymlCallbackInitializer ryml_callback_initializer;
|
||||
|
||||
try {
|
||||
ryml::EventHandlerTree evt_handler = {};
|
||||
ryml::Parser parser(&evt_handler, ryml::ParserOptions().locations(true));
|
||||
ryml::Tree config_tree;
|
||||
load_yaml_file(config_file, &parser, &config_tree);
|
||||
c4::yml::NodeRef config = config_tree.rootref();
|
||||
if (config.invalid()) {
|
||||
std::cerr << "Config file not found: " << config_file << std::endl;
|
||||
return {};
|
||||
}
|
||||
charge_bridge_config base_config;
|
||||
parse_config_impl(config, base_config, config_file, parser);
|
||||
|
||||
auto ip_list_node = config.find_child("charge_bridge_ip_list");
|
||||
if (ip_list_node.invalid()) {
|
||||
return {base_config};
|
||||
}
|
||||
std::vector<std::string> ip_list;
|
||||
ip_list_node >> ip_list;
|
||||
std::vector<charge_bridge_config> cb_config_list(ip_list.size());
|
||||
|
||||
for (std::size_t i = 0; i < ip_list.size(); ++i) {
|
||||
set_config_placeholders(base_config, cb_config_list[i], ip_list[i], i);
|
||||
}
|
||||
|
||||
return cb_config_list;
|
||||
} catch (...) {
|
||||
std::cerr << "FAILED to parse configuration!" << std::endl;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "protocol/cb_config.h"
|
||||
#include <charge_bridge/charge_bridge.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
std::string to_string(CbCanBaudrate value) {
|
||||
switch (value) {
|
||||
case CBCBR_125000:
|
||||
return "125000";
|
||||
case CBCBR_250000:
|
||||
return "250000";
|
||||
case CBCBR_500000:
|
||||
return "500000";
|
||||
case CBCBR_1000000:
|
||||
return "1000000";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid bitrate";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartBaudrate value) {
|
||||
switch (value) {
|
||||
case CBUBR_9600:
|
||||
return "9600";
|
||||
case CBUBR_19200:
|
||||
return "19200";
|
||||
case CBUBR_38400:
|
||||
return "38400";
|
||||
case CBUBR_57600:
|
||||
return "57600";
|
||||
case CBUBR_115200:
|
||||
return "115200";
|
||||
case CBUBR_230400:
|
||||
return "230400";
|
||||
case CBUBR_250000:
|
||||
return "250000";
|
||||
case CBUBR_460800:
|
||||
return "460800";
|
||||
case CBUBR_500000:
|
||||
return "500000";
|
||||
case CBUBR_1000000:
|
||||
return "1000000";
|
||||
case CBUBR_2000000:
|
||||
return "2000000";
|
||||
case CBUBR_3000000:
|
||||
return "3000000";
|
||||
case CBUBR_4000000:
|
||||
return "4000000";
|
||||
case CBUBR_6000000:
|
||||
return "6000000";
|
||||
case CBUBR_8000000:
|
||||
return "8000000";
|
||||
case CBUBR_10000000:
|
||||
return "10000000";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid baudrate";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartParity value) {
|
||||
switch (value) {
|
||||
case CBUP_None:
|
||||
return "N";
|
||||
case CBUP_Odd:
|
||||
return "O";
|
||||
case CBUP_Even:
|
||||
return "E";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid parity";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartStopbits value) {
|
||||
switch (value) {
|
||||
case CBUS_OneStopBit:
|
||||
return "1";
|
||||
case CBUS_TwoStopBits:
|
||||
return "2";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid parity";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartConfig const& value) {
|
||||
std::stringstream data;
|
||||
data << to_string(value.baudrate) << " 8" << to_string(value.parity) << to_string(value.stopbits);
|
||||
return data.str();
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
bool string_starts_with(std::string_view const& str, std::string_view const& pattern) {
|
||||
return str.rfind(pattern, 0) == 0;
|
||||
}
|
||||
|
||||
bool string_ends_with(std::string const& str, std::string const& pattern) {
|
||||
if (pattern.size() > str.size())
|
||||
return false;
|
||||
return std::equal(pattern.rbegin(), pattern.rend(), str.rbegin());
|
||||
}
|
||||
|
||||
std::string string_after_pattern(std::string_view const& str, std::string_view const& pattern) {
|
||||
if (charge_bridge::utilities::string_starts_with(str, pattern)) {
|
||||
return static_cast<std::string>(str.substr(pattern.length()));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string& replace_all_in_place(std::string& source, std::string const& placeholder, std::string const& substitute) {
|
||||
|
||||
if (placeholder.empty()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
std::size_t start_pos = 0;
|
||||
|
||||
while ((start_pos = source.find(placeholder, start_pos)) != std::string::npos) {
|
||||
source.replace(start_pos, placeholder.length(), substitute);
|
||||
start_pos += substitute.length();
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
std::string replace_all(std::string const& source, std::string const& placeholder, std::string const& substitute) {
|
||||
std::string result = source;
|
||||
return replace_all_in_place(result, placeholder, substitute);
|
||||
}
|
||||
|
||||
std::set<std::string> csv_to_set(std::string const& str) {
|
||||
std::set<std::string> result;
|
||||
std::stringstream ss(str);
|
||||
std::string item;
|
||||
|
||||
while (std::getline(ss, item, ',')) {
|
||||
if (!item.empty()) {
|
||||
result.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/symlink.hpp>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
symlink::symlink() {
|
||||
}
|
||||
|
||||
symlink::symlink(std::string const& src, std::string const& tar) {
|
||||
auto result = set_link(src, tar);
|
||||
if (not result) {
|
||||
std::string msg = "Cannot create symbolic link from '" + src + "' to '" + tar + "'";
|
||||
perror(msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool symlink::set_link(std::string const& src, std::string const& tar) {
|
||||
m_tar = tar;
|
||||
del_link();
|
||||
auto result = ::symlink(src.c_str(), tar.c_str()) == 0;
|
||||
if (result) {
|
||||
m_tar = tar;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool symlink::del_link() {
|
||||
auto result = true;
|
||||
if (not m_tar.empty()) {
|
||||
auto code = ::unlink(m_tar.c_str());
|
||||
result = code == 0 or code == ENOENT;
|
||||
m_tar = "";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
symlink::~symlink() {
|
||||
if (not m_tar.empty()) {
|
||||
auto result = del_link();
|
||||
if (not result) {
|
||||
std::string msg = "Cannot delete symbolic link '" + m_tar + "'";
|
||||
perror(msg.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,83 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "everest/io/event/fd_event_handler.hpp"
|
||||
#include <charge_bridge/utilities/sync_udp_client.hpp>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
using reply = sync_udp_client::reply;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sync_udp_client::sync_udp_client(std::string const& remote, std::uint16_t port) : m_retries(0), m_timeout_ms(1000) {
|
||||
init(remote, port);
|
||||
}
|
||||
|
||||
sync_udp_client::sync_udp_client(std::string const& remote, std::uint16_t port, std::uint16_t retries,
|
||||
std::uint16_t timeout_ms) :
|
||||
m_retries(retries), m_timeout_ms(timeout_ms) {
|
||||
init(remote, port);
|
||||
}
|
||||
|
||||
void sync_udp_client::init(std::string const& remote, std::uint16_t port) {
|
||||
m_udp.open_as_client(remote, port);
|
||||
m_handler.register_event_handler(
|
||||
m_udp.get_fd(), [this](auto) {}, everest::lib::io::event::poll_events::read);
|
||||
}
|
||||
|
||||
reply sync_udp_client::request_reply(udp_payload const& payload) {
|
||||
return request_reply(payload, m_timeout_ms, m_retries);
|
||||
}
|
||||
|
||||
reply sync_udp_client::request_reply(udp_payload const& payload, std::uint16_t timeout_ms, std::uint16_t retries) {
|
||||
udp_payload result;
|
||||
clear_socket();
|
||||
if (not m_udp.tx(payload)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
for (std::uint16_t i = 0; i < retries; ++i) {
|
||||
if (not m_handler.poll(std::chrono::milliseconds(timeout_ms))) {
|
||||
if (not m_udp.tx(payload)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (not m_udp.rx(result)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool sync_udp_client::tx(udp_payload const& payload) {
|
||||
return m_udp.tx(payload);
|
||||
}
|
||||
|
||||
reply sync_udp_client::rx() {
|
||||
return rx(m_timeout_ms);
|
||||
}
|
||||
|
||||
reply sync_udp_client::rx(std::uint16_t timeout_ms) {
|
||||
udp_payload result;
|
||||
if (not m_handler.poll(std::chrono::milliseconds(timeout_ms))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (not m_udp.rx(result)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sync_udp_client::is_open() {
|
||||
return m_udp.is_open();
|
||||
}
|
||||
|
||||
void sync_udp_client::clear_socket() {
|
||||
udp_payload tmp;
|
||||
while (m_handler.poll(0ms)) {
|
||||
m_udp.rx(tmp);
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,610 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "protocol/cb_config.h"
|
||||
#include <charge_bridge/utilities/type_converters.hpp>
|
||||
#include <cmath>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
// clang-format off
|
||||
#include <optional>
|
||||
#include <ryml_std.hpp>
|
||||
#include <ryml.hpp>
|
||||
// clang-format on
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
template <class T> void decode(c4::yml::ConstNodeRef const& node, T& rhs) {
|
||||
using namespace charge_bridge::utilities;
|
||||
try {
|
||||
node >> rhs;
|
||||
} catch (...) {
|
||||
std::string value;
|
||||
std::string key;
|
||||
if (node.has_key()) {
|
||||
key = std::string(node.key().str, node.key().len);
|
||||
}
|
||||
if (node.has_val()) {
|
||||
value = std::string(node.val().str, node.val().len);
|
||||
}
|
||||
throw charge_bridge::utilities::yml_node_error(node, key + "::" + value);
|
||||
}
|
||||
}
|
||||
|
||||
template <class T, class TmpT>
|
||||
T decode_t(c4::yml::ConstNodeRef parent, std::string const& node_id, std::function<T(TmpT)> const& transform,
|
||||
std::optional<T> const& def_value = std::nullopt) {
|
||||
auto index = ryml::to_csubstr(node_id);
|
||||
auto local_node = parent.find_child(index);
|
||||
if (local_node.invalid()) {
|
||||
if (def_value.has_value()) {
|
||||
return def_value.value();
|
||||
}
|
||||
|
||||
throw charge_bridge::utilities::yml_node_error(parent, "Cannot find nested node: " + node_id);
|
||||
}
|
||||
TmpT tmp;
|
||||
decode(local_node, tmp);
|
||||
return transform(tmp);
|
||||
}
|
||||
|
||||
template <class Ftor, class T>
|
||||
auto decode_t(c4::yml::ConstNodeRef parent, std::string const& node_id, Ftor transform, std::optional<T> const& t) {
|
||||
std::function g = transform;
|
||||
return decode_t(parent, node_id, g, t);
|
||||
}
|
||||
|
||||
template <class Ftor> auto decode_t(c4::yml::ConstNodeRef parent, std::string const& node_id, Ftor transform) {
|
||||
std::function g = transform;
|
||||
return decode_t(parent, node_id, g);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T decode(c4::yml::ConstNodeRef parent, std::string const& node_id, std::optional<T> const& def_value = std::nullopt) {
|
||||
auto identity = [](T v) { return v; };
|
||||
return decode_t(parent, node_id, identity, def_value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
yml_node_error::yml_node_error(c4::yml::ConstNodeRef node) : m_node(node) {
|
||||
}
|
||||
|
||||
yml_node_error::yml_node_error(c4::yml::ConstNodeRef node, std::string const& msg) : m_node(node), m_msg(msg) {
|
||||
}
|
||||
|
||||
bool decode_CbGpioMode(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "Input") {
|
||||
rhs = CbGpioMode::CBG_Input;
|
||||
return true;
|
||||
} else if (value == "Output") {
|
||||
rhs = CbGpioMode::CBG_Output;
|
||||
return true;
|
||||
} else if (value == "Pwm_Input") {
|
||||
rhs = CbGpioMode::CBG_Pwm_Input;
|
||||
return true;
|
||||
} else if (value == "Pwm_Output") {
|
||||
rhs = CbGpioMode::CBG_Pwm_Output;
|
||||
return true;
|
||||
} else if (value == "RS485_2_DE") {
|
||||
rhs = CbGpioMode::CBG_RS485_2_DE;
|
||||
return true;
|
||||
} else if (value == "Rcd_Selftest_Output") {
|
||||
rhs = CbGpioMode::CBG_Rcd_Selftest_Output;
|
||||
return true;
|
||||
} else if (value == "Rcd_Error_Input") {
|
||||
rhs = CbGpioMode::CBG_Rcd_Error_Input;
|
||||
return true;
|
||||
} else if (value == "Rcd_PWM_Input") {
|
||||
rhs = CbGpioMode::CBG_Rcd_PWM_Input;
|
||||
return true;
|
||||
} else if (value == "MotorLock_1") {
|
||||
rhs = CbGpioMode::CBG_MotorLock_1;
|
||||
return true;
|
||||
} else if (value == "MotorLock_2") {
|
||||
rhs = CbGpioMode::CBG_MotorLock_2;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbGpioPulls(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "NoPull") {
|
||||
rhs = CBGP_NoPull;
|
||||
return true;
|
||||
}
|
||||
if (value == "PullUp") {
|
||||
rhs = CBGP_PullUp;
|
||||
return true;
|
||||
}
|
||||
if (value == "PullDown") {
|
||||
rhs = CBGP_PullDown;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbUartBaudrate(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "9600") {
|
||||
rhs = CBUBR_9600;
|
||||
return true;
|
||||
}
|
||||
if (value == "19200") {
|
||||
rhs = CBUBR_19200;
|
||||
return true;
|
||||
}
|
||||
if (value == "38400") {
|
||||
rhs = CBUBR_38400;
|
||||
return true;
|
||||
}
|
||||
if (value == "57600") {
|
||||
rhs = CBUBR_57600;
|
||||
return true;
|
||||
}
|
||||
if (value == "115200") {
|
||||
rhs = CBUBR_115200;
|
||||
return true;
|
||||
}
|
||||
if (value == "230400") {
|
||||
rhs = CBUBR_230400;
|
||||
return true;
|
||||
}
|
||||
if (value == "250000") {
|
||||
rhs = CBUBR_250000;
|
||||
return true;
|
||||
}
|
||||
if (value == "460800") {
|
||||
rhs = CBUBR_460800;
|
||||
return true;
|
||||
}
|
||||
if (value == "500000") {
|
||||
rhs = CBUBR_500000;
|
||||
return true;
|
||||
}
|
||||
if (value == "1000000") {
|
||||
rhs = CBUBR_1000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "2000000") {
|
||||
rhs = CBUBR_2000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "3000000") {
|
||||
rhs = CBUBR_3000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "4000000") {
|
||||
rhs = CBUBR_4000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "6000000") {
|
||||
rhs = CBUBR_6000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "8000000") {
|
||||
rhs = CBUBR_8000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "10000000") {
|
||||
rhs = CBUBR_10000000;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbUartStopbits(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "OneStopBit") {
|
||||
rhs = CBUS_OneStopBit;
|
||||
return true;
|
||||
}
|
||||
if (value == "TwoStopBits") {
|
||||
rhs = CBUS_TwoStopBits;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbUartParity(c4::yml::ConstNodeRef const& node, CbUartParity& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "None") {
|
||||
rhs = CBUP_None;
|
||||
return true;
|
||||
}
|
||||
if (value == "Odd") {
|
||||
rhs = CBUP_Odd;
|
||||
return true;
|
||||
}
|
||||
if (value == "Even") {
|
||||
rhs = CBUP_Even;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbCanBaudrate(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "125000") {
|
||||
rhs = CBCBR_125000;
|
||||
return true;
|
||||
}
|
||||
if (value == "250000") {
|
||||
rhs = CBCBR_250000;
|
||||
return true;
|
||||
}
|
||||
if (value == "500000") {
|
||||
rhs = CBCBR_500000;
|
||||
return true;
|
||||
}
|
||||
if (value == "1000000") {
|
||||
rhs = CBCBR_1000000;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw yml_node_error(node);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbRelayMode(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "PowerRelay") {
|
||||
rhs = CBR_PowerRelay;
|
||||
return true;
|
||||
}
|
||||
if (value == "UserRelay") {
|
||||
rhs = CBR_UserRelay;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbSafetyMode(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "disabled") {
|
||||
rhs = CBSM_disabled;
|
||||
return true;
|
||||
}
|
||||
if (value == "US") {
|
||||
rhs = CBSM_US;
|
||||
return true;
|
||||
}
|
||||
if (value == "EU") {
|
||||
rhs = CBSM_EU;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw yml_node_error(node);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_RelayConfig(c4::yml::ConstNodeRef const& node, RelayConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.relay_mode = decode<decltype(rhs.relay_mode)>(node, "relay_mode");
|
||||
rhs.feedback_enabled = decode_t(node, "feedback_enabled", [](bool tmp) -> uint8_t { return tmp ? 1 : 0; });
|
||||
rhs.feedback_delay_ms = decode<decltype(rhs.feedback_delay_ms)>(node, "feedback_delay_ms");
|
||||
|
||||
rhs.feedback_inverted = decode_t(node, "feedback_inverted", [](bool tmp) -> uint8_t { return tmp ? 1 : 0; });
|
||||
rhs.pwm_dc = decode_t(node, "pwm_dc", [](uint8_t tmp) { return std::min<uint8_t>(tmp, 100); });
|
||||
rhs.pwm_delay_ms = decode<decltype(rhs.pwm_delay_ms)>(node, "pwm_delay_ms");
|
||||
rhs.switchoff_delay_ms = decode<decltype(rhs.switchoff_delay_ms)>(node, "switchoff_delay_ms");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_SafetyConfig(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.pp_mode = decode<decltype(rhs.pp_mode)>(node, "pp_mode");
|
||||
rhs.cp_avg_ms = decode<decltype(rhs.cp_avg_ms)>(node, "cp_avg_ms", 10);
|
||||
rhs.temperature_limit_pt1000_C =
|
||||
decode<decltype(rhs.temperature_limit_pt1000_C)>(node, "temperature_limit_pt1000_C", 0);
|
||||
rhs.inverted_emergency_input = decode<decltype(rhs.inverted_emergency_input)>(node, "inverted_emergency_input", 0);
|
||||
|
||||
rhs.relays[0] = decode<RelayConfig>(node, "relay_1");
|
||||
rhs.relays[1] = decode<RelayConfig>(node, "relay_2");
|
||||
rhs.relays[2] = decode<RelayConfig>(node, "relay_3");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbGpioConfig(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
rhs.mode = decode<decltype(rhs.mode)>(node, "mode");
|
||||
rhs.pulls = decode<decltype(rhs.pulls)>(node, "pulls");
|
||||
rhs.strap_option_mdns_naming = decode_t(
|
||||
node, "mdns", [](bool tmp) -> uint8_t { return tmp ? 1 : 0; }, std::make_optional<uint8_t>(0));
|
||||
rhs.mode_config = decode<decltype(rhs.mode_config)>(node, "config", 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbUartConfig(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.baudrate = decode<decltype(rhs.baudrate)>(node, "baudrate");
|
||||
rhs.stopbits = decode<decltype(rhs.stopbits)>(node, "stopbits");
|
||||
rhs.parity = decode<decltype(rhs.parity)>(node, "parity");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbCanConfig(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
rhs.baudrate = decode<decltype(rhs.baudrate)>(node, "baudrate");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbNetworkConfig(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ConstNodeRef local_node = node;
|
||||
local_node = node.find_child("mdns_name");
|
||||
|
||||
if (not local_node.invalid()) {
|
||||
|
||||
auto limit = sizeof(rhs.mdns_name);
|
||||
std::string name;
|
||||
decode(local_node, name);
|
||||
|
||||
if (name.size() >= limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.size() >= limit) {
|
||||
return false;
|
||||
}
|
||||
std::memset(rhs.mdns_name, 0, limit);
|
||||
std::memcpy(rhs.mdns_name, name.c_str(), std::min(name.size(), limit));
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(local_node);
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace EXT_API = everest::lib::API;
|
||||
namespace EXT_API_BSP = EXT_API::V1_0::types::evse_board_support;
|
||||
|
||||
bool decode_Connector_type(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
c4::csubstr value_view = node.val();
|
||||
size_t total_size = 2 + value_view.size();
|
||||
std::string quoted_value;
|
||||
quoted_value.reserve(total_size);
|
||||
quoted_value += "\"";
|
||||
quoted_value.append(value_view.data(), value_view.size());
|
||||
quoted_value += "\"";
|
||||
return EXT_API::deserialize(quoted_value, rhs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_HardwareCapabilities(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.max_current_A_import = decode<decltype(rhs.max_current_A_import)>(node, "max_current_A_import");
|
||||
rhs.min_current_A_import = decode<decltype(rhs.min_current_A_import)>(node, "min_current_A_import");
|
||||
rhs.max_phase_count_import = decode<decltype(rhs.max_phase_count_import)>(node, "max_phase_count_import");
|
||||
rhs.min_phase_count_import = decode<decltype(rhs.min_phase_count_import)>(node, "min_phase_count_import");
|
||||
rhs.max_current_A_export = decode<decltype(rhs.max_current_A_export)>(node, "max_current_A_export");
|
||||
rhs.min_current_A_export = decode<decltype(rhs.min_current_A_export)>(node, "min_current_A_export");
|
||||
rhs.max_phase_count_export = decode<decltype(rhs.max_phase_count_export)>(node, "max_phase_count_export");
|
||||
rhs.min_phase_count_export = decode<decltype(rhs.min_phase_count_export)>(node, "min_phase_count_export");
|
||||
rhs.supports_changing_phases_during_charging = decode<decltype(rhs.supports_changing_phases_during_charging)>(
|
||||
node, "supports_changing_phases_during_charging");
|
||||
rhs.supports_cp_state_E = decode<decltype(rhs.supports_cp_state_E)>(node, "supports_cp_state_E", false);
|
||||
rhs.connector_type = decode<decltype(rhs.connector_type)>(node, "connector_type");
|
||||
auto tmp = decode<float>(node, "max_plug_temperature_C", NAN);
|
||||
rhs.max_plug_temperature_C = std::isnan(tmp) ? std::nullopt : std::make_optional<float>(tmp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs) {
|
||||
if (decode_CbGpioMode(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbGpioMode");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs) {
|
||||
if (decode_CbGpioPulls(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbGpioPulls");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs) {
|
||||
if (decode_CbUartBaudrate(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartBaudrate");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs) {
|
||||
if (decode_CbUartStopbits(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartStopbits");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartParity& rhs) {
|
||||
if (decode_CbUartParity(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartParity");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs) {
|
||||
if (decode_CbCanBaudrate(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbCanBaudrate");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs) {
|
||||
if (decode_CbRelayMode(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbRelayMode");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs) {
|
||||
if (decode_CbSafetyMode(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbSafetyMode");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, RelayConfig& rhs) {
|
||||
if (decode_RelayConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("RelayConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs) {
|
||||
if (decode_SafetyConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("SafetyConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs) {
|
||||
if (decode_CbGpioConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbGpioConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs) {
|
||||
if (decode_CbUartConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs) {
|
||||
if (decode_CbCanConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbCanConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs) {
|
||||
if (decode_CbNetworkConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbNetworkConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs) {
|
||||
if (decode_Connector_type(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("type");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs) {
|
||||
if (decode_HardwareCapabilities(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("HardwareCapabilities");
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
Reference in New Issue
Block a user