bose_dfu/
protocol.rs

1use byteorder::{ByteOrder, ReadBytesExt, WriteBytesExt, LE};
2use hidapi::{HidDevice, HidError};
3use log::{info, trace};
4use num_enum::TryFromPrimitive;
5use std::convert::TryFrom;
6use std::io::{Read, Write};
7use std::thread::sleep;
8use std::time::Duration;
9use thiserror::Error;
10
11const XFER_HEADER_SIZE: usize = 5;
12// Gathered from USB captures. Probably corresponds to a 1024-byte internal buffer in the firmware.
13const XFER_DATA_SIZE: usize = 1017;
14
15/// Download (i.e. write firmware to) the device. `device` must be in DFU mode. `file` should
16/// contain only the firmware payload to be written, with any DFU header stripped off.
17pub fn download(device: &HidDevice, file: &mut impl Read) -> Result<(), Error> {
18    let mut report = vec![];
19
20    let mut block_num = 0u16;
21    let mut prev_delay = Duration::from_millis(0);
22    loop {
23        report.clear();
24        // Reserve 1 byte report ID + header to be filled later.
25        report.resize(1 + XFER_HEADER_SIZE, 0u8);
26
27        // Fill the rest with data from the file.
28        let data_size = file.take(XFER_DATA_SIZE as _).read_to_end(&mut report)?;
29
30        // Construct header
31        let mut cursor = std::io::Cursor::new(&mut report);
32        cursor.write_u8(DfuReportId::UploadDownload as _).unwrap();
33        cursor.write_u8(DfuRequest::DFU_DNLOAD as _).unwrap();
34        cursor.write_u16::<LE>(block_num).unwrap();
35        cursor.write_u16::<LE>(data_size as u16).unwrap();
36        assert!(cursor.position() == (1 + XFER_HEADER_SIZE) as _); // Add 1 for report ID
37
38        device
39            .send_feature_report(&report)
40            .map_err(|e| Error::DeviceIoError {
41                source: e,
42                action: "sending firmware data chunk",
43            })?;
44
45        // This emulates the behavior of the official updater, as far as I can tell, but is not
46        // compliant with the DFU spec. If the device needs more time, it's supposed to respond
47        // to a status request here with a status of dfuDNLOAD_BUSY or dfuMANIFEST with
48        // bwPollTimeout set to the number of milliseconds it needs. However, my speaker (SoundLink
49        // Color II) appears to stop responding to requests immediately after receiving the last
50        // (empty) block without waiting for a status request. Instead, it communicates how long
51        // it needs in its *previous* status response (that is, its response to the last non-empty
52        // block). That's why we have to persist prev_delay across loop iterations.
53        //
54        // Notably, although the device does also set bwPollTimeout for non-final blocks, the
55        // official updater seems to completely ignore those values and instead just rely on the
56        // device to bake the necessary delay into its GET_STATUS response latency. We do the same.
57        if data_size == 0 {
58            info!(
59                "Waiting {:?}, as requested by device, for firmware to manifest",
60                prev_delay
61            );
62            sleep(prev_delay);
63        }
64
65        let status = DfuStatusResult::read_from_device(device)?;
66        status.ensure_ok()?;
67
68        prev_delay = Duration::from_millis(status.poll_timeout as _);
69
70        trace!(
71            "Successfully downloaded block {:#06x} ({} bytes)",
72            block_num,
73            data_size
74        );
75
76        if data_size == 0 {
77            // Empty read means we're done, device should now be idle.
78            status.ensure_state(DfuState::dfuIDLE)?;
79            break;
80        } else {
81            status.ensure_state(DfuState::dfuDNLOAD_IDLE)?;
82        }
83
84        block_num = match block_num.checked_add(1) {
85            Some(i) => i,
86            None => return Err(ProtocolError::FileTooLarge.into()),
87        };
88    }
89
90    Ok(())
91}
92
93/// Upload (i.e. read firmware from) the device. `device` must be in DFU mode. No processing is
94/// done on the data written to `file` (for example, a DFU suffix is not added).
95pub fn upload(device: &HidDevice, file: &mut impl Write) -> Result<(), Error> {
96    // 1 byte report ID + header + data
97    let mut report = [0u8; 1 + XFER_HEADER_SIZE + XFER_DATA_SIZE];
98
99    loop {
100        // Zero out the report each time through to protect against hidapi bugs.
101        report.fill(0u8);
102
103        report[0] = DfuReportId::UploadDownload as u8;
104        let report_size = map_gfr(
105            device.get_feature_report(&mut report),
106            1 + XFER_HEADER_SIZE,
107            "reading firmware data chunk",
108        )?;
109
110        let status = DfuStatusResult::read_from_device(device)?;
111        status.ensure_ok()?;
112
113        let data_size = LE::read_u16(&report[1..3]) as usize;
114        let data_start = 1 + XFER_HEADER_SIZE;
115
116        if report_size < data_start + data_size {
117            return Err(ProtocolError::ReportTooShort {
118                expected: data_start + data_size,
119                actual: report_size,
120            }
121            .into());
122        }
123
124        trace!("Successfully uploaded block ({} bytes)", data_size);
125
126        file.write_all(&report[data_start..data_start + data_size])?;
127
128        if data_size != XFER_DATA_SIZE {
129            // Short read means we're done, device should now be idle.
130            status.ensure_state(DfuState::dfuIDLE)?;
131            break;
132        } else {
133            status.ensure_state(DfuState::dfuUPLOAD_IDLE)?;
134        }
135    }
136
137    Ok(())
138}
139
140/// Pieces of information that Bose's normal firmware exposes.
141#[non_exhaustive]
142pub enum InfoField {
143    DeviceModel,
144    SerialNumber,
145    CurrentFirmware,
146}
147
148/// Read an information field (as listed in [InfoField]) from the normal firmware. `device` must
149/// NOT be in DFU mode.
150pub fn read_info_field(device: &HidDevice, field: InfoField) -> Result<String, Error> {
151    const INFO_REPORT_ID: u8 = 2;
152    const INFO_REPORT_LEN: usize = 126;
153
154    use InfoField::*;
155
156    // 1 byte report ID + 2 bytes field ID + 1 byte NUL
157    let mut request_report = [0u8; 1 + 2 + 1];
158
159    // Packet captures indicate that "lc" is also a valid field type for some devices, but on mine
160    // it always returns a bus error (both when I send it and when the official updater does).
161    request_report[0] = INFO_REPORT_ID;
162    request_report[1..3].copy_from_slice(match field {
163        DeviceModel => b"pl",
164        SerialNumber => b"sn",
165        CurrentFirmware => b"vr",
166    });
167
168    device
169        .send_feature_report(&request_report)
170        .map_err(|e| Error::DeviceIoError {
171            source: e,
172            action: "requesting info field",
173        })?;
174
175    let mut response_report = [0u8; 1 + INFO_REPORT_LEN];
176    response_report[0] = INFO_REPORT_ID;
177    map_gfr(
178        device.get_feature_report(&mut response_report),
179        1,
180        "reading info field",
181    )?;
182
183    // Result is all the bytes after the report ID and before the first NUL.
184    let result = response_report[1..].split(|&x| x == 0).next().unwrap();
185
186    Ok(std::str::from_utf8(result)
187        .map_err(|e| Error::ProtocolError(e.into()))?
188        .to_owned())
189}
190
191/// Put a device running the normal firmware into DFU mode. `device` must NOT be in DFU mode.
192pub fn enter_dfu(device: &HidDevice) -> Result<(), Error> {
193    const ENTER_DFU_REPORT_ID: u8 = 1;
194
195    device
196        .send_feature_report(&[ENTER_DFU_REPORT_ID, 0xb0, 0x07]) // Magic
197        .map_err(|e| Error::DeviceIoError {
198            source: e,
199            action: "entering DFU mode",
200        })
201}
202
203/// Switch back to the normal firmware. `device` must be in DFU mode.
204pub fn leave_dfu(device: &HidDevice) -> Result<(), Error> {
205    device
206        .send_feature_report(&[DfuReportId::StateCmd as u8, DfuRequest::BOSE_EXIT_DFU as u8])
207        .map_err(|e| Error::DeviceIoError {
208            source: e,
209            action: "leaving DFU mode",
210        })
211}
212
213/// Attempt to transition the device to the [dfuIDLE](DfuState::dfuIDLE) state. If we can't or
214/// don't know how to, return an error. `device` must be in DFU mode.
215pub fn ensure_idle(device: &HidDevice) -> Result<(), Error> {
216    use DfuState::*;
217
218    let status = DfuStatusResult::read_from_device(device)?;
219    match status.state {
220        dfuIDLE => return Ok(()),
221        dfuDNLOAD_SYNC | dfuDNLOAD_IDLE | dfuMANIFEST_SYNC | dfuUPLOAD_IDLE => {
222            info!(
223                "Device not idle, state = {:?}; sending DFU_ABORT",
224                status.state
225            );
226
227            device
228                .send_feature_report(&[DfuReportId::StateCmd as u8, DfuRequest::DFU_ABORT as u8])
229                .map_err(|e| Error::DeviceIoError {
230                    source: e,
231                    action: "sending DFU_ABORT",
232                })?;
233        }
234        dfuERROR => {
235            info!(
236                "Device in error state, status = {:?} ({}); sending DFU_CLRSTATUS",
237                status.status,
238                status.status.error_str()
239            );
240
241            device
242                .send_feature_report(&[
243                    DfuReportId::StateCmd as u8,
244                    DfuRequest::DFU_CLRSTATUS as u8,
245                ])
246                .map_err(|e| Error::DeviceIoError {
247                    source: e,
248                    action: "sending DFU_CLRSTATUS",
249                })?;
250        }
251        _ => return Err(ProtocolError::BadInitialState(status.state).into()),
252    };
253
254    // If we had to send a request, ensure it succeeded and we're now idle.
255    let status = DfuStatusResult::read_from_device(device)?;
256    status.ensure_ok()?;
257    status.ensure_state(dfuIDLE).map_err(Into::into)
258}
259
260#[repr(u8)]
261enum DfuReportId {
262    // Getting this descriptor executes DFU_UPLOAD, returning its payload
263    // appended to a five-byte header containing the 16-bit, little-endian
264    // payload length followed by three unknown bytes ([0x00, 0x00, 0x5d] in
265    // my tests).
266    // Setting it executes DFU_DNLOAD, taking request data consisting of the
267    // payload appended to a five-byte header containing (in order) the
268    // constant byte 0x01 (= DFU_DNLOAD); the 16-bit, little-endian block
269    // number; and the 16-bit, little-endian payload length.
270    UploadDownload = 1,
271
272    // Getting this descriptor executes DFU_GETSTATUS and returns its payload.
273    // Setting it appears to always fail.
274    GetStatus = 2,
275
276    // Getting this descriptor executes DFU_GETSTATE and returns its payload.
277    // Setting it executes a DFU request identified by the first byte of the
278    // request data. DFU_CLRSTATUS and DFU_ABORT can be executed this way, and
279    // possibly others too.
280    StateCmd = 3,
281}
282
283/// Status codes a DFU device can return, taken from the USB DFU 1.1 spec.
284#[repr(u8)]
285#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)]
286#[allow(non_camel_case_types)] // Names from DFU spec
287pub enum DfuStatus {
288    OK = 0x00,
289    errTARGET = 0x01,
290    errFILE = 0x02,
291    errWRITE = 0x03,
292    errERASE = 0x04,
293    errCHECK_ERASED = 0x05,
294    errPROG = 0x06,
295    errVERIFY = 0x07,
296    errADDRESS = 0x08,
297    errNOTDONE = 0x09,
298    errFIRMWARE = 0x0a,
299    errVENDOR = 0x0b,
300    errUSBR = 0x0c,
301    errPOR = 0x0d,
302    errUNKNOWN = 0x0e,
303    errSTALLEDPKT = 0x0f,
304}
305
306impl DfuStatus {
307    pub fn error_str(&self) -> &'static str {
308        use DfuStatus::*;
309        match self {
310            OK => "No error condition is present.",
311            errTARGET => "File is not targeted for use by this device.",
312            errFILE => "File is for this device but fails some vendor-specific verification test.",
313            errWRITE => "Device is unable to write memory.",
314            errERASE => "Memory erase function failed.",
315            errCHECK_ERASED => "Memory erase check failed.",
316            errPROG => "Program memory function failed.",
317            errVERIFY => "Programmed memory failed verification.",
318            errADDRESS => "Cannot program memory due to received address that is out of range.",
319            errNOTDONE => "Received DFU_DNLOAD with wLength = 0, but device does not think it has all of the data yet.",
320            errFIRMWARE => "Device's firmware is corrupt. It cannot return to run-time (non-DFU) operations.",
321            errVENDOR => "iString indicates a vendor-specific error.",
322            errUSBR => "Device detected unexpected USB reset signaling.",
323            errPOR => "Device detected unexpected power on reset.",
324            errUNKNOWN => "Something went wrong, but the device does not know what it was.",
325            errSTALLEDPKT => "Device stalled an unexpected request.",
326        }
327    }
328}
329
330/// States a DFU device can be in, taken from the USB DFU 1.1 spec.
331#[repr(u8)]
332#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)]
333#[allow(non_camel_case_types)] // Names from DFU spec
334pub enum DfuState {
335    appIDLE = 0,
336    appDETACH = 1,
337    dfuIDLE = 2,
338    dfuDNLOAD_SYNC = 3,
339    dfuDNBUSY = 4,
340    dfuDNLOAD_IDLE = 5,
341    dfuMANIFEST_SYNC = 6,
342    dfuMANIFEST = 7,
343    dfuMANIFEST_WAIT_RESET = 8,
344    dfuUPLOAD_IDLE = 9,
345    dfuERROR = 10,
346}
347
348impl DfuState {
349    #[allow(dead_code)]
350    fn read_from_device(device: &HidDevice) -> Result<Self, Error> {
351        let mut report = [0u8; 1 + 1]; // 1 byte report ID + 1 byte state
352        report[0] = DfuReportId::StateCmd as u8;
353        map_gfr(
354            device.get_feature_report(&mut report),
355            report.len(),
356            "querying state",
357        )?;
358
359        Self::try_from(report[1]).map_err(|e| ProtocolError::UnknownState(e.number).into())
360    }
361
362    fn ensure(self, expected: Self) -> Result<(), ProtocolError> {
363        if self != expected {
364            Err(ProtocolError::UnexpectedState {
365                expected,
366                actual: self,
367            })
368        } else {
369            Ok(())
370        }
371    }
372}
373
374#[repr(u8)]
375#[allow(non_camel_case_types)] // Names from DFU spec
376#[allow(dead_code)] // All entries from spec included for completeness
377enum DfuRequest {
378    DFU_DETACH = 0,
379    DFU_DNLOAD = 1,
380    DFU_UPLOAD = 2,
381    DFU_GETSTATUS = 3,
382    DFU_CLRSTATUS = 4,
383    DFU_GETSTATE = 5,
384    DFU_ABORT = 6,
385    BOSE_EXIT_DFU = 0xff, // Custom, not from DFU spec
386}
387
388#[derive(Copy, Clone, Debug)]
389struct DfuStatusResult {
390    pub status: DfuStatus,
391    pub state: DfuState,
392    pub poll_timeout: u32,
393}
394
395impl DfuStatusResult {
396    fn read_from_device(device: &HidDevice) -> Result<Self, Error> {
397        let mut report = [0u8; 1 + 6]; // 1 byte report ID + 6 bytes status
398        report[0] = DfuReportId::GetStatus as u8;
399        map_gfr(
400            device.get_feature_report(&mut report),
401            report.len(),
402            "querying status",
403        )?;
404
405        let mut cursor = std::io::Cursor::new(report);
406        cursor.set_position(1); // Skip report number
407
408        let status = DfuStatus::try_from(cursor.read_u8().unwrap())
409            .map_err(|e| ProtocolError::UnknownState(e.number))?;
410        let poll_timeout = cursor.read_u24::<LE>().unwrap();
411        let state = DfuState::try_from(cursor.read_u8().unwrap())
412            .map_err(|e| ProtocolError::UnknownStatus(e.number))?;
413
414        Ok(Self {
415            status,
416            poll_timeout,
417            state,
418        })
419    }
420
421    fn ensure_ok(&self) -> Result<(), ProtocolError> {
422        if self.status != DfuStatus::OK {
423            Err(ProtocolError::ErrorStatus(self.status))
424        } else {
425            Ok(())
426        }
427    }
428
429    fn ensure_state(&self, expected: DfuState) -> Result<(), ProtocolError> {
430        self.state.ensure(expected)
431    }
432}
433
434/// Map the result of get_feature_report() into an appropriate error if it failed or was too short.
435fn map_gfr(
436    r: Result<usize, HidError>,
437    min_size: usize,
438    action: &'static str,
439) -> Result<usize, Error> {
440    match r {
441        Err(e) => Err(Error::DeviceIoError { source: e, action }),
442        Ok(s) if s < min_size => Err(ProtocolError::ReportTooShort {
443            expected: min_size,
444            actual: s,
445        }
446        .into()),
447        Ok(s) => Ok(s),
448    }
449}
450
451/// All errors (protocol and I/O) that can happen during a DFU operation.
452#[derive(Error, Debug)]
453#[non_exhaustive]
454pub enum Error {
455    #[error("DFU protocol error")]
456    ProtocolError(#[from] ProtocolError),
457
458    #[error("USB transaction error while {action}")]
459    DeviceIoError {
460        source: HidError,
461        action: &'static str,
462    },
463
464    #[error("file I/O error")]
465    FileIoError(#[from] std::io::Error),
466}
467
468/// Failure modes that can happen even when all I/O succeeds.
469#[derive(Error, Debug)]
470#[non_exhaustive]
471pub enum ProtocolError {
472    #[error("device reported state ({0}) that is not in the DFU spec")]
473    UnknownState(u8),
474
475    #[error("device reported status ({0}) that is not in the DFU spec")]
476    UnknownStatus(u8),
477
478    #[error("device reported an error: {0:?} ({})", .0.error_str())]
479    ErrorStatus(DfuStatus),
480
481    #[error("device entered unexpected state: expected {expected:?}, got {actual:?}")]
482    UnexpectedState {
483        expected: DfuState,
484        actual: DfuState,
485    },
486
487    #[error("don't know how to safely leave initial state {0:?}; please re-enter DFU mode")]
488    BadInitialState(DfuState),
489
490    #[error("file too large: overflowed 16-bit block number while sending")]
491    FileTooLarge,
492
493    #[error("device returned invalid UTF-8 string")]
494    InvalidString(#[from] std::str::Utf8Error),
495
496    #[error("feature report from device was {actual} bytes, expected at least {expected}")]
497    ReportTooShort { expected: usize, actual: usize },
498}