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::{tags as tpdu_tags, SbValue};
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::T_C_ERROR => "T_C_Error",
25 tpdu_tags::DATA_LAST => "T_Data_Last",
26 tpdu_tags::DATA_MORE => "T_Data_More",
27 _ => "T_?",
28 }
29}
30
31fn spdu_name(tag: u8) -> &'static str {
33 match tag {
34 spdu_tags::SESSION_NUMBER => "session_number",
35 spdu_tags::OPEN_SESSION_REQUEST => "open_session_request",
36 spdu_tags::OPEN_SESSION_RESPONSE => "open_session_response",
37 spdu_tags::CREATE_SESSION => "create_session",
38 spdu_tags::CREATE_SESSION_RESPONSE => "create_session_response",
39 spdu_tags::CLOSE_SESSION_REQUEST => "close_session_request",
40 spdu_tags::CLOSE_SESSION_RESPONSE => "close_session_response",
41 _ => "spdu(?)",
42 }
43}
44
45fn apdu_label(apdu: &[u8]) -> String {
47 match apdu.first_chunk::<3>() {
48 Some(&[a, b, c]) => {
49 let tag = ApduTag::from_bytes(a, b, c);
50 format!("{} ({:02X}{:02X}{:02X})", tag.name(), a, b, c)
51 }
52 None => "apdu(short)".to_string(),
53 }
54}
55
56fn spdu_label(spdu: &[u8]) -> String {
59 match spdu.first().copied() {
60 Some(spdu_tags::SESSION_NUMBER) if spdu.len() >= 4 => {
61 let nb = u16::from_be_bytes([spdu[2], spdu[3]]);
62 let rest = &spdu[4..];
63 if rest.is_empty() {
64 format!("session {nb}")
65 } else {
66 format!("session {nb} · {}", apdu_label(rest))
67 }
68 }
69 Some(spdu_tags::OPEN_SESSION_REQUEST) if spdu.len() >= 6 => {
72 let r = ResourceId(u32::from_be_bytes([spdu[2], spdu[3], spdu[4], spdu[5]]));
73 format!("open_session_request {}", r.name())
74 }
75 Some(spdu_tags::CREATE_SESSION) if spdu.len() >= 8 => {
76 let r = ResourceId(u32::from_be_bytes([spdu[2], spdu[3], spdu[4], spdu[5]]));
77 let nb = u16::from_be_bytes([spdu[6], spdu[7]]);
78 format!("create_session {} (nb {nb})", r.name())
79 }
80 Some(t @ (spdu_tags::CREATE_SESSION_RESPONSE | spdu_tags::OPEN_SESSION_RESPONSE))
81 if spdu.len() >= 9 =>
82 {
83 let status = spdu[2];
84 let r = ResourceId(u32::from_be_bytes([spdu[3], spdu[4], spdu[5], spdu[6]]));
85 let nb = u16::from_be_bytes([spdu[7], spdu[8]]);
86 format!("{} {} status={status:#04x} nb={nb}", spdu_name(t), r.name())
87 }
88 Some(t) => spdu_name(t).to_string(),
89 None => "empty".to_string(),
90 }
91}
92
93#[must_use]
98pub fn decode_frame(frame: &[u8]) -> String {
99 let Some(&tag) = frame.first() else {
100 return "empty frame".to_string();
101 };
102 match tag {
103 tpdu_tags::SB => match frame.get(3) {
104 Some(&sb) => format!(
105 "T_SB tcid={} DA={}",
106 frame.get(2).copied().unwrap_or(0),
107 u8::from(SbValue(sb).data_available())
108 ),
109 None => "T_SB (short)".to_string(),
110 },
111 tpdu_tags::CREATE_T_C | tpdu_tags::C_T_C_REPLY | tpdu_tags::RCV | tpdu_tags::T_C_ERROR => {
112 format!(
113 "{} tcid={}",
114 tpdu_name(tag),
115 frame.get(2).copied().unwrap_or(0)
116 )
117 }
118 tpdu_tags::DATA_LAST | tpdu_tags::DATA_MORE => {
119 let Ok((len, hdr)) = dvb_ci::length::decode(&frame[1..]) else {
123 return format!("{} (bad length_field)", tpdu_name(tag));
124 };
125 let start = 1 + hdr; let tcid = frame.get(start).copied().unwrap_or(0);
127 let data_end = (start + len).min(frame.len());
129 let spdu = frame.get(start + 1..data_end).unwrap_or(&[]);
130 if spdu.is_empty() {
131 format!("{} tcid={} (poll)", tpdu_name(tag), tcid)
132 } else {
133 format!("{} tcid={} · {}", tpdu_name(tag), tcid, spdu_label(spdu))
134 }
135 }
136 _ => format!("{} {:02X?}", tpdu_name(tag), &frame[..frame.len().min(8)]),
137 }
138}
139
140#[must_use]
142pub fn decode_log(log: &[LinkEvent]) -> String {
143 let mut out = String::new();
144 for ev in log {
145 let line = match ev {
146 LinkEvent::Tx(f) => format!("W {}", decode_frame(f)),
147 LinkEvent::Rx(f) => format!("R {}", decode_frame(f)),
148 LinkEvent::Reset => " reset()".to_string(),
149 LinkEvent::SlotInfo(si) => format!(" slot_info() -> ready={}", si.module_ready),
150 };
151 out.push_str(&line);
152 out.push('\n');
153 }
154 out
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn decodes_the_337_handshake_frames() {
163 assert_eq!(decode_frame(&[0x82, 0x01, 0x01]), "Create_T_C tcid=1");
165 assert_eq!(decode_frame(&[0x81, 0x01, 0x01]), "T_RCV tcid=1");
166 assert_eq!(decode_frame(&[0x80, 0x02, 0x01, 0x00]), "T_SB tcid=1 DA=0");
167
168 let osr = [
170 0xA0, 0x07, 0x01, 0x91, 0x04, 0x00, 0x01, 0x00, 0x41, 0x80, 0x02, 0x01, 0x00,
171 ];
172 assert_eq!(
173 decode_frame(&osr),
174 "T_Data_Last tcid=1 · open_session_request resource_manager"
175 );
176
177 let enq = [
179 0xA0, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00,
180 ];
181 assert_eq!(
182 decode_frame(&enq),
183 "T_Data_Last tcid=1 · session 1 · profile_enq (9F8010)"
184 );
185 }
186
187 #[test]
188 fn decodes_long_form_length_profile_reply() {
189 let f = [
193 0xA0, 0x82, 0x00, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x11, 0x00, 0x80,
194 0x02, 0x01, 0x00,
195 ];
196 assert_eq!(
197 decode_frame(&f),
198 "T_Data_Last tcid=1 · session 1 · profile (9F8011)"
199 );
200 }
201
202 #[test]
203 fn decodes_log_directions() {
204 let log = [
205 LinkEvent::Reset,
206 LinkEvent::Tx(vec![0x82, 0x01, 0x01]),
207 LinkEvent::Rx(vec![0x80, 0x02, 0x01, 0x00]),
208 ];
209 let s = decode_log(&log);
210 assert!(s.contains(" reset()"));
211 assert!(s.contains("W Create_T_C tcid=1"));
212 assert!(s.contains("R T_SB tcid=1 DA=0"));
213 }
214}