From 6555e9c0bd7d308eda31e1318e242dd1179693a2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 27 Feb 2025 01:28:08 +0100 Subject: [PATCH] Switch to persistent printer with queue --- showbits-thermal-printer/src/drawer.rs | 17 ++- .../src/drawer/backlog.rs | 12 ++ .../src/drawer/calendar.rs | 8 +- showbits-thermal-printer/src/drawer/cells.rs | 12 +- .../src/drawer/chat_message.rs | 4 +- showbits-thermal-printer/src/drawer/egg.rs | 10 +- showbits-thermal-printer/src/drawer/image.rs | 10 +- showbits-thermal-printer/src/drawer/photo.rs | 8 +- showbits-thermal-printer/src/drawer/text.rs | 4 +- .../src/drawer/tictactoe.rs | 8 +- showbits-thermal-printer/src/drawer/typst.rs | 10 +- showbits-thermal-printer/src/main.rs | 21 ++- .../src/persistent_printer.rs | 135 ++++++++++++++++++ showbits-thermal-printer/src/printer.rs | 38 ++--- 14 files changed, 221 insertions(+), 76 deletions(-) create mode 100644 showbits-thermal-printer/src/drawer/backlog.rs create mode 100644 showbits-thermal-printer/src/persistent_printer.rs diff --git a/showbits-thermal-printer/src/drawer.rs b/showbits-thermal-printer/src/drawer.rs index fac12d8..4880c88 100644 --- a/showbits-thermal-printer/src/drawer.rs +++ b/showbits-thermal-printer/src/drawer.rs @@ -1,3 +1,4 @@ +mod backlog; mod calendar; mod cells; mod chat_message; @@ -11,21 +12,23 @@ mod typst; use showbits_common::widgets::{FontStuff, HasFontStuff}; use tokio::sync::mpsc; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; pub use self::{ - calendar::CalendarDrawing, cells::CellsDrawing, chat_message::ChatMessageDrawing, - egg::EggDrawing, image::ImageDrawing, photo::PhotoDrawing, text::TextDrawing, - tictactoe::TicTacToeDrawing, typst::TypstDrawing, + backlog::BacklogDrawing, calendar::CalendarDrawing, cells::CellsDrawing, + chat_message::ChatMessageDrawing, egg::EggDrawing, image::ImageDrawing, photo::PhotoDrawing, + text::TextDrawing, tictactoe::TicTacToeDrawing, typst::TypstDrawing, }; +pub const FEED: f32 = 64.0; + #[derive(Default)] pub struct Context { font_stuff: FontStuff, } pub trait Drawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()>; + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()>; } pub struct Command(Box); @@ -44,12 +47,12 @@ impl HasFontStuff for Context { pub struct Drawer { rx: mpsc::Receiver, - printer: Printer, + printer: PersistentPrinter, ctx: Context, } impl Drawer { - pub fn new(rx: mpsc::Receiver, printer: Printer) -> Self { + pub fn new(rx: mpsc::Receiver, printer: PersistentPrinter) -> Self { Self { rx, printer, diff --git a/showbits-thermal-printer/src/drawer/backlog.rs b/showbits-thermal-printer/src/drawer/backlog.rs new file mode 100644 index 0000000..9afd542 --- /dev/null +++ b/showbits-thermal-printer/src/drawer/backlog.rs @@ -0,0 +1,12 @@ +use crate::persistent_printer::PersistentPrinter; + +use super::{Context, Drawing}; + +pub struct BacklogDrawing; + +impl Drawing for BacklogDrawing { + fn draw(&self, printer: &mut PersistentPrinter, _ctx: &mut Context) -> anyhow::Result<()> { + printer.print_backlog()?; + Ok(()) + } +} diff --git a/showbits-thermal-printer/src/drawer/calendar.rs b/showbits-thermal-printer/src/drawer/calendar.rs index a3b571e..a3cc0ad 100644 --- a/showbits-thermal-printer/src/drawer/calendar.rs +++ b/showbits-thermal-printer/src/drawer/calendar.rs @@ -9,9 +9,9 @@ use taffy::{ style_helpers::{length, percent, repeat}, }; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; pub struct CalendarDrawing { pub year: i16, @@ -19,7 +19,7 @@ pub struct CalendarDrawing { } impl Drawing for CalendarDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut date = civil::Date::new(self.year, self.month, 1)?; let mut tree = Tree::::new(WHITE); @@ -86,6 +86,7 @@ impl Drawing for CalendarDrawing { let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .with_display(Display::Flex) .with_flex_direction(FlexDirection::Column) .with_align_items(Some(AlignItems::Center)) @@ -94,7 +95,6 @@ impl Drawing for CalendarDrawing { .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/drawer/cells.rs b/showbits-thermal-printer/src/drawer/cells.rs index e5be0f0..d308c97 100644 --- a/showbits-thermal-printer/src/drawer/cells.rs +++ b/showbits-thermal-printer/src/drawer/cells.rs @@ -3,11 +3,11 @@ use image::{ imageops::{self, FilterType}, }; use showbits_common::{Node, Tree, WidgetExt, color, widgets::Image}; -use taffy::{AlignItems, Display, FlexDirection, style_helpers::percent}; +use taffy::{AlignItems, Display, FlexDirection, prelude::length, style_helpers::percent}; -use crate::printer::Printer; +use crate::{persistent_printer::PersistentPrinter, printer::Printer}; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; const BLACK: Rgba = Rgba([0, 0, 0, 255]); const WHITE: Rgba = Rgba([255, 255, 255, 255]); @@ -49,8 +49,8 @@ pub struct CellsDrawing { } impl Drawing for CellsDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { - let mut image = RgbaImage::new(Printer::WIDTH / self.scale, self.rows); + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { + let mut image: image::ImageBuffer, Vec> = RgbaImage::new(Printer::WIDTH / self.scale, self.rows); // Initialize first line randomly for x in 0..image.width() { @@ -79,6 +79,7 @@ impl Drawing for CellsDrawing { let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .with_display(Display::Flex) .with_flex_direction(FlexDirection::Column) .with_align_items(Some(AlignItems::Center)) @@ -86,7 +87,6 @@ impl Drawing for CellsDrawing { .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/drawer/chat_message.rs b/showbits-thermal-printer/src/drawer/chat_message.rs index b75718c..af2d96b 100644 --- a/showbits-thermal-printer/src/drawer/chat_message.rs +++ b/showbits-thermal-printer/src/drawer/chat_message.rs @@ -8,7 +8,7 @@ use taffy::{ style_helpers::{length, percent}, }; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; use super::{Context, Drawing}; @@ -18,7 +18,7 @@ pub struct ChatMessageDrawing { } impl Drawing for ChatMessageDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut tree = Tree::::new(WHITE); let max_username_width_in_chars = 32.0; diff --git a/showbits-thermal-printer/src/drawer/egg.rs b/showbits-thermal-printer/src/drawer/egg.rs index 6d0619d..b6e1e92 100644 --- a/showbits-thermal-printer/src/drawer/egg.rs +++ b/showbits-thermal-printer/src/drawer/egg.rs @@ -6,11 +6,11 @@ use showbits_common::{ color::{self, WHITE}, widgets::{Image, Text}, }; -use taffy::{AlignItems, Display, FlexDirection, style_helpers::percent}; +use taffy::{prelude::length, style_helpers::percent, AlignItems, Display, FlexDirection}; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; pub struct EggDrawing; @@ -21,7 +21,7 @@ fn load_image(bytes: &[u8]) -> RgbaImage { } impl Drawing for EggDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut rng = rand::rng(); // Choose which set of egg images to use @@ -84,6 +84,7 @@ impl Drawing for EggDrawing { let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .with_display(Display::Flex) .with_flex_direction(FlexDirection::Column) .with_align_items(Some(AlignItems::Center)) @@ -92,7 +93,6 @@ impl Drawing for EggDrawing { .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/drawer/image.rs b/showbits-thermal-printer/src/drawer/image.rs index 8064b57..ab4c52c 100644 --- a/showbits-thermal-printer/src/drawer/image.rs +++ b/showbits-thermal-printer/src/drawer/image.rs @@ -5,11 +5,11 @@ use showbits_common::{ color::{self, BLACK, WHITE}, widgets::{DitherAlgorithm, Image}, }; -use taffy::{AlignItems, Display, FlexDirection, style_helpers::percent}; +use taffy::{AlignItems, Display, FlexDirection, prelude::length, style_helpers::percent}; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; pub struct ImageDrawing { pub image: RgbaImage, @@ -19,7 +19,7 @@ pub struct ImageDrawing { } impl Drawing for ImageDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut image = self.image.clone(); if self.bright { for pixel in image.pixels_mut() { @@ -40,6 +40,7 @@ impl Drawing for ImageDrawing { let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .with_display(Display::Flex) .with_flex_direction(FlexDirection::Column) .with_align_items(Some(AlignItems::Center)) @@ -47,7 +48,6 @@ impl Drawing for ImageDrawing { .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/drawer/photo.rs b/showbits-thermal-printer/src/drawer/photo.rs index c58774b..981a029 100644 --- a/showbits-thermal-printer/src/drawer/photo.rs +++ b/showbits-thermal-printer/src/drawer/photo.rs @@ -9,9 +9,9 @@ use taffy::{ style_helpers::{length, percent}, }; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; pub struct PhotoDrawing { pub image: RgbaImage, @@ -19,7 +19,7 @@ pub struct PhotoDrawing { } impl Drawing for PhotoDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut tree = Tree::::new(WHITE); let mut image = self.image.clone(); @@ -45,6 +45,7 @@ impl Drawing for PhotoDrawing { let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .with_display(Display::Flex) .with_flex_direction(FlexDirection::Column) .with_align_items(Some(AlignItems::Center)) @@ -54,7 +55,6 @@ impl Drawing for PhotoDrawing { .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/drawer/text.rs b/showbits-thermal-printer/src/drawer/text.rs index 7617cbc..401fdc2 100644 --- a/showbits-thermal-printer/src/drawer/text.rs +++ b/showbits-thermal-printer/src/drawer/text.rs @@ -1,14 +1,14 @@ use showbits_common::{Node, Tree, WidgetExt, color::WHITE, widgets::Text}; use taffy::style_helpers::percent; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; use super::{Context, Drawing}; pub struct TextDrawing(pub String); impl Drawing for TextDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut tree = Tree::::new(WHITE); let text = Text::new() diff --git a/showbits-thermal-printer/src/drawer/tictactoe.rs b/showbits-thermal-printer/src/drawer/tictactoe.rs index 10d3bc8..3d167b6 100644 --- a/showbits-thermal-printer/src/drawer/tictactoe.rs +++ b/showbits-thermal-printer/src/drawer/tictactoe.rs @@ -8,14 +8,14 @@ use taffy::{ style_helpers::{length, percent, repeat}, }; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; pub struct TicTacToeDrawing; impl Drawing for TicTacToeDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let block_size = length(128.0); let width = length(2.0); @@ -57,6 +57,7 @@ impl Drawing for TicTacToeDrawing { let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .with_display(Display::Flex) .with_flex_direction(FlexDirection::Column) .with_align_items(Some(AlignItems::Center)) @@ -66,7 +67,6 @@ impl Drawing for TicTacToeDrawing { .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/drawer/typst.rs b/showbits-thermal-printer/src/drawer/typst.rs index be00db4..ea12a26 100644 --- a/showbits-thermal-printer/src/drawer/typst.rs +++ b/showbits-thermal-printer/src/drawer/typst.rs @@ -1,25 +1,25 @@ use showbits_common::{Node, Tree, WidgetExt, color::WHITE, widgets::Typst}; -use taffy::style_helpers::percent; +use taffy::{prelude::length, style_helpers::percent}; -use crate::printer::Printer; +use crate::persistent_printer::PersistentPrinter; -use super::{Context, Drawing}; +use super::{Context, Drawing, FEED}; pub struct TypstDrawing(pub String); impl Drawing for TypstDrawing { - fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> { + fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> { let mut tree = Tree::::new(WHITE); let typst = Typst::new(self.0.clone()).node().register(&mut tree)?; let root = Node::empty() .with_size_width(percent(1.0)) + .with_padding_bottom(length(FEED)) .and_child(typst) .register(&mut tree)?; printer.print_tree(&mut tree, ctx, root)?; - printer.feed()?; Ok(()) } } diff --git a/showbits-thermal-printer/src/main.rs b/showbits-thermal-printer/src/main.rs index 0a8908c..d5e37a9 100644 --- a/showbits-thermal-printer/src/main.rs +++ b/showbits-thermal-printer/src/main.rs @@ -1,19 +1,24 @@ mod drawer; +mod persistent_printer; mod printer; mod server; -use std::path::PathBuf; +use std::{path::PathBuf, time::Duration}; use clap::Parser; -use drawer::Drawer; -use printer::Printer; +use drawer::{BacklogDrawing, Command}; use tokio::{runtime::Runtime, sync::mpsc}; +use self::{drawer::Drawer, persistent_printer::PersistentPrinter}; + #[derive(Parser)] struct Args { /// Address the web server will listen at. addr: String, + /// Path to the queue directory. + queue: PathBuf, + /// Path to the printer's USB device file. /// /// Usually, this is located at `/dev/usb/lp0` or a similar location. @@ -30,11 +35,17 @@ fn main() -> anyhow::Result<()> { let (tx, rx) = mpsc::channel(3); - let printer = Printer::new(args.printer, args.export)?; + let printer = PersistentPrinter::new(args.printer, args.export, args.queue); let mut drawer = Drawer::new(rx, printer); let runtime = Runtime::new()?; - runtime.spawn(server::run(tx, args.addr)); + runtime.spawn(server::run(tx.clone(), args.addr)); + runtime.spawn(async move { + loop { + let _ = tx.send(Command::draw(BacklogDrawing)).await; + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); println!("Running"); drawer.run()?; diff --git a/showbits-thermal-printer/src/persistent_printer.rs b/showbits-thermal-printer/src/persistent_printer.rs new file mode 100644 index 0000000..0b63c9c --- /dev/null +++ b/showbits-thermal-printer/src/persistent_printer.rs @@ -0,0 +1,135 @@ +use std::{fs, io::ErrorKind, path::PathBuf}; + +use anyhow::{Context, bail}; +use image::RgbaImage; +use jiff::Timestamp; +use showbits_common::Tree; +use taffy::{AvailableSpace, NodeId, Size}; + +use crate::printer::Printer; + +pub struct PersistentPrinter { + printer_file: Option, + export_file: Option, + queue_dir: PathBuf, + + printer: Option, +} + +impl PersistentPrinter { + pub fn new( + printer_file: Option, + export_file: Option, + queue_dir: PathBuf, + ) -> Self { + Self { + printer_file, + export_file, + queue_dir, + printer: None, + } + } + + fn render_tree_to_image( + tree: &mut Tree, + ctx: &mut C, + root: NodeId, + ) -> anyhow::Result { + let available = Size { + width: AvailableSpace::Definite(Printer::WIDTH as f32), + // TODO Maybe MinContent? If not, why not? + height: AvailableSpace::MaxContent, + }; + + tree.render(ctx, root, available) + } + + fn print_image(&mut self, image: &RgbaImage) -> anyhow::Result<()> { + let Some(printer) = &mut self.printer else { + bail!("no printer found"); + }; + printer.print_image(image)?; + Ok(()) + } + + fn reconnect_printer(&mut self) -> anyhow::Result<()> { + let printer = Printer::new(self.printer_file.clone(), self.export_file.clone())?; + self.printer = Some(printer); + Ok(()) + } + + fn print_image_robustly(&mut self, image: &RgbaImage) -> anyhow::Result<()> { + println!("Printing image"); + if self.print_image(image).is_ok() { + return Ok(()); + } + println!("First attempt failed, reconnecting and retrying"); + self.reconnect_printer()?; + self.print_image(image)?; + Ok(()) + } + + fn enqueue_image(&mut self, image: &RgbaImage) -> anyhow::Result<()> { + let now = Timestamp::now(); + let path = self.queue_dir.join(format!("{now}.png")); + println!("Enqueuing image {}", path.display()); + + fs::create_dir_all(&self.queue_dir) + .with_context(|| format!("At {}", self.queue_dir.display())) + .context("Failed to create queue directory")?; + + image + .save(&path) + .with_context(|| format!("At {}", path.display())) + .context("Failed to save image to queue")?; + + Ok(()) + } + + pub fn print_tree( + &mut self, + tree: &mut Tree, + ctx: &mut C, + root: NodeId, + ) -> anyhow::Result<()> { + let image = Self::render_tree_to_image(tree, ctx, root)?; + if self.print_image_robustly(&image).is_err() { + self.enqueue_image(&image)?; + } + Ok(()) + } + + pub fn print_backlog(&mut self) -> anyhow::Result<()> { + let mut files = vec![]; + + match self.queue_dir.read_dir() { + Err(err) if err.kind() == ErrorKind::NotFound => {} + + Err(err) => Err(err) + .with_context(|| format!("At {}", self.queue_dir.display())) + .context("Failed to open queue dir")?, + + Ok(dir) => { + for entry in dir { + let entry = entry?; + if entry.file_type()?.is_file() { + files.push(entry.path()); + } + } + } + } + + files.sort_unstable(); + + for file in files { + println!("Dequeuing image {}", file.display()); + let image: RgbaImage = image::open(&file)?.into_rgba8(); + if self.print_image_robustly(&image).is_err() { + return Ok(()); + } + fs::remove_file(&file)?; + } + + Ok(()) + } +} diff --git a/showbits-thermal-printer/src/printer.rs b/showbits-thermal-printer/src/printer.rs index 124da2f..b999426 100644 --- a/showbits-thermal-printer/src/printer.rs +++ b/showbits-thermal-printer/src/printer.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use anyhow::Context; use escpos::{ driver::FileDriver, printer::Printer as EPrinter, @@ -7,8 +8,7 @@ use escpos::{ utils::{GS, PageCode, Protocol}, }; use image::{Rgba, RgbaImage}; -use showbits_common::{Tree, color}; -use taffy::{AvailableSpace, NodeId, Size}; +use showbits_common::color; pub struct Printer { printer: Option>, @@ -44,7 +44,9 @@ impl Printer { export_path: Option, ) -> anyhow::Result { let printer = if let Some(path) = printer_path { - let driver = FileDriver::open(&path)?; + let driver = FileDriver::open(&path) + .with_context(|| format!("At {}", path.display())) + .context("Failed to open printer driver")?; let protocol = Protocol::default(); let mut options = PrinterOptions::default(); options.page_code(Some(Self::PAGE_CODE)); @@ -59,34 +61,16 @@ impl Printer { }) } - pub fn feed(&mut self) -> anyhow::Result<()> { - if let Some(printer) = &mut self.printer { - printer.init()?.feeds(3)?.print()?; - } - - Ok(()) - } - - pub fn print_tree( - &mut self, - tree: &mut Tree, - ctx: &mut C, - root: NodeId, - ) -> anyhow::Result<()> { - let available = Size { - width: AvailableSpace::Definite(Self::WIDTH as f32), - // TODO Maybe MinContent? If not, why not? - height: AvailableSpace::MaxContent, - }; - - let image = tree.render(ctx, root, available)?; - + pub fn print_image(&mut self, image: &RgbaImage) -> anyhow::Result<()> { if let Some(path) = &self.export_path { - image.save(path)?; + image + .save(path) + .with_context(|| format!("At {}", path.display())) + .context("Failed to export to-be-printed image")?; } if let Some(printer) = &mut self.printer { - Self::print_image_to_printer(printer, &image)?; + Self::print_image_to_printer(printer, image).context("Failed to print image")?; } Ok(())