GKD.RoboCtrl
RoboMaster Linux 电控:异步 IO、设备驱动与机器人控制
载入中...
搜索中...
未找到
runtime.hpp
浏览该文件的文档.
1
7#pragma once
8
9#include <algorithm>
10#include <expected>
11#include <filesystem>
12#include <fstream>
13#include <iterator>
14#include <ostream>
15#include <optional>
16#include <span>
17#include <string>
18#include <vector>
19
20#include <rfl/thirdparty/enchantum/enchantum.hpp>
21
22#include "config/base.hpp"
23#include "config/validate.hpp"
24#include "ctrl/power_manager.h"
25#include "device/aim_link.hpp"
26#include "device/remote_logger.hpp"
27#include "device/referee/referee.h"
28#include "device/super_cap.h"
29#include "device/motor/j6006.h"
30#include "device/motor/m9025.h"
31
32// DJI 型号枚举使用了实际型号作为数值(2006/3508/6020),超出
33// reflect-cpp 的默认枚举扫描范围。仅在配置层为该既有枚举设置扫描范围,
34// 不改变设备层,也不引入平行的配置类型。
35namespace enchantum {
36
37template<>
38struct enum_traits<roboctrl::device::dji_motor::type> {
39 static constexpr std::size_t prefix_length = 0;
40 static constexpr int min = 2006;
41 static constexpr int max = 6020;
42};
43
44} // namespace enchantum
45
46#include <rfl/NoExtraFields.hpp>
47#include <rfl/Processors.hpp>
48#include <rfl/json.hpp>
49#include <rfl/yaml.hpp>
50
51namespace roboctrl::config {
52
58 unsigned int schema_version {1};
59 std::string profile;
60
61 std::vector<io::can::info_type> cans;
62 std::vector<io::serial::info_type> serials;
63 std::vector<device::dji_motor::info_type> dji_motors;
64 std::vector<device::j6006::info_type> j6006_motors;
65 std::vector<device::m9025::info_type> m9025_motors;
66 std::vector<device::serial_imu::info_type> additional_imus;
67 std::vector<io::udp_server::info_type> udp_servers;
68 std::vector<device::aim_link::info_type> aim_links;
69 std::vector<device::navigation_link::info_type> navigation_links;
70 std::vector<device::remote_logger::info_type> remote_loggers;
71 std::optional<device::referee::info_type> referee;
72 std::optional<device::super_cap::info_type> super_cap;
73 std::optional<ctrl::power_manager::info_type> power;
74
78};
79
80using configuration_result = std::expected<runtime_config, std::string>;
81
83inline void validate_runtime_extensions(const runtime_config& config) {
84 const auto require = [](bool value, const std::string& message) {
85 if (!value) throw std::invalid_argument(message);
86 };
87 const auto keys = []<typename Info>(const std::vector<Info>& items, std::string_view kind) {
88 validate_unique_keys(kind, std::span<const Info>{items});
89 std::unordered_set<std::string> result;
90 for (const auto& item : items) result.insert(item.key());
91 return result;
92 };
93 const auto servers = keys(config.udp_servers, "UDP server");
94 const auto aims = keys(config.aim_links, "aim link");
95 const auto navigation = keys(config.navigation_links, "navigation link");
96 const auto loggers = keys(config.remote_loggers, "remote logger");
97 std::unordered_set<std::string> listen_endpoints, protocol_routes;
98 const auto endpoint = [&](const auto& info) {
99 asio::error_code error;
100 const auto address = asio::ip::make_address(info.address, error);
101 require(!error && address.is_v4() && info.port > 0,
102 "network endpoints require a numeric IPv4 address and a nonzero port");
103 };
104 for (const auto& server : config.udp_servers) {
105 endpoint(server);
106 require(listen_endpoints.insert(std::to_string(server.port)).second,
107 "duplicate UDP listen port (including wildcard address collisions)");
108 }
109 const auto link = [&](const auto& item, unsigned header) {
110 endpoint(item);
111 require(servers.contains(item.udp_name), "network link references missing UDP server");
112 require(protocol_routes.insert(std::format("{}:{}", item.udp_name, header)).second,
113 "duplicate application header on shared UDP socket");
114 };
115 for (const auto& item : config.aim_links) {
116 require(item.header != 0x37, "vision header 0x37 is reserved for navigation");
117 link(item, item.header);
118 require(item.target_timeout_ms > 0 && item.target_timeout_ms <= 10000,
119 "aim target timeout must be in 1..10000 ms");
120 require(item.wire_format == device::vision_wire_format::legacy_14 ||
121 item.wire_format == device::vision_wire_format::compact_10,
122 "unknown vision wire format");
123 }
124 for (const auto& item : config.navigation_links) {
125 link(item, 0x37);
126 require(item.target_timeout_ms > 0 && item.target_timeout_ms <= 10000,
127 "navigation timeout must be in 1..10000 ms");
128 }
129 for (const auto& item : config.remote_loggers) {
130 endpoint(item);
131 require(servers.contains(item.udp_name), "remote logger references missing UDP server");
132 }
133 const auto& motion = config.robot.motion_info;
134 require(motion.primary_aim_key.empty() || (config.robot.enable_gimbal && aims.contains(motion.primary_aim_key)),
135 "primary aim references missing link or disabled gimbal");
136 require(motion.secondary_aim_key.empty() || (config.robot.secondary_gimbal_info && aims.contains(motion.secondary_aim_key)),
137 "secondary aim requires a secondary gimbal and existing link");
138 require(!motion.enable_navigation || (config.robot.enable_chassis && navigation.contains(motion.navigation_key)),
139 "navigation control requires chassis and existing navigation link");
140 require(motion.navigation_key.empty() || navigation.contains(motion.navigation_key), "missing navigation telemetry link");
141 require(motion.remote_logger_key.empty() || loggers.contains(motion.remote_logger_key), "missing motion remote logger");
142 require(motion.telemetry_period > std::chrono::steady_clock::duration::zero(), "invalid telemetry period");
143 require(std::isfinite(motion.large_yaw_follow_direction) && std::fabs(motion.large_yaw_follow_direction) == 1,
144 "large yaw follow direction must be +1 or -1");
145 if (config.referee) {
146 const auto& item = *config.referee;
147 const auto serial = std::ranges::find(config.serials, item.serial_name, &io::serial::info_type::name);
148 require(serial != config.serials.end() && serial->raw, "referee requires an existing raw serial");
149 require(item.offline_timeout > std::chrono::steady_clock::duration::zero() &&
150 item.ui_period >= std::chrono::milliseconds{100}, "invalid referee timeout/UI period");
151 }
152 if (config.robot.enable_shoot) {
153 require(!config.robot.shoot_info.enforce_referee || config.referee.has_value(),
154 "referee-gated shoot requires referee configuration");
155 require(!config.robot.secondary_shoot_info || !config.robot.secondary_shoot_info->enforce_referee || config.referee.has_value(),
156 "secondary referee-gated shoot requires referee configuration");
157 }
158 if (config.super_cap) {
159 const auto& item = *config.super_cap;
160 require(device::super_cap::valid_configuration(item), "invalid super capacitor configuration");
161 require(std::ranges::find(config.cans, item.can_name, &io::can::info_type::name) != config.cans.end(),
162 "super capacitor references missing CAN");
163 const auto conflict = [&](unsigned id) {
164 for (const auto& m : config.dji_motors) if (m.can_name == item.can_name) {
165 const bool gimbal = m.type_ == device::dji_motor::M6020;
166 if (id == (gimbal ? 0x204u : 0x200u) + m.id ||
167 id == (gimbal ? (m.id <= 4 ? 0x1ffu : 0x2ffu) : (m.id <= 4 ? 0x200u : 0x1ffu))) return true;
168 }
169 for (const auto& m : config.j6006_motors)
170 if (m.can_name == item.can_name && (id == m.master_id || id == 0x200u + m.id)) return true;
171 for (const auto& m : config.m9025_motors)
172 if (m.can_name == item.can_name && id == 0x140u + m.id) return true;
173 return false;
174 };
175 require(!conflict(item.receive_id) && !conflict(item.command_id), "super capacitor CAN ID conflicts with motor");
176 }
177 if (config.power) {
178 require(ctrl::power_manager::valid_configuration(*config.power), "invalid power manager configuration");
179 require(!config.power->enabled || config.robot.enable_chassis, "power manager requires enabled chassis");
180 }
181}
182
183
189inline configuration_result validate_runtime_configuration(runtime_config config) {
190 if (config.schema_version != 1) {
191 return std::unexpected("unsupported configuration schema_version: " +
192 std::to_string(config.schema_version));
193 }
194 if (config.robot.enable_chassis && config.robot.chassis_type.empty()) {
195 return std::unexpected("chassis_type must not be empty when chassis is enabled");
196 }
197 if (config.robot.enable_gimbal && config.robot.gimbal_type.empty()) {
198 return std::unexpected("gimbal_type must not be empty when gimbal is enabled");
199 }
200 try {
201 validate_configuration(
202 std::span{config.cans},
203 std::span{config.serials},
204 std::span{config.dji_motors},
205 config.control_pad,
206 config.imu,
207 config.robot,
208 std::span{config.additional_imus},
209 std::span{config.j6006_motors},
210 std::span{config.m9025_motors});
212 } catch (const std::exception& error) {
213 return std::unexpected(error.what());
214 }
215 return config;
216}
217
223inline std::expected<std::string, std::string> read_text_file(
224 const std::filesystem::path& path)
225{
226 std::ifstream input{path};
227 if (!input) {
228 return std::unexpected("cannot open configuration file: " + path.string());
229 }
230 return std::string{
231 std::istreambuf_iterator<char>{input},
232 std::istreambuf_iterator<char>{}};
233}
234
240inline configuration_result load_configuration(const std::filesystem::path& path) {
241 const auto text = read_text_file(path);
242 if (!text) return std::unexpected(text.error());
243
244 const auto extension = path.extension().string();
245 if (extension == ".yaml" || extension == ".yml") {
246 auto parsed = rfl::yaml::read<runtime_config, rfl::NoExtraFields,
247 rfl::DefaultIfMissing>(*text);
248 if (!parsed) {
249 return std::unexpected("failed to parse " + path.string() + ": " +
250 parsed.error().what());
251 }
252 return validate_runtime_configuration(std::move(*parsed));
253 }
254 if (extension == ".json") {
255 auto parsed = rfl::json::read<runtime_config, rfl::NoExtraFields,
256 rfl::DefaultIfMissing>(*text);
257 if (!parsed) {
258 return std::unexpected("failed to parse " + path.string() + ": " +
259 parsed.error().what());
260 }
261 return validate_runtime_configuration(std::move(*parsed));
262 }
263 return std::unexpected("unsupported configuration extension: " + extension);
264}
265
270inline std::filesystem::path default_configuration_path() {
271 return std::filesystem::path{"configs"} /
272 (std::string{default_profile} + ".yaml");
273}
274
281 std::ostream& output,
282 const std::filesystem::path& directory)
283{
284 output << "\n=== Configuration files in " << directory.string() << " ===\n";
285 if (!std::filesystem::exists(directory)) {
286 output << "(directory does not exist)\n";
287 return;
288 }
289
290 std::vector<std::filesystem::path> files;
291 for (const auto& entry : std::filesystem::directory_iterator{directory}) {
292 if (!entry.is_regular_file()) continue;
293 const auto extension = entry.path().extension().string();
294 if (extension == ".yaml" || extension == ".yml" || extension == ".json") {
295 files.push_back(entry.path());
296 }
297 }
298 std::ranges::sort(files);
299 if (files.empty()) {
300 output << "(no YAML or JSON configuration files)\n";
301 return;
302 }
303
304 for (const auto& file : files) {
305 output << "--- " << file.string() << " ---\n";
306 const auto text = read_text_file(file);
307 if (!text) {
308 output << "<" << text.error() << ">\n";
309 continue;
310 }
311 output << *text;
312 if (!text->empty() && text->back() != '\n') output << '\n';
313 }
314}
315
316} // namespace roboctrl::config
运行时车型选择的兼容入口。
constexpr std::string_view default_profile
未指定 --config 时使用的默认配置文件名。
Definition base.hpp:15
std::expected< std::string, std::string > read_text_file(const std::filesystem::path &path)
读取整个文本文件。
Definition runtime.hpp:223
std::filesystem::path default_configuration_path()
获取未指定 --config 时使用的默认配置路径。
Definition runtime.hpp:270
configuration_result validate_runtime_configuration(runtime_config config)
校验已解析的运行时配置。
Definition runtime.hpp:189
void print_available_configurations(std::ostream &output, const std::filesystem::path &directory)
将指定目录下的 YAML/JSON 配置按名称排序后打印到输出流。
Definition runtime.hpp:280
configuration_result load_configuration(const std::filesystem::path &path)
从 YAML 或 JSON 文件加载并校验运行时配置。
Definition runtime.hpp:240
void validate_runtime_extensions(const runtime_config &config)
Definition runtime.hpp:83
运行时配置根对象。
Definition runtime.hpp:57
机器人初始化参数及启用的子系统。
Definition robot.h:39
std::string chassis_type
配置选择的具体底盘类;当前仅实现标准麦轮底盘。
Definition robot.h:49
std::string gimbal_type
配置选择的具体云台类;当前仅实现标准双轴 IMU 云台。
Definition robot.h:51
遥控器实例化参数。
Definition controlpad.h:61
std::string name
项目内稳定名称,供电机等其他组件引用。
Definition can.h:35