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