use {
crate::{
CancelContext,
error::{Error, Result},
image::fwpkg::Fwpkg,
port::Port,
protocol::ymodem::{YmodemConfig, YmodemTransfer},
target::ws63::protocol::{CommandFrame, DEFAULT_BAUD, contains_handshake_ack},
},
log::{debug, info, trace, warn},
std::{
thread,
time::{Duration, Instant},
},
};
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
const BAUD_CHANGE_DELAY: Duration = Duration::from_millis(100);
const PARTITION_DELAY: Duration = Duration::from_millis(100);
const MAGIC_TIMEOUT: Duration = Duration::from_secs(10);
const CONNECT_RETRY_DELAY: Duration = Duration::from_millis(500);
const MAX_CONNECT_ATTEMPTS: usize = 7;
const MAX_DOWNLOAD_RETRIES: usize = 3;
fn is_interrupted_error(e: &Error) -> bool {
match e {
Error::Io(io) => {
io.kind() == std::io::ErrorKind::Interrupted
|| io.raw_os_error() == Some(4)
|| io
.to_string()
.to_ascii_lowercase()
.contains("interrupted")
},
Error::Serial(serial) => {
matches!(
serial.kind(),
serialport::ErrorKind::Io(std::io::ErrorKind::Interrupted)
) || serial
.to_string()
.to_ascii_lowercase()
.contains("interrupted")
},
_ => e
.to_string()
.to_ascii_lowercase()
.contains("interrupted"),
}
}
fn sleep_interruptible(cancel: &CancelContext, total: Duration) -> Result<()> {
const CHUNK: Duration = Duration::from_millis(20);
let start = Instant::now();
while start.elapsed() < total {
cancel.check()?;
let elapsed = start.elapsed();
let remain = total.saturating_sub(elapsed);
thread::sleep(remain.min(CHUNK));
}
Ok(())
}
pub struct Ws63Flasher<P: Port> {
port: P,
target_baud: u32,
late_baud: bool,
verbose: u8,
cancel: CancelContext,
}
impl<P: Port> Ws63Flasher<P> {
#[allow(dead_code)]
pub fn new(port: P, target_baud: u32) -> Self {
Self {
port,
target_baud,
late_baud: false,
verbose: 0,
cancel: CancelContext::none(),
}
}
pub fn with_cancel(port: P, target_baud: u32, cancel: CancelContext) -> Self {
Self {
port,
target_baud,
late_baud: false,
verbose: 0,
cancel,
}
}
#[must_use]
pub fn with_late_baud(mut self, late_baud: bool) -> Self {
self.late_baud = late_baud;
self
}
#[must_use]
pub fn with_verbose(mut self, verbose: u8) -> Self {
self.verbose = verbose;
self
}
pub fn connect(&mut self) -> Result<()> {
info!(
"Waiting for device on {}...",
self.port
.name()
);
info!("Please reset the device to enter download mode.");
for attempt in 1..=MAX_CONNECT_ATTEMPTS {
self.cancel
.check()?;
if attempt > 1 {
info!("Connection attempt {attempt}/{MAX_CONNECT_ATTEMPTS}");
}
match self.try_connect() {
Ok(()) => {
return Ok(());
},
Err(e) => {
if is_interrupted_error(&e) {
return Err(e);
}
if attempt < MAX_CONNECT_ATTEMPTS {
warn!("Connection failed (attempt {attempt}/{MAX_CONNECT_ATTEMPTS}): {e}");
sleep_interruptible(&self.cancel, CONNECT_RETRY_DELAY)?;
self.port
.clear_buffers()?;
} else {
return Err(e);
}
},
}
}
Err(Error::Timeout(format!(
"Connection failed after {MAX_CONNECT_ATTEMPTS} attempts"
)))
}
fn try_connect(&mut self) -> Result<()> {
self.cancel
.check()?;
self.port
.clear_buffers()?;
let start = Instant::now();
let handshake_frame = CommandFrame::handshake(self.target_baud);
let handshake_data = handshake_frame.build();
while start.elapsed() < HANDSHAKE_TIMEOUT {
self.cancel
.check()?;
if let Err(e) = self
.port
.write_all(&handshake_data)
{
if e.kind() == std::io::ErrorKind::Interrupted {
return Err(Error::Io(e));
}
trace!("Write error (ignoring): {e}");
}
if let Err(e) = self
.port
.flush()
{
if e.kind() == std::io::ErrorKind::Interrupted {
return Err(Error::Io(e));
}
}
sleep_interruptible(&self.cancel, Duration::from_millis(10))?;
let mut buf = [0u8; 256];
match self
.port
.read(&mut buf)
{
Ok(n) if n > 0 => {
trace!("Received {n} bytes");
if contains_handshake_ack(&buf[..n]) {
info!("Handshake successful!");
if !self.late_baud && self.target_baud != DEFAULT_BAUD {
self.change_baud_rate(self.target_baud)?;
}
return Ok(());
}
},
Ok(_) => {},
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {},
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
return Err(Error::Io(e));
}
trace!("Read error (ignoring): {e}");
},
}
}
Err(Error::Timeout(format!(
"No response after {} seconds",
HANDSHAKE_TIMEOUT.as_secs()
)))
}
fn change_baud_rate(&mut self, baud: u32) -> Result<()> {
self.cancel
.check()?;
info!("Changing baud rate to {baud}");
let frame = CommandFrame::set_baud_rate(baud);
self.send_command(&frame)?;
sleep_interruptible(&self.cancel, BAUD_CHANGE_DELAY)?;
self.port
.set_baud_rate(baud)?;
sleep_interruptible(&self.cancel, BAUD_CHANGE_DELAY)?;
self.port
.clear_buffers()?;
debug!("Baud rate changed to {baud}");
Ok(())
}
fn send_command(&mut self, frame: &CommandFrame) -> Result<()> {
let data = frame.build();
trace!(
"Sending command {:?}: {} bytes",
frame.command(),
data.len()
);
self.port
.write_all(&data)?;
self.port
.flush()?;
Ok(())
}
fn wait_for_magic(&mut self, timeout: Duration) -> Result<()> {
let magic: [u8; 4] = [0xEF, 0xBE, 0xAD, 0xDE]; let start = Instant::now();
let mut match_idx = 0;
debug!("Waiting for SEBOOT magic...");
while start.elapsed() < timeout {
self.cancel
.check()?;
let mut buf = [0u8; 1];
match self
.port
.read(&mut buf)
{
Ok(1) => {
if buf[0] == magic[match_idx] {
match_idx += 1;
if match_idx == magic.len() {
sleep_interruptible(&self.cancel, Duration::from_millis(50))?;
let mut drain = [0u8; 256];
let _ = self
.port
.read(&mut drain);
debug!("Received SEBOOT magic response");
return Ok(());
}
} else {
match_idx = usize::from(buf[0] == magic[0]);
}
},
Ok(_) => {},
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {},
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
return Err(Error::Io(e));
}
return Err(Error::Io(e));
},
}
}
Err(Error::Timeout("Timeout waiting for SEBOOT magic".into()))
}
fn transfer_loaderboot<F>(&mut self, name: &str, data: &[u8], progress: &mut F) -> Result<()>
where
F: FnMut(&str, usize, usize),
{
self.cancel
.check()?;
debug!(
"Transferring LoaderBoot {} ({} bytes) via YMODEM",
name,
data.len()
);
let config = YmodemConfig {
char_timeout: Duration::from_millis(1000),
c_timeout: Duration::from_secs(30),
max_retries: 10,
verbose: self.verbose,
};
let mut ymodem = YmodemTransfer::with_config(&mut self.port, config, &self.cancel);
ymodem.transfer(name, data, |current, total| {
progress(name, current, total);
})?;
debug!("LoaderBoot transfer complete");
Ok(())
}
pub fn flash_fwpkg<F>(
&mut self,
fwpkg: &Fwpkg,
filter: Option<&[&str]>,
mut progress: F,
) -> Result<()>
where
F: FnMut(&str, usize, usize),
{
self.cancel
.check()?;
let loaderboot = fwpkg
.loaderboot()
.ok_or_else(|| Error::InvalidFwpkg("No LoaderBoot partition found".into()))?;
info!("Flashing LoaderBoot: {}", loaderboot.name);
let lb_data = fwpkg.bin_data(loaderboot)?;
self.transfer_loaderboot(&loaderboot.name, lb_data, &mut progress)?;
self.wait_for_magic(MAGIC_TIMEOUT)?;
if self.late_baud && self.target_baud != DEFAULT_BAUD {
self.change_baud_rate(self.target_baud)?;
}
for bin in fwpkg.normal_bins() {
self.cancel
.check()?;
if let Some(names) = filter {
if !names
.iter()
.any(|n| {
bin.name
.contains(n)
})
{
debug!("Skipping partition: {}", bin.name);
continue;
}
}
info!(
"Flashing partition: {} -> 0x{:08X}",
bin.name, bin.burn_addr
);
let bin_data = fwpkg.bin_data(bin)?;
self.download_binary(&bin.name, bin_data, bin.burn_addr, &mut progress)?;
sleep_interruptible(&self.cancel, PARTITION_DELAY)?;
}
info!("Flashing complete!");
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn download_binary<F>(
&mut self,
name: &str,
data: &[u8],
addr: u32,
progress: &mut F,
) -> Result<()>
where
F: FnMut(&str, usize, usize),
{
self.cancel
.check()?;
let mut last_error = None;
for attempt in 1..=MAX_DOWNLOAD_RETRIES {
self.cancel
.check()?;
match self.try_download_binary(name, data, addr, progress) {
Ok(()) => {
return Ok(());
},
Err(e) => {
if is_interrupted_error(&e) || crate::is_interrupted_requested() {
return Err(e);
}
if attempt < MAX_DOWNLOAD_RETRIES {
warn!(
"Download failed for {name} (attempt \
{attempt}/{MAX_DOWNLOAD_RETRIES}): {e}"
);
warn!("Retrying...");
last_error = Some(e);
let _ = self
.port
.clear_buffers();
sleep_interruptible(&self.cancel, CONNECT_RETRY_DELAY)?;
} else {
return Err(e);
}
},
}
}
Err(last_error.unwrap_or_else(|| {
Error::Protocol("Download failed after all retries (no error captured)".into())
}))
}
fn try_download_binary<F>(
&mut self,
name: &str,
data: &[u8],
addr: u32,
progress: &mut F,
) -> Result<()>
where
F: FnMut(&str, usize, usize),
{
self.cancel
.check()?;
let len = u32::try_from(data.len()).map_err(|_| {
Error::Protocol(format!("Firmware too large ({} bytes > 4GB)", data.len()))
})?;
debug!(
"Downloading {} ({} bytes) to 0x{:08X}",
name,
data.len(),
addr
);
let erase_size = (len + 0xFFF) & !0xFFF;
let frame = CommandFrame::download(addr, len, erase_size);
self.send_command(&frame)?;
self.wait_for_magic(MAGIC_TIMEOUT)?;
let config = YmodemConfig {
char_timeout: Duration::from_millis(1000),
c_timeout: Duration::from_secs(30),
max_retries: 10,
verbose: self.verbose,
};
let mut ymodem = YmodemTransfer::with_config(&mut self.port, config, &self.cancel);
ymodem.transfer(name, data, |current, total| {
progress(name, current, total);
})?;
debug!("{name} transfer complete");
Ok(())
}
pub fn write_bins(&mut self, loaderboot: &[u8], bins: &[(&[u8], u32)]) -> Result<()> {
self.cancel
.check()?;
info!("Writing LoaderBoot ({} bytes)", loaderboot.len());
self.transfer_loaderboot("loaderboot", loaderboot, &mut |_, _, _| {})?;
self.wait_for_magic(MAGIC_TIMEOUT)?;
if self.late_baud && self.target_baud != DEFAULT_BAUD {
self.change_baud_rate(self.target_baud)?;
}
for (i, (data, addr)) in bins
.iter()
.enumerate()
{
self.cancel
.check()?;
let name = format!("binary_{i}");
info!("Writing {} ({} bytes) to 0x{:08X}", name, data.len(), addr);
self.download_binary(&name, data, *addr, &mut |_, _, _| {})?;
sleep_interruptible(&self.cancel, PARTITION_DELAY)?;
}
Ok(())
}
pub fn erase_all(&mut self) -> Result<()> {
self.cancel
.check()?;
info!("Erasing entire flash...");
let frame = CommandFrame::erase_all();
self.send_command(&frame)?;
sleep_interruptible(&self.cancel, Duration::from_secs(5))?;
info!("Flash erased");
Ok(())
}
pub fn reset(&mut self) -> Result<()> {
self.cancel
.check()?;
info!("Resetting device...");
let frame = CommandFrame::reset();
self.send_command(&frame)?;
Ok(())
}
}
#[cfg(feature = "native")]
mod native_impl {
use {
super::{
DEFAULT_BAUD, Duration, Error, Result, Ws63Flasher, debug, sleep_interruptible, warn,
},
crate::port::NativePort,
};
impl Ws63Flasher<NativePort> {
pub fn open(port_name: &str, target_baud: u32) -> Result<Self> {
Self::open_with_retry(port_name, target_baud)
}
pub fn open_with_config(config: crate::port::SerialConfig) -> Result<Self> {
Self::open_with_config_retry(config)
}
#[allow(clippy::needless_pass_by_value)]
fn open_with_config_retry(config: crate::port::SerialConfig) -> Result<Self> {
const MAX_OPEN_PORT_ATTEMPTS: usize = 3;
const OPEN_RETRY_DELAY: Duration = Duration::from_millis(500);
let mut last_error = None;
for attempt in 1..=MAX_OPEN_PORT_ATTEMPTS {
match NativePort::open(&config) {
Ok(port) => {
if attempt > 1 {
debug!("Port opened on attempt {attempt}");
}
return Ok(Self::with_cancel(
port,
config.baud_rate,
crate::cancel_context_from_global(),
));
},
Err(e) => {
warn!(
"Failed to open port {} (attempt {}/{}): {e}",
config.port_name, attempt, MAX_OPEN_PORT_ATTEMPTS
);
last_error = Some(e);
if attempt < MAX_OPEN_PORT_ATTEMPTS {
sleep_interruptible(
&crate::cancel_context_from_global(),
OPEN_RETRY_DELAY,
)?;
}
},
}
}
Err(last_error.unwrap_or_else(|| {
Error::Config(format!(
"Failed to open port after {MAX_OPEN_PORT_ATTEMPTS} attempts"
))
}))
}
fn open_with_retry(port_name: &str, target_baud: u32) -> Result<Self> {
const MAX_OPEN_PORT_ATTEMPTS: usize = 3;
const OPEN_RETRY_DELAY: Duration = Duration::from_millis(500);
let mut last_error = None;
for attempt in 1..=MAX_OPEN_PORT_ATTEMPTS {
let config = crate::port::SerialConfig::new(port_name, DEFAULT_BAUD);
match NativePort::open(&config) {
Ok(port) => {
if attempt > 1 {
debug!("Port opened on attempt {attempt}");
}
return Ok(Self::with_cancel(
port,
target_baud,
crate::cancel_context_from_global(),
));
},
Err(e) => {
warn!(
"Failed to open port {port_name} (attempt \
{attempt}/{MAX_OPEN_PORT_ATTEMPTS}): {e}"
);
last_error = Some(e);
if attempt < MAX_OPEN_PORT_ATTEMPTS {
sleep_interruptible(
&crate::cancel_context_from_global(),
OPEN_RETRY_DELAY,
)?;
}
},
}
}
Err(last_error.unwrap_or_else(|| {
Error::Config(format!(
"Failed to open port {port_name} after {MAX_OPEN_PORT_ATTEMPTS} attempts"
))
}))
}
}
}
impl<P: Port> crate::target::Flasher for Ws63Flasher<P> {
fn connect(&mut self) -> Result<()> {
self.connect()
}
fn flash_fwpkg(
&mut self,
fwpkg: &Fwpkg,
filter: Option<&[&str]>,
progress: &mut dyn FnMut(&str, usize, usize),
) -> Result<()> {
self.flash_fwpkg(fwpkg, filter, |name, current, total| {
progress(name, current, total);
})
}
fn write_bins(&mut self, loaderboot: &[u8], bins: &[(&[u8], u32)]) -> Result<()> {
self.write_bins(loaderboot, bins)
}
fn erase_all(&mut self) -> Result<()> {
self.erase_all()
}
fn reset(&mut self) -> Result<()> {
self.reset()
}
fn connection_baud(&self) -> u32 {
DEFAULT_BAUD
}
fn target_baud(&self) -> Option<u32> {
Some(self.target_baud)
}
fn close(&mut self) {
let _ = self
.port
.close();
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::port::Port,
std::{
io::{Read, Write},
sync::{Arc, Mutex},
},
};
#[derive(Clone)]
struct MockPort {
name: String,
baud_rate: u32,
timeout: Duration,
read_buffer: Arc<Mutex<Vec<u8>>>,
write_buffer: Arc<Mutex<Vec<u8>>>,
dtr: bool,
rts: bool,
}
impl MockPort {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
baud_rate: 115200,
timeout: Duration::from_millis(1000),
read_buffer: Arc::new(Mutex::new(Vec::new())),
write_buffer: Arc::new(Mutex::new(Vec::new())),
dtr: false,
rts: false,
}
}
fn add_read_data(&self, data: &[u8]) {
let mut buf = self
.read_buffer
.lock()
.unwrap();
buf.extend_from_slice(data);
}
fn get_written_data(&self) -> Vec<u8> {
let buf = self
.write_buffer
.lock()
.unwrap();
buf.clone()
}
fn clear(&self) {
let mut read_buf = self
.read_buffer
.lock()
.unwrap();
let mut write_buf = self
.write_buffer
.lock()
.unwrap();
read_buf.clear();
write_buf.clear();
}
}
impl Port for MockPort {
fn set_timeout(&mut self, timeout: Duration) -> Result<()> {
self.timeout = timeout;
Ok(())
}
fn timeout(&self) -> Duration {
self.timeout
}
fn set_baud_rate(&mut self, baud_rate: u32) -> Result<()> {
self.baud_rate = baud_rate;
Ok(())
}
fn baud_rate(&self) -> u32 {
self.baud_rate
}
fn clear_buffers(&mut self) -> Result<()> {
self.clear();
Ok(())
}
fn name(&self) -> &str {
&self.name
}
fn set_dtr(&mut self, level: bool) -> Result<()> {
self.dtr = level;
Ok(())
}
fn set_rts(&mut self, level: bool) -> Result<()> {
self.rts = level;
Ok(())
}
fn read_cts(&mut self) -> Result<bool> {
Ok(true) }
fn read_dsr(&mut self) -> Result<bool> {
Ok(true) }
fn close(&mut self) -> Result<()> {
self.clear();
Ok(())
}
}
impl Read for MockPort {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut read_buf = self
.read_buffer
.lock()
.map_err(|e| std::io::Error::other(format!("mutex poisoned: {e}")))?;
if read_buf.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"no data available",
));
}
let to_read = std::cmp::min(buf.len(), read_buf.len());
buf[..to_read].copy_from_slice(&read_buf[..to_read]);
read_buf.drain(..to_read);
Ok(to_read)
}
}
impl Write for MockPort {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut write_buf = self
.write_buffer
.lock()
.map_err(|e| std::io::Error::other(format!("mutex poisoned: {e}")))?;
write_buf.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn test_flasher_new_with_mock_port() {
let port = MockPort::new("/dev/ttyUSB0");
let flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none());
assert_eq!(flasher.target_baud, 921600);
assert!(!flasher.late_baud);
assert_eq!(flasher.verbose, 0);
}
#[test]
fn test_flasher_builder_methods() {
let port = MockPort::new("/dev/ttyUSB0");
let flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none())
.with_late_baud(true)
.with_verbose(2);
assert!(flasher.late_baud);
assert_eq!(flasher.verbose, 2);
}
#[test]
fn test_mock_port_read_write() {
let mut port = MockPort::new("/dev/ttyUSB0");
port.add_read_data(&[0xDE, 0xAD, 0xBE, 0xEF]);
port.write_all(b"test")
.unwrap();
port.flush()
.unwrap();
let written = port.get_written_data();
assert_eq!(written, b"test");
let mut buf = [0u8; 4];
std::io::Read::read_exact(&mut port, &mut buf).unwrap();
assert_eq!(&buf, &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn test_mock_port_buffers() {
let mut port = MockPort::new("/dev/ttyUSB0");
port.clear();
assert!(
port.get_written_data()
.is_empty()
);
port.write_all(b"hello")
.unwrap();
port.add_read_data(&[1, 2, 3]);
assert_eq!(port.get_written_data(), b"hello");
let mut buf = [0u8; 3];
std::io::Read::read_exact(&mut port, &mut buf).unwrap();
assert_eq!(&buf, &[1, 2, 3]);
port.clear();
assert!(
port.get_written_data()
.is_empty()
);
}
#[test]
fn test_mock_port_pin_control() {
let mut port = MockPort::new("/dev/ttyUSB0");
assert!(!port.dtr);
assert!(!port.rts);
port.set_dtr(true)
.unwrap();
port.set_rts(true)
.unwrap();
assert!(port.dtr);
assert!(port.rts);
}
#[test]
fn test_mock_port_baud_timeout() {
let mut port = MockPort::new("/dev/ttyUSB0");
assert_eq!(port.baud_rate(), 115200);
assert_eq!(port.timeout(), Duration::from_millis(1000));
port.set_baud_rate(921600)
.unwrap();
port.set_timeout(Duration::from_millis(500))
.unwrap();
assert_eq!(port.baud_rate(), 921600);
assert_eq!(port.timeout(), Duration::from_millis(500));
}
#[test]
fn test_mock_port_name() {
let port = MockPort::new("/dev/ttyUSB1");
assert_eq!(port.name(), "/dev/ttyUSB1");
let port2 = MockPort::new("COM3");
assert_eq!(port2.name(), "COM3");
}
#[test]
fn test_create_flasher_with_mock_port() {
use crate::target::ChipFamily;
let port = MockPort::new("/dev/ttyUSB0");
let flasher = ChipFamily::Ws63.create_flasher_with_port(port, 921600, false, 0);
assert!(flasher.is_ok());
let flasher = flasher.unwrap();
assert_eq!(flasher.connection_baud(), 115200); assert_eq!(flasher.target_baud(), Some(921600));
}
#[test]
fn test_flasher_trait_object() {
use crate::target::Flasher;
let port = MockPort::new("/dev/ttyUSB0");
let flasher: Box<dyn Flasher> = Box::new(Ws63Flasher::with_cancel(
port,
921600,
CancelContext::none(),
));
assert_eq!(flasher.connection_baud(), 115200);
assert_eq!(flasher.target_baud(), Some(921600));
}
#[test]
fn test_multiple_flashers_same_port() {
use crate::target::ChipFamily;
let port = MockPort::new("/dev/ttyUSB0");
let port_clone = port.clone();
let flasher1 = ChipFamily::Ws63.create_flasher_with_port(port, 921600, false, 0);
let flasher2 = ChipFamily::Ws63.create_flasher_with_port(port_clone, 115200, true, 1);
assert!(flasher1.is_ok());
assert!(flasher2.is_ok());
let flasher1 = flasher1.unwrap();
let flasher2 = flasher2.unwrap();
assert_eq!(flasher1.target_baud(), Some(921600));
assert_eq!(flasher2.target_baud(), Some(115200));
}
#[test]
fn test_create_flasher_with_port_unsupported_chip() {
use crate::target::ChipFamily;
let port = MockPort::new("/dev/ttyUSB0");
let result = ChipFamily::Bs2x.create_flasher_with_port(port, 115200, false, 0);
assert!(result.is_err());
assert!(matches!(result, Err(crate::error::Error::Unsupported(_))));
}
#[test]
fn test_is_interrupted_error_for_io_interrupted_and_message() {
let e1 = Error::Io(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"operation interrupted",
));
assert!(is_interrupted_error(&e1));
let e2 = Error::Io(std::io::Error::other("Interrupted system call"));
assert!(is_interrupted_error(&e2));
}
#[test]
fn test_download_binary_interrupted_short_circuits_retry() {
crate::test_set_interrupted(true);
let port = MockPort::new("/dev/ttyUSB0");
let cancel = crate::cancel_context_from_global();
let mut flasher = Ws63Flasher::with_cancel(port, 921600, cancel);
let mut progress_calls = 0usize;
let result = flasher.download_binary(
"app.bin",
&[0x01, 0x02, 0x03],
0x0023_0000,
&mut |_, _, _| {
progress_calls += 1;
},
);
assert!(matches!(
result,
Err(Error::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted
));
assert_eq!(progress_calls, 0);
assert!(
flasher
.port
.get_written_data()
.is_empty(),
"Interrupted download should not send frames or enter retry loop"
);
crate::test_set_interrupted(false);
}
#[test]
fn test_erase_size_alignment_4k() {
assert_eq!((0x1000u32 + 0xFFF) & !0xFFF, 0x1000);
assert_eq!((0x2000u32 + 0xFFF) & !0xFFF, 0x2000);
assert_eq!((0x10000u32 + 0xFFF) & !0xFFF, 0x10000);
assert_eq!((1u32 + 0xFFF) & !0xFFF, 0x1000);
assert_eq!((0x1001u32 + 0xFFF) & !0xFFF, 0x2000);
assert_eq!((0x2001u32 + 0xFFF) & !0xFFF, 0x3000);
assert_eq!((0xFFFu32 + 0xFFF) & !0xFFF, 0x1000);
assert_eq!((0x8F4u32 + 0xFFF) & !0xFFF, 0x1000);
assert_eq!((0x900u32 + 0xFFF) & !0xFFF, 0x1000);
assert_eq!((0x12345u32 + 0xFFF) & !0xFFF, 0x13000);
}
#[test]
fn test_wait_for_magic_finds_magic() {
let port = MockPort::new("/dev/ttyUSB0");
let mut response = vec![0x00, 0x41, 0x42]; response.extend_from_slice(&[0xEF, 0xBE, 0xAD, 0xDE]); response.extend_from_slice(&[0x0C, 0x00, 0xE1, 0x1E, 0x5A, 0x00, 0x00, 0x00]); port.add_read_data(&response);
let mut flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none());
let result = flasher.wait_for_magic(Duration::from_millis(500));
assert!(
result.is_ok(),
"wait_for_magic should succeed when magic is present"
);
}
#[test]
fn test_wait_for_magic_timeout_no_magic() {
let port = MockPort::new("/dev/ttyUSB0");
let mut flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none());
let result = flasher.wait_for_magic(Duration::from_millis(100));
assert!(
result.is_err(),
"wait_for_magic should timeout with no data"
);
}
#[test]
fn test_wait_for_magic_partial_then_real() {
let port = MockPort::new("/dev/ttyUSB0");
let mut response = Vec::new();
response.extend_from_slice(&[0xEF, 0xBE, 0x00]); response.extend_from_slice(&[0xEF, 0xBE, 0xAD, 0xDE]); response.extend_from_slice(&[0x0C, 0x00, 0xE1, 0x1E, 0x5A, 0x00]); port.add_read_data(&response);
let mut flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none());
let result = flasher.wait_for_magic(Duration::from_millis(500));
assert!(
result.is_ok(),
"wait_for_magic should handle partial matches"
);
}
#[test]
fn test_loaderboot_no_download_command() {
let port = MockPort::new("/dev/ttyUSB0");
let response = vec![
b'C', 0x06, 0x06, 0x06, 0x06, ];
port.add_read_data(&response);
let mut flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none());
let result = flasher.transfer_loaderboot("test.bin", &[0xAA], &mut |_, _, _| {});
let written = flasher
.port
.get_written_data();
let has_download_cmd = written
.windows(8)
.any(|w| {
w[0] == 0xEF
&& w[1] == 0xBE
&& w[2] == 0xAD
&& w[3] == 0xDE
&& w[6] == 0xD2
&& w[7] == 0x2D
});
assert!(
!has_download_cmd,
"LoaderBoot transfer must NOT send download command (0xD2). Written data should only \
contain YMODEM blocks, not SEBOOT command frames."
);
assert!(
!written.is_empty(),
"YMODEM transfer should have written data for LoaderBoot"
);
assert!(
result.is_ok(),
"LoaderBoot transfer should succeed: {:?}",
result.err()
);
}
#[test]
fn test_normal_partition_sends_download_command() {
let port = MockPort::new("/dev/ttyUSB0");
let mut response = Vec::new();
response.extend_from_slice(&[0xEF, 0xBE, 0xAD, 0xDE]);
response.extend_from_slice(&[0x0C, 0x00, 0xE1, 0x1E, 0x5A, 0x00, 0x00, 0x00]);
port.add_read_data(&response);
let mut flasher = Ws63Flasher::with_cancel(port, 921600, CancelContext::none());
let test_data = vec![0xBB; 100];
let _result = flasher.try_download_binary(
"test_partition.bin",
&test_data,
0x00800000,
&mut |_, _, _| {},
);
let written = flasher
.port
.get_written_data();
let has_download_cmd = written
.windows(8)
.any(|w| {
w[0] == 0xEF
&& w[1] == 0xBE
&& w[2] == 0xAD
&& w[3] == 0xDE
&& w[6] == 0xD2
&& w[7] == 0x2D
});
assert!(
has_download_cmd,
"Normal partition download must send download command (0xD2). Written data should \
contain a SEBOOT command frame."
);
}
#[test]
fn test_download_frame_erase_size_in_bytes() {
let frame = CommandFrame::download(0x00800000, 100, (100 + 0xFFF) & !0xFFF);
let data = frame.build();
let erase_size = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
assert_eq!(
erase_size, 0x1000,
"erase_size for 100 bytes should be 0x1000 (4KB aligned), got 0x{erase_size:X}"
);
let frame2 = CommandFrame::download(0x00800000, 0x1000, (0x1000u32 + 0xFFF) & !0xFFF);
let data2 = frame2.build();
let erase_size2 = u32::from_le_bytes([data2[16], data2[17], data2[18], data2[19]]);
assert_eq!(
erase_size2, 0x1000,
"erase_size for exactly 4KB should remain 0x1000"
);
let frame3 = CommandFrame::download(0x00800000, 0x1001, (0x1001u32 + 0xFFF) & !0xFFF);
let data3 = frame3.build();
let erase_size3 = u32::from_le_bytes([data3[16], data3[17], data3[18], data3[19]]);
assert_eq!(
erase_size3, 0x2000,
"erase_size for 0x1001 bytes should be 0x2000 (next 4KB boundary)"
);
}
}