Commit Diff


commit - c18821fb9c94f5f8b1f8b6dcd662e9310e5f1581
commit + 97903ae570f76955be9295458ee062833c6f8446
blob - d6474d46a2d7b21583540ee59081a15a83b78ec5
blob + a424f0b334946592fb8742b9533967552a94fbea
--- Cargo.lock
+++ Cargo.lock
@@ -849,6 +849,7 @@ dependencies = [
 name = "ppa6"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "log",
  "rusb",
  "thiserror 2.0.11",
blob - a7c7c73f294f52f105376f317a4115c71f303a46
blob + ee925076852f7ed1364da0cf6fa6b0747f279eb1
--- ppa6/Cargo.toml
+++ ppa6/Cargo.toml
@@ -3,7 +3,12 @@ name = "ppa6"
 version = "0.1.0"
 edition = "2021"
 
+[features]
+default = ["usb"]
+usb = ["dep:rusb"]
+
 [dependencies]
+anyhow = "1.0.95"
 log = "0.4.25"
-rusb = "0.9.4"
+rusb = { version = "0.9.4", optional = true }
 thiserror = "2.0.11"
blob - 63bb9b72b7fe331fb3971f331811ff2d9deab75d
blob + d82cf9e595e7592dc054330bee4042b4172ad4ba
--- ppa6/examples/black.rs
+++ ppa6/examples/black.rs
@@ -1,10 +1,11 @@
-use ppa6::{Document, Printer};
+use ppa6::Printer;
 
 fn main() {
-	let ctx = ppa6::usb_context().unwrap();
-	let mut printer = Printer::find(&ctx).unwrap();
+	let mut printer = Printer::find().expect("no printer found");
+	printer.reset().expect("failed to reset printer");
+	let pixels = vec![0xffu8; 384 * 384 / 8];
+	printer
+		.print_image_chunked(&pixels, 384)
+		.expect("failed to print black image");
 
-	let pixels = vec![0xffu8; 48 * 384];
-	let doc = Document::new(pixels).unwrap();
-	printer.print(&doc, true).unwrap();
 }
blob - /dev/null
blob + f9a8e93c2bb0b739ee88010c36279e5ab98af1cc (mode 644)
--- /dev/null
+++ ppa6/examples/info.rs
@@ -0,0 +1,13 @@
+use ppa6::Printer;
+
+fn main() {
+	let mut printer = Printer::find().unwrap();
+	printer.reset().expect("cannot reset printer");
+	println!("IP:            {}", printer.get_ip().unwrap());
+	println!("Name:          {}", printer.get_name().unwrap());
+	println!("Serial:        {}", printer.get_serial().unwrap());
+	println!("Firmware Ver.: {}", printer.get_firmware_ver().unwrap());
+	println!("Hardware Ver.: {}", printer.get_hardware_ver().unwrap());
+	println!("Battery Level: {}", printer.get_battery().unwrap());
+	println!("MAC address:   {}", printer.get_mac().unwrap());
+}
blob - /dev/null
blob + b7f7b64a0c2ad0903119e9328b8dd07e513404fe (mode 644)
--- /dev/null
+++ ppa6/examples/text.rs
@@ -0,0 +1,10 @@
+use ppa6::Printer;
+
+fn main() {
+	let mut printer = Printer::find().expect("no printer found");
+	printer.reset().expect("cannot reset printer");
+	printer
+		.print_text("Hello \tWorld\nThis is a sample page of text\n\nThe printer also handles newlines, but wrapping seems to be partially broken for some reason.")
+		.expect("cannot print text");
+	printer.push(0x40).expect("cannot push printer paper");
+}
blob - 8b998325e7819c0cfff34b75a979229be0fd2796 (mode 644)
blob + /dev/null
--- ppa6/src/doc.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-use std::{borrow::Cow, fmt::{self, Formatter, Debug}};
-
-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() * 8 / Self::WIDTH;
-		let expected = Self::WIDTH * height / 8;
-		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 / 8)
-	}
-
-	pub fn pixels(&self) -> &[u8] {
-		&self.pixels
-	}
-
-	pub fn get(&self, x: usize, y: usize) -> Option<bool> {
-		if x >= self.width() || y >= self.height() {
-			return None;
-		}
-
-		let b = self.pixels[(y * self.width() + x) / 8];
-		Some(b & (128 >> (x % 8)) != 0)
-	}
-}
-
-
-impl Debug for Document<'_> {
-	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-		f
-			.debug_struct("Document")
-			.field("width", &self.width())
-			.field("height", &self.height())
-			.finish()
-	}
-}
blob - 78819601014376f14aa6ead78b24ae6b80691e07
blob + f9a1acbf5ed1d96eb7716f2f9e6772ca3129a998
--- ppa6/src/lib.rs
+++ ppa6/src/lib.rs
@@ -1,292 +1,276 @@
-// Very helpful doc for USB: https://www.beyondlogic.org/usbnutshell/usb1.shtml
-use std::{iter::repeat_n, time::Duration};
+use std::{fmt::{self, Debug, Display, Formatter}, time::Duration};
 
