pub mod gps_relay;
pub mod mcu006;
pub mod motion;
pub mod scx20;
pub mod wmm;
use std::str::FromStr;
use std::time::Instant;
use crate::engine::quirk::Target;
use crate::engine::{DecodedPgn, RawFrame};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum QuirkKind {
Scx20,
Wmm,
Motion,
Mcu006,
GpsRollover(Target),
GpsRelay,
}
impl QuirkKind {
pub fn name(&self) -> &'static str {
match self {
QuirkKind::Scx20 => "scx20",
QuirkKind::Wmm => "wmm",
QuirkKind::Motion => "motion",
QuirkKind::Mcu006 => "mcu006",
QuirkKind::GpsRollover(_) => "gps-rollover",
QuirkKind::GpsRelay => "gps-relay",
}
}
pub fn gps_rollover_target(kinds: &[QuirkKind]) -> Option<&Target> {
kinds.iter().find_map(|k| match k {
QuirkKind::GpsRollover(target) => Some(target),
_ => None,
})
}
pub fn gps_relay_has_devices(kinds: &[QuirkKind]) -> bool {
matches!(Self::gps_rollover_target(kinds), Some(Target::Devices(d)) if !d.is_empty())
}
}
impl FromStr for QuirkKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, args) = match s.split_once('=') {
Some((n, a)) => (n.trim(), Some(a)),
None => (s.trim(), None),
};
let no_args = |kind: QuirkKind| match args {
None => Ok(kind),
Some(_) => Err(format!("--quirk {name} takes no argument")),
};
match name {
"scx20" => no_args(QuirkKind::Scx20),
"wmm" => no_args(QuirkKind::Wmm),
"motion" => no_args(QuirkKind::Motion),
"mcu006" => no_args(QuirkKind::Mcu006),
"gps-rollover" => Target::parse(args).map(QuirkKind::GpsRollover),
"gps-relay" => no_args(QuirkKind::GpsRelay),
_ => Err(format!(
"unknown quirk '{name}'; the quirks are: scx20, wmm, motion, mcu006, gps-rollover, gps-relay"
)),
}
}
}
pub struct Quirks {
scx20: Option<scx20::Scx20>,
wmm: Option<wmm::WmmQuirk>,
motion: Option<motion::Motion>,
mcu006: Option<mcu006::Mcu006>,
gps_relay: Option<gps_relay::GpsRelay>,
}
impl Quirks {
pub fn new(kinds: Vec<QuirkKind>, state_dir: Option<&std::path::Path>) -> Self {
match QuirkKind::gps_rollover_target(&kinds) {
Some(target) => crate::engine::quirk::enable_gps_rollover(target.clone()),
None => crate::engine::quirk::disable_gps_rollover(),
}
Self {
scx20: kinds.contains(&QuirkKind::Scx20).then(scx20::Scx20::new),
wmm: kinds.contains(&QuirkKind::Wmm).then(wmm::WmmQuirk::new),
motion: kinds
.contains(&QuirkKind::Motion)
.then(|| motion::Motion::new(state_dir)),
mcu006: kinds
.contains(&QuirkKind::Mcu006)
.then(|| mcu006::Mcu006::new(state_dir)),
gps_relay: kinds
.contains(&QuirkKind::GpsRelay)
.then(gps_relay::GpsRelay::new),
}
}
pub fn set_mcu006_link(&mut self, link: mcu006::Link) {
if let Some(m) = self.mcu006.as_mut() {
m.set_link(link);
}
}
pub fn set_txt(&mut self, txt: crate::server::mdns::Txt) {
if let Some(m) = self.motion.as_mut() {
m.set_txt(&txt);
}
if let Some(m) = self.mcu006.as_mut() {
m.set_txt(txt);
}
}
pub fn is_enabled(&self) -> bool {
self.scx20.is_some()
|| self.wmm.is_some()
|| self.motion.is_some()
|| self.mcu006.is_some()
|| self.gps_relay.is_some()
}
pub fn process_decoded(&mut self, decoded: &DecodedPgn, own_addr: Option<u8>) -> Vec<RawFrame> {
let now = Instant::now();
let mut out = Vec::new();
if let Some(scx20) = self.scx20.as_mut() {
out.extend(scx20.process(decoded, now));
}
if let Some(wmm) = self.wmm.as_mut() {
out.extend(wmm.process(decoded, now));
}
if let Some(motion) = self.motion.as_mut() {
out.extend(motion.process(decoded, now));
}
if let Some(mcu006) = self.mcu006.as_mut() {
let mut frames = mcu006.process(decoded, now);
stamp(&mut frames);
out.extend(frames);
}
if let Some(relay) = self.gps_relay.as_mut() {
out.extend(relay.process(decoded, own_addr));
}
out
}
}
fn stamp(frames: &mut [RawFrame]) {
if frames.iter().all(|f| f.timestamp.is_some()) {
return;
}
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let ts = crate::engine::format_iso_ms(ms);
for f in frames.iter_mut().filter(|f| f.timestamp.is_none()) {
f.timestamp = Some(ts.clone());
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
static SWITCH: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub(crate) fn switch_guard() -> std::sync::MutexGuard<'static, ()> {
SWITCH.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn parses_every_name() {
assert_eq!("gps-relay".parse::<QuirkKind>(), Ok(QuirkKind::GpsRelay));
assert!("gps-relay=4".parse::<QuirkKind>().is_err());
assert_eq!("mcu006".parse::<QuirkKind>(), Ok(QuirkKind::Mcu006));
assert_eq!(
"gps-rollover=4".parse::<QuirkKind>(),
Ok(QuirkKind::GpsRollover(Target::Devices(vec![
crate::engine::quirk::Device::Address(4)
])))
);
assert!("gps-rollover=lots".parse::<QuirkKind>().is_err());
assert!("nope".parse::<QuirkKind>().is_err());
}
#[test]
fn relay_needs_a_device_list() {
let devices = "gps-rollover=4".parse::<QuirkKind>().unwrap();
assert!(QuirkKind::gps_relay_has_devices(&[
QuirkKind::GpsRelay,
devices.clone()
]));
for kinds in [
vec![QuirkKind::GpsRelay],
vec![QuirkKind::GpsRelay, QuirkKind::GpsRollover(Target::Gnss)],
vec![QuirkKind::GpsRelay, QuirkKind::GpsRollover(Target::All)],
vec![
QuirkKind::GpsRelay,
QuirkKind::GpsRollover(Target::Gnss),
devices.clone(),
],
] {
assert!(!QuirkKind::gps_relay_has_devices(&kinds), "{kinds:?}");
}
}
#[test]
fn enabled_reflects_configured_quirks() {
let _guard = switch_guard();
assert!(!Quirks::new(vec![], None).is_enabled());
assert!(Quirks::new(vec![QuirkKind::Scx20], None).is_enabled());
assert!(Quirks::new(vec![QuirkKind::Wmm], None).is_enabled());
let both = Quirks::new(vec![QuirkKind::Scx20, QuirkKind::Wmm], None);
assert!(both.is_enabled());
assert!(both.scx20.is_some() && both.wmm.is_some());
}
#[test]
fn gps_rollover_does_not_leak_into_the_next_pipeline() {
let _guard = switch_guard();
let _first = Quirks::new(vec![QuirkKind::GpsRollover(Target::Gnss)], None);
assert!(crate::engine::quirk::gps_rollover_reference_day().is_some());
let _second = Quirks::new(vec![QuirkKind::Wmm], None);
assert!(crate::engine::quirk::gps_rollover_reference_day().is_none());
}
}