commit 3414c8ddec42ad54a539807732a3c66cb96038db from: Benjamins Stürz date: Fri Feb 07 22:46:12 2025 UTC Convert to library commit - f11bef8348cab155964838f1cf3c000e0721c7c5 commit + 3414c8ddec42ad54a539807732a3c66cb96038db blob - 2e341f9de998c7131836438f524b1cc63195adbd blob + 33bfae3e36ed9789ed20f831b287589fecbd877d --- Cargo.lock +++ Cargo.lock @@ -36,6 +36,12 @@ dependencies = [ ] [[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -46,10 +52,30 @@ name = "ppa6" version = "0.1.0" dependencies = [ "anyhow", + "log", "rusb", + "thiserror", ] [[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] name = "rusb" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -66,6 +92,43 @@ source = "registry+https://github.com/rust-lang/crates checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" blob - da9c3648d53ff4cc6cfe7806fe8bc889b6ce54ce blob + 839be0c8c81c73509f592bceb20d68a21cf2750c --- Cargo.toml +++ Cargo.toml @@ -5,4 +5,6 @@ edition = "2021" [dependencies] anyhow = "1.0.95" +log = "0.4.25" rusb = "0.9.4" +thiserror = "2.0.11" blob - 75a553c8506f6eca35b1bec8a56e5cbecdf9cca1 (mode 644) blob + /dev/null --- Makefile +++ /dev/null @@ -1,14 +0,0 @@ -SRC != find src -name '*.rs' - -all: ppa6 - -clean: - rm -rf target - rm -f ppa6 - -run: ppa6 - doas ./ppa6 - -ppa6: ${SRC} - cargo build - cp -f target/debug/ppa6 . blob - /dev/null blob + 63bb9b72b7fe331fb3971f331811ff2d9deab75d (mode 644) --- /dev/null +++ examples/black.rs @@ -0,0 +1,10 @@ +use ppa6::{Document, Printer}; + +fn main() { + let ctx = ppa6::usb_context().unwrap(); + let mut printer = Printer::find(&ctx).unwrap(); + + let pixels = vec![0xffu8; 48 * 384]; + let doc = Document::new(pixels).unwrap(); + printer.print(&doc, true).unwrap(); +} blob - 5abc2859e9c1c12d7f15fb2a2731ad63abdaf6e0 (mode 644) blob + /dev/null --- src/main.rs +++ /dev/null @@ -1,170 +0,0 @@ -// Very helpful doc for USB: https://www.beyondlogic.org/usbnutshell/usb1.shtml -use anyhow::{Result, Context as _}; -use std::{iter::repeat_n, time::Duration}; -use rusb::{Direction, TransferType, UsbContext, Context, DeviceHandle}; - -const VENDOR_ID: u16 = 0x09c5; -const PRODUCT_ID: u16 = 0x0200; - -struct Peripage { - handle: DeviceHandle, - ep: u8, -} - -impl Peripage { - fn find(ctx: &Context) -> Result { - let dev = ctx - .devices() - .context("cannot read list of devices")? - .iter() - .find(|dev| { - let Ok(desc) = dev.device_descriptor() else { - eprintln!("cannot get device descriptor for Bus {dev:?}"); - return false - }; - - desc.vendor_id() == VENDOR_ID && desc.product_id() == PRODUCT_ID - }) - .context("no Peripage A6 found")?; - - let handle = dev.open().context("cannot open usb device")?; - Self::connect(handle) - } - - fn connect(handle: DeviceHandle) -> Result { - let dev = handle.device(); - let _ = handle.set_auto_detach_kernel_driver(false); - - let dd = dev.device_descriptor().unwrap(); - println!("Device Descriptor = {dd:#?}"); - assert_eq!(dd.vendor_id(), VENDOR_ID); - assert_eq!(dd.product_id(), PRODUCT_ID); - if let Ok(s) = handle.read_manufacturer_string_ascii(&dd) { - println!("Vendor: {s}"); - } - if let Ok(s) = handle.read_product_string_ascii(&dd) { - println!("Product: {s}"); - } - if let Ok(s) = handle.read_serial_number_string_ascii(&dd) { - println!("Serial: {s}"); - } - - assert_eq!(dd.num_configurations(), 1); - let cd = dev.config_descriptor(0).unwrap(); - println!("Config Descriptor = {cd:#?}"); - - assert_eq!(cd.num_interfaces(), 1); - let int = cd.interfaces().next().unwrap(); - let id = int.descriptors().next().unwrap(); - println!("Interface Descriptor = {id:#?}"); - if let Some(sid) = id.description_string_index() { - println!("Interface: {}", handle.read_string_descriptor_ascii(sid).unwrap()); - } - let kactive = handle.kernel_driver_active(0); - println!("Is kernel driver attached: {kactive:?}"); - - assert_eq!(id.class_code(), 7); // Printer - assert_eq!(id.sub_class_code(), 1); // Printer - assert_eq!(id.protocol_code(), 2); // Bi-directional - assert_eq!(id.num_endpoints(), 2); - - let mut epds = id.endpoint_descriptors(); - let epd0 = epds.next().unwrap(); - let epd1 = epds.next().unwrap(); - println!("Endpoint Descriptor 0: {epd0:#?}"); - println!("Endpoint Descriptor 1: {epd1:#?}"); - - assert_eq!(epd0.address(), 129); // IN (128) + 1 - assert_eq!(epd0.direction(), Direction::In); - assert_eq!(epd0.transfer_type(), TransferType::Bulk); - - assert_eq!(epd1.address(), 2); // OUT (0) + 2 - assert_eq!(epd1.direction(), Direction::Out); - assert_eq!(epd1.transfer_type(), TransferType::Bulk); - - let ep = epd1.address(); - - if let Ok(true) = kactive { - handle - .detach_kernel_driver(0) - .context("failed to detach kernel driver")?; - } - - handle - .claim_interface(0) - .context("failed to claim interface 0")?; - - Ok(Self { - handle, - ep, - }) - } - - fn write(&mut self, buf: &[u8], timeout: u64) -> Result<()> { - self.handle.write_bulk(self.ep, buf, Duration::from_secs(timeout))?; - Ok(()) - } - - fn newline(&mut self) -> Result<()> { - let buf = &[0x10, 0xff, 0xfe, 0x01]; - self.write(buf, 1) - } - - fn confirm(&mut self) -> Result<()> { - let buf = &[0x10, 0xff, 0xfe, 0x45]; - self.write(buf, 1) - } -} - -impl Drop for Peripage { - fn drop(&mut self) { - let _ = self.handle.release_interface(0); - } -} - -fn main() { - println!("libusb version: {:?}", rusb::version()); - println!("Kernel supports detaching driver: {}", rusb::supports_detach_kernel_driver()); - - let ctx = rusb::Context::new().expect("cannot connect to libusb"); - - let mut pp = Peripage::find(&ctx).unwrap(); - - let mut img = vec![0u8; 5 * 48 * 24]; - - img - .iter_mut() - .enumerate() - .filter(|(i, _)| i % 2 == 0 && (i / 48 / 4) % 2 == 0) - .for_each(|(_, b)| *b = 0xff); - - let header = &[ - 0x10, 0xff, 0xfe, 0x01, - 0x1b, 0x40, 0x00, 0x1b, - 0x4a, 0x60, - ]; - - let mut packet = Vec::new(); - packet.extend_from_slice(header); - - const HEIGHT: u16 = 24; - const BPC: usize = 48 * HEIGHT as usize; - img - .chunks(BPC) - .chain(std::iter::repeat_n(&[0u8; BPC] as &[u8], 3)) - .for_each(|chunk| { - packet.extend_from_slice(&[ - 0x1d, 0x76, 0x30, 0x00, 0x30, 0x00, - ]); - packet.extend_from_slice(&HEIGHT.to_le_bytes()); - packet.extend_from_slice(chunk); - if chunk.len() < BPC { - let z = repeat_n(0u8, BPC - chunk.len()); - packet.extend(z); - } - }); - - // send image - pp.write(&packet, 30).unwrap(); - pp.confirm().unwrap(); -} blob - /dev/null blob + 5d790bda37f1ed9cc3af93bee47697f1639fb55c (mode 644) --- /dev/null +++ src/doc.rs @@ -0,0 +1,52 @@ +use std::borrow::Cow; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DocumentError { + #[error("document has an invalid width")] + Width, + + #[error("expected a length of {0}, got {1}")] + Len(usize, usize), +} + +/// A document, to be printed. +pub struct Document<'a> { + pixels: Cow<'a, [u8]>, +} + +impl<'a> Document<'a> { + /// The maximum width a document can have. (384px = 48mm) + pub const WIDTH: usize = 384; + + /// Create a new document. + pub fn new(pixels: impl Into>) -> Result { + Self::do_new(pixels.into()) + } + + fn do_new(pixels: Cow<'a, [u8]>) -> Result { + let height = pixels.len() / Self::WIDTH; + let expected = Self::WIDTH * height; + if expected != pixels.len() { + return Err(DocumentError::Len(expected, pixels.len())); + } + + Ok(Self { + pixels, + }) + } + + pub fn width(&self) -> usize { + Self::WIDTH + } + + pub fn height(&self) -> usize { + self.pixels.len() / Self::WIDTH + } + + pub fn pixels(&self) -> &[u8] { + &self.pixels + } +} + blob - /dev/null blob + bfe70c5bf5a707979718d3e24d8025d200b688a0 (mode 644) --- /dev/null +++ src/lib.rs @@ -0,0 +1,185 @@ +// Very helpful doc for USB: https://www.beyondlogic.org/usbnutshell/usb1.shtml +use std::{iter::repeat_n, time::Duration}; + +use rusb::{Context, DeviceHandle, Direction, TransferType, UsbContext}; +use thiserror::Error; + +pub use crate::doc::{Document, DocumentError}; +pub use rusb as usb; + +/// USB vendor ID of the PeriPage A6. +pub const VENDOR_ID: u16 = 0x09c5; + +/// USB product ID of the PeriPage A6. +pub const PRODUCT_ID: u16 = 0x0200; + +#[derive(Debug, Error)] +pub enum Error { + #[error("USB problem")] + Usb(#[from] rusb::Error), + + #[error("failed to claim the USB device")] + Claim(#[source] rusb::Error), + + #[error("no PeriPage A6 found")] + NoPrinter, +} + +pub type Result = core::result::Result; + +mod doc; + +pub struct Printer { + handle: DeviceHandle, + epin: u8, + epout: u8, +} + +impl Printer { + pub fn find(ctx: &Context) -> Result { + let dev = ctx + .devices()? + .iter() + .find(|dev| { + let Ok(desc) = dev.device_descriptor() else { + log::warn!("cannot get device descriptor for Bus {dev:?}"); + return false + }; + + desc.vendor_id() == VENDOR_ID && desc.product_id() == PRODUCT_ID + }) + .ok_or(Error::NoPrinter)?; + + Self::open(dev.open()?) + } + pub fn open(handle: DeviceHandle) -> Result { + let dev = handle.device(); + + // automatically steal the USB device from the kernel + let _ = handle.set_auto_detach_kernel_driver(true); + + let dd = dev.device_descriptor()?; + log::trace!("USB device descriptor = {dd:#?}"); + if let Ok(s) = handle.read_manufacturer_string_ascii(&dd) { + log::debug!("USB Vendor: {s}"); + } + if let Ok(s) = handle.read_product_string_ascii(&dd) { + log::debug!("USB Product: {s}"); + } + if let Ok(s) = handle.read_serial_number_string_ascii(&dd) { + log::debug!("USB Serial: {s}"); + } + + // PeriPage A6 has only one config. + debug_assert_eq!(dd.num_configurations(), 1); + + let cd = dev.config_descriptor(0)?; + log::trace!("USB configuration descriptor 0: {cd:#?}"); + + // PeriPage A6 has only one interface. + debug_assert_eq!(cd.num_interfaces(), 1); + + let int = cd.interfaces().next().unwrap(); + let id = int.descriptors().next().unwrap(); + log::trace!("USB interface descriptor 0 for configuration 0: {id:#?}"); + if let Some(sid) = id.description_string_index() { + log::trace!("Interface: {}", handle.read_string_descriptor_ascii(sid)?); + } + + log::debug!("Is kernel driver active: {:?}", handle.kernel_driver_active(0)); + + debug_assert_eq!(id.class_code(), 7); // Printer + debug_assert_eq!(id.sub_class_code(), 1); // Printer + debug_assert_eq!(id.protocol_code(), 2); // Bi-directional + assert_eq!(id.num_endpoints(), 2); + + let mut endps = id.endpoint_descriptors(); + let epd0 = endps.next().unwrap(); + let epd1 = endps.next().unwrap(); + debug_assert!(endps.next().is_none()); + + log::trace!("USB endpoint descriptor 0: {epd0:#?}"); + log::trace!("USB endpoint descriptor 1: {epd1:#?}"); + + debug_assert_eq!(epd0.address(), 129); // IN (128) + 1 + assert_eq!(epd0.direction(), Direction::In); + assert_eq!(epd0.transfer_type(), TransferType::Bulk); + + debug_assert_eq!(epd1.address(), 2); // OUT (0) + 2 + assert_eq!(epd1.direction(), Direction::Out); + assert_eq!(epd1.transfer_type(), TransferType::Bulk); + + Ok(Self { + handle, + epin: epd0.address(), + epout: epd1.address(), + }) + } + + /// Run action `f` while the interface is claimed. + fn run(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { + self.handle.claim_interface(0) + .map_err(|e| Error::Claim(e))?; + let x = f(self); + if let Err(e) = self.handle.release_interface(0) { + log::error!("failed to unclaim device: {e}"); + } + x + } + + /// Write data to the USB device. + /// NOTE: This function must be run inside of `Self::run()` + fn write(&mut self, buf: &[u8], timeout: u64) -> Result<()> { + self.handle.write_bulk(self.epout, buf, Duration::from_secs(timeout))?; + Ok(()) + } + + pub fn print(&mut self, doc: &Document, extra: bool) -> Result<()> { + let mut packet = vec![ + 0x10, 0xff, 0xfe, 0x01, + 0x1b, 0x40, 0x00, 0x1b, + 0x4a, 0x60, + ]; + + let chunk_width = doc.width() / 8; + let chunk_height = 24; // This number was derived from USB traffic. + let chunk_size = chunk_width * chunk_height; + + // TODO: allow pages smaller than 384px + assert_eq!(chunk_width, 48); + + let page_header = &[ + 0x1d, 0x76, 0x30, 0x00, 0x30, 0x00, + ]; + + // Group the pixels into pages, because that's how the Windows driver does it. + doc + .pixels() + .chunks(chunk_size) + .for_each(|chunk| { + packet.extend_from_slice(page_header); + packet.extend_from_slice(&u16::to_le_bytes(chunk_height as u16)); + packet.extend_from_slice(chunk); + if chunk.len() < chunk_size { + packet.extend(repeat_n(0u8, chunk_size - chunk.len())); + } + }); + + if extra { + let height = 3 * 24; + packet.extend_from_slice(page_header); + packet.extend_from_slice(&u16::to_le_bytes(height)); + packet.extend(repeat_n(0u8, 48 * height as usize)); + } + + self.run(|s| { + s.write(&packet, 30)?; + s.write(&[0x10, 0xff, 0xfe, 0x45], 1)?; + Ok(()) + }) + } +} + +pub fn usb_context() -> usb::Result { + Context::new() +}