use crate::config::{Config, ConnPref};
use crate::print_task::PrintTask;
use crate::printer::Pacing;
use crate::printer::{PrintOptions, PrinterClient, PrinterSummary};
use crate::profile::PrinterProfile;
use crate::protocol::Model;
#[cfg(feature = "ble")]
use crate::transport::BleTransport;
#[cfg(feature = "serial")]
use crate::transport::SerialTransport;
use crate::transport::Transport;
#[cfg(any(feature = "ble", feature = "serial"))]
use anyhow::Context;
use anyhow::{Result, bail};
use std::path::Path;
pub struct ConnectionOptions {
pub conn: ConnPref,
pub addr: String,
pub scan_secs: u64,
pub match_mode: crate::transport::BleMatchMode,
pub pacing: Pacing,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TargetOptions {
pub task: Option<PrintTask>,
pub allow_experimental: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum TaskSelection {
Auto {
default: PrintTask,
},
Explicit(PrintTask),
}
impl TaskSelection {
fn initial_task(self) -> PrintTask {
match self {
Self::Auto { default } => default,
Self::Explicit(task) => task,
}
}
fn uses_detected_default(self) -> bool {
matches!(self, Self::Auto { .. })
}
}
#[derive(Debug, Clone, Copy)]
pub struct PrintTarget {
pub model: Model,
pub task: TaskSelection,
pub allow_experimental: bool,
}
pub fn resolve_target(
cfg: &Config,
model: Option<Model>,
args: &TargetOptions,
) -> Result<PrintTarget> {
let model = cfg.resolve_model(model);
let task = resolve_task(model, args)?;
Ok(PrintTarget {
model,
task: match args.task {
Some(task) => TaskSelection::Explicit(task),
None => TaskSelection::Auto { default: task },
},
allow_experimental: args.allow_experimental,
})
}
pub struct Session<T: Transport = AnyTransport> {
client: PrinterClient<T>,
connection: ConnPref,
allow_experimental: bool,
}
#[derive(Debug, Clone, Copy)]
enum IdentityDetail {
Profile,
Full,
}
pub fn combine_job_and_close<T>(job: Result<T>, close: Result<()>) -> Result<T> {
match (job, close) {
(Err(job), Err(close)) => {
tracing::warn!(error = %close, "printer shutdown failed after operation error");
Err(job)
}
(Err(job), Ok(())) => Err(job),
(Ok(_), Err(close)) => Err(close),
(Ok(value), Ok(())) => Ok(value),
}
}
impl<T: Transport> Session<T> {
pub async fn from_transport(
transport: T,
connection: ConnPref,
target: PrintTarget,
pacing: Pacing,
) -> Result<Self> {
let client =
PrinterClient::new_with_task(transport, target.model, target.task.initial_task())
.with_pacing(pacing);
Self::finish_connect(client, connection, target, IdentityDetail::Profile).await
}
pub fn print_task(&self) -> PrintTask {
self.client.print_task()
}
#[cfg_attr(not(any(feature = "ble", feature = "serial")), allow(dead_code))]
async fn finish_connect(
mut client: PrinterClient<T>,
connection: ConnPref,
target: PrintTarget,
identity_detail: IdentityDetail,
) -> Result<Self> {
let identity_result = match identity_detail {
IdentityDetail::Profile => client.identify_profile().await,
IdentityDetail::Full => client.identify().await,
};
let identity = match identity_result {
Ok(identity) => Some(identity),
Err(error) => {
tracing::warn!(error = %error, "printer identification failed");
None
}
};
if let Some(identity) = &identity {
if let Some(profile) =
client.apply_identity(identity, target.task.uses_detected_default())
{
tracing::info!(model = %profile.model, model_id = identity.model_id, dpi = profile.dpi, task = ?profile.default_task, "identified printer");
} else {
tracing::warn!(
model_id = identity.model_id,
"printer model is not in the profile registry"
);
}
}
Ok(Self {
client,
connection,
allow_experimental: target.allow_experimental,
})
}
pub fn identity(&self) -> Option<&crate::PrinterIdentity> {
self.client.identity()
}
pub fn profile(&self) -> &'static PrinterProfile {
self.client.profile()
}
pub async fn fetch_summary(&mut self) -> Result<PrinterSummary> {
Ok(self.client.fetch_summary().await?)
}
pub async fn print_image_file_opts(&mut self, path: &Path, opts: PrintOptions) -> Result<()> {
self.ensure_print_allowed()?;
self.client.print_image_file_opts(path, opts).await?;
Ok(())
}
pub async fn render_and_print_gray<R>(
&mut self,
density: crate::Density,
render: impl FnOnce(&'static PrinterProfile) -> Result<(image::GrayImage, R)>,
) -> Result<R> {
self.ensure_print_allowed()?;
let (gray, rendered) = render(self.client.profile())?;
self.client.print_gray_image(&gray, density).await?;
Ok(rendered)
}
pub fn ensure_print_allowed(&self) -> Result<()> {
let identity = self.client.identity().ok_or_else(|| {
anyhow::anyhow!(
"printer identification failed; refusing to print with provisional geometry"
)
})?;
if crate::profile_for_identity(identity).is_none() {
bail!(
"unrecognized printer model id {}; add a checked device profile before printing",
identity.model_id
);
}
let model = self.client.model();
let task = self.client.print_task();
ensure_print_path_allowed(model, task, self.connection, self.allow_experimental)?;
if !crate::profile_for_model(model).print_path_hardware_tested(task, self.connection) {
let connection = self.connection;
eprintln!(
"warning: printer profile '{model}' with print task '{task}' over '{connection}' is experimental \
(not hardware-tested in this project)"
);
}
Ok(())
}
pub async fn print_gray_with_progress(
&mut self,
gray: &image::GrayImage,
density: crate::Density,
progress: impl FnMut(crate::printer::PrintStage) -> crate::Result<()>,
) -> Result<()> {
self.ensure_print_allowed()?;
let raster = crate::image_encode::encode_gray(
gray,
self.client.max_width_px(),
crate::Threshold::DEFAULT.get(),
false,
)?;
self.client.preflight_ready().await?;
Ok(self
.client
.print_raster_with_progress(raster, density, progress)
.await?)
}
pub async fn finish(self) -> Result<()> {
self.client.close().await.map_err(anyhow::Error::from)
}
pub async fn print_gray(
&mut self,
gray: &image::GrayImage,
density: crate::Density,
) -> Result<()> {
self.ensure_print_allowed()?;
Ok(self.client.print_gray_image(gray, density).await?)
}
}
pub async fn run_rendered_session<T: Transport, R>(
mut session: Session<T>,
density: crate::Density,
render: impl FnOnce(&'static PrinterProfile) -> Result<(image::GrayImage, R)>,
) -> Result<R> {
let result = session.render_and_print_gray(density, render).await;
let close_result = session.finish().await;
combine_job_and_close(result, close_result)
}
fn connected_safe_area(
cfg: &Config,
profile: &PrinterProfile,
full_bleed: bool,
) -> crate::geometry::SafeArea {
if full_bleed {
crate::geometry::SafeArea::NONE
} else {
cfg.resolve_safe_area(profile.pixels_per_mm())
}
}
pub async fn run_file_session<T: Transport>(
mut session: Session<T>,
cfg: &Config,
path: &Path,
mut opts: PrintOptions,
full_bleed: bool,
) -> Result<()> {
opts.safe = connected_safe_area(cfg, session.profile(), full_bleed);
let result = session.print_image_file_opts(path, opts).await;
let close_result = session.finish().await;
combine_job_and_close(result, close_result)
}
pub fn resolve_task(model: Model, args: &TargetOptions) -> Result<PrintTask> {
args.task.or_else(|| PrintTask::for_model(model)).ok_or_else(|| {
anyhow::anyhow!(
"model '{model}' has no verified default print task; identify it and pass --task explicitly"
)
})
}
#[cfg(test)]
mod tests;
mod connection;
pub use connection::AnyTransport;
mod policy;
use policy::ensure_print_path_allowed;
pub use policy::ensure_target_print_allowed;