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