diff --git a/applications/pionix_chargebridge/CMakeLists.txt b/applications/pionix_chargebridge/CMakeLists.txt index 23b9b90157..c42c62f35f 100644 --- a/applications/pionix_chargebridge/CMakeLists.txt +++ b/applications/pionix_chargebridge/CMakeLists.txt @@ -11,6 +11,7 @@ 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 @@ -25,7 +26,7 @@ add_executable(pionix_chargebridge src/can_bridge.cpp src/charge_bridge.cpp - src/evse_bridge.cpp + src/bsp_bridge.cpp src/gpio_bridge.cpp src/heartbeat_service.cpp src/plc_bridge.cpp @@ -62,3 +63,5 @@ 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") diff --git a/applications/pionix_chargebridge/config/config-CB-EVAL-EV.yaml b/applications/pionix_chargebridge/config/config-CB-EVAL-EV.yaml new file mode 100644 index 0000000000..5f60cb088f --- /dev/null +++ b/applications/pionix_chargebridge/config/config-CB-EVAL-EV.yaml @@ -0,0 +1,154 @@ +charge_bridge: + name: cb_eval_ev + ip: 10.10.10.43 + #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 diff --git a/applications/pionix_chargebridge/config/config-CB-EVAL-SIM.yaml b/applications/pionix_chargebridge/config/config-CB-EVAL-SIM.yaml new file mode 100644 index 0000000000..a0f1a0235e --- /dev/null +++ b/applications/pionix_chargebridge/config/config-CB-EVAL-SIM.yaml @@ -0,0 +1,153 @@ +charge_bridge: + name: cb_eval + ip: 10.10.10.20 + #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 diff --git a/applications/pionix_chargebridge/config/firmware/charge-bridge-fw_complete.cbfw b/applications/pionix_chargebridge/config/firmware/charge-bridge-fw_complete.cbfw index 8bd8b614ec..98afb7f2f7 100644 Binary files a/applications/pionix_chargebridge/config/firmware/charge-bridge-fw_complete.cbfw and b/applications/pionix_chargebridge/config/firmware/charge-bridge-fw_complete.cbfw differ diff --git a/applications/pionix_chargebridge/include/charge_bridge/evse_bridge.hpp b/applications/pionix_chargebridge/include/charge_bridge/bsp_bridge.hpp similarity index 82% rename from applications/pionix_chargebridge/include/charge_bridge/evse_bridge.hpp rename to applications/pionix_chargebridge/include/charge_bridge/bsp_bridge.hpp index ac5ab3a877..8d7c19b076 100644 --- a/applications/pionix_chargebridge/include/charge_bridge/evse_bridge.hpp +++ b/applications/pionix_chargebridge/include/charge_bridge/bsp_bridge.hpp @@ -9,7 +9,7 @@ namespace charge_bridge { -struct evse_bridge_config { +struct bsp_bridge_config { std::string cb; std::string item; std::uint16_t cb_port; @@ -17,10 +17,10 @@ struct evse_bridge_config { evse_bsp::everest_api_config api; }; -class evse_bridge : public everest::lib::io::event::fd_event_register_interface { +class bsp_bridge : public everest::lib::io::event::fd_event_register_interface { public: - evse_bridge(evse_bridge_config const& config); - ~evse_bridge() = default; + 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; diff --git a/applications/pionix_chargebridge/include/charge_bridge/charge_bridge.hpp b/applications/pionix_chargebridge/include/charge_bridge/charge_bridge.hpp index d99ff6949b..b40b77338e 100644 --- a/applications/pionix_chargebridge/include/charge_bridge/charge_bridge.hpp +++ b/applications/pionix_chargebridge/include/charge_bridge/charge_bridge.hpp @@ -3,8 +3,8 @@ #pragma once #include +#include #include -#include #include #include #include @@ -30,7 +30,7 @@ struct charge_bridge_config { std::optional serial2; std::optional serial3; std::optional plc; - std::optional evse; + std::optional bsp; std::optional heartbeat; std::optional gpio; firmware_update::fw_update_config firmware; @@ -61,7 +61,7 @@ class charge_bridge : public everest::lib::io::event::fd_event_register_interfac std::unique_ptr m_pty_1; std::unique_ptr m_pty_2; std::unique_ptr m_pty_3; - std::unique_ptr m_bsp; + std::unique_ptr m_bsp; std::unique_ptr m_plc; std::unique_ptr m_heartbeat; std::unique_ptr m_gpio; diff --git a/applications/pionix_chargebridge/include/charge_bridge/everest_api/api_connector.hpp b/applications/pionix_chargebridge/include/charge_bridge/everest_api/api_connector.hpp index 6061349954..81779b3439 100644 --- a/applications/pionix_chargebridge/include/charge_bridge/everest_api/api_connector.hpp +++ b/applications/pionix_chargebridge/include/charge_bridge/everest_api/api_connector.hpp @@ -2,6 +2,7 @@ // Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest #pragma once +#include #include #include #include @@ -27,8 +28,9 @@ struct everest_api_config { std::string mqtt_bind; uint16_t mqtt_port; uint32_t mqtt_ping_interval_ms; - evse_bsp_config bsp; + 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 { @@ -54,16 +56,21 @@ class api_connector : public everest::lib::io::event::fd_event_register_interfac std::chrono::steady_clock::time_point m_last_cb_heartbeat; everest::lib::io::event::timer_fd m_sync_timer; - std::string m_bsp_receive_topic; - std::string m_bsp_send_topic; + 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_bsp; + evse_bsp_api m_evse_bsp; ovm_api m_ovm; + ev_bsp_api m_ev_bsp; }; } // namespace charge_bridge::evse_bsp diff --git a/applications/pionix_chargebridge/include/charge_bridge/everest_api/ev_bsp_api.hpp b/applications/pionix_chargebridge/include/charge_bridge/everest_api/ev_bsp_api.hpp new file mode 100644 index 0000000000..2b5bd19899 --- /dev/null +++ b/applications/pionix_chargebridge/include/charge_bridge/everest_api/ev_bsp_api.hpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; + using rx_ftor = std::function; + using mqtt_ftor = std::function; + +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 diff --git a/applications/pionix_chargebridge/include/charge_bridge/everest_api/evse_bsp_api.hpp b/applications/pionix_chargebridge/include/charge_bridge/everest_api/evse_bsp_api.hpp index d30ce82ee0..fcee74bfcf 100644 --- a/applications/pionix_chargebridge/include/charge_bridge/everest_api/evse_bsp_api.hpp +++ b/applications/pionix_chargebridge/include/charge_bridge/everest_api/evse_bsp_api.hpp @@ -21,6 +21,7 @@ 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; }; diff --git a/applications/pionix_chargebridge/include/charge_bridge/everest_api/ovm_api.hpp b/applications/pionix_chargebridge/include/charge_bridge/everest_api/ovm_api.hpp index 658bff338c..6389045ec3 100644 --- a/applications/pionix_chargebridge/include/charge_bridge/everest_api/ovm_api.hpp +++ b/applications/pionix_chargebridge/include/charge_bridge/everest_api/ovm_api.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -18,7 +19,7 @@ namespace charge_bridge::evse_bsp { namespace API_OVM = everest::lib::API::V1_0::types::over_voltage_monitor; struct evse_ovm_config { - bool enabled; + bool enabled{false}; std::string module_id; }; diff --git a/applications/pionix_chargebridge/main.cpp b/applications/pionix_chargebridge/main.cpp index 4c4d2cc23e..b67fde3e9c 100644 --- a/applications/pionix_chargebridge/main.cpp +++ b/applications/pionix_chargebridge/main.cpp @@ -71,7 +71,7 @@ void signal_handler(int signum) { } int main(int argc, char* argv[]) { - std::cout << "PIONIX ChargeBridge (c) 2025\n" << std::endl; + std::cout << "PIONIX ChargeBridge (C) 2025-2026\n" << std::endl; std::signal(SIGINT, signal_handler); std::signal(SIGHUP, signal_handler); diff --git a/applications/pionix_chargebridge/shared/protocol/evse_bsp_cb_to_host.h b/applications/pionix_chargebridge/shared/protocol/evse_bsp_cb_to_host.h index c400e0cea2..c8fc0ec074 100644 --- a/applications/pionix_chargebridge/shared/protocol/evse_bsp_cb_to_host.h +++ b/applications/pionix_chargebridge/shared/protocol/evse_bsp_cb_to_host.h @@ -21,6 +21,7 @@ struct CB_COMPILER_ATTR_PACK evse_bsp_cb_to_host { uint32_t hv_mV; // still define handling set for uint8_t stop_charging; + uint16_t cp_duty_cycle; }; /* Enum definitions */ diff --git a/applications/pionix_chargebridge/shared/protocol/evse_bsp_host_to_cb.h b/applications/pionix_chargebridge/shared/protocol/evse_bsp_host_to_cb.h index d5598364d9..bee143d30d 100644 --- a/applications/pionix_chargebridge/shared/protocol/evse_bsp_host_to_cb.h +++ b/applications/pionix_chargebridge/shared/protocol/evse_bsp_host_to_cb.h @@ -16,6 +16,8 @@ struct CB_COMPILER_ATTR_PACK evse_bsp_host_to_cb { 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" diff --git a/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_cb_to_host_test.h b/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_cb_to_host_test.h index 7d501da473..157509bbdf 100644 --- a/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_cb_to_host_test.h +++ b/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_cb_to_host_test.h @@ -3,4 +3,4 @@ #pragma once -CB_STATIC_ASSERT(sizeof(struct evse_bsp_cb_to_host)== 11+4+4, "Wrong evse_bsp_cb_to_host size!!!"); +CB_STATIC_ASSERT(sizeof(struct evse_bsp_cb_to_host)== 11+4+4+2, "Wrong evse_bsp_cb_to_host size!!!"); diff --git a/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_host_to_cb_test.h b/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_host_to_cb_test.h index f28a734ef5..371f09e696 100644 --- a/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_host_to_cb_test.h +++ b/applications/pionix_chargebridge/shared/protocol/test/evse_bsp_host_to_cb_test.h @@ -3,4 +3,4 @@ #pragma once -CB_STATIC_ASSERT (sizeof(struct evse_bsp_host_to_cb) == 7+9+1, "Wrong evse_bsp_host_to_cb size!"); +CB_STATIC_ASSERT (sizeof(struct evse_bsp_host_to_cb) == 7+9+1+1+1, "Wrong evse_bsp_host_to_cb size!"); diff --git a/applications/pionix_chargebridge/src/evse_bridge.cpp b/applications/pionix_chargebridge/src/bsp_bridge.cpp similarity index 84% rename from applications/pionix_chargebridge/src/evse_bridge.cpp rename to applications/pionix_chargebridge/src/bsp_bridge.cpp index d181d03c51..ec9d8b9e19 100644 --- a/applications/pionix_chargebridge/src/evse_bridge.cpp +++ b/applications/pionix_chargebridge/src/bsp_bridge.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest -#include +#include #include #include #include @@ -14,7 +14,7 @@ const int default_udp_timeout_ms = 1000; namespace charge_bridge { -evse_bridge::evse_bridge(evse_bridge_config const& config) : +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); @@ -38,13 +38,13 @@ evse_bridge::evse_bridge(evse_bridge_config const& config) : }); } -void evse_bridge::handle_timer_event() { +void bsp_bridge::handle_timer_event() { if (m_udp_on_error) { m_udp.reset(); } } -bool evse_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) { +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; @@ -52,7 +52,7 @@ bool evse_bridge::register_events(everest::lib::io::event::fd_event_handler& han return result; } -bool evse_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) { +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; diff --git a/applications/pionix_chargebridge/src/charge_bridge.cpp b/applications/pionix_chargebridge/src/charge_bridge.cpp index a1082c2d50..3f189a6c9f 100644 --- a/applications/pionix_chargebridge/src/charge_bridge.cpp +++ b/applications/pionix_chargebridge/src/charge_bridge.cpp @@ -31,8 +31,8 @@ charge_bridge::charge_bridge(charge_bridge_config const& config) : m_config(conf if (config.plc.has_value()) { m_plc = std::make_unique(config.plc.value()); } - if (config.evse.has_value()) { - m_bsp = std::make_unique(config.evse.value()); + if (config.bsp.has_value()) { + m_bsp = std::make_unique(config.bsp.value()); } if (config.heartbeat.has_value()) { m_heartbeat = std::make_unique(config.heartbeat.value(), [this](bool connected) { @@ -229,16 +229,21 @@ void print_charge_bridge_config(charge_bridge_config const& c) { std::cout << " netmask " << c.plc->plc_netmaks; std::cout << " MTU " << c.plc->plc_mtu << std::endl; } - if (c.evse) { - std::cout << " * evse_bsp: " << c.evse->cb_remote << ":" << c.evse->cb_port; - std::cout << " module " << c.evse->api.bsp.module_id; - std::cout << " MQTT " << c.evse->api.mqtt_remote << ":" << c.evse->api.mqtt_port; - if (not c.evse->api.mqtt_bind.empty()) { - std::cout << " on " << c.evse->api.mqtt_bind; + 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 << " ping " << c.evse->api.mqtt_ping_interval_ms << "ms"; - if (c.evse->api.ovm.enabled) { - std::cout << " OVM module " << c.evse->api.ovm.module_id; + 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; } diff --git a/applications/pionix_chargebridge/src/everest_api/api_connector.cpp b/applications/pionix_chargebridge/src/everest_api/api_connector.cpp index 7978700069..6b0cea499d 100644 --- a/applications/pionix_chargebridge/src/everest_api/api_connector.cpp +++ b/applications/pionix_chargebridge/src/everest_api/api_connector.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include using namespace std::chrono_literals; namespace mqtt = everest::lib::io::mqtt; @@ -19,17 +21,27 @@ 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_bsp(config.bsp, cb_identifier, m_host_status), - m_ovm(config.ovm, cb_identifier, m_host_status) { + 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; - api_topics.setup(config.bsp.module_id, "evse_board_support", 1); - m_bsp_receive_topic = api_topics.everest_to_extern(""); - m_bsp_send_topic = api_topics.extern_to_everest(""); - m_bsp.set_mqtt_tx( - [this](auto const& topic, auto const& payload) { m_mqtt.publish(m_bsp_send_topic + topic, payload); }); - + 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(""); @@ -38,9 +50,17 @@ api_connector::api_connector(everest_api_config const& config, std::string const [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, "EVSE/MQTT", is_error) << msg << std::endl; + 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(); }); @@ -53,26 +73,43 @@ api_connector::api_connector(everest_api_config const& config, std::string const } bool api_connector::register_events(everest::lib::io::event::fd_event_handler& handler) { - auto result = handler.register_event_handler(&m_bsp); + 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&) { - m_bsp.sync(m_cb_connected); + 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 = handler.unregister_event_handler(&m_bsp); + 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; @@ -80,13 +117,21 @@ bool api_connector::unregister_events(everest::lib::io::event::fd_event_handler& void api_connector::set_cb_tx(tx_ftor const& handler) { m_tx = handler; - m_bsp.set_cb_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(); - m_bsp.set_cb_message(msg); - m_ovm.set_cb_message(msg); + 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() { @@ -97,14 +142,15 @@ bool api_connector::check_cb_heartbeat() { } void api_connector::handle_mqtt_connect() { - m_mqtt.subscribe(m_bsp_receive_topic + "#", - [this](auto&, auto const& topic, auto const& payload, auto, auto const&) { - auto operation = utilities::string_after_pattern(topic, m_bsp_receive_topic); - if (not operation.empty()) { - m_bsp.dispatch(operation, static_cast(payload)); - } - }); - + 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(payload)); + } + }); + } if (m_ovm_enabled) { m_mqtt.subscribe(m_ovm_receive_topic + "#", [this](auto&, auto const& topic, auto const& payload, auto, auto const&) { @@ -114,6 +160,15 @@ void api_connector::handle_mqtt_connect() { } }); } + 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(payload)); + } + }); + } } void api_connector::handle_cb_connection_state() { @@ -121,19 +176,29 @@ void api_connector::handle_cb_connection_state() { auto current = check_cb_heartbeat(); auto handle_status = [this](bool status) { if (status) { - utilities::print_error(m_cb_identifier, "EVSE/CB", 0) << "ChargeBridge connected." << std::endl; - m_bsp.clear_comm_fault(); + 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 { - m_bsp.raise_comm_fault(); + 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, "EVSE/CB", 1) << "Waiting for ChargeBridge...." << std::endl; + utilities::print_error(m_cb_identifier, "BSP/CB", 1) << "Waiting for ChargeBridge...." << std::endl; } }; if (m_cb_initial_comm_check) { diff --git a/applications/pionix_chargebridge/src/everest_api/ev_bsp_api.cpp b/applications/pionix_chargebridge/src/everest_api/ev_bsp_api.cpp new file mode 100644 index 0000000000..3fa181ab20 --- /dev/null +++ b/applications/pionix_chargebridge/src/everest_api/ev_bsp_api.cpp @@ -0,0 +1,418 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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(s.mask)) { + log << "[" << s.message << "] "; + } + } + + for (const auto& s : error_specs) { + if (next & static_cast(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; // Is this a string or an enum? + + 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(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(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 diff --git a/applications/pionix_chargebridge/src/utilities/parse_config.cpp b/applications/pionix_chargebridge/src/utilities/parse_config.cpp index 45803e393c..2b93a25641 100644 --- a/applications/pionix_chargebridge/src/utilities/parse_config.cpp +++ b/applications/pionix_chargebridge/src/utilities/parse_config.cpp @@ -89,7 +89,10 @@ std::pair find_node(c4::yml::NodeRef& config if (not sub.empty()) { node_str = node_str + "::" + sub; auto sub_str = ryml::to_csubstr(sub); - node = config.find_child(main_str).find_child(sub_str); + 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]; } @@ -206,20 +209,48 @@ void parse_config_impl(c4::yml::NodeRef& config, charge_bridge_config& c, std::f cfg.cb_remote = c.cb_remote; }); - get_block("evse_bsp", c.evse, [&](auto& cfg, auto const& main) { + { + 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; - get_node(cfg.api.bsp.module_id, main, "module_id"); + 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.bsp.capabilities, main, "capabilities"); + 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"); @@ -311,10 +342,10 @@ charge_bridge_config set_config_placeholders(charge_bridge_config const& src, ch result.plc->cb = result.cb_name; replace(result.plc->plc_tap); } - if (result.evse.has_value()) { - result.evse->cb_remote = ip; - result.evse->cb = result.cb_name; - replace(result.evse->api.bsp.module_id); + if (result.bsp.has_value()) { + result.bsp->cb_remote = ip; + result.bsp->cb = result.cb_name; + replace(result.bsp->api.evse.module_id); } if (result.heartbeat.has_value()) { result.heartbeat->cb = result.cb_name; diff --git a/config/config-CB-EVAL-DC-SIM.yaml b/config/config-CB-EVAL-DC-SIM.yaml new file mode 100644 index 0000000000..caf3c06dd9 --- /dev/null +++ b/config/config-CB-EVAL-DC-SIM.yaml @@ -0,0 +1,170 @@ +settings: + telemetry_enabled: false +active_modules: + auth: + config_module: + connection_timeout: 60 + prioritize_authorization_over_stopping_transaction: true + selection_algorithm: FindFirst + ignore_connector_faults: true + connections: + evse_manager: + - implementation_id: evse + module_id: connector_1 + token_provider: + - implementation_id: main + module_id: token_provider + token_validator: + - implementation_id: main + module_id: token_validator + module: Auth + energy_manager: + config_module: + switch_3ph1ph_while_charging_mode: Never + schedule_interval_duration: 60 + schedule_total_duration: 10 + debug: false + connections: + energy_trunk: + - implementation_id: energy_grid + module_id: grid_connection_point + module: EnergyManager + connector_1: + config_module: + ac_enforce_hlc: true + ac_hlc_enabled: true + ac_hlc_use_5percent: true + ac_nominal_voltage: 230 + charge_mode: DC + connector_id: 1 + ev_receipt_required: false + evse_id: DE*PNX*E12345*1 + has_ventilation: true + max_current_import_A: 16 + max_current_export_A: 16 + payment_enable_contract: false + payment_enable_eim: true + session_logging: true + session_logging_path: /tmp/everest-logs + session_logging_xml: false + switch_3ph1ph_delay_s: 5 + switch_3ph1ph_cp_state: X1 + cable_check_wait_below_60V_before_finish: false + hack_allow_bpt_with_iso2: true + connections: + bsp: + - implementation_id: main + module_id: cb_bsp + hlc: + - implementation_id: charger + module_id: iso15118_charger + slac: + - implementation_id: main + module_id: slac + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: iso_monitor + implementation_id: main + over_voltage_monitor: + - module_id: cb_ovm + implementation_id: main + module: EvseManager + telemetry: + id: 1 + iso_monitor: + config_implementation: + main: + selftest_success: true + resistance_F_Ohm: 900000 + module: IMDSimulator + comm_hub: + config_implementation: + main: + serial_port: /dev/cb_rs485 + baudrate: 19200 + parity: 2 + module: SerialCommHub + powersupply_dc: + module: DCSupplySimulator + grid_connection_point: + config_module: + fuse_limit_A: 16 + phase_count: 3 + connections: + energy_consumer: + - implementation_id: energy_grid + module_id: external_limits_node + module: EnergyNode + external_limits_node: + config_module: + fuse_limit_A: 16 + phase_count: 3 + connections: + energy_consumer: + - implementation_id: energy_grid + module_id: connector_1 + module: EnergyNode + iso15118_charger: + config_module: + device: cb_plc + tls_security: prohibit + supported_DIN70121: false + connections: {} + module: EvseV2G + connections: + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + persistent_store: + config_module: + sqlite_db_file_path: everest_persistent_store.db + connections: {} + module: PersistentStore + slac: + module: EvseSlac + config_implementation: + main: + device: cb_plc + token_provider: + config_implementation: + main: + timeout: 10 + token: DEADBEEF + connections: + evse: + - implementation_id: evse + module_id: connector_1 + module: DummyTokenProvider + token_validator: + config_implementation: + main: + sleep: 0.25 + validation_reason: Token seems valid + validation_result: Accepted + connections: {} + module: DummyTokenValidator + cb_bsp: + connections: {} + config_module: + cfg_heartbeat_interval_ms: 500 + module: evse_board_support_API + cb_ovm: + connections: {} + config_module: + cfg_heartbeat_interval_ms: 500 + module: over_voltage_monitor_API + external_limits: + connections: + energy_node: + - implementation_id: external_limits + module_id: external_limits_node + config_module: + cfg_heartbeat_interval_ms: 1000 + cfg_communication_check_to_s: 0 + module: external_energy_limits_consumer_API diff --git a/config/config-CB-EVAL-DC.yaml b/config/config-CB-EVAL-DC.yaml index 8a68c5e22a..96618f91f8 100644 --- a/config/config-CB-EVAL-DC.yaml +++ b/config/config-CB-EVAL-DC.yaml @@ -115,7 +115,6 @@ active_modules: config_module: device: cb_plc tls_security: prohibit - supported_DIN70121: false connections: {} module: EvseV2G connections: diff --git a/config/config-CB-EVAL-EV.yaml b/config/config-CB-EVAL-EV.yaml new file mode 100644 index 0000000000..65d8288986 --- /dev/null +++ b/config/config-CB-EVAL-EV.yaml @@ -0,0 +1,46 @@ +settings: + mqtt_everest_prefix: everest_ev +active_modules: + ev_api: + connections: + ev_manager: + - implementation_id: ev_manager + module_id: ev_manager + module: EvAPI + iso15118_ev: + module: PyEvJosev + config_module: + device: cb_ev_plc + supported_DIN70121: true + supported_ISO15118_2: true + supported_ISO15118_20_DC: true + tls_active: false + ev_bsp_1: + module: ev_board_support_API + config_module: + cfg_heartbeat_interval_ms: 500 + ev_slac: + module: EvSlac + config_implementation: + main: + device: cb_ev_plc + ev_manager: + module: EvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: true + auto_exec_infinite: true + auto_exec_commands: wait_for_real_plugin;iso_wait_pwm_is_running;iso_wait_slac_matched;iso_start_v2g_session DC;iso_wait_pwr_ready;iso_dc_power_on;iso_wait_for_stop 60;iso_wait_v2g_session_stopped;unplug;sleep 5; + dc_target_current: 5 + dc_target_voltage: 444 + connections: + ev_board_support: + - module_id: ev_bsp_1 + implementation_id: main + ev: + - module_id: iso15118_ev + implementation_id: ev + slac: + - module_id: ev_slac + implementation_id: main diff --git a/lib/everest/everest_api_types/CMakeLists.txt b/lib/everest/everest_api_types/CMakeLists.txt index c06adf4f3d..20dc84c60a 100644 --- a/lib/everest/everest_api_types/CMakeLists.txt +++ b/lib/everest/everest_api_types/CMakeLists.txt @@ -47,6 +47,10 @@ target_sources(everest_api_types src/everest_api_types/error_history/json_codec.cpp src/everest_api_types/error_history/wrapper.cpp + src/everest_api_types/ev_board_support/codec.cpp + src/everest_api_types/ev_board_support/json_codec.cpp + src/everest_api_types/ev_board_support/wrapper.cpp + src/everest_api_types/evse_board_support/codec.cpp src/everest_api_types/evse_board_support/json_codec.cpp src/everest_api_types/evse_board_support/wrapper.cpp diff --git a/lib/everest/everest_api_types/include/everest_api_types/ev_board_support/API.hpp b/lib/everest/everest_api_types/include/everest_api_types/ev_board_support/API.hpp new file mode 100644 index 0000000000..285bd9cadd --- /dev/null +++ b/lib/everest/everest_api_types/include/everest_api_types/ev_board_support/API.hpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include +#include +#include + +namespace everest::lib::API::V1_0::types::ev_board_support { + +enum class EvCpState { + A, + B, + C, + D, + E, +}; + +struct BspMeasurement { + evse_board_support::ProximityPilot proximity_pilot; + float cp_pwm_duty_cycle; + std::optional rcd_current_mA; +}; + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/everest_api_types/include/everest_api_types/ev_board_support/codec.hpp b/lib/everest/everest_api_types/include/everest_api_types/ev_board_support/codec.hpp new file mode 100644 index 0000000000..52092b0e99 --- /dev/null +++ b/lib/everest/everest_api_types/include/everest_api_types/ev_board_support/codec.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include "API.hpp" +#include +#include + +namespace everest::lib::API::V1_0::types::ev_board_support { + +std::string serialize(EvCpState val) noexcept; +std::string serialize(BspMeasurement const& val) noexcept; + +std::ostream& operator<<(std::ostream& os, EvCpState const& val); +std::ostream& operator<<(std::ostream& os, BspMeasurement const& val); + +template T deserialize(std::string const& val); +template std::optional try_deserialize(std::string const& val) { + try { + return deserialize(val); + } catch (...) { + return std::nullopt; + } +} +template bool adl_deserialize(std::string const& json_data, T& obj) { + auto opt = try_deserialize(json_data); + if (opt) { + obj = opt.value(); + return true; + } + return false; +} + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/everest_api_types/private_include/everest_api_types/ev_board_support/json_codec.hpp b/lib/everest/everest_api_types/private_include/everest_api_types/ev_board_support/json_codec.hpp new file mode 100644 index 0000000000..a8ba6f7fe2 --- /dev/null +++ b/lib/everest/everest_api_types/private_include/everest_api_types/ev_board_support/json_codec.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#pragma once + +#include "nlohmann/json_fwd.hpp" +#include + +namespace everest::lib::API::V1_0::types::ev_board_support { + +using json = nlohmann::json; + +void to_json(json& j, EvCpState const& k) noexcept; +void from_json(const json& j, EvCpState& k); + +void to_json(json& j, BspMeasurement const& k) noexcept; +void from_json(const json& j, BspMeasurement& k); + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/everest_api_types/private_include/everest_api_types/ev_board_support/wrapper.hpp b/lib/everest/everest_api_types/private_include/everest_api_types/ev_board_support/wrapper.hpp new file mode 100644 index 0000000000..091fe3ea56 --- /dev/null +++ b/lib/everest/everest_api_types/private_include/everest_api_types/ev_board_support/wrapper.hpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wignored-qualifiers" +#pragma GCC diagnostic ignored "-Wunused-function" +#include "generated/types/board_support_common.hpp" +#include "generated/types/ev_board_support.hpp" +#pragma GCC diagnostic pop + +namespace everest::lib::API::V1_0::types::ev_board_support { + +using EvCpState_Internal = ::types::ev_board_support::EvCpState; +using EvCpState_External = EvCpState; + +EvCpState_Internal to_internal_api(EvCpState_External const& val); +EvCpState_External to_external_api(EvCpState_Internal const& val); + +using BspMeasurement_Internal = ::types::board_support_common::BspMeasurement; +using BspMeasurement_External = BspMeasurement; + +BspMeasurement_Internal to_internal_api(BspMeasurement_External const& val); +BspMeasurement_External to_external_api(BspMeasurement_Internal const& val); + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/codec.cpp b/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/codec.cpp new file mode 100644 index 0000000000..325fb2b3d9 --- /dev/null +++ b/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/codec.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include "ev_board_support/codec.hpp" +#include "ev_board_support/API.hpp" +#include "ev_board_support/json_codec.hpp" +#include "nlohmann/json.hpp" +#include "utilities/constants.hpp" +#include + +namespace everest::lib::API::V1_0::types::ev_board_support { + +std::string serialize(EvCpState val) noexcept { + json result = val; + return result.dump(json_indent); +} + +std::string serialize(BspMeasurement const& val) noexcept { + json result = val; + return result.dump(json_indent); +} + +std::ostream& operator<<(std::ostream& os, EvCpState const& val) { + os << serialize(val); + return os; +} + +std::ostream& operator<<(std::ostream& os, const BspMeasurement& val) { + os << serialize(val); + return os; +} + +template <> EvCpState deserialize(std::string const& s) { + auto data = json::parse(s); + EvCpState result = data; + return result; +} + +template <> BspMeasurement deserialize(std::string const& s) { + auto data = json::parse(s); + BspMeasurement result = data; + return result; +} + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/json_codec.cpp b/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/json_codec.cpp new file mode 100644 index 0000000000..1b2b47a4c2 --- /dev/null +++ b/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/json_codec.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include "evse_board_support/json_codec.hpp" +#include "ev_board_support/API.hpp" +#include "ev_board_support/codec.hpp" +#include "ev_board_support/json_codec.hpp" +#include "evse_board_support/API.hpp" +#include "nlohmann/json.hpp" +#include + +namespace everest::lib::API::V1_0::types::ev_board_support { + +void to_json(json& j, EvCpState const& k) noexcept { + switch (k) { + case EvCpState::A: + j = "A"; + return; + case EvCpState::B: + j = "B"; + return; + case EvCpState::C: + j = "C"; + return; + case EvCpState::D: + j = "D"; + return; + case EvCpState::E: + j = "E"; + return; + } + j = "INVALID_VALUE__everest::lib::API::V1_0::types::ev_board_support::EvCpState"; +} + +void from_json(json const& j, EvCpState& k) { + std::string s = j; + if (s == "A") { + k = EvCpState::A; + return; + } + if (s == "B") { + k = EvCpState::B; + return; + } + if (s == "C") { + k = EvCpState::C; + return; + } + if (s == "D") { + k = EvCpState::D; + return; + } + if (s == "E") { + k = EvCpState::E; + return; + } + + throw std::out_of_range( + "Provided string " + s + + " could not be converted to enum of type everest::lib::API::1_0::types::ev_board_support::EvCpState"); +} + +void to_json(json& j, BspMeasurement const& k) noexcept { + j = json{ + {"proximity_pilot", k.proximity_pilot}, + {"cp_pwm_duty_cycle", k.cp_pwm_duty_cycle}, + }; + if (k.rcd_current_mA) { + j["rcd_current_mA"] = k.rcd_current_mA.value(); + } +} + +void from_json(json const& j, BspMeasurement& k) { + k.proximity_pilot = j.at("proximity_pilot"); + k.cp_pwm_duty_cycle = j.at("cp_pwm_duty_cycle"); + + if (j.contains("rcd_current_mA")) { + k.rcd_current_mA.emplace(j.at("rcd_current_mA")); + } +} + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/wrapper.cpp b/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/wrapper.cpp new file mode 100644 index 0000000000..4215a5a98f --- /dev/null +++ b/lib/everest/everest_api_types/src/everest_api_types/ev_board_support/wrapper.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include "ev_board_support/wrapper.hpp" +#include "ev_board_support/API.hpp" +#include "evse_board_support/wrapper.hpp" + +namespace everest::lib::API::V1_0::types::ev_board_support { + +EvCpState_Internal to_internal_api(EvCpState_External const& val) { + using SrcT = EvCpState_External; + using TarT = EvCpState_Internal; + + switch (val) { + case SrcT::A: + return TarT::A; + case SrcT::B: + return TarT::B; + case SrcT::C: + return TarT::C; + case SrcT::D: + return TarT::D; + case SrcT::E: + return TarT::E; + } + + throw std::out_of_range( + "Unexpected value for everest::lib::API::V1_0::types::ev_board_support::EvCpState_External"); +} + +EvCpState_External to_external_api(EvCpState_Internal const& val) { + using SrcT = EvCpState_Internal; + using TarT = EvCpState_External; + + switch (val) { + case SrcT::A: + return TarT::A; + case SrcT::B: + return TarT::B; + case SrcT::C: + return TarT::C; + case SrcT::D: + return TarT::D; + case SrcT::E: + return TarT::E; + } + + throw std::out_of_range( + "Unexpected value for everest::lib::API::V1_0::types::ev_board_support::EvCpState_Internal"); +} + +BspMeasurement_Internal to_internal_api(BspMeasurement_External const& val) { + BspMeasurement_Internal result; + + result.proximity_pilot = evse_board_support::to_internal_api(val.proximity_pilot); + result.cp_pwm_duty_cycle = val.cp_pwm_duty_cycle; + result.rcd_current_mA = val.rcd_current_mA; + + return result; +} + +BspMeasurement_External to_external_api(BspMeasurement_Internal const& val) { + BspMeasurement_External result; + + result.proximity_pilot = evse_board_support::to_external_api(val.proximity_pilot); + result.cp_pwm_duty_cycle = val.cp_pwm_duty_cycle; + result.rcd_current_mA = val.rcd_current_mA; + + return result; +} + +} // namespace everest::lib::API::V1_0::types::ev_board_support diff --git a/lib/everest/slac/fsm/ev/include/everest/slac/fsm/ev/context.hpp b/lib/everest/slac/fsm/ev/include/everest/slac/fsm/ev/context.hpp index 75c0347028..9d88f314b4 100644 --- a/lib/everest/slac/fsm/ev/include/everest/slac/fsm/ev/context.hpp +++ b/lib/everest/slac/fsm/ev/include/everest/slac/fsm/ev/context.hpp @@ -63,6 +63,9 @@ struct Context { // MAC address of our PLC modem (EV side) uint8_t plc_mac[ETH_ALEN] = {0x00, 0xB0, 0x52, 0x00, 0x00, 0x01}; + // MAC address to use for SET KEY req + uint8_t plc_mac_chip_commands[ETH_ALEN] = {0x00, 0xB0, 0x52, 0x00, 0x00, 0x01}; + // event specific payloads // FIXME (aw): due to the synchroneous nature of the fsm, this could be even a ptr/ref slac::messages::HomeplugMessage slac_message; diff --git a/lib/everest/slac/fsm/ev/src/states/others.cpp b/lib/everest/slac/fsm/ev/src/states/others.cpp index 903b07f455..3c9e3bfba2 100644 --- a/lib/everest/slac/fsm/ev/src/states/others.cpp +++ b/lib/everest/slac/fsm/ev/src/states/others.cpp @@ -250,7 +250,7 @@ void JoinNetworkState::enter() { msg.new_eks = slac::defs::CM_SET_KEY_REQ_PEKS_NMK_KNOWN_TO_STA; memcpy(msg.new_key, nmk, sizeof(msg.new_key)); - ctx.send_slac_message(ctx.plc_mac, msg); + ctx.send_slac_message(ctx.plc_mac_chip_commands, msg); timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(SET_KEY_TIMEOUT_MS); } diff --git a/modules/API/CMakeLists.txt b/modules/API/CMakeLists.txt index e445e7f796..91166e3adc 100644 --- a/modules/API/CMakeLists.txt +++ b/modules/API/CMakeLists.txt @@ -6,6 +6,7 @@ ev_add_module(auth_token_validator_API) ev_add_module(dc_external_derate_consumer_API) ev_add_module(display_message_API) ev_add_module(error_history_consumer_API) +ev_add_module(ev_board_support_API) ev_add_module(evse_board_support_API) ev_add_module(evse_manager_consumer_API) ev_add_module(external_energy_limits_consumer_API) diff --git a/modules/API/ev_board_support_API/CMakeLists.txt b/modules/API/ev_board_support_API/CMakeLists.txt new file mode 100644 index 0000000000..0448f67cb4 --- /dev/null +++ b/modules/API/ev_board_support_API/CMakeLists.txt @@ -0,0 +1,31 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +# insert your custom targets and additional config variables here + +target_compile_options(${MODULE_NAME} + PUBLIC -Wall -Wextra -pedantic -Werror=switch) + +target_link_libraries(${MODULE_NAME} + PRIVATE + atomic + everest::everest_api_types +) + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/ev_board_supportImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/API/ev_board_support_API/docs/index.rst b/modules/API/ev_board_support_API/docs/index.rst new file mode 100644 index 0000000000..318f4ff8b7 --- /dev/null +++ b/modules/API/ev_board_support_API/docs/index.rst @@ -0,0 +1,20 @@ +:orphan: + +.. _everest_modules_handwritten_ev_board_support_API: + +.. This file is a placeholder for optional multiple files + handwritten documentation for the ev_board_support_API module. + +.. This handwritten documentation is optional. In case + you do not want to write it, you can delete the doc/ directory. + +.. The documentation can be written in reStructuredText, + and will be converted to HTML and PDF by Sphinx. + This index.rst file is the entry point for the module documentation. + +******************************************* +ev_board_support_API +******************************************* + +:ref:`Link ` to the module's reference. +API for EV board support diff --git a/modules/API/ev_board_support_API/ev_board_support_API.cpp b/modules/API/ev_board_support_API/ev_board_support_API.cpp new file mode 100644 index 0000000000..555e209e5c --- /dev/null +++ b/modules/API/ev_board_support_API/ev_board_support_API.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "ev_board_support_API.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "everest_api_types/ev_board_support/API.hpp" +#include "utils/error.hpp" + +namespace module { + +namespace API_types = ev_API::V1_0::types; +namespace API_generic = API_types::generic; +using ev_API::deserialize; + +void ev_board_support_API::init() { + invoke_init(*p_main); + + topics.setup(info.id, "ev_board_support", 1); +} + +void ev_board_support_API::ready() { + invoke_ready(*p_main); + + generate_api_var_bsp_event(); + generate_api_var_bsp_measurement(); + generate_api_var_ev_info(); + generate_api_var_communication_check(); + + generate_api_var_raise_error(); + generate_api_var_clear_error(); + + comm_check.start(config.cfg_communication_check_to_s); + setup_heartbeat_generator(); +} + +void ev_board_support_API::generate_api_var_bsp_event() { + subscribe_api_topic("bsp_event", [=](std::string const& data) { + API_types::evse_board_support::BspEvent ext; + if (deserialize(data, ext)) { + p_main->publish_bsp_event(to_internal_api(ext)); + return true; + } + return false; + }); +} + +void ev_board_support_API::generate_api_var_bsp_measurement() { + subscribe_api_topic("bsp_measurement", [=](std::string const& data) { + API_types::ev_board_support::BspMeasurement ext; + if (deserialize(data, ext)) { + p_main->publish_bsp_measurement(to_internal_api(ext)); + return true; + } + return false; + }); +} + +void ev_board_support_API::generate_api_var_ev_info() { + subscribe_api_topic("ev_info", [=](std::string const& data) { + API_types::evse_manager::EVInfo ext; + if (deserialize(data, ext)) { + p_main->publish_ev_info(to_internal_api(ext)); + return true; + } + return false; + }); +} + +void ev_board_support_API::generate_api_var_communication_check() { + subscribe_api_topic("communication_check", [this](std::string const& data) { + bool val = false; + if (deserialize(data, val)) { + comm_check.set_value(val); + return true; + } + return false; + }); +} + +void ev_board_support_API::generate_api_var_raise_error() { + subscribe_api_topic("raise_error", [=](std::string const& data) { + API_types::generic::Error error; + if (deserialize(data, error)) { + auto sub_type_str = error.sub_type ? error.sub_type.value() : ""; + auto message_str = error.message ? error.message.value() : ""; + auto error_str = make_error_string(error); + auto ev_error = p_main->error_factory->create_error(error_str, sub_type_str, message_str, + Everest::error::Severity::High); + p_main->raise_error(ev_error); + return true; + } + return false; + }); +} + +void ev_board_support_API::generate_api_var_clear_error() { + subscribe_api_topic("clear_error", [=](std::string const& data) { + API_types::generic::Error error; + if (deserialize(data, error)) { + std::string error_str = make_error_string(error); + if (error.sub_type) { + p_main->clear_error(error_str, error.sub_type.value()); + } else { + p_main->clear_error(error_str); + } + return true; + } + return false; + }); +} + +std::string ev_board_support_API::make_error_string(API_types::generic::Error const& error) { + auto error_str = API_generic::trimmed(serialize(error.type)); + auto result = "generic/" + error_str; + return result; +} + +void ev_board_support_API::setup_heartbeat_generator() { + auto topic = topics.everest_to_extern("heartbeat"); + auto action = [this, topic]() { + mqtt.publish(topic, API_generic::serialize(hb_id++)); + return true; + }; + comm_check.heartbeat(config.cfg_heartbeat_interval_ms, action); +} + +void ev_board_support_API::subscribe_api_topic(std::string const& var, ParseAndPublishFtor const& parse_and_publish) { + auto topic = topics.extern_to_everest(var); + mqtt.subscribe(topic, [=](std::string const& data) { + try { + if (not parse_and_publish(data)) { + EVLOG_warning << "Invalid data: Deserialization failed.\n" << topic << "\n" << data; + } + } catch (const std::exception& e) { + EVLOG_warning << "Topic: '" << topic << "' failed with -> " << e.what() << "\n => " << data; + } catch (...) { + EVLOG_warning << "Invalid data: Failed to parse JSON or to get data from it.\n" << topic; + } + }); +} + +const ev_API::Topics& ev_board_support_API::get_topics() const { + return topics; +} + +} // namespace module diff --git a/modules/API/ev_board_support_API/ev_board_support_API.hpp b/modules/API/ev_board_support_API/ev_board_support_API.hpp new file mode 100644 index 0000000000..82e3252736 --- /dev/null +++ b/modules/API/ev_board_support_API/ev_board_support_API.hpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef EV_BOARD_SUPPORT_API_HPP +#define EV_BOARD_SUPPORT_API_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wignored-qualifiers" +#pragma GCC diagnostic ignored "-Wunused-function" +#include +#pragma GCC diagnostic pop + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +#include +#include +#include +#include +#include + +namespace ev_API = everest::lib::API; +namespace ev_API_v = everest::lib::API::V1_0; + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + int cfg_communication_check_to_s; + int cfg_heartbeat_interval_ms; +}; + +class ev_board_support_API : public Everest::ModuleBase { +public: + ev_board_support_API() = delete; + ev_board_support_API(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, + std::unique_ptr p_main, Conf& config) : + ModuleBase(info), + mqtt(mqtt_provider), + p_main(std::move(p_main)), + config(config), + comm_check("generic/CommunicationFault", "Bridge to implementation connection lost", this->p_main){}; + + Everest::MqttProvider& mqtt; + const std::shared_ptr p_main; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + const ev_API::Topics& get_topics() const; + ev_API::CommCheckHandler comm_check; + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + + using ParseAndPublishFtor = std::function; + using HandleErrorFtor = std::function; + struct ErrorHandler { + HandleErrorFtor raiser; + HandleErrorFtor clearer; + std::string error_id; + }; + void subscribe_api_topic(std::string const& var, ParseAndPublishFtor const& parse_and_publish); + void generate_api_var_bsp_event(); + void generate_api_var_bsp_measurement(); + void generate_api_var_ev_info(); + void generate_api_var_communication_check(); + + void generate_api_var_raise_error(); + void generate_api_var_clear_error(); + + std::string make_error_string(ev_API_v::types::generic::Error const& error); + + void setup_heartbeat_generator(); + + ev_API::Topics topics; + size_t hb_id{0}; + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // EV_BOARD_SUPPORT_API_HPP diff --git a/modules/API/ev_board_support_API/main/ev_board_supportImpl.cpp b/modules/API/ev_board_support_API/main/ev_board_supportImpl.cpp new file mode 100644 index 0000000000..7bb43bde4f --- /dev/null +++ b/modules/API/ev_board_support_API/main/ev_board_supportImpl.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "ev_board_supportImpl.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace everest::lib::API; +namespace generic = everest::lib::API::V1_0::types::generic; +namespace API_types = ev_API::V1_0::types; + +namespace module { +namespace main { + +void ev_board_supportImpl::init() { +} + +void ev_board_supportImpl::ready() { +} + +void ev_board_supportImpl::handle_enable(bool& value) { + auto topic = mod->get_topics().everest_to_extern("enable"); + auto data = generic::serialize(value); + mod->mqtt.publish(topic, data); +} + +void ev_board_supportImpl::handle_set_cp_state(types::ev_board_support::EvCpState& cp_state) { + auto topic = mod->get_topics().everest_to_extern("set_cp_state"); + auto ext = API_types::ev_board_support::to_external_api(cp_state); + auto data = serialize(ext); + mod->mqtt.publish(topic, data); +} + +void ev_board_supportImpl::handle_allow_power_on(bool& value) { + auto topic = mod->get_topics().everest_to_extern("allow_power_on"); + auto data = generic::serialize(value); + mod->mqtt.publish(topic, data); +} + +void ev_board_supportImpl::handle_diode_fail(bool& value) { + auto topic = mod->get_topics().everest_to_extern("diode_fail"); + auto data = generic::serialize(value); + mod->mqtt.publish(topic, data); +} + +void ev_board_supportImpl::handle_set_ac_max_current(double& current) { + auto topic = mod->get_topics().everest_to_extern("set_ac_max_current"); + auto data = generic::serialize(current); + mod->mqtt.publish(topic, data); +} + +void ev_board_supportImpl::handle_set_three_phases(bool& three_phases) { + auto topic = mod->get_topics().everest_to_extern("set_three_phases"); + auto data = generic::serialize(three_phases); + mod->mqtt.publish(topic, data); +} + +void ev_board_supportImpl::handle_set_rcd_error(double& rcd_current_mA) { + auto topic = mod->get_topics().everest_to_extern("set_rcd_error"); + auto data = generic::serialize(rcd_current_mA); + mod->mqtt.publish(topic, data); +} + +} // namespace main +} // namespace module diff --git a/modules/API/ev_board_support_API/main/ev_board_supportImpl.hpp b/modules/API/ev_board_support_API/main/ev_board_supportImpl.hpp new file mode 100644 index 0000000000..ad90888253 --- /dev/null +++ b/modules/API/ev_board_support_API/main/ev_board_supportImpl.hpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef MAIN_EV_BOARD_SUPPORT_IMPL_HPP +#define MAIN_EV_BOARD_SUPPORT_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../ev_board_support_API.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace main { + +struct Conf {}; + +class ev_board_supportImpl : public ev_board_supportImplBase { +public: + ev_board_supportImpl() = delete; + ev_board_supportImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, + Conf& config) : + ev_board_supportImplBase(ev, "main"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void handle_enable(bool& value) override; + virtual void handle_set_cp_state(types::ev_board_support::EvCpState& cp_state) override; + virtual void handle_allow_power_on(bool& value) override; + virtual void handle_diode_fail(bool& value) override; + virtual void handle_set_ac_max_current(double& current) override; + virtual void handle_set_three_phases(bool& three_phases) override; + virtual void handle_set_rcd_error(double& rcd_current_mA) override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + // insert your private definitions here + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace main +} // namespace module + +#endif // MAIN_EV_BOARD_SUPPORT_IMPL_HPP diff --git a/modules/API/ev_board_support_API/manifest.yaml b/modules/API/ev_board_support_API/manifest.yaml new file mode 100644 index 0000000000..319da35fb3 --- /dev/null +++ b/modules/API/ev_board_support_API/manifest.yaml @@ -0,0 +1,21 @@ +description: API for EV board support +config: + cfg_communication_check_to_s: + description: "Maximum time between two communication check events. Values <= 0 disables communication checks." + type: integer + default: 5 + cfg_heartbeat_interval_ms: + description: "Interval between two heartbeat messages send by the API. Values <= 0 disable heartbeat" + type: integer + default: 1000 + +provides: + main: + interface: ev_board_support + description: "Allows EVerest to control EV boards via API clients." + +enable_external_mqtt: true +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Jan Christoph Habig diff --git a/modules/EV/EvManager/main/car_simulatorImpl.cpp b/modules/EV/EvManager/main/car_simulatorImpl.cpp index d15d541d01..d77329b180 100644 --- a/modules/EV/EvManager/main/car_simulatorImpl.cpp +++ b/modules/EV/EvManager/main/car_simulatorImpl.cpp @@ -110,7 +110,12 @@ void car_simulatorImpl::run() { while (true) { if (enabled && execution_active) { - const auto finished = run_simulation_loop(); + bool finished = run_simulation_loop(); + + if (cancel_charging_session_flag) { + cancel_charging_session_flag = false; + finished = true; + } auto& modify_session_cmds = car_simulation->get_modify_charging_session_cmds(); if (modify_session_cmds.has_value()) { @@ -302,8 +307,7 @@ void car_simulatorImpl::subscribe_to_variables_on_init() { car_simulation->set_bsp_event(bsp_event.event); if (bsp_event.event == types::board_support_common::Event::Disconnected && car_simulation->get_state() != SimState::UNPLUGGED) { - set_execution_active(false); - car_simulation->set_state(SimState::UNPLUGGED); + cancel_charging_session(); } mod->p_ev_manager->publish_bsp_event(bsp_event); }); @@ -414,4 +418,8 @@ void car_simulatorImpl::set_execution_active(bool value) { execution_active = value; } +void car_simulatorImpl::cancel_charging_session() { + cancel_charging_session_flag = true; +} + } // namespace module::main diff --git a/modules/EV/EvManager/main/car_simulatorImpl.hpp b/modules/EV/EvManager/main/car_simulatorImpl.hpp index 68801d104b..5cfb3ee342 100644 --- a/modules/EV/EvManager/main/car_simulatorImpl.hpp +++ b/modules/EV/EvManager/main/car_simulatorImpl.hpp @@ -63,6 +63,7 @@ class car_simulatorImpl : public car_simulatorImplBase { void reset_car_simulation_defaults(); void update_command_queue(std::string& value); void set_execution_active(bool value); + void cancel_charging_session(); std::unique_ptr command_registry; @@ -71,6 +72,7 @@ class car_simulatorImpl : public car_simulatorImplBase { bool enabled{false}; std::atomic execution_active{false}; + std::atomic cancel_charging_session_flag{false}; std::atomic plugged_in{false}; size_t loop_interval_ms{}; diff --git a/modules/EV/EvSlac/main/ev_slacImpl.cpp b/modules/EV/EvSlac/main/ev_slacImpl.cpp index ccb685ad0f..50a91a93e8 100644 --- a/modules/EV/EvSlac/main/ev_slacImpl.cpp +++ b/modules/EV/EvSlac/main/ev_slacImpl.cpp @@ -59,6 +59,10 @@ void ev_slacImpl::run() { callbacks.log_error = [](const std::string& text) { EVLOG_error << "EvSlac: " << text; }; auto fsm_ctx = slac::fsm::ev::Context(callbacks); + + // Copy correct MAC address + memcpy(fsm_ctx.plc_mac, slac_io.get_mac_addr(), sizeof(fsm_ctx.plc_mac)); + // fsm_ctx.slac_config.set_key_timeout_ms = config.set_key_timeout_ms; // fsm_ctx.slac_config.ac_mode_five_percent = config.ac_mode_five_percent; // fsm_ctx.slac_config.sounding_atten_adjustment = config.sounding_attenuation_adjustment;