1use crate::device::LinkEvent;
12use dvb_ci::spdu::tags as spdu_tags;
13use dvb_ci::tag::ApduTag;
14use dvb_ci::tpdu::{tags as tpdu_tags, SbValue};
15
16fn tpdu_name(tag: u8) -> &'static str {
18 match tag {
19 tpdu_tags::SB => "T_SB",
20 tpdu_tags::RCV => "T_RCV",
21 tpdu_tags::CREATE_T_C => "Create_T_C",
22 tpdu_tags::C_T_C_REPLY => "C_T_C_Reply",
23 tpdu_tags::T_C_ERROR => "T_C_Error",
24 tpdu_tags::DATA_LAST => "T_Data_Last",
25 tpdu_tags::DATA_MORE => "T_Data_More",
26 _ => "T_?",
27 }
28}
29
30fn spdu_name(tag: u8) -> &'static str {
32 match tag {
33 spdu_tags::SESSION_NUMBER => "session_number",
34 spdu_tags::OPEN_SESSION_REQUEST => "open_session_request",
35 spdu_tags::OPEN_SESSION_RESPONSE => "open_session_response",
36 spdu_tags::CREATE_SESSION => "create_session",
37 spdu_tags::CREATE_SESSION_RESPONSE => "create_session_response",
38 spdu_tags::CLOSE_SESSION_REQUEST => "close_session_request",
39 spdu_tags::CLOSE_SESSION_RESPONSE => "close_session_response",
40 _ => "spdu(?)",
41 }
42}
43
44fn apdu_label(apdu: &[u8]) -> String {
46 match apdu.first_chunk::<3>() {
47 Some(&[a, b, c]) => {
48 let tag = ApduTag::from_bytes(a, b, c);
49 format!("{} ({:02X}{:02X}{:02X})", tag.name(), a, b, c)
50 }
51 None => "apdu(short)".to_string(),
52 }
53}
54
55fn spdu_label(spdu: &[u8]) -> String {
58 match spdu.first().copied() {
59 Some(spdu_tags::SESSION_NUMBER) if spdu.len() >= 4 => {
60 let nb = u16::from_be_bytes([spdu[2], spdu[3]]);
61 let rest = &spdu[4..];
62 if rest.is_empty() {
63 format!("session {nb}")
64 } else {
65 format!("session {nb} · {}", apdu_label(rest))
66 }
67 }
68 Some(t) => spdu_name(t).to_string(),
69 None => "empty".to_string(),
70 }
71}
72
73#[must_use]
78pub fn decode_frame(frame: &[u8]) -> String {
79 let Some(&tag) = frame.first() else {
80 return "empty frame".to_string();
81 };
82 match tag {
83 tpdu_tags::SB => match frame.get(3) {
84 Some(&sb) => format!(
85 "T_SB tcid={} DA={}",
86 frame.get(2).copied().unwrap_or(0),
87 u8::from(SbValue(sb).data_available())
88 ),
89 None => "T_SB (short)".to_string(),
90 },
91 tpdu_tags::CREATE_T_C | tpdu_tags::C_T_C_REPLY | tpdu_tags::RCV | tpdu_tags::T_C_ERROR => {
92 format!(
93 "{} tcid={}",
94 tpdu_name(tag),
95 frame.get(2).copied().unwrap_or(0)
96 )
97 }
98 tpdu_tags::DATA_LAST | tpdu_tags::DATA_MORE => {
99 let Ok((len, hdr)) = dvb_ci::length::decode(&frame[1..]) else {
103 return format!("{} (bad length_field)", tpdu_name(tag));
104 };
105 let start = 1 + hdr; let tcid = frame.get(start).copied().unwrap_or(0);
107 let data_end = (start + len).min(frame.len());
109 let spdu = frame.get(start + 1..data_end).unwrap_or(&[]);
110 if spdu.is_empty() {
111 format!("{} tcid={} (poll)", tpdu_name(tag), tcid)
112 } else {
113 format!("{} tcid={} · {}", tpdu_name(tag), tcid, spdu_label(spdu))
114 }
115 }
116 _ => format!("{} {:02X?}", tpdu_name(tag), &frame[..frame.len().min(8)]),
117 }
118}
119
120#[must_use]
122pub fn decode_log(log: &[LinkEvent]) -> String {
123 let mut out = String::new();
124 for ev in log {
125 let line = match ev {
126 LinkEvent::Tx(f) => format!("W {}", decode_frame(f)),
127 LinkEvent::Rx(f) => format!("R {}", decode_frame(f)),
128 LinkEvent::Reset => " reset()".to_string(),
129 LinkEvent::SlotInfo(si) => format!(" slot_info() -> ready={}", si.module_ready),
130 };
131 out.push_str(&line);
132 out.push('\n');
133 }
134 out
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn decodes_the_337_handshake_frames() {
143 assert_eq!(decode_frame(&[0x82, 0x01, 0x01]), "Create_T_C tcid=1");
145 assert_eq!(decode_frame(&[0x81, 0x01, 0x01]), "T_RCV tcid=1");
146 assert_eq!(decode_frame(&[0x80, 0x02, 0x01, 0x00]), "T_SB tcid=1 DA=0");
147
148 let osr = [
150 0xA0, 0x07, 0x01, 0x91, 0x04, 0x00, 0x01, 0x00, 0x41, 0x80, 0x02, 0x01, 0x00,
151 ];
152 assert_eq!(
153 decode_frame(&osr),
154 "T_Data_Last tcid=1 · open_session_request"
155 );
156
157 let enq = [
159 0xA0, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00,
160 ];
161 assert_eq!(
162 decode_frame(&enq),
163 "T_Data_Last tcid=1 · session 1 · profile_enq (9F8010)"
164 );
165 }
166
167 #[test]
168 fn decodes_long_form_length_profile_reply() {
169 let f = [
173 0xA0, 0x82, 0x00, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x11, 0x00, 0x80,
174 0x02, 0x01, 0x00,
175 ];
176 assert_eq!(
177 decode_frame(&f),
178 "T_Data_Last tcid=1 · session 1 · profile (9F8011)"
179 );
180 }
181
182 #[test]
183 fn decodes_log_directions() {
184 let log = [
185 LinkEvent::Reset,
186 LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
187 LinkEvent::Rx(vec![0x80, 0x02, 0x01, 0x00]),
188 ];
189 let s = decode_log(&log);
190 assert!(s.contains(" reset()"));
191 assert!(s.contains("W Create_T_C tcid=1"));
192 assert!(s.contains("R T_SB tcid=1 DA=0"));
193 }
194}