#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SstvMode {
Pd120,
Pd180,
Pd240,
Robot24,
Robot36,
Robot72,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ModeSpec {
pub mode: SstvMode,
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,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SyncPosition {
LineStart,
}
#[must_use]
pub fn lookup(vis_code: u8) -> Option<ModeSpec> {
match vis_code {
0x04 => Some(ROBOT24),
0x08 => Some(ROBOT36),
0x0C => Some(ROBOT72),
0x5F => Some(PD120),
0x60 => Some(PD180),
0x61 => Some(PD240),
_ => None,
}
}
#[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,
}
}
const PD120: ModeSpec = ModeSpec {
mode: SstvMode::Pd120,
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,
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,
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,
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,
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,
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,
};
#[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);
}
}