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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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;
}

View File

@@ -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.

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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!!");

View File

@@ -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!");

View File

@@ -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!!!");

View File

@@ -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!");

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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