Skip to main content

pamoja_kit/
imu.rs

1//! Tilt from a three-axis accelerometer.
2
3use core::f64::consts::PI;
4
5use libm::{atan2, sqrt};
6
7fn to_degrees(radians: f64) -> f64 {
8    radians * (180.0 / PI)
9}
10
11/// Roll and pitch angles, in degrees.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Tilt {
14    /// Rotation about the forward (x) axis, in degrees, in `[-180.0, 180.0]`.
15    pub roll: f64,
16    /// Rotation about the right (y) axis, in degrees, in `[-90.0, 90.0]`.
17    pub pitch: f64,
18}
19
20/// Computes roll and pitch from a three-axis accelerometer reading.
21///
22/// At rest, gravity tells a three-axis accelerometer which way is down, which fixes the
23/// board's tilt. This uses the standard formula (Freescale AN3461): roll is `atan2(ay, az)`
24/// and pitch is `atan2(-ax, sqrt(ay^2 + az^2))`, with `atan2` placing each angle in the
25/// correct quadrant. The reading's units do not matter (raw counts or g), because only the
26/// ratios between axes set the angle and any common scale cancels. It holds while the board
27/// is still or moving gently, since it assumes the only acceleration is gravity. Yaw
28/// (heading) cannot be found from an accelerometer alone; that needs a magnetometer.
29///
30/// # Arguments
31///
32/// * `ax` - acceleration along the x (forward) axis.
33/// * `ay` - acceleration along the y (right) axis.
34/// * `az` - acceleration along the z (up) axis.
35///
36/// # Returns
37///
38/// The [`Tilt`] in degrees.
39///
40/// # Examples
41///
42/// ```
43/// use pamoja_kit::imu::tilt_from_accel;
44///
45/// // Board level, 1 g straight down on z: no tilt.
46/// let level = tilt_from_accel(0.0, 0.0, 1.0);
47/// assert!(level.roll.abs() < 1e-6 && level.pitch.abs() < 1e-6);
48///
49/// // Tipped fully onto its y axis: 90 degrees of roll.
50/// let rolled = tilt_from_accel(0.0, 1.0, 0.0);
51/// assert!((rolled.roll - 90.0).abs() < 1e-6);
52/// ```
53pub fn tilt_from_accel(ax: f64, ay: f64, az: f64) -> Tilt {
54    let roll = to_degrees(atan2(ay, az));
55    let pitch = to_degrees(atan2(-ax, sqrt(ay * ay + az * az)));
56    Tilt { roll, pitch }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn a_level_board_has_no_tilt() {
65        let tilt = tilt_from_accel(0.0, 0.0, 1.0);
66        assert!(tilt.roll.abs() < 1e-9);
67        assert!(tilt.pitch.abs() < 1e-9);
68    }
69
70    #[test]
71    fn rolled_onto_the_y_axis_is_ninety_degrees_of_roll() {
72        let tilt = tilt_from_accel(0.0, 1.0, 0.0);
73        assert!((tilt.roll - 90.0).abs() < 1e-9);
74        assert!(tilt.pitch.abs() < 1e-9);
75    }
76
77    #[test]
78    fn pitched_onto_the_x_axis_is_minus_ninety_pitch() {
79        let tilt = tilt_from_accel(1.0, 0.0, 0.0);
80        assert!((tilt.pitch + 90.0).abs() < 1e-9);
81    }
82
83    #[test]
84    fn equal_y_and_z_is_forty_five_degrees_of_roll() {
85        let tilt = tilt_from_accel(0.0, 1.0, 1.0);
86        assert!((tilt.roll - 45.0).abs() < 1e-9);
87    }
88
89    #[test]
90    fn the_scale_of_the_reading_does_not_change_the_angle() {
91        let g = tilt_from_accel(0.0, 1.0, 1.0);
92        let counts = tilt_from_accel(0.0, 1000.0, 1000.0);
93        assert!((g.roll - counts.roll).abs() < 1e-9);
94    }
95}