commit - 3414c8ddec42ad54a539807732a3c66cb96038db
commit + 67440028473df7a5c413d092925a60f26f9c8a11
blob - ef44ab21fd8f7d4ae212f791f16a02dfa1dec7e0
blob + eb5a316cbd195d26e3f768c7dd8e1b47299e17f8
--- .gitignore
+++ .gitignore
target
-ppa6
blob - 33bfae3e36ed9789ed20f831b287589fecbd877d
blob + 4f67efa70d5c2a0b13ef8b55fc718e3249bd44be
--- Cargo.lock
+++ Cargo.lock
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"
name = "ppa6"
version = "0.1.0"
dependencies = [
- "anyhow",
"log",
"rusb",
"thiserror",
blob - 839be0c8c81c73509f592bceb20d68a21cf2750c
blob + e8e4582da43e3141ed518cd4b6b108deccec1382
--- Cargo.toml
+++ Cargo.toml
-[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
-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
+[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
+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
+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 + 730cfc6ef5534dad927f734b1577536d353e57f3 (mode 644)
--- /dev/null
+++ ppa6/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 handle(&mut self) -> &DeviceHandle<Context> {
+ &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<usb::Context> {
+ Context::new()
+}
blob - 5d790bda37f1ed9cc3af93bee47697f1639fb55c (mode 644)
blob + /dev/null
--- src/doc.rs
+++ /dev/null
-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 - bfe70c5bf5a707979718d3e24d8025d200b688a0 (mode 644)
blob + /dev/null
--- src/lib.rs
+++ /dev/null
-// 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()
-}