use crate::error::{Error, Result};
use crate::protocol::crc::crc16_xmodem;
use log::{debug, trace};
use std::io::{Read, Write};
use std::time::Duration;
pub mod control {
pub const SOH: u8 = 0x01;
pub const STX: u8 = 0x02;
pub const EOT: u8 = 0x04;
pub const ACK: u8 = 0x06;
pub const NAK: u8 = 0x15;
pub const CAN: u8 = 0x18;
pub const C: u8 = b'C';
}
pub const SOH_BLOCK_SIZE: usize = 128;
pub const STX_BLOCK_SIZE: usize = 1024;
#[derive(Debug, Clone)]
pub struct YmodemConfig {
pub char_timeout: Duration,
pub c_timeout: Duration,
pub max_retries: u32,
pub verbose: u8,
}
impl Default for YmodemConfig {
fn default() -> Self {
Self {
char_timeout: Duration::from_millis(1000),
c_timeout: Duration::from_secs(60),
max_retries: 10,
verbose: 0,
}
}
}
pub struct YmodemTransfer<'a, P: Read + Write> {
port: &'a mut P,
config: YmodemConfig,
}
impl<'a, P: Read + Write> YmodemTransfer<'a, P> {
pub fn new(port: &'a mut P) -> Self {
Self {
port,
config: YmodemConfig::default(),
}
}
pub fn with_config(port: &'a mut P, config: YmodemConfig) -> Self {
Self { port, config }
}
fn read_byte(&mut self, _timeout: Duration) -> Result<u8> {
let mut buf = [0u8; 1];
match self.port.read(&mut buf) {
Ok(1) => Ok(buf[0]),
Ok(_) => Err(Error::Timeout("read_byte: no data".into())),
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
Err(Error::Timeout("read_byte: timeout".into()))
},
Err(e) => Err(Error::Io(e)),
}
}
pub fn wait_for_c(&mut self) -> Result<()> {
debug!("Waiting for 'C' from receiver...");
let start = std::time::Instant::now();
while start.elapsed() < self.config.c_timeout {
match self.read_byte(self.config.char_timeout) {
Ok(control::C) => {
debug!("Received 'C', starting transfer");
return Ok(());
},
Ok(c) => {
trace!("Received unexpected char: 0x{c:02X}");
},
Err(Error::Timeout(_)) => {},
Err(e) => return Err(e),
}
}
Err(Error::Timeout("Timeout waiting for 'C'".into()))
}
fn build_block(seq: u8, data: &[u8], use_stx: bool) -> Vec<u8> {
let block_size = if use_stx {
STX_BLOCK_SIZE
} else {
SOH_BLOCK_SIZE
};
let header = if use_stx { control::STX } else { control::SOH };
let mut block = Vec::with_capacity(3 + block_size + 2);
block.push(header);
block.push(seq);
block.push(!seq);
if data.len() >= block_size {
block.extend_from_slice(&data[..block_size]);
} else {
block.extend_from_slice(data);
block.resize(3 + block_size, 0x00);
}
let crc = crc16_xmodem(&block[3..3 + block_size]);
block.push((crc >> 8) as u8);
block.push((crc & 0xFF) as u8);
block
}
fn send_block(&mut self, block: &[u8]) -> Result<()> {
for retry in 0..self.config.max_retries {
trace!("Sending block (attempt {})", retry + 1);
self.port.write_all(block)?;
self.port.flush()?;
match self.read_byte(self.config.char_timeout) {
Ok(control::ACK) => {
trace!("Block ACKed");
return Ok(());
},
Ok(control::NAK) => {
debug!("Block NAKed, retrying...");
},
Ok(control::CAN) => {
return Err(Error::Ymodem("Transfer cancelled by receiver".into()));
},
Ok(c) => {
debug!("Unexpected response: 0x{c:02X}, retrying...");
},
Err(Error::Timeout(_)) => {
debug!("Timeout waiting for ACK, retrying...");
},
Err(e) => return Err(e),
}
}
Err(Error::Ymodem(format!(
"Block transfer failed after {} retries",
self.config.max_retries
)))
}
pub fn send_file_info(&mut self, filename: &str, filesize: usize) -> Result<()> {
debug!("Sending file info: {filename} ({filesize} bytes)");
let mut data = Vec::with_capacity(SOH_BLOCK_SIZE);
data.extend_from_slice(filename.as_bytes());
data.push(0x00);
data.extend_from_slice(filesize.to_string().as_bytes());
data.push(0x00);
let block = Self::build_block(0, &data, false);
self.send_block(&block)
}
pub fn send_eot(&mut self) -> Result<()> {
debug!("Sending EOT");
for _retry in 0..self.config.max_retries {
self.port.write_all(&[control::EOT])?;
self.port.flush()?;
match self.read_byte(self.config.char_timeout) {
Ok(control::ACK) => {
debug!("EOT ACKed");
return Ok(());
},
Ok(control::C) => {
return Ok(());
},
Ok(_) | Err(Error::Timeout(_)) => {},
Err(e) => return Err(e),
}
}
Ok(())
}
pub fn send_finish(&mut self) -> Result<()> {
debug!("Sending finish block");
let block = Self::build_block(0, &[], false);
self.send_block(&block)
}
pub fn transfer<F>(&mut self, filename: &str, data: &[u8], mut progress: F) -> Result<()>
where
F: FnMut(usize, usize),
{
debug!(
"Starting YMODEM transfer: {} ({} bytes)",
filename,
data.len()
);
self.wait_for_c()?;
self.send_file_info(filename, data.len())?;
self.wait_for_c()?;
let mut seq: u8 = 1;
let mut offset = 0;
let total = data.len();
while offset < total {
let chunk_end = (offset + STX_BLOCK_SIZE).min(total);
let chunk = &data[offset..chunk_end];
let block = Self::build_block(seq, chunk, true);
self.send_block(&block)?;
offset = chunk_end;
seq = seq.wrapping_add(1);
progress(offset, total);
}
self.send_eot()?;
if let Ok(()) = self.wait_for_c() {
let _ = self.send_finish();
}
debug!("YMODEM transfer complete");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_block_soh() {
let data = [0x01, 0x02, 0x03];
let block = YmodemTransfer::<std::io::Cursor<Vec<u8>>>::build_block(1, &data, false);
assert_eq!(block[0], control::SOH);
assert_eq!(block[1], 1);
assert_eq!(block[2], 0xFE);
assert_eq!(block.len(), 3 + SOH_BLOCK_SIZE + 2);
}
#[test]
fn test_build_block_stx() {
let data = vec![0xAA; STX_BLOCK_SIZE];
let block = YmodemTransfer::<std::io::Cursor<Vec<u8>>>::build_block(5, &data, true);
assert_eq!(block[0], control::STX);
assert_eq!(block[1], 5);
assert_eq!(block[2], 0xFA);
assert_eq!(block.len(), 3 + STX_BLOCK_SIZE + 2);
}
}