Skip to main content

lager/nets/
debug.rs

1//! Debug-probe nets (J-Link / OpenOCD): flash, erase, reset, memory reads,
2//! and RTT log streaming.
3//!
4//! Unlike the instrument nets, debug talks to the box's dedicated **debug
5//! service on port 8765** (published on the box host), not the port-9000
6//! server. A [`crate::LagerBox`] transparently reaches both: the debug
7//! service lives on the same host, and the crate resolves the debug net's
8//! full saved record from `:9000/nets/list` to hand to the service.
9//!
10//! ```no_run
11//! # #[cfg(feature = "blocking")]
12//! # fn demo() -> lager::Result<()> {
13//! use lager::LagerBox;
14//!
15//! let lager = LagerBox::from_env()?;
16//! let debug = lager.debug("debug1");
17//!
18//! debug.connect()?;
19//! debug.erase()?;
20//! debug.flash("firmware.hex")?;
21//! debug.reset(false)?;
22//! let vector_table = debug.read_memory(0x0800_0000, 16)?;
23//! println!("{vector_table:02x?}");
24//! # Ok(())
25//! # }
26//! # fn main() {}
27//! ```
28
29use std::path::Path;
30use std::sync::{Arc, Mutex};
31use std::time::Duration;
32
33use serde_json::{json, Value};
34
35use crate::error::{Error, Result};
36use crate::wire::{self, DebugConnection, DebugInfo, DebugStatus, Timeout};
37
38/// Base64-encode bytes with the standard alphabet (with padding). Kept
39/// in-crate so firmware upload adds no extra dependency (also used by
40/// [`crate::nets::dfu`]).
41pub(crate) fn base64_encode(input: &[u8]) -> String {
42    const ALPHABET: &[u8; 64] =
43        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
44    let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
45    for chunk in input.chunks(3) {
46        let b0 = chunk[0] as u32;
47        let b1 = *chunk.get(1).unwrap_or(&0) as u32;
48        let b2 = *chunk.get(2).unwrap_or(&0) as u32;
49        let n = (b0 << 16) | (b1 << 8) | b2;
50        out.push(ALPHABET[(n >> 18 & 0x3F) as usize] as char);
51        out.push(ALPHABET[(n >> 12 & 0x3F) as usize] as char);
52        out.push(if chunk.len() > 1 {
53            ALPHABET[(n >> 6 & 0x3F) as usize] as char
54        } else {
55            '='
56        });
57        out.push(if chunk.len() > 2 {
58            ALPHABET[(n & 0x3F) as usize] as char
59        } else {
60            '='
61        });
62    }
63    out
64}
65
66/// Firmware image kind, inferred from the file extension by
67/// [`DebugNet::flash`].
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum FirmwareKind {
70    /// Intel HEX (`.hex`).
71    Hex,
72    /// ELF (`.elf`).
73    Elf,
74    /// Raw binary (`.bin`), flashed at a base address.
75    Bin,
76}
77
78impl FirmwareKind {
79    fn from_path(path: &Path) -> Result<Self> {
80        match path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase) {
81            Some(ext) if ext == "hex" => Ok(FirmwareKind::Hex),
82            Some(ext) if ext == "elf" => Ok(FirmwareKind::Elf),
83            Some(ext) if ext == "bin" => Ok(FirmwareKind::Bin),
84            _ => Err(Error::Config(format!(
85                "cannot infer firmware type from '{}'; use flash_with to set it explicitly",
86                path.display()
87            ))),
88        }
89    }
90}
91
92/// Options for [`DebugNet::connect_with`].
93#[derive(Debug, Clone)]
94pub struct ConnectOptions {
95    /// SWD/JTAG speed (e.g. `"4000"` kHz or `"adaptive"`). `None` uses the
96    /// service default (`adaptive`).
97    pub speed: Option<String>,
98    /// Force a fresh backend start even if one is already running.
99    pub force: bool,
100    /// Halt the target immediately after connecting.
101    pub halt: bool,
102    /// Start a GDB server (needed for reset/read_memory on some backends).
103    pub gdb: bool,
104}
105
106impl Default for ConnectOptions {
107    fn default() -> Self {
108        ConnectOptions {
109            speed: None,
110            force: false,
111            halt: false,
112            gdb: true,
113        }
114    }
115}
116
117/// Default per-operation timeouts, matching the Python `DebugServiceClient`.
118const CONNECT_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(30));
119const FLASH_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(180));
120const ERASE_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(120));
121const RESET_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(10));
122const MEMRD_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(30));
123const QUICK_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(10));
124
125/// Options for [`DebugNet::rtt`] / RTT streaming.
126#[derive(Debug, Clone, Copy, Default)]
127pub struct RttOptions {
128    /// RTT channel (0 or 1).
129    pub channel: u32,
130    /// RAM start address for the RTT control-block search (advanced).
131    pub search_addr: Option<u64>,
132    /// Size of the RAM region to search, in bytes (advanced).
133    pub search_size: Option<u64>,
134}
135
136/// Build the JSON body for a debug op: the full net record plus extra params.
137pub(crate) fn debug_body(net_record: &Value, extra: Value) -> Value {
138    let mut obj = serde_json::Map::new();
139    obj.insert("net".to_string(), net_record.clone());
140    if let Value::Object(map) = extra {
141        obj.extend(map);
142    }
143    Value::Object(obj)
144}
145
146/// Shared request-body builders, used by both the blocking and async handles
147/// so the two cannot diverge.
148pub(crate) mod ops {
149    use super::*;
150
151    pub(crate) fn connect(net: &Value, opts: &ConnectOptions) -> (String, Value, Timeout) {
152        let mut extra = json!({
153            "force": opts.force,
154            "halt": opts.halt,
155            "gdb": opts.gdb,
156        });
157        if let Some(speed) = &opts.speed {
158            extra["speed"] = json!(speed);
159        }
160        ("/debug/connect".into(), debug_body(net, extra), CONNECT_TIMEOUT)
161    }
162
163    pub(crate) fn disconnect(net: &Value, keep_running: bool) -> (String, Value, Timeout) {
164        (
165            "/debug/disconnect".into(),
166            debug_body(net, json!({ "keep_jlink_running": keep_running })),
167            QUICK_TIMEOUT,
168        )
169    }
170
171    pub(crate) fn reset(net: &Value, halt: bool) -> (String, Value, Timeout) {
172        (
173            "/debug/reset".into(),
174            debug_body(net, json!({ "halt": halt })),
175            RESET_TIMEOUT,
176        )
177    }
178
179    pub(crate) fn erase(net: &Value) -> (String, Value, Timeout) {
180        ("/debug/erase".into(), debug_body(net, json!({})), ERASE_TIMEOUT)
181    }
182
183    pub(crate) fn read_memory(net: &Value, address: u64, length: usize) -> (String, Value, Timeout) {
184        (
185            "/debug/memrd".into(),
186            debug_body(net, json!({ "start_addr": address, "length": length })),
187            MEMRD_TIMEOUT,
188        )
189    }
190
191    pub(crate) fn info(net: &Value) -> (String, Value, Timeout) {
192        ("/debug/info".into(), debug_body(net, json!({})), QUICK_TIMEOUT)
193    }
194
195    pub(crate) fn status(net: &Value) -> (String, Value, Timeout) {
196        ("/debug/status".into(), debug_body(net, json!({})), QUICK_TIMEOUT)
197    }
198
199    /// Build the flash body. `kind` picks the payload field; `address` is
200    /// only used for [`FirmwareKind::Bin`].
201    pub(crate) fn flash(
202        net: &Value,
203        contents: &[u8],
204        kind: FirmwareKind,
205        address: Option<u32>,
206    ) -> (String, Value, Timeout) {
207        let b64 = base64_encode(contents);
208        let payload = match kind {
209            FirmwareKind::Hex => json!({ "hexfile": { "content": b64 } }),
210            FirmwareKind::Elf => json!({ "elffile": { "content": b64 } }),
211            FirmwareKind::Bin => json!({
212                "binfile": { "content": b64, "address": address.unwrap_or(0x0800_0000) }
213            }),
214        };
215        ("/debug/flash".into(), debug_body(net, payload), FLASH_TIMEOUT)
216    }
217
218    /// Build the RTT body. RTT streaming is blocking-only (the async client
219    /// exposes no `rtt()`), so this is unused in async-only builds.
220    #[cfg(feature = "blocking")]
221    pub(crate) fn rtt_body(net: &Value, opts: &RttOptions) -> Value {
222        let mut extra = json!({ "channel": opts.channel, "timeout": Value::Null });
223        if let Some(a) = opts.search_addr {
224            extra["search_addr"] = json!(a);
225        }
226        if let Some(s) = opts.search_size {
227            extra["search_size"] = json!(s);
228        }
229        debug_body(net, extra)
230    }
231}
232
233/// Find the saved record for a debug net by name in a raw nets list.
234pub(crate) fn find_debug_record(records: Vec<Value>, name: &str) -> Result<Value> {
235    let mut wrong_role = false;
236    for rec in records {
237        if rec.get("name").and_then(Value::as_str) == Some(name) {
238            if rec.get("role").and_then(Value::as_str) == Some("debug") {
239                return Ok(rec);
240            }
241            wrong_role = true;
242        }
243    }
244    Err(Error::Box {
245        status: 404,
246        message: if wrong_role {
247            format!("net '{name}' exists but is not a debug net")
248        } else {
249            format!("debug net '{name}' not found on this box")
250        },
251    })
252}
253
254/// Read a firmware file and infer its kind from the extension.
255pub(crate) fn read_firmware(path: &Path) -> Result<(Vec<u8>, FirmwareKind)> {
256    let kind = FirmwareKind::from_path(path)?;
257    let bytes = std::fs::read(path)
258        .map_err(|e| Error::Config(format!("cannot read firmware '{}': {e}", path.display())))?;
259    Ok((bytes, kind))
260}
261
262// ---------------------------------------------------------------------------
263// Blocking handle
264// ---------------------------------------------------------------------------
265
266/// Handle for a debug-probe net (blocking).
267///
268/// Created via [`crate::LagerBox::debug`]. Cheap to construct; the net's
269/// saved record is fetched from the box on first use and cached on the
270/// handle (clones share the cache), so back-to-back debug ops pay for
271/// `/nets/list` once instead of per call. The cache is invalidated when an
272/// operation fails, so a re-saved net record is picked up on retry.
273#[cfg(feature = "blocking")]
274#[derive(Clone)]
275pub struct DebugNet<'a> {
276    pub(crate) client: &'a crate::client::LagerBox,
277    pub(crate) name: String,
278    pub(crate) record: Arc<Mutex<Option<Value>>>,
279}
280
281#[cfg(feature = "blocking")]
282impl DebugNet<'_> {
283    /// Name of the net this handle drives.
284    pub fn name(&self) -> &str {
285        &self.name
286    }
287
288    fn net_record(&self) -> Result<Value> {
289        if let Some(rec) = self.record.lock().unwrap().clone() {
290            return Ok(rec);
291        }
292        let rec = self.client.debug_net_record(&self.name)?;
293        *self.record.lock().unwrap() = Some(rec.clone());
294        Ok(rec)
295    }
296
297    fn call(&self, path: &str, body: Value, timeout: Timeout) -> Result<Value> {
298        let req = wire::debug_request(path, body, timeout);
299        let result = self
300            .client
301            .execute_debug(&req)
302            .and_then(|(status, resp)| wire::parse_debug(status, resp));
303        if result.is_err() {
304            // The failure may be a stale record (net re-saved, probe
305            // reassigned); re-resolve on the next call.
306            *self.record.lock().unwrap() = None;
307        }
308        result
309    }
310
311    /// Connect to the probe with default options (starts a GDB server).
312    pub fn connect(&self) -> Result<DebugConnection> {
313        self.connect_with(&ConnectOptions::default())
314    }
315
316    /// Connect to the probe with explicit options.
317    pub fn connect_with(&self, opts: &ConnectOptions) -> Result<DebugConnection> {
318        let net = self.net_record()?;
319        let (path, body, timeout) = ops::connect(&net, opts);
320        let resp = self.call(&path, body, timeout)?;
321        serde_json::from_value(resp).map_err(Into::into)
322    }
323
324    /// Disconnect. If `keep_running` is true the gdbserver is left running so
325    /// an external GDB client can stay attached.
326    pub fn disconnect(&self, keep_running: bool) -> Result<()> {
327        let net = self.net_record()?;
328        let (path, body, timeout) = ops::disconnect(&net, keep_running);
329        self.call(&path, body, timeout).map(|_| ())
330    }
331
332    /// Reset the target, optionally halting at the reset vector.
333    pub fn reset(&self, halt: bool) -> Result<()> {
334        let net = self.net_record()?;
335        let (path, body, timeout) = ops::reset(&net, halt);
336        self.call(&path, body, timeout).map(|_| ())
337    }
338
339    /// Mass-erase the target flash.
340    pub fn erase(&self) -> Result<()> {
341        let net = self.net_record()?;
342        let (path, body, timeout) = ops::erase(&net);
343        self.call(&path, body, timeout).map(|_| ())
344    }
345
346    /// Flash a firmware file, inferring the type from its extension
347    /// (`.hex`, `.elf`, `.bin`). `.bin` is flashed at `0x08000000`; use
348    /// [`DebugNet::flash_bin`] to choose the address.
349    pub fn flash(&self, firmware_path: impl AsRef<Path>) -> Result<()> {
350        let path = firmware_path.as_ref();
351        let (contents, kind) = read_firmware(path)?;
352        self.flash_bytes(&contents, kind, None)
353    }
354
355    /// Flash a raw binary at an explicit base address.
356    pub fn flash_bin(&self, firmware_path: impl AsRef<Path>, address: u32) -> Result<()> {
357        let contents = std::fs::read(firmware_path.as_ref())
358            .map_err(|e| Error::Config(format!("cannot read firmware: {e}")))?;
359        self.flash_bytes(&contents, FirmwareKind::Bin, Some(address))
360    }
361
362    /// Flash raw firmware bytes of a known kind.
363    pub fn flash_bytes(
364        &self,
365        contents: &[u8],
366        kind: FirmwareKind,
367        address: Option<u32>,
368    ) -> Result<()> {
369        let net = self.net_record()?;
370        let (path, body, timeout) = ops::flash(&net, contents, kind, address);
371        self.call(&path, body, timeout).map(|_| ())
372    }
373
374    /// Read `length` bytes of target memory starting at `address`.
375    pub fn read_memory(&self, address: u64, length: usize) -> Result<Vec<u8>> {
376        let net = self.net_record()?;
377        let (path, body, timeout) = ops::read_memory(&net, address, length);
378        let resp = self.call(&path, body, timeout)?;
379        wire::debug_memory_bytes(&resp)
380    }
381
382    /// Probe/target information (device, arch, backend, connected state).
383    pub fn info(&self) -> Result<DebugInfo> {
384        let net = self.net_record()?;
385        let (path, body, timeout) = ops::info(&net);
386        let resp = self.call(&path, body, timeout)?;
387        serde_json::from_value(resp).map_err(Into::into)
388    }
389
390    /// Whether a gdbserver/daemon is currently running for this probe.
391    pub fn status(&self) -> Result<DebugStatus> {
392        let net = self.net_record()?;
393        let (path, body, timeout) = ops::status(&net);
394        let resp = self.call(&path, body, timeout)?;
395        serde_json::from_value(resp).map_err(Into::into)
396    }
397
398    /// Open an RTT log stream on channel 0.
399    ///
400    /// Returns a reader that yields the target's RTT output as raw bytes
401    /// until the connection closes or the reader is dropped. Wrap it in a
402    /// [`std::io::BufReader`] to read lines.
403    pub fn rtt(&self) -> Result<RttStream> {
404        self.rtt_with(&RttOptions::default())
405    }
406
407    /// Open an RTT log stream with explicit options.
408    pub fn rtt_with(&self, opts: &RttOptions) -> Result<RttStream> {
409        let net = self.net_record()?;
410        let body = ops::rtt_body(&net, opts);
411        let req = wire::debug_request("/debug/rtt", body, Timeout::Unbounded);
412        let reader = self.client.stream_debug(&req)?;
413        Ok(RttStream { reader })
414    }
415}
416
417/// A live RTT byte stream (blocking). Implements [`std::io::Read`], so it can
418/// be wrapped in a [`std::io::BufReader`] and read line by line.
419#[cfg(feature = "blocking")]
420pub struct RttStream {
421    reader: Box<dyn std::io::Read + Send + Sync>,
422}
423
424#[cfg(feature = "blocking")]
425impl std::io::Read for RttStream {
426    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
427        self.reader.read(buf)
428    }
429}
430
431// ---------------------------------------------------------------------------
432// Async handle
433// ---------------------------------------------------------------------------
434
435/// Handle for a debug-probe net (async).
436///
437/// The net's saved record is cached after first use exactly like the
438/// blocking [`DebugNet`] (invalidated when an operation fails).
439///
440/// RTT streaming is not provided on the async client yet; use the blocking
441/// [`DebugNet::rtt`] for log streaming.
442#[cfg(feature = "async")]
443#[derive(Clone)]
444pub struct AsyncDebugNet<'a> {
445    pub(crate) client: &'a crate::async_client::AsyncLagerBox,
446    pub(crate) name: String,
447    pub(crate) record: Arc<Mutex<Option<Value>>>,
448}
449
450#[cfg(feature = "async")]
451impl AsyncDebugNet<'_> {
452    /// Name of the net this handle drives.
453    pub fn name(&self) -> &str {
454        &self.name
455    }
456
457    async fn net_record(&self) -> Result<Value> {
458        if let Some(rec) = self.record.lock().unwrap().clone() {
459            return Ok(rec);
460        }
461        let rec = self.client.debug_net_record(&self.name).await?;
462        *self.record.lock().unwrap() = Some(rec.clone());
463        Ok(rec)
464    }
465
466    async fn call(&self, path: &str, body: Value, timeout: Timeout) -> Result<Value> {
467        let req = wire::debug_request(path, body, timeout);
468        let result = match self.client.execute_debug(&req).await {
469            Ok((status, resp)) => wire::parse_debug(status, resp),
470            Err(e) => Err(e),
471        };
472        if result.is_err() {
473            // The failure may be a stale record (net re-saved, probe
474            // reassigned); re-resolve on the next call.
475            *self.record.lock().unwrap() = None;
476        }
477        result
478    }
479
480    /// Connect to the probe with default options (starts a GDB server).
481    pub async fn connect(&self) -> Result<DebugConnection> {
482        self.connect_with(&ConnectOptions::default()).await
483    }
484
485    /// Connect to the probe with explicit options.
486    pub async fn connect_with(&self, opts: &ConnectOptions) -> Result<DebugConnection> {
487        let net = self.net_record().await?;
488        let (path, body, timeout) = ops::connect(&net, opts);
489        let resp = self.call(&path, body, timeout).await?;
490        serde_json::from_value(resp).map_err(Into::into)
491    }
492
493    /// Disconnect. If `keep_running` is true the gdbserver is left running.
494    pub async fn disconnect(&self, keep_running: bool) -> Result<()> {
495        let net = self.net_record().await?;
496        let (path, body, timeout) = ops::disconnect(&net, keep_running);
497        self.call(&path, body, timeout).await.map(|_| ())
498    }
499
500    /// Reset the target, optionally halting at the reset vector.
501    pub async fn reset(&self, halt: bool) -> Result<()> {
502        let net = self.net_record().await?;
503        let (path, body, timeout) = ops::reset(&net, halt);
504        self.call(&path, body, timeout).await.map(|_| ())
505    }
506
507    /// Mass-erase the target flash.
508    pub async fn erase(&self) -> Result<()> {
509        let net = self.net_record().await?;
510        let (path, body, timeout) = ops::erase(&net);
511        self.call(&path, body, timeout).await.map(|_| ())
512    }
513
514    /// Flash a firmware file, inferring type from extension.
515    pub async fn flash(&self, firmware_path: impl AsRef<Path>) -> Result<()> {
516        let (contents, kind) = read_firmware(firmware_path.as_ref())?;
517        self.flash_bytes(&contents, kind, None).await
518    }
519
520    /// Flash a raw binary at an explicit base address.
521    pub async fn flash_bin(&self, firmware_path: impl AsRef<Path>, address: u32) -> Result<()> {
522        let contents = std::fs::read(firmware_path.as_ref())
523            .map_err(|e| Error::Config(format!("cannot read firmware: {e}")))?;
524        self.flash_bytes(&contents, FirmwareKind::Bin, Some(address)).await
525    }
526
527    /// Flash raw firmware bytes of a known kind.
528    pub async fn flash_bytes(
529        &self,
530        contents: &[u8],
531        kind: FirmwareKind,
532        address: Option<u32>,
533    ) -> Result<()> {
534        let net = self.net_record().await?;
535        let (path, body, timeout) = ops::flash(&net, contents, kind, address);
536        self.call(&path, body, timeout).await.map(|_| ())
537    }
538
539    /// Read `length` bytes of target memory starting at `address`.
540    pub async fn read_memory(&self, address: u64, length: usize) -> Result<Vec<u8>> {
541        let net = self.net_record().await?;
542        let (path, body, timeout) = ops::read_memory(&net, address, length);
543        let resp = self.call(&path, body, timeout).await?;
544        wire::debug_memory_bytes(&resp)
545    }
546
547    /// Probe/target information.
548    pub async fn info(&self) -> Result<DebugInfo> {
549        let net = self.net_record().await?;
550        let (path, body, timeout) = ops::info(&net);
551        let resp = self.call(&path, body, timeout).await?;
552        serde_json::from_value(resp).map_err(Into::into)
553    }
554
555    /// Whether a gdbserver/daemon is currently running for this probe.
556    pub async fn status(&self) -> Result<DebugStatus> {
557        let net = self.net_record().await?;
558        let (path, body, timeout) = ops::status(&net);
559        let resp = self.call(&path, body, timeout).await?;
560        serde_json::from_value(resp).map_err(Into::into)
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    #[test]
569    fn base64_matches_reference() {
570        assert_eq!(base64_encode(b""), "");
571        assert_eq!(base64_encode(b"f"), "Zg==");
572        assert_eq!(base64_encode(b"fo"), "Zm8=");
573        assert_eq!(base64_encode(b"foo"), "Zm9v");
574        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
575        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
576        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
577        assert_eq!(base64_encode(&[0x00, 0xff, 0x10]), "AP8Q");
578    }
579
580    #[test]
581    fn firmware_kind_from_extension() {
582        assert_eq!(
583            FirmwareKind::from_path(Path::new("a/b/fw.hex")).unwrap(),
584            FirmwareKind::Hex
585        );
586        assert_eq!(
587            FirmwareKind::from_path(Path::new("FW.ELF")).unwrap(),
588            FirmwareKind::Elf
589        );
590        assert!(FirmwareKind::from_path(Path::new("fw.txt")).is_err());
591    }
592
593    #[test]
594    fn debug_body_wraps_net_and_params() {
595        let net = json!({"name": "debug1", "role": "debug", "pin": "nrf52"});
596        let body = debug_body(&net, json!({"halt": true}));
597        assert_eq!(body["net"]["name"], "debug1");
598        assert_eq!(body["halt"], true);
599    }
600}