use futures_lite::future::block_on;
use nusb::transfer::{Direction, EndpointType, RequestBuffer};
use crate::frame::{DEFAULT_MAX_FRAME_SIZE, expected_frame_count, read_frames};
use crate::{PrintError, Printer, PrinterConfig, StatusQuery};
const ZEBRA_VENDOR_ID: u16 = 0x0A5F;
const USB_CLASS_PRINTER: u8 = 7;
pub struct UsbPrinter {
interface: nusb::Interface,
ep_out: u8,
ep_in: Option<u8>,
config: PrinterConfig,
}
impl UsbPrinter {
pub fn find_zebra(config: PrinterConfig) -> Result<Self, PrintError> {
let devices = nusb::list_devices().map_err(|e| PrintError::UsbError(e.to_string()))?;
for dev_info in devices {
if dev_info.vendor_id() != ZEBRA_VENDOR_ID {
continue;
}
let iface_number = dev_info
.interfaces()
.find(|iface| iface.class() == USB_CLASS_PRINTER)
.map(|iface| iface.interface_number());
if let Some(iface_number) = iface_number {
return Self::open_device(&dev_info, iface_number, config);
}
}
Err(PrintError::UsbDeviceNotFound)
}
pub fn find(
vendor_id: u16,
product_id: u16,
config: PrinterConfig,
) -> Result<Self, PrintError> {
let devices = nusb::list_devices().map_err(|e| PrintError::UsbError(e.to_string()))?;
for dev_info in devices {
if dev_info.vendor_id() == vendor_id && dev_info.product_id() == product_id {
let iface_number = dev_info
.interfaces()
.find(|iface| iface.class() == USB_CLASS_PRINTER)
.map(|iface| iface.interface_number())
.ok_or_else(|| {
PrintError::UsbError(format!(
"device {:04X}:{:04X} has no printer-class interface",
vendor_id, product_id
))
})?;
return Self::open_device(&dev_info, iface_number, config);
}
}
Err(PrintError::UsbDeviceNotFound)
}
pub fn list_devices() -> Vec<(u16, u16, String)> {
let Ok(devices) = nusb::list_devices() else {
return Vec::new();
};
devices
.map(|dev| {
let desc = dev.product_string().unwrap_or_default().to_string();
let desc = if desc.is_empty() {
format!(
"{} (VID:{:04X} PID:{:04X})",
dev.manufacturer_string().unwrap_or_default(),
dev.vendor_id(),
dev.product_id()
)
} else {
desc
};
(dev.vendor_id(), dev.product_id(), desc)
})
.collect()
}
fn open_device(
dev_info: &nusb::DeviceInfo,
interface_number: u8,
config: PrinterConfig,
) -> Result<Self, PrintError> {
let device = dev_info
.open()
.map_err(|e| PrintError::UsbError(format!("failed to open device: {}", e)))?;
let (ep_out, ep_in) = Self::discover_endpoints(&device, interface_number)?;
let interface = device
.detach_and_claim_interface(interface_number)
.map_err(|e| {
PrintError::UsbError(format!(
"failed to claim interface {}: {}",
interface_number, e
))
})?;
Ok(Self {
interface,
ep_out,
ep_in,
config,
})
}
fn discover_endpoints(
device: &nusb::Device,
interface_number: u8,
) -> Result<(u8, Option<u8>), PrintError> {
let config = device.active_configuration().map_err(|e| {
PrintError::UsbError(format!("failed to read active configuration: {}", e))
})?;
let mut ep_out: Option<u8> = None;
let mut ep_in: Option<u8> = None;
for alt_setting in config.interface_alt_settings() {
if alt_setting.interface_number() != interface_number {
continue;
}
if alt_setting.alternate_setting() != 0 {
continue;
}
for ep in alt_setting.endpoints() {
if ep.transfer_type() != EndpointType::Bulk {
continue;
}
match ep.direction() {
Direction::Out => {
if ep_out.is_none() {
ep_out = Some(ep.address());
}
}
Direction::In => {
if ep_in.is_none() {
ep_in = Some(ep.address());
}
}
}
}
break;
}
let ep_out = ep_out.ok_or_else(|| {
PrintError::UsbError("no bulk OUT endpoint found on printer interface".into())
})?;
Ok((ep_out, ep_in))
}
fn bulk_write(&self, data: &[u8]) -> Result<(), PrintError> {
let future = self.interface.bulk_out(self.ep_out, data.to_vec());
let completion = block_on(future);
completion.status.map_err(|e| {
PrintError::WriteFailed(std::io::Error::other(format!("USB bulk OUT: {}", e)))
})
}
}
impl Printer for UsbPrinter {
fn send_raw(&mut self, data: &[u8]) -> Result<(), PrintError> {
self.bulk_write(data)
}
}
impl StatusQuery for UsbPrinter {
fn query_raw(&mut self, cmd: &[u8]) -> Result<Vec<Vec<u8>>, PrintError> {
let Some(ep_in) = self.ep_in else {
return Err(PrintError::UsbError(
"no bulk IN endpoint — printer does not support status queries".into(),
));
};
self.bulk_write(cmd)?;
let expected_frames = expected_frame_count(cmd);
let timeout = self.config.timeouts.read;
let mut reader = UsbBulkReader {
interface: &self.interface,
ep_in,
buffer: Vec::new(),
pos: 0,
};
read_frames(
&mut reader,
expected_frames,
timeout,
DEFAULT_MAX_FRAME_SIZE,
)
}
}
struct UsbBulkReader<'a> {
interface: &'a nusb::Interface,
ep_in: u8,
buffer: Vec<u8>,
pos: usize,
}
impl std::io::Read for UsbBulkReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.pos < self.buffer.len() {
let available = &self.buffer[self.pos..];
let to_copy = available.len().min(buf.len());
buf[..to_copy].copy_from_slice(&available[..to_copy]);
self.pos += to_copy;
return Ok(to_copy);
}
let request = RequestBuffer::new(512);
let future = self.interface.bulk_in(self.ep_in, request);
let completion = block_on(future);
match completion.status {
Ok(()) => {
let data = completion.data;
if data.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"zero-length USB transfer (retrying)",
));
}
let to_copy = data.len().min(buf.len());
buf[..to_copy].copy_from_slice(&data[..to_copy]);
if to_copy < data.len() {
self.buffer = data;
self.pos = to_copy;
} else {
self.buffer.clear();
self.pos = 0;
}
Ok(to_copy)
}
Err(e) => Err(std::io::Error::other(e)),
}
}
}