1use super::joint::Joint;
32use thiserror::Error;
33
34#[derive(Debug, Error)]
38pub enum RobotError {
39 #[error("Hardware communication failed: {0}")]
42 HardwareFailure(String),
43
44 #[error("State machine poisoned: {reason}")]
46 StatePoisoned {
47 reason: String,
49 },
50
51 #[error("Emergency stop triggered")]
53 EmergencyStop,
54
55 #[error("CAN bus fatal error: {0}")]
57 CanBusFatal(String),
58
59 #[error("Command timeout after {timeout_ms}ms")]
62 Timeout {
63 timeout_ms: u64,
65 },
66
67 #[error("Invalid state transition: {from} -> {to}")]
69 InvalidTransition {
70 from: String,
72 to: String,
74 },
75
76 #[error("Joint {joint} limit exceeded: {value:.3} (limit: {limit:.3})")]
78 JointLimitExceeded {
79 joint: Joint,
81 value: f64,
83 limit: f64,
85 },
86
87 #[error("Velocity limit exceeded for joint {joint}: {value:.3} (limit: {limit:.3})")]
89 VelocityLimitExceeded {
90 joint: Joint,
92 value: f64,
94 limit: f64,
96 },
97
98 #[error("Torque limit exceeded for joint {joint}: {value:.3} (limit: {limit:.3})")]
100 TorqueLimitExceeded {
101 joint: Joint,
103 value: f64,
105 limit: f64,
107 },
108
109 #[error("CAN bus I/O error: {0}")]
112 CanIoError(String),
113
114 #[error("Serialization error: {0}")]
116 SerializationError(String),
117
118 #[error("Protocol encoding error: {0}")]
121 Protocol(#[from] piper_protocol::ProtocolError),
122
123 #[error("Driver infrastructure error: {0}")]
125 Infrastructure(#[from] piper_driver::DriverError),
126
127 #[error("CAN adapter error: {0}")]
129 CanAdapter(#[from] piper_can::CanError),
130
131 #[error("Invalid CAN frame ID: 0x{id:03X}")]
133 InvalidFrameId {
134 id: u32,
136 },
137
138 #[error("Invalid data length: expected {expected}, got {actual}")]
140 InvalidDataLength {
141 expected: usize,
143 actual: usize,
145 },
146
147 #[error("Configuration error: {0}")]
150 ConfigError(String),
151
152 #[error("Invalid parameter '{param}': {reason}")]
154 InvalidParameter {
155 param: String,
157 reason: String,
159 },
160
161 #[error("Unknown error: {0}")]
164 Unknown(String),
165}
166
167impl RobotError {
168 pub fn is_fatal(&self) -> bool {
172 matches!(
173 self,
174 Self::HardwareFailure(_)
175 | Self::StatePoisoned { .. }
176 | Self::EmergencyStop
177 | Self::CanBusFatal(_)
178 )
179 }
180
181 pub fn is_retryable(&self) -> bool {
185 matches!(
186 self,
187 Self::Timeout { .. } | Self::CanIoError(_) | Self::Protocol(_)
188 )
189 }
190
191 pub fn is_config_error(&self) -> bool {
193 matches!(self, Self::ConfigError(_) | Self::InvalidParameter { .. })
194 }
195
196 pub fn is_limit_error(&self) -> bool {
198 matches!(
199 self,
200 Self::JointLimitExceeded { .. }
201 | Self::VelocityLimitExceeded { .. }
202 | Self::TorqueLimitExceeded { .. }
203 )
204 }
205
206 pub fn context(self, context: impl Into<String>) -> Self {
208 match self {
209 Self::Unknown(msg) => Self::Unknown(format!("{}: {}", context.into(), msg)),
210 _ => self,
211 }
212 }
213
214 pub fn hardware_failure(msg: impl Into<String>) -> Self {
216 Self::HardwareFailure(msg.into())
217 }
218
219 pub fn state_poisoned(reason: impl Into<String>) -> Self {
221 Self::StatePoisoned {
222 reason: reason.into(),
223 }
224 }
225
226 pub fn timeout(timeout_ms: u64) -> Self {
228 Self::Timeout { timeout_ms }
229 }
230
231 pub fn invalid_transition(from: impl Into<String>, to: impl Into<String>) -> Self {
233 Self::InvalidTransition {
234 from: from.into(),
235 to: to.into(),
236 }
237 }
238
239 pub fn joint_limit(joint: Joint, value: f64, limit: f64) -> Self {
241 Self::JointLimitExceeded {
242 joint,
243 value,
244 limit,
245 }
246 }
247
248 pub fn velocity_limit(joint: Joint, value: f64, limit: f64) -> Self {
250 Self::VelocityLimitExceeded {
251 joint,
252 value,
253 limit,
254 }
255 }
256
257 pub fn torque_limit(joint: Joint, value: f64, limit: f64) -> Self {
259 Self::TorqueLimitExceeded {
260 joint,
261 value,
262 limit,
263 }
264 }
265}
266
267pub type Result<T> = std::result::Result<T, RobotError>;
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_error_classification() {
276 let fatal = RobotError::EmergencyStop;
278 assert!(fatal.is_fatal());
279 assert!(!fatal.is_retryable());
280
281 let hardware_fail = RobotError::hardware_failure("connection lost");
282 assert!(hardware_fail.is_fatal());
283
284 let poisoned = RobotError::state_poisoned("state drift detected");
285 assert!(poisoned.is_fatal());
286
287 let recoverable = RobotError::timeout(100);
289 assert!(!recoverable.is_fatal());
290 assert!(recoverable.is_retryable());
291
292 let can_io = RobotError::CanIoError("temporary failure".to_string());
293 assert!(!can_io.is_fatal());
294 assert!(can_io.is_retryable());
295 }
296
297 #[test]
298 fn test_limit_errors() {
299 let joint_limit = RobotError::joint_limit(Joint::J1, 3.5, std::f64::consts::PI);
300 assert!(joint_limit.is_limit_error());
301 assert!(!joint_limit.is_fatal());
302
303 let velocity_limit = RobotError::velocity_limit(Joint::J2, 10.0, 5.0);
304 assert!(velocity_limit.is_limit_error());
305
306 let torque_limit = RobotError::torque_limit(Joint::J3, 15.0, 10.0);
307 assert!(torque_limit.is_limit_error());
308 }
309
310 #[test]
311 fn test_config_errors() {
312 let config_err = RobotError::ConfigError("invalid frequency".to_string());
313 assert!(config_err.is_config_error());
314 assert!(!config_err.is_fatal());
315
316 let invalid_param = RobotError::InvalidParameter {
317 param: "max_velocity".to_string(),
318 reason: "must be positive".to_string(),
319 };
320 assert!(invalid_param.is_config_error());
321 }
322
323 #[test]
324 fn test_error_display() {
325 let err = RobotError::joint_limit(Joint::J1, 3.5, std::f64::consts::PI);
326 let msg = format!("{}", err);
327 assert!(msg.contains("J1"));
328 assert!(msg.contains("3.5"));
329 assert!(msg.contains("3.14"));
330
331 let timeout_err = RobotError::timeout(100);
332 let msg = format!("{}", timeout_err);
333 assert!(msg.contains("100"));
334 assert!(msg.contains("timeout"));
335 }
336
337 #[test]
338 fn test_error_context() {
339 let err = RobotError::Unknown("base error".to_string());
340 let err_with_context = err.context("during initialization");
341 let msg = format!("{}", err_with_context);
342 assert!(msg.contains("during initialization"));
343 assert!(msg.contains("base error"));
344 }
345
346 #[test]
347 fn test_invalid_transition() {
348 let err = RobotError::invalid_transition("Standby", "Active");
349 let msg = format!("{}", err);
350 assert!(msg.contains("Standby"));
351 assert!(msg.contains("Active"));
352 }
353
354 #[test]
355 fn test_protocol_errors() {
356 let invalid_id = RobotError::InvalidFrameId { id: 0x123 };
357 let msg = format!("{}", invalid_id);
358 assert!(msg.contains("0x123"));
359
360 let invalid_len = RobotError::InvalidDataLength {
361 expected: 8,
362 actual: 4,
363 };
364 let msg = format!("{}", invalid_len);
365 assert!(msg.contains("8"));
366 assert!(msg.contains("4"));
367 }
368
369 #[test]
370 fn test_result_type() {
371 let ok: Result<i32> = Ok(42);
372 assert!(matches!(ok, Ok(42)));
373
374 let err: Result<i32> = Err(RobotError::EmergencyStop);
375 assert!(err.is_err());
376 }
377
378 #[test]
379 fn test_error_is_send_sync() {
380 fn assert_send_sync<T: Send + Sync>() {}
381 assert_send_sync::<RobotError>();
382 }
383}