// SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest #include "c4/yml/node.hpp" #include #include #include #include #include #include #include // clang-format off #include #include // 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 c4::yml::ConstNodeRef decode(c4::yml::ConstNodeRef const& node, T& rhs) { using namespace charge_bridge::utilities; node >> rhs; return node; } std::pair 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 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 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 ip_list; ip_list_node >> ip_list; std::vector 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