Skip to main content

lager/nets/
dfu.rs

1//! USB-DFU via box-side `dfu-util` (box-level, not a saved net): list
2//! DFU-capable devices, download firmware, and detach — no host-side USB
3//! tooling required.
4//!
5//! The box must have `dfu-util` installed (`lager box-config apt add
6//! dfu-util`) and serve `POST /usb/dfu` (box software >= 0.33.0; older
7//! boxes fail with [`crate::Error::UnsupportedByBox`]).
8//!
9//! ```no_run
10//! # #[cfg(feature = "blocking")]
11//! # fn demo() -> lager::Result<()> {
12//! use lager::{DfuOptions, LagerBox};
13//!
14//! let lager = LagerBox::from_env()?;
15//! let dfu = lager.dfu();
16//!
17//! // Wait for the DUT to show up in DFU mode, then flash it.
18//! let devices = dfu.list()?;
19//! assert!(devices.iter().any(|d| d.mode == "DFU"));
20//! let firmware = std::fs::read("firmware.bin").expect("read firmware");
21//! dfu.download(
22//!     &firmware,
23//!     &DfuOptions {
24//!         vid_pid: Some("0483:df11".into()),
25//!         alt: Some(0),
26//!         dfuse_address: Some("0x08000000:leave".into()),
27//!         ..Default::default()
28//!     },
29//! )?;
30//! # Ok(())
31//! # }
32//! # fn main() {}
33//! ```
34
35pub use crate::wire::{DfuDevice, DfuOutput};
36
37/// Device selection and transfer options for DFU operations, mirroring the
38/// corresponding `dfu-util` flags. The default selects whatever single DFU
39/// device is on the bus (dfu-util errors out when the selection is
40/// ambiguous).
41#[derive(Debug, Clone, Default)]
42pub struct DfuOptions {
43    /// Match by `vid:pid` (hex, e.g. `"0483:df11"`) — `dfu-util -d`.
44    pub vid_pid: Option<String>,
45    /// Match by device serial — `dfu-util -S`.
46    pub serial: Option<String>,
47    /// Alternate interface setting — `dfu-util -a`.
48    pub alt: Option<u32>,
49    /// DfuSe address (and modifiers, e.g. `"0x08000000:leave"`) —
50    /// `dfu-util -s`. STM32 system-bootloader targets need this.
51    pub dfuse_address: Option<String>,
52    /// Reset the device after download — `dfu-util -R`.
53    pub reset: bool,
54}
55
56pub(crate) mod ops {
57    use std::time::Duration;
58
59    use serde_json::{json, Value};
60
61    use super::DfuOptions;
62    use crate::error::Result;
63    use crate::nets::debug::base64_encode;
64    use crate::wire::{
65        box_command, dfu_unsupported, map_route_missing, value_as, value_list_field, DfuDevice,
66        DfuOutput, Op, Timeout,
67    };
68
69    const PATH: &str = "/usb/dfu";
70
71    /// Box-side dfu-util budget is 120s by default; leave headroom for the
72    /// upload and queueing behind another DFU run.
73    const LIST_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(30));
74    const RUN_TIMEOUT: Timeout = Timeout::After(Duration::from_secs(180));
75
76    fn params(opts: &DfuOptions) -> Value {
77        let mut params = json!({});
78        if let Some(vid_pid) = &opts.vid_pid {
79            params["vid_pid"] = json!(vid_pid);
80        }
81        if let Some(serial) = &opts.serial {
82            params["serial"] = json!(serial);
83        }
84        if let Some(alt) = opts.alt {
85            params["alt"] = json!(alt);
86        }
87        if let Some(addr) = &opts.dfuse_address {
88            params["dfuse_address"] = json!(addr);
89        }
90        if opts.reset {
91            params["reset"] = json!(true);
92        }
93        params
94    }
95
96    pub(crate) fn list() -> Op<Vec<DfuDevice>> {
97        Op {
98            req: box_command(PATH, "list", json!({}), LIST_TIMEOUT),
99            parse: |resp| value_list_field(resp, "devices"),
100        }
101    }
102
103    pub(crate) fn download(firmware: &[u8], opts: &DfuOptions) -> Op<DfuOutput> {
104        let mut p = params(opts);
105        p["firmware"] = json!(base64_encode(firmware));
106        Op {
107            req: box_command(PATH, "download", p, RUN_TIMEOUT),
108            parse: value_as::<DfuOutput>,
109        }
110    }
111
112    pub(crate) fn detach(opts: &DfuOptions) -> Op<DfuOutput> {
113        Op {
114            req: box_command(PATH, "detach", params(opts), RUN_TIMEOUT),
115            parse: value_as::<DfuOutput>,
116        }
117    }
118
119    /// Map a route-missing 404 (box image predating `/usb/dfu`) to
120    /// [`Error::UnsupportedByBox`].
121    pub(crate) fn compat<T>(result: Result<T>) -> Result<T> {
122        result.map_err(|e| map_route_missing(e, dfu_unsupported))
123    }
124}
125
126/// Handle for box-side DFU (from [`crate::LagerBox::dfu`]).
127#[cfg(feature = "blocking")]
128#[derive(Clone)]
129pub struct Dfu<'a> {
130    pub(crate) client: &'a crate::client::LagerBox,
131}
132
133#[cfg(feature = "blocking")]
134impl Dfu<'_> {
135    /// List DFU-capable devices on the box's bus (`dfu-util -l`).
136    pub fn list(&self) -> crate::Result<Vec<DfuDevice>> {
137        ops::compat(self.client.run(ops::list()))
138    }
139
140    /// Download `firmware` to the selected device (`dfu-util -D`). Returns
141    /// the captured dfu-util output.
142    pub fn download(&self, firmware: &[u8], opts: &DfuOptions) -> crate::Result<DfuOutput> {
143        ops::compat(self.client.run(ops::download(firmware, opts)))
144    }
145
146    /// Detach the selected device from DFU mode (`dfu-util -e`).
147    pub fn detach(&self, opts: &DfuOptions) -> crate::Result<DfuOutput> {
148        ops::compat(self.client.run(ops::detach(opts)))
149    }
150}
151
152/// Handle for box-side DFU (from [`crate::AsyncLagerBox::dfu`]).
153#[cfg(feature = "async")]
154#[derive(Clone)]
155pub struct AsyncDfu<'a> {
156    pub(crate) client: &'a crate::async_client::AsyncLagerBox,
157}
158
159#[cfg(feature = "async")]
160impl AsyncDfu<'_> {
161    /// List DFU-capable devices on the box's bus (`dfu-util -l`).
162    pub async fn list(&self) -> crate::Result<Vec<DfuDevice>> {
163        ops::compat(self.client.run(ops::list()).await)
164    }
165
166    /// Download `firmware` to the selected device (`dfu-util -D`). Returns
167    /// the captured dfu-util output.
168    pub async fn download(&self, firmware: &[u8], opts: &DfuOptions) -> crate::Result<DfuOutput> {
169        ops::compat(self.client.run(ops::download(firmware, opts)).await)
170    }
171
172    /// Detach the selected device from DFU mode (`dfu-util -e`).
173    pub async fn detach(&self, opts: &DfuOptions) -> crate::Result<DfuOutput> {
174        ops::compat(self.client.run(ops::detach(opts)).await)
175    }
176}