use crate::error::{FaxError, Result}; use crate::fax::class2::commands::{Class2Commands, DccParams, BR_V17_14400}; use crate::fax::encoder::MhEncoder; use crate::modem::driver::ModemDriver; use crate::document::convert::Page; const DLE: u8 = 0x10; const ETX: u8 = 0x03; const DLE_ETX: [u8; 2] = [DLE, ETX]; /// Class 2 Fax Sender - simpler than Class 1 /// The modem handles T.30 protocol, HDLC framing, and negotiation automatically. pub struct Class2Send<'a> { driver: &'a mut ModemDriver, station_id: String, header: String, params: DccParams, } impl<'a> Class2Send<'a> { pub fn new(driver: &'a mut ModemDriver) -> Self { Self { driver, station_id: "TELFAX".to_string(), header: "Telfax Fax Server".to_string(), params: DccParams::default(), } } pub fn with_station_id(mut self, id: &str) -> Self { self.station_id = id.to_string(); self } pub fn with_header(mut self, header: &str) -> Self { self.header = header.to_string(); self } pub fn with_params(mut self, params: DccParams) -> Self { self.params = params; self } fn at(&mut self) -> crate::modem::at::AtChannel<'_> { crate::modem::at::AtChannel::new(self.driver) } /// Send fax document using Class 2 protocol /// Pages should be pre-encoded in T.4 MH format pub fn send_fax(&mut self, number: &str, pages: &[Page]) -> Result<()> { tracing::info!("Starting Class 2 fax to {}", number); // Enter Class 2 mode self.at().send_command(Class2Commands::ENTER_CLASS2, 5000)?; tracing::info!("Entered Class 2 mode"); // Set station ID let station_cmd = Class2Commands::set_station_id(&self.station_id); self.at().send_command(&station_cmd, 3000)?; tracing::info!("Station ID set: {}", self.station_id); // Set header let header_cmd = Class2Commands::set_header(&self.header); self.at().send_command(&header_cmd, 3000)?; // Set capabilities let speed = if self.params.speed > 0 { self.params.speed } else { BR_V17_14400 }; let dis_cmd = Class2Commands::set_dis_params(self.params.resolution, speed, self.params.ecm); self.at().send_command(&dis_cmd, 3000)?; // Dial self.dial(number)?; // Send pages for (i, page) in pages.iter().enumerate() { tracing::info!("Sending page {} of {}", i + 1, pages.len()); self.send_page(page)?; let is_last = i == pages.len() - 1; self.end_page(is_last)?; } // Hang up self.hang_up()?; tracing::info!("Fax sent successfully"); Ok(()) } fn dial(&mut self, number: &str) -> Result<()> { self.driver.drain()?; let cmd = Class2Commands::dial(number); self.driver.write_raw(cmd.as_bytes())?; self.driver.write_raw(b"\r")?; self.driver.flush()?; // Wait for CONNECT or response let response = self.driver.read_until(b"\r\n", 60000)?; let text = String::from_utf8_lossy(&response); tracing::info!("Dial response: {}", text); // Check for successful connection if text.contains("NO CARRIER") || text.contains("BUSY") { return Err(FaxError::NoCarrier); } // Class 2: wait for fax session indicators // CONNECT indicates fax session is ready // The modem returns: CONNECT, +FCON: let extra = self.driver.read_all(5000)?; let extra_text = String::from_utf8_lossy(&extra); tracing::info!("Session info: {}", extra_text); Ok(()) } fn send_page(&mut self, page: &Page) -> Result<()> { // Start page transmission self.driver.write_raw(b"AT+FDT\r")?; self.driver.flush()?; // Wait for CONNECT let response = self.driver.read_until(b"CONNECT\r\n", 10000)?; let text = String::from_utf8_lossy(&response); tracing::debug!("FDT response: {}", text); if text.contains("ERROR") { return Err(FaxError::modem("FDT command failed")); } // Encode pixel data to T.4 MH format tracing::info!("Encoding {}x{} pixels to T.4 MH...", page.width_pels, page.rows); let mh_data = MhEncoder::encode_page(page)?; tracing::info!("Encoded to {} bytes", mh_data.len()); // Escape any DLE (0x10) bytes in the data by doubling them let escaped_data = self.escape_dle(&mh_data); self.driver.write_raw(&escaped_data)?; self.driver.write_raw(&DLE_ETX)?; self.driver.flush()?; // Wait for page confirmation let result = self.driver.read_until(b"\r\n", 30000)?; let result_text = String::from_utf8_lossy(&result); tracing::info!("Page result: {}", result_text); // Check for errors if result_text.contains("ERROR") { return Err(FaxError::protocol("Page transmission failed")); } Ok(()) } fn end_page(&mut self, last_page: bool) -> Result<()> { let cmd = Class2Commands::end_page(!last_page); self.driver.write_raw(cmd.as_bytes())?; self.driver.write_raw(b"\r")?; self.driver.flush()?; // Wait for confirmation let response = self.driver.read_until(b"\r\n", 10000)?; tracing::debug!("End page response: {}", String::from_utf8_lossy(&response)); Ok(()) } fn hang_up(&mut self) -> Result<()> { self.driver.write_raw(b"AT+FHNG=0\r")?; std::thread::sleep(std::time::Duration::from_millis(500)); self.driver.drain()?; // Also send ATH to ensure hangup self.driver.write_raw(b"ATH\r")?; self.driver.drain()?; Ok(()) } /// Escape DLE characters in fax data (Class 2 requires DLE doubling) fn escape_dle(&self, data: &[u8]) -> Vec { let mut escaped = Vec::with_capacity(data.len() * 2); for &byte in data { if byte == DLE { escaped.push(DLE); escaped.push(DLE); // Double DLE to escape it } else { escaped.push(byte); } } escaped } }