catprinter/
ble.rs

1use crate::dithering::{atkinson_dither, bayer_dither, halftone_dither, ImageDithering};
2use crate::printer::PrinterStatus;
3use crate::protocol::{build_control_packet, chunk_data, pack_1bpp_pixels, parse_notification};
4use async_trait::async_trait;
5use btleplug::api::{
6    Central as _, Characteristic, Manager as _, Peripheral as _, ScanFilter, WriteType,
7};
8use btleplug::platform::{Manager, Peripheral};
9use futures::stream::StreamExt;
10use std::time::Duration;
11use tokio::time;
12use uuid::Uuid;
13
14#[derive(Debug, Clone)]
15pub struct DeviceInfo {
16    pub id: String,
17    pub name: Option<String>,
18}
19
20#[derive(Debug)]
21struct BtleCharacteristics {
22    control: Characteristic, // AE01
23    notify: Characteristic,  // AE02
24    data: Characteristic,    // AE03
25}
26
27#[async_trait]
28pub trait TransportAsync: Send + Sync {
29    async fn write_control(&self, data: &[u8]) -> Result<(), String>;
30    async fn write_data(&self, data: &[u8]) -> Result<(), String>;
31    async fn read_notification(&self, timeout: Duration) -> Result<Vec<u8>, String>;
32}
33
34/// Scans for CatPrinter-compatible BLE devices.
35///
36/// - `timeout`: scan duration
37///
38/// Returns Vec<DeviceInfo> on success
39pub async fn scan(timeout: Duration) -> Result<Vec<DeviceInfo>, String> {
40    let manager = Manager::new()
41        .await
42        .map_err(|e| format!("manager error: {:?}", e))?;
43    let adapters = manager
44        .adapters()
45        .await
46        .map_err(|e| format!("adapter list error: {:?}", e))?;
47    let adapter = adapters
48        .into_iter()
49        .next()
50        .ok_or_else(|| "no BLE adapters found".to_string())?;
51    adapter
52        .start_scan(ScanFilter::default())
53        .await
54        .map_err(|e| format!("scan start error: {:?}", e))?;
55    time::sleep(timeout).await;
56    let peripherals = adapter
57        .peripherals()
58        .await
59        .map_err(|e| format!("peripherals error: {:?}", e))?;
60    let mut list = vec![];
61    for p in peripherals {
62        let id = p.id().to_string();
63        let name = p
64            .properties()
65            .await
66            .ok()
67            .flatten()
68            .and_then(|props| props.local_name);
69        list.push(DeviceInfo { id, name });
70    }
71    Ok(list)
72}
73
74/// Connects to a CatPrinter BLE device by ID.
75///
76/// - `device_id`: device identifier string
77/// - `_timeout`: connection timeout (unused)
78///
79/// Returns CatPrinterAsync on success
80pub async fn connect(device_id: &str, _timeout: Duration) -> Result<CatPrinterAsync, String> {
81    let manager = Manager::new()
82        .await
83        .map_err(|e| format!("manager error: {:?}", e))?;
84    let adapters = manager
85        .adapters()
86        .await
87        .map_err(|e| format!("adapter list error: {:?}", e))?;
88    let adapter = adapters
89        .into_iter()
90        .next()
91        .ok_or_else(|| "no BLE adapters found".to_string())?;
92    let peripherals = adapter
93        .peripherals()
94        .await
95        .map_err(|e| format!("peripherals error: {:?}", e))?;
96    let maybe = peripherals
97        .into_iter()
98        .find(|p| p.id().to_string() == device_id);
99    let peripheral = maybe.ok_or_else(|| format!("device {} not found", device_id))?;
100    if !peripheral
101        .is_connected()
102        .await
103        .map_err(|e| format!("{:?}", e))?
104    {
105        peripheral
106            .connect()
107            .await
108            .map_err(|e| format!("connect error: {:?}", e))?;
109    }
110    peripheral
111        .discover_services()
112        .await
113        .map_err(|e| format!("discover error: {:?}", e))?;
114
115    let ae01 = Uuid::parse_str("0000ae01-0000-1000-8000-00805f9b34fb").unwrap();
116    let ae02 = Uuid::parse_str("0000ae02-0000-1000-8000-00805f9b34fb").unwrap();
117    let ae03 = Uuid::parse_str("0000ae03-0000-1000-8000-00805f9b34fb").unwrap();
118
119    let chars = peripheral.characteristics();
120    let mut control_c: Option<Characteristic> = None;
121    let mut notify_c: Option<Characteristic> = None;
122    let mut data_c: Option<Characteristic> = None;
123    for c in &chars {
124        if c.uuid == ae01 {
125            control_c = Some(c.clone());
126        }
127        if c.uuid == ae02 {
128            notify_c = Some(c.clone());
129        }
130        if c.uuid == ae03 {
131            data_c = Some(c.clone());
132        }
133    }
134    let control = control_c.ok_or_else(|| "AE01 control characteristic not found".to_string())?;
135    let notify = notify_c.ok_or_else(|| "AE02 notify characteristic not found".to_string())?;
136    let data = data_c.ok_or_else(|| "AE03 data characteristic not found".to_string())?;
137
138    peripheral
139        .subscribe(&notify)
140        .await
141        .map_err(|e| format!("subscribe error: {:?}", e))?;
142
143    let transport = BtleTransport::new(
144        peripheral.clone(),
145        control.clone(),
146        notify.clone(),
147        data.clone(),
148    );
149    let cat = CatPrinterAsync::new(Box::new(transport));
150    Ok(cat)
151}
152
153pub struct BtleTransport {
154    peripheral: Peripheral,
155    control: Characteristic,
156    notify: Characteristic,
157    data: Characteristic,
158}
159
160impl BtleTransport {
161    pub fn new(
162        peripheral: Peripheral,
163        control: Characteristic,
164        notify: Characteristic,
165        data: Characteristic,
166    ) -> Self {
167        Self {
168            peripheral,
169            control,
170            notify,
171            data,
172        }
173    }
174}
175
176#[async_trait]
177impl TransportAsync for BtleTransport {
178    async fn write_control(&self, data: &[u8]) -> Result<(), String> {
179        self.peripheral
180            .write(&self.control, data, WriteType::WithoutResponse)
181            .await
182            .map_err(|e| format!("write_control error: {:?}", e))
183    }
184    async fn write_data(&self, data: &[u8]) -> Result<(), String> {
185        self.peripheral
186            .write(&self.data, data, WriteType::WithoutResponse)
187            .await
188            .map_err(|e| format!("write_data error: {:?}", e))
189    }
190    async fn read_notification(&self, timeout: Duration) -> Result<Vec<u8>, String> {
191        let mut notifications = self
192            .peripheral
193            .notifications()
194            .await
195            .map_err(|e| format!("notifications stream error: {:?}", e))?;
196        let deadline = time::Instant::now() + timeout;
197        loop {
198            let remaining = deadline
199                .checked_duration_since(time::Instant::now())
200                .unwrap_or_else(|| Duration::from_secs(0));
201            let maybe = time::timeout(remaining, notifications.next()).await;
202            match maybe {
203                Ok(Some(note)) => {
204                    if note.uuid == self.notify.uuid {
205                        return Ok(note.value.clone());
206                    } else {
207                        continue;
208                    }
209                }
210                Ok(None) => return Err("notifications stream ended".to_string()),
211                Err(_) => return Err("timeout waiting for notification".to_string()),
212            }
213        }
214    }
215}
216
217/// Asynchronous CatPrinter API for printing text and images.
218///
219/// - `transport`: implements TransportAsync trait (BLE)
220/// - `chunk_size`: bytes per data chunk (default: 180)
221pub struct CatPrinterAsync {
222    pub transport: Box<dyn TransportAsync + Send + Sync>,
223    chunk_size: usize,
224}
225
226impl CatPrinterAsync {
227    pub fn new(transport: Box<dyn TransportAsync + Send + Sync>) -> Self {
228        Self {
229            transport,
230            chunk_size: 180,
231        }
232    }
233
234    pub fn with_chunk_size(mut self, size: usize) -> Self {
235        self.chunk_size = size;
236        self
237    }
238
239    pub async fn get_status(&self, timeout: Duration) -> Result<PrinterStatus, String> {
240        let req = build_control_packet(0xA1, &[0x00]);
241        self.transport.write_control(&req).await?;
242        let raw = self.transport.read_notification(timeout).await?;
243        println!("DEBUG: Raw status notification bytes (0xA1): {:?}", raw);
244        let notif = parse_notification(&raw).map_err(|e| e.to_string())?;
245        Ok(crate::protocol::parse_printer_status(&notif.payload))
246    }
247
248    pub async fn get_battery(&self, timeout: Duration) -> Result<u8, String> {
249        let req = build_control_packet(0xAB, &[0x00]);
250        self.transport.write_control(&req).await?;
251        let raw = self.transport.read_notification(timeout).await?;
252        println!("DEBUG: Raw battery notification bytes (0xAB): {:?}", raw);
253        let notif = parse_notification(&raw).map_err(|e| e.to_string())?;
254        // Print all payload bytes for debugging
255        println!("DEBUG: Parsed battery payload: {:?}", notif.payload);
256        // Try to extract battery percent from payload
257        if !notif.payload.is_empty() {
258            Ok(notif.payload[0])
259        } else {
260            Err("Battery payload too short".to_string())
261        }
262    }
263
264    pub async fn print_text(&self, main: &str, author: &str) -> Result<(), String> {
265        let width = 384usize;
266        let pixels = crate::protocol::render_text_to_pixels(main, author, width);
267        let height = pixels.len() / width;
268        let rotated_pixels = crate::protocol::rotate_mirror_pixels(&pixels, width, height);
269        self.print_image(&rotated_pixels, width, height, 0x00, None)
270            .await
271    }
272
273    pub async fn print_image_from_path(
274        &self,
275        path: &str,
276        dithering: ImageDithering,
277    ) -> Result<(), String> {
278        let img = image::open(path).map_err(|e| e.to_string())?;
279        let width = 384;
280        let resized = img.thumbnail(width, u32::MAX);
281        let mut gray = resized.to_luma8();
282
283        match dithering {
284            ImageDithering::FloydSteinberg => {
285                image::imageops::dither(&mut gray, &image::imageops::BiLevel);
286            }
287            ImageDithering::Atkinson => {
288                atkinson_dither(&mut gray);
289            }
290            ImageDithering::Bayer => {
291                bayer_dither(&mut gray);
292            }
293            ImageDithering::Halftone => {
294                gray = halftone_dither(&gray);
295            }
296            ImageDithering::Threshold => {
297                // manual thresholding
298                for pixel in gray.pixels_mut() {
299                    if pixel[0] > 127 {
300                        pixel[0] = 255;
301                    } else {
302                        pixel[0] = 0;
303                    }
304                }
305            }
306        }
307
308        let (width, height) = gray.dimensions();
309        let pixels = gray.as_raw();
310        self.print_image(pixels, width as usize, height as usize, 0x00, None)
311            .await
312    }
313
314    pub async fn print_image(
315        &self,
316        pixels: &[u8],
317        width: usize,
318        height: usize,
319        mode: u8,
320        chunk_size: Option<usize>,
321    ) -> Result<(), String> {
322        let packed = pack_1bpp_pixels(pixels, width, height).map_err(|e| e.to_string())?;
323        let line_count: u16 = height as u16;
324        let mut a9_payload = Vec::new();
325        a9_payload.extend_from_slice(&line_count.to_le_bytes());
326        a9_payload.push(0x30);
327        a9_payload.push(mode);
328        let a9 = build_control_packet(0xA9, &a9_payload);
329        self.transport.write_control(&a9).await?;
330        let resp = self
331            .transport
332            .read_notification(Duration::from_secs(2))
333            .await?;
334        let parsed = parse_notification(&resp).map_err(|e| e.to_string())?;
335        if parsed.command_id != 0xA9 || parsed.payload.first() == Some(&0x01u8) {
336            return Err("printer rejected print request".into());
337        }
338        let size = chunk_size.unwrap_or(self.chunk_size);
339        for chunk in chunk_data(&packed, size) {
340            self.transport.write_data(chunk).await?;
341            time::sleep(Duration::from_millis(10)).await;
342        }
343        let ad = build_control_packet(0xAD, &[0x00]);
344        self.transport.write_control(&ad).await?;
345        let deadline = time::Instant::now() + Duration::from_secs(60);
346        loop {
347            let remaining = deadline
348                .checked_duration_since(time::Instant::now())
349                .unwrap_or_else(|| Duration::from_secs(0));
350            if remaining.is_zero() {
351                return Err("timed out waiting for print complete".into());
352            }
353            let raw = self.transport.read_notification(remaining).await?;
354            let notif = parse_notification(&raw).map_err(|e| e.to_string())?;
355            if notif.command_id == 0xAA {
356                return Ok(());
357            }
358        }
359    }
360}