use super::pacing::Pacing;
use super::raw::RawPrinter;
use crate::errors::{Error, Result};
use crate::packet::PacketDecoder;
use crate::print_task::PrintTask;
use crate::profile::{PrinterDevice, PrinterIdentity, PrinterProfile};
use crate::protocol::Model;
use crate::transport::Transport;
use std::time::Duration;
use tracing::{info, warn};
pub struct PrinterClient<T: Transport> {
pub(crate) transport: T,
pub(crate) device: PrinterDevice,
pub(crate) pacing: Pacing,
pub(crate) decoder: PacketDecoder,
}
pub const LOW_BATTERY_LEVEL: u8 = 1;
fn warn_if_battery_low(power_level: Option<u8>) {
if let Some(level) = power_level
&& level <= LOW_BATTERY_LEVEL
{
warn!(
level,
"battery low — dense or dark labels may print only partway. \
Charge the printer, or use a lower --density, if output is clipped"
);
}
}
impl<T: Transport> PrinterClient<T> {
pub fn new(transport: T, model: Model) -> Self {
let task = PrintTask::for_model(model)
.expect("model has no default print task; construct with new_with_task");
Self::new_with_task(transport, model, task)
}
pub fn new_with_task(transport: T, model: Model, task: PrintTask) -> Self {
Self {
transport,
device: PrinterDevice::configured(model, task),
pacing: Pacing::REAL,
decoder: PacketDecoder::new(),
}
}
pub fn with_print_task(mut self, task: PrintTask) -> Self {
self.device.set_task(task);
self
}
pub fn with_pacing(mut self, pacing: Pacing) -> Self {
self.pacing = pacing;
self
}
pub fn max_width_px(&self) -> u32 {
self.device.profile().max_width_px
}
pub fn profile(&self) -> &'static PrinterProfile {
self.device.profile()
}
pub fn transport(&self) -> &T {
&self.transport
}
pub fn model(&self) -> Model {
self.device.model()
}
pub fn print_task(&self) -> PrintTask {
self.device.task()
}
pub fn identity(&self) -> Option<&PrinterIdentity> {
self.device.identity()
}
pub fn raw(&mut self) -> RawPrinter<'_, T> {
RawPrinter { client: self }
}
pub(crate) fn require_ack(ok: bool, step: &'static str, cmd: u8) -> Result<()> {
if ok {
Ok(())
} else {
Err(Error::CommandRejected { step, cmd })
}
}
pub(crate) async fn maybe_sleep(&self, d: Duration) {
if !d.is_zero() {
tokio::time::sleep(d).await;
}
}
pub async fn preflight_ready(&mut self) -> Result<()> {
match self.heartbeat().await {
Ok(hb) => {
info!(
power = ?hb.power_level,
lid = ?hb.closing_state,
paper = ?hb.paper_state,
rfid = ?hb.rfid_read_state,
"preflight heartbeat"
);
if let Some(code) = hb.print_blocker() {
return Err(Error::Printer(code));
}
warn_if_battery_low(hb.power_level);
Ok(())
}
Err(e @ (Error::Printer(_) | Error::Transport(_))) => Err(e),
Err(e) => {
warn!(error = %e, "preflight heartbeat unavailable; continuing");
Ok(())
}
}
}
pub async fn close(mut self) -> Result<()> {
self.transport.close().await
}
}
#[cfg(test)]
#[path = "client_tests.rs"]
mod tests;