Commit Diff


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<Context>,
-	ep: u8,
-}
-
-impl Peripage {
-	fn find(ctx: &Context) -> Result<Self> {
-		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<Context>) -> Result<Self> {
-		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<Cow<'a, [u8]>>) -> Result<Self, DocumentError> {
+		Self::do_new(pixels.into())
+	}
+
+	fn do_new(pixels: Cow<'a, [u8]>) -> Result<Self, DocumentError> {
+		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<T> = core::result::Result<T, Error>;
+
+mod doc;
+
+pub struct Printer {
+	handle: DeviceHandle<Context>,
+	epin: u8,
+	epout: u8,
+}
+
+impl Printer {
+	pub fn find(ctx: &Context) -> Result<Self> {
+		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<Context>) -> Result<Self> {
+		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<T>(&mut self, f: impl FnOnce(&mut Self) -> Result<T>) -> Result<T> {
+		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<usb::Context> {
+	Context::new()
+}