- 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
416 lines
15 KiB
C++
416 lines
15 KiB
C++
// 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
|