commit 67440028473df7a5c413d092925a60f26f9c8a11 from: Benjamins Stürz date: Fri Feb 07 22:50:00 2025 UTC make workspace commit - 3414c8ddec42ad54a539807732a3c66cb96038db commit + 67440028473df7a5c413d092925a60f26f9c8a11 blob - ef44ab21fd8f7d4ae212f791f16a02dfa1dec7e0 blob + eb5a316cbd195d26e3f768c7dd8e1b47299e17f8 --- .gitignore +++ .gitignore @@ -1,2 +1 @@ target -ppa6 blob - 33bfae3e36ed9789ed20f831b287589fecbd877d blob + 4f67efa70d5c2a0b13ef8b55fc718e3249bd44be --- Cargo.lock +++ Cargo.lock @@ -3,12 +3,6 @@ version = 4 [[package]] -name = "anyhow" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" - -[[package]] name = "cc" version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -51,7 +45,6 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c name = "ppa6" version = "0.1.0" dependencies = [ - "anyhow", "log", "rusb", "thiserror", blob - 839be0c8c81c73509f592bceb20d68a21cf2750c blob + e8e4582da43e3141ed518cd4b6b108deccec1382 --- Cargo.toml +++ Cargo.toml @@ -1,10 +1,6 @@ -[package] -name = "ppa6" -version = "0.1.0" -edition = "2021" +[workspace] +resolver = "2" +members = [ + "ppa6" +] -[dependencies] -anyhow = "1.0.95" -log = "0.4.25" -rusb = "0.9.4" -thiserror = "2.0.11" blob - 63bb9b72b7fe331fb3971f331811ff2d9deab75d (mode 644) blob + /dev/null --- examples/black.rs +++ /dev/null @@ -1,10 +0,0 @@ -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 - /dev/null blob + a7c7c73f294f52f105376f317a4115c71f303a46 (mode 644) --- /dev/null +++ ppa6/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ppa6" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "0.4.25" +rusb = "0.9.4" +thiserror = "2.0.11" blob - /dev/null blob + 63bb9b72b7fe331fb3971f331811ff2d9deab75d (mode 644) --- /dev/null +++ ppa6/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 - /dev/null blob + 5d790bda37f1ed9cc3af93bee47697f1639fb55c (mode 644) --- /dev/null +++ ppa6/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 + 730cfc6ef5534dad927f734b1577536d353e57f3 (mode 644) --- /dev/null +++ ppa6/src/lib.rs @@ -0,0 +1,195 @@ +// 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 handle(&mut self) -> &DeviceHandle { + &mut self.handle + } + pub fn endpoint_in(&self) -> u8 { + self.epin + } + pub fn endpoint_out(&self) -> u8 { + self.epout + } +} + +pub fn usb_context() -> usb::Result { + Context::new() +} blob - 5d790bda37f1ed9cc3af93bee47697f1639fb55c (mode 644) blob + /dev/null --- src/doc.rs +++ /dev/null @@ -1,52 +0,0 @@ -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 - bfe70c5bf5a707979718d3e24d8025d200b688a0 (mode 644) blob + /dev/null --- src/lib.rs +++ /dev/null @@ -1,185 +0,0 @@ -// 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() -}