use anyhow::Result;
use super::client::{open_dfu_device, read_response, NORMAL_PID, VID};
use super::protocol::*;
pub const RECOVERY_DECLARED_LEN: u32 = 0x40000;
pub const RECOVERY_WAIT_TIMEOUT_SECS: u64 = 20;
pub const RECOVERY_POLL_INTERVAL_MS: u64 = 500;
pub fn scan_stuck_device() -> Result<bool> {
let api = hidapi::HidApi::new().map_err(|e| anyhow::anyhow!("HidApi 创建失败: {e}"))?;
let found = api
.device_list()
.any(|d| d.vendor_id() == VID && d.product_id() == DFU_PID);
Ok(found)
}
pub fn scan_normal_device() -> Result<bool> {
let api = hidapi::HidApi::new().map_err(|e| anyhow::anyhow!("HidApi 创建失败: {e}"))?;
let found = api
.device_list()
.any(|d| d.vendor_id() == VID && d.product_id() == NORMAL_PID);
Ok(found)
}
pub fn send_recovery_sequence() -> Result<bool> {
let device = open_dfu_device()?;
Ok(send_recovery_sequence_on(&device))
}
pub(crate) fn send_recovery_sequence_on(device: &hidapi::HidDevice) -> bool {
let mut delivered = false;
match write_then_read(
device,
&DfuPacketEncoder::prepare(RECOVERY_DECLARED_LEN),
DFU_PREPARE_TIMEOUT_MS,
) {
WriteOutcome::Answered(resp) => {
delivered = true;
log::info!(target: "board", "[dfu-recover] PREPARE 已应答 result=0x{:02X}", resp.result);
}
WriteOutcome::SentNoAnswer(e) => {
delivered = true;
log::warn!(target: "board", "[dfu-recover] PREPARE 已发出但无应答: {e}(仍继续发 END)");
}
WriteOutcome::WriteFailed(e) => {
log::warn!(target: "board", "[dfu-recover] PREPARE 发送失败: {e}(仍继续发 END)");
}
}
match write_then_read(device, &DfuPacketEncoder::end(), DFU_END_TIMEOUT_MS) {
WriteOutcome::Answered(resp) => {
delivered = true;
log::info!(
target: "board",
"[dfu-recover] END 已应答 result=0x{:02X} written={}(非 0 属预期)",
resp.result, resp.total_written
);
}
WriteOutcome::SentNoAnswer(e) => {
delivered = true;
log::info!(target: "board", "[dfu-recover] END 已发出但无应答: {e}(设备多半已在重启,属正常)");
}
WriteOutcome::WriteFailed(e) => {
log::warn!(target: "board", "[dfu-recover] END 发送失败: {e}");
}
}
if !delivered {
log::warn!(target: "board", "[dfu-recover] 两个恢复包都未能写入设备(可能已拔线)");
}
delivered
}
enum WriteOutcome {
Answered(DfuResponse),
SentNoAnswer(anyhow::Error),
WriteFailed(anyhow::Error),
}
fn write_then_read(device: &hidapi::HidDevice, packet: &[u8], timeout_ms: i32) -> WriteOutcome {
if let Err(e) = device.write(packet) {
return WriteOutcome::WriteFailed(anyhow::anyhow!("DFU write 失败: {e}"));
}
match read_response(device, timeout_ms) {
Ok(resp) => WriteOutcome::Answered(resp),
Err(e) => WriteOutcome::SentNoAnswer(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn declared_len_must_be_nonzero() {
assert_ne!(RECOVERY_DECLARED_LEN, 0);
}
#[test]
fn declared_len_matches_cross_impl_contract() {
assert_eq!(RECOVERY_DECLARED_LEN, 0x40000);
}
#[test]
fn recovery_prepare_packet_declares_nonzero_length() {
let packet = DfuPacketEncoder::prepare(RECOVERY_DECLARED_LEN);
assert_eq!(packet[0], DFU_REPORT_ID_OUTPUT);
assert_eq!(packet[1], FLAG_PREPARE);
let declared = u32::from_le_bytes([packet[2], packet[3], packet[4], packet[5]]);
assert_eq!(declared, RECOVERY_DECLARED_LEN);
assert_ne!(declared, 0);
}
#[test]
fn recovery_prepare_packet_checksum_covers_declared_length() {
let packet = DfuPacketEncoder::prepare(RECOVERY_DECLARED_LEN);
let checksum = u16::from_le_bytes([packet[6], packet[7]]);
assert_eq!(
checksum,
compute_checksum(&RECOVERY_DECLARED_LEN.to_le_bytes())
);
}
#[test]
fn recovery_end_packet_is_flag_end() {
let packet = DfuPacketEncoder::end();
assert_eq!(packet[0], DFU_REPORT_ID_OUTPUT);
assert_eq!(packet[1], FLAG_END);
}
}