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, notify: Characteristic, data: Characteristic, }
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
34pub 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
74pub 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(¬ify)
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
217pub 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(¬if.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 println!("DEBUG: Parsed battery payload: {:?}", notif.payload);
256 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 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}