Skip to main content

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}