refactor: enforce layered crates and Rust audit gates

This commit is contained in:
Horldsence
2026-09-24 13:00:03 +08:00
parent af151525d1
commit 8a582fc102
67 changed files with 3918 additions and 581 deletions
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "pid"
description = "PID controller"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
[dev-dependencies]
approx = { workspace = true }
[package.metadata.sentry-chassis]
layer = "control/algorithm"
+272
View File
@@ -0,0 +1,272 @@
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
//! 通用 PID 控制器。
/// 并联式 PID 控制器(f32,显式时间步)。
///
/// 特性:
/// - 输出限幅 `[out_min, out_max]`;
/// - 积分抗饱和(clamping:输出已饱和且误差继续同向时暂停积分);
/// - 微分作用于**测量值**(避免设定值阶跃的微分冲击),可选一阶低通滤波。
///
/// 时间步 `dt` 由调用方显式传入(秒)。`dt <= 0` 或非有限值时本次调用
/// 直接返回上次输出且不更新内部状态(防御跳帧 / 首帧场景)。
#[derive(Debug, Clone)]
pub struct Pid {
kp: f32,
ki: f32,
kd: f32,
out_min: f32,
out_max: f32,
/// 微分低通时间常数(秒);`0` 关闭滤波。
derivative_tau: f32,
integral: f32,
prev_measurement: Option<f32>,
derivative_filtered: f32,
last_output: f32,
}
impl Pid {
/// 创建 PID。`out_min` 必须小于 `out_max`。
///
/// # Panics
///
/// 参数非有限或 `out_min >= out_max` 时 panic。
#[must_use]
pub fn new(kp: f32, ki: f32, kd: f32, out_min: f32, out_max: f32) -> Self {
assert!(
out_min < out_max && [kp, ki, kd, out_min, out_max].iter().all(|v| v.is_finite()),
"invalid PID parameters"
);
Self {
kp,
ki,
kd,
out_min,
out_max,
derivative_tau: 0.0,
integral: 0.0,
prev_measurement: None,
derivative_filtered: 0.0,
last_output: 0.0,
}
}
/// 设置微分低通时间常数(秒),`0` 关闭滤波。链式调用。
#[must_use]
pub fn with_derivative_tau(mut self, tau: f32) -> Self {
self.derivative_tau = tau.max(0.0);
self
}
/// 单步更新,返回限幅后的输出。
pub fn update(&mut self, setpoint: f32, measurement: f32, dt: f32) -> f32 {
// NaN 与 +inf 的比较结果均为 false,故 `dt > 0` 已隐含有限性。
let input_valid = dt > 0.0 && setpoint.is_finite() && measurement.is_finite();
if !input_valid {
return self.last_output;
}
let error = setpoint - measurement;
// 微分作用于测量值(反向),一阶低通滤波。
let d_raw = match self.prev_measurement {
Some(prev) => -(measurement - prev) / dt,
None => 0.0,
};
if self.derivative_tau > 0.0 {
let alpha = dt / (self.derivative_tau + dt);
self.derivative_filtered += alpha * (d_raw - self.derivative_filtered);
} else {
self.derivative_filtered = d_raw;
}
let d_term = self.kd * self.derivative_filtered;
// 积分抗饱和(clamping):仅在不会把输出进一步推入饱和时积分。
let i_candidate = self.integral + self.ki * error * dt;
let unsat = self.kp * error + i_candidate + d_term;
let saturating_up = unsat > self.out_max && error > 0.0;
let saturating_down = unsat < self.out_min && error < 0.0;
if !saturating_up && !saturating_down {
self.integral = i_candidate;
}
let output = (self.kp * error + self.integral + d_term).clamp(self.out_min, self.out_max);
self.prev_measurement = Some(measurement);
self.last_output = output;
output
}
/// 清空积分、微分记忆与上次输出。
pub fn reset(&mut self) {
self.integral = 0.0;
self.prev_measurement = None;
self.derivative_filtered = 0.0;
self.last_output = 0.0;
}
/// 上一次 `update` 的输出(诊断 / 串级外环取速度目标用)。
#[must_use]
pub fn last_output(&self) -> f32 {
self.last_output
}
/// 当前积分累加项。
#[must_use]
pub fn integral(&self) -> f32 {
self.integral
}
/// 比例增益。
#[must_use]
pub fn kp(&self) -> f32 {
self.kp
}
/// 积分增益。
#[must_use]
pub fn ki(&self) -> f32 {
self.ki
}
/// 微分增益。
#[must_use]
pub fn kd(&self) -> f32 {
self.kd
}
/// 输出下限。
#[must_use]
pub fn output_min(&self) -> f32 {
self.out_min
}
/// 输出上限。
#[must_use]
pub fn output_max(&self) -> f32 {
self.out_max
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f32 = 0.001;
#[test]
fn pure_proportional() {
let mut pid = Pid::new(2.0, 0.0, 0.0, -100.0, 100.0);
let out = pid.update(10.0, 4.0, DT);
assert!((out - 12.0).abs() < 1e-4, "out = {out}");
}
#[test]
fn output_clamps_both_ends() {
let mut pid = Pid::new(100.0, 0.0, 0.0, -10.0, 10.0);
assert!((pid.update(1.0, 0.0, DT) - 10.0).abs() < 1e-5);
assert!((pid.update(-1.0, 0.0, DT) - (-10.0)).abs() < 1e-5);
}
#[test]
fn integral_accumulates() {
let mut pid = Pid::new(0.0, 10.0, 0.0, -1e6, 1e6);
pid.update(1.0, 0.0, DT); // integral += 10 * 1 * 0.001
pid.update(1.0, 0.0, DT);
let out = pid.update(1.0, 0.0, DT);
let expected = 10.0 * 1.0 * DT * 3.0;
assert!((out - expected).abs() < 1e-4, "out = {out}, expected {expected}");
}
#[test]
fn anti_windup_bounds_integral_when_saturated() {
// 大误差持续饱和 200 步:kp = 0 时输出即积分项。clamping 抗饱和要求
// 积分不超过 out_max,且进入保持区后输出不再增长。
let ki = 5.0;
let mut pid = Pid::new(0.0, ki, 0.0, -16384.0, 16384.0);
for _ in 0..200 {
pid.update(1e6, 0.0, DT);
}
assert!(pid.integral() <= 16384.0, "integral = {}", pid.integral());
let frozen = pid.update(1e6, 0.0, DT);
let still = pid.update(1e6, 0.0, DT);
assert!(
(frozen - still).abs() < 1e-3,
"saturated output must stop growing: {frozen} vs {still}"
);
}
#[test]
fn anti_windup_recovers_quickly_on_reverse() {
// 饱和后目标反向,输出应在一个反向误差步内脱离正饱和。
let mut pid = Pid::new(1.0, 10.0, 0.0, -100.0, 100.0);
for _ in 0..500 {
pid.update(1000.0, 0.0, DT);
}
assert!((pid.update(1000.0, 0.0, DT) - 100.0).abs() < 1e-3);
// 一步反向大误差(kp=1, e=-1000)足以击穿下限,说明无深度饱和。
let out = pid.update(-1000.0, 0.0, DT);
assert!(out <= 0.0, "expected non-positive output, got {out}");
}
#[test]
fn derivative_acts_on_measurement() {
// 设定值恒 0,测量值阶跃:输出应为负向微分冲击(-kd*Δ/dt)。
let mut pid = Pid::new(0.0, 0.0, 0.01, -1e6, 1e6);
pid.update(0.0, 0.0, DT); // 建立初值,首帧微分为 0
let out = pid.update(0.0, 0.5, DT);
let expected = -0.01 * (0.5 / DT);
assert!((out - expected).abs() < 1.0, "out = {out}, expected {expected}");
}
#[test]
fn derivative_filter_softens_spike() {
let step = |tau: f32| {
let mut pid = Pid::new(0.0, 0.0, 10.0, -1e9, 1e9).with_derivative_tau(tau);
pid.update(0.0, 0.0, DT);
pid.update(0.0, 1.0, DT).abs()
};
let unfiltered = step(0.0);
let filtered = step(0.05);
assert!(
filtered < unfiltered * 0.05,
"filtered {filtered} should be far below unfiltered {unfiltered}"
);
}
#[test]
fn nonpositive_dt_returns_last_output_and_keeps_state() {
let mut pid = Pid::new(1.0, 1.0, 0.0, -100.0, 100.0);
let first = pid.update(10.0, 0.0, DT);
let integral_before = pid.integral().to_bits();
// 逐位相等:无效输入必须原样返回上次输出。
assert_eq!(pid.update(99.0, 0.0, 0.0).to_bits(), first.to_bits());
assert_eq!(pid.update(99.0, 0.0, f32::NAN).to_bits(), first.to_bits());
assert_eq!(pid.integral().to_bits(), integral_before);
}
#[test]
fn nan_input_returns_last_output() {
let mut pid = Pid::new(1.0, 0.0, 0.0, -100.0, 100.0);
let first = pid.update(1.0, 0.0, DT);
assert_eq!(pid.update(f32::NAN, 0.0, DT).to_bits(), first.to_bits());
assert_eq!(pid.update(1.0, f32::INFINITY, DT).to_bits(), first.to_bits());
}
#[test]
fn reset_clears_history() {
let mut pid = Pid::new(1.0, 10.0, 1.0, -1e6, 1e6);
pid.update(5.0, 1.0, DT);
pid.update(5.0, 1.0, DT);
pid.reset();
assert_eq!(pid.integral().to_bits(), 0.0f32.to_bits());
// 重置后首帧微分为 0,输出 = P 项 + 本步积分(ki*e*dt = 0.04)。
let out = pid.update(5.0, 1.0, DT);
assert!((out - 4.04).abs() < 1e-4, "out = {out}");
}
#[test]
#[should_panic = "invalid PID parameters"]
fn new_rejects_inverted_limits() {
let _pid = Pid::new(1.0, 0.0, 0.0, 1.0, -1.0);
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "mecanum"
description = "Mecanum chassis kinematics"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
[dev-dependencies]
proptest = { workspace = true }
approx = { workspace = true }
[package.metadata.sentry-chassis]
layer = "control/chassis"
+286
View File
@@ -0,0 +1,286 @@
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
//! 麦克纳姆轮底盘正 / 逆运动学。
//!
//! # 约定
//!
//! - 机器人坐标系(ISO):x 前、y 左、ω 绕 z 逆时针为正;
//! - 轮序固定为 `[前左, 前右, 后左, 后右]`(常量 [`FL`] / [`FR`] / [`RL`] /
//! [`RR`]);
//! - 辊子 45°、俯视呈 X 型布置(RoboMaster 底盘标准布局);
//! - 角速度单位 rad/s,线速度 m/s,换算见 [`wheel_rpm_from_angular`]。
//!
//! 四轮线速度(`a = half_length + half_width`):
//!
//! ```text
//! v_FL = vx - vy - a·ω
//! v_FR = vx + vy + a·ω
//! v_RL = vx + vy - a·ω
//! v_RR = vx - vy + a·ω
//! ```
//!
//! 正解为逆解的左伪逆(JᵀJ 对角,可解析求平均)。
use std::f32::consts::TAU;
/// 前左轮下标。
pub const FL: usize = 0;
/// 前右轮下标。
pub const FR: usize = 1;
/// 后左轮下标。
pub const RL: usize = 2;
/// 后右轮下标。
pub const RR: usize = 3;
/// 底盘平面速度:x 前进(m/s)、y 左移(m/s)、ω 逆时针(rad/s)。
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Twist {
/// 前进速度(m/s,前正)。
pub vx: f32,
/// 横移速度(m/s,左正)。
pub vy: f32,
/// 角速度(rad/s,逆时针正)。
pub omega: f32,
}
impl Twist {
/// 全零速度。
pub const ZERO: Self = Self { vx: 0.0, vy: 0.0, omega: 0.0 };
/// 幅值限幅:任一分量超界时按比例收缩整体,保持运动方向不变。
///
/// `max_forward` / `max_strafe` 单位 m/s,`max_spin` 单位 rad/s。
#[must_use]
pub fn clamped(self, max_forward: f32, max_strafe: f32, max_spin: f32) -> Self {
let scale = [
(self.vx.abs() / max_forward).max(f32::EPSILON),
(self.vy.abs() / max_strafe).max(f32::EPSILON),
(self.omega.abs() / max_spin).max(f32::EPSILON),
]
.into_iter()
.fold(1.0f32, f32::max);
Self { vx: self.vx / scale, vy: self.vy / scale, omega: self.omega / scale }
}
}
/// 麦轮底盘几何参数。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mecanum {
/// 前后轮轴线到底盘中心的纵向半距 lx(米,正)。
pub half_length: f32,
/// 左右轮到底盘中心的横向半距 ly(米,正)。
pub half_width: f32,
/// 车轮半径(米,正)。
pub wheel_radius: f32,
}
impl Mecanum {
/// 按几何参数构造。
///
/// # Panics
///
/// debug 构建下参数非正时 panic(配置层应先行校验)。
#[must_use]
pub fn new(half_length: f32, half_width: f32, wheel_radius: f32) -> Self {
debug_assert!(
half_length > 0.0 && half_width > 0.0 && wheel_radius > 0.0,
"mecanum geometry must be positive"
);
Self { half_length, half_width, wheel_radius }
}
/// 逆解:底盘速度 → 四轮角速度(rad/s,顺序 FL/FR/RL/RR)。
#[must_use]
pub fn wheel_speeds(&self, twist: &Twist) -> [f32; 4] {
let a = self.half_length + self.half_width;
[
twist.vx - twist.vy - a * twist.omega,
twist.vx + twist.vy + a * twist.omega,
twist.vx + twist.vy - a * twist.omega,
twist.vx - twist.vy + a * twist.omega,
]
.map(|v| v / self.wheel_radius)
}
/// 正解:四轮角速度(rad/s)→ 底盘速度(最小二乘意义)。
#[must_use]
pub fn twist(&self, wheels: &[f32; 4]) -> Twist {
let w = wheels.map(|x| x * self.wheel_radius);
let a = self.half_length + self.half_width;
Twist {
vx: (w[FL] + w[FR] + w[RL] + w[RR]) / 4.0,
vy: (-w[FL] + w[FR] + w[RL] - w[RR]) / 4.0,
omega: (-w[FL] + w[FR] - w[RL] + w[RR]) / (4.0 * a),
}
}
/// 逆解并直接输出四轮转速(rpm)。
#[must_use]
pub fn wheel_rpms(&self, twist: &Twist) -> [f32; 4] {
self.wheel_speeds(twist).map(wheel_rpm_from_angular)
}
/// 由四轮转速(rpm)正解底盘速度。
#[must_use]
pub fn twist_from_wheel_rpms(&self, wheel_rpms: &[f32; 4]) -> Twist {
self.twist(&wheel_rpms.map(wheel_angular_from_rpm))
}
}
/// 轮角速度(rad/s)→ 转速(rpm)。
#[must_use]
pub const fn wheel_rpm_from_angular(rad_per_s: f32) -> f32 {
rad_per_s * 60.0 / TAU
}
/// 轮转速(rpm)→ 角速度(rad/s)。
#[must_use]
pub const fn wheel_angular_from_rpm(rpm: f32) -> f32 {
rpm * TAU / 60.0
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
/// 0.2m 半轴距 / 0.25m 半轮距 / 76.2mm 轮半径的典型 RM 底盘。
fn chassis() -> Mecanum {
Mecanum::new(0.2, 0.25, 0.0762)
}
fn assert_twist_eq(a: &Twist, b: &Twist) {
assert_relative_eq!(a.vx, b.vx, epsilon = 1e-5);
assert_relative_eq!(a.vy, b.vy, epsilon = 1e-5);
assert_relative_eq!(a.omega, b.omega, epsilon = 1e-5);
}
#[test]
fn pure_forward_drives_all_wheels_equally() {
let m = chassis();
let w = m.wheel_speeds(&Twist { vx: 1.0, ..Twist::ZERO });
for x in w {
assert_relative_eq!(x, 1.0 / 0.0762, epsilon = 1e-5);
}
}
#[test]
fn pure_strafe_left_opposes_diagonal_wheels() {
let m = chassis();
let w = m.wheel_speeds(&Twist { vy: 2.0, ..Twist::ZERO });
let v = 2.0 / 0.0762;
assert_relative_eq!(w[FL], -v, epsilon = 1e-5);
assert_relative_eq!(w[FR], v, epsilon = 1e-5);
assert_relative_eq!(w[RL], v, epsilon = 1e-5);
assert_relative_eq!(w[RR], -v, epsilon = 1e-5);
}
#[test]
fn pure_rotation_splits_left_right() {
let m = chassis();
let w = m.wheel_speeds(&Twist { omega: 1.0, ..Twist::ZERO });
let v = (0.2 + 0.25) / 0.0762;
// 逆时针:左侧后退、右侧前进。
assert_relative_eq!(w[FL], -v, epsilon = 1e-5);
assert_relative_eq!(w[FR], v, epsilon = 1e-5);
assert_relative_eq!(w[RL], -v, epsilon = 1e-5);
assert_relative_eq!(w[RR], v, epsilon = 1e-5);
}
#[test]
fn diagonal_motion_idles_two_wheels() {
// 前左 45° 对角线:vx = vy ⇒ FL、RR 停转。
let m = chassis();
let w = m.wheel_speeds(&Twist { vx: 1.0, vy: 1.0, omega: 0.0 });
assert_relative_eq!(w[FL], 0.0, epsilon = 1e-6);
assert_relative_eq!(w[RR], 0.0, epsilon = 1e-6);
assert_relative_eq!(w[FR], 2.0 / 0.0762, epsilon = 1e-5);
assert_relative_eq!(w[RL], 2.0 / 0.0762, epsilon = 1e-5);
}
#[test]
fn zero_twist_zero_wheels() {
let wheels = chassis().wheel_speeds(&Twist::ZERO);
assert!(wheels.iter().all(|w| f64::from(*w).abs() < 1e-12), "{wheels:?}");
}
#[test]
fn inverse_then_forward_recovers_twist() {
let m = chassis();
let t = Twist { vx: 1.5, vy: -0.8, omega: 2.2 };
assert_twist_eq(&m.twist(&m.wheel_speeds(&t)), &t);
}
#[test]
fn rpm_helpers_roundtrip() {
assert_relative_eq!(
wheel_rpm_from_angular(wheel_angular_from_rpm(482.0)),
482.0,
epsilon = 1e-4
);
assert_relative_eq!(wheel_angular_from_rpm(60.0 / TAU), 1.0, epsilon = 1e-6);
}
#[test]
fn rpm_path_matches_angular_path() {
let m = chassis();
let t = Twist { vx: 0.3, vy: 1.1, omega: -0.7 };
assert_twist_eq(&m.twist_from_wheel_rpms(&m.wheel_rpms(&t)), &t);
}
#[test]
fn clamp_preserves_direction_and_limits() {
let t = Twist { vx: 4.0, vy: 1.0, omega: 0.5 }.clamped(2.0, 2.0, 3.0);
assert_relative_eq!(t.vx, 2.0, epsilon = 1e-6);
assert_relative_eq!(t.vy, 0.5, epsilon = 1e-6);
assert_relative_eq!(t.omega, 0.25, epsilon = 1e-6);
// 限内不缩放。
let keep = Twist { vx: 1.0, vy: -1.0, omega: 2.0 }.clamped(2.0, 2.0, 3.0);
assert_eq!(keep, Twist { vx: 1.0, vy: -1.0, omega: 2.0 });
}
}
#[cfg(test)]
mod proptests {
use super::*;
use approx::assert_relative_eq;
use proptest::prelude::*;
proptest! {
/// 正逆解互逆:forward(inverse(t)) == t(对任意合理几何与速度)。
#[test]
fn kinematics_roundtrip(
lx in 0.05f32..0.5,
ly in 0.05f32..0.5,
r in 0.03f32..0.2,
vx in -3.0f32..3.0,
vy in -3.0f32..3.0,
omega in -6.0f32..6.0,
) {
let m = Mecanum::new(lx, ly, r);
let t = Twist { vx, vy, omega };
let back = m.twist(&m.wheel_speeds(&t));
assert_relative_eq!(back.vx, t.vx, epsilon = 1e-4);
assert_relative_eq!(back.vy, t.vy, epsilon = 1e-4);
assert_relative_eq!(back.omega, t.omega, epsilon = 1e-4);
}
/// 左右镜像对称:vy、ω 取反(镜像底盘)等价于 FL↔FR、RL↔RR 对调。
#[test]
fn strafe_symmetry(
vx in -2.0f32..2.0,
vy in -2.0f32..2.0,
omega in -4.0f32..4.0,
) {
let m = Mecanum::new(0.2, 0.25, 0.0762);
let t = Twist { vx, vy, omega };
let mirrored = Twist { vx, vy: -vy, omega: -omega };
let w = m.wheel_speeds(&t);
let w2 = m.wheel_speeds(&mirrored);
assert_relative_eq!(w[FL], w2[FR], epsilon = 1e-4);
assert_relative_eq!(w[FR], w2[FL], epsilon = 1e-4);
assert_relative_eq!(w[RL], w2[RR], epsilon = 1e-4);
assert_relative_eq!(w[RR], w2[RL], epsilon = 1e-4);
}
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "motor-control"
description = "Motor velocity, position, cascade, and feedforward control"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
pid = { workspace = true }
motor = { workspace = true }
[dev-dependencies]
approx = { workspace = true }
[package.metadata.sentry-chassis]
layer = "control/motor"
+798
View File
@@ -0,0 +1,798 @@
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod feedforward {
//! 前馈补偿(ARCHITECTURE.md §5.5)。
//!
//! 速度环的前馈是**比例前馈**:稳态下所需控制量与目标转速近似成正比,
//! 因此 `u_ff = k_ff · setpoint` 可以直接把积分项从"慢慢顶"变成"一开始就到位"。
//!
//! # 为什么前馈在空载台架上必须为 0
//!
//! `k_ff ≈ 1/K`,其中 `K` 是被控对象的稳态增益(GM3508 空载实测
//! `K ≈ 0.57 rpm/raw` → `k_ff ≈ 1.77`)。空载时维持转速只需要克服摩擦的极小
//! 电流,任何接近 1.77 的前馈都会把轮子直接推到最高转速(见 `docs/chassis.md` §7.2)。
/// 比例前馈:`输出 = k_ff · setpoint`。
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct ProportionalFeedforward {
/// 前馈系数(控制量 / 目标量)。
pub k_ff: f32,
}
impl ProportionalFeedforward {
/// 构造。
#[must_use]
pub const fn new(k_ff: f32) -> Self {
Self { k_ff }
}
/// 由被控对象稳态增益换算:`k_ff = 1 / K`。
///
/// `K` 非有限或 ≤ 0 时返回 0(宁可不前馈,也不要一个发散的系数)。
#[must_use]
pub fn from_plant_gain(plant_gain: f32) -> Self {
if plant_gain.is_finite() && plant_gain > 0.0 {
Self { k_ff: 1.0 / plant_gain }
} else {
Self { k_ff: 0.0 }
}
}
/// 计算前馈量。
#[must_use]
pub fn compute(&self, setpoint: f32) -> f32 {
if setpoint.is_finite() { self.k_ff * setpoint } else { 0.0 }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inverse_plant_gain_matches_the_documented_bench_feedforward() {
// GM3508 空载 K ≈ 0.5648 rpm/raw → k_ff ≈ 1.77 raw/rpm
let feedforward = ProportionalFeedforward::from_plant_gain(9254.0 / 16384.0);
assert!((feedforward.k_ff - 1.77).abs() < 0.02, "{}", feedforward.k_ff);
}
#[test]
fn degenerate_plant_gain_yields_no_feedforward() {
assert!((ProportionalFeedforward::from_plant_gain(0.0).k_ff).abs() < 1e-6);
assert!((ProportionalFeedforward::from_plant_gain(-1.0).k_ff).abs() < 1e-6);
assert!((ProportionalFeedforward::from_plant_gain(f32::NAN).k_ff).abs() < 1e-6);
}
#[test]
fn compute_is_linear_and_safe() {
let feedforward = ProportionalFeedforward::new(2.0);
assert!((feedforward.compute(3.0) - 6.0).abs() < 1e-6);
assert!((feedforward.compute(f32::INFINITY)).abs() < 1e-6);
assert!((ProportionalFeedforward::default().compute(100.0)).abs() < 1e-6);
}
}
}
pub use feedforward::*;
pub mod plant {
//! 被控对象抽象:让**仿真与实机共用同一套 PID**(ARCHITECTURE.md §5.5)。
//!
//! 这是整定能否迁移到实机的关键 seam:整定器只认识 [`ControlPlant`],
//! 因此"在仿真里搜出来的增益"与"在台架上验出来的增益"评估的是同一条接口。
//!
//! 实现方:
//! - 仿真:`sim-harness::SimPlant`(浮点积分,无 IO,可并行上千次);
//! - 实机/台架:`bench` 的台架采样(每次试验 = 真机跑一条场景)。
/// 单输入单输出的被控对象。
///
/// 约定:`step` 推进 `dt` 秒并返回**本次的测量输出**;输入是控制量
/// (`motor-control` 里即 effort raw)。实现必须保证 `dt <= 0` 时不推进状态。
pub trait ControlPlant {
/// 推进一拍的误差类型(平台相关实现可以自定义)。
type Error;
/// 推进一步,返回测量输出。
///
/// # Errors
///
/// 数值发散或输入非法时返回错误,由调用方折算成硬约束罚值。
fn step(&mut self, input: f32, dt: f32) -> Result<f32, Self::Error>;
/// 复位到初始状态(两次试验之间必须调用,否则结果依赖历史)。
fn reset(&mut self);
/// 人类可读描述(写进报告,便于复现实验条件)。
fn describe(&self) -> String;
}
/// 无错误的被控对象(纯代数模型 / 已在内部处理发散的实现)用它的适配器。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Infallible;
impl core::fmt::Display for Infallible {
fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Ok(())
}
}
impl core::error::Error for Infallible {}
/// 一阶惯性环节:`y' = (k·u − y) / tau`,显式欧拉。
///
/// 这是最小可用的 [`ControlPlant`] 实现,用来给控制律 / 整定器做单元测试;
/// 带摩擦与延迟的完整电机模型在 `sim-harness`。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FirstOrderPlant {
/// 稳态增益。
pub gain: f32,
/// 时间常数(秒)。
pub tau_s: f32,
/// 当前输出。
pub output: f32,
}
impl FirstOrderPlant {
/// 构造(`tau_s` 必须为正,否则退化为直通)。
#[must_use]
pub fn new(gain: f32, tau_s: f32) -> Self {
Self { gain, tau_s: if tau_s > 0.0 { tau_s } else { f32::EPSILON }, output: 0.0 }
}
}
impl ControlPlant for FirstOrderPlant {
type Error = Infallible;
fn step(&mut self, input: f32, dt: f32) -> Result<f32, Self::Error> {
if dt > 0.0 && input.is_finite() {
let alpha = (dt / self.tau_s).clamp(0.0, 1.0);
let target = self.gain * input;
self.output += alpha * (target - self.output);
}
Ok(self.output)
}
fn reset(&mut self) {
self.output = 0.0;
}
fn describe(&self) -> String {
format!("first-order(gain={}, tau={}s)", self.gain, self.tau_s)
}
}
/// 标准二阶系统:`G(s) = gain·wn² / (s² + 2ζ·wn·s + wn²)`,显式欧拉。
///
/// 用作"整定算法是否真的收敛"的试金石——ARCHITECTURE.md §5.5 的验收标准就要求
/// 继电整定在标准二阶系统上收敛。
///
/// # 相频特性(做继电实验时必须知道)
///
/// `∠G(jω) = −atan2(2ζ·wn·ω, wn² − ω²)`:在 `ω = wn` 处只有 **−90°**,
/// 随 `ω → ∞` **渐近**趋于 −180°,**不存在有限的 −180° 穿越点**。
/// 因此纯二阶对象在继电反馈下的极限环由**滞环宽度**决定(幅值 ≈ 滞环量级、
/// 周期远小于 `2π/wn`),而不是由 `Tu = 2π/wn` 决定。
/// 真实电机带延迟(`sim-harness` 的模型有 2 拍命令延迟 + 摩擦),相频会真正穿过
/// −180°,那时的 `Tu` 才是有物理意义的临界周期。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SecondOrderPlant {
/// 自然频率(rad/s)。
pub wn: f32,
/// 阻尼比。
pub zeta: f32,
/// 直流增益。
pub gain: f32,
/// 位置状态。
pub position: f32,
/// 速度状态。
pub velocity: f32,
}
impl SecondOrderPlant {
/// 构造(`wn > 0`;`zeta` 取绝对值)。
#[must_use]
pub fn new(wn: f32, zeta: f32, gain: f32) -> Self {
Self {
wn: if wn > 0.0 { wn } else { f32::EPSILON },
zeta: zeta.abs(),
gain,
position: 0.0,
velocity: 0.0,
}
}
}
impl ControlPlant for SecondOrderPlant {
type Error = Infallible;
fn step(&mut self, input: f32, dt: f32) -> Result<f32, Self::Error> {
if dt > 0.0 && input.is_finite() {
let accel = self.gain * self.wn * self.wn * input
- 2.0 * self.zeta * self.wn * self.velocity
- self.wn * self.wn * self.position;
self.velocity += accel * dt;
self.position += self.velocity * dt;
}
Ok(self.position)
}
fn reset(&mut self) {
self.position = 0.0;
self.velocity = 0.0;
}
fn describe(&self) -> String {
format!("second-order(wn={} rad/s, zeta={}, gain={})", self.wn, self.zeta, self.gain)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn second_order_settles_at_dc_gain() {
let mut plant = SecondOrderPlant::new(20.0, 0.7, 2.0);
for _ in 0..20_000 {
plant.step(1.0, 0.0001).unwrap();
}
assert!((plant.position - 2.0).abs() < 5e-2, "{}", plant.position);
plant.reset();
assert!((plant.position).abs() < 1e-6);
assert!(plant.describe().contains("second-order"));
}
#[test]
fn second_order_ignores_non_positive_dt() {
let mut plant = SecondOrderPlant::new(20.0, 0.5, 1.0);
assert!((plant.step(1.0, 0.0).unwrap()).abs() < 1e-6);
assert!((plant.velocity).abs() < 1e-6);
}
#[test]
fn first_order_converges_to_gain_times_input() {
let mut plant = FirstOrderPlant::new(2.0, 0.1);
for _ in 0..2000 {
plant.step(1.0, 0.001).unwrap();
}
assert!((plant.output - 2.0).abs() < 1e-3, "{}", plant.output);
}
#[test]
fn non_positive_dt_does_not_advance() {
let mut plant = FirstOrderPlant::new(1.0, 0.1);
assert!((plant.step(5.0, 0.0).unwrap()).abs() < 1e-6);
assert!((plant.step(5.0, -1.0).unwrap()).abs() < 1e-6);
assert!((plant.step(f32::NAN, 0.001).unwrap()).abs() < 1e-6);
}
#[test]
fn reset_clears_history() {
let mut plant = FirstOrderPlant::new(1.0, 0.05);
for _ in 0..100 {
plant.step(1.0, 0.001).unwrap();
}
assert!(plant.output > 0.0);
plant.reset();
assert!((plant.output).abs() < 1e-6);
assert!(plant.describe().contains("first-order"));
}
}
}
pub use plant::*;
pub mod position {
//! 位置环控制器:单环(直出)或级联(位置→限速→速度环)。
use crate::velocity::VelocityController;
use pid::Pid;
/// 位置环拓扑。
#[derive(Debug, Clone)]
pub enum PositionLoop {
/// 单环:位置误差 → effort_raw(适合电压硬闭环 / 刚性云台)。
Direct(Pid),
/// 级联:位置 → 目标转速 → 速度环 → effort_raw。
Cascade {
/// 外环位置 PID(输出限幅 = 最大 rpm)。
pos: Pid,
/// 内环速度控制器(输出限幅 = 最大 effort)。
vel: VelocityController,
},
}
/// 位置环封装。输入为目标 / 实测多圈位置(圈),输出为 effort_raw。
#[derive(Debug, Clone)]
pub struct PositionController {
inner: PositionLoop,
}
impl PositionController {
/// 单环位置控制器。
#[must_use]
pub fn direct(pid: Pid) -> Self {
Self { inner: PositionLoop::Direct(pid) }
}
/// 级联位置控制器(限速)。
#[must_use]
pub fn cascade(pos: Pid, vel: VelocityController) -> Self {
Self { inner: PositionLoop::Cascade { pos, vel } }
}
/// 单步更新,返回 effort_raw(已含限幅)。
///
/// `measured_rpm` 仅级联内环使用;单环忽略该参数。
pub fn update(
&mut self,
target_rev: f32,
measured_rev: f32,
measured_rpm: f32,
dt: f32,
) -> f32 {
match &mut self.inner {
PositionLoop::Direct(pid) => pid.update(target_rev, measured_rev, dt),
PositionLoop::Cascade { pos, vel } => {
let vel_target = pos.update(target_rev, measured_rev, dt);
vel.update(vel_target, measured_rpm, dt)
}
}
}
/// 清空内部状态(切换模式 / 重新使能时调用)。
pub fn reset(&mut self) {
match &mut self.inner {
PositionLoop::Direct(pid) => pid.reset(),
PositionLoop::Cascade { pos, vel } => {
pos.reset();
vel.reset();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f32 = 0.001;
#[test]
fn direct_outputs_pid() {
let mut pc = PositionController::direct(Pid::new(2.0, 0.0, 0.0, -100.0, 100.0));
let out = pc.update(1.0, 0.5, 0.0, DT);
assert!((out - 1.0).abs() < 1e-4, "out = {out}");
}
#[test]
fn cascade_limits_velocity_then_effort() {
// 外环 kp=10 → 误差 1 圈输出 10 rpm(限幅 5 rpm);内环纯前馈 2 raw/rpm。
let pos = Pid::new(10.0, 0.0, 0.0, -5.0, 5.0);
let vel = VelocityController::new(Pid::new(0.0, 0.0, 0.0, -1000.0, 1000.0), 2.0);
let mut pc = PositionController::cascade(pos, vel);
let out = pc.update(1.0, 0.0, 0.0, DT);
// 外环饱和到 5 rpm,内环 ff=2 → 10 raw。
assert!((out - 10.0).abs() < 1e-3, "out = {out}");
}
#[test]
fn reset_clears_state() {
let mut pc = PositionController::direct(Pid::new(0.0, 10.0, 0.0, -1e6, 1e6));
pc.update(1.0, 0.0, 0.0, DT);
pc.reset();
let out = pc.update(1.0, 0.0, 0.0, DT);
// 重置后只剩本步积分:10 * 1 * 0.001。
assert!((out - 0.01).abs() < 1e-5, "out = {out}");
}
}
}
pub use position::*;
pub mod velocity {
//! 速度环控制器:PID + 速度前馈。
use pid::Pid;
/// 速度环增益组(可整定参数)。
///
/// 单位:`kp` 为 raw / rpm,`ki` 为 raw / (rpm·s),`kd` 为 raw / (rpm/s),
/// `feedforward` 为 raw / rpm。整套增益是**纯数据**,便于整定工具在候选之间
/// 传递、落盘与热更新,而无需重建整个速度环。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VelocityGains {
/// 比例增益(raw / rpm)。
pub kp: f32,
/// 积分增益(raw / (rpm·s))。
pub ki: f32,
/// 微分增益(raw / (rpm/s))。
pub kd: f32,
/// 速度前馈(raw / rpm)。
pub feedforward: f32,
}
impl VelocityGains {
/// 全零增益。
pub const ZERO: Self = Self { kp: 0.0, ki: 0.0, kd: 0.0, feedforward: 0.0 };
/// 由四个分量构造。
#[must_use]
pub const fn new(kp: f32, ki: f32, kd: f32, feedforward: f32) -> Self {
Self { kp, ki, kd, feedforward }
}
/// 所有增益是否有限(NaN / inf 不得进入 [`Pid::new`],它会 panic)。
#[must_use]
pub fn is_finite(&self) -> bool {
self.kp.is_finite()
&& self.ki.is_finite()
&& self.kd.is_finite()
&& self.feedforward.is_finite()
}
}
/// 速度环封装:在 [`Pid`] 之上叠加比例前馈。
///
/// 输入为目标 / 实测转速(rpm,物理方向),输出为开环控制量
/// effort_raw(协议 raw:ESC=电流,GM6020=电压)。
/// 前馈系数 `feedforward` 的单位是 **raw / rpm**——稳态下所需控制量
/// 与转速近似成正比(GM3508 电流典型值约 9 raw/rpm,需按实车整定)。
///
/// 由驱动层按控制节拍(如 C620 的 1kHz 发送周期)调用。
#[derive(Debug, Clone)]
pub struct VelocityController {
pid: Pid,
feedforward: f32,
}
impl VelocityController {
/// 创建速度环。`pid` 的输出限幅应设为该电机的 effort 限幅(raw)。
#[must_use]
pub fn new(pid: Pid, feedforward: f32) -> Self {
Self { pid, feedforward }
}
/// 由增益组与 effort 限幅直接创建(把 [`VelocityGains`] 展开成 [`Pid`])。
///
/// # Panics
///
/// `output_limit_raw` 为 0,或增益含 NaN / inf 时 panic(同 [`Pid::new`])。
#[must_use]
pub fn from_gains(output_limit_raw: i16, gains: VelocityGains) -> Self {
let limit = f32::from(output_limit_raw);
let pid = Pid::new(gains.kp, gains.ki, gains.kd, -limit, limit);
Self { pid, feedforward: gains.feedforward }
}
/// 当前增益组(含前馈),用于诊断与热更新前的读取。
#[must_use]
pub fn gains(&self) -> VelocityGains {
VelocityGains {
kp: self.pid.kp(),
ki: self.pid.ki(),
kd: self.pid.kd(),
feedforward: self.feedforward,
}
}
/// 单步更新,返回 effort_raw(已含 PID 输出限幅)。
pub fn update(&mut self, target_rpm: f32, measured_rpm: f32, dt: f32) -> f32 {
let pid_out = self.pid.update(target_rpm, measured_rpm, dt);
(pid_out + self.feedforward * target_rpm).clamp(
// 前馈可能把输出推出限幅之外,这里以 PID 限幅为准再夹一次。
self.pid.output_min(),
self.pid.output_max(),
)
}
/// 清空 PID 内部状态(切换模式 / 重新使能时调用)。
pub fn reset(&mut self) {
self.pid.reset();
}
/// 前馈系数(raw / rpm)。
#[must_use]
pub fn feedforward(&self) -> f32 {
self.feedforward
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pure_feedforward_tracks_target() {
let mut vc = VelocityController::new(Pid::new(0.0, 0.0, 0.0, -16384.0, 16384.0), 9.0);
assert!((vc.update(1000.0, 0.0, 0.001) - 9000.0).abs() < 1e-3);
// 前馈 -18000 被限幅到 -16384。
assert!((vc.update(-2000.0, 0.0, 0.001) + 16384.0).abs() < 1e-3);
}
#[test]
fn feedforward_plus_pid_respects_limit() {
let mut vc = VelocityController::new(Pid::new(50.0, 0.0, 0.0, -12000.0, 12000.0), 9.0);
// 前馈 9000 + P 项 50*100 = 14000 → 限幅到 12000。
let out = vc.update(1000.0, 0.0, 0.001);
assert!((out - 12000.0).abs() < 1e-3, "out = {out}");
}
#[test]
fn zero_error_yields_pure_feedforward() {
// 误差为 0 时 PID 贡献为 0,输出只剩前馈。
let mut vc = VelocityController::new(Pid::new(10.0, 5.0, 0.0, -1e6, 1e6), 9.0);
let out = vc.update(1000.0, 1000.0, 0.001);
assert!((out - 9000.0).abs() < 1e-4, "out = {out}");
}
#[test]
fn reset_clears_pid_state() {
let mut vc = VelocityController::new(Pid::new(10.0, 100.0, 0.0, -1e6, 1e6), 0.0);
for _ in 0..10 {
vc.update(100.0, 0.0, 0.001);
}
vc.reset();
// 重置后首步输出 = 纯 P + 本步积分(ki*e*dt = 10)。
let out = vc.update(100.0, 0.0, 0.001);
assert!((out - 1010.0).abs() < 1e-3, "out = {out}");
}
#[test]
fn from_gains_round_trips_through_gains() {
let gains = VelocityGains::new(8.0, 2.0, 0.5, 1.77);
let mut from_data = VelocityController::from_gains(12_000, gains);
let mut by_hand =
VelocityController::new(Pid::new(8.0, 2.0, 0.5, -12_000.0, 12_000.0), 1.77);
assert_eq!(from_data.gains(), gains);
// 两条构造路径必须逐位一致。
for step in 0..16 {
let target = 1000.0 + step as f32;
let measured = 900.0 + step as f32 * 3.0;
assert_eq!(
from_data.update(target, measured, 0.001).to_bits(),
by_hand.update(target, measured, 0.001).to_bits()
);
}
}
#[test]
fn from_gains_clamps_to_output_limit() {
let mut vc =
VelocityController::from_gains(1000, VelocityGains::new(100.0, 0.0, 0.0, 0.0));
assert!((vc.update(1e5, 0.0, 0.001) - 1000.0).abs() < 1e-3);
}
#[test]
fn gains_finiteness_is_checkable() {
assert!(VelocityGains::new(1.0, 2.0, 3.0, 4.0).is_finite());
assert!(!VelocityGains::new(f32::NAN, 2.0, 3.0, 4.0).is_finite());
assert!(!VelocityGains::new(1.0, f32::INFINITY, 3.0, 4.0).is_finite());
assert_eq!(VelocityGains::ZERO, VelocityGains::new(0.0, 0.0, 0.0, 0.0));
}
}
}
pub use velocity::*;
pub mod cascade {
//! 串级控制:位置环(外)+ 速度环(内)(ARCHITECTURE.md §5.5)。
//!
//! 外环输出的是**速度目标**而不是控制量,因此内环必须在每个节拍都跑一次——
//! 这也是云台(位置)与底盘(速度)共用的结构:底盘只用内环,云台两环都用。
//!
//! ```text
//! 位置目标 ──►[ 外环 PID ]──► 速度目标 ──►[ 内环 PID ]──► effort
//! ▲ ▲
//! 位置反馈 速度反馈
//! ```
use pid::Pid;
/// 位置-速度串级控制器。
#[derive(Debug, Clone)]
pub struct CascadeController {
/// 外环:位置 → 速度目标。
outer: Pid,
/// 内环:速度 → 控制量。
inner: Pid,
}
impl CascadeController {
/// 由两个已配置好的 PID 组装。
#[must_use]
pub const fn new(outer: Pid, inner: Pid) -> Self {
Self { outer, inner }
}
/// 单步更新,返回内环输出(控制量)。
///
/// 非有限输入会被内环防御性忽略(见 [`Pid::update`]),因此本函数不会 panic。
pub fn compute(
&mut self,
position_setpoint: f32,
position_feedback: f32,
velocity_feedback: f32,
dt: f32,
) -> f32 {
let velocity_setpoint = self.outer.update(position_setpoint, position_feedback, dt);
self.inner.update(velocity_setpoint, velocity_feedback, dt)
}
/// 外环当前建议的速度目标(诊断 / 限幅用)。
#[must_use]
pub fn last_velocity_setpoint(&self) -> f32 {
self.outer.last_output()
}
/// 清空两环的积分与微分记忆。
pub fn reset(&mut self) {
self.outer.reset();
self.inner.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plant::{ControlPlant, FirstOrderPlant};
#[test]
fn cascade_drives_a_first_order_plant_to_the_position_setpoint() {
// 内环(速度)与对象同量纲简化:直接把外环输出当对象输入,
// 用真实的一阶对象验证"两环串联能收敛"。
let mut cascade = CascadeController::new(
Pid::new(4.0, 8.0, 0.0, -100.0, 100.0),
Pid::new(6.0, 20.0, 0.0, -100.0, 100.0),
);
let mut plant = FirstOrderPlant::new(1.0, 0.05);
let dt = 0.001;
let mut position = 0.0f32;
for _ in 0..4000 {
let effort = cascade.compute(1.0, position, 0.0, dt);
let velocity = plant.step(effort, dt).unwrap();
position += velocity * dt;
}
assert!((position - 1.0).abs() < 0.05, "position = {position}");
assert!(plant.describe().contains("first-order"));
}
#[test]
fn reset_clears_integral_windup() {
let mut cascade = CascadeController::new(
Pid::new(1.0, 1.0, 0.0, -10.0, 10.0),
Pid::new(1.0, 1.0, 0.0, -10.0, 10.0),
);
for _ in 0..100 {
cascade.compute(100.0, 0.0, 0.0, 0.001);
}
assert!(cascade.last_velocity_setpoint().abs() > 0.0);
cascade.reset();
assert!((cascade.last_velocity_setpoint()).abs() < 1e-6);
}
}
}
pub use cascade::*;
/// Per-motor controller state used by [`MotorPolicy`].
struct PolicyLoop {
current_limit: i16,
gains: VelocityGains,
velocity: VelocityController,
position: PositionController,
last_mode: motor::CommandMode,
}
impl PolicyLoop {
fn new(limit: i16, gains: VelocityGains) -> Self {
let bound = f32::from(limit);
Self {
current_limit: limit,
gains,
velocity: VelocityController::from_gains(limit, gains),
position: PositionController::direct(pid::Pid::new(
gains.kp, gains.ki, gains.kd, -bound, bound,
)),
last_mode: motor::CommandMode::Disabled,
}
}
fn reset(&mut self) {
self.velocity.reset();
self.position.reset();
self.last_mode = motor::CommandMode::Disabled;
}
fn effort(&mut self, sample: motor::PolicySample, dt: f32) -> i16 {
if !sample.online {
self.reset();
return 0;
}
if self.last_mode != sample.command.mode() {
self.reset();
self.last_mode = sample.command.mode();
}
match sample.command {
motor::Command::Disabled => {
self.reset();
0
}
motor::Command::EffortRaw(raw) => raw,
motor::Command::Velocity(target) => {
self.velocity.update(target, sample.state.rotor_speed_rpm, dt) as i16
}
motor::Command::Position(target) => self.position.update(
target,
sample.state.rotor_revolutions,
sample.state.rotor_speed_rpm,
dt,
) as i16,
}
}
}
/// Concrete velocity/position policy for up to eight ESCs.
pub struct MotorPolicy {
loops: [Option<PolicyLoop>; 8],
}
impl MotorPolicy {
/// Create an empty policy.
#[must_use]
pub fn new() -> Self {
Self { loops: std::array::from_fn(|_| None) }
}
/// Configure one ESC controller.
pub fn configure(&mut self, esc_id: u8, current_limit: i16, gains: VelocityGains) {
if let Some(slot) = self.loops.get_mut(usize::from(esc_id.saturating_sub(1))) {
*slot = Some(PolicyLoop::new(current_limit, gains));
}
}
/// Read one controller's current gains.
#[must_use]
pub fn velocity_gains(&self, esc_id: u8) -> Option<VelocityGains> {
self.loops
.get(usize::from(esc_id.saturating_sub(1)))
.and_then(Option::as_ref)
.map(|loop_| loop_.gains)
}
/// Replace one controller's gains without changing its output limit.
pub fn set_velocity_gains(&mut self, esc_id: u8, gains: VelocityGains) {
if let Some(Some(loop_)) = self.loops.get_mut(usize::from(esc_id.saturating_sub(1))) {
let limit = loop_.current_limit;
*loop_ = PolicyLoop::new(limit, gains);
}
}
}
impl Default for MotorPolicy {
fn default() -> Self {
Self::new()
}
}
impl motor::EffortPolicy for MotorPolicy {
fn step(&mut self, input: &motor::PolicyInput) -> [i16; 8] {
std::array::from_fn(|index| {
let sample = input.samples[index];
match self.loops[index].as_mut() {
Some(loop_) if sample.id == u8::try_from(index + 1).unwrap_or(0) => {
loop_.effort(sample, input.dt)
}
_ => 0,
}
})
}
fn reset(&mut self, esc_id: u8) {
if let Some(Some(loop_)) = self.loops.get_mut(usize::from(esc_id.saturating_sub(1))) {
loop_.reset();
}
}
}