use std::net::Ipv4Addr;
use crate::types::{
StreamKind, V2ipStreamSource, SCALING_FLAG_AUTO_SCALING, SCALING_FLAG_MODE_VALID,
SCALING_FLAG_OPTIONS_VALID,
};
use crate::wire::{
cstr, op, BayFeatures, BayStatus, DeviceFeature, MxrSignalType, BAY_CONFIG_SIZE, V2IP_DSCP_SET,
V2IP_PORT_VIDEO,
};
use crate::testing::{bay_config_rec, field, poisoned, Cfg};
use super::Harness;
fn dscp(value: u8) -> u8 {
V2IP_DSCP_SET | value
}
fn v2ip_device(n: u8) -> Harness {
let mut h = Harness::new(n);
h.hello(0x28, "ONEIP-TX", "TX0001", DeviceFeature::V2IP_SOURCE);
h
}
fn ip(s: &str) -> Ipv4Addr {
s.parse().expect("test address")
}
#[test]
fn device_config_carries_dscp_and_rate() {
let mut h = v2ip_device(20);
let sender = h.sender;
let mut full = Cfg::addresses(sender, "239.1.2.3");
full.rate = 40;
full.dscp_video = dscp(34);
full.dscp_audio = dscp(46);
full.dscp_anc = dscp(0);
h.feed(op::V2IP_DEVICE_CFG, &full.bytes());
let details = h.device().v2ip_details.expect("no details");
assert_eq!(details.tx_rate, Some(40));
assert!(details.dscp.is_complete());
assert_eq!(
(details.dscp.video, details.dscp.audio, details.dscp.anc),
(Some(34), Some(46), Some(0))
);
h.feed(
op::V2IP_DEVICE_CFG,
&Cfg::addresses(sender, "239.9.9.9").bytes(),
);
let details = h.device().v2ip_details.expect("no details");
assert_eq!(details.video.ip, ip("239.9.9.9"));
assert_eq!(
details.tx_rate,
Some(40),
"an address-only write cleared the rate"
);
assert!(details.dscp.is_complete());
assert_eq!(details.dscp.audio, Some(46));
}
#[test]
fn a_rate_only_write_keeps_the_addresses() {
let mut h = v2ip_device(21);
let sender = h.sender;
h.feed(
op::V2IP_DEVICE_CFG,
&Cfg::addresses(sender, "239.1.2.3").bytes(),
);
let rate_only = Cfg {
uid: sender,
rate: 40,
..Cfg::default()
};
h.feed(op::V2IP_DEVICE_CFG, &rate_only.bytes());
let details = h.device().v2ip_details.expect("no details");
assert_eq!(details.video.ip, ip("239.1.2.3"));
assert_eq!(details.anc.ip, ip("239.1.2.3"));
assert_eq!(details.tx_rate, Some(40));
}
#[test]
fn stream_source_validity() {
let cases = [
(
"multicast with port",
ip("239.1.2.3"),
V2IP_PORT_VIDEO,
true,
),
("unicast", ip("10.8.8.9"), V2IP_PORT_VIDEO, false),
("multicast, port 0", ip("239.1.2.3"), 0, false),
("zero", Ipv4Addr::UNSPECIFIED, 0, false),
];
for (name, addr, port, want) in cases {
let source = V2ipStreamSource {
kind: StreamKind::Video,
ip: addr,
port,
};
assert_eq!(source.is_valid(), want, "{name}");
}
}
#[test]
fn device_config_rejects_unusable_addresses() {
let mut h = v2ip_device(22);
let sender = h.sender;
h.feed(
op::V2IP_DEVICE_CFG,
&Cfg::addresses(sender, "239.1.2.3").bytes(),
);
let mut unicast = Cfg::addresses(sender, "239.4.5.6");
unicast.video_ip = "10.8.8.9";
h.feed(op::V2IP_DEVICE_CFG, &unicast.bytes());
assert_eq!(
h.device().v2ip_details.expect("no details").anc.ip,
ip("239.1.2.3")
);
let mut no_port = Cfg::addresses(sender, "239.4.5.6");
no_port.anc_port = 0;
h.feed(op::V2IP_DEVICE_CFG, &no_port.bytes());
assert_eq!(
h.device().v2ip_details.expect("no details").video.ip,
ip("239.1.2.3")
);
}
#[test]
fn scaling_merges_per_field() {
let mut h = v2ip_device(23);
let sender = h.sender;
let mut both = Cfg::addresses(sender, "239.1.2.3");
both.mode = 16;
both.refresh = 60;
both.flags = SCALING_FLAG_MODE_VALID | SCALING_FLAG_OPTIONS_VALID | SCALING_FLAG_AUTO_SCALING;
h.feed(op::V2IP_DEVICE_CFG, &both.bytes());
let options_only = Cfg {
uid: sender,
flags: SCALING_FLAG_OPTIONS_VALID,
..Cfg::default()
};
h.feed(op::V2IP_DEVICE_CFG, &options_only.bytes());
let scaling = h.device().v2ip_details.expect("no details").scaling;
assert_eq!(scaling.mode.svd(), 16);
assert_eq!(scaling.refresh, 60);
assert_eq!(scaling.flags & SCALING_FLAG_AUTO_SCALING, 0);
let mode_only = Cfg {
uid: sender,
mode: 31,
refresh: 50,
flags: SCALING_FLAG_MODE_VALID,
..Cfg::default()
};
h.feed(op::V2IP_DEVICE_CFG, &mode_only.bytes());
let scaling = h.device().v2ip_details.expect("no details").scaling;
assert_eq!(scaling.mode.svd(), 31);
assert_eq!(scaling.refresh, 50);
assert_ne!(scaling.flags & SCALING_FLAG_OPTIONS_VALID, 0);
}
#[test]
fn undefined_scaling_flag_bits_are_dropped() {
let mut h = v2ip_device(28);
let sender = h.sender;
let mut noisy = Cfg::addresses(sender, "239.1.2.3");
noisy.flags = 0xFF;
h.feed(op::V2IP_DEVICE_CFG, &noisy.bytes());
let flags = h.device().v2ip_details.expect("no details").scaling.flags;
assert_eq!(
flags & !(SCALING_FLAG_MODE_VALID | SCALING_FLAG_OPTIONS_VALID | SCALING_FLAG_AUTO_SCALING),
0,
"undefined flag bits reached the cache: {flags:#04x}"
);
}
#[test]
fn a_partial_dscp_is_no_marking() {
let mut h = v2ip_device(24);
let sender = h.sender;
let mut c = Cfg::addresses(sender, "239.1.2.3");
c.dscp_video = dscp(34);
c.dscp_audio = dscp(46);
h.feed(op::V2IP_DEVICE_CFG, &c.bytes());
let dscp = h.device().v2ip_details.expect("no details").dscp;
assert!(!dscp.is_complete());
assert_eq!(dscp.anc, None);
}
#[test]
fn bay_config_pages_merge() {
let mut h = Harness::new(22);
h.hello(0x28, "FF88", "PG0001", DeviceFeature::VIDEO_ROUTING);
let mut page = bay_config_rec(
1,
0,
0,
"Input 1",
"Apple TV",
BayStatus::NONE,
BayFeatures::HDMI_IN,
);
page.extend(bay_config_rec(
2,
0,
1,
"Input 2",
"Blu-ray",
BayStatus::NONE,
BayFeatures::HDMI_IN,
));
h.feed(op::SYS_BAY_CONFIG, &page);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
3,
1,
0,
"Output 1",
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
),
);
for port in [1, 2, 3] {
assert!(
h.device().bay(port).is_some(),
"bay on port {port} missing after a paged config"
);
}
}
#[test]
fn link_pages_merge() {
let mut h = Harness::new(23);
h.hello(0x28, "FF88", "PG0002", DeviceFeature::VIDEO_ROUTING);
let mut cfg = bay_config_rec(
1,
0,
0,
"Input 1",
"Apple TV",
BayStatus::NONE,
BayFeatures::HDMI_IN,
);
for (port, name) in [(2u8, "Output 1"), (3, "Output 2")] {
cfg.extend(bay_config_rec(
port,
1,
0,
name,
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
));
}
h.feed(op::SYS_BAY_CONFIG, &cfg);
let link_rec = |port: u8, serial: &str, bay: &str| {
let mut rec = poisoned(38);
rec[0] = port;
field(&mut rec, 2, 16, serial);
field(&mut rec, 18, 16, bay);
rec
};
let mut page = link_rec(1, "AMP00001", "Zone 1");
page.extend(link_rec(2, "AMP00001", "Zone 2"));
h.feed(op::SYS_LINKS, &page);
h.feed(op::SYS_LINKS, &link_rec(3, "AMP00001", "Zone 3"));
let sender = h.sender;
for (port, want) in [(1u16, "Zone 1"), (2, "Zone 2"), (3, "Zone 3")] {
let key = h
.state
.link_key(crate::wire::BayUid::new(sender, port))
.expect("no link key");
let link = h
.state
.links
.get(key)
.expect("link lost after a second page");
assert_eq!(link.linked_bay, want, "port {port}");
}
}
#[test]
fn a_name_filling_its_field_stops_at_the_field_edge() {
let mut h = Harness::new(24);
let full = "0123456789ABCDEF";
h.hello(0x28, full, "SN0001", DeviceFeature::VIDEO_ROUTING);
assert_eq!(h.device().name(), full);
assert_eq!(h.device().serial(), "SN0001");
}
#[test]
fn a_non_ascii_byte_costs_one_character() {
assert_eq!(cstr(b"Zone \xff1"), "Zone \u{fffd}1");
assert_eq!(
cstr(&[0u8; 16]),
"",
"an all-NUL field should decode to the empty string"
);
}
#[test]
fn signal_status_decodes_the_video_and_bay_blocks() {
let mut h = Harness::new(25);
h.hello(0x28, "ONEIP-RX", "SS0001", DeviceFeature::VIDEO_ROUTING);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
2,
1,
0,
"Output 1",
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
),
);
let mut p = poisoned(112);
p[2] = 1 << 1; p[40] = 16; p[41] = 0; p[42] = 8; p[48..50].copy_from_slice(&300u16.to_le_bytes());
p[50..54].copy_from_slice(&594_000_000u32.to_le_bytes()); p[100..102].copy_from_slice(&2u16.to_le_bytes()); p[102..106].copy_from_slice(&BayStatus::SIGNAL_DETECTED.bits().to_le_bytes());
p[106] = 16;
p[107] = 1 | (2 << 5);
p[108..112].copy_from_slice(&148_500_000u32.to_le_bytes());
h.feed(op::BAY_SIGNAL_STATUS, &p);
let d = h.bay(2).signal_details.expect("no signal details");
assert_eq!(d.frame_rate, 300.0);
assert_eq!(d.tmds_clock, 594_000_000);
assert_eq!(d.clock_rate, 148_500_000);
assert!(d.status.has(BayStatus::SIGNAL_DETECTED));
assert_eq!(d.scaling.svd(), 16);
assert_eq!(d.scaling.colour_space(), 1);
assert_eq!(d.scaling.bpp(), Some(10));
assert!(d.scaling.is_set());
}
const CAPTURED_INPUT_BAY: [u8; 112] = [
0x01, 0x00, 0xFF, 0x28, 0x00, 0x00, 0x00, 0x00, 0x02, 0x50, 0xA8, 0x00, 0x10, 0x00, 0x00, 0x34,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x02, 0x80, 0xBB, 0x00, 0x00, 0x10, 0x01, 0x08, 0x02, 0x01, 0x00, 0x00, 0x00,
0x3C, 0x00, 0x20, 0xEE, 0xD9, 0x08, 0x00, 0x00, 0x98, 0x08, 0x80, 0x07, 0x58, 0x00, 0x94, 0x00,
0x2C, 0x00, 0x65, 0x04, 0x38, 0x04, 0x04, 0x00, 0x24, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x01, 0x81, 0x00, 0x00, 0x00, 0x20, 0xEE, 0xD9, 0x08,
];
const CAPTURED_OUTPUT_BAY: [u8; 112] = [
0x01, 0x00, 0xFF, 0x28, 0x00, 0x00, 0x00, 0x00, 0x02, 0x72, 0xA8, 0x00, 0x61, 0x00, 0x00, 0xA4,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x02, 0x80, 0xBB, 0x00, 0x00, 0x61, 0x03, 0x08, 0x02, 0x01, 0x00, 0x00, 0x00,
0x3C, 0x00, 0x80, 0xB8, 0x67, 0x23, 0x00, 0x00, 0x30, 0x11, 0x00, 0x0F, 0xB0, 0x00, 0x28, 0x01,
0x58, 0x00, 0xCA, 0x08, 0x70, 0x08, 0x08, 0x00, 0x48, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x98, 0x01, 0x81, 0x00, 0x00, 0xA0, 0x40, 0xDC, 0xB3, 0x11,
];
fn captured_unit() -> Harness {
let mut h = Harness::new(27);
h.hello(0x28, "ONEIP", "CAP0001", DeviceFeature::V2IP_SOURCE);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
0,
0,
0,
"Input 1",
"Apple TV",
BayStatus::NONE,
BayFeatures::HDMI_IN,
),
);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
16,
1,
0,
"Output 1",
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
),
);
h
}
#[test]
fn a_captured_input_report_decodes_to_the_mode_it_names() {
let mut h = captured_unit();
h.feed(op::BAY_SIGNAL_STATUS, &CAPTURED_INPUT_BAY);
let d = h.bay(0).signal_details.expect("no signal details");
assert_eq!(d.tmds_clock, 148_500_000);
assert_eq!(d.frame_rate, 59.94);
assert_eq!(d.status, BayStatus::from_bits(0x0081_0188));
assert!(d.status.has(BayStatus::SIGNAL_DETECTED));
assert!(!d.status.has(BayStatus::HPD_DETECTED));
let audio = d.audio.expect("no audio block");
assert_eq!(audio.format, 1); assert_eq!(audio.channels, 2);
assert_eq!(audio.sample_rate, 48_000);
assert_eq!(audio.coding, Some(0));
assert!(!d.scaling.is_set());
}
#[test]
fn a_captured_output_report_decodes_to_the_mode_it_names() {
let mut h = captured_unit();
h.feed(op::BAY_SIGNAL_STATUS, &CAPTURED_OUTPUT_BAY);
let d = h.bay(16).signal_details.expect("no signal details");
assert_eq!(d.tmds_clock, 594_000_000);
assert_eq!(d.frame_rate, 59.94);
assert_eq!(d.clock_rate, 297_000_000);
assert_eq!(d.status, BayStatus::from_bits(0x0081_0198));
assert!(d.status.has(BayStatus::HPD_DETECTED));
assert!(d.status.has(BayStatus::SIGNAL_DETECTED));
let audio = d.audio.expect("no audio block");
assert_eq!(audio.format, 1);
assert_eq!(audio.channels, 2);
assert_eq!(audio.sample_rate, 48_000);
assert_eq!(d.scaling.bpp_index(), 5);
assert!(!d.scaling.is_set());
assert!(h.bay(0).signal_details.is_none());
}
#[test]
fn a_bay_with_no_signal_is_described_as_firmware_describes_it() {
let mut h = captured_unit();
let mut p = CAPTURED_INPUT_BAY;
p[2] &= !(1 << 1); h.feed(op::BAY_SIGNAL_STATUS, &p);
assert_eq!(h.bay(0).signal_type.as_deref(), Some("no signal"));
assert_eq!(h.bay(0).signal_detected, Some(false));
}
#[test]
fn a_report_without_the_audio_bit_carries_no_audio() {
let mut h = captured_unit();
let mut p = CAPTURED_INPUT_BAY;
p[2] &= !((1 << 4) | (1 << 5));
h.feed(op::BAY_SIGNAL_STATUS, &p);
let d = h.bay(0).signal_details.expect("no signal details");
assert!(d.audio.is_none(), "the block was not claimed");
}
#[test]
fn a_source_sending_no_infoframe_claims_no_coding() {
let mut h = captured_unit();
let mut p = CAPTURED_INPUT_BAY;
p[2] &= !(1 << 4);
h.feed(op::BAY_SIGNAL_STATUS, &p);
let audio = h
.bay(0)
.signal_details
.expect("details")
.audio
.expect("audio");
assert_eq!(audio.sample_rate, 48_000);
assert_eq!(audio.coding, None);
}
#[test]
fn a_short_signal_report_is_dropped() {
let mut h = Harness::new(26);
h.hello(0x28, "ONEIP-RX", "SS0002", DeviceFeature::VIDEO_ROUTING);
h.feed(
op::SYS_BAY_CONFIG,
&bay_config_rec(
2,
1,
0,
"Output 1",
"TV",
BayStatus::NONE,
BayFeatures::HDMI_OUT,
),
);
h.feed(op::BAY_SIGNAL_STATUS, &[0u8; 68]);
assert!(
h.bay(2).signal_details.is_none(),
"a short report was decoded"
);
}
#[test]
fn the_unset_depth_sentinel_is_not_a_depth() {
let unset = MxrSignalType::from_wire(5 << 13);
assert!(!unset.is_set());
assert_eq!(unset.to_string(), "unset");
for (index, want) in [(1u16, 8u8), (2, 10), (3, 12), (4, 16)] {
let signal = MxrSignalType::from_wire(index << 13);
assert_eq!(signal.bpp(), Some(want), "depth index {index}");
}
assert_eq!(MxrSignalType::from_wire(0).bpp(), None);
}
#[test]
fn every_amp_opcode_stamps_under_the_amp_cap() {
for opcode in [
op::SYS_HELLO,
op::SYS_DISCOVER,
op::RC_SETTINGS,
op::V2IP_DEVICE_CFG,
op::V2IP_MANUAL_SRC_SWITCH,
op::AUDIO_SET_VOLUME,
op::AMP_ZONE_SETTINGS,
op::AMP_DOLBY_STATE,
] {
let stamped = crate::wire::protocol_for(opcode);
assert!(
stamped <= 0x22,
"opcode {:#04x} stamps {stamped:#04x}",
opcode.0
);
}
assert_eq!(crate::wire::protocol_for(op::V2IP_VIDEO_WALL), 0x28);
}
#[test]
fn a_per_record_loop_is_bounded_by_the_declared_length() {
let mut h = Harness::new(27);
h.hello(0x28, "FF88", "PB0001", DeviceFeature::VIDEO_ROUTING);
let mut data = super::datagram(
h.sender,
op::SYS_BAY_CONFIG,
0x01,
&bay_config_rec(
1,
0,
0,
"Input 1",
"Apple TV",
BayStatus::NONE,
BayFeatures::HDMI_IN,
),
);
data.extend(bay_config_rec(
2,
0,
1,
"Input 2",
"Blu-ray",
BayStatus::NONE,
BayFeatures::HDMI_IN,
));
let events = crate::rx::process_frame(&mut h.state, &data, None, std::time::Instant::now());
h.events.extend(events);
assert!(
h.device().bay(1).is_some(),
"the declared record should have registered"
);
assert!(
h.device().bay(2).is_none(),
"a record beyond the declared length became a bay"
);
assert_eq!(BAY_CONFIG_SIZE, 61);
}