#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransitionKind {
FrequencyHop,
ModulationChange,
BurstStart,
BurstEnd,
PowerLevelChange,
ScheduledSlotBoundary,
Unknown,
}
impl TransitionKind {
pub const fn label(self) -> &'static str {
match self {
TransitionKind::FrequencyHop => "FrequencyHop",
TransitionKind::ModulationChange => "ModulationChange",
TransitionKind::BurstStart => "BurstStart",
TransitionKind::BurstEnd => "BurstEnd",
TransitionKind::PowerLevelChange => "PowerLevelChange",
TransitionKind::ScheduledSlotBoundary => "ScheduledSlotBoundary",
TransitionKind::Unknown => "Unknown",
}
}
pub const fn requires_margin(self) -> bool {
matches!(self, TransitionKind::FrequencyHop | TransitionKind::ModulationChange)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TransitionWindow {
pub start_k: u32,
pub end_k: u32,
pub suppression_margin: u32,
pub kind: TransitionKind,
}
impl TransitionWindow {
#[inline]
pub const fn suppression_start(&self) -> u32 {
self.start_k
}
#[inline]
pub const fn suppression_end(&self) -> u32 {
self.end_k.saturating_add(self.suppression_margin)
}
#[inline]
pub fn is_active(&self, k: u32) -> bool {
k >= self.suppression_start() && k <= self.suppression_end()
}
#[inline]
pub const fn duration_k(&self) -> u32 {
self.end_k.saturating_sub(self.start_k) + 1
}
}
pub struct WaveformSchedule<const N: usize> {
windows: [TransitionWindow; N],
count: usize,
}
impl<const N: usize> WaveformSchedule<N> {
pub const fn new() -> Self {
Self {
windows: [TransitionWindow {
start_k: 0,
end_k: 0,
suppression_margin: 0,
kind: TransitionKind::Unknown,
}; N],
count: 0,
}
}
pub fn add(&mut self, window: TransitionWindow) -> bool {
if self.count >= N { return false; }
self.windows[self.count] = window;
self.count += 1;
true
}
pub fn clear(&mut self) {
self.count = 0;
}
#[inline]
pub fn len(&self) -> usize {
self.count
}
#[inline]
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn is_suppressed(&self, k: u32) -> bool {
self.windows[..self.count].iter().any(|w| w.is_active(k))
}
pub fn active_window(&self, k: u32) -> Option<&TransitionWindow> {
self.windows[..self.count].iter().find(|w| w.is_active(k))
}
pub fn overlap_count(&self, k: u32) -> usize {
self.windows[..self.count].iter().filter(|w| w.is_active(k)).count()
}
#[inline]
pub fn is_full(&self) -> bool {
self.count >= N
}
#[inline]
pub fn capacity_fraction(&self) -> f32 {
self.count as f32 / N as f32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuppressionDecision {
Active,
Suppressed(TransitionKind),
}
pub fn suppress_escalation<const N: usize>(
k: u32,
schedule: &WaveformSchedule<N>,
) -> SuppressionDecision {
match schedule.active_window(k) {
None => SuppressionDecision::Active,
Some(win) => SuppressionDecision::Suppressed(win.kind),
}
}
#[inline]
pub fn freq_hop_window(start_k: u32, end_k: u32, margin: u32) -> TransitionWindow {
TransitionWindow {
start_k,
end_k,
suppression_margin: margin,
kind: TransitionKind::FrequencyHop,
}
}
#[inline]
pub fn burst_start_window(start_k: u32, preamble_len: u32) -> TransitionWindow {
TransitionWindow {
start_k,
end_k: start_k + preamble_len,
suppression_margin: 0,
kind: TransitionKind::BurstStart,
}
}
#[inline]
pub fn power_change_window(start_k: u32, ramp_duration_k: u32) -> TransitionWindow {
TransitionWindow {
start_k,
end_k: start_k + ramp_duration_k,
suppression_margin: 2,
kind: TransitionKind::PowerLevelChange,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_schedule_is_empty() {
let s = WaveformSchedule::<8>::new();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
}
#[test]
fn add_and_query_window() {
let mut s = WaveformSchedule::<8>::new();
let win = freq_hop_window(100, 105, 3);
assert!(s.add(win));
assert_eq!(s.is_suppressed(100), true);
assert_eq!(s.is_suppressed(108), true);
assert_eq!(s.is_suppressed(99), false);
assert_eq!(s.is_suppressed(109), false);
}
#[test]
fn full_schedule_returns_false_on_add() {
let mut s = WaveformSchedule::<2>::new();
let w = freq_hop_window(0, 5, 0);
assert!(s.add(w));
assert!(s.add(w));
assert!(!s.add(w), "full schedule must reject add");
assert!(s.is_full());
}
#[test]
fn clear_resets_schedule() {
let mut s = WaveformSchedule::<4>::new();
s.add(freq_hop_window(10, 20, 2));
s.add(freq_hop_window(50, 60, 2));
assert_eq!(s.len(), 2);
s.clear();
assert!(s.is_empty());
assert!(!s.is_suppressed(15), "cleared schedule must not suppress");
}
#[test]
fn suppress_escalation_returns_active_when_clear() {
let s = WaveformSchedule::<8>::new();
assert_eq!(suppress_escalation(42, &s), SuppressionDecision::Active);
}
#[test]
fn suppress_escalation_returns_suppressed_in_window() {
let mut s = WaveformSchedule::<8>::new();
s.add(burst_start_window(200, 10));
let dec = suppress_escalation(205, &s);
assert_eq!(dec, SuppressionDecision::Suppressed(TransitionKind::BurstStart));
}
#[test]
fn overlap_count_detects_simultaneous_transitions() {
let mut s = WaveformSchedule::<8>::new();
s.add(freq_hop_window(100, 110, 3));
s.add(power_change_window(105, 8));
assert_eq!(s.overlap_count(107), 2, "should detect 2 overlapping windows");
assert_eq!(s.overlap_count(99), 0, "before all windows");
assert_eq!(s.overlap_count(120), 0, "after all windows");
}
#[test]
fn transition_kind_labels() {
assert_eq!(TransitionKind::FrequencyHop.label(), "FrequencyHop");
assert_eq!(TransitionKind::ModulationChange.label(), "ModulationChange");
assert_eq!(TransitionKind::BurstStart.label(), "BurstStart");
assert_eq!(TransitionKind::ScheduledSlotBoundary.label(), "ScheduledSlotBoundary");
assert_eq!(TransitionKind::Unknown.label(), "Unknown");
}
#[test]
fn requires_margin_correct() {
assert!(TransitionKind::FrequencyHop.requires_margin());
assert!(TransitionKind::ModulationChange.requires_margin());
assert!(!TransitionKind::BurstStart.requires_margin());
assert!(!TransitionKind::PowerLevelChange.requires_margin());
assert!(!TransitionKind::ScheduledSlotBoundary.requires_margin());
}
#[test]
fn window_duration_k_correct() {
let w = TransitionWindow {
start_k: 100, end_k: 110, suppression_margin: 0,
kind: TransitionKind::FrequencyHop,
};
assert_eq!(w.duration_k(), 11); assert_eq!(w.suppression_end(), 110);
}
#[test]
fn window_margin_extends_suppression() {
let w = freq_hop_window(100, 110, 5);
assert!( w.is_active(115), "margin extends to 115");
assert!(!w.is_active(116), "116 is past margin");
}
#[test]
fn capacity_fraction_reports_correctly() {
let mut s = WaveformSchedule::<4>::new();
assert!((s.capacity_fraction() - 0.0).abs() < 1e-5);
s.add(freq_hop_window(0, 5, 0));
assert!((s.capacity_fraction() - 0.25).abs() < 1e-5);
s.add(freq_hop_window(10, 15, 0));
assert!((s.capacity_fraction() - 0.50).abs() < 1e-5);
}
#[test]
fn multiple_windows_distinct_ranges_no_cross_suppression() {
let mut s = WaveformSchedule::<8>::new();
s.add(freq_hop_window(10, 20, 0));
s.add(freq_hop_window(100, 110, 0));
assert!( s.is_suppressed(15));
assert!(!s.is_suppressed(50), "gap between windows is active");
assert!( s.is_suppressed(105));
}
}