mod addr;
mod config;
mod error;
mod frame;
mod job;
mod retry;
#[cfg(feature = "serial")]
mod serial;
mod status;
#[cfg(feature = "tcp")]
mod tcp;
#[cfg(feature = "usb")]
mod usb;
#[cfg(feature = "tcp")]
pub use addr::resolve_printer_addr;
pub use config::{BatchOptions, PrinterConfig, PrinterTimeouts, RetryConfig};
pub use error::{PrintError, PrinterErrorKind};
pub use frame::{expected_frame_count, read_frames};
pub use job::{JobId, JobPhase, create_job_id};
pub use retry::{ReconnectRetryPrinter, RetryPrinter};
#[cfg(feature = "serial")]
pub use serial::{
SerialDataBits, SerialFlowControl, SerialParity, SerialPrinter, SerialSettings, SerialStopBits,
};
pub use status::{HostStatus, PrintMode, PrinterInfo};
#[cfg(feature = "tcp")]
pub use tcp::TcpPrinter;
#[cfg(feature = "usb")]
pub use usb::UsbPrinter;
use std::ops::ControlFlow;
use std::time::{Duration, Instant};
pub trait Printer: Send {
fn send_raw(&mut self, data: &[u8]) -> Result<(), PrintError>;
fn send_zpl(&mut self, zpl: &str) -> Result<(), PrintError> {
self.send_raw(zpl.as_bytes())
}
}
pub trait StatusQuery: Printer {
fn query_raw(&mut self, cmd: &[u8]) -> Result<Vec<Vec<u8>>, PrintError>;
fn query_status(&mut self) -> Result<HostStatus, PrintError> {
let frames = self.query_raw(b"~HS")?;
HostStatus::parse(&frames)
}
fn query_info(&mut self) -> Result<PrinterInfo, PrintError> {
let frames = self.query_raw(b"~HI")?;
PrinterInfo::parse(&frames)
}
}
pub trait Reconnectable {
fn reconnect(&mut self) -> Result<(), PrintError>;
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchProgress {
pub sent: usize,
pub total: usize,
pub status: Option<HostStatus>,
pub phase: JobPhase,
pub job_id: JobId,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchResult {
pub sent: usize,
pub total: usize,
pub job_id: JobId,
}
pub fn send_batch<P, F>(
printer: &mut P,
labels: &[impl AsRef<[u8]>],
mut on_progress: F,
) -> Result<BatchResult, PrintError>
where
P: Printer,
F: FnMut(BatchProgress) -> ControlFlow<(), ()>,
{
let job_id = create_job_id();
let total = labels.len();
let queued = BatchProgress {
sent: 0,
total,
status: None,
phase: JobPhase::Queued,
job_id: job_id.clone(),
};
if let ControlFlow::Break(()) = on_progress(queued) {
let aborted = BatchProgress {
sent: 0,
total,
status: None,
phase: JobPhase::Aborted,
job_id: job_id.clone(),
};
let _ = on_progress(aborted);
return Ok(BatchResult {
sent: 0,
total,
job_id,
});
}
for (i, label) in labels.iter().enumerate() {
let phase = if i + 1 < total {
JobPhase::Sending
} else {
JobPhase::Sent
};
if let Err(err) = printer.send_raw(label.as_ref()) {
let failed = BatchProgress {
sent: i,
total,
status: None,
phase: JobPhase::Failed,
job_id: job_id.clone(),
};
let _ = on_progress(failed);
return Err(err);
}
let progress = BatchProgress {
sent: i + 1,
total,
status: None,
phase,
job_id: job_id.clone(),
};
if let ControlFlow::Break(()) = on_progress(progress) {
let aborted = BatchProgress {
sent: i + 1,
total,
status: None,
phase: JobPhase::Aborted,
job_id: job_id.clone(),
};
let _ = on_progress(aborted);
return Ok(BatchResult {
sent: i + 1,
total,
job_id: job_id.clone(),
});
}
}
Ok(BatchResult {
sent: total,
total,
job_id,
})
}
pub fn send_batch_with_status<P, F>(
printer: &mut P,
labels: &[impl AsRef<[u8]>],
opts: &BatchOptions,
mut on_progress: F,
) -> Result<BatchResult, PrintError>
where
P: StatusQuery,
F: FnMut(BatchProgress) -> ControlFlow<(), ()>,
{
let job_id = create_job_id();
let total = labels.len();
let queued = BatchProgress {
sent: 0,
total,
status: None,
phase: JobPhase::Queued,
job_id: job_id.clone(),
};
if let ControlFlow::Break(()) = on_progress(queued) {
let aborted = BatchProgress {
sent: 0,
total,
status: None,
phase: JobPhase::Aborted,
job_id: job_id.clone(),
};
let _ = on_progress(aborted);
return Ok(BatchResult {
sent: 0,
total,
job_id,
});
}
for (i, label) in labels.iter().enumerate() {
let phase = if i + 1 < total {
JobPhase::Sending
} else {
JobPhase::Sent
};
if let Err(err) = printer.send_raw(label.as_ref()) {
let failed = BatchProgress {
sent: i,
total,
status: None,
phase: JobPhase::Failed,
job_id: job_id.clone(),
};
let _ = on_progress(failed);
return Err(err);
}
let status = if let Some(interval) = opts.status_interval {
if (i + 1) % interval.get() == 0 {
printer.query_status().ok()
} else {
None
}
} else {
None
};
let progress = BatchProgress {
sent: i + 1,
total,
status: status.clone(),
phase,
job_id: job_id.clone(),
};
if let ControlFlow::Break(()) = on_progress(progress) {
let aborted = BatchProgress {
sent: i + 1,
total,
status: status.clone(),
phase: JobPhase::Aborted,
job_id: job_id.clone(),
};
let _ = on_progress(aborted);
return Ok(BatchResult {
sent: i + 1,
total,
job_id: job_id.clone(),
});
}
}
Ok(BatchResult {
sent: total,
total,
job_id,
})
}
pub fn wait_for_completion<S: StatusQuery>(
printer: &mut S,
poll_interval: Duration,
timeout: Duration,
) -> Result<(), PrintError> {
let now = Instant::now();
let deadline = now
.checked_add(timeout)
.unwrap_or_else(|| now + Duration::from_secs(86400));
loop {
let status = printer.query_status()?;
if status.formats_in_buffer == 0 && status.labels_remaining == 0 {
return Ok(());
}
if Instant::now() >= deadline {
return Err(PrintError::CompletionTimeout {
formats_in_buffer: status.formats_in_buffer,
labels_remaining: status.labels_remaining,
});
}
std::thread::sleep(poll_interval);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::ControlFlow;
struct MockBatchPrinter {
sent: Vec<Vec<u8>>,
fail_on: Option<usize>,
}
impl Printer for MockBatchPrinter {
fn send_raw(&mut self, data: &[u8]) -> Result<(), PrintError> {
if Some(self.sent.len()) == self.fail_on {
return Err(PrintError::WriteFailed(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"mock error",
)));
}
self.sent.push(data.to_vec());
Ok(())
}
}
#[test]
fn batch_happy_path() {
let mut printer = MockBatchPrinter {
sent: Vec::new(),
fail_on: None,
};
let labels = vec!["^XA^FDOne^FS^XZ", "^XA^FDTwo^FS^XZ", "^XA^FDThree^FS^XZ"];
let result = send_batch(&mut printer, &labels, |_| ControlFlow::Continue(())).unwrap();
assert_eq!(result.sent, 3);
assert_eq!(result.total, 3);
assert_eq!(printer.sent.len(), 3);
assert!(result.job_id.as_str().starts_with("job-"));
}
#[test]
fn batch_empty() {
let mut printer = MockBatchPrinter {
sent: Vec::new(),
fail_on: None,
};
let labels: Vec<&str> = vec![];
let result = send_batch(&mut printer, &labels, |_| ControlFlow::Continue(())).unwrap();
assert_eq!(result.sent, 0);
assert_eq!(result.total, 0);
assert!(result.job_id.as_str().starts_with("job-"));
}
#[test]
fn batch_early_abort() {
let mut printer = MockBatchPrinter {
sent: Vec::new(),
fail_on: None,
};
let labels = vec!["one", "two", "three", "four", "five"];
let result = send_batch(&mut printer, &labels, |progress| {
if progress.sent >= 2 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.unwrap();
assert_eq!(result.sent, 2);
assert_eq!(result.total, 5);
assert!(result.job_id.as_str().starts_with("job-"));
}
#[test]
fn batch_error_propagates() {
let mut printer = MockBatchPrinter {
sent: Vec::new(),
fail_on: Some(1),
};
let labels = vec!["ok", "fail", "never"];
let result = send_batch(&mut printer, &labels, |_| ControlFlow::Continue(()));
assert!(result.is_err());
assert_eq!(printer.sent.len(), 1);
}
struct MockStatusPrinter {
sent: Vec<Vec<u8>>,
fail_on: Option<usize>,
status_queries: usize,
}
impl Printer for MockStatusPrinter {
fn send_raw(&mut self, data: &[u8]) -> Result<(), PrintError> {
if Some(self.sent.len()) == self.fail_on {
return Err(PrintError::WriteFailed(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"mock error",
)));
}
self.sent.push(data.to_vec());
Ok(())
}
}
impl StatusQuery for MockStatusPrinter {
fn query_raw(&mut self, _cmd: &[u8]) -> Result<Vec<Vec<u8>>, PrintError> {
self.status_queries += 1;
Ok(vec![
b"030,0,0,1245,000,0,0,0,000,0,0,0".to_vec(),
b"000,0,0,0,0,2,0,0,00000000,0,000".to_vec(),
b"1234,0".to_vec(),
])
}
}
#[test]
fn batch_with_status_happy_path() {
use std::num::NonZeroUsize;
let mut printer = MockStatusPrinter {
sent: Vec::new(),
fail_on: None,
status_queries: 0,
};
let labels: Vec<&str> = vec!["L1", "L2", "L3", "L4", "L5"];
let opts = BatchOptions {
status_interval: Some(NonZeroUsize::new(2).unwrap()),
..BatchOptions::default()
};
let mut progresses = Vec::new();
let result = send_batch_with_status(&mut printer, &labels, &opts, |p| {
progresses.push(p.clone());
ControlFlow::Continue(())
})
.unwrap();
assert_eq!(result.sent, 5);
assert_eq!(result.total, 5);
assert_eq!(printer.sent.len(), 5);
assert_eq!(printer.status_queries, 2);
assert_eq!(progresses[0].sent, 0);
assert_eq!(progresses[0].phase, JobPhase::Queued);
assert!(progresses[2].status.is_some()); assert!(progresses[4].status.is_some());
assert!(progresses[1].status.is_none()); assert!(progresses[3].status.is_none()); assert!(progresses[5].status.is_none()); }
#[test]
fn batch_with_status_no_interval() {
let mut printer = MockStatusPrinter {
sent: Vec::new(),
fail_on: None,
status_queries: 0,
};
let labels: Vec<&str> = vec!["L1", "L2", "L3"];
let opts = BatchOptions {
status_interval: None,
..BatchOptions::default()
};
let result =
send_batch_with_status(&mut printer, &labels, &opts, |_| ControlFlow::Continue(()))
.unwrap();
assert_eq!(result.sent, 3);
assert_eq!(result.total, 3);
assert_eq!(printer.status_queries, 0);
}
#[test]
fn batch_with_status_early_abort() {
use std::num::NonZeroUsize;
let mut printer = MockStatusPrinter {
sent: Vec::new(),
fail_on: None,
status_queries: 0,
};
let labels: Vec<&str> = vec!["L1", "L2", "L3", "L4", "L5"];
let opts = BatchOptions {
status_interval: Some(NonZeroUsize::new(2).unwrap()),
..BatchOptions::default()
};
let result = send_batch_with_status(&mut printer, &labels, &opts, |p| {
if p.sent >= 3 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.unwrap();
assert_eq!(result.sent, 3);
assert_eq!(result.total, 5);
assert_eq!(printer.sent.len(), 3);
assert_eq!(printer.status_queries, 1); }
#[test]
fn batch_emits_queued_and_aborted_phases() {
let mut printer = MockBatchPrinter {
sent: Vec::new(),
fail_on: None,
};
let labels = vec!["one", "two", "three"];
let mut phases = Vec::new();
let result = send_batch(&mut printer, &labels, |progress| {
phases.push(progress.phase);
if progress.sent >= 1 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.unwrap();
assert_eq!(result.sent, 1);
assert_eq!(phases[0], JobPhase::Queued);
assert!(phases.contains(&JobPhase::Sending) || phases.contains(&JobPhase::Sent));
assert_eq!(
*phases.last().expect("at least one phase"),
JobPhase::Aborted
);
}
#[test]
fn batch_emits_failed_phase_before_error() {
let mut printer = MockBatchPrinter {
sent: Vec::new(),
fail_on: Some(0),
};
let labels = vec!["fail"];
let mut phases = Vec::new();
let result = send_batch(&mut printer, &labels, |progress| {
phases.push(progress.phase);
ControlFlow::Continue(())
});
assert!(result.is_err());
assert_eq!(phases[0], JobPhase::Queued);
assert_eq!(
*phases.last().expect("at least one phase"),
JobPhase::Failed
);
}
#[test]
fn batch_with_status_error_propagates() {
use std::num::NonZeroUsize;
let mut printer = MockStatusPrinter {
sent: Vec::new(),
fail_on: Some(1), status_queries: 0,
};
let labels: Vec<&str> = vec!["L1", "L2", "L3"];
let opts = BatchOptions {
status_interval: Some(NonZeroUsize::new(1).unwrap()),
..BatchOptions::default()
};
let result =
send_batch_with_status(&mut printer, &labels, &opts, |_| ControlFlow::Continue(()));
assert!(result.is_err());
assert_eq!(printer.sent.len(), 1);
}
struct MockCompletionPrinter {
polls: usize,
complete_after: usize,
}
impl Printer for MockCompletionPrinter {
fn send_raw(&mut self, _data: &[u8]) -> Result<(), PrintError> {
Ok(())
}
}
impl StatusQuery for MockCompletionPrinter {
fn query_raw(&mut self, _cmd: &[u8]) -> Result<Vec<Vec<u8>>, PrintError> {
self.polls += 1;
let remaining = if self.polls >= self.complete_after {
0
} else {
5
};
Ok(vec![
b"030,0,0,1245,000,0,0,0,000,0,0,0".to_vec(),
format!("000,0,0,0,0,2,0,{remaining},00000000,0,000").into_bytes(),
b"1234,0".to_vec(),
])
}
}
#[test]
fn wait_for_completion_immediate() {
let mut printer = MockCompletionPrinter {
polls: 0,
complete_after: 1, };
let result = wait_for_completion(
&mut printer,
Duration::from_millis(10),
Duration::from_secs(5),
);
assert!(result.is_ok());
assert_eq!(printer.polls, 1);
}
#[test]
fn wait_for_completion_after_polls() {
let mut printer = MockCompletionPrinter {
polls: 0,
complete_after: 3, };
let result = wait_for_completion(
&mut printer,
Duration::from_millis(10), Duration::from_secs(5),
);
assert!(result.is_ok());
assert_eq!(printer.polls, 3);
}
#[test]
fn wait_for_completion_timeout() {
let mut printer = MockCompletionPrinter {
polls: 0,
complete_after: 999, };
let result = wait_for_completion(
&mut printer,
Duration::from_millis(1),
Duration::from_millis(10),
);
match result {
Err(PrintError::CompletionTimeout {
formats_in_buffer,
labels_remaining,
}) => {
assert_eq!(formats_in_buffer, 0);
assert_eq!(labels_remaining, 5);
}
other => panic!("expected CompletionTimeout, got {:?}", other),
}
}
struct MockFormatsInBufferPrinter {
polls: usize,
}
impl Printer for MockFormatsInBufferPrinter {
fn send_raw(&mut self, _data: &[u8]) -> Result<(), PrintError> {
Ok(())
}
}
impl StatusQuery for MockFormatsInBufferPrinter {
fn query_raw(&mut self, _cmd: &[u8]) -> Result<Vec<Vec<u8>>, PrintError> {
self.polls += 1;
let formats = if self.polls >= 3 { 0 } else { 2 };
Ok(vec![
format!("030,0,0,1245,{formats:03},0,0,0,000,0,0,0").into_bytes(),
b"000,0,0,0,0,2,0,0,00000000,0,000".to_vec(),
b"1234,0".to_vec(),
])
}
}
#[test]
fn wait_for_completion_waits_for_formats_in_buffer() {
let mut printer = MockFormatsInBufferPrinter { polls: 0 };
let result = wait_for_completion(
&mut printer,
Duration::from_millis(1),
Duration::from_secs(5),
);
assert!(result.is_ok());
assert!(
printer.polls >= 3,
"should have polled until formats cleared"
);
}
}