use super::menu_scan::{SCAN_1ST_PERIODIC, menu_scan};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)]
pub enum ScanType {
#[default]
Passive,
Event,
IoIntr,
Menu(u16),
}
impl ScanType {
pub const SEC10: Self = Self::Menu(3);
pub const SEC5: Self = Self::Menu(4);
pub const SEC2: Self = Self::Menu(5);
pub const SEC1: Self = Self::Menu(6);
pub const SEC05: Self = Self::Menu(7);
pub const SEC02: Self = Self::Menu(8);
pub const SEC01: Self = Self::Menu(9);
pub fn from_u16(v: u16) -> Self {
match v {
0 => Self::Passive,
1 => Self::Event,
2 => Self::IoIntr,
other => Self::Menu(other),
}
}
pub fn to_u16(self) -> u16 {
match self {
Self::Passive => 0,
Self::Event => 1,
Self::IoIntr => 2,
Self::Menu(v) => v,
}
}
pub fn scan_list(self) -> Option<ScanList> {
ScanList::of(self)
}
pub fn interval(&self) -> Option<std::time::Duration> {
match self {
Self::Menu(v) => menu_scan().period_at(*v),
_ => None,
}
}
pub fn periodic_index(self) -> Option<usize> {
match self {
Self::Menu(v) if menu_scan().period_at(v).is_some() => {
Some((v - SCAN_1ST_PERIODIC) as usize)
}
_ => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct ScanList(ScanType);
impl ScanList {
pub fn of(scan: ScanType) -> Option<Self> {
match scan {
ScanType::Passive => None,
ScanType::Event | ScanType::IoIntr => Some(Self(scan)),
ScanType::Menu(v) => menu_scan().period_at(v).is_some().then_some(Self(scan)),
}
}
pub fn scan(self) -> ScanType {
self.0
}
pub fn count() -> usize {
menu_scan().n_periodic() + (SCAN_1ST_PERIODIC as usize - 1)
}
pub fn slot(self) -> usize {
self.0.to_u16() as usize - 1
}
pub fn all() -> Vec<Self> {
(1..=menu_scan().n_periodic() as u16 + SCAN_1ST_PERIODIC - 1)
.filter_map(|v| Self::of(ScanType::from_u16(v)))
.collect()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SimModeScan(ScanType);
impl Default for SimModeScan {
fn default() -> Self {
Self(ScanType::Menu(Self::DO_NOT_USE))
}
}
impl SimModeScan {
pub const DO_NOT_USE: u16 = 65535;
pub fn from_u16(v: u16) -> Self {
Self(ScanType::from_u16(v))
}
pub fn from_scan(s: ScanType) -> Self {
Self(s)
}
pub fn to_u16(self) -> u16 {
self.0.to_u16()
}
pub fn is_unset(self) -> bool {
self.to_u16() == Self::DO_NOT_USE
}
pub fn scan(self) -> Option<ScanType> {
(!self.is_unset()).then_some(self.0)
}
}
impl std::fmt::Display for SimModeScan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Display for ScanType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match menu_scan().label_at(self.to_u16()) {
Some(label) => write!(f, "{label}"),
None => write!(f, "{}", self.to_u16()),
}
}
}
#[cfg(test)]
mod sim_mode_scan_tests {
use super::*;
#[test]
fn default_is_the_65535_sentinel() {
assert!(SimModeScan::default().is_unset());
assert_eq!(SimModeScan::default().to_u16(), 65535);
assert_eq!(SimModeScan::default().scan(), None);
}
#[test]
fn sentinel_round_trips_through_u16() {
assert!(SimModeScan::from_u16(65535).is_unset());
assert_eq!(SimModeScan::from_u16(65535).to_u16(), 65535);
}
#[test]
fn valid_menu_indices_map_to_scan_choices() {
for v in 0u16..=9 {
assert_eq!(SimModeScan::from_u16(v).scan(), Some(ScanType::from_u16(v)));
assert_eq!(SimModeScan::from_u16(v).to_u16(), v);
assert!(!SimModeScan::from_u16(v).is_unset());
}
}
#[test]
fn labels_match_the_loaded_menu() {
for (i, label) in menu_scan().choices().iter().enumerate() {
assert_eq!(ScanType::from_u16(i as u16).to_string(), *label);
}
}
#[test]
fn an_out_of_menu_index_is_carried_and_is_not_the_sentinel() {
for v in [10u16, 40000] {
let s = SimModeScan::from_u16(v);
assert_eq!(s.to_u16(), v, "the written index is what reads back");
assert!(!s.is_unset(), "{v} is illegal, but it is not USHRT_MAX");
assert_eq!(
s.scan(),
Some(ScanType::Menu(v)),
"C swaps it into SCAN; scanAdd then scans nothing"
);
}
}
#[test]
fn scan_carries_an_illegal_index_instead_of_erasing_it_to_passive() {
assert_eq!(ScanType::from_u16(9), ScanType::SEC01);
assert_eq!(ScanType::from_u16(9).to_u16(), 9);
assert_eq!(ScanType::from_u16(10), ScanType::Menu(10));
assert_eq!(ScanType::from_u16(10).to_u16(), 10);
assert_ne!(ScanType::from_u16(10), ScanType::Passive);
assert_eq!(ScanType::from_u16(65535), ScanType::Menu(65535));
assert_eq!(ScanType::from_u16(65535).to_u16(), 65535);
assert_eq!(ScanType::Menu(10).interval(), None);
assert_ne!(ScanType::Menu(10), ScanType::IoIntr);
}
#[test]
fn only_a_menu_choice_other_than_passive_names_a_scan_list() {
assert_eq!(ScanType::Passive.scan_list(), None);
for v in 1u16..=9 {
let scan = ScanType::from_u16(v);
assert_eq!(
scan.scan_list().map(ScanList::scan),
Some(scan),
"index {v} is a menuScan choice"
);
}
for v in [10u16, 42, 65535] {
assert_eq!(
ScanType::from_u16(v).scan_list(),
None,
"index {v} is outside menuScan; scanAdd adds the record nowhere"
);
}
}
#[test]
fn every_legal_index_round_trips() {
for v in 0u16..=9 {
assert_eq!(ScanType::from_u16(v).to_u16(), v);
}
}
#[test]
fn every_scan_list_has_a_distinct_slot() {
let mut seen = vec![false; ScanList::count()];
for v in 0u16..=9 {
let Some(list) = ScanType::from_u16(v).scan_list() else {
assert_eq!(v, 0, "only Passive names no list inside the menu");
continue;
};
let slot = list.slot();
assert!(slot < ScanList::count(), "slot {slot} out of the table");
assert!(!seen[slot], "slot {slot} claimed twice");
seen[slot] = true;
}
assert!(seen.iter().all(|s| *s), "every slot must be claimed");
for (i, list) in ScanList::all().iter().enumerate() {
assert_eq!(list.slot(), i, "all() must be in slot order");
}
assert_eq!(ScanType::Menu(65535).scan_list(), None);
}
#[test]
fn the_stock_rates_come_back_through_the_loaded_menu() {
use std::time::Duration;
for (scan, period) in [
(ScanType::SEC10, Duration::from_secs(10)),
(ScanType::SEC5, Duration::from_secs(5)),
(ScanType::SEC2, Duration::from_secs(2)),
(ScanType::SEC1, Duration::from_secs(1)),
(ScanType::SEC05, Duration::from_millis(500)),
(ScanType::SEC02, Duration::from_millis(200)),
(ScanType::SEC01, Duration::from_millis(100)),
] {
assert_eq!(scan.interval(), Some(period), "{scan}");
}
assert_eq!(ScanType::Passive.interval(), None);
assert_eq!(ScanType::Event.interval(), None);
assert_eq!(ScanType::IoIntr.interval(), None);
}
}