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