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;
12const XFER_DATA_SIZE: usize = 1017;
14
15pub 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 report.resize(1 + XFER_HEADER_SIZE, 0u8);
26
27 let data_size = file.take(XFER_DATA_SIZE as _).read_to_end(&mut report)?;
29
30 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 _); device
39 .send_feature_report(&report)
40 .map_err(|e| Error::DeviceIoError {
41 source: e,
42 action: "sending firmware data chunk",
43 })?;
44
45 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 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
93pub fn upload(device: &HidDevice, file: &mut impl Write) -> Result<(), Error> {
96 let mut report = [0u8; 1 + XFER_HEADER_SIZE + XFER_DATA_SIZE];
98
99 loop {
100 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 status.ensure_state(DfuState::dfuIDLE)?;
131 break;
132 } else {
133 status.ensure_state(DfuState::dfuUPLOAD_IDLE)?;
134 }
135 }
136
137 Ok(())
138}
139
140#[non_exhaustive]
142pub enum InfoField {
143 DeviceModel,
144 SerialNumber,
145 CurrentFirmware,
146}
147
148pub 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 let mut request_report = [0u8; 1 + 2 + 1];
158
159 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 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
191pub 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]) .map_err(|e| Error::DeviceIoError {
198 source: e,
199 action: "entering DFU mode",
200 })
201}
202
203pub 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
213pub 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 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 UploadDownload = 1,
271
272 GetStatus = 2,
275
276 StateCmd = 3,
281}
282
283#[repr(u8)]
285#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)]
286#[allow(non_camel_case_types)] pub 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#[repr(u8)]
332#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)]
333#[allow(non_camel_case_types)] pub 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]; 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)] #[allow(dead_code)] enum 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, }
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]; 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); 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
434fn 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#[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#[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}