58 unsigned int schema_version {1};
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;
84 const auto require = [](
bool value,
const std::string& message) {
85 if (!value)
throw std::invalid_argument(message);
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());
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");
104 for (
const auto& server : config.udp_servers) {
106 require(listen_endpoints.insert(std::to_string(server.port)).second,
107 "duplicate UDP listen port (including wildcard address collisions)");
109 const auto link = [&](
const auto& item,
unsigned header) {
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");
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");
124 for (
const auto& item : config.navigation_links) {
126 require(item.target_timeout_ms > 0 && item.target_timeout_ms <= 10000,
127 "navigation timeout must be in 1..10000 ms");
129 for (
const auto& item : config.remote_loggers) {
131 require(servers.contains(item.udp_name),
"remote logger references missing UDP server");
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");
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");
158 if (config.super_cap) {
159 const auto& item = *config.super_cap;
160 require(device::super_cap::valid_configuration(item),
"invalid super capacitor configuration");
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;
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;
175 require(!conflict(item.receive_id) && !conflict(item.command_id),
"super capacitor CAN ID conflicts with motor");
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");