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