-use rusb::{Direction, TransferType, UsbContext};
-use thiserror::Error;
+use anyhow::{bail, Context, Result};
 
-pub use crate::doc::{Document, DocumentError};
-pub use rusb as usb;
-pub use crate::usb::Context;
+macro_rules! backends {
+	[$($(# [$($m:tt)*])? $mod:ident :: $name:ident),* $(,)?] => {
+		$(
+			$(# [$($m)*])*
+			mod $mod;
+			$(# [$($m)*])*
+			pub use crate::$mod::$name;
+		)*
+	};
+}
 
-/// USB vendor ID of the PeriPage A6.
-pub const VENDOR_ID: u16 = 0x09c5;
+backends! [
+	#[cfg(feature = "usb")]
+	usb::UsbBackend,
+];
 
-/// 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),
+/// Printing backend.
+pub trait Backend {
+	/// Send data to the printer.
+	/// TODO: return number of bytes sent
+	fn send(&mut self, buf: &[u8], timeout: Duration) -> Result<()>;
 
-	#[error("failed to claim the USB device")]
-	Claim(#[source] rusb::Error),
-	
-	#[error("no PeriPage A6 found")]
-	NoPrinter,
+	/// Receive at most `buf.len()` bytes of data from the printer.
+	///
+	/// # Return value
+	/// This functions the number of bytes received from the printer.
+	fn recv(&mut self, buf: &mut [u8], timeout: Duration) -> Result<usize>;
 }
 
-pub type Device = rusb::Device<Context>;
-pub type DeviceHandle = rusb::DeviceHandle<Context>;
-pub type Result<T> = core::result::Result<T, Error>;
+/// MAC Address, see [`Printer::get_mac()`].
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct MacAddr(pub [u8; 6]);
 
-mod doc;
-
+/// PeriPage A6 printer.
 pub struct Printer {
-	handle: DeviceHandle,
-	epin: u8,
-	epout: u8,
+	backend: Box<dyn Backend>,
 }
 
 impl Printer {
-	/// List all available printers.
-	pub fn list(ctx: &Context) -> Result<Vec<Device>> {
-		let devs = ctx
-			.devices()?
-			.iter()
-			.filter(|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
-			})
-			.collect();
-		Ok(devs)
-	}
-	
-	/// Find the first printer and open it.
-	pub fn find(ctx: &Context) -> Result<Self> {
-		match Self::list(ctx)?.first() {
-			Some(dev) => Self::open(dev.open()?),
-			None => Err(Error::NoPrinter),
+	/// Construct a new printer using `backend` as it's printing [`Backend`].
+	pub fn new(backend: impl Backend + 'static) -> Self {
+		Self {
+			backend: Box::new(backend),
 		}
 	}
-	/// Open a specific printer.
-	pub fn open(handle: DeviceHandle) -> 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::debug!("USB device descriptor = {dd:#?}");
-		if let Ok(s) = handle.read_manufacturer_string_ascii(&dd) {
-			log::info!("USB Vendor: {s}");
+	/// Find any printer, connected using any backend.
+	pub fn find() -> Result<Self> {
+		#[cfg(feature = "usb")] {
+			match crate::usb::UsbBackend::list() {
+				Ok(devs) => {
+					if let Some(dev) = devs.first() {
+						let backend = UsbBackend::open(dev)?;
+						return Ok(Self::new(backend));
+					}
+				},
+				Err(e) => log::error!("cannot get list of usb devices: {e}"),
+			}
 		}
-		if let Ok(s) = handle.read_product_string_ascii(&dd) {
-			log::info!("USB Product: {s}");
-		}
-		if let Ok(s) = handle.read_serial_number_string_ascii(&dd) {
-			log::info!("USB Serial: {s}");
-		}
-
-		// PeriPage A6 has only one config.
-		debug_assert_eq!(dd.num_configurations(), 1);
-
-		let cd = dev.config_descriptor(0)?;
-		log::debug!("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::debug!("USB interface descriptor 0 for configuration 0: {id:#?}");
-		if let Some(sid) = id.description_string_index() {
-			log::debug!("Interface: {}", handle.read_string_descriptor_ascii(sid)?);
-		}
-
-		log::info!("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::debug!("USB endpoint descriptor 0: {epd0:#?}");
-		log::debug!("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(),
-		})
+		
+		bail!("no printer found");
 	}
 
-	/// 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
+	fn send(&mut self, buf: &[u8], timeout: u64) -> Result<()> {
+		log::trace!("send({}{buf:x?}, {timeout}s);", buf.len());
+		self.backend.send(buf, Duration::from_secs(timeout))
 	}
-
-	/// 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<()> {
-		log::trace!("write({buf:x?}, {timeout});");
-		self.handle.write_bulk(self.epout, buf, Duration::from_secs(timeout))?;
-		Ok(())
-	}
-
-	fn read(&mut self, buf: &mut [u8], timeout: u64) -> Result<usize> {
-		let n = self.handle.read_bulk(self.epin, buf, Duration::from_secs(timeout))?;
+	fn recv(&mut self, buf: &mut [u8], timeout: u64) -> Result<usize> {
+		let n = self.backend.recv(buf, Duration::from_secs(timeout))?;
+		log::trace!("recv({}, {timeout}s): {n}{:x?}", buf.len(), &buf[0..n]);
 		Ok(n)
 	}
-
-	fn query(&mut self, opcode: u32) -> Result<Vec<u8>> {
-		self.run(|s| {
-			let opcode = u32::to_be_bytes(opcode);
-			s.write(&opcode, 1).unwrap();
-			let mut buf = vec![0u8; 64];
-			let n = s.read(&mut buf, 3).unwrap();
-			buf.truncate(n);
-			Ok(buf)
-		})
+	fn query(&mut self, cmd: &[u8]) -> Result<Vec<u8>> {
+		self.send(cmd, 3).context("failed to send request")?;
+		let mut buf = vec![0u8; 1024];
+		let n = self.recv(&mut buf, 3).context("failed receive response")?;
+		buf.truncate(n);
+		Ok(buf)
 	}
-
-	fn query_str(&mut self, opcode: u32) -> Result<String> {
-		let x = self.query(opcode)?;
-		let s = String::from_utf8_lossy(&x).into_owned();
-		Ok(s)
+	fn query_string(&mut self, cmd: &[u8]) -> Result<String> {
+		let buf = self.query(cmd)?;
+		let s = String::from_utf8_lossy(&buf);
+		Ok(s.into_owned())
 	}
 
+	/// Get printer's "IP" string.
 	pub fn get_ip(&mut self) -> Result<String> {
-		self.query_str(0x10ff20f0)
+		self.query_string(&[0x10, 0xff, 0x20, 0xf0])
 	}
 
-	pub fn get_firmware(&mut self) -> Result<String> {
-		self.query_str(0x10ff20f1)
+	/// Get printer's firmware version.
+	pub fn get_firmware_ver(&mut self) -> Result<String> {
+		self.query_string(&[0x10, 0xff, 0x20, 0xf1])
 	}
 
+	/// Get printer's serial number.
 	pub fn get_serial(&mut self) -> Result<String> {
-		self.query_str(0x10ff20f2)
+		self.query_string(&[0x10, 0xff, 0x20, 0xf2])
 	}
 
-	pub fn get_hardware(&mut self) -> Result<String> {
-		self.query_str(0x10ff3010)
+	/// Get printer's hardware version.
+	pub fn get_hardware_ver(&mut self) -> Result<String> {
+		self.query_string(&[0x10, 0xff, 0x30, 0x10])
 	}
 
+	/// Get printer's name.
 	pub fn get_name(&mut self) -> Result<String> {
-		self.query_str(0x10ff3011)
+		self.query_string(&[0x10, 0xff, 0x30, 0x11])
 	}
-
-	pub fn get_mac(&mut self) -> Result<[u8; 6]> {
-		let mac = self.query(0x10ff3012)?;
-		let mut buf = [0u8; 6];
-		buf.copy_from_slice(&mac[0..6]);
-		Ok(buf)
+	
+	/// Get printer's MAC address.
+	/// TODO: Return a MacAddr struct i
+	pub fn get_mac(&mut self) -> Result<MacAddr> {
+		let buf = self.query(&[0x10, 0xff, 0x30, 0x12])?;
+		// for some reason the printer sends the MAC address twice
+		if buf.len() < 6 {
+			bail!("invalid MAC address response, got {} bytes: {:x?}", buf.len(), &buf);
+		}
+		let mut mac = [0u8; 6];
+		mac.copy_from_slice(&buf[0..6]);
+		Ok(MacAddr(mac))
 	}
 
+	/// Get printer's battery state.
 	pub fn get_battery(&mut self) -> Result<u8> {
-		let x = self.query(0x10ff50f1)?;
-		assert_eq!(x.len(), 2);
-		Ok(x[1])
+		let buf = self.query(&[0x10, 0xff, 0x50, 0xf1])?;
+		if buf.len() != 2 {
+			bail!("invalid battery response");
+		}
+		Ok(buf[1])
 	}
 
+	/// Set printing concentration, valid values are between `0..=2`.
 	pub fn set_concentration(&mut self, c: u8) -> Result<()> {
-		let c = c.min(2);
-		self.run(|s| {
-			let buf = [0x10, 0xff, 0x10, 0x00, c];
-			s.write(&buf, 1)?;
-			Ok(())
-		})
+		if c > 2 {
+			bail!("invalid concentration: {c}");
+		}
+
+		self.send(&[0x10, 0xff, 0x10, 0x00, c], 1)
 	}
 
-	pub fn print(&mut self, doc: &Document, extra: bool) -> Result<()> {
-		let mut packet = vec![
-			0x10, 0xff, 0xfe, 0x01, // reset
-			0x1b, 0x40, 0x00, // no idea
-			0x1b, 0x4a, 0x60, // print break of len 0x60
+	/// Reset the printer.
+	/// This command has to be sent, before printing can be done.
+	pub fn reset(&mut self) -> Result<()> {
+		let buf = [
+			0x10, 0xff, 0xfe, 0x01,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
 		];
+		self.send(&buf, 3)?;
+		let mut buf = [0u8; 128];
+		let _ = self.recv(&mut buf, 1);
+		Ok(())
+	}
 
-		let chunk_width = doc.width() / 8;
-		let chunk_height  = 24; // This number was derived from USB traffic.
-		let chunk_size = chunk_width * chunk_height;
+	/// Print ASCII text.
+	/// Please don't use this, better use a font rasterizer, like [cosmic-text](https://docs.rs/cosmic-text).
+	///
+	/// # Printer Bugs (PeriPage A6)
+	/// - Only ASCII, no Unicode
+	/// - No ASCII escape sequences, except '\n' (line feed)
+	/// - Line wrapping is very buggy, sometimes it works, sometimes it discards the rest of the line.
+	/// - No font size/weight settings
+	pub fn print_text(&mut self, text: &str) -> Result<()> {
+		let text: Vec<u8> = text
+			.chars()
+			.filter(|ch| matches!(ch, '\n' | '\x20'..='\x7f'))
+			.map(|ch| ch as u8)
+			.collect();
 
-		// TODO: allow pages smaller than 384px
-		assert_eq!(chunk_width, 48);
+		self.send(&text, 30)?;
+		Ok(())
+	}
 
-		let page_header = &[
-			0x1d, 0x76, 0x30, 0x00, // print command
-			0x30, 0x00, // big endian 0x0030 row size
-		];
+	/// Print raw pixels.
+	///
+	/// # Overheating
+	/// The printer can overheat, if too much black is being printed at once,
+	/// therefore it's better to use the [`Printer::print_image_chunked()`] function instead.
+	///
+	/// # Printing Limitations
+	/// While the printer has a density of 203dpi,
+	/// printing very small things and thin lines should be avoided,
+	/// as the printer is simply not precise enough.
+	///
+	/// # "Concentration"
+	/// The printing concentration can be adjusted with [`Printer::set_concentration()`],
+	/// to make the output brighter or darker.
+	///
+	/// # Format
+	/// TODO: describe pixel format:
+	/// - monochrome
+	/// - 0=white, 1=black
+	/// - MSB: left, LSB: right
+	/// - must be multiples of `width/8` bytes
+	/// - must not be longer than `65535` rows
+	/// - due to accuracy constraints, printing single pixels should be avoided
+	///
+	/// # Notes
+	/// Printing gray scale pictures is possible,
+	/// by using [dithering](https://en.wikipedia.org/wiki/Dithering) to convert them to monochrome first.
+	/// Similarly, color images must be first converted to gray scale.
+	/// The [image](https://docs.rs/image/latest/image/) crate can be used, to do the conversions.
+	pub fn print_image(&mut self, pixels: &[u8], width: u16) -> Result<()> {
+		if width == 0 || width % 8 != 0 {
+			bail!("width must be non-zero and divisible by 8");
+		}
+		
+		let n = pixels.len() * 8;
+		let w = width as usize;
+		let h = n / w;
 
-		// 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 h > 0xff {
+			bail!("document too long");
+		}
 
-		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));
+		if pixels.len() != (w * h / 8) {
+			bail!("invalid length of pixels: {}", pixels.len());
 		}
-		
-		self.run(|s| {
-			s.write(&packet, 30)?;
-			s.write(&[0x10, 0xff, 0xfe, 0x45], 1)?; // no idea
-			Ok(())
-		})
-	}
 
-	fn reset(&mut self) -> Result<()> {
-		let buf = [
-			0x10, 0xff, 0xfe, 0x01,
-			0x00, 0x00, 0x00, 0x00,
-			0x00, 0x00, 0x00, 0x00,
-			0x00, 0x00, 0x00, 0x00,
+		let rs = w / 8;
+
+		let mut packet = vec![
+			0x1d, 0x76, 0x30,
+			(rs >> 8) as u8, (rs & 0xff) as u8,
+			0x00, h as u8,
 		];
-		self.write(&buf, 1)?;
+		packet.extend_from_slice(pixels);
+		self.send(&packet, 60)?;
+
+		// no idea what this does, but the Windows driver sends this after every print.
+		self.send(&[0x10, 0xff, 0xfe, 0x45], 1)?;
 		Ok(())
 	}
 
-	pub fn print_ascii(&mut self, text: &str) -> Result<()> {
-		self.run(|s| {
-			s.reset()?;
-			s.write(text.as_bytes(), 30)?;
-			Ok(())
-		})
+	/// Just like [`Printer::print_image()`], but breaks the pixels into rows of `chunk_height`.
+	/// This may be needed, to prevent the printer from overheating, while printing a long document.
+	pub fn print_image_chunked_ext(&mut self, pixels: &[u8], width: u16, chunk_height: u16, delay: Duration) -> Result<()> {
+		pixels
+			.chunks(width as usize * chunk_height as usize / 8)
+			.try_for_each(|chunk| {
+				self.print_image(chunk, width)?;
+				std::thread::sleep(delay);
+				Ok(())
+			})
 	}
 
-	pub fn handle(&mut self) -> &DeviceHandle {
-		&mut self.handle
+	pub fn print_image_chunked(&mut self, pixels: &[u8], width: u16) -> Result<()> {
+		self.print_image_chunked_ext(pixels, width, 24, Duration::from_millis(50))
 	}
-	pub fn endpoint_in(&self) -> u8 {
-		self.epin
+
+	/// Push out `num` rows of paper.
+	pub fn push(&mut self, num: u8) -> Result<()> {
+		self.send(&[0x1b, 0x4a, num], 5)?;
+		Ok(())
 	}
-	pub fn endpoint_out(&self) -> u8 {
-		self.epout
+}
+
+impl Display for MacAddr {
+	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+		let [x0, x1, x2, x3, x4, x5] = self.0;
+		write!(f, "{x0:02x}:{x1:02x}:{x2:02x}:{x3:02x}:{x4:02x}:{x5:02x}")
 	}
 }
 
-pub fn usb_context() -> usb::Result<usb::Context> {
-	Context::new()
+impl Debug for MacAddr {
+	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+		<Self as Display>::fmt(self, f)
+	}
 }
blob - /dev/null
blob + fc76e80ddf6e5557f2271033d22234a00b830d14 (mode 644)
--- /dev/null
+++ ppa6/src/usb.rs
@@ -0,0 +1,126 @@
+use std::time::Duration;
+use anyhow::{Result, Context};
+use rusb::{Direction, GlobalContext, TransferType};
+
+const VENDOR_ID: u16 = 0x09c5;
+const PRODUCT_ID: u16 = 0x0200;
+
+use crate::Backend;
+
+pub type Device = rusb::Device<GlobalContext>;
+pub type DeviceHandle = rusb::DeviceHandle<GlobalContext>;
+
+/// A USB backend for [`Printer`](crate::Printer).
+pub struct UsbBackend {
+	handle: DeviceHandle,
+	epin: u8,
+	epout: u8,
+}
+
+impl UsbBackend {
+	/// Get a list of printer devices connected via usb.
+	pub fn list() -> rusb::Result<Vec<Device>> {
+		let devs = rusb::devices()?
+			.iter()
+			.filter(|dev| {
+				let Ok(desc) = dev.device_descriptor() else {
+					log::error!("cannot get device descriptor for device {dev:?}");
+					return false
+				};
+
+				desc.vendor_id() == VENDOR_ID && desc.product_id() == PRODUCT_ID
+			})
+			.collect();
+		Ok(devs)
+	}
+
+	/// Open a USB printing device.
+	pub fn open(dev: &Device) -> Result<Self> {
+		let handle = dev
+			.open()
+			.context("cannot open usb device")?;
+
+		// automatically steal the USB device from the kernel
+		let _ = handle.set_auto_detach_kernel_driver(true);
+
+		let dd = dev
+			.device_descriptor()
+			.context("cannot get usb device descriptor")?;
+
+		log::debug!("USB device descriptor = {dd:#?}");
+		if let Ok(s) = handle.read_manufacturer_string_ascii(&dd) {
+			log::info!("USB Vendor: {s}");
+		}
+		if let Ok(s) = handle.read_product_string_ascii(&dd) {
+			log::info!("USB Product: {s}");
+		}
+		if let Ok(s) = handle.read_serial_number_string_ascii(&dd) {
+			log::info!("USB Serial: {s}");
+		}
+
+		// PeriPage A6 has only one config.
+		debug_assert_eq!(dd.num_configurations(), 1);
+
+		let cd = dev
+			.config_descriptor(0)
+			.context("cannot get usb config descriptor")?;
+		log::debug!("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::debug!("USB interface descriptor 0 for configuration 0: {id:#?}");
+		if let Some(sid) = id.description_string_index() {
+			log::debug!("Interface: {}", handle.read_string_descriptor_ascii(sid)?);
+		}
+
+		log::info!("Is usb 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::debug!("USB endpoint descriptor 0: {epd0:#?}");
+		log::debug!("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);
+
+		let epin = epd0.address();
+		let epout = epd1.address();
+
+		handle.claim_interface(0).context("cannot claim usb interface 0")?;
+
+		Ok(Self {
+			handle,
+			epin,
+			epout,
+		})
+	}
+}
+
+impl Backend for UsbBackend {
+	fn send(&mut self, buf: &[u8], timeout: Duration) -> anyhow::Result<()> {
+		self.handle.write_bulk(self.epout, buf, timeout)?;
+		Ok(())
+	}
+
+	fn recv(&mut self, buf: &mut [u8], timeout: Duration) -> anyhow::Result<usize> {
+		let n = self.handle.read_bulk(self.epin, buf, timeout)?;
+		Ok(n)
+	}
+}
+