1use core::fmt;
7use std::collections::HashMap;
8use std::sync::OnceLock;
9
10use crate::event::Event;
11use crate::state::State;
12use crate::types::{BayAudioDetails, BaySignalDetails};
13use crate::wire::{BayStatus, BayUid, MxrSignalType};
14
15use super::handlers::{u16_at, u32_at};
16use super::Rx;
17
18const SVD_TABLE: &str = include_str!("../svd.csv");
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct Svd {
24 pub id: u16,
26 pub picture_aspect: u16,
28 pub pixel_aspect: u16,
30 pub horizontal_active: u16,
32 pub horizontal_total: u16,
34 pub vertical_active: u16,
36 pub vertical_total: u16,
38 pub refresh: u16,
40 pub interlaced: bool,
42 pub multiplier: u16,
44}
45
46impl fmt::Display for Svd {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(
49 f,
50 "{}x{}@{}Hz",
51 self.horizontal_active, self.vertical_active, self.refresh
52 )
53 }
54}
55
56fn table() -> &'static HashMap<u16, Svd> {
57 static TABLE: OnceLock<HashMap<u16, Svd>> = OnceLock::new();
58 TABLE.get_or_init(|| {
59 let mut map = HashMap::new();
60 for line in SVD_TABLE.lines() {
61 let fields: Vec<u16> = line
62 .trim()
63 .split(';')
64 .filter_map(|f| f.trim().parse().ok())
65 .collect();
66 let Ok(n) = <[u16; 10]>::try_from(fields.as_slice()) else {
67 continue;
68 };
69 map.insert(
70 n[0],
71 Svd {
72 id: n[0],
73 picture_aspect: n[1],
74 pixel_aspect: n[2],
75 horizontal_active: n[3],
76 horizontal_total: n[4],
77 vertical_active: n[5],
78 vertical_total: n[6],
79 refresh: n[7],
80 interlaced: n[8] == 1,
81 multiplier: n[9],
82 },
83 );
84 }
85 map
86 })
87}
88
89pub fn lookup_svd(id: u16) -> Option<Svd> {
91 table().get(&id).copied()
92}
93
94fn colour_space(v: u8) -> &'static str {
96 match v {
97 0 => "RGB",
98 1 => "4:4:4",
99 2 => "4:2:2",
100 3 => "4:2:0",
101 _ => "unknown",
102 }
103}
104
105const AV_DETAILS_SIZE: usize = 112;
117
118const STREAM_INTERLACED: u8 = 1 << 1;
120const STREAM_NON_INTEGER_CLOCK: u8 = 1 << 3;
121const STREAM_HDR: u8 = 1 << 4;
122
123const NO_SIGNAL: &str = "no signal";
126
127const VSYNC_H_ACTIVE: usize = 58;
129const VSYNC_V_ACTIVE: usize = 68;
130
131const INVALID_SIGNAL: &str = "invalid signal";
133
134const SUPPORT_STREAM_DETECTED: u8 = 1 << 0;
136
137const SUPPORT_STREAM_VALID: u8 = 1 << 1;
139
140const SUPPORT_AUDIO_INFOFRAME: u8 = 1 << 4;
142
143const SUPPORT_AUDIO_VALID: u8 = 1 << 5;
145
146fn audio_details(audio: &[u8], support_flags: u8) -> Option<BayAudioDetails> {
148 if support_flags & SUPPORT_AUDIO_VALID == 0 {
149 return None;
150 }
151 let infoframe = support_flags & SUPPORT_AUDIO_INFOFRAME != 0;
152 let channels = match audio[11] {
157 0 if infoframe => (audio[1] & 0x07) + 1,
158 n => n,
159 };
160 Some(BayAudioDetails {
161 format: audio[10],
162 channels,
163 sample_rate: u32_at(audio, 12),
164 coding: infoframe.then(|| audio[1] >> 4),
167 })
168}
169
170pub(super) fn signal_status(state: &mut State, rx: &Rx<'_>, ev: &mut Vec<Event>) {
181 let p = rx.frame.payload();
182 if p.len() < AV_DETAILS_SIZE {
183 return;
184 }
185 let support_flags = p[2];
186 let stream_flags = p[3];
187 let stream_detected = support_flags & SUPPORT_STREAM_DETECTED != 0;
188 let stream_valid = support_flags & SUPPORT_STREAM_VALID != 0;
189
190 let bay_block = &p[100..112];
191 let port = u16_at(bay_block, 0);
192 let bay = BayUid::new(rx.sender(), port);
193 if state.bay(bay).is_none() {
194 return;
195 }
196
197 let video = &p[40..56];
198 let svd_id = u16::from(video[0]);
199 let mut frame_rate = f64::from(u16_at(video, 8));
200 if stream_flags & STREAM_NON_INTEGER_CLOCK != 0 {
201 frame_rate = (frame_rate * 1000.0 / 1001.0 * 100.0).round() / 100.0;
202 }
203
204 let (h, v) = match lookup_svd(svd_id) {
207 Some(svd) if svd_id != 0 => (svd.horizontal_active, svd.vertical_active),
208 _ => (u16_at(p, VSYNC_H_ACTIVE), u16_at(p, VSYNC_V_ACTIVE)),
209 };
210 let signal_type = if !stream_detected {
211 NO_SIGNAL.to_owned()
212 } else if !stream_valid {
213 INVALID_SIGNAL.to_owned()
214 } else {
215 let mut description = format!("{h}x{v} / {} / {}bpp", colour_space(video[1]), video[2]);
216 if stream_flags & STREAM_INTERLACED != 0 {
217 description.push_str(" interlaced");
218 }
219 if stream_flags & STREAM_HDR != 0 {
220 description.push_str(" HDR");
221 }
222 description.push_str(&format!(" / {frame_rate}Hz"));
223 description
224 };
225
226 let details = BaySignalDetails {
227 frame_rate,
228 tmds_clock: u32_at(video, 10),
229 status: BayStatus::from_bits(u32_at(bay_block, 2)),
230 scaling: MxrSignalType::from_wire(u16_at(bay_block, 6)),
231 clock_rate: u32_at(bay_block, 8),
232 audio: audio_details(&p[24..40], support_flags),
233 };
234
235 if let Some(bay) = state.bay_mut(bay) {
238 bay.apply_bay_status(details.status, ev);
239 bay.set_signal_details(details);
240 bay.set_signal_type(signal_type, ev);
241 }
242}