1mod motion;
4
5pub use motion::{KinematicConfig, MotionLimits};
6
7use std::collections::BTreeMap;
8
9use crate::component::capability::{Capability, Encoder, Motor, StructuralTarget};
10use crate::component::{CapabilityRef, Component};
11use crate::simulation::Simulation;
12use crate::structure::Structure;
13use crate::{DecodeError, EncodeError, ModelError, strict_json};
14
15pub const ROBOT_SCHEMA: &str = "phoxal/robot/v0";
17
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct RobotIdentity {
22 id: String,
23 namespace: String,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct ComponentInstance {
30 id: String,
31 component_type: String,
32 mount_link: String,
33 direction_signs: BTreeMap<String, i8>,
34}
35
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct MotionModel {
40 kinematic: KinematicConfig,
41 limits: MotionLimits,
42}
43
44#[doc(hidden)]
46pub struct RobotParts {
47 pub id: String,
48 pub namespace: String,
49 pub kinematic: KinematicConfig,
50 pub motion_limits: MotionLimits,
51 pub component_instances: BTreeMap<String, ComponentInstance>,
52 pub component_types: BTreeMap<String, Component>,
53 pub simulation_types: BTreeMap<String, Simulation>,
54 pub structure: Structure,
55}
56
57#[derive(Debug, Clone)]
59pub struct Robot {
60 identity: RobotIdentity,
61 motion: MotionModel,
62 component_instances: BTreeMap<String, ComponentInstance>,
63 component_types: BTreeMap<String, Component>,
64 simulation_types: BTreeMap<String, Simulation>,
65 structure: Structure,
66}
67
68impl RobotIdentity {
69 #[must_use]
70 pub fn id(&self) -> &str {
71 &self.id
72 }
73
74 #[must_use]
75 pub fn namespace(&self) -> &str {
76 &self.namespace
77 }
78}
79
80impl ComponentInstance {
81 #[doc(hidden)]
83 pub fn __new(
84 id: String,
85 component_type: String,
86 mount_link: String,
87 direction_signs: BTreeMap<String, i8>,
88 ) -> Self {
89 Self {
90 id,
91 component_type,
92 mount_link,
93 direction_signs,
94 }
95 }
96
97 #[must_use]
98 pub fn id(&self) -> &str {
99 &self.id
100 }
101
102 #[must_use]
103 pub fn component_type(&self) -> &str {
104 &self.component_type
105 }
106
107 #[must_use]
108 pub fn mount_link(&self) -> &str {
109 &self.mount_link
110 }
111}
112
113impl MotionModel {
114 #[must_use]
115 pub fn kinematic(&self) -> &KinematicConfig {
116 &self.kinematic
117 }
118
119 #[must_use]
120 pub const fn limits(&self) -> MotionLimits {
121 self.limits
122 }
123}
124
125impl Robot {
126 #[doc(hidden)]
128 pub fn __from_compiler(parts: RobotParts) -> Result<Self, ModelError> {
129 let robot = Self {
130 identity: RobotIdentity {
131 id: parts.id,
132 namespace: parts.namespace,
133 },
134 motion: MotionModel {
135 kinematic: parts.kinematic,
136 limits: parts.motion_limits,
137 },
138 component_instances: parts.component_instances,
139 component_types: parts.component_types,
140 simulation_types: parts.simulation_types,
141 structure: parts.structure,
142 };
143 robot.validate()?;
144 Ok(robot)
145 }
146
147 pub fn encode(&self) -> Result<Vec<u8>, EncodeError> {
149 serde_json::to_vec_pretty(&RobotWire {
150 schema: ROBOT_SCHEMA,
151 robot: RobotPayloadRef {
152 identity: &self.identity,
153 motion: &self.motion,
154 component_instances: &self.component_instances,
155 component_types: &self.component_types,
156 simulation_types: &self.simulation_types,
157 structure: &self.structure,
158 },
159 })
160 .map_err(EncodeError::from)
161 }
162
163 pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
165 let value = strict_json::parse(bytes)?;
166 let wire: RobotWireOwned = serde_json::from_value(value)?;
167 if wire.schema != ROBOT_SCHEMA {
168 return Err(DecodeError::UnsupportedSchema(wire.schema));
169 }
170 Self::__from_compiler(RobotParts {
171 id: wire.robot.identity.id,
172 namespace: wire.robot.identity.namespace,
173 kinematic: wire.robot.motion.kinematic,
174 motion_limits: wire.robot.motion.limits,
175 component_instances: wire.robot.component_instances,
176 component_types: wire.robot.component_types,
177 simulation_types: wire.robot.simulation_types,
178 structure: wire.robot.structure,
179 })
180 .map_err(DecodeError::Model)
181 }
182
183 #[must_use]
184 pub fn identity(&self) -> &RobotIdentity {
185 &self.identity
186 }
187
188 #[must_use]
189 pub fn robot_id(&self) -> &str {
190 self.identity.id()
191 }
192
193 #[must_use]
194 pub fn namespace(&self) -> &str {
195 self.identity.namespace()
196 }
197
198 #[must_use]
199 pub fn motion(&self) -> &MotionModel {
200 &self.motion
201 }
202
203 #[must_use]
204 pub fn kinematic(&self) -> &KinematicConfig {
205 self.motion.kinematic()
206 }
207
208 #[must_use]
209 pub const fn motion_limits(&self) -> MotionLimits {
210 self.motion.limits()
211 }
212
213 pub fn components(&self) -> impl ExactSizeIterator<Item = &ComponentInstance> {
214 self.component_instances.values()
215 }
216
217 pub fn component_ids(&self) -> impl ExactSizeIterator<Item = &str> {
218 self.component_instances.keys().map(String::as_str)
219 }
220
221 #[must_use]
222 pub fn component(&self, id: &str) -> Option<&ComponentInstance> {
223 self.component_instances.get(id)
224 }
225
226 pub fn component_instance(&self, id: &str) -> Result<&ComponentInstance, ModelError> {
227 self.component(id)
228 .ok_or_else(|| ModelError::Invalid(format!("unknown component instance '{id}'")))
229 }
230
231 pub fn component_for_instance(&self, id: &str) -> Result<&Component, ModelError> {
232 let instance = self.component_instance(id)?;
233 self.component_types
234 .get(instance.component_type())
235 .ok_or_else(|| {
236 ModelError::Invalid(format!(
237 "component type '{}' for instance '{id}' is not loaded",
238 instance.component_type()
239 ))
240 })
241 }
242
243 #[must_use]
244 pub fn simulation_for_component_type(&self, component_type: &str) -> Option<&Simulation> {
245 self.simulation_types.get(component_type)
246 }
247
248 pub fn simulation_for_instance(
249 &self,
250 component_id: &str,
251 ) -> Result<Option<&Simulation>, ModelError> {
252 let instance = self.component_instance(component_id)?;
253 Ok(self.simulation_for_component_type(instance.component_type()))
254 }
255
256 #[must_use]
257 pub fn structure(&self) -> &Structure {
258 &self.structure
259 }
260
261 pub fn capability(&self, reference: &CapabilityRef) -> Result<&Capability, ModelError> {
262 self.component_for_instance(&reference.component_id)?
263 .capability(&reference.capability_id)
264 .ok_or_else(|| ModelError::Invalid(format!("unknown capability '{reference}'")))
265 }
266
267 pub fn camera_capabilities(&self) -> Result<Vec<CapabilityRef>, ModelError> {
268 let mut capabilities = Vec::new();
269 for component_id in self.component_ids() {
270 let component = self.component_for_instance(component_id)?;
271 capabilities.extend(
272 component
273 .capabilities()
274 .filter(|(_, capability)| matches!(capability, Capability::Camera(_)))
275 .map(|(capability_id, _)| CapabilityRef::new(component_id, capability_id)),
276 );
277 }
278 capabilities.sort();
279 Ok(capabilities)
280 }
281
282 pub fn require_motor(&self, reference: &CapabilityRef) -> Result<(&Motor, i8), ModelError> {
283 let capability = self.capability(reference)?;
284 let Capability::Motor(motor) = capability else {
285 return Err(ModelError::Invalid(format!(
286 "capability '{reference}' must reference a motor, found {}",
287 capability.kind_name()
288 )));
289 };
290 Ok((motor, self.direction_sign(reference)?))
291 }
292
293 pub fn require_encoder(&self, reference: &CapabilityRef) -> Result<(&Encoder, i8), ModelError> {
294 let capability = self.capability(reference)?;
295 let Capability::Encoder(encoder) = capability else {
296 return Err(ModelError::Invalid(format!(
297 "capability '{reference}' must reference an encoder, found {}",
298 capability.kind_name()
299 )));
300 };
301 Ok((encoder, self.direction_sign(reference)?))
302 }
303
304 pub fn require_link_target(&self, reference: &CapabilityRef) -> Result<String, ModelError> {
305 let StructuralTarget::Link { id } = self
306 .capability(reference)?
307 .target()
308 .namespaced(&reference.component_id)
309 else {
310 return Err(ModelError::Invalid(format!(
311 "capability '{reference}' must target a link"
312 )));
313 };
314 self.structure.link(&id).map(|_| id.clone()).ok_or_else(|| {
315 ModelError::Invalid(format!(
316 "link target '{id}' for capability '{reference}' not found"
317 ))
318 })
319 }
320
321 pub fn component_mount_link(&self, id: &str) -> Result<String, ModelError> {
322 Ok(self.component_instance(id)?.mount_link().to_string())
323 }
324
325 fn direction_sign(&self, reference: &CapabilityRef) -> Result<i8, ModelError> {
326 Ok(self
327 .component_instance(&reference.component_id)?
328 .direction_signs
329 .get(&reference.capability_id)
330 .copied()
331 .unwrap_or(1))
332 }
333
334 fn validate(&self) -> Result<(), ModelError> {
335 validate_token(self.identity.id(), "robot id")?;
336 validate_token(self.identity.namespace(), "robot namespace")?;
337 self.motion.limits.validate()?;
338 self.structure.validate()?;
339 for (id, instance) in &self.component_instances {
340 if id != instance.id() {
341 return Err(ModelError::Invalid(format!(
342 "component map identity '{id}' does not match embedded id '{}'",
343 instance.id()
344 )));
345 }
346 if !self.component_types.contains_key(instance.component_type()) {
347 return Err(ModelError::Invalid(format!(
348 "component '{id}' references unknown component type '{}'",
349 instance.component_type()
350 )));
351 }
352 if self.structure.link(instance.mount_link()).is_none() {
353 return Err(ModelError::Invalid(format!(
354 "component '{id}' references unknown mount link '{}'",
355 instance.mount_link()
356 )));
357 }
358 }
359 for (component_type, simulation) in &self.simulation_types {
360 let component = self.component_types.get(component_type).ok_or_else(|| {
361 ModelError::Invalid(format!(
362 "simulation type '{component_type}' has no matching component type"
363 ))
364 })?;
365 for (id, simulated) in simulation.capabilities() {
366 let capability = component.capability(id).ok_or_else(|| {
367 ModelError::Invalid(format!(
368 "simulation capability '{component_type}.{id}' has no component capability"
369 ))
370 })?;
371 if simulated.kind_name() != capability.kind_name() {
372 return Err(ModelError::Invalid(format!(
373 "simulation capability '{component_type}.{id}' kind does not match component"
374 )));
375 }
376 }
377 }
378 match self.kinematic() {
379 KinematicConfig::Differential {
380 left_actuators,
381 right_actuators,
382 left_encoders,
383 right_encoders,
384 wheel_radius_m,
385 wheel_base_m,
386 } => {
387 validate_positive(*wheel_radius_m, "differential wheel_radius_m")?;
388 validate_positive(*wheel_base_m, "differential wheel_base_m")?;
389 for reference in left_actuators.iter().chain(right_actuators) {
390 self.require_motor(reference)?;
391 }
392 for reference in left_encoders.iter().chain(right_encoders) {
393 self.require_encoder(reference)?;
394 }
395 }
396 KinematicConfig::Mecanum {
397 front_left_actuator,
398 front_right_actuator,
399 rear_left_actuator,
400 rear_right_actuator,
401 wheel_radius_m,
402 wheel_base_m,
403 track_m,
404 } => {
405 validate_positive(*wheel_radius_m, "mecanum wheel_radius_m")?;
406 validate_positive(*wheel_base_m, "mecanum wheel_base_m")?;
407 validate_positive(*track_m, "mecanum track_m")?;
408 for reference in [
409 front_left_actuator,
410 front_right_actuator,
411 rear_left_actuator,
412 rear_right_actuator,
413 ] {
414 self.require_motor(reference)?;
415 }
416 }
417 KinematicConfig::Ackermann {
418 steering_actuator,
419 drive_actuator,
420 steering_encoder,
421 drive_encoder,
422 wheel_base_m,
423 track_m,
424 max_steering_angle_rad,
425 } => {
426 validate_positive(*wheel_base_m, "ackermann wheel_base_m")?;
427 validate_positive(*track_m, "ackermann track_m")?;
428 validate_positive(*max_steering_angle_rad, "ackermann max_steering_angle_rad")?;
429 self.require_motor(steering_actuator)?;
430 self.require_motor(drive_actuator)?;
431 if let Some(reference) = steering_encoder {
432 self.require_encoder(reference)?;
433 }
434 if let Some(reference) = drive_encoder {
435 self.require_encoder(reference)?;
436 }
437 }
438 KinematicConfig::Omnidirectional {
439 actuators,
440 encoders,
441 } => {
442 for reference in actuators {
443 self.require_motor(reference)?;
444 }
445 for reference in encoders {
446 self.require_encoder(reference)?;
447 }
448 }
449 }
450 Ok(())
451 }
452}
453
454fn validate_token(value: &str, label: &str) -> Result<(), ModelError> {
455 if value.is_empty()
456 || !value
457 .chars()
458 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '_' | '-'))
459 {
460 return Err(ModelError::Invalid(format!(
461 "{label} '{value}' is not normalized"
462 )));
463 }
464 Ok(())
465}
466
467fn validate_positive(value: f64, label: &str) -> Result<(), ModelError> {
468 if !value.is_finite() || value <= 0.0 {
469 return Err(ModelError::Invalid(format!(
470 "{label} must be finite and positive"
471 )));
472 }
473 Ok(())
474}
475
476#[derive(serde::Serialize)]
477#[serde(deny_unknown_fields)]
478struct RobotWire<'a> {
479 schema: &'static str,
480 robot: RobotPayloadRef<'a>,
481}
482
483#[derive(serde::Serialize)]
484#[serde(deny_unknown_fields)]
485struct RobotPayloadRef<'a> {
486 identity: &'a RobotIdentity,
487 motion: &'a MotionModel,
488 component_instances: &'a BTreeMap<String, ComponentInstance>,
489 component_types: &'a BTreeMap<String, Component>,
490 simulation_types: &'a BTreeMap<String, Simulation>,
491 structure: &'a Structure,
492}
493
494#[derive(serde::Deserialize)]
495#[serde(deny_unknown_fields)]
496struct RobotWireOwned {
497 schema: String,
498 robot: RobotPayloadOwned,
499}
500
501#[derive(serde::Deserialize)]
502#[serde(deny_unknown_fields)]
503struct RobotPayloadOwned {
504 identity: RobotIdentity,
505 motion: MotionModel,
506 component_instances: BTreeMap<String, ComponentInstance>,
507 component_types: BTreeMap<String, Component>,
508 simulation_types: BTreeMap<String, Simulation>,
509 structure: Structure,
510}