Skip to main content

ops/
capture.rs

1//! Talking to the card directly: discovery, listening, replay, raw frames.
2
3use crate::util::{
4    await_any_frame, await_reply, hexdump, is_card_frame, is_our_frame, is_sender_frame, mac, open,
5};
6use crate::{protocol, Ctx, Progress};
7use anyhow::{Context, Result};
8use rawlink::{read_pcap, PcapPacket};
9use std::time::{Duration, Instant};
10
11pub fn parse_hex(s: &str) -> Result<Vec<u8>> {
12    let clean: String = s
13        .chars()
14        .filter(|c| !c.is_whitespace() && *c != ',')
15        .collect();
16    anyhow::ensure!(
17        clean.len().is_multiple_of(2),
18        "hex string must have an even length"
19    );
20    (0..clean.len())
21        .step_by(2)
22        .map(|i| u8::from_str_radix(&clean[i..i + 2], 16).context("bad hex"))
23        .collect()
24}
25
26/// Send a hand-built frame and report replies. For probing unknown commands.
27pub fn raw_send(
28    ctx: &Ctx,
29    ty: &str,
30    payload: &str,
31    pad: usize,
32    wait: u64,
33    show: usize,
34    p: &mut dyn Progress,
35) -> Result<()> {
36    let t = parse_hex(ty)?;
37    anyhow::ensure!(t.len() == 2, "--type must be exactly two hex bytes");
38    let mut pl = parse_hex(payload)?;
39    if pl.len() < pad {
40        pl.resize(pad, 0);
41    }
42
43    let mut frame = Vec::with_capacity(14 + pl.len());
44    frame.extend_from_slice(&protocol::CARD_MAC);
45    frame.extend_from_slice(&protocol::SENDER_MAC);
46    frame.extend_from_slice(&t);
47    frame.extend_from_slice(&pl);
48
49    let mut dev = open(ctx)?;
50    dev.send(&frame)?;
51
52    let mut seen = 0;
53    await_reply(&mut dev, Duration::from_secs(wait), |f| {
54        seen += 1;
55        p.out(&format!(
56            "reply {seen}: type {:02x}{:02x}, {} bytes",
57            f[12],
58            f[13],
59            f.len()
60        ));
61        hexdump(p, &f[14..f.len().min(14 + show)]);
62        (seen >= 2).then_some(())
63    })?;
64    if seen == 0 {
65        p.out(&format!("no reply within {wait}s"));
66    }
67    Ok(())
68}
69
70/// A packet's capture time in seconds.
71fn ts(p: &PcapPacket) -> f64 {
72    f64::from(p.ts_sec) + f64::from(p.ts_usec) / 1e6
73}
74
75pub fn pcap_summary(path: &str, dump: bool, p: &mut dyn Progress) -> Result<()> {
76    let pcap = read_pcap(path)?;
77    let pkts: Vec<_> = pcap.packets().collect();
78    p.out(&format!("{} packets", pkts.len()));
79    let t0 = pkts.first().map_or(0.0, ts);
80    let mut counts: std::collections::BTreeMap<(bool, u8), (usize, usize)> =
81        std::collections::BTreeMap::default();
82    for pk in &pkts {
83        let d = pk.data;
84        if d.len() < 14 {
85            continue;
86        }
87        let (dir_tx, ty) = if is_sender_frame(d) {
88            (true, d[12])
89        } else if is_card_frame(d) {
90            (false, d[12])
91        } else {
92            continue;
93        };
94        let e = counts.entry((dir_tx, ty)).or_default();
95        e.0 += 1;
96        e.1 += d.len();
97        if dump && ty != 0x55 && ty != 0x01 && ty != 0x0a {
98            p.out(&format!(
99                "\n[{:9.4}s] {} type 0x{:02x} len {}",
100                ts(pk) - t0,
101                if dir_tx { "PC->card" } else { "card->PC" },
102                ty,
103                d.len()
104            ));
105            hexdump(p, &d[..d.len().min(160)]);
106        }
107    }
108    p.out(&format!(
109        "\n{:<10} {:>6} {:>10}  type",
110        "direction", "count", "bytes"
111    ));
112    for ((tx, ty), (n, bytes)) in counts {
113        p.out(&format!(
114            "{:<10} {:>6} {:>10}  0x{ty:02x}",
115            if tx { "PC->card" } else { "card->PC" },
116            n,
117            bytes
118        ));
119    }
120    Ok(())
121}
122
123pub fn replay(
124    ctx: &Ctx,
125    path: &str,
126    types: Option<&str>,
127    gap_us: u64,
128    all: bool,
129    p: &mut dyn Progress,
130) -> Result<()> {
131    let filter: Option<Vec<u8>> = match types {
132        Some(t) => Some(
133            t.split(',')
134                .map(|s| u8::from_str_radix(s.trim(), 16))
135                .collect::<Result<_, _>>()
136                .context("bad --types list")?,
137        ),
138        None => None,
139    };
140    let pcap = read_pcap(path)?;
141    let mut dev = open(ctx)?;
142    let mut sent = 0usize;
143    for pk in pcap.packets() {
144        let d = pk.data;
145        if !is_sender_frame(d) {
146            continue;
147        }
148        let ty = d[12];
149        let selected = match &filter {
150            Some(f) => f.contains(&ty),
151            None => all || !matches!(ty, 0x55 | 0x01 | 0x0a | 0x07),
152        };
153        if !selected {
154            continue;
155        }
156        dev.send(d)?;
157        sent += 1;
158        std::thread::sleep(Duration::from_micros(gap_us));
159    }
160    p.out(&format!("replayed {sent} frames from {path}"));
161    Ok(())
162}
163
164/// Send one discovery frame and return the first card that answers.
165pub fn discover_one(ctx: &Ctx, wait: u64) -> Result<Option<protocol::DiscoveryInfo>> {
166    let mut dev = open(ctx)?;
167    dev.send(&protocol::discovery())?;
168    await_any_frame(
169        &mut dev,
170        Duration::from_secs(wait),
171        protocol::parse_discovery_response,
172    )
173}
174
175/// One line per card, as `rxp discover` prints it; the model from
176/// `config/cards/` by the id byte, `unknown` when no file carries it.
177#[must_use]
178pub fn describe(info: &protocol::DiscoveryInfo) -> String {
179    let model = receivers::by_id(info.card_id).map_or("unknown", |m| m.name.as_str());
180    format!(
181        "receiver card #{}: id=0x{:02x} model={model} firmware={}.{:02} detected size {}x{}",
182        info.controller, info.card_id, info.ver_major, info.ver_minor, info.cols, info.rows
183    )
184}
185
186/// Send one discovery frame, report every reply as it arrives until `wait`
187/// runs out, and return them. Empty is not an error here; the CLI makes it one.
188pub fn discover(
189    ctx: &Ctx,
190    wait: u64,
191    p: &mut dyn Progress,
192) -> Result<Vec<protocol::DiscoveryInfo>> {
193    discover_all(ctx, wait, |info| p.out(&describe(info)))
194}
195
196/// Send one discovery frame and collect every reply until `wait` runs out,
197/// handing each to `each` as it arrives.
198pub fn discover_all(
199    ctx: &Ctx,
200    wait: u64,
201    mut each: impl FnMut(&protocol::DiscoveryInfo),
202) -> Result<Vec<protocol::DiscoveryInfo>> {
203    let mut dev = open(ctx)?;
204    dev.send(&protocol::discovery())?;
205    let deadline = Instant::now() + Duration::from_secs(wait);
206    let mut found = Vec::new();
207    while Instant::now() < deadline {
208        for f in dev.recv()? {
209            // The kernel loops our own transmissions back.
210            if is_our_frame(f) {
211                continue;
212            }
213            if let Some(info) = protocol::parse_discovery_response(f) {
214                each(&info);
215                found.push(info);
216            }
217        }
218    }
219    Ok(found)
220}
221
222pub fn listen(ctx: &Ctx, wait: u64, include_ours: bool, p: &mut dyn Progress) -> Result<()> {
223    let mut dev = open(ctx)?;
224    let deadline = Instant::now() + Duration::from_secs(wait);
225    while Instant::now() < deadline {
226        for f in dev.recv()? {
227            // Our own transmissions are normally noise, but they are the only
228            // way to confirm a display frame actually reached the wire.
229            if f.len() < 14 || (!include_ours && is_our_frame(f)) {
230                continue;
231            }
232            p.out(&format!(
233                "frame: dst {} src {} type {:02x}{:02x} len {}",
234                mac(&f[0..6]),
235                mac(&f[6..12]),
236                f[12],
237                f[13],
238                f.len()
239            ));
240            hexdump(p, &f[14..f.len().min(14 + 96)]);
241        }
242    }
243    Ok(())
244}