rust_gnc/control/axis.rs
1//! # Axis Processing
2//!
3//! This module implements the `AxisProcessor`, which encapsulates the full
4//! control pipeline for a single degree of freedom (e.g., Roll, Pitch, or Yaw).
5//!
6//! By combining a `Filter` and a `PidController`, the `AxisProcessor` transforms
7//! raw inertial data into a normalized control signal.
8
9use crate::filters::{Filter, InertialInput};
10use crate::control::pid::PidController;
11use crate::units::Radians;
12
13/// A generic controller for a single physical axis.
14///
15/// The `AxisProcessor` is generic over any `Filter` that accepts `InertialInput`
16/// and produces `Radians`. This allows the same control logic to be used with
17/// Complementary filters, Kalman filters, or simple low-pass filters.
18pub struct AxisProcessor<F>
19where
20 F: Filter<Input = InertialInput, Output = Radians>
21{
22 /// The PID algorithm responsible for generating the corrective signal.
23 pub pid: PidController,
24 /// The sensor fusion filter responsible for estimating the current orientation.
25 pub filter: F,
26}
27
28impl<F> AxisProcessor<F>
29where
30 F: Filter<Input = InertialInput, Output = Radians>
31{
32 /// Creates a new `AxisProcessor` with the provided estimation and control components.
33 pub fn new(filter: F, pid: PidController) -> Self {
34 Self { pid, filter }
35 }
36
37 /// Executes one iteration of the control loop for this axis.
38 ///
39 /// This method performs two primary steps:
40 /// 1. **State Estimation**: Updates the internal filter with new IMU data.
41 /// 2. **Control Generation**: Computes the PID output based on the error
42 /// between the `target_angle` and the estimated `current_angle`.
43 ///
44 /// ### Parameters
45 /// * `input` - Raw rate and reference data from sensors.
46 /// * `target_angle` - The desired orientation for this axis.
47 /// * `dt` - Time delta since the last update in seconds.
48 ///
49 /// ### Returns
50 /// A normalized control signal (f32), typically in the range [-1.0, 1.0].
51 pub fn process(&mut self, input: InertialInput, target_angle: Radians, dt: f32) -> f32 {
52 // Step 1: Perception (Where are we?)
53 let current_angle = self.filter.update(input, dt);
54
55 // Step 2: Control (How do we get to where we want to be?)
56 self.pid.update(target_angle.0, current_angle.0, dt)
57 }
58}