1use crate::round::div_nearest_ties_away;
11
12mod sealed {
13 pub trait Sealed {}
14}
15
16pub trait TemporalSample: sealed::Sealed + Copy + Ord {
35 #[doc(hidden)]
37 const ZERO: Self;
38 #[doc(hidden)]
40 const MAX_WINDOW: usize;
41
42 #[doc(hidden)]
44 fn to_i64(self) -> i64;
45 #[doc(hidden)]
47 fn from_i64(value: i64) -> Self;
48}
49
50impl sealed::Sealed for u16 {}
51
52impl TemporalSample for u16 {
53 const ZERO: Self = 0;
54 const MAX_WINDOW: usize = if usize::BITS > 32 {
55 (i64::MAX / u16::MAX as i64) as usize
56 } else {
57 usize::MAX
58 };
59
60 fn to_i64(self) -> i64 {
61 i64::from(self)
62 }
63
64 fn from_i64(value: i64) -> Self {
65 debug_assert!((0..=i64::from(u16::MAX)).contains(&value));
66 value as u16
67 }
68}
69
70impl sealed::Sealed for i32 {}
71
72impl TemporalSample for i32 {
73 const ZERO: Self = 0;
74 const MAX_WINDOW: usize = if usize::BITS > 32 {
75 (i64::MAX / 2_147_483_648) as usize
76 } else {
77 usize::MAX
78 };
79
80 fn to_i64(self) -> i64 {
81 i64::from(self)
82 }
83
84 fn from_i64(value: i64) -> Self {
85 debug_assert!((i64::from(i32::MIN)..=i64::from(i32::MAX)).contains(&value));
86 value as i32
87 }
88}
89
90impl sealed::Sealed for u32 {}
91
92impl TemporalSample for u32 {
93 const ZERO: Self = 0;
94 const MAX_WINDOW: usize = if usize::BITS < 32 {
98 usize::MAX
99 } else {
100 (i64::MAX / u32::MAX as i64) as usize
101 };
102
103 fn to_i64(self) -> i64 {
104 i64::from(self)
105 }
106
107 fn from_i64(value: i64) -> Self {
108 debug_assert!((0..=i64::from(u32::MAX)).contains(&value));
109 value as u32
110 }
111}
112
113#[cfg(target_pointer_width = "16")]
116const _: () = assert!(<u32 as TemporalSample>::MAX_WINDOW == usize::MAX);
117
118#[derive(Copy, Clone, Debug, Eq, PartialEq)]
120pub enum FilterOutput<T> {
121 WarmingUp {
123 samples: usize,
125 required: usize,
127 },
128 Ready(T),
130}
131
132impl<T> FilterOutput<T> {
133 pub fn ready(self) -> Option<T> {
135 match self {
136 Self::WarmingUp { .. } => None,
137 Self::Ready(value) => Some(value),
138 }
139 }
140}
141
142pub trait TemporalFilter<T> {
144 fn update(&mut self, value: T) -> FilterOutput<T>;
146 fn reset(&mut self);
148}
149
150#[derive(Clone, Debug)]
160pub struct MovingAverage<T: TemporalSample, const N: usize> {
161 samples: [T; N],
162 sum: i64,
163 next: usize,
164 len: usize,
165}
166
167impl<T: TemporalSample, const N: usize> MovingAverage<T, N> {
168 pub const fn new() -> Self {
176 assert!(N > 0);
177 assert!(N <= T::MAX_WINDOW);
178 Self {
179 samples: [T::ZERO; N],
180 sum: 0,
181 next: 0,
182 len: 0,
183 }
184 }
185
186 pub const fn len(&self) -> usize {
188 self.len
189 }
190
191 pub const fn is_empty(&self) -> bool {
193 self.len == 0
194 }
195}
196
197impl<T: TemporalSample, const N: usize> Default for MovingAverage<T, N> {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203impl<T: TemporalSample, const N: usize> TemporalFilter<T> for MovingAverage<T, N> {
204 fn update(&mut self, value: T) -> FilterOutput<T> {
205 if self.len == N {
206 self.sum -= self.samples[self.next].to_i64();
207 } else {
208 self.len += 1;
209 }
210
211 self.samples[self.next] = value;
212 self.sum += value.to_i64();
213 self.next += 1;
214 if self.next == N {
215 self.next = 0;
216 }
217
218 if self.len < N {
219 FilterOutput::WarmingUp {
220 samples: self.len,
221 required: N,
222 }
223 } else {
224 FilterOutput::Ready(T::from_i64(div_nearest_ties_away(self.sum, N as i64)))
225 }
226 }
227
228 fn reset(&mut self) {
229 self.samples = [T::ZERO; N];
230 self.sum = 0;
231 self.next = 0;
232 self.len = 0;
233 }
234}
235
236#[derive(Clone, Debug)]
241pub struct MedianFilter<T: TemporalSample, const N: usize> {
242 samples: [T; N],
243 next: usize,
244 len: usize,
245}
246
247impl<T: TemporalSample, const N: usize> MedianFilter<T, N> {
248 pub const fn new() -> Self {
254 assert!(N > 0 && N % 2 == 1);
255 Self {
256 samples: [T::ZERO; N],
257 next: 0,
258 len: 0,
259 }
260 }
261
262 pub const fn len(&self) -> usize {
264 self.len
265 }
266
267 pub const fn is_empty(&self) -> bool {
269 self.len == 0
270 }
271}
272
273impl<T: TemporalSample, const N: usize> Default for MedianFilter<T, N> {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279impl<T: TemporalSample, const N: usize> TemporalFilter<T> for MedianFilter<T, N> {
280 fn update(&mut self, value: T) -> FilterOutput<T> {
281 self.samples[self.next] = value;
282 self.next += 1;
283 if self.next == N {
284 self.next = 0;
285 }
286 if self.len < N {
287 self.len += 1;
288 }
289 if self.len < N {
290 return FilterOutput::WarmingUp {
291 samples: self.len,
292 required: N,
293 };
294 }
295
296 let mut sorted = self.samples;
297 let mut index = 1;
298 while index < N {
299 let value = sorted[index];
300 let mut insert = index;
301 while insert > 0 && sorted[insert - 1] > value {
302 sorted[insert] = sorted[insert - 1];
303 insert -= 1;
304 }
305 sorted[insert] = value;
306 index += 1;
307 }
308 FilterOutput::Ready(sorted[N / 2])
309 }
310
311 fn reset(&mut self) {
312 self.samples = [T::ZERO; N];
313 self.next = 0;
314 self.len = 0;
315 }
316}
317
318#[derive(Copy, Clone, Debug)]
330pub struct ExponentialSmoother<T: TemporalSample> {
331 alpha: u16,
332 value: T,
333 initialized: bool,
334}
335
336impl<T: TemporalSample> ExponentialSmoother<T> {
337 pub const fn new(alpha: u16) -> Self {
339 Self {
340 alpha,
341 value: T::ZERO,
342 initialized: false,
343 }
344 }
345
346 pub const fn alpha(&self) -> u16 {
348 self.alpha
349 }
350
351 pub const fn value(&self) -> Option<T> {
353 if self.initialized {
354 Some(self.value)
355 } else {
356 None
357 }
358 }
359}
360
361impl<T: TemporalSample> TemporalFilter<T> for ExponentialSmoother<T> {
362 fn update(&mut self, value: T) -> FilterOutput<T> {
363 if !self.initialized {
364 self.value = value;
365 self.initialized = true;
366 return FilterOutput::Ready(value);
367 }
368
369 let current = self.value.to_i64();
370 let delta = value.to_i64() - current;
371 let adjustment = div_nearest_ties_away(delta * i64::from(self.alpha), i64::from(u16::MAX));
372 self.value = T::from_i64(current + adjustment);
373 FilterOutput::Ready(self.value)
374 }
375
376 fn reset(&mut self) {
377 self.value = T::ZERO;
378 self.initialized = false;
379 }
380}
381
382#[derive(Copy, Clone, Debug, Eq, PartialEq)]
384pub enum Stability<T> {
385 WarmingUp {
387 samples: usize,
389 required: usize,
391 },
392 Unstable {
394 minimum: T,
396 maximum: T,
398 span: u64,
400 },
401 Stable {
403 minimum: T,
405 maximum: T,
407 span: u64,
409 },
410}
411
412#[derive(Clone, Debug)]
417pub struct StabilityDetector<T: TemporalSample, const N: usize> {
418 samples: [T; N],
419 threshold: u64,
420 next: usize,
421 len: usize,
422}
423
424impl<T: TemporalSample, const N: usize> StabilityDetector<T, N> {
425 pub const fn new(threshold: u64) -> Self {
432 assert!(N > 0);
433 Self {
434 samples: [T::ZERO; N],
435 threshold,
436 next: 0,
437 len: 0,
438 }
439 }
440
441 pub const fn threshold(&self) -> u64 {
443 self.threshold
444 }
445
446 pub const fn len(&self) -> usize {
448 self.len
449 }
450
451 pub const fn is_empty(&self) -> bool {
453 self.len == 0
454 }
455
456 pub fn update(&mut self, value: T) -> Stability<T> {
458 self.samples[self.next] = value;
459 self.next += 1;
460 if self.next == N {
461 self.next = 0;
462 }
463 if self.len < N {
464 self.len += 1;
465 }
466 if self.len < N {
467 return Stability::WarmingUp {
468 samples: self.len,
469 required: N,
470 };
471 }
472
473 let mut minimum = self.samples[0];
474 let mut maximum = self.samples[0];
475 let mut index = 1;
476 while index < N {
477 minimum = minimum.min(self.samples[index]);
478 maximum = maximum.max(self.samples[index]);
479 index += 1;
480 }
481 let span = (maximum.to_i64() - minimum.to_i64()) as u64;
482 if span <= self.threshold {
483 Stability::Stable {
484 minimum,
485 maximum,
486 span,
487 }
488 } else {
489 Stability::Unstable {
490 minimum,
491 maximum,
492 span,
493 }
494 }
495 }
496
497 pub fn reset(&mut self) {
499 self.samples = [T::ZERO; N];
500 self.next = 0;
501 self.len = 0;
502 }
503}
504
505#[derive(Copy, Clone, Debug, Eq, PartialEq)]
513pub struct Hysteresis<T: TemporalSample> {
514 low: T,
515 high: T,
516 latched: bool,
517 initial: bool,
518}
519
520impl Hysteresis<i32> {
521 pub const fn new(low: i32, high: i32) -> Self {
527 assert!(low <= high);
528 Self {
529 low,
530 high,
531 latched: false,
532 initial: false,
533 }
534 }
535}
536
537impl Hysteresis<u16> {
538 pub const fn new(low: u16, high: u16) -> Self {
544 assert!(low <= high);
545 Self {
546 low,
547 high,
548 latched: false,
549 initial: false,
550 }
551 }
552}
553
554impl Hysteresis<u32> {
555 pub const fn new(low: u32, high: u32) -> Self {
561 assert!(low <= high);
562 Self {
563 low,
564 high,
565 latched: false,
566 initial: false,
567 }
568 }
569}
570
571impl<T: TemporalSample> Hysteresis<T> {
572 pub const fn with_initial(mut self, on: bool) -> Self {
575 self.latched = on;
576 self.initial = on;
577 self
578 }
579
580 pub const fn low(&self) -> T {
582 self.low
583 }
584
585 pub const fn high(&self) -> T {
587 self.high
588 }
589
590 pub fn update(&mut self, value: T) -> bool {
596 if value >= self.high {
597 self.latched = true;
598 } else if value <= self.low {
599 self.latched = false;
600 }
601 self.latched
602 }
603
604 pub const fn state(&self) -> bool {
606 self.latched
607 }
608
609 pub fn reset(&mut self) {
612 self.latched = self.initial;
613 }
614}
615
616#[derive(Copy, Clone, Debug, Eq, PartialEq)]
623pub enum DebounceOutput {
624 WarmingUp {
627 streak: usize,
629 required: usize,
631 },
632 Steady(bool),
634 Edge {
636 level: bool,
638 },
639}
640
641#[derive(Copy, Clone, Debug, Eq, PartialEq)]
648pub struct Debounce<const N: usize> {
649 candidate: bool,
650 streak: usize,
651 latched: bool,
652 armed: bool,
653}
654
655impl<const N: usize> Debounce<N> {
656 pub const fn new() -> Self {
662 assert!(N > 0);
663 Self {
664 candidate: false,
665 streak: 0,
666 latched: false,
667 armed: false,
668 }
669 }
670
671 pub fn update(&mut self, sample: bool) -> DebounceOutput {
673 if self.streak == 0 || sample != self.candidate {
674 self.candidate = sample;
675 self.streak = 1;
676 } else if self.streak < N {
677 self.streak += 1;
678 }
679
680 if self.streak < N {
681 if self.armed {
682 DebounceOutput::Steady(self.latched)
683 } else {
684 DebounceOutput::WarmingUp {
685 streak: self.streak,
686 required: N,
687 }
688 }
689 } else if !self.armed {
690 self.armed = true;
691 self.latched = self.candidate;
692 DebounceOutput::Edge {
693 level: self.latched,
694 }
695 } else if self.candidate != self.latched {
696 self.latched = self.candidate;
697 DebounceOutput::Edge {
698 level: self.latched,
699 }
700 } else {
701 DebounceOutput::Steady(self.latched)
702 }
703 }
704
705 pub const fn state(&self) -> Option<bool> {
708 if self.armed { Some(self.latched) } else { None }
709 }
710
711 pub fn reset(&mut self) {
713 self.candidate = false;
714 self.streak = 0;
715 self.latched = false;
716 self.armed = false;
717 }
718}
719
720impl<const N: usize> Default for Debounce<N> {
721 fn default() -> Self {
722 Self::new()
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 extern crate std;
729
730 use super::*;
731
732 #[test]
733 fn moving_average_warms_up_and_rolls() {
734 let mut filter = MovingAverage::<i32, 3>::new();
735 assert_eq!(
736 filter.update(3),
737 FilterOutput::WarmingUp {
738 samples: 1,
739 required: 3
740 }
741 );
742 assert_eq!(filter.update(6).ready(), None);
743 assert_eq!(filter.update(9), FilterOutput::Ready(6));
744 assert_eq!(filter.update(12), FilterOutput::Ready(9));
745 }
746
747 #[test]
748 fn moving_average_rounds_signed_ties_away() {
749 let mut positive = MovingAverage::<i32, 2>::new();
750 positive.update(0);
751 assert_eq!(positive.update(1), FilterOutput::Ready(1));
752
753 let mut negative = MovingAverage::<i32, 2>::new();
754 negative.update(0);
755 assert_eq!(negative.update(-1), FilterOutput::Ready(-1));
756 }
757
758 #[test]
759 fn moving_average_handles_integer_extremes() {
760 let mut signed = MovingAverage::<i32, 2>::new();
761 signed.update(i32::MIN);
762 assert_eq!(signed.update(i32::MAX), FilterOutput::Ready(-1));
763
764 let mut unsigned = MovingAverage::<u16, 2>::new();
765 unsigned.update(0);
766 assert_eq!(unsigned.update(u16::MAX), FilterOutput::Ready(32_768));
767 }
768
769 #[test]
770 fn median_rejects_isolated_spike() {
771 let mut filter = MedianFilter::<u16, 5>::new();
772 for value in [1000, 1001, 4095, 999] {
773 assert!(filter.update(value).ready().is_none());
774 }
775 assert_eq!(filter.update(1002), FilterOutput::Ready(1001));
776 }
777
778 #[test]
779 fn exponential_smoother_has_explicit_step_response() {
780 let mut filter = ExponentialSmoother::<i32>::new(32_768);
781 assert_eq!(filter.update(0), FilterOutput::Ready(0));
782 assert_eq!(filter.update(1000), FilterOutput::Ready(500));
783 assert_eq!(filter.update(1000), FilterOutput::Ready(750));
784 assert_eq!(filter.value(), Some(750));
785 }
786
787 #[test]
788 fn exponential_smoother_quantization_floor_ignores_small_steps() {
789 let mut filter = ExponentialSmoother::<i32>::new(100);
790 filter.update(0);
791 assert_eq!(filter.update(327), FilterOutput::Ready(0));
793 assert_eq!(filter.update(328), FilterOutput::Ready(1));
795 }
796
797 #[test]
798 fn reset_restores_warmup_or_uninitialized_state() {
799 let mut average = MovingAverage::<u16, 2>::new();
800 average.update(10);
801 average.update(20);
802 average.reset();
803 assert!(average.is_empty());
804 assert!(average.update(30).ready().is_none());
805
806 let mut exponential = ExponentialSmoother::<i32>::new(1000);
807 exponential.update(42);
808 exponential.reset();
809 assert_eq!(exponential.value(), None);
810 assert_eq!(exponential.update(-7), FilterOutput::Ready(-7));
811 }
812
813 #[test]
814 fn detector_distinguishes_warm_stable_and_unstable() {
815 let mut detector = StabilityDetector::<i32, 3>::new(4);
816 assert!(matches!(detector.update(100), Stability::WarmingUp { .. }));
817 assert!(matches!(detector.update(102), Stability::WarmingUp { .. }));
818 assert_eq!(
819 detector.update(104),
820 Stability::Stable {
821 minimum: 100,
822 maximum: 104,
823 span: 4
824 }
825 );
826 assert_eq!(
827 detector.update(110),
828 Stability::Unstable {
829 minimum: 102,
830 maximum: 110,
831 span: 8
832 }
833 );
834 }
835
836 #[test]
837 fn detector_span_handles_full_i32_range() {
838 let mut detector = StabilityDetector::<i32, 2>::new(u64::MAX);
839 detector.update(i32::MIN);
840 assert_eq!(
841 detector.update(i32::MAX),
842 Stability::Stable {
843 minimum: i32::MIN,
844 maximum: i32::MAX,
845 span: u64::from(u32::MAX)
846 }
847 );
848 }
849
850 #[test]
851 fn invalid_window_sizes_are_rejected() {
852 assert!(std::panic::catch_unwind(MovingAverage::<i32, 0>::new).is_err());
853 assert!(std::panic::catch_unwind(MedianFilter::<i32, 2>::new).is_err());
854 assert!(std::panic::catch_unwind(|| StabilityDetector::<i32, 0>::new(0)).is_err());
855 }
856
857 #[test]
858 fn hysteresis_latches_with_hold_band() {
859 let mut hyst = Hysteresis::<i32>::new(10, 20);
860 assert!(!hyst.update(15));
861 assert!(hyst.update(20));
862 assert!(hyst.update(15));
863 assert!(!hyst.update(10));
864 assert!(!hyst.update(15));
865 }
866
867 #[test]
868 fn hysteresis_equal_thresholds_are_simple_threshold() {
869 let mut hyst = Hysteresis::<u16>::new(100, 100);
870 assert!(!hyst.update(99));
871 assert!(hyst.update(100));
873 assert!(hyst.update(100));
874 assert!(!hyst.update(99));
875 }
876
877 #[test]
878 fn hysteresis_with_initial_and_reset() {
879 let mut hyst = Hysteresis::<i32>::new(-5, 5).with_initial(true);
880 assert!(hyst.state());
881 assert!(hyst.update(0));
882 assert!(!hyst.update(-5));
883 hyst.reset();
884 assert!(hyst.state());
885 }
886
887 #[test]
888 fn hysteresis_rejects_inverted_band() {
889 assert!(std::panic::catch_unwind(|| Hysteresis::<i32>::new(2, 1)).is_err());
890 assert!(std::panic::catch_unwind(|| Hysteresis::<u16>::new(2, 1)).is_err());
891 }
892
893 #[test]
894 fn debounce_warms_up_then_edges_on_change() {
895 let mut deb = Debounce::<3>::new();
896 assert_eq!(
897 deb.update(true),
898 DebounceOutput::WarmingUp {
899 streak: 1,
900 required: 3
901 }
902 );
903 assert_eq!(
904 deb.update(true),
905 DebounceOutput::WarmingUp {
906 streak: 2,
907 required: 3
908 }
909 );
910 assert_eq!(deb.update(true), DebounceOutput::Edge { level: true });
911 assert_eq!(deb.state(), Some(true));
912 assert_eq!(deb.update(true), DebounceOutput::Steady(true));
913 assert_eq!(deb.update(false), DebounceOutput::Steady(true));
914 assert_eq!(deb.update(false), DebounceOutput::Steady(true));
915 assert_eq!(deb.update(false), DebounceOutput::Edge { level: false });
916 assert_eq!(deb.update(false), DebounceOutput::Steady(false));
917 }
918
919 #[test]
920 fn debounce_candidate_flip_resets_streak() {
921 let mut deb = Debounce::<3>::new();
922 assert!(matches!(
923 deb.update(true),
924 DebounceOutput::WarmingUp { streak: 1, .. }
925 ));
926 assert!(matches!(
927 deb.update(false),
928 DebounceOutput::WarmingUp { streak: 1, .. }
929 ));
930 assert!(matches!(
931 deb.update(false),
932 DebounceOutput::WarmingUp { streak: 2, .. }
933 ));
934 assert_eq!(deb.update(false), DebounceOutput::Edge { level: false });
935 }
936
937 #[test]
938 fn debounce_n_one_is_passthrough_with_edges() {
939 let mut deb = Debounce::<1>::new();
940 assert_eq!(deb.update(false), DebounceOutput::Edge { level: false });
941 assert_eq!(deb.update(false), DebounceOutput::Steady(false));
942 assert_eq!(deb.update(true), DebounceOutput::Edge { level: true });
943 assert_eq!(deb.update(true), DebounceOutput::Steady(true));
944 }
945
946 #[test]
947 fn debounce_reset_returns_to_warmup() {
948 let mut deb = Debounce::<2>::new();
949 deb.update(true);
950 deb.update(true);
951 assert_eq!(deb.state(), Some(true));
952 deb.reset();
953 assert_eq!(deb.state(), None);
954 assert!(matches!(
955 deb.update(false),
956 DebounceOutput::WarmingUp { .. }
957 ));
958 }
959
960 #[test]
961 fn debounce_rejects_zero_window() {
962 assert!(std::panic::catch_unwind(Debounce::<0>::new).is_err());
963 }
964
965 #[test]
966 fn hysteresis_into_debounce_composition() {
967 let mut hyst = Hysteresis::<i32>::new(10, 20);
968 let mut deb = Debounce::<2>::new();
969 let mut edges = 0u8;
970 for sample in [0, 25, 25, 15, 5, 5, 5] {
971 let level = hyst.update(sample);
972 if matches!(deb.update(level), DebounceOutput::Edge { .. }) {
973 edges += 1;
974 }
975 }
976 assert_eq!(edges, 2);
977 assert_eq!(deb.state(), Some(false));
978 }
979
980 #[test]
981 fn u32_moving_average_warms_up_rolls_and_resets() {
982 let mut filter = MovingAverage::<u32, 3>::new();
983 assert!(filter.is_empty());
984 assert_eq!(
985 filter.update(3),
986 FilterOutput::WarmingUp {
987 samples: 1,
988 required: 3
989 }
990 );
991 assert_eq!(filter.len(), 1);
992 assert_eq!(filter.update(6).ready(), None);
993 assert_eq!(filter.update(9), FilterOutput::Ready(6));
994 assert_eq!(filter.len(), 3);
995 assert_eq!(filter.update(12), FilterOutput::Ready(9));
996 filter.reset();
997 assert!(filter.is_empty());
998 assert_eq!(filter.len(), 0);
999 assert!(filter.update(30).ready().is_none());
1000 }
1001
1002 #[test]
1003 fn u32_moving_average_handles_zero_max_and_ties() {
1004 let mut filter = MovingAverage::<u32, 2>::new();
1005 filter.update(0);
1006 assert_eq!(filter.update(u32::MAX), FilterOutput::Ready(2_147_483_648));
1007
1008 let mut both_max = MovingAverage::<u32, 2>::new();
1009 both_max.update(u32::MAX);
1010 assert_eq!(both_max.update(u32::MAX), FilterOutput::Ready(u32::MAX));
1011
1012 let mut zeros = MovingAverage::<u32, 2>::new();
1013 zeros.update(0);
1014 assert_eq!(zeros.update(0), FilterOutput::Ready(0));
1015 }
1016
1017 #[test]
1018 fn u32_moving_average_window_bound_is_accumulator_limited() {
1019 const MAX: usize = <u32 as TemporalSample>::MAX_WINDOW;
1020 assert_eq!(MAX, 2_147_483_648);
1021 assert_eq!(MAX as i64, i64::MAX / i64::from(u32::MAX));
1022 assert!((MAX as i64).checked_mul(i64::from(u32::MAX)).is_some());
1023 assert!(
1024 ((MAX as i64) + 1)
1025 .checked_mul(i64::from(u32::MAX))
1026 .is_none()
1027 );
1028 }
1029
1030 #[test]
1031 fn u32_median_covers_full_unsigned_width() {
1032 let mut filter = MedianFilter::<u32, 3>::new();
1033 assert!(filter.update(0).ready().is_none());
1034 assert!(filter.update(u32::MAX).ready().is_none());
1035 assert_eq!(filter.update(1), FilterOutput::Ready(1));
1036 assert_eq!(filter.update(u32::MAX), FilterOutput::Ready(u32::MAX));
1037 }
1038
1039 #[test]
1040 fn u32_exponential_smoother_extreme_transitions() {
1041 let mut up = ExponentialSmoother::<u32>::new(u16::MAX);
1042 assert_eq!(up.update(0), FilterOutput::Ready(0));
1043 assert_eq!(up.update(u32::MAX), FilterOutput::Ready(u32::MAX));
1044
1045 let mut down = ExponentialSmoother::<u32>::new(u16::MAX);
1046 assert_eq!(down.update(u32::MAX), FilterOutput::Ready(u32::MAX));
1047 assert_eq!(down.update(0), FilterOutput::Ready(0));
1048 }
1049
1050 #[test]
1051 fn u32_detector_span_covers_full_unsigned_range() {
1052 let mut detector = StabilityDetector::<u32, 2>::new(u64::MAX);
1053 detector.update(0);
1054 assert_eq!(
1055 detector.update(u32::MAX),
1056 Stability::Stable {
1057 minimum: 0,
1058 maximum: u32::MAX,
1059 span: u64::from(u32::MAX)
1060 }
1061 );
1062 }
1063
1064 #[test]
1065 fn u32_hysteresis_latches_at_unsigned_boundaries() {
1066 let mut hyst = Hysteresis::<u32>::new(0, u32::MAX);
1067 assert!(!hyst.update(1));
1068 assert!(hyst.update(u32::MAX));
1069 assert!(hyst.update(1));
1070 assert!(!hyst.update(0));
1071 assert!(!hyst.update(1));
1072 }
1073
1074 #[test]
1075 fn u32_hysteresis_equal_thresholds_at_boundaries() {
1076 let mut at_zero = Hysteresis::<u32>::new(0, 0);
1077 assert!(at_zero.update(0));
1078
1079 let mut at_max = Hysteresis::<u32>::new(u32::MAX, u32::MAX);
1080 assert!(!at_max.update(u32::MAX - 1));
1081 assert!(at_max.update(u32::MAX));
1082 assert!(at_max.update(u32::MAX));
1083 assert!(!at_max.update(u32::MAX - 1));
1084 }
1085
1086 #[test]
1087 fn u32_hysteresis_rejects_inverted_band() {
1088 assert!(std::panic::catch_unwind(|| Hysteresis::<u32>::new(2, 1)).is_err());
1089 assert!(std::panic::catch_unwind(|| Hysteresis::<u32>::new(u32::MAX, 0)).is_err());
1090 }
1091}