1use std::collections::HashMap;
6
7#[allow(unused_imports)]
8use super::functions::*;
9use super::functions::{Mat4, ROS_MAGIC, Vec3};
10
11#[derive(Debug, Clone, Default)]
13pub struct JointStateMsg {
14 pub timestamp: f64,
16 pub names: Vec<String>,
18 pub positions: Vec<f64>,
20 pub velocities: Vec<f64>,
22 pub efforts: Vec<f64>,
24}
25impl JointStateMsg {
26 pub fn new(
30 timestamp: f64,
31 names: Vec<String>,
32 positions: Vec<f64>,
33 velocities: Vec<f64>,
34 efforts: Vec<f64>,
35 ) -> Self {
36 debug_assert_eq!(names.len(), positions.len());
37 debug_assert_eq!(names.len(), velocities.len());
38 debug_assert_eq!(names.len(), efforts.len());
39 Self {
40 timestamp,
41 names,
42 positions,
43 velocities,
44 efforts,
45 }
46 }
47 pub fn len(&self) -> usize {
49 self.names.len()
50 }
51 pub fn is_empty(&self) -> bool {
53 self.names.is_empty()
54 }
55 pub fn index_of(&self, name: &str) -> Option<usize> {
57 self.names.iter().position(|n| n == name)
58 }
59 pub fn position_of(&self, name: &str) -> Option<f64> {
61 self.index_of(name).map(|i| self.positions[i])
62 }
63 pub fn unit_kinetic_energy(&self) -> f64 {
65 self.velocities.iter().map(|v| 0.5 * v * v).sum()
66 }
67 pub fn to_csv_line(&self) -> String {
69 let fields: Vec<String> = self
70 .names
71 .iter()
72 .enumerate()
73 .map(|(i, n)| {
74 format!(
75 "{}:{:.6}:{:.6}:{:.6}",
76 n, self.positions[i], self.velocities[i], self.efforts[i]
77 )
78 })
79 .collect();
80 format!("{:.6},{}", self.timestamp, fields.join(","))
81 }
82 pub fn to_bytes(&self) -> Vec<u8> {
84 let n = self.names.len();
85 let mut buf = Vec::with_capacity(8 + n * 24);
86 buf.extend_from_slice(&(self.timestamp).to_le_bytes());
87 for i in 0..n {
88 buf.extend_from_slice(&self.positions[i].to_le_bytes());
89 buf.extend_from_slice(&self.velocities[i].to_le_bytes());
90 buf.extend_from_slice(&self.efforts[i].to_le_bytes());
91 }
92 buf
93 }
94}
95#[derive(Debug, Clone)]
100pub struct WorkspaceGrid {
101 pub nx: usize,
103 pub ny: usize,
105 pub nz: usize,
107 pub cell_size: f64,
109 pub origin: Vec3,
111 pub data: Vec<f32>,
113}
114impl WorkspaceGrid {
115 pub fn new(nx: usize, ny: usize, nz: usize, cell_size: f64, origin: Vec3) -> Self {
117 Self {
118 nx,
119 ny,
120 nz,
121 cell_size,
122 origin,
123 data: vec![0.0; nx * ny * nz],
124 }
125 }
126 pub fn world_to_index(&self, pos: Vec3) -> Option<(usize, usize, usize)> {
128 let ix = ((pos[0] - self.origin[0]) / self.cell_size) as isize;
129 let iy = ((pos[1] - self.origin[1]) / self.cell_size) as isize;
130 let iz = ((pos[2] - self.origin[2]) / self.cell_size) as isize;
131 if ix < 0 || iy < 0 || iz < 0 {
132 return None;
133 }
134 let (ix, iy, iz) = (ix as usize, iy as usize, iz as usize);
135 if ix >= self.nx || iy >= self.ny || iz >= self.nz {
136 return None;
137 }
138 Some((ix, iy, iz))
139 }
140 pub fn get_at(&self, pos: Vec3) -> Option<f32> {
142 let (ix, iy, iz) = self.world_to_index(pos)?;
143 Some(self.data[ix + self.nx * (iy + self.ny * iz)])
144 }
145 pub fn set_at(&mut self, pos: Vec3, value: f32) -> bool {
147 if let Some((ix, iy, iz)) = self.world_to_index(pos) {
148 self.data[ix + self.nx * (iy + self.ny * iz)] = value;
149 true
150 } else {
151 false
152 }
153 }
154 pub fn reachability_fraction(&self, threshold: f32) -> f64 {
156 let count = self.data.iter().filter(|&&v| v >= threshold).count();
157 count as f64 / self.data.len() as f64
158 }
159 pub fn max_reachability(&self) -> f32 {
161 self.data.iter().cloned().fold(0.0f32, f32::max)
162 }
163 pub fn to_bytes(&self) -> Vec<u8> {
165 let header_size = 4 * 3 + 8 + 8 * 3;
166 let mut buf = Vec::with_capacity(header_size + self.data.len() * 4);
167 buf.extend_from_slice(&(self.nx as u32).to_le_bytes());
168 buf.extend_from_slice(&(self.ny as u32).to_le_bytes());
169 buf.extend_from_slice(&(self.nz as u32).to_le_bytes());
170 buf.extend_from_slice(&self.cell_size.to_le_bytes());
171 for v in &self.origin {
172 buf.extend_from_slice(&v.to_le_bytes());
173 }
174 for &v in &self.data {
175 buf.extend_from_slice(&v.to_le_bytes());
176 }
177 buf
178 }
179}
180#[derive(Debug, Clone)]
182pub struct UrdfJoint {
183 pub name: String,
185 pub joint_type: JointType,
187 pub parent: String,
189 pub child: String,
191 pub origin_xyz: Vec3,
193 pub origin_rpy: Vec3,
195 pub axis: Vec3,
197 pub limits: JointLimits,
199 pub damping: f64,
201 pub friction: f64,
203}
204impl UrdfJoint {
205 pub fn revolute(
207 name: impl Into<String>,
208 parent: impl Into<String>,
209 child: impl Into<String>,
210 ) -> Self {
211 Self {
212 name: name.into(),
213 joint_type: JointType::Revolute,
214 parent: parent.into(),
215 child: child.into(),
216 origin_xyz: [0.0; 3],
217 origin_rpy: [0.0; 3],
218 axis: [0.0, 0.0, 1.0],
219 limits: JointLimits::default(),
220 damping: 0.1,
221 friction: 0.01,
222 }
223 }
224 pub fn is_moveable(&self) -> bool {
226 !matches!(self.joint_type, JointType::Fixed)
227 }
228 pub fn local_transform_zero(&self) -> Mat4 {
230 let t = mat4_translation(self.origin_xyz);
231 let r = rpy_to_mat4(self.origin_rpy);
232 mat4_mul(t, r)
233 }
234}
235#[derive(Debug, Clone)]
237pub struct IkSolution {
238 pub success: bool,
240 pub joint_positions: Vec<f64>,
242 pub position_error: f64,
244 pub orientation_error: f64,
246 pub iterations: u32,
248 pub solver_name: String,
250}
251impl IkSolution {
252 pub fn success(
254 joint_positions: Vec<f64>,
255 position_error: f64,
256 orientation_error: f64,
257 iterations: u32,
258 ) -> Self {
259 Self {
260 success: true,
261 joint_positions,
262 position_error,
263 orientation_error,
264 iterations,
265 solver_name: "default".to_string(),
266 }
267 }
268 pub fn failure(iterations: u32) -> Self {
270 Self {
271 success: false,
272 joint_positions: Vec::new(),
273 position_error: f64::INFINITY,
274 orientation_error: f64::INFINITY,
275 iterations,
276 solver_name: "default".to_string(),
277 }
278 }
279 pub fn total_error(&self) -> f64 {
281 (self.position_error * self.position_error
282 + self.orientation_error * self.orientation_error)
283 .sqrt()
284 }
285 pub fn to_json_string(&self) -> String {
287 let joints: Vec<String> = self
288 .joint_positions
289 .iter()
290 .map(|q| format!("{q:.6}"))
291 .collect();
292 format!(
293 "{{\"success\":{},\"joints\":[{}],\"pos_err\":{:.8},\"ori_err\":{:.8},\"iter\":{}}}",
294 self.success,
295 joints.join(","),
296 self.position_error,
297 self.orientation_error,
298 self.iterations
299 )
300 }
301}
302#[derive(Debug, Default, Clone)]
304pub struct IkSolutionLog {
305 pub solutions: Vec<IkSolution>,
307}
308impl IkSolutionLog {
309 pub fn new() -> Self {
311 Self::default()
312 }
313 pub fn push(&mut self, sol: IkSolution) {
315 self.solutions.push(sol);
316 }
317 pub fn success_rate(&self) -> f64 {
319 if self.solutions.is_empty() {
320 return 0.0;
321 }
322 let n = self.solutions.iter().filter(|s| s.success).count();
323 n as f64 / self.solutions.len() as f64
324 }
325 pub fn mean_position_error(&self) -> f64 {
327 let succ: Vec<_> = self.solutions.iter().filter(|s| s.success).collect();
328 if succ.is_empty() {
329 return f64::NAN;
330 }
331 succ.iter().map(|s| s.position_error).sum::<f64>() / succ.len() as f64
332 }
333 pub fn mean_iterations(&self) -> f64 {
335 if self.solutions.is_empty() {
336 return 0.0;
337 }
338 self.solutions
339 .iter()
340 .map(|s| s.iterations as f64)
341 .sum::<f64>()
342 / self.solutions.len() as f64
343 }
344}
345#[derive(Debug, Clone, Copy)]
347pub struct CameraIntrinsics {
348 pub fx: f64,
350 pub fy: f64,
352 pub cx: f64,
354 pub cy: f64,
356 pub width: u32,
358 pub height: u32,
360}
361impl CameraIntrinsics {
362 pub fn new(fx: f64, fy: f64, cx: f64, cy: f64, width: u32, height: u32) -> Self {
364 Self {
365 fx,
366 fy,
367 cx,
368 cy,
369 width,
370 height,
371 }
372 }
373 pub fn fov_x(&self) -> f64 {
375 2.0 * (self.width as f64 / (2.0 * self.fx)).atan()
376 }
377 pub fn fov_y(&self) -> f64 {
379 2.0 * (self.height as f64 / (2.0 * self.fy)).atan()
380 }
381 pub fn project(&self, point: Vec3) -> Option<[f64; 2]> {
385 let z = point[2];
386 if z <= 0.0 {
387 return None;
388 }
389 let u = self.fx * point[0] / z + self.cx;
390 let v = self.fy * point[1] / z + self.cy;
391 Some([u, v])
392 }
393}
394#[derive(Debug, Clone, Copy)]
396pub struct ScanPoint {
397 pub position: Vec3,
399 pub intensity: f32,
401 pub ring: u16,
403 pub return_num: u8,
405}
406impl ScanPoint {
407 pub fn from_position(pos: Vec3) -> Self {
409 Self {
410 position: pos,
411 intensity: 0.0,
412 ring: 0,
413 return_num: 1,
414 }
415 }
416}
417#[derive(Debug, Clone, Copy)]
419pub(super) struct CubicCoeffs {
420 pub(super) a0: f64,
421 pub(super) a1: f64,
422 pub(super) a2: f64,
423 pub(super) a3: f64,
424}
425impl CubicCoeffs {
426 fn eval(&self, t: f64) -> f64 {
427 self.a0 + self.a1 * t + self.a2 * t * t + self.a3 * t * t * t
428 }
429 fn deriv(&self, t: f64) -> f64 {
430 self.a1 + 2.0 * self.a2 * t + 3.0 * self.a3 * t * t
431 }
432}
433#[derive(Debug, Clone)]
435pub struct UrdfVisualElement {
436 pub name: Option<String>,
438 pub origin_xyz: Vec3,
440 pub origin_rpy: Vec3,
442 pub geometry: UrdfGeometry,
444 pub material: Option<String>,
446}
447impl UrdfVisualElement {
448 pub fn new(geometry: UrdfGeometry) -> Self {
450 Self {
451 name: None,
452 origin_xyz: [0.0; 3],
453 origin_rpy: [0.0; 3],
454 geometry,
455 material: None,
456 }
457 }
458 pub fn bounding_radius(&self) -> f64 {
460 match &self.geometry {
461 UrdfGeometry::Box { half_extents } => norm3(*half_extents),
462 UrdfGeometry::Sphere { radius } => *radius,
463 UrdfGeometry::Cylinder {
464 radius,
465 half_length,
466 } => (radius * radius + half_length * half_length).sqrt(),
467 UrdfGeometry::Mesh { scale, .. } => norm3(*scale),
468 }
469 }
470}
471#[derive(Debug, Clone, Copy)]
473pub struct WrenchStamped {
474 pub timestamp: f64,
476 pub wrench: Wrench,
478}
479impl WrenchStamped {
480 pub fn new(timestamp: f64, wrench: Wrench) -> Self {
482 Self { timestamp, wrench }
483 }
484}
485#[derive(Debug, Clone)]
487pub struct CameraRobotCalibration {
488 pub camera_to_robot: Mat4,
490 pub robot_to_camera: Mat4,
492 pub intrinsics: CameraIntrinsics,
494 pub residual_pixels: f64,
496 pub calibration_date: String,
498}
499impl CameraRobotCalibration {
500 pub fn identity(intrinsics: CameraIntrinsics) -> Self {
502 Self {
503 camera_to_robot: mat4_identity(),
504 robot_to_camera: mat4_identity(),
505 intrinsics,
506 residual_pixels: 0.0,
507 calibration_date: "2026-01-01".to_string(),
508 }
509 }
510 pub fn camera_to_robot_point(&self, p: Vec3) -> Vec3 {
512 apply_transform(&self.camera_to_robot, p)
513 }
514 pub fn robot_to_camera_point(&self, p: Vec3) -> Vec3 {
516 apply_transform(&self.robot_to_camera, p)
517 }
518}
519#[derive(Debug, Clone, Copy, Default)]
521pub struct Wrench {
522 pub force: Vec3,
524 pub torque: Vec3,
526}
527impl Wrench {
528 pub fn new(force: Vec3, torque: Vec3) -> Self {
530 Self { force, torque }
531 }
532 pub fn force_magnitude(&self) -> f64 {
534 norm3(self.force)
535 }
536 pub fn torque_magnitude(&self) -> f64 {
538 norm3(self.torque)
539 }
540 pub fn add(&self, other: &Wrench) -> Wrench {
542 Wrench {
543 force: add3(self.force, other.force),
544 torque: add3(self.torque, other.torque),
545 }
546 }
547 pub fn scale(&self, s: f64) -> Wrench {
549 Wrench {
550 force: scale3(self.force, s),
551 torque: scale3(self.torque, s),
552 }
553 }
554 pub fn to_bytes(&self) -> [u8; 48] {
556 let mut buf = [0u8; 48];
557 for (i, v) in self.force.iter().chain(self.torque.iter()).enumerate() {
558 buf[i * 8..(i + 1) * 8].copy_from_slice(&v.to_le_bytes());
559 }
560 buf
561 }
562}
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565pub enum MsgType {
566 JointState = 1,
568 Wrench = 2,
570 GripperState = 3,
572 PointCloud = 4,
574 Generic = 255,
576}
577#[derive(Debug, Clone)]
579pub struct UrdfInertial {
580 pub mass: f64,
582 pub com: Vec3,
584 pub inertia_diag: Vec3,
586 pub inertia_off: Vec3,
588}
589#[derive(Debug, Clone, PartialEq)]
591pub enum UrdfGeometry {
592 Box {
594 half_extents: Vec3,
596 },
597 Sphere {
599 radius: f64,
601 },
602 Cylinder {
604 radius: f64,
606 half_length: f64,
608 },
609 Mesh {
611 filename: String,
613 scale: Vec3,
615 },
616}
617#[derive(Debug, Clone, PartialEq)]
619pub enum JointType {
620 Fixed,
622 Revolute,
624 Prismatic,
626 Continuous,
628 Planar,
630 Floating,
632}
633#[derive(Debug, Clone)]
635pub struct UrdfLink {
636 pub name: String,
638 pub inertial: UrdfInertial,
640 pub visuals: Vec<UrdfVisualElement>,
642 pub collisions: Vec<UrdfVisualElement>,
644}
645impl UrdfLink {
646 pub fn new(name: impl Into<String>) -> Self {
648 Self {
649 name: name.into(),
650 inertial: UrdfInertial::default(),
651 visuals: Vec::new(),
652 collisions: Vec::new(),
653 }
654 }
655 pub fn approximate_visual_volume(&self) -> f64 {
657 self.visuals
658 .iter()
659 .map(|v| {
660 let r = v.bounding_radius();
661 (4.0 / 3.0) * std::f64::consts::PI * r * r * r
662 })
663 .sum()
664 }
665}
666#[derive(Debug, Clone)]
668pub struct JointWaypoint {
669 pub time: f64,
671 pub positions: Vec<f64>,
673 pub velocities: Option<Vec<f64>>,
675 pub accelerations: Option<Vec<f64>>,
677}
678impl JointWaypoint {
679 pub fn new(time: f64, positions: Vec<f64>) -> Self {
681 Self {
682 time,
683 positions,
684 velocities: None,
685 accelerations: None,
686 }
687 }
688}
689#[derive(Debug, Clone)]
693pub struct RosMsg {
694 pub msg_type: MsgType,
696 pub seq: u32,
698 pub payload: Vec<u8>,
700}
701impl RosMsg {
702 pub fn new(msg_type: MsgType, seq: u32, payload: Vec<u8>) -> Self {
704 Self {
705 msg_type,
706 seq,
707 payload,
708 }
709 }
710 fn checksum(data: &[u8]) -> u32 {
712 data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
713 }
714 pub fn to_bytes(&self) -> Vec<u8> {
716 let len = self.payload.len() as u32;
717 let crc = Self::checksum(&self.payload);
718 let mut buf = Vec::with_capacity(17 + self.payload.len());
719 buf.extend_from_slice(&ROS_MAGIC);
720 buf.push(self.msg_type as u8);
721 buf.extend_from_slice(&self.seq.to_le_bytes());
722 buf.extend_from_slice(&len.to_le_bytes());
723 buf.extend_from_slice(&self.payload);
724 buf.extend_from_slice(&crc.to_le_bytes());
725 buf
726 }
727 pub fn from_bytes(data: &[u8]) -> Option<Self> {
729 if data.len() < 17 {
730 return None;
731 }
732 if data[0..4] != ROS_MAGIC {
733 return None;
734 }
735 let msg_type = MsgType::try_from(data[4]).ok()?;
736 let seq = u32::from_le_bytes(data[5..9].try_into().ok()?);
737 let len = u32::from_le_bytes(data[9..13].try_into().ok()?) as usize;
738 if data.len() < 13 + len + 4 {
739 return None;
740 }
741 let payload = data[13..13 + len].to_vec();
742 let crc_stored = u32::from_le_bytes(data[13 + len..17 + len].try_into().ok()?);
743 let crc_computed = Self::checksum(&payload);
744 if crc_stored != crc_computed {
745 return None;
746 }
747 Some(Self {
748 msg_type,
749 seq,
750 payload,
751 })
752 }
753}
754#[derive(Debug, Default, Clone)]
756pub struct RobotPointCloud {
757 pub timestamp: f64,
759 pub points: Vec<ScanPoint>,
761 pub frame_id: String,
763 pub scanner_model: String,
765}
766impl RobotPointCloud {
767 pub fn new(timestamp: f64, frame_id: impl Into<String>) -> Self {
769 Self {
770 timestamp,
771 points: Vec::new(),
772 frame_id: frame_id.into(),
773 scanner_model: "unknown".to_string(),
774 }
775 }
776 pub fn len(&self) -> usize {
778 self.points.len()
779 }
780 pub fn is_empty(&self) -> bool {
782 self.points.is_empty()
783 }
784 pub fn aabb(&self) -> Option<(Vec3, Vec3)> {
786 if self.points.is_empty() {
787 return None;
788 }
789 let mut mn = [f64::INFINITY; 3];
790 let mut mx = [f64::NEG_INFINITY; 3];
791 for p in &self.points {
792 for k in 0..3 {
793 if p.position[k] < mn[k] {
794 mn[k] = p.position[k];
795 }
796 if p.position[k] > mx[k] {
797 mx[k] = p.position[k];
798 }
799 }
800 }
801 Some((mn, mx))
802 }
803 pub fn centroid(&self) -> Option<Vec3> {
805 if self.points.is_empty() {
806 return None;
807 }
808 let n = self.points.len() as f64;
809 let mut sum = [0.0; 3];
810 for p in &self.points {
811 sum[0] += p.position[0];
812 sum[1] += p.position[1];
813 sum[2] += p.position[2];
814 }
815 Some([sum[0] / n, sum[1] / n, sum[2] / n])
816 }
817 pub fn transform_in_place(&mut self, m: &Mat4) {
819 for p in &mut self.points {
820 p.position = apply_transform(m, p.position);
821 }
822 }
823 pub fn downsample(&self, stride: usize) -> Self {
825 let stride = stride.max(1);
826 let points: Vec<ScanPoint> = self
827 .points
828 .iter()
829 .enumerate()
830 .filter(|(i, _)| i % stride == 0)
831 .map(|(_, p)| *p)
832 .collect();
833 Self {
834 timestamp: self.timestamp,
835 points,
836 frame_id: self.frame_id.clone(),
837 scanner_model: self.scanner_model.clone(),
838 }
839 }
840 pub fn to_bytes(&self) -> Vec<u8> {
844 let n = self.points.len();
845 let mut buf = Vec::with_capacity(4 + n * 16);
846 buf.extend_from_slice(&(n as u32).to_le_bytes());
847 for p in &self.points {
848 for k in 0..3 {
849 buf.extend_from_slice(&(p.position[k] as f32).to_le_bytes());
850 }
851 buf.extend_from_slice(&p.intensity.to_le_bytes());
852 }
853 buf
854 }
855 pub fn from_bytes(data: &[u8]) -> Option<Self> {
857 if data.len() < 4 {
858 return None;
859 }
860 let n = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
861 if data.len() < 4 + n * 16 {
862 return None;
863 }
864 let mut points = Vec::with_capacity(n);
865 for i in 0..n {
866 let base = 4 + i * 16;
867 let read_f32 = |b: &[u8]| f32::from_le_bytes(b.try_into().unwrap_or([0u8; 4]));
868 let x = read_f32(&data[base..base + 4]) as f64;
869 let y = read_f32(&data[base + 4..base + 8]) as f64;
870 let z = read_f32(&data[base + 8..base + 12]) as f64;
871 let intensity = read_f32(&data[base + 12..base + 16]);
872 points.push(ScanPoint {
873 position: [x, y, z],
874 intensity,
875 ring: 0,
876 return_num: 1,
877 });
878 }
879 Some(Self {
880 timestamp: 0.0,
881 points,
882 frame_id: String::new(),
883 scanner_model: String::new(),
884 })
885 }
886}
887#[derive(Debug, Clone)]
889pub struct JointLimits {
890 pub lower: f64,
892 pub upper: f64,
894 pub effort: f64,
896 pub velocity: f64,
898}
899impl JointLimits {
900 pub fn in_range(&self, q: f64) -> bool {
902 q >= self.lower && q <= self.upper
903 }
904 pub fn clamp(&self, q: f64) -> f64 {
906 q.clamp(self.lower, self.upper)
907 }
908 pub fn range(&self) -> f64 {
910 self.upper - self.lower
911 }
912}
913#[derive(Debug, Clone, Copy)]
915pub struct GripperState {
916 pub position: f64,
918 pub force: f64,
920 pub max_width: f64,
922 pub object_detected: bool,
924 pub temperature: f64,
926}
927impl GripperState {
928 pub fn openness(&self) -> f64 {
930 if self.max_width < 1e-9 {
931 0.0
932 } else {
933 (self.position / self.max_width).clamp(0.0, 1.0)
934 }
935 }
936 pub fn is_closed(&self) -> bool {
938 self.position < 1e-3
939 }
940 pub fn to_bytes(&self) -> [u8; 40] {
942 let mut buf = [0u8; 40];
943 buf[0..8].copy_from_slice(&self.position.to_le_bytes());
944 buf[8..16].copy_from_slice(&self.force.to_le_bytes());
945 buf[16..24].copy_from_slice(&self.max_width.to_le_bytes());
946 buf[24..32].copy_from_slice(&self.temperature.to_le_bytes());
947 buf[32] = self.object_detected as u8;
948 buf
949 }
950 pub fn from_bytes(b: &[u8; 40]) -> Self {
952 let read = |s: &[u8]| f64::from_le_bytes(s.try_into().unwrap_or([0u8; 8]));
953 Self {
954 position: read(&b[0..8]),
955 force: read(&b[8..16]),
956 max_width: read(&b[16..24]),
957 temperature: read(&b[24..32]),
958 object_detected: b[32] != 0,
959 }
960 }
961}
962#[derive(Debug, Default, Clone)]
964pub struct RobotTrajectory {
965 pub waypoints: Vec<JointWaypoint>,
967 pub joint_names: Vec<String>,
969}
970impl RobotTrajectory {
971 pub fn new(joint_names: Vec<String>) -> Self {
973 Self {
974 waypoints: Vec::new(),
975 joint_names,
976 }
977 }
978 pub fn push(&mut self, wp: JointWaypoint) {
980 self.waypoints.push(wp);
981 }
982 pub fn duration(&self) -> f64 {
984 self.waypoints.last().map(|w| w.time).unwrap_or(0.0)
985 }
986 pub fn dof(&self) -> usize {
988 self.waypoints
989 .first()
990 .map(|w| w.positions.len())
991 .unwrap_or(0)
992 }
993 pub fn interpolate(&self, t: f64) -> Option<Vec<f64>> {
998 let wps = &self.waypoints;
999 if wps.len() < 2 {
1000 return None;
1001 }
1002 let dof = self.dof();
1003 let t_clamped = t.clamp(wps[0].time, wps[wps.len() - 1].time);
1004 let seg = wps
1005 .windows(2)
1006 .enumerate()
1007 .find(|(_, pair)| t_clamped >= pair[0].time && t_clamped <= pair[1].time)
1008 .map(|(i, _)| i)
1009 .unwrap_or(wps.len() - 2);
1010 let t0 = wps[seg].time;
1011 let t1 = wps[seg + 1].time;
1012 let dt = (t1 - t0).max(1e-12);
1013 let u = (t_clamped - t0) / dt;
1014 let mut result = vec![0.0; dof];
1015 for j in 0..dof {
1016 let p0 = wps[seg].positions[j];
1017 let p1 = wps[seg + 1].positions[j];
1018 let v0 = wps[seg]
1019 .velocities
1020 .as_ref()
1021 .map(|v| v[j] * dt)
1022 .unwrap_or(0.0);
1023 let v1 = wps[seg + 1]
1024 .velocities
1025 .as_ref()
1026 .map(|v| v[j] * dt)
1027 .unwrap_or(0.0);
1028 let c = CubicCoeffs {
1029 a0: p0,
1030 a1: v0,
1031 a2: 3.0 * (p1 - p0) - 2.0 * v0 - v1,
1032 a3: -2.0 * (p1 - p0) + v0 + v1,
1033 };
1034 result[j] = c.eval(u);
1035 }
1036 Some(result)
1037 }
1038 pub fn sample_uniform(&self, n_samples: usize) -> Vec<(f64, Vec<f64>)> {
1040 if n_samples == 0 || self.waypoints.is_empty() {
1041 return Vec::new();
1042 }
1043 let dur = self.duration();
1044 (0..n_samples)
1045 .filter_map(|i| {
1046 let t = if n_samples == 1 {
1047 0.0
1048 } else {
1049 i as f64 * dur / (n_samples - 1) as f64
1050 };
1051 self.interpolate(t).map(|q| (t, q))
1052 })
1053 .collect()
1054 }
1055 pub fn to_csv(&self) -> String {
1057 let header: Vec<String> = std::iter::once("time".to_string())
1058 .chain(self.joint_names.iter().cloned())
1059 .collect();
1060 let mut lines = vec![header.join(",")];
1061 for wp in &self.waypoints {
1062 let row: Vec<String> = std::iter::once(format!("{:.6}", wp.time))
1063 .chain(wp.positions.iter().map(|p| format!("{:.6}", p)))
1064 .collect();
1065 lines.push(row.join(","));
1066 }
1067 lines.join("\n")
1068 }
1069}
1070#[derive(Debug, Default, Clone)]
1072pub struct UrdfRobot {
1073 pub name: String,
1075 pub links: HashMap<String, UrdfLink>,
1077 pub joints: HashMap<String, UrdfJoint>,
1079 pub joint_order: Vec<String>,
1081}
1082impl UrdfRobot {
1083 pub fn new(name: impl Into<String>) -> Self {
1085 Self {
1086 name: name.into(),
1087 ..Default::default()
1088 }
1089 }
1090 pub fn add_link(&mut self, link: UrdfLink) {
1092 self.links.insert(link.name.clone(), link);
1093 }
1094 pub fn add_joint(&mut self, joint: UrdfJoint) {
1096 if !self.joint_order.contains(&joint.name) {
1097 self.joint_order.push(joint.name.clone());
1098 }
1099 self.joints.insert(joint.name.clone(), joint);
1100 }
1101 pub fn moveable_joint_names(&self) -> Vec<&str> {
1103 self.joint_order
1104 .iter()
1105 .filter(|n| {
1106 self.joints
1107 .get(n.as_str())
1108 .map(|j| j.is_moveable())
1109 .unwrap_or(false)
1110 })
1111 .map(|s| s.as_str())
1112 .collect()
1113 }
1114 pub fn dof(&self) -> usize {
1116 self.moveable_joint_names().len()
1117 }
1118 pub fn total_mass(&self) -> f64 {
1120 self.links.values().map(|l| l.inertial.mass).sum()
1121 }
1122 pub fn to_text(&self) -> String {
1124 let mut out = format!("robot name=\"{}\"\n", self.name);
1125 for (name, link) in &self.links {
1126 out += &format!(" link \"{name}\" mass={:.4} kg\n", link.inertial.mass);
1127 }
1128 for name in &self.joint_order {
1129 if let Some(j) = self.joints.get(name) {
1130 out += &format!(
1131 " joint \"{}\" type={} parent=\"{}\" child=\"{}\"\n",
1132 j.name, j.joint_type, j.parent, j.child
1133 );
1134 }
1135 }
1136 out
1137 }
1138 pub fn from_text(text: &str) -> Option<Self> {
1142 let mut robot = UrdfRobot::default();
1143 for line in text.lines() {
1144 let line = line.trim();
1145 if let Some(rest) = line.strip_prefix("robot name=\"") {
1146 robot.name = rest.trim_end_matches('"').to_string();
1147 } else if let Some(rest) = line.strip_prefix("link \"") {
1148 let parts: Vec<&str> = rest.splitn(2, '"').collect();
1149 if let Some(name) = parts.first() {
1150 let mass_str = rest
1151 .find("mass=")
1152 .map(|i| &rest[i + 5..])
1153 .and_then(|s| s.split_whitespace().next())
1154 .and_then(|s| s.trim_end_matches(" kg").parse::<f64>().ok())
1155 .unwrap_or(1.0);
1156 let mut link = UrdfLink::new(*name);
1157 link.inertial.mass = mass_str;
1158 robot.add_link(link);
1159 }
1160 }
1161 }
1162 if robot.name.is_empty() {
1163 None
1164 } else {
1165 Some(robot)
1166 }
1167 }
1168}