use std::time::{Duration, Instant};
use crate::device::{self, Device, Target};
use crate::dongle::{self, Dongle, DongleTarget};
use crate::error::{Error, Result};
use crate::progress::{Event, ProgressFn};
#[cfg(target_os = "macos")]
use super::vdm;
use super::{discovery, host_can_trigger_dfu, DfuTarget};
#[derive(Debug, Clone, Default)]
pub enum DfuVia {
#[default]
Auto,
Dongle(String),
Ecid(u64),
Host(Option<i32>),
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum DfuOutcome {
Entered(Device),
Sent,
}
enum Route {
Host(DfuTarget),
Dongle(Dongle),
}
fn resolve(via: DfuVia) -> Result<Route> {
match via {
DfuVia::Dongle(id) => Ok(Route::Dongle(dongle::find(DongleTarget::Id(id))?)),
DfuVia::Ecid(e) => match dongle::find_for_ecid(e) {
Ok(d) => Ok(Route::Dongle(d)),
Err(_) => Ok(Route::Host(DfuTarget::Ecid(e))),
},
DfuVia::Host(port) => Ok(Route::Host(match port {
Some(rid) => DfuTarget::Port(rid),
None => DfuTarget::Auto,
})),
DfuVia::Auto => {
let mut ds = dongle::list()?;
match ds.len() {
0 => Ok(Route::Host(DfuTarget::Auto)),
1 => Ok(Route::Dongle(ds.remove(0))),
_ => Err(Error::MultipleDongles(
ds.iter()
.map(|d| d.serial.as_str())
.collect::<Vec<_>>()
.join(", "),
)),
}
}
}
}
const TRIGGER_ATTEMPTS: u32 = 3;
const RETRY_AFTER: Duration = Duration::from_secs(10);
pub fn trigger_dfu(via: DfuVia, timeout: Duration, progress: ProgressFn) -> Result<DfuOutcome> {
let want_ecid = match &via {
DfuVia::Ecid(e) => Some(*e),
_ => None,
};
match resolve(via)? {
Route::Dongle(d) => trigger_via_dongle(&d, want_ecid, timeout, progress),
Route::Host(target) => trigger_via_host(&target, want_ecid, timeout, progress),
}
}
pub fn reboot(via: DfuVia, progress: ProgressFn) -> Result<()> {
match resolve(via)? {
Route::Dongle(d) => reboot_via_dongle(&d, progress),
Route::Host(target) => host_reboot(&target, progress),
}
}
fn dfu_attached(d: &Dongle) -> bool {
matches!(d.attached_device(), Ok(Some(dev)) if dev.in_dfu())
}
fn reboot_via_dongle(d: &Dongle, progress: ProgressFn) -> Result<()> {
if !dfu_attached(d) {
return d.reboot();
}
let deadline = Instant::now() + Duration::from_secs(90);
let mut attempt = 0;
loop {
attempt += 1;
progress(Event::DfuTriggerStage {
stage: format!("rebooting the target out of DFU (attempt {attempt})"),
});
let _ = d.reboot();
std::thread::sleep(Duration::from_secs(8));
if !dfu_attached(d) {
return Ok(());
}
if Instant::now() >= deadline {
return Err(Error::Dongle(
"target stayed in DFU after repeated reboots; hold its power \
button ~10s or run a restore to exit DFU"
.into(),
));
}
}
}
fn trigger_via_host(
target: &DfuTarget,
want_ecid: Option<u64>,
timeout: Duration,
progress: ProgressFn,
) -> Result<DfuOutcome> {
if !host_can_trigger_dfu() {
return Err(Error::UnsupportedHost(
"cannot trigger DFU on this host".into(),
));
}
let mut watch = discovery::watch()?;
let deadline = Instant::now() + timeout;
for attempt in 1..=TRIGGER_ATTEMPTS {
host_trigger(target, progress)?;
let slice = attempt_slice(attempt, deadline);
match wait_entered(&mut watch, want_ecid, slice) {
Ok(dev) => return Ok(DfuOutcome::Entered(dev)),
Err(Error::WaitTimeout) if attempt < TRIGGER_ATTEMPTS && Instant::now() < deadline => {}
Err(e) => return Err(e),
}
}
Err(Error::WaitTimeout)
}
fn trigger_via_dongle(
d: &Dongle,
want_ecid: Option<u64>,
timeout: Duration,
progress: ProgressFn,
) -> Result<DfuOutcome> {
let mut watch = discovery::watch()?;
let deadline = Instant::now() + timeout;
for attempt in 1..=TRIGGER_ATTEMPTS {
progress(Event::DfuTriggerStage {
stage: if attempt == 1 {
format!("sending DFU trigger via dongle {}", d.serial)
} else {
format!(
"target didn't enter DFU; re-sending via dongle ({attempt}/{TRIGGER_ATTEMPTS})"
)
},
});
d.dfu()?;
progress(Event::DfuTriggerStage {
stage: "waiting for the target to enter DFU mode".into(),
});
let slice = attempt_slice(attempt, deadline);
match wait_entered(&mut watch, want_ecid, slice) {
Ok(dev) => return Ok(DfuOutcome::Entered(dev)),
Err(Error::WaitTimeout) if attempt < TRIGGER_ATTEMPTS && Instant::now() < deadline => {}
Err(Error::WaitTimeout) => return Ok(DfuOutcome::Sent),
Err(e) => return Err(e),
}
}
Ok(DfuOutcome::Sent)
}
fn attempt_slice(attempt: u32, deadline: Instant) -> Duration {
let remaining = deadline.saturating_duration_since(Instant::now());
if attempt < TRIGGER_ATTEMPTS {
RETRY_AFTER.min(remaining)
} else {
remaining
}
}
fn wait_entered(
watch: &mut discovery::Watch,
want_ecid: Option<u64>,
timeout: Duration,
) -> Result<Device> {
let matches = |d: &Device| d.in_dfu() && want_ecid.is_none_or(|e| d.ecid == Some(e));
if let Some(d) = device::list()?.into_iter().find(&matches) {
return Ok(d);
}
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::WaitTimeout);
}
let dev = watch.wait(remaining)?;
if matches(&dev) {
return Ok(dev);
}
}
}
fn host_trigger(target: &DfuTarget, progress: ProgressFn) -> Result<()> {
#[cfg(target_os = "macos")]
{
vdm::enter_dfu(target, progress)
}
#[cfg(not(target_os = "macos"))]
{
let _ = (target, progress);
Err(Error::UnsupportedHost(
"cannot trigger DFU on this host".into(),
))
}
}
fn host_reboot(target: &DfuTarget, progress: ProgressFn) -> Result<()> {
if !host_can_trigger_dfu() {
return Err(Error::UnsupportedHost(
"cannot control the target from this host".into(),
));
}
#[cfg(target_os = "macos")]
{
vdm::reboot(target, progress)
}
#[cfg(not(target_os = "macos"))]
{
let _ = (target, progress);
Ok(())
}
}
pub fn wait_manual(want_ecid: Option<u64>, timeout: Duration) -> Result<Device> {
match want_ecid {
Some(e) => device::wait_where(timeout, |d| d.in_dfu() && d.ecid == Some(e)),
None => device::wait(Target::One, timeout),
}
}