#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SstvMode {
Pd120,
Pd180,
Pd240,
Robot24,
Robot36,
Robot72,
Scottie1,
Scottie2,
ScottieDx,
Martin1,
Martin2,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ModeSpec {
pub mode: SstvMode,
pub short_name: &'static str,
pub name: &'static str,
pub vis_code: u8,
pub line_pixels: u32,
pub image_lines: u32,
pub line_seconds: f64,
pub sync_seconds: f64,
pub porch_seconds: f64,
pub pixel_seconds: f64,
pub septr_seconds: f64,
pub channel_layout: ChannelLayout,
pub sync_position: SyncPosition,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ChannelLayout {
PdYcbcr,
RobotYuv,
RgbSequential,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SyncPosition {
LineStart,
Scottie,
}
impl ModeSpec {
#[must_use]
pub(crate) fn skip_correction_seconds(&self) -> f64 {
match self.sync_position {
SyncPosition::LineStart => 0.0,
SyncPosition::Scottie => {
let chan_len = f64::from(self.line_pixels) * self.pixel_seconds;
-chan_len / 2.0 + 2.0 * self.porch_seconds
}
}
}
}
#[must_use]
pub fn lookup(vis_code: u8) -> Option<ModeSpec> {
ALL_SPECS.iter().find(|s| s.vis_code == vis_code).copied()
}
#[must_use]
pub fn for_mode(mode: SstvMode) -> ModeSpec {
match mode {
SstvMode::Pd120 => PD120,
SstvMode::Pd180 => PD180,
SstvMode::Pd240 => PD240,
SstvMode::Robot24 => ROBOT24,
SstvMode::Robot36 => ROBOT36,
SstvMode::Robot72 => ROBOT72,
SstvMode::Scottie1 => SCOTTIE1,
SstvMode::Scottie2 => SCOTTIE2,
SstvMode::ScottieDx => SCOTTIE_DX,
SstvMode::Martin1 => MARTIN1,
SstvMode::Martin2 => MARTIN2,
}
}
const PD120: ModeSpec = ModeSpec {
mode: SstvMode::Pd120,
short_name: "pd120",
name: "PD-120",
vis_code: 0x5F,
line_pixels: 640,
image_lines: 496,
line_seconds: 0.508_48,
sync_seconds: 0.020,
porch_seconds: 0.002_08,
pixel_seconds: 0.000_19,
septr_seconds: 0.0, channel_layout: ChannelLayout::PdYcbcr,
sync_position: SyncPosition::LineStart,
};
const PD180: ModeSpec = ModeSpec {
mode: SstvMode::Pd180,
short_name: "pd180",
name: "PD-180",
vis_code: 0x60,
line_pixels: 640,
image_lines: 496,
line_seconds: 0.754_24,
sync_seconds: 0.020,
porch_seconds: 0.002_08,
pixel_seconds: 0.000_286,
septr_seconds: 0.0, channel_layout: ChannelLayout::PdYcbcr,
sync_position: SyncPosition::LineStart,
};
const PD240: ModeSpec = ModeSpec {
mode: SstvMode::Pd240,
short_name: "pd240",
name: "PD-240",
vis_code: 0x61,
line_pixels: 640,
image_lines: 496,
line_seconds: 1.000,
sync_seconds: 0.020,
porch_seconds: 0.002_08,
pixel_seconds: 0.000_382,
septr_seconds: 0.0, channel_layout: ChannelLayout::PdYcbcr,
sync_position: SyncPosition::LineStart,
};
const ROBOT24: ModeSpec = ModeSpec {
mode: SstvMode::Robot24,
short_name: "robot24",
name: "Robot 24",
vis_code: 0x04,
line_pixels: 320,
image_lines: 240,
line_seconds: 0.150,
sync_seconds: 0.009,
porch_seconds: 0.003,
pixel_seconds: 0.000_137_5,
septr_seconds: 0.006,
channel_layout: ChannelLayout::RobotYuv,
sync_position: SyncPosition::LineStart,
};
const ROBOT36: ModeSpec = ModeSpec {
mode: SstvMode::Robot36,
short_name: "robot36",
name: "Robot 36",
vis_code: 0x08,
line_pixels: 320,
image_lines: 240,
line_seconds: 0.150,
sync_seconds: 0.009,
porch_seconds: 0.003,
pixel_seconds: 0.000_137_5,
septr_seconds: 0.006,
channel_layout: ChannelLayout::RobotYuv,
sync_position: SyncPosition::LineStart,
};
const ROBOT72: ModeSpec = ModeSpec {
mode: SstvMode::Robot72,
short_name: "robot72",
name: "Robot 72",
vis_code: 0x0C,
line_pixels: 320,
image_lines: 240,
line_seconds: 0.300,
sync_seconds: 0.009,
porch_seconds: 0.003,
pixel_seconds: 0.000_287_5,
septr_seconds: 0.0047,
channel_layout: ChannelLayout::RobotYuv,
sync_position: SyncPosition::LineStart,
};
const SCOTTIE1: ModeSpec = ModeSpec {
mode: SstvMode::Scottie1,
short_name: "scottie1",
name: "Scottie 1",
vis_code: 0x3C,
line_pixels: 320,
image_lines: 256,
line_seconds: 0.428_38,
sync_seconds: 0.009,
porch_seconds: 0.001_5,
pixel_seconds: 0.000_432_0,
septr_seconds: 0.001_5,
channel_layout: ChannelLayout::RgbSequential,
sync_position: SyncPosition::Scottie,
};
const SCOTTIE2: ModeSpec = ModeSpec {
mode: SstvMode::Scottie2,
short_name: "scottie2",
name: "Scottie 2",
vis_code: 0x38,
line_pixels: 320,
image_lines: 256,
line_seconds: 0.277_692,
sync_seconds: 0.009,
porch_seconds: 0.001_5,
pixel_seconds: 0.000_275_2,
septr_seconds: 0.001_5,
channel_layout: ChannelLayout::RgbSequential,
sync_position: SyncPosition::Scottie,
};
const SCOTTIE_DX: ModeSpec = ModeSpec {
mode: SstvMode::ScottieDx,
short_name: "scottiedx",
name: "Scottie DX",
vis_code: 0x4C,
line_pixels: 320,
image_lines: 256,
line_seconds: 1.050_3,
sync_seconds: 0.009,
porch_seconds: 0.001_5,
pixel_seconds: 0.001_080_53,
septr_seconds: 0.001_5,
channel_layout: ChannelLayout::RgbSequential,
sync_position: SyncPosition::Scottie,
};
const MARTIN1: ModeSpec = ModeSpec {
mode: SstvMode::Martin1,
short_name: "martin1",
name: "Martin 1",
vis_code: 0x2C,
line_pixels: 320,
image_lines: 256,
line_seconds: 0.446_446,
sync_seconds: 0.004_862,
porch_seconds: 0.000_572,
pixel_seconds: 0.000_457_6,
septr_seconds: 0.000_572,
channel_layout: ChannelLayout::RgbSequential,
sync_position: SyncPosition::LineStart,
};
const MARTIN2: ModeSpec = ModeSpec {
mode: SstvMode::Martin2,
short_name: "martin2",
name: "Martin 2",
vis_code: 0x28,
line_pixels: 320,
image_lines: 256,
line_seconds: 0.226_798_6,
sync_seconds: 0.004_862,
porch_seconds: 0.000_572,
pixel_seconds: 0.000_228_8,
septr_seconds: 0.000_572,
channel_layout: ChannelLayout::RgbSequential,
sync_position: SyncPosition::LineStart,
};
pub(crate) const ALL_SPECS: [ModeSpec; 11] = [
PD120, PD180, PD240, ROBOT24, ROBOT36, ROBOT72, SCOTTIE1, SCOTTIE2, SCOTTIE_DX, MARTIN1,
MARTIN2,
];
#[cfg(test)]
#[allow(clippy::expect_used, clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn pd120_vis_code_resolves() {
let spec = lookup(0x5F).expect("PD120 VIS resolves");
assert_eq!(spec.mode, SstvMode::Pd120);
assert_eq!(spec.vis_code, 0x5F);
assert_eq!(spec.line_pixels, 640);
assert_eq!(spec.image_lines, 496);
assert_eq!(spec.channel_layout, ChannelLayout::PdYcbcr);
assert_eq!(spec.line_seconds, 0.508_48);
assert_eq!(spec.sync_seconds, 0.020);
assert_eq!(spec.porch_seconds, 0.002_08);
assert_eq!(spec.pixel_seconds, 0.000_19);
}
#[test]
fn pd180_vis_code_resolves() {
let spec = lookup(0x60).expect("PD180 VIS resolves");
assert_eq!(spec.mode, SstvMode::Pd180);
assert_eq!(spec.pixel_seconds, 0.000_286);
}
#[test]
fn unknown_vis_codes_return_none() {
assert!(lookup(0x00).is_none());
assert!(lookup(0x42).is_none()); assert!(lookup(0xFF).is_none());
}
#[test]
fn for_mode_returns_matching_spec() {
assert_eq!(for_mode(SstvMode::Pd120).vis_code, 0x5F);
assert_eq!(for_mode(SstvMode::Pd180).vis_code, 0x60);
}
#[test]
fn pd_modes_have_zero_septr_seconds() {
let pd120 = lookup(0x5F).expect("PD120");
let pd180 = lookup(0x60).expect("PD180");
let pd240 = lookup(0x61).expect("PD240");
assert_eq!(pd120.septr_seconds, 0.0);
assert_eq!(pd180.septr_seconds, 0.0);
assert_eq!(pd240.septr_seconds, 0.0);
}
#[test]
fn all_v2_modes_have_line_start_sync_position() {
for mode in [
SstvMode::Pd120,
SstvMode::Pd180,
SstvMode::Pd240,
SstvMode::Robot24,
SstvMode::Robot36,
SstvMode::Robot72,
] {
let spec = for_mode(mode);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
}
}
#[test]
fn pd240_vis_code_resolves() {
let spec = lookup(0x61).expect("PD240 VIS resolves");
assert_eq!(spec.mode, SstvMode::Pd240);
assert_eq!(spec.vis_code, 0x61);
assert_eq!(spec.line_pixels, 640);
assert_eq!(spec.image_lines, 496);
assert_eq!(spec.channel_layout, ChannelLayout::PdYcbcr);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
assert_eq!(spec.line_seconds, 1.000);
assert_eq!(spec.sync_seconds, 0.020);
assert_eq!(spec.porch_seconds, 0.002_08);
assert_eq!(spec.pixel_seconds, 0.000_382);
assert_eq!(spec.septr_seconds, 0.0);
}
#[test]
fn for_mode_returns_pd240_spec() {
assert_eq!(for_mode(SstvMode::Pd240).vis_code, 0x61);
}
#[test]
fn robot24_vis_code_resolves() {
let spec = lookup(0x04).expect("R24 VIS resolves");
assert_eq!(spec.mode, SstvMode::Robot24);
assert_eq!(spec.vis_code, 0x04);
assert_eq!(spec.line_pixels, 320);
assert_eq!(spec.image_lines, 240);
assert_eq!(spec.channel_layout, ChannelLayout::RobotYuv);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
assert_eq!(spec.line_seconds, 0.150);
assert_eq!(spec.sync_seconds, 0.009);
assert_eq!(spec.porch_seconds, 0.003);
assert_eq!(spec.septr_seconds, 0.006);
assert_eq!(spec.pixel_seconds, 0.000_137_5);
}
#[test]
fn robot36_vis_code_resolves() {
let spec = lookup(0x08).expect("R36 VIS resolves");
assert_eq!(spec.mode, SstvMode::Robot36);
assert_eq!(spec.vis_code, 0x08);
assert_eq!(spec.line_pixels, 320);
assert_eq!(spec.image_lines, 240);
assert_eq!(spec.channel_layout, ChannelLayout::RobotYuv);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
assert_eq!(spec.line_seconds, 0.150);
assert_eq!(spec.sync_seconds, 0.009);
assert_eq!(spec.porch_seconds, 0.003);
assert_eq!(spec.septr_seconds, 0.006);
assert_eq!(spec.pixel_seconds, 0.000_137_5);
}
#[test]
fn robot72_vis_code_resolves() {
let spec = lookup(0x0C).expect("R72 VIS resolves");
assert_eq!(spec.mode, SstvMode::Robot72);
assert_eq!(spec.vis_code, 0x0C);
assert_eq!(spec.line_pixels, 320);
assert_eq!(spec.image_lines, 240);
assert_eq!(spec.channel_layout, ChannelLayout::RobotYuv);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
assert_eq!(spec.line_seconds, 0.300);
assert_eq!(spec.sync_seconds, 0.009);
assert_eq!(spec.porch_seconds, 0.003);
assert_eq!(spec.septr_seconds, 0.0047);
assert_eq!(spec.pixel_seconds, 0.000_287_5);
}
#[test]
fn for_mode_returns_robot_specs() {
assert_eq!(for_mode(SstvMode::Robot24).vis_code, 0x04);
assert_eq!(for_mode(SstvMode::Robot36).vis_code, 0x08);
assert_eq!(for_mode(SstvMode::Robot72).vis_code, 0x0C);
}
#[test]
fn scottie1_modespec() {
let spec = for_mode(SstvMode::Scottie1);
assert_eq!(spec.mode, SstvMode::Scottie1);
assert_eq!(spec.vis_code, 0x3C);
assert_eq!(spec.line_pixels, 320);
assert_eq!(spec.image_lines, 256);
assert_eq!(spec.channel_layout, ChannelLayout::RgbSequential);
assert_eq!(spec.sync_position, SyncPosition::Scottie);
assert!((spec.pixel_seconds - 0.4320e-3).abs() < 1e-9);
assert!((spec.line_seconds - 428.38e-3).abs() < 1e-9);
}
#[test]
fn scottie2_modespec() {
let spec = for_mode(SstvMode::Scottie2);
assert_eq!(spec.mode, SstvMode::Scottie2);
assert_eq!(spec.vis_code, 0x38);
assert!((spec.pixel_seconds - 0.2752e-3).abs() < 1e-9);
assert!((spec.line_seconds - 277.692e-3).abs() < 1e-9);
assert_eq!(spec.channel_layout, ChannelLayout::RgbSequential);
assert_eq!(spec.sync_position, SyncPosition::Scottie);
}
#[test]
fn scottie_dx_modespec() {
let spec = for_mode(SstvMode::ScottieDx);
assert_eq!(spec.mode, SstvMode::ScottieDx);
assert_eq!(spec.vis_code, 0x4C);
assert!((spec.pixel_seconds - 1.08053e-3).abs() < 1e-9);
assert!((spec.line_seconds - 1050.3e-3).abs() < 1e-9);
assert_eq!(spec.channel_layout, ChannelLayout::RgbSequential);
assert_eq!(spec.sync_position, SyncPosition::Scottie);
}
#[test]
fn scottie_vis_codes_resolve() {
assert_eq!(
lookup(0x3C).expect("S1 VIS resolves").mode,
SstvMode::Scottie1
);
assert_eq!(
lookup(0x38).expect("S2 VIS resolves").mode,
SstvMode::Scottie2
);
assert_eq!(
lookup(0x4C).expect("SDX VIS resolves").mode,
SstvMode::ScottieDx
);
}
#[test]
fn martin1_modespec() {
let spec = for_mode(SstvMode::Martin1);
assert_eq!(spec.mode, SstvMode::Martin1);
assert_eq!(spec.vis_code, 0x2C);
assert_eq!(spec.line_pixels, 320);
assert_eq!(spec.image_lines, 256);
assert_eq!(spec.channel_layout, ChannelLayout::RgbSequential);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
assert!((spec.pixel_seconds - 0.000_457_6).abs() < 1e-9);
assert!((spec.line_seconds - 0.446_446).abs() < 1e-9);
}
#[test]
fn martin2_modespec() {
let spec = for_mode(SstvMode::Martin2);
assert_eq!(spec.mode, SstvMode::Martin2);
assert_eq!(spec.vis_code, 0x28);
assert!((spec.pixel_seconds - 0.000_228_8).abs() < 1e-9);
assert!((spec.line_seconds - 0.226_798_6).abs() < 1e-9);
assert_eq!(spec.channel_layout, ChannelLayout::RgbSequential);
assert_eq!(spec.sync_position, SyncPosition::LineStart);
}
#[test]
fn martin_vis_codes_resolve() {
assert_eq!(lookup(0x2C).expect("M1").mode, SstvMode::Martin1);
assert_eq!(lookup(0x28).expect("M2").mode, SstvMode::Martin2);
}
#[test]
fn skip_correction_seconds_zero_for_line_start_modes() {
for mode in [
SstvMode::Pd120,
SstvMode::Pd240,
SstvMode::Pd180,
SstvMode::Robot24,
SstvMode::Robot36,
SstvMode::Robot72,
SstvMode::Martin1,
SstvMode::Martin2,
] {
let spec = for_mode(mode);
assert_eq!(
spec.skip_correction_seconds(),
0.0,
"{mode:?} expected 0.0 skip correction"
);
}
}
#[test]
fn skip_correction_seconds_scottie_formula() {
for mode in [SstvMode::Scottie1, SstvMode::Scottie2, SstvMode::ScottieDx] {
let spec = for_mode(mode);
let expected =
-f64::from(spec.line_pixels) * spec.pixel_seconds / 2.0 + 2.0 * spec.porch_seconds;
assert!(
(spec.skip_correction_seconds() - expected).abs() < 1e-12,
"{mode:?} got {} expected {expected}",
spec.skip_correction_seconds()
);
assert!(
spec.skip_correction_seconds() < 0.0,
"{mode:?} Scottie correction should be negative"
);
}
}
#[test]
fn all_specs_roundtrip() {
use std::collections::HashSet;
let modes: HashSet<_> = ALL_SPECS.iter().map(|s| s.mode).collect();
assert_eq!(
modes.len(),
ALL_SPECS.len(),
"ALL_SPECS has duplicate modes"
);
let vis: HashSet<_> = ALL_SPECS.iter().map(|s| s.vis_code).collect();
assert_eq!(
vis.len(),
ALL_SPECS.len(),
"ALL_SPECS has duplicate VIS codes"
);
let short_names: HashSet<_> = ALL_SPECS.iter().map(|s| s.short_name).collect();
assert_eq!(
short_names.len(),
ALL_SPECS.len(),
"ALL_SPECS has duplicate short_names"
);
let names: HashSet<_> = ALL_SPECS.iter().map(|s| s.name).collect();
assert_eq!(
names.len(),
ALL_SPECS.len(),
"ALL_SPECS has duplicate `name`s"
);
for spec in ALL_SPECS.iter().copied() {
assert_eq!(
lookup(spec.vis_code),
Some(spec),
"lookup({:#04x}) did not return ALL_SPECS entry for {:?}",
spec.vis_code,
spec.mode
);
assert_eq!(
for_mode(spec.mode),
spec,
"for_mode({:?}) did not match ALL_SPECS entry",
spec.mode
);
assert!(
!spec.short_name.is_empty(),
"{:?}: short_name empty",
spec.mode
);
assert!(!spec.name.is_empty(), "{:?}: name empty", spec.mode);
}
}
}