1#![no_std]
21#![forbid(unsafe_code)]
22
23#[cfg(test)]
24extern crate std;
25
26use core::{fmt, str::FromStr};
27
28pub const MAX_STANDARD_ID: u16 = 0x07FF;
29pub const MAX_EXTENDED_ID: u32 = 0x1FFF_FFFF;
30pub const MAX_CAN_DATA_LEN: usize = 8;
31
32pub const DUTY_SCALE: f64 = 100_000.0;
33pub const CURRENT_SCALE: f64 = 1_000.0;
34pub const POSITION_SCALE: f64 = 10_000.0;
35pub const MAX_DUTY_CYCLE: f64 = 0.95;
36pub const MAX_CURRENT_AMPS: f64 = 60.0;
37pub const MAX_POSITION_DEGREES: f64 = 36_000.0;
38pub const MIN_POSITION_DEGREES: f64 = -36_000.0;
39pub const MAX_PROTOCOL_ERPM: i32 = 100_000;
40pub const MIN_PROTOCOL_ERPM: i32 = -100_000;
41pub const MAX_PROFILE_ERPM: i32 = 327_670;
42pub const MIN_PROFILE_ERPM: i32 = -327_680;
43pub const MAX_PROFILE_ACCELERATION_ERPM_PER_SEC: i32 = 327_670;
44pub const MIN_PROFILE_ACCELERATION_ERPM_PER_SEC: i32 = 0;
45pub const PROFILE_ERPM_PER_UNIT: i32 = 10;
46
47pub const ROBSTRIDE_MIT_BASE_ID: u32 = 0x200;
48pub const ROBSTRIDE_POSITION_BASE_ID: u32 = 0x300;
49pub const ROBSTRIDE_SPEED_BASE_ID: u32 = 0x400;
50pub const ROBSTRIDE_STATUS_BASE_ID: u32 = 0x500;
51pub const ROBSTRIDE_CONFIG_BASE_ID: u32 = 0x600;
52
53pub type Result<T> = core::result::Result<T, Error>;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Error {
57 InvalidCanId(u32),
58 InvalidCanIdText,
59 PayloadTooLong { len: usize },
60 UnknownCanIdKind,
61}
62
63impl fmt::Display for Error {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::InvalidCanId(id) => write!(f, "invalid CAN id: 0x{id:X}"),
67 Self::InvalidCanIdText => write!(f, "invalid CAN id text"),
68 Self::PayloadTooLong { len } => write!(f, "CAN payload too long: {len} bytes"),
69 Self::UnknownCanIdKind => write!(f, "unknown CAN id kind"),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum CanId {
76 Standard(u16),
77 Extended(u32),
78}
79
80impl CanId {
81 pub const fn standard(id: u16) -> Result<Self> {
82 if id > MAX_STANDARD_ID {
83 Err(Error::InvalidCanId(id as u32))
84 } else {
85 Ok(Self::Standard(id))
86 }
87 }
88
89 pub const fn extended(id: u32) -> Result<Self> {
90 if id > MAX_EXTENDED_ID {
91 Err(Error::InvalidCanId(id))
92 } else {
93 Ok(Self::Extended(id))
94 }
95 }
96
97 pub const fn raw(self) -> u32 {
98 match self {
99 Self::Standard(id) => id as u32,
100 Self::Extended(id) => id,
101 }
102 }
103
104 pub const fn is_extended(self) -> bool {
105 matches!(self, Self::Extended(_))
106 }
107
108 pub const fn kind_name(self) -> &'static str {
109 match self {
110 Self::Standard(_) => "standard",
111 Self::Extended(_) => "extended",
112 }
113 }
114}
115
116impl fmt::Display for CanId {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{} 0x{:X}", self.kind_name(), self.raw())
119 }
120}
121
122impl FromStr for CanId {
123 type Err = Error;
124
125 fn from_str(value: &str) -> Result<Self> {
126 let value = value.trim();
127 let (kind, raw) = if let Some((kind, raw)) = value.split_once(':') {
128 (kind.trim(), raw.trim())
129 } else {
130 let mut parts = value.split_whitespace();
131 match (parts.next(), parts.next(), parts.next()) {
132 (Some(kind), Some(raw), None) => (kind, raw),
133 _ => return Err(Error::InvalidCanIdText),
134 }
135 };
136
137 let raw = parse_can_id_number(raw)?;
138 if matches_kind(kind, &["standard", "std", "s"]) {
139 if raw > MAX_STANDARD_ID as u32 {
140 return Err(Error::InvalidCanId(raw));
141 }
142 CanId::standard(raw as u16)
143 } else if matches_kind(kind, &["extended", "ext", "e"]) {
144 CanId::extended(raw)
145 } else {
146 Err(Error::UnknownCanIdKind)
147 }
148 }
149}
150
151fn matches_kind(value: &str, candidates: &[&str]) -> bool {
152 candidates
153 .iter()
154 .any(|candidate| value.eq_ignore_ascii_case(candidate))
155}
156
157fn parse_can_id_number(value: &str) -> Result<u32> {
158 if let Some(hex) = value
159 .strip_prefix("0x")
160 .or_else(|| value.strip_prefix("0X"))
161 {
162 u32::from_str_radix(hex, 16).map_err(|_| Error::InvalidCanIdText)
163 } else {
164 value.parse::<u32>().map_err(|_| Error::InvalidCanIdText)
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct CanFrame {
170 id: CanId,
171 data: [u8; MAX_CAN_DATA_LEN],
172 len: u8,
173}
174
175impl CanFrame {
176 pub fn new(id: CanId, data: &[u8]) -> Result<Self> {
177 if data.len() > MAX_CAN_DATA_LEN {
178 return Err(Error::PayloadTooLong { len: data.len() });
179 }
180
181 let mut padded = [0_u8; MAX_CAN_DATA_LEN];
182 padded[..data.len()].copy_from_slice(data);
183 Ok(Self {
184 id,
185 data: padded,
186 len: data.len() as u8,
187 })
188 }
189
190 pub const fn from_payload(id: CanId, payload: DirectPayload) -> Self {
191 Self {
192 id,
193 data: payload.padded_data(),
194 len: payload.len as u8,
195 }
196 }
197
198 pub const fn id(&self) -> CanId {
199 self.id
200 }
201
202 pub fn data(&self) -> &[u8] {
203 &self.data[..self.len()]
204 }
205
206 pub const fn padded_data(&self) -> [u8; MAX_CAN_DATA_LEN] {
207 self.data
208 }
209
210 pub const fn len(&self) -> usize {
211 self.len as usize
212 }
213
214 pub const fn is_empty(&self) -> bool {
215 self.len == 0
216 }
217}
218
219impl fmt::Display for CanFrame {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 write!(f, "{} len={} data=", self.id, self.len())?;
222 for (index, byte) in self.data().iter().enumerate() {
223 if index > 0 {
224 f.write_str(" ")?;
225 }
226 write!(f, "{byte:02X}")?;
227 }
228 Ok(())
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233#[repr(u8)]
234pub enum ControlMode {
235 DutyCycle = 0x00,
236 CurrentLoop = 0x01,
237 CurrentBrake = 0x02,
238 VelocityLoop = 0x03,
239 PositionLoop = 0x04,
240 SetOrigin = 0x05,
241 PositionVelocity = 0x06,
242 MitMode = 0x08,
243}
244
245impl ControlMode {
246 pub const fn extended_id(self, motor_id: u8) -> Result<CanId> {
247 CanId::extended(((self as u32) << 8) | motor_id as u32)
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252#[repr(u8)]
253pub enum OriginMode {
254 Temporary = 0x00,
255 Permanent = 0x01,
256}
257
258impl OriginMode {
259 pub const fn data(self) -> u8 {
260 self as u8
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq)]
265pub enum DirectCommand {
266 DutyCycle(f64),
267 Current(f64),
268 BrakeCurrent(f64),
269 Velocity(i32),
270 Position(f64),
271 SetOrigin(OriginMode),
272 PositionVelocity {
273 position_degrees: f64,
274 velocity_erpm: i32,
275 acceleration_erpm_per_sec: i32,
276 },
277}
278
279impl DirectCommand {
280 pub const fn mode(self) -> ControlMode {
281 match self {
282 Self::DutyCycle(_) => ControlMode::DutyCycle,
283 Self::Current(_) => ControlMode::CurrentLoop,
284 Self::BrakeCurrent(_) => ControlMode::CurrentBrake,
285 Self::Velocity(_) => ControlMode::VelocityLoop,
286 Self::Position(_) => ControlMode::PositionLoop,
287 Self::SetOrigin(_) => ControlMode::SetOrigin,
288 Self::PositionVelocity { .. } => ControlMode::PositionVelocity,
289 }
290 }
291
292 pub fn payload(self) -> DirectPayload {
293 match self {
294 Self::DutyCycle(duty) => DirectPayload::from_slice(&scaled_i32_bytes(
295 duty,
296 -MAX_DUTY_CYCLE,
297 MAX_DUTY_CYCLE,
298 DUTY_SCALE,
299 )),
300 Self::Current(current_amps) | Self::BrakeCurrent(current_amps) => {
301 DirectPayload::from_slice(&scaled_i32_bytes(
302 current_amps,
303 -MAX_CURRENT_AMPS,
304 MAX_CURRENT_AMPS,
305 CURRENT_SCALE,
306 ))
307 }
308 Self::Velocity(erpm) => DirectPayload::from_slice(
309 &erpm
310 .clamp(MIN_PROTOCOL_ERPM, MAX_PROTOCOL_ERPM)
311 .to_be_bytes(),
312 ),
313 Self::Position(position_degrees) => DirectPayload::from_slice(&scaled_i32_bytes(
314 position_degrees,
315 MIN_POSITION_DEGREES,
316 MAX_POSITION_DEGREES,
317 POSITION_SCALE,
318 )),
319 Self::SetOrigin(origin_mode) => DirectPayload::from_slice(&[origin_mode.data()]),
320 Self::PositionVelocity {
321 position_degrees,
322 velocity_erpm,
323 acceleration_erpm_per_sec,
324 } => {
325 let mut payload = [0_u8; MAX_CAN_DATA_LEN];
326 payload[..4].copy_from_slice(&scaled_i32_bytes(
327 position_degrees,
328 MIN_POSITION_DEGREES,
329 MAX_POSITION_DEGREES,
330 POSITION_SCALE,
331 ));
332 payload[4..6].copy_from_slice(&scaled_i16_units(
333 velocity_erpm,
334 MIN_PROFILE_ERPM,
335 MAX_PROFILE_ERPM,
336 PROFILE_ERPM_PER_UNIT,
337 ));
338 payload[6..8].copy_from_slice(&scaled_i16_units(
339 acceleration_erpm_per_sec,
340 MIN_PROFILE_ACCELERATION_ERPM_PER_SEC,
341 MAX_PROFILE_ACCELERATION_ERPM_PER_SEC,
342 PROFILE_ERPM_PER_UNIT,
343 ));
344 DirectPayload::from_array(payload, MAX_CAN_DATA_LEN)
345 }
346 }
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351pub struct DirectPayload {
352 data: [u8; MAX_CAN_DATA_LEN],
353 len: usize,
354}
355
356impl DirectPayload {
357 pub const fn len(&self) -> usize {
358 self.len
359 }
360
361 pub const fn is_empty(&self) -> bool {
362 self.len == 0
363 }
364
365 pub fn data(&self) -> &[u8] {
366 &self.data[..self.len]
367 }
368
369 pub const fn padded_data(&self) -> [u8; MAX_CAN_DATA_LEN] {
370 self.data
371 }
372
373 fn from_slice(data: &[u8]) -> Self {
374 let mut payload = [0_u8; MAX_CAN_DATA_LEN];
375 payload[..data.len()].copy_from_slice(data);
376 Self {
377 data: payload,
378 len: data.len(),
379 }
380 }
381
382 const fn from_array(data: [u8; MAX_CAN_DATA_LEN], len: usize) -> Self {
383 Self { data, len }
384 }
385}
386
387pub fn direct_command_frame(motor_id: u8, command: DirectCommand) -> Result<CanFrame> {
388 let payload = command.payload();
389 Ok(CanFrame::from_payload(
390 command.mode().extended_id(motor_id)?,
391 payload,
392 ))
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum RobStrideFrameKind {
397 Mit,
398 Position,
399 Speed,
400 Status,
401 Config,
402}
403
404impl RobStrideFrameKind {
405 pub const fn base_id(self) -> u32 {
406 match self {
407 Self::Mit => ROBSTRIDE_MIT_BASE_ID,
408 Self::Position => ROBSTRIDE_POSITION_BASE_ID,
409 Self::Speed => ROBSTRIDE_SPEED_BASE_ID,
410 Self::Status => ROBSTRIDE_STATUS_BASE_ID,
411 Self::Config => ROBSTRIDE_CONFIG_BASE_ID,
412 }
413 }
414
415 pub const fn can_id(self, motor_id: u8) -> Result<CanId> {
416 CanId::extended(self.base_id() + motor_id as u32)
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq)]
421pub struct RobStrideFeedback {
422 pub position_rad: f64,
423 pub velocity_rad_s: f64,
424 pub torque_nm: f64,
425}
426
427impl RobStrideFeedback {
428 pub fn parse(data: &[u8]) -> Option<Self> {
429 if data.len() < MAX_CAN_DATA_LEN {
430 return None;
431 }
432
433 let position_raw = i32::from_le_bytes([data[0], data[1], data[2], data[3]]);
434 let velocity_raw = i16::from_le_bytes([data[4], data[5]]);
435 let torque_raw = i16::from_le_bytes([data[6], data[7]]);
436
437 Some(Self {
438 position_rad: position_raw as f64 / 1_000.0,
439 velocity_rad_s: velocity_raw as f64 / 1_000.0,
440 torque_nm: torque_raw as f64 / 10.0,
441 })
442 }
443}
444
445fn scaled_i32_bytes(value: f64, min: f64, max: f64, scale: f64) -> [u8; 4] {
446 let scaled = round_f64(value.clamp(min, max) * scale);
447 (scaled.clamp(i32::MIN as f64, i32::MAX as f64) as i32).to_be_bytes()
448}
449
450fn round_f64(value: f64) -> f64 {
451 if value >= 0.0 {
452 (value + 0.5) as i64 as f64
453 } else {
454 (value - 0.5) as i64 as f64
455 }
456}
457
458fn scaled_i16_units(value: i32, min: i32, max: i32, units_per_lsb: i32) -> [u8; 2] {
459 let units = value.clamp(min, max) / units_per_lsb;
460 (units as i16).to_be_bytes()
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use std::string::ToString;
467
468 #[test]
469 fn direct_payloads_use_protocol_scaling() {
470 let current = DirectCommand::Current(1.0).payload();
471 assert_eq!(current.data(), &[0x00, 0x00, 0x03, 0xE8]);
472
473 let profile = DirectCommand::PositionVelocity {
474 position_degrees: 1.5,
475 velocity_erpm: -100,
476 acceleration_erpm_per_sec: 200,
477 }
478 .payload();
479 assert_eq!(profile.len(), 8);
480 assert_eq!(&profile.data()[..4], &15_000_i32.to_be_bytes());
481 assert_eq!(&profile.data()[4..6], &(-10_i16).to_be_bytes());
482 assert_eq!(&profile.data()[6..8], &20_i16.to_be_bytes());
483 }
484
485 #[test]
486 fn direct_frame_uses_natural_length() {
487 let frame =
488 direct_command_frame(0x03, DirectCommand::SetOrigin(OriginMode::Permanent)).unwrap();
489
490 assert_eq!(frame.id(), CanId::Extended(0x0503));
491 assert_eq!(frame.len(), 1);
492 assert_eq!(frame.data(), &[0x01]);
493 }
494
495 #[test]
496 fn can_ids_and_frames_format_and_parse() {
497 let standard = CanId::standard(0x123).unwrap();
498 let extended = CanId::extended(0x501).unwrap();
499 let frame = CanFrame::new(extended, &[0x0A, 0xB0]).unwrap();
500
501 assert_eq!(standard.kind_name(), "standard");
502 assert_eq!(extended.kind_name(), "extended");
503 assert_eq!(standard.to_string(), "standard 0x123");
504 assert_eq!(extended.to_string(), "extended 0x501");
505 assert_eq!(frame.to_string(), "extended 0x501 len=2 data=0A B0");
506 assert_eq!(standard.to_string().parse::<CanId>().unwrap(), standard);
507 assert_eq!(extended.to_string().parse::<CanId>().unwrap(), extended);
508 assert_eq!("s:0x123".parse::<CanId>().unwrap(), standard);
509 assert_eq!("e:0x501".parse::<CanId>().unwrap(), extended);
510 assert_eq!(
511 "0x123".parse::<CanId>().unwrap_err(),
512 Error::InvalidCanIdText
513 );
514 assert_eq!(
515 "standard 0x800".parse::<CanId>().unwrap_err(),
516 Error::InvalidCanId(0x800)
517 );
518 }
519
520 #[test]
521 fn robstride_status_feedback_decodes() {
522 let mut data = [0_u8; 8];
523 data[..4].copy_from_slice(&1_250_i32.to_le_bytes());
524 data[4..6].copy_from_slice(&(-500_i16).to_le_bytes());
525 data[6..8].copy_from_slice(&42_i16.to_le_bytes());
526
527 let feedback = RobStrideFeedback::parse(&data).unwrap();
528
529 assert_eq!(RobStrideFrameKind::Status.can_id(1).unwrap().raw(), 0x501);
530 assert_eq!(feedback.position_rad, 1.25);
531 assert_eq!(feedback.velocity_rad_s, -0.5);
532 assert_eq!(feedback.torque_nm, 4.2);
533 }
534}