commit - f11bef8348cab155964838f1cf3c000e0721c7c5
commit + 3414c8ddec42ad54a539807732a3c66cb96038db
blob - 2e341f9de998c7131836438f524b1cc63195adbd
blob + 33bfae3e36ed9789ed20f831b287589fecbd877d
--- Cargo.lock
+++ Cargo.lock
]
[[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"
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"
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
[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
-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
+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
-// 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
+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
+// 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()
+}