1use std::fmt;
9
10use crate::component::capability::{CapabilityKind, CapabilityRole, StructuralKind};
11use crate::identity::{
12 CapabilityId, CapabilityRef, ComponentInstanceId, ComponentTypeId, JointId, LinkId,
13 MODULE_INSTANCE_SEPARATOR,
14};
15use crate::robot::KinematicKind;
16use crate::structure::JointKind;
17
18#[derive(Debug, thiserror::Error)]
20pub enum ModelError {
21 #[error("{kind} '{value}' is not normalized")]
23 NotNormalized { kind: IdentifierKind, value: String },
24
25 #[error("{kind} '{value}' must not contain reserved separator '{MODULE_INSTANCE_SEPARATOR}'")]
28 ReservedSeparator { kind: IdentifierKind, value: String },
29
30 #[error("capability reference '{value}' must use component.capability")]
32 MalformedCapabilityReference { value: String },
33
34 #[error("invalid logical asset id '{value}'")]
36 MalformedAssetId { value: String },
37
38 #[error("unknown capability '{reference}'")]
40 UnknownCapability { reference: CapabilityRef },
41
42 #[error("capability '{reference}' must reference a {expected}, found {actual}")]
44 CapabilityKindMismatch {
45 reference: CapabilityRef,
46 expected: CapabilityKind,
47 actual: CapabilityKind,
48 },
49
50 #[error("capability '{reference}' must target a {expected}")]
53 CapabilityTargetKind {
54 reference: CapabilityRef,
55 expected: StructuralKind,
56 },
57
58 #[error("{kind} target '{id}' for capability '{reference}' not found")]
61 UnknownBoundTarget {
62 reference: CapabilityRef,
63 kind: StructuralKind,
64 id: String,
65 },
66
67 #[error(
70 "component type '{component_type}' capability '{capability_id}' references unknown {kind} '{id}'"
71 )]
72 UnknownDeclaredTarget {
73 component_type: ComponentTypeId,
74 capability_id: CapabilityId,
75 kind: StructuralKind,
76 id: String,
77 },
78
79 #[error("component '{instance}' references unknown component type '{component_type}'")]
81 UnknownComponentType {
82 instance: ComponentInstanceId,
83 component_type: ComponentTypeId,
84 },
85
86 #[error("component '{instance}' references unknown mount link '{link}'")]
88 UnknownMountLink {
89 instance: ComponentInstanceId,
90 link: LinkId,
91 },
92
93 #[error("component '{instance}' capability '{capability_id}' direction sign must be -1 or 1")]
95 DirectionSign {
96 instance: ComponentInstanceId,
97 capability_id: CapabilityId,
98 value: i8,
99 },
100
101 #[error(
103 "component '{instance}' direction sign references unknown capability '{capability_id}'"
104 )]
105 UnknownDirectionSignCapability {
106 instance: ComponentInstanceId,
107 capability_id: CapabilityId,
108 },
109
110 #[error(
113 "component '{instance}' role assignment references unknown capability '{capability_id}'"
114 )]
115 UnknownRoleCapability {
116 instance: ComponentInstanceId,
117 capability_id: CapabilityId,
118 },
119
120 #[error("capability '{capability_id}' has an empty role assignment")]
126 EmptyCapabilityRoles { capability_id: CapabilityId },
127
128 #[error("capability '{capability_id}' repeats role '{role}'")]
130 DuplicateCapabilityRole {
131 capability_id: CapabilityId,
132 role: CapabilityRole,
133 },
134
135 #[error("simulation capability '{component_type}.{capability_id}' has no component capability")]
137 SimulationWithoutCapability {
138 component_type: ComponentTypeId,
139 capability_id: CapabilityId,
140 },
141
142 #[error(
145 "simulation capability '{component_type}.{capability_id}' kind does not match component"
146 )]
147 SimulationCapabilityKindMismatch {
148 component_type: ComponentTypeId,
149 capability_id: CapabilityId,
150 simulated: CapabilityKind,
151 declared: CapabilityKind,
152 },
153
154 #[error("{owner} joint '{joint}' uses unsupported runtime kind '{kind:?}'")]
156 UnsupportedJointKind {
157 owner: JointOwner,
158 joint: JointId,
159 kind: JointKind,
160 },
161
162 #[error("motion {field} must be finite, positive, and fit in f32")]
167 MotionLimit { field: MotionLimitField },
168
169 #[error("{kinematics} {field} must be finite and positive")]
171 KinematicScalar {
172 kinematics: KinematicKind,
173 field: KinematicScalarField,
174 },
175
176 #[error("stock safety footprint cannot include movable joint '{joint}'")]
179 FootprintMovableJoint { joint: JointId },
180
181 #[error("stock safety footprint does not support mesh collision on link '{link}'")]
184 FootprintMesh { link: LinkId },
185
186 #[error("stock safety footprint contains a non-finite value")]
188 FootprintNonFinite,
189
190 #[error("stock safety footprint radius must be finite and positive")]
193 FootprintRadius,
194
195 #[error(transparent)]
197 Structure(#[from] StructureError),
198}
199
200impl From<phoxal_runtime_contract::identity::TopologyIdError> for ModelError {
201 fn from(error: phoxal_runtime_contract::identity::TopologyIdError) -> Self {
202 use phoxal_runtime_contract::identity::TopologyIdError;
203 match error {
204 TopologyIdError::Robot(value) => Self::NotNormalized {
205 kind: IdentifierKind::RobotId,
206 value,
207 },
208 TopologyIdError::ComponentInstance(value) => Self::NotNormalized {
209 kind: IdentifierKind::ComponentInstance,
210 value,
211 },
212 TopologyIdError::Service(value) => Self::NotNormalized {
213 kind: IdentifierKind::Service,
214 value,
215 },
216 }
217 }
218}
219
220#[derive(Debug, thiserror::Error)]
222pub enum StructureError {
223 #[error("duplicate {kind} identity '{name}'")]
225 DuplicateIdentity { kind: StructuralKind, name: String },
226
227 #[error("joint '{joint}' references unknown {role} link '{link}'")]
229 UnknownJointLink {
230 joint: JointId,
231 role: LinkRole,
232 link: LinkId,
233 },
234
235 #[error("link '{link}' is the child of multiple joints")]
237 MultipleParentJoints { link: LinkId },
238
239 #[error("joint '{joint}' cannot use '{link}' as both parent and child")]
241 SelfReferentialJoint { joint: JointId, link: LinkId },
242
243 #[error("structure must have exactly one root link, found {found}")]
245 RootLinkCount { found: usize },
246
247 #[error("structure contains a joint cycle involving '{link}'")]
249 JointCycle { link: LinkId },
250
251 #[error("{owner} pose must be finite")]
253 Pose { owner: PoseOwner },
254
255 #[error("link '{link}' mass must be finite and non-negative")]
257 Mass { link: LinkId },
258
259 #[error("link '{link}' inertia must be finite and positive semidefinite")]
262 Inertia { link: LinkId },
263
264 #[error("link '{link}' geometry dimensions must be finite and positive")]
266 Geometry { link: LinkId },
267
268 #[error("joint '{joint}' axis must be finite")]
270 AxisNotFinite { joint: JointId },
271
272 #[error("joint '{joint}' axis must be non-zero")]
274 AxisNotOriented { joint: JointId },
275
276 #[error("joint '{joint}' limits must be finite with lower <= upper")]
278 JointLimits { joint: JointId },
279
280 #[error("joint '{joint}' dynamics must be finite and non-negative")]
282 JointDynamics { joint: JointId },
283
284 #[error("joint '{joint}' mimics unknown joint '{mimicked}'")]
286 UnknownMimicJoint { joint: JointId, mimicked: JointId },
287
288 #[error("joint '{joint}' safety limits must be finite with lower <= upper")]
290 JointSafety { joint: JointId },
291
292 #[error("structure root link must be '{expected}', found '{found}'")]
294 RootLinkName { expected: LinkId, found: LinkId },
295
296 #[error("structure must attach 'base_link' under 'base_footprint' with a fixed joint")]
298 MissingBaseLink,
299
300 #[error("structure must attach 'base_link' directly under 'base_footprint' with a fixed joint")]
303 MisattachedBaseLink,
304
305 #[error("canonical structure document is invalid: {0}")]
307 Document(#[from] serde_json::Error),
308}
309
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
312pub enum IdentifierKind {
313 RobotId,
314 RobotLink,
315 RobotJoint,
316 ComponentType,
317 ComponentInstance,
318 Capability,
319 Service,
320}
321
322impl fmt::Display for IdentifierKind {
323 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
324 formatter.write_str(match self {
325 Self::RobotId => "robot id",
326 Self::RobotLink => "robot link",
327 Self::RobotJoint => "robot joint",
328 Self::ComponentType => "component type",
329 Self::ComponentInstance => "component instance id",
330 Self::Capability => "capability id",
331 Self::Service => "service id",
332 })
333 }
334}
335
336#[derive(Clone, Debug, PartialEq, Eq)]
338pub enum JointOwner {
339 Robot,
341 ComponentType(ComponentTypeId),
343}
344
345impl fmt::Display for JointOwner {
346 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347 match self {
348 Self::Robot => formatter.write_str("robot"),
349 Self::ComponentType(component_type) => write!(formatter, "{component_type}"),
350 }
351 }
352}
353
354#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub enum LinkRole {
357 Parent,
358 Child,
359}
360
361impl fmt::Display for LinkRole {
362 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
363 formatter.write_str(match self {
364 Self::Parent => "parent",
365 Self::Child => "child",
366 })
367 }
368}
369
370#[derive(Clone, Debug, PartialEq, Eq)]
372pub enum PoseOwner {
373 LinkInertial(LinkId),
374 LinkVisual(LinkId),
375 LinkCollision(LinkId),
376 Joint(JointId),
377}
378
379impl fmt::Display for PoseOwner {
380 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381 match self {
382 Self::LinkInertial(link) => write!(formatter, "link '{link}' inertial"),
383 Self::LinkVisual(link) => write!(formatter, "link '{link}' visual"),
384 Self::LinkCollision(link) => write!(formatter, "link '{link}' collision"),
385 Self::Joint(joint) => write!(formatter, "joint '{joint}'"),
386 }
387 }
388}
389
390#[derive(Clone, Copy, Debug, PartialEq, Eq)]
392pub enum MotionLimitField {
393 MaxLinearSpeedMps,
394 MaxAngularSpeedRadps,
395}
396
397impl fmt::Display for MotionLimitField {
398 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
399 formatter.write_str(match self {
400 Self::MaxLinearSpeedMps => "max_linear_speed_mps",
401 Self::MaxAngularSpeedRadps => "max_angular_speed_radps",
402 })
403 }
404}
405
406#[derive(Clone, Copy, Debug, PartialEq, Eq)]
408pub enum KinematicScalarField {
409 WheelRadiusM,
410 WheelBaseM,
411 TrackM,
412 MaxSteeringAngleRad,
413}
414
415impl fmt::Display for KinematicScalarField {
416 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
417 formatter.write_str(match self {
418 Self::WheelRadiusM => "wheel_radius_m",
419 Self::WheelBaseM => "wheel_base_m",
420 Self::TrackM => "track_m",
421 Self::MaxSteeringAngleRad => "max_steering_angle_rad",
422 })
423 }
424}