Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/bsp_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <cstring>
|
||||
#include <everest/io/udp/udp_payload.hpp>
|
||||
#include <iostream>
|
||||
#include <protocol/evse_bsp_cb_to_host.h>
|
||||
#include <protocol/evse_bsp_host_to_cb.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
}
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
bsp_bridge::bsp_bridge(bsp_bridge_config const& config) :
|
||||
m_api(config.api, config.cb + "/" + config.item), m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(5s);
|
||||
|
||||
m_api.set_cb_tx([this](auto& data) {
|
||||
everest::lib::io::udp::udp_payload pl;
|
||||
pl.set_message(&data, sizeof(data));
|
||||
m_udp.tx(pl);
|
||||
});
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) {
|
||||
evse_bsp_cb_to_host msg;
|
||||
std::memcpy(&msg, data.buffer.data(), data.size());
|
||||
m_api.set_cb_message(msg);
|
||||
});
|
||||
|
||||
auto identifier = config.cb + "/" + config.item;
|
||||
m_udp.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "BSP/UDP", id) << msg << std::endl;
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
}
|
||||
|
||||
void bsp_bridge::handle_timer_event() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool bsp_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.register_event_handler(&m_api) && result;
|
||||
result = handler.register_event_handler(&m_udp) && result;
|
||||
result = handler.register_event_handler(&m_timer, [this](auto&) { handle_timer_event(); }) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool bsp_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.unregister_event_handler(&m_api) && result;
|
||||
result = handler.unregister_event_handler(&m_udp) && result;
|
||||
result = handler.unregister_event_handler(&m_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,162 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/can_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/netlink/vcan_netlink_manager.hpp>
|
||||
#include <memory>
|
||||
#include <protocol/cb_can_message.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
}
|
||||
|
||||
namespace charge_bridge {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
void msg_cb_to_host(cb_can_message const& src, everest::lib::io::can::socket_can::ClientPayloadT& tar) {
|
||||
tar.set_can_id_with_flags(src.can_id, src.can_flags & CanFlags_EFF, src.can_flags & CanFlags_RTR,
|
||||
src.can_flags & CanFlags_ERR);
|
||||
tar.len8_dlc = 0;
|
||||
tar.payload.resize(src.dlc);
|
||||
std::memcpy(tar.payload.data(), src.data, src.dlc);
|
||||
}
|
||||
|
||||
void msg_host_to_cb(everest::lib::io::can::socket_can::ClientPayloadT const& src, cb_can_message& tar) {
|
||||
tar = cb_can_message_set_zero;
|
||||
tar.can_id = src.get_can_id();
|
||||
tar.can_flags = 0;
|
||||
if (src.eff_flag()) {
|
||||
tar.can_flags |= CanFlags_EFF;
|
||||
}
|
||||
if (src.rtr_flag()) {
|
||||
tar.can_flags |= CanFlags_RTR;
|
||||
}
|
||||
if (src.err_flag()) {
|
||||
tar.can_flags |= CanFlags_ERR;
|
||||
}
|
||||
tar.dlc = std::min<uint8_t>(src.payload.size(), sizeof(tar.data));
|
||||
std::memcpy(tar.data, src.payload.data(), src.payload.size());
|
||||
}
|
||||
|
||||
bool is_data_msg([[maybe_unused]] cb_can_message const& msg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
can_bridge::can_bridge(can_bridge_config const& config) :
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
|
||||
m_can_device(config.can_device),
|
||||
m_last_msg_to_cb(std::chrono::steady_clock::time_point()) {
|
||||
|
||||
auto& manager = everest::lib::io::netlink::vcan_netlink_manager::Instance();
|
||||
auto success = manager.create(config.can_device) && manager.bring_up(config.can_device);
|
||||
if (success) {
|
||||
m_can = std::make_unique<everest::lib::io::can::socket_can>(config.can_device);
|
||||
} else {
|
||||
manager.destroy(config.can_device);
|
||||
success = manager.create(config.can_device) && manager.bring_up(config.can_device);
|
||||
if (success) {
|
||||
m_can = std::make_unique<everest::lib::io::can::socket_can>(config.can_device);
|
||||
} else {
|
||||
manager.destroy(config.can_device);
|
||||
throw std::runtime_error("Failed to setup virtual CAN device: " + config.can_device);
|
||||
}
|
||||
}
|
||||
|
||||
m_can->set_rx_handler([this](auto const& data, auto&) {
|
||||
everest::lib::io::udp::udp_client::ClientPayloadT pl;
|
||||
cb_can_message msg;
|
||||
msg_host_to_cb(data, msg);
|
||||
send_can_to_udp(msg);
|
||||
});
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) {
|
||||
everest::lib::io::can::socket_can::ClientPayloadT pl;
|
||||
cb_can_message msg;
|
||||
std::memcpy(&msg, data.buffer.data(), sizeof(cb_can_message));
|
||||
|
||||
msg_cb_to_host(msg, pl);
|
||||
if (is_data_msg(msg)) {
|
||||
m_can->tx(pl);
|
||||
}
|
||||
});
|
||||
|
||||
m_identifier = config.cb + "/" + config.item;
|
||||
m_can->set_error_handler([this](auto id, auto const& msg) {
|
||||
utilities::print_error(m_identifier, "CAN/HW", id) << msg << std::endl;
|
||||
if (id not_eq 0) {
|
||||
// This is a smart pointer!! Using .reset() would delete the obj!
|
||||
m_can->reset();
|
||||
}
|
||||
});
|
||||
|
||||
m_udp.set_error_handler([this](auto id, auto const& msg) {
|
||||
utilities::print_error(m_identifier, "CAN/UDP", id) << msg << std::endl;
|
||||
if (id not_eq 0) {
|
||||
m_udp.reset();
|
||||
}
|
||||
});
|
||||
|
||||
m_heartbeat_timer.set_timeout(10s);
|
||||
}
|
||||
|
||||
can_bridge::~can_bridge() {
|
||||
auto& manager = everest::lib::io::netlink::vcan_netlink_manager::Instance();
|
||||
if (m_can) {
|
||||
m_can.reset();
|
||||
manager.destroy(m_can_device);
|
||||
}
|
||||
}
|
||||
|
||||
bool can_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.register_event_handler(m_can.get());
|
||||
result = handler.register_event_handler(&m_udp) && result;
|
||||
result = handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); }) && result;
|
||||
|
||||
if (result) {
|
||||
handler.add_action([this]() { handle_heartbeat_timer(); });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool can_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.unregister_event_handler(m_can.get());
|
||||
result = handler.unregister_event_handler(&m_udp) && result;
|
||||
result = handler.unregister_event_handler(&m_heartbeat_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void can_bridge::send_can_to_udp(cb_can_message const& msg) {
|
||||
everest::lib::io::udp::udp_client::ClientPayloadT udp_pl;
|
||||
udp_pl.buffer.resize(sizeof(cb_can_message));
|
||||
std::memcpy(udp_pl.buffer.data(), &msg, sizeof(cb_can_message));
|
||||
m_udp.tx(udp_pl);
|
||||
m_last_msg_to_cb = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void can_bridge::handle_heartbeat_timer() {
|
||||
if (m_udp.on_error()) {
|
||||
// If the connection is not available, retry soon and invalidate last hearbeat
|
||||
m_heartbeat_timer.set_timeout(250ms);
|
||||
m_last_msg_to_cb = std::chrono::steady_clock::time_point();
|
||||
return;
|
||||
} else {
|
||||
// otherwise go back to regular interval
|
||||
m_heartbeat_timer.set_timeout(10s);
|
||||
}
|
||||
auto delta = std::chrono::steady_clock::now() - m_last_msg_to_cb;
|
||||
if (delta > 10s) {
|
||||
cb_can_message msg = cb_can_message_set_zero;
|
||||
msg.packet_type = CanPacketType_Keep_Alive;
|
||||
send_can_to_udp(msg);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,388 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "protocol/cb_config.h"
|
||||
#include <charge_bridge/charge_bridge.hpp>
|
||||
#include <charge_bridge/discovery.hpp>
|
||||
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
|
||||
#include <charge_bridge/gpio_bridge.hpp>
|
||||
#include <charge_bridge/heartbeat_service.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/print_config.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <charge_bridge/utilities/sync_udp_client.hpp>
|
||||
#include <everest/io/event/fd_event_sync_interface.hpp>
|
||||
#include <everest/io/netlink/vcan_netlink_manager.hpp>
|
||||
#include <everest/util/misc/bind.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
namespace {
|
||||
std::pair<bool, std::set<std::string>> make_interface_list(std::string const& str, std::string const& pattern) {
|
||||
if (str == pattern) {
|
||||
return {false, {}};
|
||||
};
|
||||
auto raw = utilities::string_after_pattern(str, pattern).substr(1);
|
||||
if (raw.size() <= 2) {
|
||||
return {false, {}};
|
||||
}
|
||||
auto exclude = raw.substr(0, 1) == "!";
|
||||
auto items = utilities::csv_to_set(raw.substr(exclude ? 1 : 0));
|
||||
for (auto const& elem : items) {
|
||||
std::cout << elem << ", ";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
return {exclude, items};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
charge_bridge::charge_bridge(charge_bridge_config const& config) : m_config(config) {
|
||||
if (utilities::string_starts_with(config.cb_remote, "ANY_EVSE")) {
|
||||
auto params = make_interface_list(config.cb_remote, "ANY_EVSE");
|
||||
init_discovery(discovery_device_type::CB_EVSE, params.second, params.first);
|
||||
} else if (utilities::string_starts_with(config.cb_remote, "ANY_EV")) {
|
||||
auto params = make_interface_list(config.cb_remote, "ANY_EV");
|
||||
init_discovery(discovery_device_type::CB_EV, params.second, params.first);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
void charge_bridge::init_discovery(discovery_device_type type, std::set<std::string> const& interfaces,
|
||||
bool excluding) {
|
||||
using namespace everest::lib::util;
|
||||
utilities::print_error(m_config.cb_name, "DISCOVERY", -1) << "Discovery pending" << std::endl;
|
||||
|
||||
m_discovery = std::make_unique<discovery>(type, interfaces, excluding);
|
||||
m_discovery->set_discovery_callback(bind_obj(&charge_bridge::handle_discovery, this));
|
||||
{
|
||||
auto handle = m_cb_status.handle();
|
||||
handle->discovery_pending = true;
|
||||
}
|
||||
m_cb_status.notify_one();
|
||||
}
|
||||
|
||||
void charge_bridge::handle_discovery(std::string const& ip) {
|
||||
utilities::print_error(m_config.cb_name, "DISCOVERY", 0) << "Discovered at: " + ip << std::endl;
|
||||
|
||||
m_config.cb_remote = ip;
|
||||
if (m_config.can0) {
|
||||
m_config.can0->cb_remote = ip;
|
||||
}
|
||||
if (m_config.serial1) {
|
||||
m_config.serial1->cb_remote = ip;
|
||||
}
|
||||
if (m_config.serial2) {
|
||||
m_config.serial2->cb_remote = ip;
|
||||
}
|
||||
if (m_config.serial3) {
|
||||
m_config.serial3->cb_remote = ip;
|
||||
}
|
||||
if (m_config.plc) {
|
||||
m_config.plc->cb_remote = ip;
|
||||
}
|
||||
if (m_config.bsp) {
|
||||
m_config.bsp->cb_remote = ip;
|
||||
}
|
||||
if (m_config.heartbeat) {
|
||||
m_config.heartbeat->cb_remote = ip;
|
||||
}
|
||||
if (m_config.gpio) {
|
||||
m_config.gpio->cb_remote = ip;
|
||||
}
|
||||
m_config.firmware.cb_remote = ip;
|
||||
|
||||
m_event_handler->add_action([this]() {
|
||||
std::unique_ptr<discovery> tmp;
|
||||
std::swap(m_discovery, tmp);
|
||||
|
||||
init();
|
||||
{
|
||||
auto handle = m_cb_status.handle();
|
||||
handle->discovery_pending = false;
|
||||
}
|
||||
m_cb_status.notify_one();
|
||||
});
|
||||
}
|
||||
|
||||
void charge_bridge::init() {
|
||||
if (m_config.can0.has_value()) {
|
||||
m_can_0_client = std::make_unique<can_bridge>(m_config.can0.value());
|
||||
}
|
||||
if (m_config.serial1.has_value()) {
|
||||
m_pty_1 = std::make_unique<serial_bridge>(m_config.serial1.value());
|
||||
}
|
||||
if (m_config.serial2.has_value()) {
|
||||
m_pty_2 = std::make_unique<serial_bridge>(m_config.serial2.value());
|
||||
}
|
||||
if (m_config.serial3.has_value()) {
|
||||
m_pty_3 = std::make_unique<serial_bridge>(m_config.serial3.value());
|
||||
}
|
||||
if (m_config.plc.has_value()) {
|
||||
m_plc = std::make_unique<plc_bridge>(m_config.plc.value());
|
||||
}
|
||||
if (m_config.bsp.has_value()) {
|
||||
m_bsp = std::make_unique<bsp_bridge>(m_config.bsp.value());
|
||||
}
|
||||
if (m_config.heartbeat.has_value()) {
|
||||
m_heartbeat = std::make_unique<heartbeat_service>(m_config.heartbeat.value(), [this](bool connected) {
|
||||
{
|
||||
auto handle = m_cb_status.handle();
|
||||
handle->is_connected = connected;
|
||||
}
|
||||
m_cb_status.notify_one();
|
||||
});
|
||||
}
|
||||
if (m_config.gpio.has_value()) {
|
||||
m_gpio = std::make_unique<gpio_bridge>(m_config.gpio.value());
|
||||
}
|
||||
}
|
||||
|
||||
charge_bridge::~charge_bridge() {
|
||||
m_cb_status.notify_one();
|
||||
}
|
||||
|
||||
void charge_bridge::manage(everest::lib::io::event::fd_event_handler& handler, std::atomic_bool const& run,
|
||||
bool force_update) {
|
||||
using namespace std::chrono_literals;
|
||||
m_event_handler = &handler;
|
||||
m_force_firmware_update = force_update;
|
||||
|
||||
auto action = [this](bool is_connected, bool discovery_pending, int& error_count) {
|
||||
if (discovery_pending) {
|
||||
if (m_discovery_active) {
|
||||
return;
|
||||
}
|
||||
m_discovery_active = true;
|
||||
m_event_handler->add_action([this]() { register_events(*m_event_handler); });
|
||||
return;
|
||||
}
|
||||
if (m_was_connected and not is_connected) {
|
||||
if (error_count > 1) {
|
||||
m_event_handler->add_action([this]() { unregister_events(*m_event_handler); });
|
||||
m_was_connected = false;
|
||||
} else {
|
||||
error_count++;
|
||||
}
|
||||
}
|
||||
if (not m_was_connected) {
|
||||
if (update_firmware(m_force_firmware_update)) {
|
||||
m_event_handler->add_action([this]() { register_events(*m_event_handler); });
|
||||
m_was_connected = true;
|
||||
error_count = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::thread manager([&run, action, this]() {
|
||||
auto handle = m_cb_status.handle();
|
||||
bool last_is_connected = handle->is_connected;
|
||||
bool last_discovery_pending = handle->discovery_pending;
|
||||
int error_count = 0;
|
||||
auto condition = [&] {
|
||||
if (handle->is_connected not_eq last_is_connected) {
|
||||
return true;
|
||||
}
|
||||
if (handle->discovery_pending not_eq last_discovery_pending) {
|
||||
return true;
|
||||
}
|
||||
if (not run.load()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (run.load()) {
|
||||
action(handle->is_connected, handle->discovery_pending, error_count);
|
||||
handle.wait_for(condition, 10s);
|
||||
last_is_connected = handle->is_connected;
|
||||
last_discovery_pending = handle->discovery_pending;
|
||||
}
|
||||
});
|
||||
manager.detach();
|
||||
}
|
||||
|
||||
bool charge_bridge::update_firmware(bool force) {
|
||||
firmware_update::sync_fw_updater updater(m_config.firmware);
|
||||
auto is_connected = updater.quick_check_connection();
|
||||
if (not is_connected) {
|
||||
return false;
|
||||
}
|
||||
updater.print_fw_version();
|
||||
|
||||
auto do_update = force or (m_config.firmware.fw_update_on_start and not updater.check_if_correct_fw_installed());
|
||||
|
||||
if (not do_update) {
|
||||
return true;
|
||||
}
|
||||
auto result = updater.upload_fw() && updater.check_connection();
|
||||
if (not result) {
|
||||
std::cout << "Error: could not install correct firmware version" << std::endl;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string charge_bridge::get_pty_1_slave_path() {
|
||||
if (m_pty_1) {
|
||||
return m_pty_1->get_slave_path();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string charge_bridge::get_pty_2_slave_path() {
|
||||
if (m_pty_2) {
|
||||
return m_pty_2->get_slave_path();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string charge_bridge::get_pty_3_slave_path() {
|
||||
if (m_pty_3) {
|
||||
return m_pty_3->get_slave_path();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool charge_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_can_0_client) {
|
||||
result = handler.register_event_handler(m_can_0_client.get()) && result;
|
||||
}
|
||||
if (m_pty_1) {
|
||||
result = handler.register_event_handler(m_pty_1.get()) && result;
|
||||
}
|
||||
if (m_pty_2) {
|
||||
result = handler.register_event_handler(m_pty_2.get()) && result;
|
||||
}
|
||||
if (m_pty_3) {
|
||||
result = handler.register_event_handler(m_pty_3.get()) && result;
|
||||
}
|
||||
if (m_bsp) {
|
||||
result = handler.register_event_handler(m_bsp.get()) && result;
|
||||
}
|
||||
if (m_plc) {
|
||||
result = handler.register_event_handler(m_plc.get()) && result;
|
||||
}
|
||||
if (m_heartbeat) {
|
||||
result = handler.register_event_handler(m_heartbeat.get()) && result;
|
||||
}
|
||||
if (m_gpio) {
|
||||
result = handler.register_event_handler(m_gpio.get()) && result;
|
||||
}
|
||||
if (m_discovery) {
|
||||
result = handler.register_event_handler(m_discovery.get()) && result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
bool charge_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_can_0_client) {
|
||||
result = handler.unregister_event_handler(m_can_0_client.get()) && result;
|
||||
}
|
||||
if (m_pty_1) {
|
||||
result = handler.unregister_event_handler(m_pty_1.get()) && result;
|
||||
}
|
||||
if (m_pty_2) {
|
||||
result = handler.unregister_event_handler(m_pty_2.get()) && result;
|
||||
}
|
||||
if (m_pty_3) {
|
||||
result = handler.unregister_event_handler(m_pty_3.get()) && result;
|
||||
}
|
||||
if (m_bsp) {
|
||||
result = handler.unregister_event_handler(m_bsp.get()) && result;
|
||||
}
|
||||
if (m_plc) {
|
||||
result = handler.unregister_event_handler(m_plc.get()) && result;
|
||||
}
|
||||
if (m_heartbeat) {
|
||||
result = handler.unregister_event_handler(m_heartbeat.get()) && result;
|
||||
}
|
||||
if (m_gpio) {
|
||||
result = handler.unregister_event_handler(m_gpio.get()) && result;
|
||||
}
|
||||
if (m_discovery) {
|
||||
result = handler.unregister_event_handler(m_discovery.get()) && result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void charge_bridge::print_config() {
|
||||
print_charge_bridge_config(m_config);
|
||||
}
|
||||
|
||||
void print_charge_bridge_config(charge_bridge_config const& c) {
|
||||
using namespace utilities;
|
||||
std::cout << "ChargeBridge: " << c.cb_name << std::endl;
|
||||
std::cout << " * remote: " << c.cb_remote << std::endl;
|
||||
if (c.serial1) {
|
||||
std::cout << " * serial 1: " << c.serial1->serial_device;
|
||||
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 1) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[0]);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.serial2) {
|
||||
std::cout << " * serial 2: " << c.serial2->serial_device;
|
||||
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 2) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[1]);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.serial3) {
|
||||
std::cout << " * serial 3: " << c.serial3->serial_device;
|
||||
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 3) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[2]);
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.can0) {
|
||||
std::cout << " * can 0: " << c.can0->can_device;
|
||||
if (c.heartbeat.has_value()) {
|
||||
std::cout << " " << to_string(c.heartbeat->cb_config.can.baudrate) << "bps" << std::endl;
|
||||
}
|
||||
}
|
||||
if (c.plc) {
|
||||
std::cout << " * plc: " << c.plc->plc_tap << std::flush;
|
||||
std::cout << " " << c.cb_remote << ":" << c.plc->cb_port;
|
||||
std::cout << " adress " << c.plc->plc_ip;
|
||||
std::cout << " netmask " << c.plc->plc_netmaks;
|
||||
std::cout << " MTU " << c.plc->plc_mtu << std::endl;
|
||||
}
|
||||
if (c.bsp) {
|
||||
if (c.bsp->api.evse.enabled) {
|
||||
std::cout << " * evse_bsp: ";
|
||||
} else if (c.bsp->api.ev.enabled) {
|
||||
std::cout << " * ev_bsp: ";
|
||||
}
|
||||
std::cout << c.bsp->cb_remote << ":" << c.bsp->cb_port;
|
||||
std::cout << " module " << c.bsp->api.evse.module_id;
|
||||
std::cout << " MQTT " << c.bsp->api.mqtt_remote << ":" << c.bsp->api.mqtt_port;
|
||||
if (not c.bsp->api.mqtt_bind.empty()) {
|
||||
std::cout << " on " << c.bsp->api.mqtt_bind;
|
||||
}
|
||||
std::cout << " ping " << c.bsp->api.mqtt_ping_interval_ms << "ms";
|
||||
if (c.bsp->api.ovm.enabled) {
|
||||
std::cout << " OVM module " << c.bsp->api.ovm.module_id;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
if (c.heartbeat) {
|
||||
std::cout << " * heartbeat: " << c.cb_remote << ":" << c.cb_port;
|
||||
std::cout << " heartbeat interval " << c.heartbeat->interval_s << "s" << std::endl;
|
||||
}
|
||||
if (c.gpio) {
|
||||
std::cout << " * gpio: " << c.cb_remote << ":" << c.cb_port;
|
||||
std::cout << " MQTT " << c.gpio->mqtt_remote << ":" << c.gpio->mqtt_port;
|
||||
if (not c.gpio->mqtt_bind.empty()) {
|
||||
std::cout << " on " << c.gpio->mqtt_bind;
|
||||
}
|
||||
std::cout << " send interval " << c.gpio->interval_s << "s" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "\n" << std::endl;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,122 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "charge_bridge/utilities/string.hpp"
|
||||
#include <charge_bridge/discovery.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <type_traits>
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
namespace {
|
||||
std::string to_string(discovery_device_type val) {
|
||||
switch (val) {
|
||||
case discovery_device_type::CB_EV:
|
||||
return "CB-CCS-EV-LU";
|
||||
|
||||
case discovery_device_type::CB_EVSE:
|
||||
return "CB-CCS-EVSE-LU";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
|
||||
bool is_cb_match(std::string const& board_type, discovery_device_type discriminator) {
|
||||
auto result = board_type == to_string(discriminator);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::string discovery::discovery_id = "_chargebridge._udp.local";
|
||||
|
||||
discovery::discovery(discovery_device_type type) : m_type(type) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(1s);
|
||||
|
||||
for (auto const& item : everest::lib::io::socket::get_all_interaces()) {
|
||||
add_client(item.name);
|
||||
}
|
||||
}
|
||||
|
||||
discovery::discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding) :
|
||||
m_type(type) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(1s);
|
||||
|
||||
for (auto const& item : everest::lib::io::socket::get_all_interaces()) {
|
||||
if (not interfaces.empty()) {
|
||||
if (interfaces.count(item.name) == 1 and excluding) {
|
||||
continue;
|
||||
}
|
||||
if (interfaces.count(item.name) == 0 and not excluding) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
std::cout << " using interface: " << item.name << std::endl;
|
||||
add_client(item.name);
|
||||
}
|
||||
}
|
||||
|
||||
void discovery::add_client(std::string const& interface) {
|
||||
auto client = std::make_unique<everest::lib::io::mdns::mdns_client>(interface);
|
||||
client->set_rx_handler([&](auto const& data, auto&) {
|
||||
auto discovery = everest::lib::io::mdns::parse_mdns_packet(data.buffer);
|
||||
if (discovery.has_value()) {
|
||||
if (m_registry.update(discovery.value())) {
|
||||
query_registry();
|
||||
}
|
||||
}
|
||||
});
|
||||
m_mdns.push_back(std::move(client));
|
||||
}
|
||||
|
||||
void discovery::query_registry() {
|
||||
auto obj = m_registry.get();
|
||||
for (auto const& [key, value] : obj) {
|
||||
if (not utilities::string_ends_with(key, discovery_id)) {
|
||||
continue;
|
||||
}
|
||||
if (not value.txt.count("board_type") or not is_cb_match(value.txt.at("board_type"), m_type)) {
|
||||
continue;
|
||||
}
|
||||
if (not m_on_discover) {
|
||||
continue;
|
||||
}
|
||||
m_on_discover(value.ip);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void discovery::set_discovery_callback(discovery_cb const& cb) {
|
||||
m_on_discover = cb;
|
||||
}
|
||||
|
||||
bool discovery::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
for (auto& item : m_mdns) {
|
||||
if (item) {
|
||||
result = handler.register_event_handler(item.get()) && result;
|
||||
}
|
||||
}
|
||||
handler.register_event_handler(&m_timer, [&](auto) {
|
||||
for (auto& item : m_mdns) {
|
||||
item->get_raw_handler()->query(discovery_id);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool discovery::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
for (auto& item : m_mdns) {
|
||||
if (item) {
|
||||
result = handler.unregister_event_handler(item.get()) && result;
|
||||
}
|
||||
}
|
||||
handler.unregister_event_handler(&m_timer);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,214 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "charge_bridge/utilities/string.hpp"
|
||||
#include "everest/io/mqtt/mosquitto_cpp.hpp"
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/api_connector.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
namespace mqtt = everest::lib::io::mqtt;
|
||||
|
||||
namespace {
|
||||
const int mqtt_reconnect_to_ms = 1000;
|
||||
}
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
api_connector::api_connector(everest_api_config const& config, std::string const& cb_identifier) :
|
||||
m_cb_identifier(cb_identifier),
|
||||
m_mqtt(mqtt_reconnect_to_ms),
|
||||
m_evse_bsp(config.evse, cb_identifier, m_host_status),
|
||||
m_ovm(config.ovm, cb_identifier, m_host_status),
|
||||
m_ev_bsp(config.ev, cb_identifier, m_host_status) {
|
||||
|
||||
everest::lib::API::Topics api_topics;
|
||||
m_evse_bsp_enabled = config.evse.enabled;
|
||||
m_ovm_enabled = config.ovm.enabled;
|
||||
m_ev_bsp_enabled = config.ev.enabled;
|
||||
|
||||
if (m_evse_bsp_enabled && m_ev_bsp_enabled) {
|
||||
throw std::runtime_error("Configuration error: Cannot enable EV and EVSE BSP at the same time");
|
||||
}
|
||||
utilities::print_error(m_cb_identifier, "BSP/CB", 0) << "ChargeBridge connected." << std::endl;
|
||||
|
||||
if (m_evse_bsp_enabled) {
|
||||
api_topics.setup(config.evse.module_id, "evse_board_support", 1);
|
||||
m_evse_bsp_receive_topic = api_topics.everest_to_extern("");
|
||||
m_evse_bsp_send_topic = api_topics.extern_to_everest("");
|
||||
m_evse_bsp.set_mqtt_tx(
|
||||
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_evse_bsp_send_topic + topic, payload); });
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
api_topics.setup(config.ovm.module_id, "over_voltage_monitor", 1);
|
||||
m_ovm_receive_topic = api_topics.everest_to_extern("");
|
||||
m_ovm_send_topic = api_topics.extern_to_everest("");
|
||||
m_ovm.set_mqtt_tx(
|
||||
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_ovm_send_topic + topic, payload); });
|
||||
}
|
||||
|
||||
if (m_ev_bsp_enabled) {
|
||||
api_topics.setup(config.ev.module_id, "ev_board_support", 1);
|
||||
m_ev_bsp_receive_topic = api_topics.everest_to_extern("");
|
||||
m_ev_bsp_send_topic = api_topics.extern_to_everest("");
|
||||
m_ev_bsp.set_mqtt_tx(
|
||||
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_ev_bsp_send_topic + topic, payload); });
|
||||
}
|
||||
|
||||
m_mqtt.set_error_handler([this](int code, std::string const& msg) {
|
||||
auto is_error = code == 0 ? 0 : 1;
|
||||
utilities::print_error(m_cb_identifier, "BSP/MQTT", is_error) << msg << std::endl;
|
||||
});
|
||||
|
||||
m_mqtt.set_callback_connect([this, config](auto&, auto, auto, auto const&) { handle_mqtt_connect(); });
|
||||
|
||||
m_mqtt.connect(config.mqtt_bind, config.mqtt_remote, config.mqtt_port, config.mqtt_ping_interval_ms);
|
||||
|
||||
m_sync_timer.set_timeout(1s);
|
||||
|
||||
std::memset(&m_host_status, 0, sizeof(m_host_status));
|
||||
}
|
||||
|
||||
bool api_connector::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_evse_bsp_enabled) {
|
||||
result = handler.register_event_handler(&m_evse_bsp) && result;
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
result = handler.register_event_handler(&m_ovm) && result;
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
result = handler.register_event_handler(&m_ev_bsp) && result;
|
||||
}
|
||||
result = handler.register_event_handler(&m_mqtt) && result;
|
||||
result = handler.register_event_handler(&m_sync_timer, [this](auto&) {
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.sync(m_cb_connected);
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.sync(m_cb_connected);
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.sync(m_cb_connected);
|
||||
}
|
||||
handle_cb_connection_state();
|
||||
}) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool api_connector::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
if (m_evse_bsp_enabled) {
|
||||
result = handler.unregister_event_handler(&m_evse_bsp) && result;
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
result = handler.unregister_event_handler(&m_ovm) && result;
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
result = handler.unregister_event_handler(&m_ev_bsp) && result;
|
||||
}
|
||||
result = handler.unregister_event_handler(&m_mqtt) && result;
|
||||
result = handler.unregister_event_handler(&m_sync_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void api_connector::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
m_evse_bsp.set_cb_tx(handler);
|
||||
m_ev_bsp.set_cb_tx(handler);
|
||||
}
|
||||
|
||||
void api_connector::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
m_last_cb_heartbeat = std::chrono::steady_clock::now();
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.set_cb_message(msg);
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.set_cb_message(msg);
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.set_cb_message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
bool api_connector::check_cb_heartbeat() {
|
||||
if (m_last_cb_heartbeat == std::chrono::steady_clock::time_point::max()) {
|
||||
return false;
|
||||
}
|
||||
return std::chrono::steady_clock::now() - m_last_cb_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void api_connector::handle_mqtt_connect() {
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_mqtt.subscribe(m_evse_bsp_receive_topic + "#",
|
||||
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
|
||||
auto operation = utilities::string_after_pattern(topic, m_evse_bsp_receive_topic);
|
||||
if (not operation.empty()) {
|
||||
m_evse_bsp.dispatch(operation, static_cast<std::string>(payload));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_mqtt.subscribe(m_ovm_receive_topic + "#",
|
||||
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
|
||||
auto operation = utilities::string_after_pattern(topic, m_ovm_receive_topic);
|
||||
if (not operation.empty()) {
|
||||
m_ovm.dispatch(operation, static_cast<std::string>(payload));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_mqtt.subscribe(m_ev_bsp_receive_topic + "#",
|
||||
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
|
||||
auto operation = utilities::string_after_pattern(topic, m_ev_bsp_receive_topic);
|
||||
if (not operation.empty()) {
|
||||
m_ev_bsp.dispatch(operation, static_cast<std::string>(payload));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void api_connector::handle_cb_connection_state() {
|
||||
m_tx(m_host_status);
|
||||
auto current = check_cb_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "BSP/CB", 0) << "ChargeBridge connected." << std::endl;
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.clear_comm_fault();
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.clear_comm_fault();
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.clear_comm_fault();
|
||||
}
|
||||
|
||||
} else {
|
||||
if (m_evse_bsp_enabled) {
|
||||
m_evse_bsp.raise_comm_fault();
|
||||
}
|
||||
if (m_ovm_enabled) {
|
||||
m_ovm.raise_comm_fault();
|
||||
}
|
||||
if (m_ev_bsp_enabled) {
|
||||
m_ev_bsp.raise_comm_fault();
|
||||
}
|
||||
|
||||
utilities::print_error(m_cb_identifier, "BSP/CB", 1) << "Waiting for ChargeBridge...." << std::endl;
|
||||
}
|
||||
};
|
||||
if (m_cb_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_cb_initial_comm_check = false;
|
||||
}
|
||||
if (m_cb_connected != current) {
|
||||
handle_status(not m_cb_connected);
|
||||
}
|
||||
m_cb_connected = current;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,418 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "protocol/cb_common.h"
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/ev_bsp_api.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest_api_types/ev_board_support/codec.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::API::V1_0::types::generic;
|
||||
using namespace everest::lib::API;
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
ev_bsp_api::ev_bsp_api([[maybe_unused]] evse_ev_bsp_config const& config, std::string const& cb_identifier,
|
||||
evse_bsp_host_to_cb& host_status) :
|
||||
host_status(host_status), m_cb_identifier(cb_identifier) {
|
||||
|
||||
last_everest_heartbeat = std::chrono::steady_clock::time_point();
|
||||
}
|
||||
|
||||
void ev_bsp_api::sync(bool cb_connected) {
|
||||
m_cb_connected = cb_connected;
|
||||
handle_everest_connection_state();
|
||||
}
|
||||
|
||||
bool ev_bsp_api::register_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ev_bsp_api::unregister_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ev_bsp_api::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
}
|
||||
|
||||
void ev_bsp_api::tx(evse_bsp_host_to_cb const& msg) {
|
||||
if (m_tx) {
|
||||
m_tx(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::set_mqtt_tx(mqtt_ftor const& handler) {
|
||||
m_mqtt_tx = handler;
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_bsp_event(API_EVSE_BSP::Event data) {
|
||||
API_EVSE_BSP::BspEvent event{data};
|
||||
send_mqtt("bsp_event", serialize(event));
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_bsp_measurement(API_EV_BSP::BspMeasurement data) {
|
||||
API_EV_BSP::BspMeasurement measurement{data};
|
||||
send_mqtt("bsp_measurement", serialize(measurement));
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_event_relay(std::uint8_t relay) {
|
||||
using bc_event = API_EVSE_BSP::Event;
|
||||
bc_event relaise_event;
|
||||
bool relaise_state_valid = true;
|
||||
switch (relay) {
|
||||
case RelaiseState::RelayState_Open:
|
||||
relaise_event = bc_event::PowerOff;
|
||||
break;
|
||||
case RelaiseState::RelayState_Closed:
|
||||
relaise_event = bc_event::PowerOn;
|
||||
break;
|
||||
default:
|
||||
relaise_state_valid = false;
|
||||
}
|
||||
if (relaise_state_valid) {
|
||||
send_bsp_event(relaise_event);
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_event_cp(std::uint8_t cp) {
|
||||
using bc_event = API_EVSE_BSP::Event;
|
||||
bc_event cp_event;
|
||||
bool cp_state_valid = true;
|
||||
switch (cp) {
|
||||
case CpState_A:
|
||||
cp_event = bc_event::A;
|
||||
break;
|
||||
case CpState_B:
|
||||
cp_event = bc_event::B;
|
||||
break;
|
||||
case CpState_C:
|
||||
cp_event = bc_event::C;
|
||||
break;
|
||||
case CpState_D:
|
||||
cp_event = bc_event::D;
|
||||
break;
|
||||
case CpState_E:
|
||||
cp_event = bc_event::Disconnected;
|
||||
break;
|
||||
case CpState_F:
|
||||
cp_event = bc_event::F;
|
||||
break;
|
||||
case CpState_DF:
|
||||
cp_event = bc_event::E;
|
||||
break;
|
||||
case CpState::CpState_INVALID:
|
||||
cp_event = bc_event::E;
|
||||
break;
|
||||
default:
|
||||
cp_state_valid = false;
|
||||
}
|
||||
if (cp_state_valid) {
|
||||
last_cp_event = cp_event;
|
||||
send_bsp_event(cp_event);
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_bsp_measurement(uint16_t cp, [[maybe_unused]] uint8_t pp_1, [[maybe_unused]] uint8_t pp2) {
|
||||
// FIXME implement PP correctly
|
||||
API_EV_BSP::BspMeasurement data;
|
||||
data.cp_pwm_duty_cycle = cp / 65536. * 100.;
|
||||
API_EVSE_BSP::ProximityPilot pp;
|
||||
API_EVSE_BSP::Ampacity amp;
|
||||
amp = API_EVSE_BSP::Ampacity::None;
|
||||
pp.ampacity = amp;
|
||||
data.proximity_pilot = pp;
|
||||
send_bsp_measurement(data);
|
||||
}
|
||||
|
||||
inline static bool operator==(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw == b.raw;
|
||||
}
|
||||
inline static bool operator!=(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw != b.raw;
|
||||
}
|
||||
|
||||
void ev_bsp_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
|
||||
if (m_cb_status.cp_state not_eq msg.cp_state) {
|
||||
handle_event_cp(msg.cp_state);
|
||||
}
|
||||
|
||||
if (m_cb_status.relay_state != msg.relay_state) {
|
||||
handle_event_relay(msg.relay_state);
|
||||
}
|
||||
|
||||
if (m_cb_status.cp_duty_cycle not_eq msg.cp_duty_cycle or m_cb_status.pp_state_type1 not_eq msg.pp_state_type1 or
|
||||
m_cb_status.pp_state_type2 not_eq msg.pp_state_type2) {
|
||||
handle_bsp_measurement(msg.cp_duty_cycle, m_cb_status.pp_state_type1, m_cb_status.pp_state_type2);
|
||||
}
|
||||
|
||||
if (m_cb_status.error_flags not_eq msg.error_flags) {
|
||||
handle_error(msg.error_flags);
|
||||
}
|
||||
|
||||
// This is not supported in EVerest yet but should be added at some point
|
||||
/*
|
||||
if (cb_status.stop_charging not_eq msg.stop_charging) {
|
||||
handle_stop_button(msg.stop_charging);
|
||||
}*/
|
||||
|
||||
// The ev_board_support interface in EVerest does not yet have proper errors defined, so we do not handle errors
|
||||
// here yet
|
||||
/*
|
||||
if (m_cb_status.error_flags not_eq msg.error_flags) {
|
||||
handle_error(msg.error_flags);
|
||||
}*/
|
||||
|
||||
m_cb_status = msg;
|
||||
}
|
||||
|
||||
enum class SafetyErrorMask : std::uint32_t {
|
||||
cp_not_state_c = (1 << 0),
|
||||
pwm_not_enabled = (1 << 1),
|
||||
pp_invalid = (1 << 2),
|
||||
plug_temperature_too_high = (1 << 3),
|
||||
internal_temperature_too_high = (1 << 4),
|
||||
emergency_input_latched = (1 << 5),
|
||||
relay_health_latched = (1 << 6),
|
||||
vdd_3v3_out_of_range = (1 << 7),
|
||||
vdd_core_out_of_range = (1 << 8),
|
||||
vdd_12V_out_of_range = (1 << 9),
|
||||
vdd_N12V_out_of_range = (1 << 10),
|
||||
vdd_refint_out_of_range = (1 << 11),
|
||||
external_allow_power_on = (1 << 12),
|
||||
config_mem_error = (1 << 13),
|
||||
dc_hv_ov = (1 << 14),
|
||||
};
|
||||
|
||||
// Table that maps a mask to our API error + message
|
||||
struct FlagSpec {
|
||||
SafetyErrorMask mask;
|
||||
const char* message;
|
||||
};
|
||||
|
||||
static constexpr FlagSpec error_specs[] = {
|
||||
{SafetyErrorMask::pp_invalid, "PP invalid"},
|
||||
{SafetyErrorMask::plug_temperature_too_high, "Plug temperature too high"},
|
||||
{SafetyErrorMask::internal_temperature_too_high, "ChargeBridge internal over temperature"},
|
||||
{SafetyErrorMask::emergency_input_latched, "Emergency input latched"},
|
||||
{SafetyErrorMask::relay_health_latched, "Relay welded error"},
|
||||
{SafetyErrorMask::vdd_3v3_out_of_range, "Supply voltage 3.3V out of range"},
|
||||
{SafetyErrorMask::vdd_core_out_of_range, "Internal supply core voltage out of range"},
|
||||
{SafetyErrorMask::vdd_12V_out_of_range, "Internal supply 12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_N12V_out_of_range, "Internal supply -12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_refint_out_of_range, "Internal supply VREF voltage out of range"},
|
||||
{SafetyErrorMask::config_mem_error, "Internal config memory error"},
|
||||
{SafetyErrorMask::dc_hv_ov, "DC HV OVM. FIXME: This should be on OVM not EVSE interface"},
|
||||
};
|
||||
|
||||
static constexpr FlagSpec print_warning_specs[] = {
|
||||
{SafetyErrorMask::cp_not_state_c, "CP is not state C"},
|
||||
{SafetyErrorMask::pwm_not_enabled, "PWM not enabled"},
|
||||
{SafetyErrorMask::external_allow_power_on, "Allow power on from EVerest missing"},
|
||||
};
|
||||
|
||||
void ev_bsp_api::handle_error(const SafetyErrorFlags& data) {
|
||||
std::uint32_t next = data.raw; // current raw value
|
||||
std::stringstream log;
|
||||
|
||||
for (const auto& s : print_warning_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& s : error_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
if (m_everest_connected && m_cb_connected) {
|
||||
if (log.str().empty()) {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "Relays can be switched on." << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "Relays off due to:" << log.str() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::dispatch(std::string const& operation, std::string const& payload) {
|
||||
if (operation == "enable") {
|
||||
receive_enable(payload);
|
||||
} else if (operation == "set_cp_state") {
|
||||
receive_set_cp_state(payload);
|
||||
} else if (operation == "allow_power_on") {
|
||||
receive_allow_power_on(payload);
|
||||
} else if (operation == "diode_fail") {
|
||||
receive_diode_fail(payload);
|
||||
} else if (operation == "set_ac_max_current") {
|
||||
receive_set_ac_max_current(payload);
|
||||
} else if (operation == "set_three_phases") {
|
||||
receive_set_three_phases(payload);
|
||||
} else if (operation == "set_rcd_error") {
|
||||
receive_set_rcd_error(payload);
|
||||
} else if (operation == "heartbeat") {
|
||||
receive_heartbeat(payload);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api: RECEIVE invalid operation: " << operation << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::raise_comm_fault() {
|
||||
send_raise_error(API_GENERIC::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
|
||||
}
|
||||
|
||||
void ev_bsp_api::clear_comm_fault() {
|
||||
send_clear_error(API_GENERIC::ErrorEnum::CommunicationFault, "ChargeBridge not available");
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_enable([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
static CpState evcpstate_to_cpstate(API_EV_BSP::EvCpState s) {
|
||||
switch (s) {
|
||||
case API_EV_BSP::EvCpState::A:
|
||||
return CpState::CpState_A;
|
||||
case API_EV_BSP::EvCpState::B:
|
||||
return CpState::CpState_B;
|
||||
case API_EV_BSP::EvCpState::C:
|
||||
return CpState::CpState_C;
|
||||
case API_EV_BSP::EvCpState::D:
|
||||
return CpState::CpState_D;
|
||||
case API_EV_BSP::EvCpState::E:
|
||||
return CpState::CpState_E;
|
||||
default:
|
||||
return CpState::CpState_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_cp_state(std::string const& payload) {
|
||||
API_EV_BSP::EvCpState cp;
|
||||
|
||||
if (everest::lib::API::deserialize(payload, cp)) {
|
||||
host_status.ev_set_cp_state = evcpstate_to_cpstate(cp);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api::receive_set_cp_state: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_allow_power_on(std::string const& payload) {
|
||||
bool on;
|
||||
|
||||
if (everest::lib::API::deserialize(payload, on)) {
|
||||
host_status.allow_power_on = static_cast<std::uint8_t>(on);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api::receive_allow_power_on: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_diode_fail(std::string const& payload) {
|
||||
bool on;
|
||||
|
||||
if (everest::lib::API::deserialize(payload, on)) {
|
||||
host_status.ev_set_diodefault = static_cast<std::uint8_t>(on);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ev_bsp_api::receive_diode_fail: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_ac_max_current([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_three_phases([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_set_rcd_error([[maybe_unused]] std::string const& payload) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
void ev_bsp_api::receive_heartbeat(std::string const& pl) {
|
||||
last_everest_heartbeat = std::chrono::steady_clock::now();
|
||||
std::size_t id = 0;
|
||||
if (deserialize(pl, id)) {
|
||||
auto delta = id - m_last_hb_id;
|
||||
if (delta > 1) {
|
||||
utilities::print_error(m_cb_identifier, "EV_BSP/EVEREST", -1)
|
||||
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
|
||||
}
|
||||
m_last_hb_id = id;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EV_BSP/EVEREST", -1)
|
||||
<< "EVerest invalid heartbeat message: " << pl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_communication_check() {
|
||||
send_mqtt("communication_check", serialize(true));
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
if (m_mqtt_tx) {
|
||||
m_mqtt_tx(topic, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool ev_bsp_api::check_everest_heartbeat() {
|
||||
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void ev_bsp_api::handle_everest_connection_state() {
|
||||
send_communication_check();
|
||||
auto current = check_everest_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "EVerest connected" << std::endl;
|
||||
// re-send last CP state event
|
||||
send_bsp_event(last_cp_event);
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EV/EVEREST", 1) << "Waiting for EVerest..." << std::endl;
|
||||
// unplug CP if EVerest disconnects
|
||||
host_status.ev_set_cp_state = CpState_A;
|
||||
tx(host_status);
|
||||
}
|
||||
};
|
||||
|
||||
if (m_bc_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_bc_initial_comm_check = false;
|
||||
} else if (m_everest_connected != current) {
|
||||
handle_status(not m_everest_connected);
|
||||
}
|
||||
m_everest_connected = current;
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_raise_error(API_GENERIC::ErrorEnum error, std::string const& subtype, std::string const& msg) {
|
||||
API_GENERIC::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
send_mqtt("raise_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void ev_bsp_api::send_clear_error(API_GENERIC::ErrorEnum error, std::string const& subtype) {
|
||||
API_GENERIC::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
send_mqtt("clear_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,514 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/evse_bsp_api.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <everest_api_types/evse_manager/codec.hpp>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::API::V1_0::types::generic;
|
||||
using namespace everest::lib::API;
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
evse_bsp_api::evse_bsp_api(evse_bsp_config const& config, std::string const& cb_identifier,
|
||||
evse_bsp_host_to_cb& host_status) :
|
||||
host_status(host_status), m_capabilities(config.capabilities), m_cb_identifier(cb_identifier) {
|
||||
|
||||
last_everest_heartbeat = std::chrono::steady_clock::time_point();
|
||||
|
||||
m_capabilities_timer.set_timeout(10s);
|
||||
|
||||
std::memset(&cb_status, 0, sizeof(cb_status));
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
|
||||
void evse_bsp_api::sync(bool cb_connected) {
|
||||
m_cb_connected = cb_connected;
|
||||
handle_everest_connection_state();
|
||||
}
|
||||
|
||||
bool evse_bsp_api::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.register_event_handler(&m_capabilities_timer, [this](auto&) {
|
||||
send_capabilities();
|
||||
});
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
bool evse_bsp_api::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.unregister_event_handler(&m_capabilities_timer);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
void evse_bsp_api::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
}
|
||||
|
||||
void evse_bsp_api::tx(evse_bsp_host_to_cb const& msg) {
|
||||
if (m_tx) {
|
||||
m_tx(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::set_mqtt_tx(mqtt_ftor const& handler) {
|
||||
m_mqtt_tx = handler;
|
||||
}
|
||||
|
||||
inline static bool operator==(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw == b.raw;
|
||||
}
|
||||
inline static bool operator!=(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
|
||||
return a.raw != b.raw;
|
||||
}
|
||||
|
||||
void evse_bsp_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
if (cb_status.reset_reason not_eq msg.reset_reason) {
|
||||
}
|
||||
if (cb_status.cp_state not_eq msg.cp_state) {
|
||||
handle_event_cp(msg.cp_state);
|
||||
}
|
||||
if (cb_status.relay_state != msg.relay_state) {
|
||||
handle_event_relay(msg.relay_state);
|
||||
}
|
||||
if (cb_status.error_flags not_eq msg.error_flags) {
|
||||
handle_error(msg.error_flags);
|
||||
}
|
||||
if (cb_status.pp_state_type1 not_eq msg.pp_state_type1) {
|
||||
handle_pp_type1(msg.pp_state_type1);
|
||||
}
|
||||
if (cb_status.pp_state_type2 not_eq msg.pp_state_type2) {
|
||||
handle_pp_type2(msg.pp_state_type2);
|
||||
}
|
||||
if (cb_status.stop_charging not_eq msg.stop_charging) {
|
||||
handle_stop_button(msg.stop_charging);
|
||||
}
|
||||
// cb_status.lock_state is not checked here as it cannot be reported to EVerest.
|
||||
|
||||
cb_status = msg;
|
||||
}
|
||||
|
||||
void evse_bsp_api::dispatch(std::string const& operation, std::string const& payload) {
|
||||
if (operation == "enable") {
|
||||
receive_enable(payload);
|
||||
} else if (operation == "pwm_on") {
|
||||
receive_pwm_on(payload);
|
||||
} else if (operation == "cp_state_X1") {
|
||||
receive_cp_state_X1(payload);
|
||||
} else if (operation == "cp_state_F") {
|
||||
receive_cp_state_F(payload);
|
||||
} else if (operation == "allow_power_on") {
|
||||
receive_allow_power_on(payload);
|
||||
} else if (operation == "ac_switch_three_phases_while_charging") {
|
||||
receive_ac_switch_three_phases_while_charging(payload);
|
||||
} else if (operation == "ac_overcurrent_limit") {
|
||||
receive_ac_overcurrent_limit(payload);
|
||||
} else if (operation == "lock") {
|
||||
receive_lock();
|
||||
} else if (operation == "unlock") {
|
||||
receive_unlock();
|
||||
} else if (operation == "self_test") {
|
||||
receive_self_test(payload);
|
||||
} else if (operation == "reset") {
|
||||
receive_request_reset(payload);
|
||||
} else if (operation == "heartbeat") {
|
||||
receive_heartbeat(payload);
|
||||
} else {
|
||||
std::cerr << "evse_bsp: RECEIVE invalid operation: " << operation << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::raise_comm_fault() {
|
||||
send_raise_error(API_BSP::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
|
||||
}
|
||||
|
||||
void evse_bsp_api::clear_comm_fault() {
|
||||
send_clear_error(API_BSP::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_event_cp(std::uint8_t cp) {
|
||||
using bc_event = API_BSP::Event;
|
||||
bc_event cp_event;
|
||||
bool cp_state_valid = true;
|
||||
switch (cp) {
|
||||
case CpState_A:
|
||||
cp_event = bc_event::A;
|
||||
send_clear_error(API_BSP::ErrorEnum::MREC14PilotFault, "", "");
|
||||
send_clear_error(API_BSP::ErrorEnum::DiodeFault, "", "");
|
||||
break;
|
||||
case CpState_B:
|
||||
cp_event = bc_event::B;
|
||||
break;
|
||||
case CpState_C:
|
||||
cp_event = bc_event::C;
|
||||
break;
|
||||
case CpState_D:
|
||||
cp_event = bc_event::D;
|
||||
break;
|
||||
case CpState_E:
|
||||
cp_event = bc_event::E;
|
||||
break;
|
||||
case CpState_F:
|
||||
cp_event = bc_event::F;
|
||||
break;
|
||||
case CpState_DF:
|
||||
cp_event = bc_event::E;
|
||||
send_raise_error(API_BSP::ErrorEnum::DiodeFault, "", "Diode Fault");
|
||||
break;
|
||||
case CpState::CpState_INVALID:
|
||||
cp_event = bc_event::E;
|
||||
send_raise_error(API_BSP::ErrorEnum::MREC14PilotFault, "", "Pilot Fault");
|
||||
break;
|
||||
default:
|
||||
cp_state_valid = false;
|
||||
}
|
||||
if (cp_state_valid and m_enabled) {
|
||||
send_event(cp_event);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_event_relay(std::uint8_t relay) {
|
||||
using bc_event = API_BSP::Event;
|
||||
bc_event relaise_event;
|
||||
bool relaise_state_valid = true;
|
||||
switch (relay) {
|
||||
case RelaiseState::RelayState_Open:
|
||||
relaise_event = bc_event::PowerOff;
|
||||
break;
|
||||
case RelaiseState::RelayState_Closed:
|
||||
relaise_event = bc_event::PowerOn;
|
||||
break;
|
||||
default:
|
||||
relaise_state_valid = false;
|
||||
}
|
||||
if (relaise_state_valid) {
|
||||
send_event(relaise_event);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_pp_type2(std::uint8_t data) {
|
||||
API_BSP::Ampacity bc_ampacity;
|
||||
bool bc_ampacity_valid = true;
|
||||
switch (data) {
|
||||
case PpState_Type2_STATE_NC:
|
||||
bc_ampacity = API_BSP::Ampacity::None;
|
||||
break;
|
||||
case PpState_Type2_STATE_13A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_13;
|
||||
break;
|
||||
case PpState_Type2_STATE_20A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_20;
|
||||
break;
|
||||
case PpState_Type2_STATE_32A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_32;
|
||||
break;
|
||||
case PpState_Type2_STATE_70A:
|
||||
bc_ampacity = API_BSP::Ampacity::A_63_3ph_70_1ph;
|
||||
break;
|
||||
case PpState_Type2_STATE_FAULT:
|
||||
// Raise error check state
|
||||
bc_ampacity_valid = false;
|
||||
send_raise_error(API_BSP::ErrorEnum::MREC23ProximityFault, "", "Proximity Pilot Fault State");
|
||||
break;
|
||||
default:
|
||||
bc_ampacity_valid = false;
|
||||
}
|
||||
if (bc_ampacity_valid) {
|
||||
send_ac_pp_amapcity(bc_ampacity);
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_pp_type1(std::uint8_t data) {
|
||||
// EVerest does not really have support for type 1 PP.
|
||||
// We just send a stop charging if some one presses the button,
|
||||
// for everything else the PP state does not really matter.
|
||||
if (data == PpState_Type1_STATE_Connected_Button_Pressed) {
|
||||
auto reason = API_EVM::StopTransactionReason::EVDisconnected;
|
||||
send_request_stop_transaction(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
// Define bit masks
|
||||
enum class SafetyErrorMask : std::uint32_t {
|
||||
cp_not_state_c = (1 << 0),
|
||||
pwm_not_enabled = (1 << 1),
|
||||
pp_invalid = (1 << 2),
|
||||
plug_temperature_too_high = (1 << 3),
|
||||
internal_temperature_too_high = (1 << 4),
|
||||
emergency_input_latched = (1 << 5),
|
||||
relay_health_latched = (1 << 6),
|
||||
vdd_3v3_out_of_range = (1 << 7),
|
||||
vdd_core_out_of_range = (1 << 8),
|
||||
vdd_12V_out_of_range = (1 << 9),
|
||||
vdd_N12V_out_of_range = (1 << 10),
|
||||
vdd_refint_out_of_range = (1 << 11),
|
||||
external_allow_power_on = (1 << 12),
|
||||
config_mem_error = (1 << 13),
|
||||
dc_hv_ov = (1 << 14),
|
||||
};
|
||||
|
||||
// Table that maps a mask to our API error + message
|
||||
struct FlagSpec {
|
||||
SafetyErrorMask mask;
|
||||
API_BSP::ErrorEnum error;
|
||||
const char* subtype;
|
||||
const char* message;
|
||||
};
|
||||
|
||||
static constexpr FlagSpec error_specs[] = {
|
||||
{SafetyErrorMask::pp_invalid, API_BSP::ErrorEnum::MREC23ProximityFault, "", "PP invalid"},
|
||||
{SafetyErrorMask::plug_temperature_too_high, API_BSP::ErrorEnum::MREC19CableOverTempStop, "",
|
||||
"Plug temperature too high"},
|
||||
{SafetyErrorMask::internal_temperature_too_high, API_BSP::ErrorEnum::VendorError, "INTTEMP",
|
||||
"ChargeBridge internal over temperature"},
|
||||
{SafetyErrorMask::emergency_input_latched, API_BSP::ErrorEnum::VendorError, "EMGINPUT", "Emergency input latched"},
|
||||
{SafetyErrorMask::relay_health_latched, API_BSP::ErrorEnum::VendorError, "RELAYS", "Relay welded error"},
|
||||
{SafetyErrorMask::vdd_3v3_out_of_range, API_BSP::ErrorEnum::VendorError, "3V3", "Supply voltage 3.3V out of range"},
|
||||
{SafetyErrorMask::vdd_core_out_of_range, API_BSP::ErrorEnum::VendorError, "VDDCORE",
|
||||
"Internal supply core voltage out of range"},
|
||||
{SafetyErrorMask::vdd_12V_out_of_range, API_BSP::ErrorEnum::VendorError, "VCC12",
|
||||
"Internal supply 12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_N12V_out_of_range, API_BSP::ErrorEnum::VendorError, "VCCN12",
|
||||
"Internal supply -12V voltage out of range"},
|
||||
{SafetyErrorMask::vdd_refint_out_of_range, API_BSP::ErrorEnum::VendorError, "VCCREF",
|
||||
"Internal supply VREF voltage out of range"},
|
||||
{SafetyErrorMask::config_mem_error, API_BSP::ErrorEnum::VendorError, "CONFIGMEM", "Internal config memory error"},
|
||||
{SafetyErrorMask::dc_hv_ov, API_BSP::ErrorEnum::VendorError, "DV_HV",
|
||||
"DC HV OVM. FIXME: This should be on OVM not EVSE interface"},
|
||||
};
|
||||
|
||||
static constexpr FlagSpec print_warning_specs[] = {
|
||||
{SafetyErrorMask::cp_not_state_c, API_BSP::ErrorEnum::VendorWarning, "", "CP is not state C"},
|
||||
{SafetyErrorMask::pwm_not_enabled, API_BSP::ErrorEnum::VendorWarning, "", "PWM not enabled"},
|
||||
{SafetyErrorMask::external_allow_power_on, API_BSP::ErrorEnum::VendorWarning, "",
|
||||
"Allow power on from EVerest missing"},
|
||||
};
|
||||
|
||||
// 4) Edge-driven handler
|
||||
void evse_bsp_api::handle_error(const SafetyErrorFlags& data) {
|
||||
std::uint32_t prev = cb_status.error_flags.raw; // cached raw value from before
|
||||
std::uint32_t next = data.raw; // current raw value
|
||||
|
||||
std::uint32_t became_active = next & ~prev; // rising edges
|
||||
std::uint32_t became_inactive = prev & ~next; // falling edges
|
||||
|
||||
for (const auto& s : error_specs) {
|
||||
if (became_active & static_cast<std::uint32_t>(s.mask)) {
|
||||
send_raise_error(s.error, s.subtype, s.message);
|
||||
}
|
||||
if (became_inactive & static_cast<std::uint32_t>(s.mask)) {
|
||||
send_clear_error(s.error, s.subtype, "");
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream log;
|
||||
|
||||
for (const auto& s : print_warning_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& s : error_specs) {
|
||||
if (next & static_cast<std::uint32_t>(s.mask)) {
|
||||
log << "[" << s.message << "] ";
|
||||
}
|
||||
}
|
||||
|
||||
if (everest_connected && m_cb_connected) {
|
||||
if (log.str().empty()) {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0) << "Relays can be switched on." << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0)
|
||||
<< "Relays off due to:" << log.str() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_stop_button([[maybe_unused]] std::uint8_t data) {
|
||||
auto reason = API_EVM::StopTransactionReason::Local;
|
||||
send_request_stop_transaction(reason);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_enable(std::string const& payload) {
|
||||
if (everest::lib::API::deserialize(payload, m_enabled)) {
|
||||
handle_event_cp(cb_status.cp_state);
|
||||
handle_event_relay(cb_status.relay_state);
|
||||
} else {
|
||||
std::cerr << "evse_bsp_api::receive_enabled: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_pwm_on(std::string const& payload) {
|
||||
double pwm = 0;
|
||||
if (everest::lib::API::deserialize(payload, pwm)) {
|
||||
host_status.pwm_duty_cycle = pwm * 100;
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "evse_bsp_api::receive_pwm_on: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_cp_state_X1([[maybe_unused]] std::string const& payload) {
|
||||
host_status.pwm_duty_cycle = 10001;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_cp_state_F([[maybe_unused]] std::string const& payload) {
|
||||
host_status.pwm_duty_cycle = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_allow_power_on(std::string const& payload) {
|
||||
API_BSP::PowerOnOff obj;
|
||||
if (everest::lib::API::deserialize(payload, obj)) {
|
||||
host_status.allow_power_on = obj.allow_power_on;
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "evse_bsp_api::receive_allow_power_on: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_ac_switch_three_phases_while_charging(std::string const&) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_ac_overcurrent_limit(std::string const&) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_lock() {
|
||||
host_status.connector_lock = 1;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_unlock() {
|
||||
host_status.connector_lock = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_self_test([[maybe_unused]] std::string const& payload) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_request_reset(std::string const&) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::receive_heartbeat(std::string const& pl) {
|
||||
last_everest_heartbeat = std::chrono::steady_clock::now();
|
||||
std::size_t id = 0;
|
||||
if (deserialize(pl, id)) {
|
||||
auto delta = id - m_last_hb_id;
|
||||
if (delta > 1) {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
|
||||
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
|
||||
}
|
||||
m_last_hb_id = id;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
|
||||
<< "EVerest invalid heartbeat message: " << pl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_event(API_BSP::Event data) {
|
||||
API_BSP::BspEvent event{data};
|
||||
send_mqtt("event", serialize(event));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_ac_nr_of_phases(std::uint8_t data) {
|
||||
auto phases = static_cast<int>(data);
|
||||
if (phases > 0 && phases <= 3) {
|
||||
send_mqtt("ac_nr_of_phases", serialize(phases));
|
||||
}
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_capabilities() {
|
||||
send_mqtt("capabilities", serialize(m_capabilities));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_ac_pp_amapcity(API_BSP::Ampacity data) {
|
||||
API_BSP::ProximityPilot msg{data};
|
||||
send_mqtt("ac_pp_ampacity", serialize(msg));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_request_stop_transaction(API_EVM::StopTransactionReason data) {
|
||||
API_EVM::StopTransactionRequest request;
|
||||
request.reason = data;
|
||||
send_mqtt("request_stop_transaction", serialize(request));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_rcd_current(std::uint8_t) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_raise_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg) {
|
||||
API_BSP::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
send_mqtt("raise_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_clear_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg) {
|
||||
API_BSP::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
send_mqtt("clear_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_communication_check() {
|
||||
send_mqtt("communication_check", serialize(true));
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_reply_reset([[maybe_unused]] std::string const& replyTo) {
|
||||
}
|
||||
|
||||
void evse_bsp_api::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
m_mqtt_tx(topic, message);
|
||||
}
|
||||
|
||||
bool evse_bsp_api::check_everest_heartbeat() {
|
||||
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void evse_bsp_api::handle_everest_connection_state() {
|
||||
send_communication_check();
|
||||
auto current = check_everest_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0) << "EVerest connected" << std::endl;
|
||||
send_capabilities();
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 1) << "Waiting for EVerest...." << std::endl;
|
||||
host_status.allow_power_on = 0;
|
||||
host_status.pwm_duty_cycle = 65535;
|
||||
tx(host_status);
|
||||
}
|
||||
};
|
||||
|
||||
if (m_bc_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_bc_initial_comm_check = false;
|
||||
} else if (everest_connected != current) {
|
||||
handle_status(not everest_connected);
|
||||
}
|
||||
everest_connected = current;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,226 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "protocol/cb_common.h"
|
||||
#include "protocol/evse_bsp_cb_to_host.h"
|
||||
#include <charge_bridge/everest_api/ovm_api.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/over_voltage_monitor/API.hpp>
|
||||
#include <everest_api_types/over_voltage_monitor/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace everest::lib::API::V1_0::types::generic;
|
||||
using namespace everest::lib::API;
|
||||
|
||||
namespace charge_bridge::evse_bsp {
|
||||
|
||||
ovm_api::ovm_api([[maybe_unused]] evse_ovm_config const& config, std::string const& cb_identifier,
|
||||
evse_bsp_host_to_cb& host_status) :
|
||||
host_status(host_status), m_cb_identifier(cb_identifier) {
|
||||
|
||||
last_everest_heartbeat = std::chrono::steady_clock::time_point();
|
||||
}
|
||||
|
||||
void ovm_api::sync(bool cb_connected) {
|
||||
m_cb_connected = cb_connected;
|
||||
handle_everest_connection_state();
|
||||
}
|
||||
|
||||
bool ovm_api::register_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ovm_api::unregister_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ovm_api::set_cb_tx(tx_ftor const& handler) {
|
||||
m_tx = handler;
|
||||
}
|
||||
|
||||
void ovm_api::tx(evse_bsp_host_to_cb const& msg) {
|
||||
if (m_tx) {
|
||||
m_tx(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::set_mqtt_tx(mqtt_ftor const& handler) {
|
||||
m_mqtt_tx = handler;
|
||||
}
|
||||
|
||||
void ovm_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
|
||||
const double voltage_V = msg.hv_mV * 0.001;
|
||||
send_voltage_measurement_V(voltage_V);
|
||||
|
||||
if (msg.error_flags.flags.dc_hv_ov_emergency not_eq m_cb_status.error_flags.flags.dc_hv_ov_emergency) {
|
||||
handle_dc_hv_ov_emergency(msg.error_flags.flags.dc_hv_ov_emergency not_eq 0);
|
||||
}
|
||||
if (msg.error_flags.flags.dc_hv_ov_error not_eq m_cb_status.error_flags.flags.dc_hv_ov_error) {
|
||||
handle_dc_hv_ov_error(msg.error_flags.flags.dc_hv_ov_error not_eq 0);
|
||||
}
|
||||
|
||||
if (msg.cp_state not_eq m_cb_status.cp_state) {
|
||||
handle_cp_state(static_cast<CpState>(msg.cp_state));
|
||||
}
|
||||
|
||||
m_cb_status = msg;
|
||||
}
|
||||
|
||||
void ovm_api::dispatch(std::string const& operation, std::string const& payload) {
|
||||
if (operation == "set_limits") {
|
||||
receive_set_limits(payload);
|
||||
} else if (operation == "start") {
|
||||
receive_start();
|
||||
} else if (operation == "stop") {
|
||||
receive_stop();
|
||||
} else if (operation == "reset_over_voltage_error") {
|
||||
receive_reset_over_voltage_error();
|
||||
} else if (operation == "heartbeat") {
|
||||
receive_heartbeat(payload);
|
||||
} else {
|
||||
std::cerr << "ovm_api: RECEIVE invalid operation: " << operation << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::raise_comm_fault() {
|
||||
send_raise_error(API_OVM::ErrorEnum::CommunicationFault, "ChargeBridge not available", "",
|
||||
API_OVM::ErrorSeverityEnum::High);
|
||||
}
|
||||
|
||||
void ovm_api::clear_comm_fault() {
|
||||
send_clear_error(API_OVM::ErrorEnum::CommunicationFault, "ChargeBridge not available");
|
||||
}
|
||||
|
||||
void ovm_api::handle_dc_hv_ov_emergency(bool high) {
|
||||
static const std::string subtype = "Emergency";
|
||||
if (high) {
|
||||
send_raise_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype, "", API_OVM::ErrorSeverityEnum::High);
|
||||
} else {
|
||||
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype);
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::handle_dc_hv_ov_error(bool high) {
|
||||
static const std::string subtype = "Error";
|
||||
if (high) {
|
||||
send_raise_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype, "", API_OVM::ErrorSeverityEnum::Medium);
|
||||
} else {
|
||||
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype);
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::handle_cp_state(CpState state) {
|
||||
if (state == CpState_A) {
|
||||
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, "");
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::receive_set_limits(std::string const& payload) {
|
||||
static auto const V_to_mV_factor = 1000;
|
||||
if (everest::lib::API::deserialize(payload, m_limits)) {
|
||||
host_status.ovm_limit_emergency_mV = static_cast<std::uint32_t>(m_limits.emergency_limit_V * V_to_mV_factor);
|
||||
host_status.ovm_limit_error_mV = static_cast<std::uint32_t>(m_limits.error_limit_V * V_to_mV_factor);
|
||||
tx(host_status);
|
||||
} else {
|
||||
std::cerr << "ovm_api::receive_set_limits: payload invalid -> " << payload << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::receive_start() {
|
||||
host_status.ovm_enable = 1;
|
||||
host_status.ovm_reset_errors = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void ovm_api::receive_stop() {
|
||||
host_status.ovm_enable = 0;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void ovm_api::receive_reset_over_voltage_error() {
|
||||
host_status.ovm_reset_errors = 1;
|
||||
tx(host_status);
|
||||
}
|
||||
|
||||
void ovm_api::receive_heartbeat(std::string const& pl) {
|
||||
last_everest_heartbeat = std::chrono::steady_clock::now();
|
||||
std::size_t id = 0;
|
||||
if (deserialize(pl, id)) {
|
||||
auto delta = id - m_last_hb_id;
|
||||
if (delta > 1) {
|
||||
utilities::print_error(m_cb_identifier, "OVM/EVEREST", -1)
|
||||
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
|
||||
}
|
||||
m_last_hb_id = id;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
|
||||
<< "EVerest invalid heartbeat message: " << pl << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void ovm_api::send_voltage_measurement_V(double data) {
|
||||
send_mqtt("voltage_measurement_V", serialize(data));
|
||||
}
|
||||
|
||||
void ovm_api::send_raise_error(API_OVM::ErrorEnum error, std::string const& subtype, std::string const& msg,
|
||||
API_OVM::ErrorSeverityEnum severity) {
|
||||
API_OVM::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
error_msg.message = msg;
|
||||
error_msg.severity = severity;
|
||||
send_mqtt("raise_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void ovm_api::send_clear_error(API_OVM::ErrorEnum error, std::string const& subtype) {
|
||||
API_OVM::Error error_msg;
|
||||
error_msg.type = error;
|
||||
error_msg.sub_type = subtype;
|
||||
send_mqtt("clear_error", serialize(error_msg));
|
||||
}
|
||||
|
||||
void ovm_api::send_communication_check() {
|
||||
send_mqtt("communication_check", serialize(true));
|
||||
}
|
||||
|
||||
void ovm_api::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
if (m_mqtt_tx) {
|
||||
m_mqtt_tx(topic, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool ovm_api::check_everest_heartbeat() {
|
||||
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
|
||||
}
|
||||
|
||||
void ovm_api::handle_everest_connection_state() {
|
||||
send_communication_check();
|
||||
auto current = check_everest_heartbeat();
|
||||
auto handle_status = [this](bool status) {
|
||||
if (status) {
|
||||
utilities::print_error(m_cb_identifier, "OVM/EVEREST", 0) << "EVerest connected" << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_cb_identifier, "OVM/EVEREST", 1) << "Waiting for EVerest...." << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
if (m_bc_initial_comm_check) {
|
||||
handle_status(current);
|
||||
m_bc_initial_comm_check = false;
|
||||
} else if (m_everest_connected != current) {
|
||||
handle_status(not m_everest_connected);
|
||||
}
|
||||
m_everest_connected = current;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::evse_bsp
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "everest/io/udp/udp_payload.hpp"
|
||||
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
|
||||
#include <charge_bridge/utilities/filesystem.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/platform_utils.hpp>
|
||||
|
||||
#include <protocol/cb_management.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 3000;
|
||||
}
|
||||
|
||||
namespace charge_bridge::firmware_update {
|
||||
|
||||
const std::uint32_t sync_fw_updater::app_udp_sector_size = 0x2000;
|
||||
const std::uint16_t sync_fw_updater::sub_chunk_size = 1024;
|
||||
|
||||
using namespace everest::lib::io::udp;
|
||||
|
||||
static everest::lib::io::udp::udp_payload make_ping_command() {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
|
||||
CbManagementPacket<CbFirmwarePing> packet;
|
||||
packet.type = CbStructType::CST_CbFirmwarePing;
|
||||
packet.data.dummy = 0;
|
||||
utilities::struct_to_vector(packet, payload.buffer);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
static everest::lib::io::udp::udp_payload make_get_version_command() {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
|
||||
CbManagementPacket<CbFirmwareGetVersion> packet;
|
||||
packet.type = CbStructType::CST_CbFirmwareGetVersion;
|
||||
packet.data.dummy = 0;
|
||||
utilities::struct_to_vector(packet, payload.buffer);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
sync_fw_updater::sync_fw_updater(fw_update_config const& config) :
|
||||
m_udp(config.cb_remote, config.cb_port, 3, default_udp_timeout_ms), m_config(config) {
|
||||
}
|
||||
|
||||
std::optional<std::string> sync_fw_updater::get_fw_version() {
|
||||
auto pl = make_get_version_command();
|
||||
|
||||
auto result = m_udp.request_reply(pl);
|
||||
if (not result) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result->buffer[result->buffer.size() - 1] = 0x00; // ensure it is actually a 0 terminated string
|
||||
auto* str_ptr = reinterpret_cast<char*>(result->buffer.data()); // reinterpret for string conversion
|
||||
return std::string(str_ptr + 2); // skip 2 byte header
|
||||
}
|
||||
|
||||
void sync_fw_updater::print_fw_version() {
|
||||
auto result = get_fw_version();
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", not result.has_value())
|
||||
<< "Firmware version " << result.value_or("ERROR") << std::endl;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::check_if_correct_fw_installed() {
|
||||
auto installed_fw = get_fw_version();
|
||||
|
||||
if (not installed_fw.has_value()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader hdr;
|
||||
std::uint32_t offset;
|
||||
if (not read_crypt_signed_header(m_config.fw_path, hdr, offset)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1)
|
||||
<< "Could not read header for file: " << m_config.fw_path << std::endl;
|
||||
return false;
|
||||
}
|
||||
auto available_fw = hdr.firmware_version;
|
||||
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0)
|
||||
<< "Firmware installed: \"" << installed_fw.value() << "\" Firmware available: \"" << available_fw << "\""
|
||||
<< std::endl;
|
||||
|
||||
if (installed_fw.value() == available_fw) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool sync_fw_updater::quick_check_connection() {
|
||||
static const std::uint16_t rr_timeout_ms = 200;
|
||||
static const std::uint16_t rr_retires_ms = 10;
|
||||
|
||||
everest::lib::io::udp::udp_payload pl = make_ping_command();
|
||||
auto result = m_udp.request_reply(pl, rr_timeout_ms, rr_retires_ms).has_value();
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", not result)
|
||||
<< (result ? "ChargeBride Connected" : "No connection to ChargeBridge") << std::endl;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::check_connection() {
|
||||
static const std::uint16_t rr_timeout_ms = 150;
|
||||
static const std::uint16_t rr_retires_ms = 100;
|
||||
|
||||
everest::lib::io::udp::udp_payload pl = make_ping_command();
|
||||
auto result = m_udp.request_reply(pl, rr_timeout_ms, rr_retires_ms).has_value();
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", not result)
|
||||
<< (result ? "ChargeBride Connected" : "No connection to ChargeBridge") << std::endl;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::ping() {
|
||||
everest::lib::io::udp::udp_payload pl = make_ping_command();
|
||||
|
||||
return m_udp.request_reply(pl).has_value();
|
||||
}
|
||||
|
||||
bool sync_fw_updater::check_reply(utilities::sync_udp_client::reply const& val) {
|
||||
if (val && val->size() == (sizeof(AppUDPResponse) + 2)) {
|
||||
AppUDPResponse reply;
|
||||
memcpy(&reply, val->buffer.data() + 2, sizeof(AppUDPResponse));
|
||||
return (reply == AppUDPResponse::AUR_Ok);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_fw() {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0) << "Upload in progress" << std::endl;
|
||||
|
||||
if (not upload_firmware()) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "Upload of firmware image: " << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0) << "Upload completed" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_firmware() {
|
||||
auto path = m_config.fw_path;
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0) << path << std::endl;
|
||||
|
||||
if (not fs::exists(path) || not fs::is_regular_file(path)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "firmware file not found: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t offset;
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader hdr;
|
||||
|
||||
if (not upload_init(path, offset, hdr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint32_t total_bytes = 0;
|
||||
std::uint16_t sector = 0;
|
||||
|
||||
if (not upload_transfer(path, sector, offset, total_bytes)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "Upload failed at sector: " << sector << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (not upload_finish(path, total_bytes, hdr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
# File format for the binary update bundle:
|
||||
# 32 byte header [reserved]
|
||||
# 1 byte length of signature
|
||||
# signature binary
|
||||
# 1 byte NUM_SECTORS: This is the number of secure sectors
|
||||
# 16 byte IV
|
||||
# ... rest of the file is assembled firmware image: secure part...padding...non secure part (encrypted)
|
||||
*/
|
||||
|
||||
bool sync_fw_updater::upload_init(const fs::path& file_path, std::uint32_t& offset,
|
||||
charge_bridge::filesystem_utils::CryptSignedHeader& hdr) {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
|
||||
if (not read_crypt_signed_header(file_path, hdr, offset)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1)
|
||||
<< "Could not read header for file: " << file_path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 0)
|
||||
<< "Loaded firmware version file: " << file_path << " Version: " << hdr.firmware_version << std::endl;
|
||||
|
||||
CbManagementPacket<CbFirmwareStart> msg;
|
||||
msg.type = CbStructType::CST_CbFirmwareStart;
|
||||
|
||||
msg.data.is_secure_fw = true;
|
||||
msg.data.requires_crc_verification = true;
|
||||
msg.data.requires_sha256_verification = true;
|
||||
msg.data.requires_signature_verification = true;
|
||||
msg.data.requires_decryption = true;
|
||||
|
||||
// Copy the IV from the header
|
||||
std::memcpy(msg.data.iv, hdr.iv.data(), sizeof(msg.data.iv));
|
||||
|
||||
utilities::struct_to_vector(msg, payload.buffer);
|
||||
auto result = m_udp.request_reply(payload);
|
||||
|
||||
return check_reply(result);
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_transfer(const fs::path& file_path, std::uint16_t& sector, std::uint32_t offset,
|
||||
std::uint32_t& total_bytes) {
|
||||
bool send_failed = false;
|
||||
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip the header
|
||||
file.seekg(offset, std::ios::beg);
|
||||
|
||||
bool processed_file = filesystem_utils::process_file(
|
||||
file, sub_chunk_size, [&](const std::vector<std::uint8_t>& buffer, bool last_chunk) -> bool {
|
||||
total_bytes += buffer.size();
|
||||
|
||||
// Care must be taken when sending this over, since on the
|
||||
// receiving end we must remove the PKCS#7 added bytes
|
||||
auto block = make_fw_chunk(sector, last_chunk, buffer);
|
||||
auto result = m_udp.request_reply(block);
|
||||
|
||||
if (not check_reply(result)) {
|
||||
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "chunk could not be sent" << std::endl;
|
||||
|
||||
send_failed = true;
|
||||
return true; // Interrupt
|
||||
}
|
||||
|
||||
sector++;
|
||||
|
||||
return false; // Continue
|
||||
});
|
||||
|
||||
return (processed_file) && (send_failed == false);
|
||||
}
|
||||
|
||||
bool sync_fw_updater::upload_finish([[maybe_unused]] const fs::path& file_path, std::uint32_t total_bytes,
|
||||
const charge_bridge::filesystem_utils::CryptSignedHeader& hdr) {
|
||||
CbManagementPacket<CbFirmwareEnd> fw_check_packet;
|
||||
|
||||
fw_check_packet.type = CbStructType::CST_CbFirmwareFinish;
|
||||
fw_check_packet.data.firmware_len = total_bytes;
|
||||
fw_check_packet.data.watermark_secure_end = hdr.num_sectors;
|
||||
|
||||
if (hdr.sig_len > sizeof(fw_check_packet.data.fw_signature) || hdr.sig_len > hdr.signature.size()) {
|
||||
return false;
|
||||
}
|
||||
memcpy(fw_check_packet.data.fw_signature, hdr.signature.data(), hdr.sig_len);
|
||||
fw_check_packet.data.fw_signature_len = hdr.sig_len;
|
||||
|
||||
udp_payload payload;
|
||||
utilities::struct_to_vector(fw_check_packet, payload.buffer);
|
||||
|
||||
// The final check can be a very slow operation due to the cryptography involved
|
||||
static const std::uint16_t rr_timeout_ms = 10000;
|
||||
static const std::uint16_t rr_retires_ms = 1;
|
||||
auto result = m_udp.request_reply(payload, rr_timeout_ms, rr_retires_ms);
|
||||
|
||||
return check_reply(result);
|
||||
}
|
||||
|
||||
udp_payload sync_fw_updater::make_fw_chunk(std::uint16_t sector, std::uint8_t last_chunk,
|
||||
std::vector<std::uint8_t> const& data) {
|
||||
CbManagementPacket<CbFirmwarePacket> fw_data_packet;
|
||||
fw_data_packet.type = CbStructType::CST_CbFirmwarePacket;
|
||||
fw_data_packet.data.last_packet = last_chunk;
|
||||
fw_data_packet.data.sector = sector;
|
||||
fw_data_packet.data.data_len = data.size();
|
||||
std::memcpy(fw_data_packet.data.data, data.data(), data.size());
|
||||
|
||||
udp_payload result;
|
||||
utilities::struct_to_vector(fw_data_packet, result.buffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::firmware_update
|
||||
@@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <charge_bridge/gpio_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/platform_utils.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/mqtt/mqtt_client.hpp>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <protocol/cb_management.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace charge_bridge {
|
||||
using namespace std::chrono_literals;
|
||||
namespace mqtt = everest::lib::io::mqtt;
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
const int mqtt_reconnect_timeout_ms = 1000;
|
||||
} // namespace
|
||||
|
||||
gpio_bridge::gpio_bridge(gpio_config const& config) :
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
|
||||
m_mqtt(mqtt_reconnect_timeout_ms)
|
||||
|
||||
{
|
||||
m_identifier = config.cb + "/" + config.item;
|
||||
|
||||
m_heartbeat_timer.set_timeout(std::chrono::seconds(config.interval_s));
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) { handle_udp_rx(data); });
|
||||
|
||||
m_udp.set_error_handler([this](auto id, auto const& msg) {
|
||||
utilities::print_error(m_identifier, "GPIO/UDP", id) << msg << std::endl;
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
|
||||
m_receive_topic = "pionix/chargebridge/" + config.cb + "/gpio/output/";
|
||||
m_send_topic = "pionix/chargebridge/" + config.cb + "/gpio/input/";
|
||||
|
||||
m_mqtt.set_error_handler([this, config](int id, std::string const& msg) {
|
||||
utilities::print_error(m_identifier, "GPIO/MQTT", id) << msg << std::endl;
|
||||
m_mqtt_on_error = id not_eq 0;
|
||||
});
|
||||
|
||||
m_mqtt.set_callback_connect([this](auto&, auto, auto, auto const&) {
|
||||
m_mqtt.subscribe(
|
||||
m_receive_topic + "#", [this](auto&, auto const& payload) { dispatch(payload); },
|
||||
everest::lib::io::mqtt::mqtt_client::QoS::at_most_once);
|
||||
});
|
||||
|
||||
m_mqtt.connect(config.mqtt_bind, config.mqtt_remote, config.mqtt_port, config.mqtt_ping_interval_ms);
|
||||
|
||||
m_message.type = CbStructType::CST_HostToCb_Gpio;
|
||||
m_message.data.number_of_gpios = CB_NUMBER_OF_GPIOS;
|
||||
std::memset(m_message.data.gpio_values, 0, sizeof(m_message.data.gpio_values));
|
||||
}
|
||||
|
||||
gpio_bridge::~gpio_bridge() {
|
||||
}
|
||||
|
||||
bool gpio_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.register_event_handler(&m_udp);
|
||||
result = handler.register_event_handler(&m_mqtt) && result;
|
||||
result = handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); }) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool gpio_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = handler.unregister_event_handler(&m_udp);
|
||||
result = handler.unregister_event_handler(&m_mqtt) && result;
|
||||
result = handler.unregister_event_handler(&m_heartbeat_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void gpio_bridge::dispatch(everest::lib::io::mqtt::mqtt_client::message const& data) {
|
||||
auto& topic = data.topic;
|
||||
auto& payload = data.payload;
|
||||
auto operation = utilities::string_after_pattern(topic, m_receive_topic);
|
||||
uint16_t value = 0;
|
||||
int id = 0;
|
||||
|
||||
auto stous = [](std::string const& data) {
|
||||
auto val = stoi(data);
|
||||
if (val < 0 or val > std::numeric_limits<uint16_t>::max()) {
|
||||
throw std::range_error("");
|
||||
}
|
||||
return static_cast<uint16_t>(val);
|
||||
};
|
||||
|
||||
try {
|
||||
value = stous(payload);
|
||||
} catch (...) {
|
||||
std::cout << "INVALID DATA on MQTT for GPIO DATA" << std::endl;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
id = std::stoi(operation);
|
||||
} catch (...) {
|
||||
std::cout << "INVALID DATA on MQTT for GPIO ID" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (id < 0 or id >= CB_NUMBER_OF_GPIOS) {
|
||||
std::cout << "INVALID GPIO ID" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
m_message.data.gpio_values[id] = value;
|
||||
send_udp();
|
||||
}
|
||||
|
||||
void gpio_bridge::send_mqtt(std::string const& topic, std::string const& message) {
|
||||
everest::lib::io::mqtt::mqtt_client::message payload;
|
||||
payload.topic = m_send_topic + topic;
|
||||
payload.payload = message;
|
||||
m_mqtt.publish(payload);
|
||||
}
|
||||
|
||||
void gpio_bridge::send_udp() {
|
||||
if (not m_udp_on_error) {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
utilities::struct_to_vector(m_message, payload.buffer);
|
||||
m_udp.tx(payload);
|
||||
}
|
||||
}
|
||||
|
||||
void gpio_bridge::handle_error_timer() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void gpio_bridge::handle_heartbeat_timer() {
|
||||
send_udp();
|
||||
}
|
||||
|
||||
void gpio_bridge::handle_udp_rx(everest::lib::io::udp::udp_payload const& payload) {
|
||||
CbManagementPacket<CbGpioPacket> data;
|
||||
if (payload.size() == sizeof(data)) {
|
||||
std::memcpy(&data, payload.buffer.data(), sizeof(data));
|
||||
for (std::size_t i = 0; i < sizeof(data.data.gpio_values) / sizeof(data.data.gpio_values[0]); ++i) {
|
||||
send_mqtt(std::to_string(i), std::to_string(data.data.gpio_values[i]));
|
||||
}
|
||||
} else {
|
||||
std::cout << "INVALID DATA SIZE in UDP RX of GPIO: " << payload.size() << " vs " << sizeof(data) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <charge_bridge/heartbeat_service.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <charge_bridge/utilities/platform_utils.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <protocol/cb_management.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
const std::uint16_t s_to_ms_factor = 1000;
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
heartbeat_service::heartbeat_service(heartbeat_config const& config,
|
||||
std::function<void(bool)> const& publish_connection_status) :
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
|
||||
m_publish_connection_status(publish_connection_status) {
|
||||
m_identifier = config.cb + "/" + config.item;
|
||||
std::memcpy(&m_config_message.data, &config.cb_config, sizeof(CbConfig));
|
||||
m_config_message.type = CbStructType::CST_HostToCb_Heartbeat;
|
||||
m_heartbeat_interval = std::chrono::milliseconds(config.interval_s * s_to_ms_factor);
|
||||
m_connection_to = std::chrono::milliseconds(config.connection_to_s * s_to_ms_factor);
|
||||
m_heartbeat_timer.set_timeout(m_heartbeat_interval);
|
||||
m_last_heartbeat_reply = std::chrono::steady_clock::time_point();
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) { handle_udp_rx(data); });
|
||||
|
||||
m_udp.set_error_handler([this](auto id, auto const& msg) {
|
||||
if (m_inital_cb_commcheck and id == 0) {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 1) << "Waiting for ChargeBridge" << std::endl;
|
||||
} else {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", id) << msg << std::endl;
|
||||
}
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
}
|
||||
|
||||
heartbeat_service::~heartbeat_service() {
|
||||
}
|
||||
|
||||
bool heartbeat_service::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.register_event_handler(&m_udp) &&
|
||||
handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); });
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
bool heartbeat_service::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
// clang-format off
|
||||
return
|
||||
handler.unregister_event_handler(&m_udp) &&
|
||||
handler.unregister_event_handler(&m_heartbeat_timer);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
void heartbeat_service::handle_error_timer() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void heartbeat_service::handle_heartbeat_timer() {
|
||||
if (not m_udp_on_error) {
|
||||
everest::lib::io::udp::udp_payload payload;
|
||||
utilities::struct_to_vector(m_config_message, payload.buffer);
|
||||
m_udp.tx(payload);
|
||||
}
|
||||
auto timeout = std::chrono::steady_clock::now() - m_last_heartbeat_reply > m_connection_to;
|
||||
if (timeout and m_cb_connected) {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 1) << "ChargeBridge connection lost" << std::endl;
|
||||
m_cb_connected = false;
|
||||
}
|
||||
|
||||
else if (not timeout and not m_cb_connected) {
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 0) << "ChargeBridge connected" << std::endl;
|
||||
m_cb_connected = true;
|
||||
}
|
||||
if (m_publish_connection_status) {
|
||||
m_publish_connection_status(m_cb_connected);
|
||||
}
|
||||
}
|
||||
|
||||
void heartbeat_service::handle_udp_rx(everest::lib::io::udp::udp_payload const& payload) {
|
||||
CbManagementPacket<CbHeartbeatReplyPacket> data;
|
||||
if (payload.size() == sizeof(data)) {
|
||||
std::memcpy(&data, payload.buffer.data(), sizeof(data));
|
||||
m_last_heartbeat_reply = std::chrono::steady_clock::now();
|
||||
auto mcu_current = static_cast<uint32_t>(data.data.uptime_ms);
|
||||
if (mcu_current <= m_mcu_timestamp) {
|
||||
m_mcu_reset_count++;
|
||||
utilities::print_error(m_identifier, "HEARTBEAT/UDP", -1)
|
||||
<< "ChargeBridge reset count " << m_mcu_reset_count << std::endl;
|
||||
}
|
||||
m_mcu_timestamp = mcu_current;
|
||||
|
||||
// TODO: Once we have the telemetry framework in EVerest, we should publish those values.
|
||||
/*printf(
|
||||
"CP: %.2f/%.2f PP: %i MCU_temp %i degC\nVoltages: 12V: %.2f, -12V: %.2f, ref %.3f, 3.3V: %.3f, core:
|
||||
%.3f\n", data.data.cp_hi_mV / 1000., data.data.cp_lo_mV / 1000., (int)data.data.pp_mOhm / 1000,
|
||||
data.data.temperature_mcu_C, data.data.vdd_12V/1000., data.data.vdd_N12V/1000., data.data.vdd_refint/1000.,
|
||||
data.data.vdd_3v3/1000., data.data.vdd_core/1000.);*/
|
||||
} else {
|
||||
std::cout << "INVALID DATA SIZE in UDP RX of HEARTBEAT: " << payload.size() << " vs " << sizeof(data)
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/plc_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <everest/io/udp/udp_payload.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
plc_bridge::plc_bridge(plc_bridge_config const& config) :
|
||||
m_tap(config.plc_tap, config.plc_ip, config.plc_netmaks, config.plc_mtu),
|
||||
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.set_timeout(5s);
|
||||
|
||||
m_tap.set_rx_handler([this](auto const& data, auto&) {
|
||||
everest::lib::io::udp::udp_payload pl;
|
||||
pl.buffer = data;
|
||||
m_udp.tx(pl);
|
||||
});
|
||||
|
||||
m_udp.set_rx_handler([this](auto const& data, auto&) { m_tap.tx(data.buffer); });
|
||||
|
||||
auto identifier = config.cb + "/" + config.item;
|
||||
m_tap.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "PLC/TAP", id) << msg << std::endl;
|
||||
m_tap_on_error = id not_eq 0;
|
||||
});
|
||||
|
||||
m_udp.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "PLC/UDP", id) << msg << std::endl;
|
||||
m_udp_on_error = id not_eq 0;
|
||||
});
|
||||
}
|
||||
|
||||
void plc_bridge::handle_timer_event() {
|
||||
if (m_udp_on_error) {
|
||||
m_udp.reset();
|
||||
}
|
||||
if (m_tap_on_error) {
|
||||
m_tap.reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool plc_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.register_event_handler(&m_tap) && result;
|
||||
result = handler.register_event_handler(&m_udp) && result;
|
||||
result = handler.register_event_handler(&m_timer, [this](auto) { handle_timer_event(); }) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool plc_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.unregister_event_handler(&m_tap) && result;
|
||||
result = handler.unregister_event_handler(&m_udp) && result;
|
||||
result = handler.unregister_event_handler(&m_timer) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "everest/io/serial/event_pty.hpp"
|
||||
#include <charge_bridge/serial_bridge.hpp>
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <cstring>
|
||||
#include <everest/io/event/fd_event_handler.hpp>
|
||||
#include <protocol/cb_can_message.h>
|
||||
|
||||
namespace {
|
||||
const int default_udp_timeout_ms = 1000;
|
||||
const std::uint32_t tcp_user_timeout_ms = 4000;
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge {
|
||||
|
||||
serial_bridge::serial_bridge(serial_bridge_config const& config) :
|
||||
m_pty(), m_tcp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto link_ok = m_symlink.set_link(m_pty.get_slave_path(), config.serial_device);
|
||||
if (not link_ok) {
|
||||
throw std::runtime_error("Failed to setup symbolic links for serial ports");
|
||||
}
|
||||
|
||||
m_tcp.set_on_ready_action([this]() {
|
||||
m_tcp.get_raw_handler()->set_keep_alive(3, 1, 1);
|
||||
m_tcp.get_raw_handler()->set_user_timeout(tcp_user_timeout_ms);
|
||||
});
|
||||
|
||||
m_pty.set_data_handler([this](auto const& data, auto&) { m_tcp.tx(data); });
|
||||
|
||||
m_tcp.set_rx_handler([this](auto const& data, auto&) { m_pty.tx(data); });
|
||||
|
||||
auto identifier = config.cb + "/" + config.item;
|
||||
m_pty.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
utilities::print_error(identifier, "SERIAL/PTY", id) << msg << std::endl;
|
||||
if (id not_eq 0) {
|
||||
m_pty.reset();
|
||||
}
|
||||
});
|
||||
|
||||
m_tcp.set_error_handler([this, identifier](auto id, auto const& msg) {
|
||||
if (m_tcp_last_error_id not_eq id) {
|
||||
utilities::print_error(identifier, "SERIAL/TCP", id) << msg << std::endl;
|
||||
m_tcp_last_error_id = id;
|
||||
}
|
||||
if (id not_eq 0) {
|
||||
m_tcp.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void serial_bridge::reset_tcp() {
|
||||
m_tcp.reset();
|
||||
}
|
||||
|
||||
std::string serial_bridge::get_slave_path() {
|
||||
return m_pty.get_slave_path();
|
||||
}
|
||||
|
||||
bool serial_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.register_event_handler(&m_pty) && result;
|
||||
result = handler.register_event_handler(&m_tcp) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool serial_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
|
||||
auto result = true;
|
||||
result = handler.unregister_event_handler(&m_pty) && result;
|
||||
result = handler.unregister_event_handler(&m_tcp) && result;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge
|
||||
@@ -0,0 +1,147 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/filesystem.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
namespace charge_bridge::filesystem_utils {
|
||||
|
||||
bool read_from_file_partial(const fs::path& file_path, const std::size_t byte_count, std::string& out_data) {
|
||||
try {
|
||||
if (fs::is_regular_file(file_path)) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (file.is_open()) {
|
||||
std::vector<char> buffer(byte_count);
|
||||
file.read(buffer.data(), byte_count);
|
||||
|
||||
std::size_t read_bytes = file.gcount();
|
||||
|
||||
if (read_bytes == byte_count) {
|
||||
out_data.assign(buffer.data(), read_bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool read_from_file(const fs::path& file_path, std::string& out_data) {
|
||||
try {
|
||||
if (fs::is_regular_file(file_path)) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (file.is_open()) {
|
||||
out_data = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool process_file(const fs::path& file_path, std::size_t buffer_size,
|
||||
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
return process_file(file, buffer_size, std::move(func));
|
||||
}
|
||||
|
||||
bool process_file(std::ifstream& file, std::size_t buffer_size,
|
||||
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func) {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> buffer(buffer_size);
|
||||
bool interupted = false;
|
||||
|
||||
while (file.read(reinterpret_cast<char*>(buffer.data()), buffer_size)) {
|
||||
interupted = func(buffer, false);
|
||||
|
||||
if (interupted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the remaining bytes
|
||||
if (interupted == false) {
|
||||
std::size_t remaining = file.gcount();
|
||||
|
||||
// Keep only remaining elements
|
||||
buffer.resize(remaining);
|
||||
func(buffer, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns true on success, fills `hdr`, and sets `image_offset` to the byte
|
||||
// position (from start of file) where the firmware image begins.
|
||||
bool read_crypt_signed_header(const fs::path& path, CryptSignedHeader& hdr, std::uint32_t& image_offset) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto read_exact = [&](void* dst, std::size_t n) -> bool {
|
||||
f.read(reinterpret_cast<char*>(dst), static_cast<std::streamsize>(n));
|
||||
return f.good() || (f.eof() && static_cast<std::size_t>(f.gcount()) == n);
|
||||
};
|
||||
|
||||
char firmware_version_str[32];
|
||||
std::memset(firmware_version_str, 0, sizeof(firmware_version_str)); // all zeros
|
||||
// 32-byte reserved header
|
||||
if (!read_exact(firmware_version_str, sizeof(firmware_version_str))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hdr.firmware_version = std::string(firmware_version_str);
|
||||
|
||||
// 1-byte signature length
|
||||
if (!read_exact(&hdr.sig_len, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// L-byte signature
|
||||
hdr.signature.resize(hdr.sig_len);
|
||||
if (hdr.sig_len > 0) {
|
||||
if (!read_exact(hdr.signature.data(), hdr.signature.size())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 1-byte NUM_SECTORS
|
||||
if (!read_exact(&hdr.num_sectors, 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 16-byte IV
|
||||
if (!read_exact(hdr.iv.data(), hdr.iv.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Where the firmware image starts:
|
||||
// offset = 32 + 1 + L + 1 + 16
|
||||
image_offset = static_cast<std::uint32_t>(f.tellg());
|
||||
// As a sanity fallback, compute if tellg() failed:
|
||||
// Disabled, since it is always false
|
||||
// if (static_cast<std::streamoff>(image_offset)< 0) {
|
||||
// image_offset = 32u + 1u + static_cast<std::uint64_t>(hdr.sig_len) + 1u + 16u;
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::filesystem_utils
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/logging.hpp>
|
||||
#include <iomanip>
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
enum class color {
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
message,
|
||||
unit,
|
||||
standard,
|
||||
terminal,
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& s, color c) {
|
||||
switch (c) {
|
||||
case color::error:
|
||||
s << "\033[31m";
|
||||
break;
|
||||
case color::success:
|
||||
s << "\033[32m";
|
||||
break;
|
||||
case color::warning:
|
||||
s << "\033[33m";
|
||||
break;
|
||||
case color::message:
|
||||
s << "\033[37m";
|
||||
break;
|
||||
case color::unit:
|
||||
s << "\033[1;37m";
|
||||
break;
|
||||
case color::terminal:
|
||||
s << "\033[m";
|
||||
break;
|
||||
case color::standard:
|
||||
default:
|
||||
s << "\033[39;49m";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
std::ostream& print_error(std::string const& device, std::string const& unit, int status) {
|
||||
// clang-format off
|
||||
auto ctrl =
|
||||
status == 0 ? color::success :
|
||||
status == -1 ? color::warning:
|
||||
color::error;
|
||||
std::cout << "[ " << ctrl << std::setw(13) << std::left << unit << color::terminal << " ] "
|
||||
<< color::unit << std::setw(20) << device << color::terminal << " ";
|
||||
if(status not_eq 0){
|
||||
if(status == -1){
|
||||
std::cout << color::standard << "WARNING ";
|
||||
}
|
||||
else{
|
||||
std::cout << color::standard << "ERROR ( " << status << " ) ";
|
||||
}
|
||||
}
|
||||
return std::cout << color::standard;
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,415 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include "c4/yml/node.hpp"
|
||||
#include <charge_bridge/utilities/parse_config.hpp>
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <charge_bridge/utilities/type_converters.hpp>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <iostream>
|
||||
|
||||
#include <filesystem>
|
||||
// clang-format off
|
||||
#include <ryml_std.hpp>
|
||||
#include <ryml.hpp>
|
||||
// clang-format on
|
||||
using namespace everest::lib::API::V1_0::types;
|
||||
|
||||
namespace {
|
||||
static const int g_cb_port_management = 6000;
|
||||
static const int g_cb_port_evse_bsp = 6001;
|
||||
static const int g_cb_port_plc = 6002;
|
||||
static const int g_cb_port_can0 = 6003;
|
||||
static const int g_cb_port_serial_1 = 6004;
|
||||
static const int g_cb_port_serial_2 = 6005;
|
||||
static const std::uint16_t default_mqtt_ping_interval_ms = 1000;
|
||||
|
||||
std::string print_yaml_location(ryml::Location const& loc) {
|
||||
std::stringstream error_msg;
|
||||
|
||||
if (loc) {
|
||||
if (not loc.name.empty()) {
|
||||
auto tmp = std::string(loc.name.str, loc.name.len);
|
||||
if (charge_bridge::utilities::string_ends_with(tmp, ".hpp")) {
|
||||
return "";
|
||||
}
|
||||
error_msg << "\n file ";
|
||||
error_msg << tmp;
|
||||
}
|
||||
error_msg << "\n line " << loc.line;
|
||||
if (loc.col) {
|
||||
error_msg << " column " << loc.col;
|
||||
}
|
||||
if (loc.offset) {
|
||||
error_msg << " offset " << loc.offset << "B";
|
||||
}
|
||||
error_msg << "\n";
|
||||
}
|
||||
return error_msg.str();
|
||||
}
|
||||
|
||||
void yaml_error_handler(const char* msg, std::size_t len, ryml::Location loc, void*) {
|
||||
std::stringstream error_msg;
|
||||
error_msg << "YAML parsing error: ";
|
||||
error_msg << print_yaml_location(loc);
|
||||
error_msg.write(msg, len);
|
||||
|
||||
std::cerr << error_msg.str() << std::endl;
|
||||
throw std::runtime_error(error_msg.str());
|
||||
}
|
||||
|
||||
void print_location(ryml::ConstNodeRef node, ryml::Parser& parser) {
|
||||
std::cerr << print_yaml_location(node.location(parser)) << std::endl;
|
||||
}
|
||||
|
||||
void load_yaml_file(const std::string& filename, ryml::Parser* parser, ryml::Tree* t) {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Could not open file: " + filename);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string file_content = buffer.str();
|
||||
|
||||
parse_in_arena(parser, ryml::to_csubstr(filename), ryml::to_csubstr(file_content), t);
|
||||
}
|
||||
|
||||
template <class T> c4::yml::ConstNodeRef decode(c4::yml::ConstNodeRef const& node, T& rhs) {
|
||||
using namespace charge_bridge::utilities;
|
||||
node >> rhs;
|
||||
return node;
|
||||
}
|
||||
|
||||
std::pair<std::string, c4::yml::ConstNodeRef> find_node(c4::yml::NodeRef& config, std::string const& main,
|
||||
std::string const& sub) {
|
||||
auto main_str = ryml::to_csubstr(main);
|
||||
auto node_str = main;
|
||||
c4::yml::ConstNodeRef node;
|
||||
if (not sub.empty()) {
|
||||
node_str = node_str + "::" + sub;
|
||||
auto sub_str = ryml::to_csubstr(sub);
|
||||
node = config.find_child(main_str);
|
||||
if (not node.invalid()) {
|
||||
node = config.find_child(main_str).find_child(sub_str);
|
||||
}
|
||||
} else {
|
||||
node = config[main_str];
|
||||
}
|
||||
return {node_str, node};
|
||||
}
|
||||
|
||||
template <class DataT>
|
||||
bool get_node_impl(c4::yml::ConstNodeRef node, ryml::Parser& parser, std::string const& node_str, DataT& data) {
|
||||
if (node.invalid()) {
|
||||
std::cerr << "Node not found: " << node_str << std::endl;
|
||||
throw std::runtime_error("");
|
||||
}
|
||||
try {
|
||||
decode(node, data);
|
||||
return true;
|
||||
} catch (std::exception const& e) {
|
||||
std::cerr << "Cannot parse config: " << node_str << std::endl;
|
||||
std::cerr << e.what() << std::endl;
|
||||
} catch (charge_bridge::utilities::yml_node_error const& e) {
|
||||
std::cerr << "Error source: \n"
|
||||
<< " parent " << node_str << "\n"
|
||||
<< " data " << e.m_msg << std::flush;
|
||||
print_location(e.m_node, parser);
|
||||
}
|
||||
throw std::runtime_error("");
|
||||
}
|
||||
|
||||
struct RymlCallbackInitializer {
|
||||
RymlCallbackInitializer() {
|
||||
ryml::set_callbacks({nullptr, nullptr, nullptr, yaml_error_handler});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
void parse_config_impl(c4::yml::NodeRef& config, charge_bridge_config& c, std::filesystem::path const& config_path,
|
||||
ryml::Parser& parser) {
|
||||
auto get_node = [&config, &parser](auto& data, std::string const& main, std::string const& sub = "") {
|
||||
auto [node_str, node] = find_node(config, main, sub);
|
||||
get_node_impl(node, parser, node_str, data);
|
||||
};
|
||||
|
||||
auto get_node_or_default = [&get_node, &config](auto& data, std::string const& main, std::string const& sub,
|
||||
auto fallback) {
|
||||
auto [node_str, node] = find_node(config, main, sub);
|
||||
if (node.invalid()) {
|
||||
data = fallback;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
get_node(data, main, sub);
|
||||
} catch (...) {
|
||||
data = fallback;
|
||||
}
|
||||
};
|
||||
|
||||
auto get_block = [&config, &c](std::string const& block, auto& block_cfg, auto const& ftor) {
|
||||
bool enable = false;
|
||||
auto block_str = ryml::to_csubstr(block);
|
||||
if (not config.find_child(block_str).invalid()) {
|
||||
if (config[block_str].find_child("enable").invalid()) {
|
||||
enable = true;
|
||||
} else {
|
||||
decode(config[block_str]["enable"], enable);
|
||||
}
|
||||
}
|
||||
if (enable) {
|
||||
block_cfg.emplace();
|
||||
ftor(*block_cfg, block);
|
||||
block_cfg->cb = c.cb_name;
|
||||
block_cfg->item = block;
|
||||
}
|
||||
};
|
||||
|
||||
get_node(c.cb_name, "charge_bridge", "name");
|
||||
|
||||
get_node(c.cb_remote, "charge_bridge", "ip");
|
||||
|
||||
c.cb_port = g_cb_port_management;
|
||||
|
||||
get_block("can_0", c.can0, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.can_device, main, "local");
|
||||
cfg.cb_port = g_cb_port_can0;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
get_block("serial_1", c.serial1, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.serial_device, main, "local");
|
||||
cfg.cb_port = g_cb_port_serial_1;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
get_block("serial_2", c.serial2, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.serial_device, main, "local");
|
||||
cfg.cb_port = g_cb_port_serial_2;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
// FIXME (JH) serial3 not availabe in first release
|
||||
// get_block("serial_3", c.serial3, [&](auto& cfg, auto const& main) {
|
||||
// get_node(main, "local", cfg.serial_device);
|
||||
// get_node(main, "port", cfg.cb_port);
|
||||
// cfg.cb_remote = c.cb_remote;
|
||||
// });
|
||||
|
||||
get_block("plc", c.plc, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.plc_tap, main, "tap");
|
||||
get_node(cfg.plc_ip, main, "ip");
|
||||
get_node(cfg.plc_netmaks, main, "netmask");
|
||||
get_node(cfg.plc_mtu, main, "mtu");
|
||||
cfg.cb_port = g_cb_port_plc;
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
});
|
||||
|
||||
{
|
||||
bool wants_ev = false;
|
||||
bool wants_evse = false;
|
||||
get_node_or_default(wants_ev, "ev_bsp", "enable", false);
|
||||
get_node_or_default(wants_evse, "evse_bsp", "enable", false);
|
||||
if (wants_ev && wants_evse) {
|
||||
std::cerr << "Configuration error: Cannot enable EVSE and EV BSP at the same time" << std::endl;
|
||||
throw std::exception();
|
||||
}
|
||||
}
|
||||
|
||||
get_block("evse_bsp", c.bsp, [&](auto& cfg, auto const& main) {
|
||||
cfg.cb_port = g_cb_port_evse_bsp;
|
||||
cfg.api.evse.enabled = true;
|
||||
get_node(cfg.api.evse.module_id, main, "module_id");
|
||||
get_node(cfg.api.mqtt_remote, main, "mqtt_remote");
|
||||
get_node_or_default(cfg.api.mqtt_bind, main, "mqtt_bind", "");
|
||||
get_node(cfg.api.mqtt_port, main, "mqtt_port");
|
||||
get_node_or_default(cfg.api.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms",
|
||||
default_mqtt_ping_interval_ms);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
get_node(cfg.api.evse.capabilities, main, "capabilities");
|
||||
get_node(cfg.api.ovm.enabled, main, "ovm_enabled");
|
||||
get_node(cfg.api.ovm.module_id, main, "ovm_module_id");
|
||||
});
|
||||
|
||||
if (not c.bsp.has_value()) {
|
||||
get_block("ev_bsp", c.bsp, [&](auto& cfg, auto const& main) {
|
||||
cfg.cb_port = g_cb_port_evse_bsp;
|
||||
cfg.api.ev.enabled = true;
|
||||
get_node(cfg.api.ev.module_id, main, "module_id");
|
||||
get_node(cfg.api.mqtt_remote, main, "mqtt_remote");
|
||||
get_node_or_default(cfg.api.mqtt_bind, main, "mqtt_bind", "");
|
||||
get_node(cfg.api.mqtt_port, main, "mqtt_port");
|
||||
get_node_or_default(cfg.api.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms",
|
||||
default_mqtt_ping_interval_ms);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
get_node(cfg.api.ovm.enabled, main, "ovm_enabled");
|
||||
get_node(cfg.api.ovm.module_id, main, "ovm_module_id");
|
||||
});
|
||||
}
|
||||
|
||||
get_block("gpio", c.gpio, [&](auto& cfg, auto const& main) {
|
||||
get_node(cfg.interval_s, main, "interval_s");
|
||||
get_node(cfg.mqtt_remote, main, "mqtt_remote");
|
||||
get_node_or_default(cfg.mqtt_bind, main, "mqtt_bind", "");
|
||||
get_node(cfg.mqtt_port, main, "mqtt_port");
|
||||
get_node_or_default(cfg.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms", default_mqtt_ping_interval_ms);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
cfg.cb_port = c.cb_port;
|
||||
});
|
||||
|
||||
get_block("heartbeat", c.heartbeat, [&](auto& cfg, auto const& main) {
|
||||
get_node_or_default(cfg.interval_s, main, "interval_s", 1);
|
||||
get_node_or_default(cfg.connection_to_s, main, "connection_to_s", 3 * cfg.interval_s);
|
||||
cfg.cb_remote = c.cb_remote;
|
||||
cfg.cb_port = c.cb_port;
|
||||
get_node(cfg.cb_config.network, "charge_bridge");
|
||||
get_node(cfg.cb_config.safety, "safety");
|
||||
|
||||
std::memset(cfg.cb_config.gpios, 0, CB_NUMBER_OF_GPIOS * sizeof(CbGpioConfig));
|
||||
std::memset(cfg.cb_config.uarts, 0, CB_NUMBER_OF_UARTS * sizeof(CbUartConfig));
|
||||
if (c.serial1) {
|
||||
get_node(cfg.cb_config.uarts[0], "serial_1");
|
||||
}
|
||||
if (c.serial2) {
|
||||
get_node(cfg.cb_config.uarts[1], "serial_2");
|
||||
}
|
||||
// FIXME (JH) serial 3 not available in first release
|
||||
// if (c.serial3) {
|
||||
// get_main_node("serial_3", cfg.cb_config.uarts[2]);
|
||||
// }
|
||||
if (c.gpio) {
|
||||
for (auto i = 0; i < CB_NUMBER_OF_GPIOS; ++i) {
|
||||
get_node(cfg.cb_config.gpios[i], "gpio", "gpio_" + std::to_string(i));
|
||||
}
|
||||
}
|
||||
if (c.can0) {
|
||||
get_node(cfg.cb_config.can, "can_0");
|
||||
}
|
||||
get_node(cfg.cb_config.plc_powersaving_mode, "plc", "powersaving_mode");
|
||||
cfg.cb_config.config_version = CB_CONFIG_VERSION;
|
||||
});
|
||||
|
||||
get_node(c.firmware.fw_path, "charge_bridge", "fw_file");
|
||||
get_node(c.firmware.fw_update_on_start, "charge_bridge", "fw_update_on_start");
|
||||
|
||||
// If the path to the firmware file is relative, make it relative to the config file
|
||||
std::filesystem::path fw_path = c.firmware.fw_path;
|
||||
if (fw_path.is_relative()) {
|
||||
c.firmware.fw_path = config_path.parent_path().append(c.firmware.fw_path);
|
||||
}
|
||||
|
||||
c.firmware.cb_remote = c.cb_remote;
|
||||
c.firmware.cb_port = c.cb_port;
|
||||
c.firmware.cb = c.cb_name;
|
||||
}
|
||||
|
||||
charge_bridge_config set_config_placeholders(charge_bridge_config const& src, charge_bridge_config& result,
|
||||
std::string const& ip, std::size_t index) {
|
||||
auto index_str = std::to_string(index);
|
||||
result = src;
|
||||
auto replace = [index_str](std::string& src) { replace_all_in_place(src, "##", index_str); };
|
||||
|
||||
result.cb_remote = ip;
|
||||
result.firmware.cb_remote = ip;
|
||||
replace(result.cb_name);
|
||||
result.firmware.cb = result.cb_name;
|
||||
if (result.can0.has_value()) {
|
||||
result.can0->cb_remote = ip;
|
||||
result.can0->cb = result.cb_name;
|
||||
replace(result.can0->can_device);
|
||||
}
|
||||
if (result.serial1.has_value()) {
|
||||
result.serial1->cb_remote = ip;
|
||||
result.serial1->cb = result.cb_name;
|
||||
replace(result.serial1->serial_device);
|
||||
}
|
||||
if (result.serial2.has_value()) {
|
||||
result.serial2->cb_remote = ip;
|
||||
result.serial2->cb = result.cb_name;
|
||||
replace(result.serial2->serial_device);
|
||||
}
|
||||
if (result.serial3.has_value()) {
|
||||
result.serial3->cb_remote = ip;
|
||||
result.serial3->cb = result.cb_name;
|
||||
replace(result.serial3->serial_device);
|
||||
}
|
||||
if (result.plc.has_value()) {
|
||||
result.plc->cb_remote = ip;
|
||||
result.plc->cb = result.cb_name;
|
||||
replace(result.plc->plc_tap);
|
||||
}
|
||||
if (result.bsp.has_value()) {
|
||||
result.bsp->cb_remote = ip;
|
||||
result.bsp->cb = result.cb_name;
|
||||
replace(result.bsp->api.evse.module_id);
|
||||
replace(result.bsp->api.ev.module_id);
|
||||
replace(result.bsp->api.ovm.module_id);
|
||||
}
|
||||
if (result.heartbeat.has_value()) {
|
||||
result.heartbeat->cb = result.cb_name;
|
||||
result.heartbeat->cb_remote = ip;
|
||||
}
|
||||
if (result.gpio.has_value()) {
|
||||
result.gpio->cb = result.cb_name;
|
||||
result.gpio->cb_remote = ip;
|
||||
}
|
||||
|
||||
if (result.heartbeat.has_value()) {
|
||||
auto& raw = result.heartbeat->cb_config.network.mdns_name;
|
||||
std::string item = raw;
|
||||
replace(item);
|
||||
auto limit = sizeof(raw);
|
||||
if (item.size() > limit) {
|
||||
item = "cb_" + index_str;
|
||||
std::cout << "WARNING: Replacement for mdns_name is too long. Fallback to '" + item + "'" << std::endl;
|
||||
}
|
||||
std::memset(raw, 0, limit);
|
||||
std::memcpy(raw, item.c_str(), std::min(item.size(), limit));
|
||||
|
||||
result.heartbeat->cb_remote = ip;
|
||||
result.heartbeat->cb = result.cb_name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<charge_bridge_config> parse_config_multi(std::string const& config_file) {
|
||||
const static RymlCallbackInitializer ryml_callback_initializer;
|
||||
|
||||
try {
|
||||
ryml::EventHandlerTree evt_handler = {};
|
||||
ryml::Parser parser(&evt_handler, ryml::ParserOptions().locations(true));
|
||||
ryml::Tree config_tree;
|
||||
load_yaml_file(config_file, &parser, &config_tree);
|
||||
c4::yml::NodeRef config = config_tree.rootref();
|
||||
if (config.invalid()) {
|
||||
std::cerr << "Config file not found: " << config_file << std::endl;
|
||||
return {};
|
||||
}
|
||||
charge_bridge_config base_config;
|
||||
parse_config_impl(config, base_config, config_file, parser);
|
||||
|
||||
auto ip_list_node = config.find_child("charge_bridge_ip_list");
|
||||
if (ip_list_node.invalid()) {
|
||||
return {base_config};
|
||||
}
|
||||
std::vector<std::string> ip_list;
|
||||
ip_list_node >> ip_list;
|
||||
std::vector<charge_bridge_config> cb_config_list(ip_list.size());
|
||||
|
||||
for (std::size_t i = 0; i < ip_list.size(); ++i) {
|
||||
set_config_placeholders(base_config, cb_config_list[i], ip_list[i], i);
|
||||
}
|
||||
|
||||
return cb_config_list;
|
||||
} catch (...) {
|
||||
std::cerr << "FAILED to parse configuration!" << std::endl;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "protocol/cb_config.h"
|
||||
#include <charge_bridge/charge_bridge.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
std::string to_string(CbCanBaudrate value) {
|
||||
switch (value) {
|
||||
case CBCBR_125000:
|
||||
return "125000";
|
||||
case CBCBR_250000:
|
||||
return "250000";
|
||||
case CBCBR_500000:
|
||||
return "500000";
|
||||
case CBCBR_1000000:
|
||||
return "1000000";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid bitrate";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartBaudrate value) {
|
||||
switch (value) {
|
||||
case CBUBR_9600:
|
||||
return "9600";
|
||||
case CBUBR_19200:
|
||||
return "19200";
|
||||
case CBUBR_38400:
|
||||
return "38400";
|
||||
case CBUBR_57600:
|
||||
return "57600";
|
||||
case CBUBR_115200:
|
||||
return "115200";
|
||||
case CBUBR_230400:
|
||||
return "230400";
|
||||
case CBUBR_250000:
|
||||
return "250000";
|
||||
case CBUBR_460800:
|
||||
return "460800";
|
||||
case CBUBR_500000:
|
||||
return "500000";
|
||||
case CBUBR_1000000:
|
||||
return "1000000";
|
||||
case CBUBR_2000000:
|
||||
return "2000000";
|
||||
case CBUBR_3000000:
|
||||
return "3000000";
|
||||
case CBUBR_4000000:
|
||||
return "4000000";
|
||||
case CBUBR_6000000:
|
||||
return "6000000";
|
||||
case CBUBR_8000000:
|
||||
return "8000000";
|
||||
case CBUBR_10000000:
|
||||
return "10000000";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid baudrate";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartParity value) {
|
||||
switch (value) {
|
||||
case CBUP_None:
|
||||
return "N";
|
||||
case CBUP_Odd:
|
||||
return "O";
|
||||
case CBUP_Even:
|
||||
return "E";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid parity";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartStopbits value) {
|
||||
switch (value) {
|
||||
case CBUS_OneStopBit:
|
||||
return "1";
|
||||
case CBUS_TwoStopBits:
|
||||
return "2";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Invalid parity";
|
||||
}
|
||||
|
||||
std::string to_string(CbUartConfig const& value) {
|
||||
std::stringstream data;
|
||||
data << to_string(value.baudrate) << " 8" << to_string(value.parity) << to_string(value.stopbits);
|
||||
return data.str();
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/string.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
bool string_starts_with(std::string_view const& str, std::string_view const& pattern) {
|
||||
return str.rfind(pattern, 0) == 0;
|
||||
}
|
||||
|
||||
bool string_ends_with(std::string const& str, std::string const& pattern) {
|
||||
if (pattern.size() > str.size())
|
||||
return false;
|
||||
return std::equal(pattern.rbegin(), pattern.rend(), str.rbegin());
|
||||
}
|
||||
|
||||
std::string string_after_pattern(std::string_view const& str, std::string_view const& pattern) {
|
||||
if (charge_bridge::utilities::string_starts_with(str, pattern)) {
|
||||
return static_cast<std::string>(str.substr(pattern.length()));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string& replace_all_in_place(std::string& source, std::string const& placeholder, std::string const& substitute) {
|
||||
|
||||
if (placeholder.empty()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
std::size_t start_pos = 0;
|
||||
|
||||
while ((start_pos = source.find(placeholder, start_pos)) != std::string::npos) {
|
||||
source.replace(start_pos, placeholder.length(), substitute);
|
||||
start_pos += substitute.length();
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
std::string replace_all(std::string const& source, std::string const& placeholder, std::string const& substitute) {
|
||||
std::string result = source;
|
||||
return replace_all_in_place(result, placeholder, substitute);
|
||||
}
|
||||
|
||||
std::set<std::string> csv_to_set(std::string const& str) {
|
||||
std::set<std::string> result;
|
||||
std::stringstream ss(str);
|
||||
std::string item;
|
||||
|
||||
while (std::getline(ss, item, ',')) {
|
||||
if (!item.empty()) {
|
||||
result.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include <charge_bridge/utilities/symlink.hpp>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
symlink::symlink() {
|
||||
}
|
||||
|
||||
symlink::symlink(std::string const& src, std::string const& tar) {
|
||||
auto result = set_link(src, tar);
|
||||
if (not result) {
|
||||
std::string msg = "Cannot create symbolic link from '" + src + "' to '" + tar + "'";
|
||||
perror(msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool symlink::set_link(std::string const& src, std::string const& tar) {
|
||||
m_tar = tar;
|
||||
del_link();
|
||||
auto result = ::symlink(src.c_str(), tar.c_str()) == 0;
|
||||
if (result) {
|
||||
m_tar = tar;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool symlink::del_link() {
|
||||
auto result = true;
|
||||
if (not m_tar.empty()) {
|
||||
auto code = ::unlink(m_tar.c_str());
|
||||
result = code == 0 or code == ENOENT;
|
||||
m_tar = "";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
symlink::~symlink() {
|
||||
if (not m_tar.empty()) {
|
||||
auto result = del_link();
|
||||
if (not result) {
|
||||
std::string msg = "Cannot delete symbolic link '" + m_tar + "'";
|
||||
perror(msg.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,83 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "everest/io/event/fd_event_handler.hpp"
|
||||
#include <charge_bridge/utilities/sync_udp_client.hpp>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
using reply = sync_udp_client::reply;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
sync_udp_client::sync_udp_client(std::string const& remote, std::uint16_t port) : m_retries(0), m_timeout_ms(1000) {
|
||||
init(remote, port);
|
||||
}
|
||||
|
||||
sync_udp_client::sync_udp_client(std::string const& remote, std::uint16_t port, std::uint16_t retries,
|
||||
std::uint16_t timeout_ms) :
|
||||
m_retries(retries), m_timeout_ms(timeout_ms) {
|
||||
init(remote, port);
|
||||
}
|
||||
|
||||
void sync_udp_client::init(std::string const& remote, std::uint16_t port) {
|
||||
m_udp.open_as_client(remote, port);
|
||||
m_handler.register_event_handler(
|
||||
m_udp.get_fd(), [this](auto) {}, everest::lib::io::event::poll_events::read);
|
||||
}
|
||||
|
||||
reply sync_udp_client::request_reply(udp_payload const& payload) {
|
||||
return request_reply(payload, m_timeout_ms, m_retries);
|
||||
}
|
||||
|
||||
reply sync_udp_client::request_reply(udp_payload const& payload, std::uint16_t timeout_ms, std::uint16_t retries) {
|
||||
udp_payload result;
|
||||
clear_socket();
|
||||
if (not m_udp.tx(payload)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
for (std::uint16_t i = 0; i < retries; ++i) {
|
||||
if (not m_handler.poll(std::chrono::milliseconds(timeout_ms))) {
|
||||
if (not m_udp.tx(payload)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (not m_udp.rx(result)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool sync_udp_client::tx(udp_payload const& payload) {
|
||||
return m_udp.tx(payload);
|
||||
}
|
||||
|
||||
reply sync_udp_client::rx() {
|
||||
return rx(m_timeout_ms);
|
||||
}
|
||||
|
||||
reply sync_udp_client::rx(std::uint16_t timeout_ms) {
|
||||
udp_payload result;
|
||||
if (not m_handler.poll(std::chrono::milliseconds(timeout_ms))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (not m_udp.rx(result)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sync_udp_client::is_open() {
|
||||
return m_udp.is_open();
|
||||
}
|
||||
|
||||
void sync_udp_client::clear_socket() {
|
||||
udp_payload tmp;
|
||||
while (m_handler.poll(0ms)) {
|
||||
m_udp.rx(tmp);
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
@@ -0,0 +1,610 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
#include "protocol/cb_config.h"
|
||||
#include <charge_bridge/utilities/type_converters.hpp>
|
||||
#include <cmath>
|
||||
#include <everest_api_types/evse_board_support/API.hpp>
|
||||
#include <everest_api_types/evse_board_support/codec.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
// clang-format off
|
||||
#include <optional>
|
||||
#include <ryml_std.hpp>
|
||||
#include <ryml.hpp>
|
||||
// clang-format on
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
template <class T> void decode(c4::yml::ConstNodeRef const& node, T& rhs) {
|
||||
using namespace charge_bridge::utilities;
|
||||
try {
|
||||
node >> rhs;
|
||||
} catch (...) {
|
||||
std::string value;
|
||||
std::string key;
|
||||
if (node.has_key()) {
|
||||
key = std::string(node.key().str, node.key().len);
|
||||
}
|
||||
if (node.has_val()) {
|
||||
value = std::string(node.val().str, node.val().len);
|
||||
}
|
||||
throw charge_bridge::utilities::yml_node_error(node, key + "::" + value);
|
||||
}
|
||||
}
|
||||
|
||||
template <class T, class TmpT>
|
||||
T decode_t(c4::yml::ConstNodeRef parent, std::string const& node_id, std::function<T(TmpT)> const& transform,
|
||||
std::optional<T> const& def_value = std::nullopt) {
|
||||
auto index = ryml::to_csubstr(node_id);
|
||||
auto local_node = parent.find_child(index);
|
||||
if (local_node.invalid()) {
|
||||
if (def_value.has_value()) {
|
||||
return def_value.value();
|
||||
}
|
||||
|
||||
throw charge_bridge::utilities::yml_node_error(parent, "Cannot find nested node: " + node_id);
|
||||
}
|
||||
TmpT tmp;
|
||||
decode(local_node, tmp);
|
||||
return transform(tmp);
|
||||
}
|
||||
|
||||
template <class Ftor, class T>
|
||||
auto decode_t(c4::yml::ConstNodeRef parent, std::string const& node_id, Ftor transform, std::optional<T> const& t) {
|
||||
std::function g = transform;
|
||||
return decode_t(parent, node_id, g, t);
|
||||
}
|
||||
|
||||
template <class Ftor> auto decode_t(c4::yml::ConstNodeRef parent, std::string const& node_id, Ftor transform) {
|
||||
std::function g = transform;
|
||||
return decode_t(parent, node_id, g);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T decode(c4::yml::ConstNodeRef parent, std::string const& node_id, std::optional<T> const& def_value = std::nullopt) {
|
||||
auto identity = [](T v) { return v; };
|
||||
return decode_t(parent, node_id, identity, def_value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace charge_bridge::utilities {
|
||||
|
||||
yml_node_error::yml_node_error(c4::yml::ConstNodeRef node) : m_node(node) {
|
||||
}
|
||||
|
||||
yml_node_error::yml_node_error(c4::yml::ConstNodeRef node, std::string const& msg) : m_node(node), m_msg(msg) {
|
||||
}
|
||||
|
||||
bool decode_CbGpioMode(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "Input") {
|
||||
rhs = CbGpioMode::CBG_Input;
|
||||
return true;
|
||||
} else if (value == "Output") {
|
||||
rhs = CbGpioMode::CBG_Output;
|
||||
return true;
|
||||
} else if (value == "Pwm_Input") {
|
||||
rhs = CbGpioMode::CBG_Pwm_Input;
|
||||
return true;
|
||||
} else if (value == "Pwm_Output") {
|
||||
rhs = CbGpioMode::CBG_Pwm_Output;
|
||||
return true;
|
||||
} else if (value == "RS485_2_DE") {
|
||||
rhs = CbGpioMode::CBG_RS485_2_DE;
|
||||
return true;
|
||||
} else if (value == "Rcd_Selftest_Output") {
|
||||
rhs = CbGpioMode::CBG_Rcd_Selftest_Output;
|
||||
return true;
|
||||
} else if (value == "Rcd_Error_Input") {
|
||||
rhs = CbGpioMode::CBG_Rcd_Error_Input;
|
||||
return true;
|
||||
} else if (value == "Rcd_PWM_Input") {
|
||||
rhs = CbGpioMode::CBG_Rcd_PWM_Input;
|
||||
return true;
|
||||
} else if (value == "MotorLock_1") {
|
||||
rhs = CbGpioMode::CBG_MotorLock_1;
|
||||
return true;
|
||||
} else if (value == "MotorLock_2") {
|
||||
rhs = CbGpioMode::CBG_MotorLock_2;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbGpioPulls(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "NoPull") {
|
||||
rhs = CBGP_NoPull;
|
||||
return true;
|
||||
}
|
||||
if (value == "PullUp") {
|
||||
rhs = CBGP_PullUp;
|
||||
return true;
|
||||
}
|
||||
if (value == "PullDown") {
|
||||
rhs = CBGP_PullDown;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbUartBaudrate(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "9600") {
|
||||
rhs = CBUBR_9600;
|
||||
return true;
|
||||
}
|
||||
if (value == "19200") {
|
||||
rhs = CBUBR_19200;
|
||||
return true;
|
||||
}
|
||||
if (value == "38400") {
|
||||
rhs = CBUBR_38400;
|
||||
return true;
|
||||
}
|
||||
if (value == "57600") {
|
||||
rhs = CBUBR_57600;
|
||||
return true;
|
||||
}
|
||||
if (value == "115200") {
|
||||
rhs = CBUBR_115200;
|
||||
return true;
|
||||
}
|
||||
if (value == "230400") {
|
||||
rhs = CBUBR_230400;
|
||||
return true;
|
||||
}
|
||||
if (value == "250000") {
|
||||
rhs = CBUBR_250000;
|
||||
return true;
|
||||
}
|
||||
if (value == "460800") {
|
||||
rhs = CBUBR_460800;
|
||||
return true;
|
||||
}
|
||||
if (value == "500000") {
|
||||
rhs = CBUBR_500000;
|
||||
return true;
|
||||
}
|
||||
if (value == "1000000") {
|
||||
rhs = CBUBR_1000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "2000000") {
|
||||
rhs = CBUBR_2000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "3000000") {
|
||||
rhs = CBUBR_3000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "4000000") {
|
||||
rhs = CBUBR_4000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "6000000") {
|
||||
rhs = CBUBR_6000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "8000000") {
|
||||
rhs = CBUBR_8000000;
|
||||
return true;
|
||||
}
|
||||
if (value == "10000000") {
|
||||
rhs = CBUBR_10000000;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbUartStopbits(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "OneStopBit") {
|
||||
rhs = CBUS_OneStopBit;
|
||||
return true;
|
||||
}
|
||||
if (value == "TwoStopBits") {
|
||||
rhs = CBUS_TwoStopBits;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbUartParity(c4::yml::ConstNodeRef const& node, CbUartParity& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "None") {
|
||||
rhs = CBUP_None;
|
||||
return true;
|
||||
}
|
||||
if (value == "Odd") {
|
||||
rhs = CBUP_Odd;
|
||||
return true;
|
||||
}
|
||||
if (value == "Even") {
|
||||
rhs = CBUP_Even;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbCanBaudrate(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "125000") {
|
||||
rhs = CBCBR_125000;
|
||||
return true;
|
||||
}
|
||||
if (value == "250000") {
|
||||
rhs = CBCBR_250000;
|
||||
return true;
|
||||
}
|
||||
if (value == "500000") {
|
||||
rhs = CBCBR_500000;
|
||||
return true;
|
||||
}
|
||||
if (value == "1000000") {
|
||||
rhs = CBCBR_1000000;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw yml_node_error(node);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbRelayMode(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "PowerRelay") {
|
||||
rhs = CBR_PowerRelay;
|
||||
return true;
|
||||
}
|
||||
if (value == "UserRelay") {
|
||||
rhs = CBR_UserRelay;
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(node);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_CbSafetyMode(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs) {
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
std::string value;
|
||||
decode(node, value);
|
||||
|
||||
if (value == "disabled") {
|
||||
rhs = CBSM_disabled;
|
||||
return true;
|
||||
}
|
||||
if (value == "US") {
|
||||
rhs = CBSM_US;
|
||||
return true;
|
||||
}
|
||||
if (value == "EU") {
|
||||
rhs = CBSM_EU;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw yml_node_error(node);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool decode_RelayConfig(c4::yml::ConstNodeRef const& node, RelayConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.relay_mode = decode<decltype(rhs.relay_mode)>(node, "relay_mode");
|
||||
rhs.feedback_enabled = decode_t(node, "feedback_enabled", [](bool tmp) -> uint8_t { return tmp ? 1 : 0; });
|
||||
rhs.feedback_delay_ms = decode<decltype(rhs.feedback_delay_ms)>(node, "feedback_delay_ms");
|
||||
|
||||
rhs.feedback_inverted = decode_t(node, "feedback_inverted", [](bool tmp) -> uint8_t { return tmp ? 1 : 0; });
|
||||
rhs.pwm_dc = decode_t(node, "pwm_dc", [](uint8_t tmp) { return std::min<uint8_t>(tmp, 100); });
|
||||
rhs.pwm_delay_ms = decode<decltype(rhs.pwm_delay_ms)>(node, "pwm_delay_ms");
|
||||
rhs.switchoff_delay_ms = decode<decltype(rhs.switchoff_delay_ms)>(node, "switchoff_delay_ms");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_SafetyConfig(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.pp_mode = decode<decltype(rhs.pp_mode)>(node, "pp_mode");
|
||||
rhs.cp_avg_ms = decode<decltype(rhs.cp_avg_ms)>(node, "cp_avg_ms", 10);
|
||||
rhs.temperature_limit_pt1000_C =
|
||||
decode<decltype(rhs.temperature_limit_pt1000_C)>(node, "temperature_limit_pt1000_C", 0);
|
||||
rhs.inverted_emergency_input = decode<decltype(rhs.inverted_emergency_input)>(node, "inverted_emergency_input", 0);
|
||||
|
||||
rhs.relays[0] = decode<RelayConfig>(node, "relay_1");
|
||||
rhs.relays[1] = decode<RelayConfig>(node, "relay_2");
|
||||
rhs.relays[2] = decode<RelayConfig>(node, "relay_3");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbGpioConfig(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
rhs.mode = decode<decltype(rhs.mode)>(node, "mode");
|
||||
rhs.pulls = decode<decltype(rhs.pulls)>(node, "pulls");
|
||||
rhs.strap_option_mdns_naming = decode_t(
|
||||
node, "mdns", [](bool tmp) -> uint8_t { return tmp ? 1 : 0; }, std::make_optional<uint8_t>(0));
|
||||
rhs.mode_config = decode<decltype(rhs.mode_config)>(node, "config", 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbUartConfig(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.baudrate = decode<decltype(rhs.baudrate)>(node, "baudrate");
|
||||
rhs.stopbits = decode<decltype(rhs.stopbits)>(node, "stopbits");
|
||||
rhs.parity = decode<decltype(rhs.parity)>(node, "parity");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbCanConfig(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
rhs.baudrate = decode<decltype(rhs.baudrate)>(node, "baudrate");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_CbNetworkConfig(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ConstNodeRef local_node = node;
|
||||
local_node = node.find_child("mdns_name");
|
||||
|
||||
if (not local_node.invalid()) {
|
||||
|
||||
auto limit = sizeof(rhs.mdns_name);
|
||||
std::string name;
|
||||
decode(local_node, name);
|
||||
|
||||
if (name.size() >= limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.size() >= limit) {
|
||||
return false;
|
||||
}
|
||||
std::memset(rhs.mdns_name, 0, limit);
|
||||
std::memcpy(rhs.mdns_name, name.c_str(), std::min(name.size(), limit));
|
||||
return true;
|
||||
}
|
||||
throw yml_node_error(local_node);
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace EXT_API = everest::lib::API;
|
||||
namespace EXT_API_BSP = EXT_API::V1_0::types::evse_board_support;
|
||||
|
||||
bool decode_Connector_type(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
c4::csubstr value_view = node.val();
|
||||
size_t total_size = 2 + value_view.size();
|
||||
std::string quoted_value;
|
||||
quoted_value.reserve(total_size);
|
||||
quoted_value += "\"";
|
||||
quoted_value.append(value_view.data(), value_view.size());
|
||||
quoted_value += "\"";
|
||||
return EXT_API::deserialize(quoted_value, rhs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_HardwareCapabilities(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs) {
|
||||
using ryml::ConstNodeRef;
|
||||
|
||||
if (node.invalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rhs.max_current_A_import = decode<decltype(rhs.max_current_A_import)>(node, "max_current_A_import");
|
||||
rhs.min_current_A_import = decode<decltype(rhs.min_current_A_import)>(node, "min_current_A_import");
|
||||
rhs.max_phase_count_import = decode<decltype(rhs.max_phase_count_import)>(node, "max_phase_count_import");
|
||||
rhs.min_phase_count_import = decode<decltype(rhs.min_phase_count_import)>(node, "min_phase_count_import");
|
||||
rhs.max_current_A_export = decode<decltype(rhs.max_current_A_export)>(node, "max_current_A_export");
|
||||
rhs.min_current_A_export = decode<decltype(rhs.min_current_A_export)>(node, "min_current_A_export");
|
||||
rhs.max_phase_count_export = decode<decltype(rhs.max_phase_count_export)>(node, "max_phase_count_export");
|
||||
rhs.min_phase_count_export = decode<decltype(rhs.min_phase_count_export)>(node, "min_phase_count_export");
|
||||
rhs.supports_changing_phases_during_charging = decode<decltype(rhs.supports_changing_phases_during_charging)>(
|
||||
node, "supports_changing_phases_during_charging");
|
||||
rhs.supports_cp_state_E = decode<decltype(rhs.supports_cp_state_E)>(node, "supports_cp_state_E", false);
|
||||
rhs.connector_type = decode<decltype(rhs.connector_type)>(node, "connector_type");
|
||||
auto tmp = decode<float>(node, "max_plug_temperature_C", NAN);
|
||||
rhs.max_plug_temperature_C = std::isnan(tmp) ? std::nullopt : std::make_optional<float>(tmp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs) {
|
||||
if (decode_CbGpioMode(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbGpioMode");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs) {
|
||||
if (decode_CbGpioPulls(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbGpioPulls");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs) {
|
||||
if (decode_CbUartBaudrate(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartBaudrate");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs) {
|
||||
if (decode_CbUartStopbits(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartStopbits");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartParity& rhs) {
|
||||
if (decode_CbUartParity(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartParity");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs) {
|
||||
if (decode_CbCanBaudrate(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbCanBaudrate");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs) {
|
||||
if (decode_CbRelayMode(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbRelayMode");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs) {
|
||||
if (decode_CbSafetyMode(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbSafetyMode");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, RelayConfig& rhs) {
|
||||
if (decode_RelayConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("RelayConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs) {
|
||||
if (decode_SafetyConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("SafetyConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs) {
|
||||
if (decode_CbGpioConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbGpioConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs) {
|
||||
if (decode_CbUartConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbUartConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs) {
|
||||
if (decode_CbCanConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbCanConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs) {
|
||||
if (decode_CbNetworkConfig(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("CbNetworkConfig");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs) {
|
||||
if (decode_Connector_type(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("type");
|
||||
}
|
||||
|
||||
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs) {
|
||||
if (decode_HardwareCapabilities(node, rhs)) {
|
||||
return node;
|
||||
}
|
||||
throw std::runtime_error("HardwareCapabilities");
|
||||
}
|
||||
|
||||
} // namespace charge_bridge::utilities
|
||||
Reference in New Issue
Block a user