1use crate::device::LinkEvent;
12use dvb_ci::resource::ResourceId;
13use dvb_ci::spdu::tags as spdu_tags;
14use dvb_ci::tag::ApduTag;
15use dvb_ci::tpdu::{SbValue, tags as tpdu_tags};
16
17fn tpdu_name(tag: u8) -> &'static str {
19 match tag {
20 tpdu_tags::SB => "T_SB",
21 tpdu_tags::RCV => "T_RCV",
22 tpdu_tags::CREATE_T_C => "Create_T_C",
23 tpdu_tags::C_T_C_REPLY => "C_T_C_Reply",
24 tpdu_tags::DELETE_T_C => "Delete_T_C",
25 tpdu_tags::D_T_C_REPLY => "D_T_C_Reply",
26 tpdu_tags::REQUEST_T_C => "Request_T_C",
27 tpdu_tags::NEW_T_C => "New_T_C",
28 tpdu_tags::T_C_ERROR => "T_C_Error",
29 tpdu_tags::DATA_LAST => "T_Data_Last",
30 tpdu_tags::DATA_MORE => "T_Data_More",
31 _ => "T_?",
32 }
33}
34
35fn spdu_name(tag: u8) -> &'static str {
37 match tag {
38 spdu_tags::SESSION_NUMBER => "session_number",
39 spdu_tags::OPEN_SESSION_REQUEST => "open_session_request",
40 spdu_tags::OPEN_SESSION_RESPONSE => "open_session_response",
41 spdu_tags::CREATE_SESSION => "create_session",
42 spdu_tags::CREATE_SESSION_RESPONSE => "create_session_response",
43 spdu_tags::CLOSE_SESSION_REQUEST => "close_session_request",
44 spdu_tags::CLOSE_SESSION_RESPONSE => "close_session_response",
45 _ => "spdu(?)",
46 }
47}
48
49fn apdu_label(apdu: &[u8]) -> String {
51 match apdu.first_chunk::<3>() {
52 Some(&[a, b, c]) => {
53 let tag = ApduTag::from_bytes(a, b, c);
54 format!("{} ({:02X}{:02X}{:02X})", tag.name(), a, b, c)
55 }
56 None => "apdu(short)".to_string(),
57 }
58}
59
60fn spdu_label(spdu: &[u8]) -> String {
63 match spdu.first().copied() {
64 Some(spdu_tags::SESSION_NUMBER) if spdu.len() >= 4 => {
65 let nb = u16::from_be_bytes([spdu[2], spdu[3]]);
66 let rest = &spdu[4..];
67 if rest.is_empty() {
68 format!("session {nb}")
69 } else {
70 format!("session {nb} · {}", apdu_label(rest))
71 }
72 }
73 Some(spdu_tags::OPEN_SESSION_REQUEST) if spdu.len() >= 6 => {
76 let r = ResourceId(u32::from_be_bytes([spdu[2], spdu[3], spdu[4], spdu[5]]));
77 format!("open_session_request {}", r.name())
78 }
79 Some(spdu_tags::CREATE_SESSION) if spdu.len() >= 8 => {
80 let r = ResourceId(u32::from_be_bytes([spdu[2], spdu[3], spdu[4], spdu[5]]));
81 let nb = u16::from_be_bytes([spdu[6], spdu[7]]);
82 format!("create_session {} (nb {nb})", r.name())
83 }
84 Some(t @ (spdu_tags::CREATE_SESSION_RESPONSE | spdu_tags::OPEN_SESSION_RESPONSE))
85 if spdu.len() >= 9 =>
86 {
87 let status = spdu[2];
88 let r = ResourceId(u32::from_be_bytes([spdu[3], spdu[4], spdu[5], spdu[6]]));
89 let nb = u16::from_be_bytes([spdu[7], spdu[8]]);
90 format!("{} {} status={status:#04x} nb={nb}", spdu_name(t), r.name())
91 }
92 Some(t) => spdu_name(t).to_string(),
93 None => "empty".to_string(),
94 }
95}
96
97#[must_use]
102pub fn decode_frame(frame: &[u8]) -> String {
103 let Some(&tag) = frame.first() else {
104 return "empty frame".to_string();
105 };
106 match tag {
107 tpdu_tags::SB => match frame.get(3) {
108 Some(&sb) => format!(
109 "T_SB tcid={} DA={}",
110 frame.get(2).copied().unwrap_or(0),
111 u8::from(SbValue(sb).data_available())
112 ),
113 None => "T_SB (short)".to_string(),
114 },
115 tpdu_tags::CREATE_T_C
116 | tpdu_tags::C_T_C_REPLY
117 | tpdu_tags::DELETE_T_C
118 | tpdu_tags::D_T_C_REPLY
119 | tpdu_tags::REQUEST_T_C
120 | tpdu_tags::NEW_T_C
121 | tpdu_tags::RCV
122 | tpdu_tags::T_C_ERROR => {
123 format!(
124 "{} tcid={}",
125 tpdu_name(tag),
126 frame.get(2).copied().unwrap_or(0)
127 )
128 }
129 tpdu_tags::DATA_LAST | tpdu_tags::DATA_MORE => {
130 let Ok((len, hdr)) = dvb_ci::length::decode(&frame[1..]) else {
134 return format!("{} (bad length_field)", tpdu_name(tag));
135 };
136 let start = 1 + hdr; let tcid = frame.get(start).copied().unwrap_or(0);
138 let data_end = (start + len).min(frame.len());
140 let spdu = frame.get(start + 1..data_end).unwrap_or(&[]);
141 if spdu.is_empty() {
142 format!("{} tcid={} (poll)", tpdu_name(tag), tcid)
143 } else {
144 format!("{} tcid={} · {}", tpdu_name(tag), tcid, spdu_label(spdu))
145 }
146 }
147 _ => format!("{} {:02X?}", tpdu_name(tag), &frame[..frame.len().min(8)]),
148 }
149}
150
151#[must_use]
153pub fn decode_log(log: &[LinkEvent]) -> String {
154 let mut out = String::new();
155 for ev in log {
156 let line = match ev {
157 LinkEvent::Tx(f) => format!("W {}", decode_frame(f)),
158 LinkEvent::Rx(f) => format!("R {}", decode_frame(f)),
159 LinkEvent::Reset => " reset()".to_string(),
160 LinkEvent::SlotInfo(si) => format!(
161 " slot_info() -> present={} ready={}",
162 si.module_present, si.module_ready
163 ),
164 };
165 out.push_str(&line);
166 out.push('\n');
167 }
168 out
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn names_every_connection_management_tpdu() {
177 assert_eq!(decode_frame(&[0x84, 0x01, 0x01]), "Delete_T_C tcid=1");
179 assert_eq!(decode_frame(&[0x85, 0x01, 0x01]), "D_T_C_Reply tcid=1");
180 assert_eq!(decode_frame(&[0x86, 0x01, 0x01]), "Request_T_C tcid=1");
181 }
182
183 #[test]
184 fn decodes_the_337_handshake_frames() {
185 assert_eq!(decode_frame(&[0x82, 0x01, 0x01]), "Create_T_C tcid=1");
187 assert_eq!(decode_frame(&[0x81, 0x01, 0x01]), "T_RCV tcid=1");
188 assert_eq!(decode_frame(&[0x80, 0x02, 0x01, 0x00]), "T_SB tcid=1 DA=0");
189
190 let osr = [
192 0xA0, 0x07, 0x01, 0x91, 0x04, 0x00, 0x01, 0x00, 0x41, 0x80, 0x02, 0x01, 0x00,
193 ];
194 assert_eq!(
195 decode_frame(&osr),
196 "T_Data_Last tcid=1 · open_session_request resource_manager"
197 );
198
199 let enq = [
201 0xA0, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00,
202 ];
203 assert_eq!(
204 decode_frame(&enq),
205 "T_Data_Last tcid=1 · session 1 · profile_enq (9F8010)"
206 );
207 }
208
209 #[test]
210 fn decodes_long_form_length_profile_reply() {
211 let f = [
215 0xA0, 0x82, 0x00, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x11, 0x00, 0x80,
216 0x02, 0x01, 0x00,
217 ];
218 assert_eq!(
219 decode_frame(&f),
220 "T_Data_Last tcid=1 · session 1 · profile (9F8011)"
221 );
222 }
223
224 #[test]
225 fn decodes_log_directions() {
226 let log = [
227 LinkEvent::Reset,
228 LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
229 LinkEvent::Rx(vec![0x80, 0x02, 0x01, 0x00]),
230 ];
231 let s = decode_log(&log);
232 assert!(s.contains(" reset()"));
233 assert!(s.contains("W Create_T_C tcid=1"));
234 assert!(s.contains("R T_SB tcid=1 DA=0"));
235 }
236}