use crate::dithering::{atkinson_dither, bayer_dither, halftone_dither, ImageDithering};
use crate::printer::PrinterStatus;
use crate::protocol::{build_control_packet, chunk_data, pack_1bpp_pixels, parse_notification};
use async_trait::async_trait;
use btleplug::api::{
Central as _, Characteristic, Manager as _, Peripheral as _, ScanFilter, WriteType,
};
use btleplug::platform::{Manager, Peripheral};
use futures::stream::StreamExt;
use std::time::Duration;
use tokio::time;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct DeviceInfo {
pub id: String,
pub name: Option<String>,
}
#[derive(Debug)]
struct BtleCharacteristics {
control: Characteristic, notify: Characteristic, data: Characteristic, }
#[async_trait]
pub trait TransportAsync: Send + Sync {
async fn write_control(&self, data: &[u8]) -> Result<(), String>;
async fn write_data(&self, data: &[u8]) -> Result<(), String>;
async fn read_notification(&self, timeout: Duration) -> Result<Vec<u8>, String>;
}
pub async fn scan(timeout: Duration) -> Result<Vec<DeviceInfo>, String> {
let manager = Manager::new()
.await
.map_err(|e| format!("manager error: {:?}", e))?;
let adapters = manager
.adapters()
.await
.map_err(|e| format!("adapter list error: {:?}", e))?;
let adapter = adapters
.into_iter()
.next()
.ok_or_else(|| "no BLE adapters found".to_string())?;
adapter
.start_scan(ScanFilter::default())
.await
.map_err(|e| format!("scan start error: {:?}", e))?;
time::sleep(timeout).await;
let peripherals = adapter
.peripherals()
.await
.map_err(|e| format!("peripherals error: {:?}", e))?;
let mut list = vec![];
for p in peripherals {
let id = p.id().to_string();
let name = p
.properties()
.await
.ok()
.flatten()
.and_then(|props| props.local_name);
list.push(DeviceInfo { id, name });
}
Ok(list)
}
pub async fn connect(device_id: &str, _timeout: Duration) -> Result<CatPrinterAsync, String> {
let manager = Manager::new()
.await
.map_err(|e| format!("manager error: {:?}", e))?;
let adapters = manager
.adapters()
.await
.map_err(|e| format!("adapter list error: {:?}", e))?;
let adapter = adapters
.into_iter()
.next()
.ok_or_else(|| "no BLE adapters found".to_string())?;
let peripherals = adapter
.peripherals()
.await
.map_err(|e| format!("peripherals error: {:?}", e))?;
let maybe = peripherals
.into_iter()
.find(|p| p.id().to_string() == device_id);
let peripheral = maybe.ok_or_else(|| format!("device {} not found", device_id))?;
if !peripheral
.is_connected()
.await
.map_err(|e| format!("{:?}", e))?
{
peripheral
.connect()
.await
.map_err(|e| format!("connect error: {:?}", e))?;
}
peripheral
.discover_services()
.await
.map_err(|e| format!("discover error: {:?}", e))?;
let ae01 = Uuid::parse_str("0000ae01-0000-1000-8000-00805f9b34fb").unwrap();
let ae02 = Uuid::parse_str("0000ae02-0000-1000-8000-00805f9b34fb").unwrap();
let ae03 = Uuid::parse_str("0000ae03-0000-1000-8000-00805f9b34fb").unwrap();
let chars = peripheral.characteristics();
let mut control_c: Option<Characteristic> = None;
let mut notify_c: Option<Characteristic> = None;
let mut data_c: Option<Characteristic> = None;
for c in &chars {
if c.uuid == ae01 {
control_c = Some(c.clone());
}
if c.uuid == ae02 {
notify_c = Some(c.clone());
}
if c.uuid == ae03 {
data_c = Some(c.clone());
}
}
let control = control_c.ok_or_else(|| "AE01 control characteristic not found".to_string())?;
let notify = notify_c.ok_or_else(|| "AE02 notify characteristic not found".to_string())?;
let data = data_c.ok_or_else(|| "AE03 data characteristic not found".to_string())?;
peripheral
.subscribe(¬ify)
.await
.map_err(|e| format!("subscribe error: {:?}", e))?;
let transport = BtleTransport::new(
peripheral.clone(),
control.clone(),
notify.clone(),
data.clone(),
);
let cat = CatPrinterAsync::new(Box::new(transport));
Ok(cat)
}
pub struct BtleTransport {
peripheral: Peripheral,
control: Characteristic,
notify: Characteristic,
data: Characteristic,
}
impl BtleTransport {
pub fn new(
peripheral: Peripheral,
control: Characteristic,
notify: Characteristic,
data: Characteristic,
) -> Self {
Self {
peripheral,
control,
notify,
data,
}
}
}
#[async_trait]
impl TransportAsync for BtleTransport {
async fn write_control(&self, data: &[u8]) -> Result<(), String> {
self.peripheral
.write(&self.control, data, WriteType::WithoutResponse)
.await
.map_err(|e| format!("write_control error: {:?}", e))
}
async fn write_data(&self, data: &[u8]) -> Result<(), String> {
self.peripheral
.write(&self.data, data, WriteType::WithoutResponse)
.await
.map_err(|e| format!("write_data error: {:?}", e))
}
async fn read_notification(&self, timeout: Duration) -> Result<Vec<u8>, String> {
let mut notifications = self
.peripheral
.notifications()
.await
.map_err(|e| format!("notifications stream error: {:?}", e))?;
let deadline = time::Instant::now() + timeout;
loop {
let remaining = deadline
.checked_duration_since(time::Instant::now())
.unwrap_or_else(|| Duration::from_secs(0));
let maybe = time::timeout(remaining, notifications.next()).await;
match maybe {
Ok(Some(note)) => {
if note.uuid == self.notify.uuid {
return Ok(note.value.clone());
} else {
continue;
}
}
Ok(None) => return Err("notifications stream ended".to_string()),
Err(_) => return Err("timeout waiting for notification".to_string()),
}
}
}
}
pub struct CatPrinterAsync {
pub transport: Box<dyn TransportAsync + Send + Sync>,
chunk_size: usize,
}
impl CatPrinterAsync {
pub fn new(transport: Box<dyn TransportAsync + Send + Sync>) -> Self {
Self {
transport,
chunk_size: 180,
}
}
pub fn with_chunk_size(mut self, size: usize) -> Self {
self.chunk_size = size;
self
}
pub async fn get_status(&self, timeout: Duration) -> Result<PrinterStatus, String> {
let req = build_control_packet(0xA1, &[0x00]);
self.transport.write_control(&req).await?;
let raw = self.transport.read_notification(timeout).await?;
println!("DEBUG: Raw status notification bytes (0xA1): {:?}", raw);
let notif = parse_notification(&raw).map_err(|e| e.to_string())?;
Ok(crate::protocol::parse_printer_status(¬if.payload))
}
pub async fn get_battery(&self, timeout: Duration) -> Result<u8, String> {
let req = build_control_packet(0xAB, &[0x00]);
self.transport.write_control(&req).await?;
let raw = self.transport.read_notification(timeout).await?;
println!("DEBUG: Raw battery notification bytes (0xAB): {:?}", raw);
let notif = parse_notification(&raw).map_err(|e| e.to_string())?;
println!("DEBUG: Parsed battery payload: {:?}", notif.payload);
if !notif.payload.is_empty() {
Ok(notif.payload[0])
} else {
Err("Battery payload too short".to_string())
}
}
pub async fn print_text(&self, main: &str, author: &str) -> Result<(), String> {
let width = 384usize;
let pixels = crate::protocol::render_text_to_pixels(main, author, width);
let height = pixels.len() / width;
let rotated_pixels = crate::protocol::rotate_mirror_pixels(&pixels, width, height);
self.print_image(&rotated_pixels, width, height, 0x00, None)
.await
}
pub async fn print_image_from_path(
&self,
path: &str,
dithering: ImageDithering,
) -> Result<(), String> {
let img = image::open(path).map_err(|e| e.to_string())?;
let width = 384;
let resized = img.thumbnail(width, u32::MAX);
let mut gray = resized.to_luma8();
match dithering {
ImageDithering::FloydSteinberg => {
image::imageops::dither(&mut gray, &image::imageops::BiLevel);
}
ImageDithering::Atkinson => {
atkinson_dither(&mut gray);
}
ImageDithering::Bayer => {
bayer_dither(&mut gray);
}
ImageDithering::Halftone => {
gray = halftone_dither(&gray);
}
ImageDithering::Threshold => {
for pixel in gray.pixels_mut() {
if pixel[0] > 127 {
pixel[0] = 255;
} else {
pixel[0] = 0;
}
}
}
}
let (width, height) = gray.dimensions();
let pixels = gray.as_raw();
self.print_image(pixels, width as usize, height as usize, 0x00, None)
.await
}
pub async fn print_image(
&self,
pixels: &[u8],
width: usize,
height: usize,
mode: u8,
chunk_size: Option<usize>,
) -> Result<(), String> {
let packed = pack_1bpp_pixels(pixels, width, height).map_err(|e| e.to_string())?;
let line_count: u16 = height as u16;
let mut a9_payload = Vec::new();
a9_payload.extend_from_slice(&line_count.to_le_bytes());
a9_payload.push(0x30);
a9_payload.push(mode);
let a9 = build_control_packet(0xA9, &a9_payload);
self.transport.write_control(&a9).await?;
let resp = self
.transport
.read_notification(Duration::from_secs(2))
.await?;
let parsed = parse_notification(&resp).map_err(|e| e.to_string())?;
if parsed.command_id != 0xA9 || parsed.payload.first() == Some(&0x01u8) {
return Err("printer rejected print request".into());
}
let size = chunk_size.unwrap_or(self.chunk_size);
for chunk in chunk_data(&packed, size) {
self.transport.write_data(chunk).await?;
time::sleep(Duration::from_millis(10)).await;
}
let ad = build_control_packet(0xAD, &[0x00]);
self.transport.write_control(&ad).await?;
let deadline = time::Instant::now() + Duration::from_secs(60);
loop {
let remaining = deadline
.checked_duration_since(time::Instant::now())
.unwrap_or_else(|| Duration::from_secs(0));
if remaining.is_zero() {
return Err("timed out waiting for print complete".into());
}
let raw = self.transport.read_notification(remaining).await?;
let notif = parse_notification(&raw).map_err(|e| e.to_string())?;
if notif.command_id == 0xAA {
return Ok(());
}
}
}
}