pamoja_ffi/motion.rs
1//! The C ABI for the motion helpers: chassis kinematics, an arm, odometry, waypoint
2//! guidance, the safety gate, and the conversions to servo, ESC, and encoder units.
3//!
4//! These wrap the robotics half of [`pamoja_kit`]. The kinematic models, the arm, the
5//! waypoint follower, and the servo, ESC, and encoder scales hold only their parameters and
6//! never change, so each crosses by value as a small struct the caller fills in and passes
7//! to every call; the helper applies the same fallbacks the Rust constructor does, such as
8//! taking a length's magnitude. The helpers that remember something between calls, the
9//! odometry, the e-stop, the watchdog, the limits, the safety gate, and the quadrature
10//! decoder, are opaque handles, each with its own `_free`. A constructor never returns
11//! null, and each method documents what it returns for a null handle.
12
13use std::ptr;
14
15use pamoja_kit::{
16 forward_kinematics, obstacle_stop, Ackermann, Coordinate, DhParameters, DiffDrive, EStop,
17 Elbow, Esc, Limits, Mecanum, Odometry, Pose, Quadrature, QuadratureScale, SafetyGate, ServoMap,
18 SkidSteer, Transform, Twist, TwoLinkArm, Watchdog, WaypointFollower, WheelSpeeds,
19};
20
21use crate::kit::PamojaCoordinate;
22
23/// Where a robot is and which way it faces.
24#[repr(C)]
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct PamojaPose {
27 /// Position along the world x axis, in meters.
28 pub x: f32,
29 /// Position along the world y axis, in meters.
30 pub y: f32,
31 /// Heading from the world x axis, in radians, positive counter-clockwise.
32 pub theta: f32,
33}
34
35impl From<PamojaPose> for Pose {
36 fn from(value: PamojaPose) -> Self {
37 Pose::new(value.x, value.y, value.theta)
38 }
39}
40
41impl From<Pose> for PamojaPose {
42 fn from(value: Pose) -> Self {
43 Self {
44 x: value.x,
45 y: value.y,
46 theta: value.theta,
47 }
48 }
49}
50
51/// How fast a robot is asked to move.
52#[repr(C)]
53#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct PamojaTwist {
55 /// Forward speed along the x axis.
56 pub vx: f32,
57 /// Leftward speed along the y axis; zero for drives that cannot strafe.
58 pub vy: f32,
59 /// Yaw rate about the z axis, positive counter-clockwise.
60 pub omega: f32,
61}
62
63impl From<PamojaTwist> for Twist {
64 fn from(value: PamojaTwist) -> Self {
65 Twist::new(value.vx, value.vy, value.omega)
66 }
67}
68
69impl From<Twist> for PamojaTwist {
70 fn from(value: Twist) -> Self {
71 Self {
72 vx: value.vx,
73 vy: value.vy,
74 omega: value.omega,
75 }
76 }
77}
78
79/// The speeds of a two-sided drive's left and right wheels or tracks.
80#[repr(C)]
81#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct PamojaSideSpeeds {
83 /// The left side's speed.
84 pub left: f32,
85 /// The right side's speed.
86 pub right: f32,
87}
88
89/// A planar body motion: a forward speed and a yaw rate.
90#[repr(C)]
91#[derive(Clone, Copy, Debug, PartialEq)]
92pub struct PamojaBodyMotion {
93 /// The forward speed.
94 pub linear: f32,
95 /// The yaw rate, positive turning left.
96 pub angular: f32,
97}
98
99/// A differential drive: two wheels `track` apart.
100#[repr(C)]
101#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct PamojaDiffDrive {
103 /// The distance between the left and right wheels; its magnitude is used.
104 pub track: f32,
105}
106
107/// Returns the wheel speeds a differential drive needs for a body motion.
108///
109/// # Arguments
110///
111/// * `drive` - the drive.
112/// * `linear` - the forward speed.
113/// * `angular` - the turn rate, positive turning left.
114///
115/// # Returns
116///
117/// The left and right wheel speeds.
118#[no_mangle]
119pub extern "C" fn pamoja_diff_drive_wheel_speeds(
120 drive: PamojaDiffDrive,
121 linear: f32,
122 angular: f32,
123) -> PamojaSideSpeeds {
124 let (left, right) = DiffDrive::new(drive.track).wheel_speeds(linear, angular);
125 PamojaSideSpeeds { left, right }
126}
127
128/// Returns the body motion a differential drive makes from measured wheel speeds.
129///
130/// # Arguments
131///
132/// * `drive` - the drive.
133/// * `left` - the left wheel speed.
134/// * `right` - the right wheel speed.
135///
136/// # Returns
137///
138/// The forward speed and turn rate; the turn rate is 0 for a drive with no track.
139#[no_mangle]
140pub extern "C" fn pamoja_diff_drive_body_motion(
141 drive: PamojaDiffDrive,
142 left: f32,
143 right: f32,
144) -> PamojaBodyMotion {
145 let (linear, angular) = DiffDrive::new(drive.track).body_motion(left, right);
146 PamojaBodyMotion { linear, angular }
147}
148
149/// Car-like steering: one steered axle and a driven axle `wheelbase` apart.
150#[repr(C)]
151#[derive(Clone, Copy, Debug, PartialEq)]
152pub struct PamojaAckermann {
153 /// The distance from the steered axle to the driven axle; its magnitude is used.
154 pub wheelbase: f32,
155}
156
157/// Returns the steering angle for a forward speed and a yaw rate.
158///
159/// # Arguments
160///
161/// * `car` - the vehicle.
162/// * `linear` - the forward speed.
163/// * `angular` - the desired yaw rate, positive turning left.
164///
165/// # Returns
166///
167/// The steering angle in radians, or 0 while the vehicle is stopped.
168#[no_mangle]
169pub extern "C" fn pamoja_ackermann_steering_angle(
170 car: PamojaAckermann,
171 linear: f32,
172 angular: f32,
173) -> f32 {
174 Ackermann::new(car.wheelbase).steering_angle(linear, angular)
175}
176
177/// Returns the yaw rate a forward speed and a steering angle produce.
178///
179/// # Arguments
180///
181/// * `car` - the vehicle.
182/// * `linear` - the forward speed.
183/// * `steering` - the steering angle in radians.
184///
185/// # Returns
186///
187/// The yaw rate, or 0 for a vehicle with no wheelbase.
188#[no_mangle]
189pub extern "C" fn pamoja_ackermann_yaw_rate(
190 car: PamojaAckermann,
191 linear: f32,
192 steering: f32,
193) -> f32 {
194 Ackermann::new(car.wheelbase).yaw_rate(linear, steering)
195}
196
197/// Returns the turn radius of a steering angle.
198///
199/// # Arguments
200///
201/// * `car` - the vehicle.
202/// * `steering` - the steering angle in radians.
203///
204/// # Returns
205///
206/// The radius in meters, or infinity with the wheels straight.
207#[no_mangle]
208pub extern "C" fn pamoja_ackermann_turn_radius(car: PamojaAckermann, steering: f32) -> f32 {
209 Ackermann::new(car.wheelbase).turn_radius(steering)
210}
211
212/// Returns the path curvature of a steering angle, the reciprocal of the turn radius.
213///
214/// # Arguments
215///
216/// * `car` - the vehicle.
217/// * `steering` - the steering angle in radians.
218///
219/// # Returns
220///
221/// The curvature, or 0 for a vehicle with no wheelbase.
222#[no_mangle]
223pub extern "C" fn pamoja_ackermann_curvature(car: PamojaAckermann, steering: f32) -> f32 {
224 Ackermann::new(car.wheelbase).curvature(steering)
225}
226
227/// A skid-steer drive: tracks or wheels `track` apart that slip sideways to turn.
228#[repr(C)]
229#[derive(Clone, Copy, Debug, PartialEq)]
230pub struct PamojaSkidSteer {
231 /// The distance between the left and right sides; its magnitude is used.
232 pub track: f32,
233 /// How much wider the effective track is than the geometric one; its magnitude is
234 /// used, and 0 is taken as 1, no slip.
235 pub slip: f32,
236}
237
238/// Returns the side speeds a skid-steer drive needs for a body motion.
239///
240/// # Arguments
241///
242/// * `drive` - the drive.
243/// * `linear` - the forward speed.
244/// * `angular` - the yaw rate, positive turning left.
245///
246/// # Returns
247///
248/// The left and right speeds, split across the slip-corrected track.
249#[no_mangle]
250pub extern "C" fn pamoja_skid_steer_wheel_speeds(
251 drive: PamojaSkidSteer,
252 linear: f32,
253 angular: f32,
254) -> PamojaSideSpeeds {
255 let (left, right) = SkidSteer::new(drive.track, drive.slip).wheel_speeds(linear, angular);
256 PamojaSideSpeeds { left, right }
257}
258
259/// Returns the body motion a skid-steer drive makes from measured side speeds.
260///
261/// # Arguments
262///
263/// * `drive` - the drive.
264/// * `left` - the left side's speed.
265/// * `right` - the right side's speed.
266///
267/// # Returns
268///
269/// The forward speed and yaw rate; the yaw rate is 0 for a drive with no track.
270#[no_mangle]
271pub extern "C" fn pamoja_skid_steer_body_motion(
272 drive: PamojaSkidSteer,
273 left: f32,
274 right: f32,
275) -> PamojaBodyMotion {
276 let (linear, angular) = SkidSteer::new(drive.track, drive.slip).body_motion(left, right);
277 PamojaBodyMotion { linear, angular }
278}
279
280/// A mecanum base: four wheels `wheelbase` front to rear and `track` side to side.
281#[repr(C)]
282#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct PamojaMecanum {
284 /// The front-to-rear distance between the axles; its magnitude is used.
285 pub wheelbase: f32,
286 /// The left-to-right distance between the wheels; its magnitude is used.
287 pub track: f32,
288}
289
290/// The four wheel speeds of a mecanum base.
291#[repr(C)]
292#[derive(Clone, Copy, Debug, PartialEq)]
293pub struct PamojaWheelSpeeds {
294 /// The front-left wheel's speed.
295 pub front_left: f32,
296 /// The front-right wheel's speed.
297 pub front_right: f32,
298 /// The rear-left wheel's speed.
299 pub rear_left: f32,
300 /// The rear-right wheel's speed.
301 pub rear_right: f32,
302}
303
304impl From<PamojaWheelSpeeds> for WheelSpeeds {
305 fn from(value: PamojaWheelSpeeds) -> Self {
306 WheelSpeeds {
307 front_left: value.front_left,
308 front_right: value.front_right,
309 rear_left: value.rear_left,
310 rear_right: value.rear_right,
311 }
312 }
313}
314
315impl From<WheelSpeeds> for PamojaWheelSpeeds {
316 fn from(value: WheelSpeeds) -> Self {
317 Self {
318 front_left: value.front_left,
319 front_right: value.front_right,
320 rear_left: value.rear_left,
321 rear_right: value.rear_right,
322 }
323 }
324}
325
326/// Returns the four wheel speeds a mecanum base needs for a body twist.
327///
328/// # Arguments
329///
330/// * `base` - the base.
331/// * `twist` - the body motion, using its forward, sideways, and yaw parts.
332///
333/// # Returns
334///
335/// The four wheel speeds.
336#[no_mangle]
337pub extern "C" fn pamoja_mecanum_wheel_speeds(
338 base: PamojaMecanum,
339 twist: PamojaTwist,
340) -> PamojaWheelSpeeds {
341 Mecanum::new(base.wheelbase, base.track)
342 .wheel_speeds(twist.into())
343 .into()
344}
345
346/// Returns the body twist a mecanum base makes from measured wheel speeds.
347///
348/// # Arguments
349///
350/// * `base` - the base.
351/// * `wheels` - the four measured wheel speeds.
352///
353/// # Returns
354///
355/// The body twist; its yaw rate is 0 for a base with no size.
356#[no_mangle]
357pub extern "C" fn pamoja_mecanum_body_motion(
358 base: PamojaMecanum,
359 wheels: PamojaWheelSpeeds,
360) -> PamojaTwist {
361 Mecanum::new(base.wheelbase, base.track)
362 .body_motion(wheels.into())
363 .into()
364}
365
366/// A planar two-link arm: a shoulder link `l1` long and an elbow link `l2` long.
367#[repr(C)]
368#[derive(Clone, Copy, Debug, PartialEq)]
369pub struct PamojaTwoLinkArm {
370 /// The shoulder link's length; its magnitude is used.
371 pub l1: f32,
372 /// The elbow link's length; its magnitude is used.
373 pub l2: f32,
374}
375
376/// Which way a two-link arm's elbow bends.
377#[repr(C)]
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
379pub enum PamojaElbow {
380 /// The elbow angle is positive, counter-clockwise.
381 Up = 0,
382 /// The elbow angle is negative, clockwise.
383 Down = 1,
384}
385
386/// The closest and farthest a two-link arm's hand reaches from its shoulder.
387#[repr(C)]
388#[derive(Clone, Copy, Debug, PartialEq)]
389pub struct PamojaReach {
390 /// The closest reach, the difference of the link lengths.
391 pub min: f32,
392 /// The farthest reach, the sum of the link lengths.
393 pub max: f32,
394}
395
396/// A point in a plane.
397#[repr(C)]
398#[derive(Clone, Copy, Debug, PartialEq)]
399pub struct PamojaPoint {
400 /// The x coordinate.
401 pub x: f32,
402 /// The y coordinate.
403 pub y: f32,
404}
405
406/// A two-link arm's joint angles, in radians.
407#[repr(C)]
408#[derive(Clone, Copy, Debug, PartialEq)]
409pub struct PamojaJoints {
410 /// The shoulder angle, from the x axis.
411 pub shoulder: f32,
412 /// The elbow angle, relative to the first link.
413 pub elbow: f32,
414}
415
416/// Returns how close and how far a two-link arm reaches.
417///
418/// # Arguments
419///
420/// * `arm` - the arm.
421///
422/// # Returns
423///
424/// The closest and farthest reach.
425#[no_mangle]
426pub extern "C" fn pamoja_two_link_arm_reach(arm: PamojaTwoLinkArm) -> PamojaReach {
427 let (min, max) = TwoLinkArm::new(arm.l1, arm.l2).reach();
428 PamojaReach { min, max }
429}
430
431/// Returns where a two-link arm's hand is for its joint angles.
432///
433/// # Arguments
434///
435/// * `arm` - the arm.
436/// * `shoulder` - the shoulder angle, in radians from the x axis.
437/// * `elbow` - the elbow angle, in radians relative to the first link.
438///
439/// # Returns
440///
441/// The hand's position.
442#[no_mangle]
443pub extern "C" fn pamoja_two_link_arm_tip(
444 arm: PamojaTwoLinkArm,
445 shoulder: f32,
446 elbow: f32,
447) -> PamojaPoint {
448 let (x, y) = TwoLinkArm::new(arm.l1, arm.l2).tip(shoulder, elbow);
449 PamojaPoint { x, y }
450}
451
452/// Finds the joint angles that put a two-link arm's hand at a point.
453///
454/// # Arguments
455///
456/// * `arm` - the arm.
457/// * `x` - the target's x coordinate.
458/// * `y` - the target's y coordinate.
459/// * `elbow` - which way the elbow bends.
460/// * `out_joints` - where to write the angles.
461///
462/// # Returns
463///
464/// `true` with the angles written to `out_joints`, or `false` for a target out of reach,
465/// an arm with a link of no length, or a coordinate that is not a finite number.
466///
467/// # Safety
468///
469/// `out_joints` must point to a writable `PamojaJoints`, or be null.
470#[no_mangle]
471pub unsafe extern "C" fn pamoja_two_link_arm_joints_for(
472 arm: PamojaTwoLinkArm,
473 x: f32,
474 y: f32,
475 elbow: PamojaElbow,
476 out_joints: *mut PamojaJoints,
477) -> bool {
478 let branch = match elbow {
479 PamojaElbow::Up => Elbow::Up,
480 PamojaElbow::Down => Elbow::Down,
481 };
482 let Some((shoulder, elbow)) = TwoLinkArm::new(arm.l1, arm.l2).joints_for(x, y, branch) else {
483 return false;
484 };
485 if let Some(slot) = out_joints.as_mut() {
486 *slot = PamojaJoints { shoulder, elbow };
487 }
488 true
489}
490
491/// One joint of a serial arm in the Denavit-Hartenberg convention.
492#[repr(C)]
493#[derive(Clone, Copy, Debug, PartialEq)]
494pub struct PamojaDhParameters {
495 /// The link length along the common normal, in meters.
496 pub a: f32,
497 /// The link twist about the common normal, in radians.
498 pub alpha: f32,
499 /// The link offset along the previous z axis, in meters.
500 pub d: f32,
501 /// The joint angle about the previous z axis, in radians.
502 pub theta: f32,
503}
504
505impl From<PamojaDhParameters> for DhParameters {
506 fn from(value: PamojaDhParameters) -> Self {
507 DhParameters {
508 a: value.a,
509 alpha: value.alpha,
510 d: value.d,
511 theta: value.theta,
512 }
513 }
514}
515
516/// A 4x4 homogeneous transform, a rotation and a translation, stored row-major.
517#[repr(C)]
518#[derive(Clone, Copy, Debug, PartialEq)]
519pub struct PamojaTransform {
520 /// The sixteen elements, row 0 first; the translation is elements 3, 7, and 11.
521 pub m: [f32; 16],
522}
523
524impl From<Transform> for PamojaTransform {
525 fn from(value: Transform) -> Self {
526 Self { m: value.m }
527 }
528}
529
530/// Returns the transform one Denavit-Hartenberg joint makes.
531///
532/// # Arguments
533///
534/// * `joint` - the joint's four parameters.
535///
536/// # Returns
537///
538/// The joint's homogeneous transform.
539#[no_mangle]
540pub extern "C" fn pamoja_dh_transform(joint: PamojaDhParameters) -> PamojaTransform {
541 DhParameters::from(joint).transform().into()
542}
543
544/// Returns the transform from a serial arm's base to its tool.
545///
546/// # Arguments
547///
548/// * `joints` - the arm's joints, base first.
549/// * `len` - how many joints `joints` holds.
550///
551/// # Returns
552///
553/// The composed transform, or the identity for an arm with no joints or a null `joints`.
554///
555/// # Safety
556///
557/// `joints` must point to `len` readable `PamojaDhParameters`, or be null.
558#[no_mangle]
559pub unsafe extern "C" fn pamoja_forward_kinematics(
560 joints: *const PamojaDhParameters,
561 len: usize,
562) -> PamojaTransform {
563 if joints.is_null() || len == 0 {
564 return Transform::identity().into();
565 }
566 let chain: Vec<DhParameters> = std::slice::from_raw_parts(joints, len)
567 .iter()
568 .map(|&joint| joint.into())
569 .collect();
570 forward_kinematics(&chain).into()
571}
572
573/// Returns the transform that applies `second` and then `first`, their product.
574///
575/// # Arguments
576///
577/// * `first` - the transform on the left of the product, nearer the base.
578/// * `second` - the transform on the right, further down the chain.
579///
580/// # Returns
581///
582/// `first * second`.
583#[no_mangle]
584pub extern "C" fn pamoja_transform_multiply(
585 first: PamojaTransform,
586 second: PamojaTransform,
587) -> PamojaTransform {
588 Transform { m: first.m }
589 .multiply(&Transform { m: second.m })
590 .into()
591}
592
593/// Returns the identity transform: no rotation and no translation.
594///
595/// # Returns
596///
597/// The identity.
598#[no_mangle]
599pub extern "C" fn pamoja_transform_identity() -> PamojaTransform {
600 Transform::identity().into()
601}
602
603/// An opaque handle to a dead-reckoned pose.
604pub struct PamojaOdometry {
605 inner: Odometry,
606}
607
608/// Creates an odometry estimate starting from a known pose.
609///
610/// # Arguments
611///
612/// * `start` - the starting pose; its heading is wrapped into `(-pi, pi]`.
613///
614/// # Returns
615///
616/// A handle the caller must release with [`pamoja_odometry_free`].
617///
618/// # Safety
619///
620/// The returned handle must be freed exactly once.
621#[no_mangle]
622pub unsafe extern "C" fn pamoja_odometry_new(start: PamojaPose) -> *mut PamojaOdometry {
623 Box::into_raw(Box::new(PamojaOdometry {
624 inner: Odometry::new(start.into()),
625 }))
626}
627
628/// Reads an odometry estimate's pose.
629///
630/// # Returns
631///
632/// The pose, or the origin if `odometry` is null.
633///
634/// # Safety
635///
636/// `odometry` must be a live handle from [`pamoja_odometry_new`], or null.
637#[no_mangle]
638pub unsafe extern "C" fn pamoja_odometry_pose(odometry: *const PamojaOdometry) -> PamojaPose {
639 match odometry.as_ref() {
640 Some(odometry) => odometry.inner.pose().into(),
641 None => Pose::origin().into(),
642 }
643}
644
645/// Sets an odometry estimate to a known pose.
646///
647/// # Safety
648///
649/// `odometry` must be a live handle from [`pamoja_odometry_new`], or null.
650#[no_mangle]
651pub unsafe extern "C" fn pamoja_odometry_reset(odometry: *mut PamojaOdometry, pose: PamojaPose) {
652 if let Some(odometry) = odometry.as_mut() {
653 odometry.inner.reset(pose.into());
654 }
655}
656
657/// Integrates a body motion over a time step onto an odometry estimate.
658///
659/// A speed, rate, or time step that is not a finite number is ignored.
660///
661/// # Returns
662///
663/// The new pose, or the origin if `odometry` is null.
664///
665/// # Safety
666///
667/// `odometry` must be a live handle from [`pamoja_odometry_new`], or null.
668#[no_mangle]
669pub unsafe extern "C" fn pamoja_odometry_integrate(
670 odometry: *mut PamojaOdometry,
671 linear: f32,
672 angular: f32,
673 dt: f32,
674) -> PamojaPose {
675 match odometry.as_mut() {
676 Some(odometry) => odometry.inner.integrate(linear, angular, dt).into(),
677 None => Pose::origin().into(),
678 }
679}
680
681/// Integrates the distances two wheels rolled onto an odometry estimate.
682///
683/// A distance that is not a finite number is ignored.
684///
685/// # Returns
686///
687/// The new pose, or the origin if `odometry` is null.
688///
689/// # Safety
690///
691/// `odometry` must be a live handle from [`pamoja_odometry_new`], or null.
692#[no_mangle]
693pub unsafe extern "C" fn pamoja_odometry_integrate_wheels(
694 odometry: *mut PamojaOdometry,
695 left: f32,
696 right: f32,
697 drive: PamojaDiffDrive,
698) -> PamojaPose {
699 match odometry.as_mut() {
700 Some(odometry) => odometry
701 .inner
702 .integrate_wheels(left, right, &DiffDrive::new(drive.track))
703 .into(),
704 None => Pose::origin().into(),
705 }
706}
707
708/// Nudges an odometry estimate's heading toward an absolute measurement.
709///
710/// `weight` is how strongly to trust `measured`, from 0 to 1. A measurement that is not a
711/// finite number, or a weight that is not a number, leaves the heading as it was.
712///
713/// # Safety
714///
715/// `odometry` must be a live handle from [`pamoja_odometry_new`], or null.
716#[no_mangle]
717pub unsafe extern "C" fn pamoja_odometry_fuse_heading(
718 odometry: *mut PamojaOdometry,
719 measured: f32,
720 weight: f32,
721) {
722 if let Some(odometry) = odometry.as_mut() {
723 odometry.inner.fuse_heading(measured, weight);
724 }
725}
726
727/// Releases an odometry handle.
728///
729/// Passing null is a no-op.
730///
731/// # Safety
732///
733/// `odometry` must be a handle from [`pamoja_odometry_new`] that has not already been
734/// freed, or null. After this call it must not be used again.
735#[no_mangle]
736pub unsafe extern "C" fn pamoja_odometry_free(odometry: *mut PamojaOdometry) {
737 if !odometry.is_null() {
738 drop(Box::from_raw(odometry));
739 }
740}
741
742/// Steers toward a waypoint: the speeds and gains of a carrot-following guide.
743#[repr(C)]
744#[derive(Clone, Copy, Debug, PartialEq)]
745pub struct PamojaWaypointFollower {
746 /// The forward speed when pointed at the target; its magnitude is used.
747 pub cruise: f32,
748 /// How close, in meters, counts as arrived; its magnitude is used.
749 pub arrival_m: f64,
750 /// The yaw rate commanded per radian of heading error; its magnitude is used.
751 pub heading_gain: f32,
752 /// The largest yaw rate to command; its magnitude is used.
753 pub max_angular: f32,
754}
755
756/// The command toward a waypoint, with the geometry behind it.
757#[repr(C)]
758#[derive(Clone, Copy, Debug, PartialEq)]
759pub struct PamojaGuidance {
760 /// The body motion to drive.
761 pub twist: PamojaTwist,
762 /// The distance left to the target, in meters.
763 pub distance_m: f64,
764 /// The heading error to the target, in degrees, in `(-180, 180]`.
765 pub heading_error_deg: f32,
766 /// 1 once the target is within the arrival radius, else 0.
767 pub arrived: u8,
768}
769
770/// Produces the command from a robot's position and heading toward a target.
771///
772/// # Arguments
773///
774/// * `follower` - the guide's speeds and gains.
775/// * `here` - the robot's position.
776/// * `heading_deg` - the robot's heading in degrees clockwise from north.
777/// * `target` - the waypoint.
778///
779/// # Returns
780///
781/// The guidance. Within the arrival radius its twist is zero and `arrived` is 1; a heading
782/// or position that is not a finite number also gives a zero twist.
783#[no_mangle]
784pub extern "C" fn pamoja_waypoint_follower_guide(
785 follower: PamojaWaypointFollower,
786 here: PamojaCoordinate,
787 heading_deg: f32,
788 target: PamojaCoordinate,
789) -> PamojaGuidance {
790 let guidance = WaypointFollower::new(
791 follower.cruise,
792 follower.arrival_m,
793 follower.heading_gain,
794 follower.max_angular,
795 )
796 .guide(
797 Coordinate::from(here),
798 heading_deg,
799 Coordinate::from(target),
800 );
801 PamojaGuidance {
802 twist: guidance.twist.into(),
803 distance_m: guidance.distance_m,
804 heading_error_deg: guidance.heading_error_deg,
805 arrived: u8::from(guidance.arrived),
806 }
807}
808
809/// Cuts forward and sideways motion when an obstacle is within the stopping distance.
810///
811/// # Arguments
812///
813/// * `twist` - the requested motion.
814/// * `range_m` - the nearest range ahead, in meters.
815/// * `stop_distance_m` - the range at or below which motion is cut; its magnitude is used.
816///
817/// # Returns
818///
819/// `twist` while the way is clear, or it with no forward or sideways speed, keeping the
820/// turn. A range that is not a number counts as an obstacle.
821#[no_mangle]
822pub extern "C" fn pamoja_obstacle_stop(
823 twist: PamojaTwist,
824 range_m: f32,
825 stop_distance_m: f32,
826) -> PamojaTwist {
827 obstacle_stop(twist.into(), range_m, stop_distance_m).into()
828}
829
830/// An opaque handle to a latching emergency stop.
831pub struct PamojaEStop {
832 inner: EStop,
833}
834
835/// Creates an e-stop that is not engaged.
836///
837/// # Returns
838///
839/// A handle the caller must release with [`pamoja_estop_free`].
840///
841/// # Safety
842///
843/// The returned handle must be freed exactly once.
844#[no_mangle]
845pub unsafe extern "C" fn pamoja_estop_new() -> *mut PamojaEStop {
846 Box::into_raw(Box::new(PamojaEStop {
847 inner: EStop::new(),
848 }))
849}
850
851/// Engages an e-stop; it holds until reset.
852///
853/// # Safety
854///
855/// `estop` must be a live handle from [`pamoja_estop_new`], or null.
856#[no_mangle]
857pub unsafe extern "C" fn pamoja_estop_engage(estop: *mut PamojaEStop) {
858 if let Some(estop) = estop.as_mut() {
859 estop.inner.engage();
860 }
861}
862
863/// Clears an e-stop.
864///
865/// # Safety
866///
867/// `estop` must be a live handle from [`pamoja_estop_new`], or null.
868#[no_mangle]
869pub unsafe extern "C" fn pamoja_estop_reset(estop: *mut PamojaEStop) {
870 if let Some(estop) = estop.as_mut() {
871 estop.inner.reset();
872 }
873}
874
875/// Reports whether an e-stop is engaged.
876///
877/// # Returns
878///
879/// `true` while it is engaged, and also if `estop` is null, so a missing stop reads as a
880/// stop.
881///
882/// # Safety
883///
884/// `estop` must be a live handle from [`pamoja_estop_new`], or null.
885#[no_mangle]
886pub unsafe extern "C" fn pamoja_estop_is_engaged(estop: *const PamojaEStop) -> bool {
887 match estop.as_ref() {
888 Some(estop) => estop.inner.is_engaged(),
889 None => true,
890 }
891}
892
893/// Passes a command through an e-stop.
894///
895/// # Returns
896///
897/// `desired` while the stop is clear, or a zero twist while it is engaged or if `estop` is
898/// null.
899///
900/// # Safety
901///
902/// `estop` must be a live handle from [`pamoja_estop_new`], or null.
903#[no_mangle]
904pub unsafe extern "C" fn pamoja_estop_gate(
905 estop: *const PamojaEStop,
906 desired: PamojaTwist,
907) -> PamojaTwist {
908 match estop.as_ref() {
909 Some(estop) => estop.inner.gate(desired.into()).into(),
910 None => Twist::zero().into(),
911 }
912}
913
914/// Releases an e-stop handle.
915///
916/// Passing null is a no-op.
917///
918/// # Safety
919///
920/// `estop` must be a handle from [`pamoja_estop_new`] that has not already been freed, or
921/// null. After this call it must not be used again.
922#[no_mangle]
923pub unsafe extern "C" fn pamoja_estop_free(estop: *mut PamojaEStop) {
924 if !estop.is_null() {
925 drop(Box::from_raw(estop));
926 }
927}
928
929/// An opaque handle to a deadman timer.
930pub struct PamojaWatchdog {
931 inner: Watchdog,
932}
933
934/// Creates a watchdog that expires after `timeout` without being fed.
935///
936/// A timeout that is not a number is taken as 0, so the watchdog expires rather than never.
937///
938/// # Returns
939///
940/// A freshly fed watchdog; the caller must release it with [`pamoja_watchdog_free`].
941///
942/// # Safety
943///
944/// The returned handle must be freed exactly once.
945#[no_mangle]
946pub unsafe extern "C" fn pamoja_watchdog_new(timeout: f32) -> *mut PamojaWatchdog {
947 Box::into_raw(Box::new(PamojaWatchdog {
948 inner: Watchdog::new(timeout),
949 }))
950}
951
952/// Feeds a watchdog, restarting its silence timer.
953///
954/// # Safety
955///
956/// `watchdog` must be a live handle from [`pamoja_watchdog_new`], or null.
957#[no_mangle]
958pub unsafe extern "C" fn pamoja_watchdog_feed(watchdog: *mut PamojaWatchdog) {
959 if let Some(watchdog) = watchdog.as_mut() {
960 watchdog.inner.feed();
961 }
962}
963
964/// Advances a watchdog's timer and reports whether it has expired.
965///
966/// A time step that is not a finite number expires the watchdog until it is fed.
967///
968/// # Returns
969///
970/// `true` if it has expired, and also if `watchdog` is null.
971///
972/// # Safety
973///
974/// `watchdog` must be a live handle from [`pamoja_watchdog_new`], or null.
975#[no_mangle]
976pub unsafe extern "C" fn pamoja_watchdog_update(watchdog: *mut PamojaWatchdog, dt: f32) -> bool {
977 match watchdog.as_mut() {
978 Some(watchdog) => watchdog.inner.update(dt),
979 None => true,
980 }
981}
982
983/// Reports whether a watchdog has expired.
984///
985/// # Returns
986///
987/// `true` if the time since feeding is past the timeout, and also if `watchdog` is null.
988///
989/// # Safety
990///
991/// `watchdog` must be a live handle from [`pamoja_watchdog_new`], or null.
992#[no_mangle]
993pub unsafe extern "C" fn pamoja_watchdog_is_expired(watchdog: *const PamojaWatchdog) -> bool {
994 match watchdog.as_ref() {
995 Some(watchdog) => watchdog.inner.is_expired(),
996 None => true,
997 }
998}
999
1000/// Releases a watchdog handle.
1001///
1002/// Passing null is a no-op.
1003///
1004/// # Safety
1005///
1006/// `watchdog` must be a handle from [`pamoja_watchdog_new`] that has not already been
1007/// freed, or null. After this call it must not be used again.
1008#[no_mangle]
1009pub unsafe extern "C" fn pamoja_watchdog_free(watchdog: *mut PamojaWatchdog) {
1010 if !watchdog.is_null() {
1011 drop(Box::from_raw(watchdog));
1012 }
1013}
1014
1015/// An opaque handle to speed and acceleration limits, with the motion they last allowed.
1016pub struct PamojaLimits {
1017 inner: Limits,
1018}
1019
1020/// Creates limits on speed and acceleration, starting from rest.
1021///
1022/// Each ceiling's magnitude is used, and one that is not a number is taken as 0, which holds
1023/// the robot still.
1024///
1025/// # Arguments
1026///
1027/// * `max_linear` - the largest planar speed.
1028/// * `max_angular` - the largest yaw rate.
1029/// * `max_linear_accel` - the largest change in speed per second.
1030/// * `max_angular_accel` - the largest change in yaw rate per second.
1031///
1032/// # Returns
1033///
1034/// A handle the caller must release with [`pamoja_limits_free`].
1035///
1036/// # Safety
1037///
1038/// The returned handle must be freed exactly once.
1039#[no_mangle]
1040pub unsafe extern "C" fn pamoja_limits_new(
1041 max_linear: f32,
1042 max_angular: f32,
1043 max_linear_accel: f32,
1044 max_angular_accel: f32,
1045) -> *mut PamojaLimits {
1046 Box::into_raw(Box::new(PamojaLimits {
1047 inner: Limits::new(max_linear, max_angular, max_linear_accel, max_angular_accel),
1048 }))
1049}
1050
1051/// Bounds a command in speed and acceleration.
1052///
1053/// A command part that is not a finite number is taken as 0, and a time step that is not
1054/// finite allows no change.
1055///
1056/// # Returns
1057///
1058/// The bounded command, or a zero twist if `limits` is null.
1059///
1060/// # Safety
1061///
1062/// `limits` must be a live handle from [`pamoja_limits_new`], or null.
1063#[no_mangle]
1064pub unsafe extern "C" fn pamoja_limits_apply(
1065 limits: *mut PamojaLimits,
1066 desired: PamojaTwist,
1067 dt: f32,
1068) -> PamojaTwist {
1069 match limits.as_mut() {
1070 Some(limits) => limits.inner.apply(desired.into(), dt).into(),
1071 None => Twist::zero().into(),
1072 }
1073}
1074
1075/// Forgets the motion limits last allowed, so the next command eases up from rest.
1076///
1077/// # Safety
1078///
1079/// `limits` must be a live handle from [`pamoja_limits_new`], or null.
1080#[no_mangle]
1081pub unsafe extern "C" fn pamoja_limits_reset(limits: *mut PamojaLimits) {
1082 if let Some(limits) = limits.as_mut() {
1083 limits.inner.reset();
1084 }
1085}
1086
1087/// Releases a limits handle.
1088///
1089/// Passing null is a no-op.
1090///
1091/// # Safety
1092///
1093/// `limits` must be a handle from [`pamoja_limits_new`] that has not already been freed,
1094/// or null. After this call it must not be used again.
1095#[no_mangle]
1096pub unsafe extern "C" fn pamoja_limits_free(limits: *mut PamojaLimits) {
1097 if !limits.is_null() {
1098 drop(Box::from_raw(limits));
1099 }
1100}
1101
1102/// An opaque handle to the gate every motion command passes through: an e-stop, a
1103/// watchdog, and limits.
1104pub struct PamojaSafetyGate {
1105 inner: SafetyGate,
1106}
1107
1108/// Creates a safety gate from limits and a watchdog timeout.
1109///
1110/// # Arguments
1111///
1112/// * `limits` - the limits for normal motion, copied as they stand; the handle stays the
1113/// caller's.
1114/// * `watchdog_timeout` - the silence after which the gate stops the robot.
1115///
1116/// # Returns
1117///
1118/// A cleared, freshly fed gate the caller must release with [`pamoja_safety_gate_free`],
1119/// or null if `limits` is null.
1120///
1121/// # Safety
1122///
1123/// `limits` must be a live handle from [`pamoja_limits_new`], or null. A returned handle
1124/// must be freed exactly once.
1125#[no_mangle]
1126pub unsafe extern "C" fn pamoja_safety_gate_new(
1127 limits: *const PamojaLimits,
1128 watchdog_timeout: f32,
1129) -> *mut PamojaSafetyGate {
1130 let Some(limits) = limits.as_ref() else {
1131 return ptr::null_mut();
1132 };
1133 Box::into_raw(Box::new(PamojaSafetyGate {
1134 inner: SafetyGate::new(limits.inner, watchdog_timeout),
1135 }))
1136}
1137
1138/// Feeds a safety gate's watchdog; call it whenever a fresh command arrives.
1139///
1140/// # Safety
1141///
1142/// `gate` must be a live handle from [`pamoja_safety_gate_new`], or null.
1143#[no_mangle]
1144pub unsafe extern "C" fn pamoja_safety_gate_feed(gate: *mut PamojaSafetyGate) {
1145 if let Some(gate) = gate.as_mut() {
1146 gate.inner.feed();
1147 }
1148}
1149
1150/// Engages a safety gate's latching e-stop.
1151///
1152/// # Safety
1153///
1154/// `gate` must be a live handle from [`pamoja_safety_gate_new`], or null.
1155#[no_mangle]
1156pub unsafe extern "C" fn pamoja_safety_gate_engage_estop(gate: *mut PamojaSafetyGate) {
1157 if let Some(gate) = gate.as_mut() {
1158 gate.inner.engage_estop();
1159 }
1160}
1161
1162/// Clears a safety gate's e-stop.
1163///
1164/// # Safety
1165///
1166/// `gate` must be a live handle from [`pamoja_safety_gate_new`], or null.
1167#[no_mangle]
1168pub unsafe extern "C" fn pamoja_safety_gate_reset_estop(gate: *mut PamojaSafetyGate) {
1169 if let Some(gate) = gate.as_mut() {
1170 gate.inner.reset_estop();
1171 }
1172}
1173
1174/// Reports whether a safety gate is forcing a stop.
1175///
1176/// # Returns
1177///
1178/// `true` while its e-stop is engaged or its watchdog has expired, and also if `gate` is
1179/// null.
1180///
1181/// # Safety
1182///
1183/// `gate` must be a live handle from [`pamoja_safety_gate_new`], or null.
1184#[no_mangle]
1185pub unsafe extern "C" fn pamoja_safety_gate_is_stopped(gate: *const PamojaSafetyGate) -> bool {
1186 match gate.as_ref() {
1187 Some(gate) => gate.inner.is_stopped(),
1188 None => true,
1189 }
1190}
1191
1192/// Returns the command that is safe to drive for a desired one over a time step.
1193///
1194/// # Returns
1195///
1196/// A zero twist while the gate is stopped or if `gate` is null, and otherwise `desired`
1197/// bounded by the limits.
1198///
1199/// # Safety
1200///
1201/// `gate` must be a live handle from [`pamoja_safety_gate_new`], or null.
1202#[no_mangle]
1203pub unsafe extern "C" fn pamoja_safety_gate_command(
1204 gate: *mut PamojaSafetyGate,
1205 desired: PamojaTwist,
1206 dt: f32,
1207) -> PamojaTwist {
1208 match gate.as_mut() {
1209 Some(gate) => gate.inner.command(desired.into(), dt).into(),
1210 None => Twist::zero().into(),
1211 }
1212}
1213
1214/// Releases a safety gate handle.
1215///
1216/// Passing null is a no-op.
1217///
1218/// # Safety
1219///
1220/// `gate` must be a handle from [`pamoja_safety_gate_new`] that has not already been freed,
1221/// or null. After this call it must not be used again.
1222#[no_mangle]
1223pub unsafe extern "C" fn pamoja_safety_gate_free(gate: *mut PamojaSafetyGate) {
1224 if !gate.is_null() {
1225 drop(Box::from_raw(gate));
1226 }
1227}
1228
1229/// A hobby servo's pulse range and travel.
1230#[repr(C)]
1231#[derive(Clone, Copy, Debug, PartialEq)]
1232pub struct PamojaServoMap {
1233 /// The pulse width at zero degrees, in microseconds.
1234 pub min_us: u16,
1235 /// The pulse width at full travel, in microseconds.
1236 pub max_us: u16,
1237 /// The full travel in degrees; its magnitude is used.
1238 pub range_deg: f32,
1239}
1240
1241/// Returns the standard hobby servo: 1000 to 2000 microseconds over 180 degrees.
1242///
1243/// # Returns
1244///
1245/// The standard servo.
1246#[no_mangle]
1247pub extern "C" fn pamoja_servo_map_standard() -> PamojaServoMap {
1248 let standard = ServoMap::standard();
1249 PamojaServoMap {
1250 min_us: standard.min_us(),
1251 max_us: standard.max_us(),
1252 range_deg: standard.range_deg(),
1253 }
1254}
1255
1256/// Returns the pulse width that sets a servo to an angle.
1257///
1258/// # Arguments
1259///
1260/// * `servo` - the servo.
1261/// * `angle_deg` - the angle in degrees, held to the servo's travel.
1262///
1263/// # Returns
1264///
1265/// The pulse width in microseconds, or 0, no pulse, for an angle that is not a number.
1266#[no_mangle]
1267pub extern "C" fn pamoja_servo_map_pulse(servo: PamojaServoMap, angle_deg: f32) -> u16 {
1268 ServoMap::new(servo.min_us, servo.max_us, servo.range_deg).pulse(angle_deg)
1269}
1270
1271/// Returns the angle a servo pulse width sets.
1272///
1273/// # Arguments
1274///
1275/// * `servo` - the servo.
1276/// * `pulse_us` - the pulse width in microseconds, held to the servo's range.
1277///
1278/// # Returns
1279///
1280/// The angle in degrees, or 0 for a servo whose pulse range is empty.
1281#[no_mangle]
1282pub extern "C" fn pamoja_servo_map_angle(servo: PamojaServoMap, pulse_us: u16) -> f32 {
1283 ServoMap::new(servo.min_us, servo.max_us, servo.range_deg).angle(pulse_us)
1284}
1285
1286/// An electronic speed controller's reverse, neutral, and forward pulse widths.
1287#[repr(C)]
1288#[derive(Clone, Copy, Debug, PartialEq)]
1289pub struct PamojaEsc {
1290 /// The pulse width at full reverse, in microseconds.
1291 pub min_us: u16,
1292 /// The pulse width at rest, in microseconds.
1293 pub neutral_us: u16,
1294 /// The pulse width at full forward, in microseconds.
1295 pub max_us: u16,
1296}
1297
1298/// Returns the common reversible ESC: 1000, 1500, and 2000 microseconds.
1299///
1300/// # Returns
1301///
1302/// The reversible ESC.
1303#[no_mangle]
1304pub extern "C" fn pamoja_esc_bidirectional() -> PamojaEsc {
1305 let reversible = Esc::bidirectional();
1306 PamojaEsc {
1307 min_us: reversible.min_us(),
1308 neutral_us: reversible.neutral_us(),
1309 max_us: reversible.max_us(),
1310 }
1311}
1312
1313/// Returns the pulse width for a throttle.
1314///
1315/// # Arguments
1316///
1317/// * `esc` - the controller.
1318/// * `throttle` - the demand from -1, full reverse, to 1, full forward, held to that range.
1319///
1320/// # Returns
1321///
1322/// The pulse width in microseconds, or the neutral one for a throttle that is not a number.
1323#[no_mangle]
1324pub extern "C" fn pamoja_esc_pulse(esc: PamojaEsc, throttle: f32) -> u16 {
1325 Esc::new(esc.min_us, esc.neutral_us, esc.max_us).pulse(throttle)
1326}
1327
1328/// An opaque handle to a quadrature encoder decoder.
1329pub struct PamojaQuadrature {
1330 inner: Quadrature,
1331}
1332
1333/// Creates a quadrature decoder seeded with the encoder's current channel levels.
1334///
1335/// # Arguments
1336///
1337/// * `a` - the A channel's level now.
1338/// * `b` - the B channel's level now.
1339///
1340/// # Returns
1341///
1342/// A decoder with a count of 0 the caller must release with [`pamoja_quadrature_free`].
1343///
1344/// # Safety
1345///
1346/// The returned handle must be freed exactly once.
1347#[no_mangle]
1348pub unsafe extern "C" fn pamoja_quadrature_new(a: bool, b: bool) -> *mut PamojaQuadrature {
1349 Box::into_raw(Box::new(PamojaQuadrature {
1350 inner: Quadrature::starting(a, b),
1351 }))
1352}
1353
1354/// Feeds a quadrature decoder the channel levels it reads now.
1355///
1356/// # Returns
1357///
1358/// 1 or -1 for a step in either direction, and 0 for no change, a jump past a step, or a
1359/// null `quadrature`.
1360///
1361/// # Safety
1362///
1363/// `quadrature` must be a live handle from [`pamoja_quadrature_new`], or null.
1364#[no_mangle]
1365pub unsafe extern "C" fn pamoja_quadrature_update(
1366 quadrature: *mut PamojaQuadrature,
1367 a: bool,
1368 b: bool,
1369) -> i8 {
1370 match quadrature.as_mut() {
1371 Some(quadrature) => quadrature.inner.update(a, b),
1372 None => 0,
1373 }
1374}
1375
1376/// Reads a quadrature decoder's signed count of steps.
1377///
1378/// # Returns
1379///
1380/// The count, or 0 if `quadrature` is null.
1381///
1382/// # Safety
1383///
1384/// `quadrature` must be a live handle from [`pamoja_quadrature_new`], or null.
1385#[no_mangle]
1386pub unsafe extern "C" fn pamoja_quadrature_count(quadrature: *const PamojaQuadrature) -> i64 {
1387 match quadrature.as_ref() {
1388 Some(quadrature) => quadrature.inner.count(),
1389 None => 0,
1390 }
1391}
1392
1393/// Sets a quadrature decoder's count back to 0, keeping the channel state it last read.
1394///
1395/// # Safety
1396///
1397/// `quadrature` must be a live handle from [`pamoja_quadrature_new`], or null.
1398#[no_mangle]
1399pub unsafe extern "C" fn pamoja_quadrature_reset(quadrature: *mut PamojaQuadrature) {
1400 if let Some(quadrature) = quadrature.as_mut() {
1401 quadrature.inner.reset();
1402 }
1403}
1404
1405/// Releases a quadrature decoder handle.
1406///
1407/// Passing null is a no-op.
1408///
1409/// # Safety
1410///
1411/// `quadrature` must be a handle from [`pamoja_quadrature_new`] that has not already been
1412/// freed, or null. After this call it must not be used again.
1413#[no_mangle]
1414pub unsafe extern "C" fn pamoja_quadrature_free(quadrature: *mut PamojaQuadrature) {
1415 if !quadrature.is_null() {
1416 drop(Box::from_raw(quadrature));
1417 }
1418}
1419
1420/// An encoder's resolution and the radius of the wheel it turns with.
1421#[repr(C)]
1422#[derive(Clone, Copy, Debug, PartialEq)]
1423pub struct PamojaQuadratureScale {
1424 /// Steps per wheel revolution; its magnitude is used.
1425 pub counts_per_rev: f32,
1426 /// The wheel radius in meters; its magnitude is used.
1427 pub wheel_radius: f32,
1428}
1429
1430/// Returns the distance a wheel rolled for a step count.
1431///
1432/// # Returns
1433///
1434/// The distance in meters, or 0 for a scale with no resolution.
1435#[no_mangle]
1436pub extern "C" fn pamoja_quadrature_scale_distance(
1437 scale: PamojaQuadratureScale,
1438 count: i64,
1439) -> f32 {
1440 QuadratureScale::new(scale.counts_per_rev, scale.wheel_radius).distance(count)
1441}
1442
1443/// Returns a wheel's speed from the steps counted over a time step.
1444///
1445/// # Returns
1446///
1447/// The speed in meters per second, or 0 when `dt` is 0.
1448#[no_mangle]
1449pub extern "C" fn pamoja_quadrature_scale_velocity(
1450 scale: PamojaQuadratureScale,
1451 delta_count: i64,
1452 dt: f32,
1453) -> f32 {
1454 QuadratureScale::new(scale.counts_per_rev, scale.wheel_radius).velocity(delta_count, dt)
1455}
1456
1457#[cfg(test)]
1458mod tests {
1459 use super::*;
1460
1461 #[test]
1462 fn a_differential_drive_turns_both_ways() {
1463 let drive = PamojaDiffDrive { track: 0.5 };
1464 let spin = pamoja_diff_drive_wheel_speeds(drive, 0.0, 2.0);
1465 assert_eq!((spin.left, spin.right), (-0.5, 0.5));
1466 let body = pamoja_diff_drive_body_motion(drive, -0.5, 0.5);
1467 assert_eq!((body.linear, body.angular), (0.0, 2.0));
1468 }
1469
1470 #[test]
1471 fn a_car_goes_straight_with_its_wheels_straight() {
1472 let car = PamojaAckermann { wheelbase: 2.5 };
1473 assert_eq!(pamoja_ackermann_turn_radius(car, 0.0), f32::INFINITY);
1474 let omega = pamoja_ackermann_yaw_rate(car, 5.0, 0.4);
1475 assert!((pamoja_ackermann_steering_angle(car, 5.0, omega) - 0.4).abs() < 1e-5);
1476 }
1477
1478 #[test]
1479 fn a_mecanum_base_round_trips_a_twist() {
1480 let base = PamojaMecanum {
1481 wheelbase: 0.5,
1482 track: 0.4,
1483 };
1484 let twist = PamojaTwist {
1485 vx: 0.8,
1486 vy: -0.3,
1487 omega: 0.6,
1488 };
1489 let back = pamoja_mecanum_body_motion(base, pamoja_mecanum_wheel_speeds(base, twist));
1490 assert!((back.vx - 0.8).abs() < 1e-6 && (back.vy + 0.3).abs() < 1e-6);
1491 assert!((back.omega - 0.6).abs() < 1e-6);
1492 }
1493
1494 #[test]
1495 fn an_arm_finds_the_joints_for_where_its_hand_went() {
1496 let arm = PamojaTwoLinkArm { l1: 1.0, l2: 1.0 };
1497 let tip = pamoja_two_link_arm_tip(arm, 0.5, 0.7);
1498 let mut joints = PamojaJoints {
1499 shoulder: 0.0,
1500 elbow: 0.0,
1501 };
1502 // Safety: the out-pointer is a local.
1503 unsafe {
1504 assert!(pamoja_two_link_arm_joints_for(
1505 arm,
1506 tip.x,
1507 tip.y,
1508 PamojaElbow::Up,
1509 &mut joints
1510 ));
1511 assert!(!pamoja_two_link_arm_joints_for(
1512 arm,
1513 5.0,
1514 0.0,
1515 PamojaElbow::Up,
1516 &mut joints
1517 ));
1518 }
1519 assert!((joints.shoulder - 0.5).abs() < 1e-4 && (joints.elbow - 0.7).abs() < 1e-4);
1520 assert_eq!(
1521 pamoja_two_link_arm_reach(arm),
1522 PamojaReach { min: 0.0, max: 2.0 }
1523 );
1524 }
1525
1526 #[test]
1527 fn forward_kinematics_reaches_out_along_a_flat_arm() {
1528 let link = PamojaDhParameters {
1529 a: 1.0,
1530 alpha: 0.0,
1531 d: 0.0,
1532 theta: 0.0,
1533 };
1534 // Safety: the slice outlives the call, and null is handled.
1535 unsafe {
1536 let tool = pamoja_forward_kinematics([link, link].as_ptr(), 2);
1537 assert!((tool.m[3] - 2.0).abs() < 1e-6 && tool.m[7].abs() < 1e-6);
1538 assert_eq!(
1539 pamoja_forward_kinematics(ptr::null(), 3),
1540 pamoja_transform_identity()
1541 );
1542 }
1543 }
1544
1545 #[test]
1546 fn odometry_follows_a_quarter_circle() {
1547 // Safety: the handle is live for the whole test and freed once.
1548 unsafe {
1549 let odometry = pamoja_odometry_new(PamojaPose {
1550 x: 0.0,
1551 y: 0.0,
1552 theta: 0.0,
1553 });
1554 let pose = pamoja_odometry_integrate(odometry, 1.0, 1.0, core::f32::consts::FRAC_PI_2);
1555 assert!((pose.x - 1.0).abs() < 1e-5 && (pose.y - 1.0).abs() < 1e-5);
1556 assert_eq!(
1557 pamoja_odometry_integrate(odometry, f32::NAN, 1.0, 1.0),
1558 pose
1559 );
1560 pamoja_odometry_free(odometry);
1561 assert_eq!(
1562 pamoja_odometry_pose(ptr::null()),
1563 PamojaPose {
1564 x: 0.0,
1565 y: 0.0,
1566 theta: 0.0
1567 }
1568 );
1569 }
1570 }
1571
1572 #[test]
1573 fn a_follower_arrives_and_an_obstacle_stops_the_robot() {
1574 let follower = PamojaWaypointFollower {
1575 cruise: 1.5,
1576 arrival_m: 3.0,
1577 heading_gain: 1.5,
1578 max_angular: 1.0,
1579 };
1580 let here = PamojaCoordinate {
1581 latitude: 0.0,
1582 longitude: 0.0,
1583 };
1584 let east = PamojaCoordinate {
1585 latitude: 0.0,
1586 longitude: 0.01,
1587 };
1588 let guidance = pamoja_waypoint_follower_guide(follower, here, 90.0, east);
1589 assert_eq!(guidance.arrived, 0);
1590 assert!((guidance.twist.vx - 1.5).abs() < 1e-3);
1591 assert_eq!(
1592 pamoja_waypoint_follower_guide(follower, here, 90.0, here).arrived,
1593 1
1594 );
1595
1596 let driving = PamojaTwist {
1597 vx: 1.0,
1598 vy: 0.0,
1599 omega: 0.5,
1600 };
1601 let stopped = pamoja_obstacle_stop(driving, f32::NAN, 0.5);
1602 assert_eq!((stopped.vx, stopped.omega), (0.0, 0.5));
1603 }
1604
1605 #[test]
1606 fn a_safety_gate_eases_on_and_stops_on_its_estop() {
1607 // Safety: every handle is live until freed once, and null is handled.
1608 unsafe {
1609 let limits = pamoja_limits_new(1.0, 2.0, 0.5, 4.0);
1610 let gate = pamoja_safety_gate_new(limits, 0.2);
1611 pamoja_limits_free(limits);
1612 let ahead = PamojaTwist {
1613 vx: 1.0,
1614 vy: 0.0,
1615 omega: 0.0,
1616 };
1617 pamoja_safety_gate_feed(gate);
1618 assert!((pamoja_safety_gate_command(gate, ahead, 0.1).vx - 0.05).abs() < 1e-6);
1619 pamoja_safety_gate_engage_estop(gate);
1620 assert!(pamoja_safety_gate_is_stopped(gate));
1621 assert_eq!(pamoja_safety_gate_command(gate, ahead, 0.1).vx, 0.0);
1622 pamoja_safety_gate_free(gate);
1623 assert!(pamoja_safety_gate_new(ptr::null(), 0.2).is_null());
1624 assert!(pamoja_safety_gate_is_stopped(ptr::null()));
1625
1626 let dog = pamoja_watchdog_new(0.5);
1627 assert!(!pamoja_watchdog_update(dog, 0.3));
1628 assert!(pamoja_watchdog_update(dog, f32::NAN));
1629 pamoja_watchdog_feed(dog);
1630 assert!(!pamoja_watchdog_is_expired(dog));
1631 pamoja_watchdog_free(dog);
1632
1633 let estop = pamoja_estop_new();
1634 pamoja_estop_engage(estop);
1635 assert_eq!(pamoja_estop_gate(estop, ahead).vx, 0.0);
1636 pamoja_estop_reset(estop);
1637 assert_eq!(pamoja_estop_gate(estop, ahead), ahead);
1638 pamoja_estop_free(estop);
1639 }
1640 }
1641
1642 #[test]
1643 fn servo_esc_and_encoder_convert_to_their_units() {
1644 let servo = pamoja_servo_map_standard();
1645 assert_eq!(pamoja_servo_map_pulse(servo, 90.0), 1500);
1646 assert_eq!(pamoja_servo_map_pulse(servo, f32::NAN), 0);
1647 assert!((pamoja_servo_map_angle(servo, 1500) - 90.0).abs() < 1e-3);
1648 assert_eq!(pamoja_esc_pulse(pamoja_esc_bidirectional(), 0.5), 1750);
1649
1650 // Safety: the handle is live for the block and freed once.
1651 unsafe {
1652 let encoder = pamoja_quadrature_new(false, false);
1653 for (a, b) in [(false, true), (true, true), (true, false), (false, false)] {
1654 assert_eq!(pamoja_quadrature_update(encoder, a, b), 1);
1655 }
1656 assert_eq!(pamoja_quadrature_count(encoder), 4);
1657 pamoja_quadrature_reset(encoder);
1658 assert_eq!(pamoja_quadrature_count(encoder), 0);
1659 pamoja_quadrature_free(encoder);
1660 }
1661 let scale = PamojaQuadratureScale {
1662 counts_per_rev: 360.0,
1663 wheel_radius: 0.05,
1664 };
1665 let turn = pamoja_quadrature_scale_distance(scale, 360);
1666 assert!((turn - 2.0 * core::f32::consts::PI * 0.05).abs() < 1e-6);
1667 assert_eq!(pamoja_quadrature_scale_velocity(scale, 10, 0.0), 0.0);
1668 }
1669}