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 on connect. This is reset-then-halt (the box runs
101    /// OpenOCD's `reset halt` / J-Link's halting connect), which pulses
102    /// nRESET and re-enters through the reset vector — on a part executing
103    /// in place out of QSPI that re-runs the bootloader. There is no
104    /// halt-in-place over the debug service's HTTP API today; on J-Link a
105    /// halt-first script via [`ConnectOptions::jlink_script`] is the
106    /// supported route. Honored on the OpenOCD backend by box >= 0.43.0
107    /// (older boxes pinned it to `false` on that backend; J-Link always
108    /// honored it).
109    pub halt: bool,
110    /// Start a GDB server (needed for reset/read_memory on some backends).
111    pub gdb: bool,
112    /// Contents of a `.JLinkScript` to run for this connection (J-Link
113    /// backend only; ignored by OpenOCD). Rides base64-encoded in the
114    /// request and takes precedence over a script saved on the net.
115    pub jlink_script: Option<Vec<u8>>,
116    /// Contents of an OpenOCD `.cfg` for this connection (OpenOCD backend
117    /// only; ignored by J-Link). Must be a *complete* cfg that selects the
118    /// adapter driver — lager still appends its own `ftdi channel <N>` for
119    /// a net with a probe channel, and that command dies at startup unless
120    /// a cfg has selected the ftdi adapter first. Takes precedence over a
121    /// config saved on the net.
122    pub openocd_config: Option<Vec<u8>>,
123}
124
125impl Default for ConnectOptions {
126    fn default() -> Self {
127        ConnectOptions {
128            speed: None,
129            force: false,
130            halt: false,
131            gdb: true,
132            jlink_script: None,
133            openocd_config: None,
134        }
135    }
136}
137
138/// Default per-operation timeouts, matching the Python `DebugServiceClient`.
139const CONNECT_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(30));
140const FLASH_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(180));
141const ERASE_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(120));
142const RESET_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(10));
143const MEMRD_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(30));
144const QUICK_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(10));
145
146/// Options for [`DebugNet::rtt`] / RTT streaming (one-way and interactive).
147#[derive(Debug, Clone, Copy, Default)]
148pub struct RttOptions {
149    /// RTT channel (0 or 1).
150    pub channel: u32,
151    /// RAM start address for the RTT control-block search (advanced).
152    pub search_addr: Option<u64>,
153    /// Size of the RAM region to search, in bytes (advanced).
154    pub search_size: Option<u64>,
155    /// Read chunk size on the box side (J-Link only, advanced). Only used
156    /// by interactive RTT; the one-way HTTP stream ignores it.
157    pub chunk_size: Option<u64>,
158}
159
160/// Build the JSON body for a debug op: the full net record plus extra params.
161pub(crate) fn debug_body(net_record: &Value, extra: Value) -> Value {
162    let mut obj = serde_json::Map::new();
163    obj.insert("net".to_string(), net_record.clone());
164    if let Value::Object(map) = extra {
165        obj.extend(map);
166    }
167    Value::Object(obj)
168}
169
170/// Shared request-body builders, used by both the blocking and async handles
171/// so the two cannot diverge.
172pub(crate) mod ops {
173    use super::*;
174
175    pub(crate) fn connect(net: &Value, opts: &ConnectOptions) -> (String, Value, Timeout) {
176        let mut extra = json!({
177            "force": opts.force,
178            "halt": opts.halt,
179            "gdb": opts.gdb,
180        });
181        if let Some(speed) = &opts.speed {
182            extra["speed"] = json!(speed);
183        }
184        if let Some(script) = &opts.jlink_script {
185            extra["jlink_script"] = json!(base64_encode(script));
186        }
187        if let Some(cfg) = &opts.openocd_config {
188            extra["openocd_config"] = json!(base64_encode(cfg));
189        }
190        ("/debug/connect".into(), debug_body(net, extra), CONNECT_TIMEOUT)
191    }
192
193    pub(crate) fn disconnect(net: &Value, keep_running: bool) -> (String, Value, Timeout) {
194        (
195            "/debug/disconnect".into(),
196            debug_body(net, json!({ "keep_jlink_running": keep_running })),
197            QUICK_TIMEOUT,
198        )
199    }
200
201    pub(crate) fn reset(net: &Value, halt: bool) -> (String, Value, Timeout) {
202        (
203            "/debug/reset".into(),
204            debug_body(net, json!({ "halt": halt })),
205            RESET_TIMEOUT,
206        )
207    }
208
209    pub(crate) fn erase(net: &Value) -> (String, Value, Timeout) {
210        ("/debug/erase".into(), debug_body(net, json!({})), ERASE_TIMEOUT)
211    }
212
213    pub(crate) fn read_memory(net: &Value, address: u64, length: usize) -> (String, Value, Timeout) {
214        (
215            "/debug/memrd".into(),
216            debug_body(net, json!({ "start_addr": address, "length": length })),
217            MEMRD_TIMEOUT,
218        )
219    }
220
221    pub(crate) fn info(net: &Value) -> (String, Value, Timeout) {
222        ("/debug/info".into(), debug_body(net, json!({})), QUICK_TIMEOUT)
223    }
224
225    pub(crate) fn status(net: &Value) -> (String, Value, Timeout) {
226        ("/debug/status".into(), debug_body(net, json!({})), QUICK_TIMEOUT)
227    }
228
229    /// Build the flash body. `kind` picks the payload field; `address` is
230    /// only used for [`FirmwareKind::Bin`].
231    pub(crate) fn flash(
232        net: &Value,
233        contents: &[u8],
234        kind: FirmwareKind,
235        address: Option<u32>,
236    ) -> (String, Value, Timeout) {
237        let b64 = base64_encode(contents);
238        let payload = match kind {
239            FirmwareKind::Hex => json!({ "hexfile": { "content": b64 } }),
240            FirmwareKind::Elf => json!({ "elffile": { "content": b64 } }),
241            FirmwareKind::Bin => json!({
242                "binfile": { "content": b64, "address": address.unwrap_or(0x0800_0000) }
243            }),
244        };
245        ("/debug/flash".into(), debug_body(net, payload), FLASH_TIMEOUT)
246    }
247
248    /// Build the RTT body. RTT streaming is blocking-only (the async client
249    /// exposes no `rtt()`), so this is unused in async-only builds.
250    #[cfg(feature = "blocking")]
251    pub(crate) fn rtt_body(net: &Value, opts: &RttOptions) -> Value {
252        let mut extra = json!({ "channel": opts.channel, "timeout": Value::Null });
253        if let Some(a) = opts.search_addr {
254            extra["search_addr"] = json!(a);
255        }
256        if let Some(s) = opts.search_size {
257            extra["search_size"] = json!(s);
258        }
259        debug_body(net, extra)
260    }
261}
262
263/// Find the saved record for a debug net by name in a raw nets list.
264pub(crate) fn find_debug_record(records: Vec<Value>, name: &str) -> Result<Value> {
265    let mut wrong_role = false;
266    for rec in records {
267        if rec.get("name").and_then(Value::as_str) == Some(name) {
268            if rec.get("role").and_then(Value::as_str) == Some("debug") {
269                return Ok(rec);
270            }
271            wrong_role = true;
272        }
273    }
274    Err(Error::Box {
275        status: 404,
276        message: if wrong_role {
277            format!("net '{name}' exists but is not a debug net")
278        } else {
279            format!("debug net '{name}' not found on this box")
280        },
281    })
282}
283
284/// Read a firmware file and infer its kind from the extension.
285pub(crate) fn read_firmware(path: &Path) -> Result<(Vec<u8>, FirmwareKind)> {
286    let kind = FirmwareKind::from_path(path)?;
287    let bytes = std::fs::read(path)
288        .map_err(|e| Error::Config(format!("cannot read firmware '{}': {e}", path.display())))?;
289    Ok((bytes, kind))
290}
291
292// ---------------------------------------------------------------------------
293// Blocking handle
294// ---------------------------------------------------------------------------
295
296/// Handle for a debug-probe net (blocking).
297///
298/// Created via [`crate::LagerBox::debug`]. Cheap to construct; the net's
299/// saved record is fetched from the box on first use and cached on the
300/// handle (clones share the cache), so back-to-back debug ops pay for
301/// `/nets/list` once instead of per call. The cache is invalidated when an
302/// operation fails, so a re-saved net record is picked up on retry.
303#[cfg(feature = "blocking")]
304#[derive(Clone)]
305pub struct DebugNet<'a> {
306    pub(crate) client: &'a crate::client::LagerBox,
307    pub(crate) name: String,
308    pub(crate) record: Arc<Mutex<Option<Value>>>,
309}
310
311#[cfg(feature = "blocking")]
312impl DebugNet<'_> {
313    /// Name of the net this handle drives.
314    pub fn name(&self) -> &str {
315        &self.name
316    }
317
318    fn net_record(&self) -> Result<Value> {
319        if let Some(rec) = self.record.lock().unwrap().clone() {
320            return Ok(rec);
321        }
322        let rec = self.client.debug_net_record(&self.name)?;
323        *self.record.lock().unwrap() = Some(rec.clone());
324        Ok(rec)
325    }
326
327    fn call(&self, path: &str, body: Value, timeout: Timeout) -> Result<Value> {
328        let req = wire::debug_request(path, body, timeout);
329        let result = self
330            .client
331            .execute_debug(&req)
332            .and_then(|(status, resp)| wire::parse_debug(status, resp));
333        if result.is_err() {
334            // The failure may be a stale record (net re-saved, probe
335            // reassigned); re-resolve on the next call.
336            *self.record.lock().unwrap() = None;
337        }
338        result
339    }
340
341    /// Connect to the probe with default options (starts a GDB server).
342    pub fn connect(&self) -> Result<DebugConnection> {
343        self.connect_with(&ConnectOptions::default())
344    }
345
346    /// Connect to the probe with explicit options.
347    pub fn connect_with(&self, opts: &ConnectOptions) -> Result<DebugConnection> {
348        let net = self.net_record()?;
349        let (path, body, timeout) = ops::connect(&net, opts);
350        let resp = self.call(&path, body, timeout)?;
351        serde_json::from_value(resp).map_err(Into::into)
352    }
353
354    /// Disconnect. If `keep_running` is true the gdbserver is left running so
355    /// an external GDB client can stay attached.
356    pub fn disconnect(&self, keep_running: bool) -> Result<()> {
357        let net = self.net_record()?;
358        let (path, body, timeout) = ops::disconnect(&net, keep_running);
359        self.call(&path, body, timeout).map(|_| ())
360    }
361
362    /// Reset the target, optionally halting at the reset vector.
363    pub fn reset(&self, halt: bool) -> Result<()> {
364        let net = self.net_record()?;
365        let (path, body, timeout) = ops::reset(&net, halt);
366        self.call(&path, body, timeout).map(|_| ())
367    }
368
369    /// Mass-erase the target flash.
370    pub fn erase(&self) -> Result<()> {
371        let net = self.net_record()?;
372        let (path, body, timeout) = ops::erase(&net);
373        self.call(&path, body, timeout).map(|_| ())
374    }
375
376    /// Flash a firmware file, inferring the type from its extension
377    /// (`.hex`, `.elf`, `.bin`). `.bin` is flashed at `0x08000000`; use
378    /// [`DebugNet::flash_bin`] to choose the address.
379    pub fn flash(&self, firmware_path: impl AsRef<Path>) -> Result<()> {
380        let path = firmware_path.as_ref();
381        let (contents, kind) = read_firmware(path)?;
382        self.flash_bytes(&contents, kind, None)
383    }
384
385    /// Flash a raw binary at an explicit base address.
386    pub fn flash_bin(&self, firmware_path: impl AsRef<Path>, address: u32) -> Result<()> {
387        let contents = std::fs::read(firmware_path.as_ref())
388            .map_err(|e| Error::Config(format!("cannot read firmware: {e}")))?;
389        self.flash_bytes(&contents, FirmwareKind::Bin, Some(address))
390    }
391
392    /// Flash raw firmware bytes of a known kind.
393    pub fn flash_bytes(
394        &self,
395        contents: &[u8],
396        kind: FirmwareKind,
397        address: Option<u32>,
398    ) -> Result<()> {
399        let net = self.net_record()?;
400        let (path, body, timeout) = ops::flash(&net, contents, kind, address);
401        self.call(&path, body, timeout).map(|_| ())
402    }
403
404    /// Read `length` bytes of target memory starting at `address`.
405    pub fn read_memory(&self, address: u64, length: usize) -> Result<Vec<u8>> {
406        let net = self.net_record()?;
407        let (path, body, timeout) = ops::read_memory(&net, address, length);
408        let resp = self.call(&path, body, timeout)?;
409        wire::debug_memory_bytes(&resp)
410    }
411
412    /// Probe/target information (device, arch, backend, connected state).
413    pub fn info(&self) -> Result<DebugInfo> {
414        let net = self.net_record()?;
415        let (path, body, timeout) = ops::info(&net);
416        let resp = self.call(&path, body, timeout)?;
417        serde_json::from_value(resp).map_err(Into::into)
418    }
419
420    /// Whether a gdbserver/daemon is currently running for this probe.
421    pub fn status(&self) -> Result<DebugStatus> {
422        let net = self.net_record()?;
423        let (path, body, timeout) = ops::status(&net);
424        let resp = self.call(&path, body, timeout)?;
425        serde_json::from_value(resp).map_err(Into::into)
426    }
427
428    /// Open an RTT log stream on channel 0.
429    ///
430    /// Returns a reader that yields the target's RTT output as raw bytes
431    /// until the connection closes or the reader is dropped. Wrap it in a
432    /// [`std::io::BufReader`] to read lines.
433    pub fn rtt(&self) -> Result<RttStream> {
434        self.rtt_with(&RttOptions::default())
435    }
436
437    /// Open an RTT log stream with explicit options.
438    pub fn rtt_with(&self, opts: &RttOptions) -> Result<RttStream> {
439        let net = self.net_record()?;
440        let body = ops::rtt_body(&net, opts);
441        let req = wire::debug_request("/debug/rtt", body, Timeout::Unbounded);
442        let reader = self.client.stream_debug(&req)?;
443        Ok(RttStream { reader })
444    }
445
446    /// Open a bi-directional RTT session on channel 0 (feature `rtt`,
447    /// box software >= 0.35.0).
448    ///
449    /// Unlike [`DebugNet::rtt`], the returned session can also **write** to
450    /// the target's RTT down-channel, so firmware that reads commands over
451    /// RTT can be driven from a test:
452    ///
453    /// ```no_run
454    /// # #[cfg(all(feature = "blocking", feature = "rtt"))]
455    /// # fn demo() -> lager::Result<()> {
456    /// use std::time::Duration;
457    /// use lager::LagerBox;
458    ///
459    /// let lager = LagerBox::from_env()?;
460    /// let debug = lager.debug("debug1");
461    /// debug.connect()?;                       // gdbserver must be up first
462    ///
463    /// let mut rtt = debug.rtt_interactive()?;
464    /// rtt.write_str("self_test\n")?;
465    /// rtt.wait_for(b"self_test: pass", Duration::from_secs(5))?;
466    /// # Ok(())
467    /// # }
468    /// # fn main() {}
469    /// ```
470    ///
471    /// Two prerequisites (see [`crate::nets::rtt`] for the full story): the
472    /// gdbserver must already be running — call [`DebugNet::connect`] first —
473    /// and writing needs a firmware-declared RTT **down** buffer on the
474    /// channel (`defmt-rtt` alone only provides the up buffer; without one
475    /// the target silently discards writes).
476    #[cfg(feature = "rtt")]
477    pub fn rtt_interactive(&self) -> Result<crate::nets::rtt::RttSession> {
478        self.rtt_interactive_with(&RttOptions::default())
479    }
480
481    /// Open a bi-directional RTT session with explicit options
482    /// (feature `rtt`). `opts.channel` selects the RTT channel in both
483    /// directions.
484    #[cfg(feature = "rtt")]
485    pub fn rtt_interactive_with(&self, opts: &RttOptions) -> Result<crate::nets::rtt::RttSession> {
486        crate::nets::rtt::RttSession::open(
487            self.client.base_url(),
488            self.name.clone(),
489            opts,
490            self.client.current_token(),
491        )
492    }
493}
494
495/// A live RTT byte stream (blocking). Implements [`std::io::Read`], so it can
496/// be wrapped in a [`std::io::BufReader`] and read line by line.
497#[cfg(feature = "blocking")]
498pub struct RttStream {
499    reader: Box<dyn std::io::Read + Send + Sync>,
500}
501
502#[cfg(feature = "blocking")]
503impl std::io::Read for RttStream {
504    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
505        self.reader.read(buf)
506    }
507}
508
509// ---------------------------------------------------------------------------
510// Async handle
511// ---------------------------------------------------------------------------
512
513/// Handle for a debug-probe net (async).
514///
515/// The net's saved record is cached after first use exactly like the
516/// blocking [`DebugNet`] (invalidated when an operation fails).
517///
518/// RTT streaming is not provided on the async client yet; use the blocking
519/// [`DebugNet::rtt`] for log streaming.
520#[cfg(feature = "async")]
521#[derive(Clone)]
522pub struct AsyncDebugNet<'a> {
523    pub(crate) client: &'a crate::async_client::AsyncLagerBox,
524    pub(crate) name: String,
525    pub(crate) record: Arc<Mutex<Option<Value>>>,
526}
527
528#[cfg(feature = "async")]
529impl AsyncDebugNet<'_> {
530    /// Name of the net this handle drives.
531    pub fn name(&self) -> &str {
532        &self.name
533    }
534
535    async fn net_record(&self) -> Result<Value> {
536        if let Some(rec) = self.record.lock().unwrap().clone() {
537            return Ok(rec);
538        }
539        let rec = self.client.debug_net_record(&self.name).await?;
540        *self.record.lock().unwrap() = Some(rec.clone());
541        Ok(rec)
542    }
543
544    async fn call(&self, path: &str, body: Value, timeout: Timeout) -> Result<Value> {
545        let req = wire::debug_request(path, body, timeout);
546        let result = match self.client.execute_debug(&req).await {
547            Ok((status, resp)) => wire::parse_debug(status, resp),
548            Err(e) => Err(e),
549        };
550        if result.is_err() {
551            // The failure may be a stale record (net re-saved, probe
552            // reassigned); re-resolve on the next call.
553            *self.record.lock().unwrap() = None;
554        }
555        result
556    }
557
558    /// Connect to the probe with default options (starts a GDB server).
559    pub async fn connect(&self) -> Result<DebugConnection> {
560        self.connect_with(&ConnectOptions::default()).await
561    }
562
563    /// Connect to the probe with explicit options.
564    pub async fn connect_with(&self, opts: &ConnectOptions) -> Result<DebugConnection> {
565        let net = self.net_record().await?;
566        let (path, body, timeout) = ops::connect(&net, opts);
567        let resp = self.call(&path, body, timeout).await?;
568        serde_json::from_value(resp).map_err(Into::into)
569    }
570
571    /// Disconnect. If `keep_running` is true the gdbserver is left running.
572    pub async fn disconnect(&self, keep_running: bool) -> Result<()> {
573        let net = self.net_record().await?;
574        let (path, body, timeout) = ops::disconnect(&net, keep_running);
575        self.call(&path, body, timeout).await.map(|_| ())
576    }
577
578    /// Reset the target, optionally halting at the reset vector.
579    pub async fn reset(&self, halt: bool) -> Result<()> {
580        let net = self.net_record().await?;
581        let (path, body, timeout) = ops::reset(&net, halt);
582        self.call(&path, body, timeout).await.map(|_| ())
583    }
584
585    /// Mass-erase the target flash.
586    pub async fn erase(&self) -> Result<()> {
587        let net = self.net_record().await?;
588        let (path, body, timeout) = ops::erase(&net);
589        self.call(&path, body, timeout).await.map(|_| ())
590    }
591
592    /// Flash a firmware file, inferring type from extension.
593    pub async fn flash(&self, firmware_path: impl AsRef<Path>) -> Result<()> {
594        let (contents, kind) = read_firmware(firmware_path.as_ref())?;
595        self.flash_bytes(&contents, kind, None).await
596    }
597
598    /// Flash a raw binary at an explicit base address.
599    pub async fn flash_bin(&self, firmware_path: impl AsRef<Path>, address: u32) -> Result<()> {
600        let contents = std::fs::read(firmware_path.as_ref())
601            .map_err(|e| Error::Config(format!("cannot read firmware: {e}")))?;
602        self.flash_bytes(&contents, FirmwareKind::Bin, Some(address)).await
603    }
604
605    /// Flash raw firmware bytes of a known kind.
606    pub async fn flash_bytes(
607        &self,
608        contents: &[u8],
609        kind: FirmwareKind,
610        address: Option<u32>,
611    ) -> Result<()> {
612        let net = self.net_record().await?;
613        let (path, body, timeout) = ops::flash(&net, contents, kind, address);
614        self.call(&path, body, timeout).await.map(|_| ())
615    }
616
617    /// Read `length` bytes of target memory starting at `address`.
618    pub async fn read_memory(&self, address: u64, length: usize) -> Result<Vec<u8>> {
619        let net = self.net_record().await?;
620        let (path, body, timeout) = ops::read_memory(&net, address, length);
621        let resp = self.call(&path, body, timeout).await?;
622        wire::debug_memory_bytes(&resp)
623    }
624
625    /// Probe/target information.
626    pub async fn info(&self) -> Result<DebugInfo> {
627        let net = self.net_record().await?;
628        let (path, body, timeout) = ops::info(&net);
629        let resp = self.call(&path, body, timeout).await?;
630        serde_json::from_value(resp).map_err(Into::into)
631    }
632
633    /// Whether a gdbserver/daemon is currently running for this probe.
634    pub async fn status(&self) -> Result<DebugStatus> {
635        let net = self.net_record().await?;
636        let (path, body, timeout) = ops::status(&net);
637        let resp = self.call(&path, body, timeout).await?;
638        serde_json::from_value(resp).map_err(Into::into)
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    #[test]
647    fn base64_matches_reference() {
648        assert_eq!(base64_encode(b""), "");
649        assert_eq!(base64_encode(b"f"), "Zg==");
650        assert_eq!(base64_encode(b"fo"), "Zm8=");
651        assert_eq!(base64_encode(b"foo"), "Zm9v");
652        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
653        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
654        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
655        assert_eq!(base64_encode(&[0x00, 0xff, 0x10]), "AP8Q");
656    }
657
658    #[test]
659    fn firmware_kind_from_extension() {
660        assert_eq!(
661            FirmwareKind::from_path(Path::new("a/b/fw.hex")).unwrap(),
662            FirmwareKind::Hex
663        );
664        assert_eq!(
665            FirmwareKind::from_path(Path::new("FW.ELF")).unwrap(),
666            FirmwareKind::Elf
667        );
668        assert!(FirmwareKind::from_path(Path::new("fw.txt")).is_err());
669    }
670
671    #[test]
672    fn debug_body_wraps_net_and_params() {
673        let net = json!({"name": "debug1", "role": "debug", "pin": "nrf52"});
674        let body = debug_body(&net, json!({"halt": true}));
675        assert_eq!(body["net"]["name"], "debug1");
676        assert_eq!(body["halt"], true);
677    }
678}