Skip to main content

embassy_usb/class/dfu/
app_mode.rs

1use embassy_time::{Duration, Instant};
2use embassy_usb_driver::Driver;
3
4use super::consts::{
5    APPN_SPEC_SUBCLASS_DFU, DESC_DFU_FUNCTIONAL, DFU_PROTOCOL_RT, DfuAttributes, Request, State, Status,
6    USB_CLASS_APPN_SPEC,
7};
8use crate::control::{InResponse, OutResponse, Recipient, Request as ControlRequest, RequestType};
9use crate::{Builder, FunctionBuilder};
10
11/// Handler trait for DFU runtime mode.
12///
13/// Implement this trait to handle entering DFU mode.
14pub trait Handler {
15    /// Called when the device should enter DFU mode.
16    ///
17    /// This is called after a valid detach sequence (detach request followed by
18    /// USB reset within the timeout period). The implementation should mark the
19    /// device for DFU mode and perform a system reset.
20    fn enter_dfu(&mut self);
21}
22
23/// Internal state for the DFU class
24pub struct DfuState<H: Handler> {
25    handler: H,
26    state: State,
27    attrs: DfuAttributes,
28    detach_start: Option<Instant>,
29    timeout: Duration,
30}
31
32impl<H: Handler> DfuState<H> {
33    /// Create a new DFU instance to expose a DFU interface.
34    pub fn new(handler: H, attrs: DfuAttributes, timeout: Duration) -> Self {
35        DfuState {
36            handler,
37            state: State::AppIdle,
38            attrs,
39            detach_start: None,
40            timeout,
41        }
42    }
43
44    /// Get a mutable reference to the handler.
45    pub fn handler_mut(&mut self) -> &mut H {
46        &mut self.handler
47    }
48}
49
50impl<H: Handler> crate::Handler for DfuState<H> {
51    fn reset(&mut self) {
52        if let Some(start) = self.detach_start {
53            let delta = Instant::now() - start;
54            trace!(
55                "Received RESET with delta = {}, timeout = {}",
56                delta.as_millis(),
57                self.timeout.as_millis()
58            );
59            if delta < self.timeout {
60                self.handler.enter_dfu();
61            }
62        }
63    }
64
65    fn control_out(&mut self, req: ControlRequest, _: &[u8]) -> Option<OutResponse> {
66        if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
67            return None;
68        }
69
70        trace!("Received out request {:?}", req);
71
72        match Request::try_from(req.request) {
73            Ok(Request::Detach) => {
74                trace!("Received DETACH");
75                self.state = State::AppDetach;
76                self.detach_start = Some(Instant::now());
77                if self.attrs.contains(DfuAttributes::WILL_DETACH) {
78                    trace!("WILL_DETACH set, performing reset");
79                    self.handler.enter_dfu();
80                } else {
81                    trace!("Awaiting USB reset");
82                }
83                Some(OutResponse::Accepted)
84            }
85            _ => None,
86        }
87    }
88
89    fn control_in<'a>(&'a mut self, req: ControlRequest, buf: &'a mut [u8]) -> Option<InResponse<'a>> {
90        if (req.request_type, req.recipient) != (RequestType::Class, Recipient::Interface) {
91            return None;
92        }
93
94        trace!("Received in request {:?}", req);
95
96        match Request::try_from(req.request) {
97            Ok(Request::GetStatus) => {
98                let timeout_ms = self.timeout.as_millis() as u16;
99                buf[0..6].copy_from_slice(&[
100                    Status::Ok as u8,
101                    (timeout_ms & 0xff) as u8,
102                    ((timeout_ms >> 8) & 0xff) as u8,
103                    0x00,
104                    self.state as u8,
105                    0x00,
106                ]);
107                Some(InResponse::Accepted(buf))
108            }
109            _ => None,
110        }
111    }
112}
113
114/// An implementation of the USB DFU 1.1 runtime protocol
115///
116/// This function will add a DFU interface descriptor to the provided Builder, and register the provided Control as a handler for the USB device. The USB builder can be used as normal once this is complete.
117/// The handler is responsive to DFU GetStatus and Detach commands.
118///
119/// Once a detach command, followed by a USB reset is received by the host, a magic number will be written into the bootloader state partition to indicate that
120/// it should expose a DFU device, and a software reset will be issued.
121///
122/// To apply USB DFU updates, the bootloader must be capable of recognizing the DFU magic and exposing a device to handle the full DFU transaction with the host.
123pub fn usb_dfu<'d, D: Driver<'d>, H: Handler>(
124    builder: &mut Builder<'d, D>,
125    state: &'d mut DfuState<H>,
126    func_modifier: impl Fn(&mut FunctionBuilder<'_, 'd, D>),
127) {
128    let mut func = builder.function(0x00, 0x00, 0x00);
129
130    // Here we give users the opportunity to add their own function level MSOS headers for instance.
131    // This is useful when DFU functionality is part of a composite USB device.
132    func_modifier(&mut func);
133
134    let timeout_ms = state.timeout.as_millis() as u16;
135    let mut iface = func.interface();
136    let mut alt = iface.alt_setting(USB_CLASS_APPN_SPEC, APPN_SPEC_SUBCLASS_DFU, DFU_PROTOCOL_RT, None);
137    alt.descriptor(
138        DESC_DFU_FUNCTIONAL,
139        &[
140            state.attrs.bits(),
141            (timeout_ms & 0xff) as u8,
142            ((timeout_ms >> 8) & 0xff) as u8,
143            0x40,
144            0x00, // 64B control buffer size for application side
145            0x10,
146            0x01, // DFU 1.1
147        ],
148    );
149
150    drop(func);
151    builder.handler(state);
152}