pub const N_CH: usize = 10;
pub const F_WINDOW: usize = 32;
pub const F_ROLL: usize = 32;
pub const F_RES_SPAN: u32 = 20;
pub const F_ROLL_PERSIST: u32 = 8;
pub const F_REPEAT_MARGIN: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FLeg {
Residual,
Stuck,
LevelShift,
}
#[derive(Debug, Clone, Copy)]
pub struct FCalib {
pub ar_a: f64,
pub ar_b: f64,
pub ar_sd: f64,
pub mean: f64,
pub roll_max_dev: f64,
pub max_run: u32,
pub repeat_enabled: bool,
}
impl Default for FCalib {
fn default() -> Self {
FCalib { ar_a: 0.0, ar_b: 0.0, ar_sd: 1.0, mean: 0.0, roll_max_dev: 1.0, max_run: 1, repeat_enabled: true }
}
}
#[derive(Debug, Clone, Copy)]
struct FState {
roll_ring: [f64; F_ROLL],
prev: f64,
run: u32,
t: u32,
res_hit_prev: u32,
roll_streak: u32,
}
impl Default for FState {
fn default() -> Self {
FState {
roll_ring: [0.0; F_ROLL],
prev: 0.0,
run: 1,
t: 0,
res_hit_prev: u32::MAX,
roll_streak: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FAlarm {
pub leg: FLeg,
pub channel: u8,
pub tick: u32,
}
pub struct RoverMonitor {
calib: [FCalib; N_CH],
state: [FState; N_CH],
res_thr: f64,
alarmed: bool,
}
impl RoverMonitor {
pub const fn new() -> Self {
const C: FCalib = FCalib { ar_a: 0.0, ar_b: 0.0, ar_sd: 1.0, mean: 0.0, roll_max_dev: 1.0, max_run: 1, repeat_enabled: true };
const S: FState = FState { roll_ring: [0.0; F_ROLL], prev: 0.0, run: 1, t: 0, res_hit_prev: u32::MAX, roll_streak: 0 };
RoverMonitor {
calib: [C; N_CH],
state: [S; N_CH],
res_thr: 5.0,
alarmed: false,
}
}
pub fn calibrate_channel(&mut self, ch: usize, cal: FCalib) {
if ch < N_CH { self.calib[ch] = cal; }
}
pub fn set_residual_threshold(&mut self, thr: f64) {
self.res_thr = thr;
}
pub fn push(&mut self, sample: &[f64; N_CH]) -> Option<FAlarm> {
if self.alarmed { return None; }
for ch in 0..N_CH {
let cc = &self.calib[ch];
let st = &mut self.state[ch];
let t = st.t;
st.t = t.wrapping_add(1);
let v = sample[ch];
if t == 0 {
st.prev = v;
st.roll_ring[0] = v;
continue;
}
let z = (v - (cc.ar_a + cc.ar_b * st.prev)) / cc.ar_sd;
let z_abs = if z < 0.0 { -z } else { z };
if z_abs > self.res_thr {
if st.res_hit_prev != u32::MAX && t - st.res_hit_prev < F_RES_SPAN {
self.alarmed = true;
return Some(FAlarm { leg: FLeg::Residual, channel: ch as u8, tick: t });
}
st.res_hit_prev = t;
}
if v == st.prev {
st.run += 1;
if cc.repeat_enabled && st.run >= cc.max_run + F_REPEAT_MARGIN {
self.alarmed = true;
return Some(FAlarm { leg: FLeg::Stuck, channel: ch as u8, tick: t });
}
} else {
st.run = 1;
}
st.prev = v;
st.roll_ring[(t % F_ROLL as u32) as usize] = v;
if t >= F_ROLL as u32 {
let mut sum = 0.0f64;
for i in 0..F_ROLL {
sum += st.roll_ring[i];
}
let dev = sum / F_ROLL as f64 - cc.mean;
let dev_abs = if dev < 0.0 { -dev } else { dev };
if dev_abs > cc.roll_max_dev * 2.0 {
st.roll_streak += 1;
if st.roll_streak >= F_ROLL_PERSIST {
self.alarmed = true;
return Some(FAlarm { leg: FLeg::LevelShift, channel: ch as u8, tick: t });
}
} else {
st.roll_streak = 0;
}
}
}
None
}
pub fn reset(&mut self) {
self.alarmed = false;
for st in self.state.iter_mut() {
st.run = 1;
st.roll_streak = 0;
st.res_hit_prev = u32::MAX;
}
}
}
static mut ROVER_MON: RoverMonitor = RoverMonitor::new();
#[no_mangle]
pub unsafe extern "C" fn rover_monitor_init() {
let mon = core::ptr::addr_of_mut!(ROVER_MON);
(*mon) = RoverMonitor::new();
}
#[no_mangle]
pub unsafe extern "C" fn rover_monitor_calibrate_channel(
ch: u32,
ar_a: f64, ar_b: f64, ar_sd: f64,
mean: f64, roll_max_dev: f64,
max_run: u32, repeat_enabled: i32,
) {
let mon = &mut *core::ptr::addr_of_mut!(ROVER_MON);
mon.calibrate_channel(ch as usize, FCalib {
ar_a, ar_b, ar_sd, mean, roll_max_dev, max_run,
repeat_enabled: repeat_enabled != 0,
});
}
#[no_mangle]
pub unsafe extern "C" fn rover_monitor_set_res_threshold(thr: f64) {
let mon = &mut *core::ptr::addr_of_mut!(ROVER_MON);
mon.set_residual_threshold(thr);
}
#[repr(C)]
pub struct CAlarm {
pub leg: u8,
pub channel: u8,
pub tick: u32,
}
#[no_mangle]
pub unsafe extern "C" fn rover_monitor_push(
sample: *const f64,
out: *mut CAlarm,
) -> i32 {
let mon = &mut *core::ptr::addr_of_mut!(ROVER_MON);
let s = core::slice::from_raw_parts(sample, N_CH);
let mut arr = [0.0f64; N_CH];
arr.copy_from_slice(s);
match mon.push(&arr) {
Some(alarm) => {
(*out).leg = alarm.leg as u8;
(*out).channel = alarm.channel;
(*out).tick = alarm.tick;
1
}
None => 0,
}
}
#[no_mangle]
pub unsafe extern "C" fn rover_monitor_reset() {
let mon = &mut *core::ptr::addr_of_mut!(ROVER_MON);
mon.reset();
}
impl Default for RoverMonitor {
fn default() -> Self { Self::new() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flight_monitor_is_const_constructible() {
static _MON: RoverMonitor = RoverMonitor::new();
}
#[test]
fn flight_monitor_size() {
let size = core::mem::size_of::<RoverMonitor>();
assert!(size < 8192, "RoverMonitor is {} bytes — must be < 8KB", size);
}
#[test]
fn detects_stuck_sensor() {
let mut mon = RoverMonitor::new();
let cal = FCalib { ar_a: 1.0, ar_b: 0.0, ar_sd: 0.05, max_run: 2, ..FCalib::default() };
for ch in 0..N_CH { mon.calibrate_channel(ch, cal); }
mon.set_residual_threshold(20.0);
let mut sample = [1.0f64; N_CH];
for i in 0..100u32 {
for ch in 0..N_CH {
sample[ch] = 1.0 + 0.005 * ((i as f64 * 0.1 + ch as f64).sin());
}
assert!(mon.push(&sample).is_none(), "normal data alarmed at i={}", i);
}
for i in 100..120u32 {
for ch in 0..N_CH {
if ch == 3 {
sample[ch] = 1.05; } else {
sample[ch] = 1.0 + 0.005 * ((i as f64 * 0.1 + ch as f64).sin());
}
}
if let Some(alarm) = mon.push(&sample) {
assert_eq!(alarm.leg, FLeg::Stuck);
assert_eq!(alarm.channel, 3);
return;
}
}
panic!("stuck sensor not detected");
}
#[test]
fn no_alloc_proof() {
let mon = RoverMonitor::new();
assert!(!mon.alarmed);
}
}