ql_label/printer.rs
1use log::{debug, error, info, warn};
2use rusb::{Context, Device, DeviceDescriptor, DeviceHandle, Direction, TransferType, UsbContext};
3use std::time::Duration;
4
5use crate::{
6 error::{Error, PrinterError},
7 media::Media,
8 model::Model,
9 utils::TwoColorMatrix,
10 Matrix,
11};
12
13// Vendoer id of Brother Industries, Ltd
14const VENDOR_ID: u16 = 0x04f9;
15
16#[derive(Debug, Clone, Copy)]
17#[allow(dead_code)]
18struct Endpoint {
19 config: u8,
20 iface: u8,
21 setting: u8,
22 address: u8,
23}
24
25pub struct Printer {
26 handle: Box<DeviceHandle<Context>>,
27 endpoint_out: Endpoint,
28 endpoint_in: Endpoint,
29 config: Config,
30}
31
32impl Printer {
33 /// Create a new printer instance with the specified configuration.
34 ///
35 /// This constructor handles USB device enumeration, connection, and initialization.
36 /// It will search for a Brother P-Touch printer matching the model and serial number
37 /// specified in the configuration.
38 ///
39 /// # Arguments
40 /// * `config` - Printer configuration containing model, serial, media, and print settings
41 ///
42 /// # Returns
43 /// * `Ok(Printer)` - Successfully connected printer instance
44 /// * `Err(Error)` - Connection failed, device not found, or USB error
45 ///
46 /// # Example
47 /// ```rust,no_run
48 /// # use ptouch::{Config, Model, Media, ContinuousType, Printer};
49 /// let config = Config::new(Model::QL820NWB, "E8N117P02180".to_string(),
50 /// Media::Continuous(ContinuousType::Continuous62));
51 /// let printer = Printer::new(config)?;
52 /// # Ok::<(), ptouch::Error>(())
53 /// ```
54 pub fn new(config: Config) -> Result<Self, Error> {
55 // rusb::set_log_level(rusb::LogLevel::Debug);
56 match Context::new() {
57 Ok(mut context) => {
58 match Self::open_device(&mut context, config.model.pid(), config.serial.clone()) {
59 Ok((mut device, device_desc, handle)) => {
60 handle.reset()?;
61
62 let endpoint_in = match Self::find_endpoint(
63 &mut device,
64 &device_desc,
65 Direction::In,
66 TransferType::Bulk,
67 ) {
68 Some(endpoint) => endpoint,
69 None => return Err(Error::MissingEndpoint),
70 };
71
72 let endpoint_out = match Self::find_endpoint(
73 &mut device,
74 &device_desc,
75 Direction::Out,
76 TransferType::Bulk,
77 ) {
78 Some(endpoint) => endpoint,
79 None => return Err(Error::MissingEndpoint),
80 };
81
82 // QL-800では`has_kernel_driver`が`true`となる
83 // QL-820NWBでは`has_kernel_driver`が`false`となる
84 // `has_kernel_driver`が`true`の場合に、カーネルドライバーをデタッチしないとエラーとなる
85 //
86 handle.set_auto_detach_kernel_driver(true)?;
87 let has_kernel_driver = match handle.kernel_driver_active(0) {
88 Ok(true) => {
89 handle.detach_kernel_driver(0).ok();
90 true
91 }
92 _ => false,
93 };
94 info!(" Kernel driver support is {}", has_kernel_driver);
95 handle.set_active_configuration(1)?;
96 handle.claim_interface(0)?;
97 handle.set_alternate_setting(0, 0)?;
98
99 Ok(Printer {
100 handle: Box::new(handle),
101 endpoint_out,
102 endpoint_in,
103 config,
104 })
105 }
106 Err(err) => {
107 debug!("Device connection failed: {:?}", err);
108 Err(Error::DeviceOffline)
109 }
110 }
111 }
112 Err(err) => Err(Error::UsbError(err)),
113 }
114 }
115
116 /// Cancel current print job and reset printer state.
117 ///
118 /// Sends an initialization command to cancel any ongoing print job
119 /// and reset the printer to a ready state.
120 ///
121 /// # Returns
122 /// * `Ok(())` - Cancel command sent successfully
123 /// * `Err(Error)` - Communication error
124 ///
125 /// # Example
126 /// ```rust,no_run
127 /// # use ptouch::{Config, Model, Media, ContinuousType, Printer};
128 /// # let config = Config::new(Model::QL820NWB, "serial".to_string(),
129 /// # Media::Continuous(ContinuousType::Continuous62));
130 /// let printer = Printer::new(config)?;
131 /// printer.cancel()?; // Cancel any ongoing job
132 /// # Ok::<(), ptouch::Error>(())
133 /// ```
134 pub fn cancel(&self) -> Result<(), Error> {
135 let buf = self.initialize();
136 self.write(buf)?;
137 Ok(())
138 }
139
140 /// Read current printer status including media type, errors, and phase.
141 ///
142 /// This method is convenient for inspection when a new media is added
143 /// or to check for printer errors before starting a print job.
144 ///
145 /// # Returns
146 /// * `Ok(Status)` - Current printer status information
147 /// * `Err(Error)` - Communication error or timeout
148 ///
149 /// # Example
150 /// ```rust,no_run
151 /// # use ptouch::{Config, Model, Media, ContinuousType, Printer};
152 /// # let config = Config::new(Model::QL820NWB, "serial".to_string(),
153 /// # Media::Continuous(ContinuousType::Continuous62));
154 /// let printer = Printer::new(config)?;
155 /// match printer.check_status() {
156 /// Ok(status) => println!("Printer ready: {:?}", status),
157 /// Err(e) => eprintln!("Printer error: {:?}", e),
158 /// }
159 /// # Ok::<(), ptouch::Error>(())
160 /// ```
161 pub fn check_status(&self) -> Result<Status, Error> {
162 self.request_status()?;
163 self.read_status()
164 }
165
166 /// Print single-color labels.
167 ///
168 /// This method prints labels using black ink only. For two-color printing,
169 /// use `print_two_color()` method instead.
170 ///
171 /// # Arguments
172 /// * `images` - Iterator of `Matrix` (`Vec<Vec<u8>>`) containing 1-bit bitmap data
173 ///
174 /// # Returns
175 /// * `Ok(())` - Print job completed successfully
176 /// * `Err(Error)` - Printer error, communication error, or media mismatch
177 ///
178 /// # Image Format
179 /// - Width: 720 pixels (90 bytes) for normal printers, 1296 pixels for wide printers
180 /// - Height: Variable, depends on label length
181 /// - Format: 1-bit bitmap packed into bytes (8 pixels per byte)
182 ///
183 /// # Example
184 /// ```rust,no_run
185 /// # use ptouch::{Config, Model, Media, ContinuousType, Printer, Matrix};
186 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
187 /// Media::Continuous(ContinuousType::Continuous62));
188 /// let printer = Printer::new(config)?;
189 ///
190 /// // Create simple black and white pattern
191 /// let image_data: Matrix = vec![vec![0xFF; 90]; 300]; // 300 lines of solid black
192 ///
193 /// printer.print(vec![image_data].into_iter())?;
194 /// # Ok::<(), ptouch::Error>(())
195 /// ```
196 pub fn print(&self, images: impl Iterator<Item = Matrix>) -> Result<(), Error> {
197 info!("Requesting printer status before print job");
198
199 self.request_status()?;
200
201 match self.read_status() {
202 Ok(status) => {
203 info!("Verifying correct media is installed");
204 status.check_media(self.config.media)?;
205
206 info!("Starting print job");
207 self.print_label(images)?;
208 Ok(())
209 }
210 Err(err) => {
211 error!("Failed to read printer status: {:?}", err);
212 Err(err)
213 }
214 }
215 }
216
217 /// Print two-color labels using black and red colors.
218 ///
219 /// This method is specifically designed for QL-820NWB printers with
220 /// red/black tape installed. The configuration must have `two_colors(true)`
221 /// enabled for this method to work.
222 ///
223 /// # Arguments
224 /// * `images` - Iterator of `TwoColorMatrix` containing black and red image data
225 ///
226 /// # Returns
227 /// * `Ok(())` - Print job completed successfully
228 /// * `Err(Error)` - Printer error, communication error, or invalid configuration
229 ///
230 /// # Example
231 /// ```rust,no_run
232 /// # use ptouch::{Config, Model, Media, ContinuousType, Printer, TwoColorMatrix};
233 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
234 /// Media::Continuous(ContinuousType::Continuous62Red))
235 /// .two_colors(true);
236 /// let printer = Printer::new(config)?;
237 ///
238 /// // Create two-color image data
239 /// let black_data = vec![vec![0u8; 90]; 300];
240 /// let red_data = vec![vec![0u8; 90]; 300];
241 /// let two_color = TwoColorMatrix::new(black_data, red_data)?;
242 ///
243 /// printer.print_two_color(vec![two_color].into_iter())?;
244 /// # Ok::<(), Box<dyn std::error::Error>>(())
245 /// ```
246 pub fn print_two_color(&self, images: impl Iterator<Item = TwoColorMatrix>) -> Result<(), Error> {
247 if !self.config.two_colors {
248 return Err(Error::InvalidConfig("Two-color printing not enabled in config".to_string()));
249 }
250
251 info!("Requesting printer status before two-color print job");
252
253 self.request_status()?;
254
255 match self.read_status() {
256 Ok(status) => {
257 info!("Verifying correct media is installed");
258 status.check_media(self.config.media)?;
259
260 info!("Starting two-color print job");
261 let alternating_images = images.map(|two_color| two_color.to_alternating_matrix());
262 self.print_label(alternating_images)?;
263 Ok(())
264 }
265 Err(err) => {
266 error!("Failed to read printer status: {:?}", err);
267 Err(err)
268 }
269 }
270 }
271
272 // Private helper methods
273
274 fn open_device(
275 context: &mut Context,
276 pid: u16,
277 serial: String,
278 ) -> Result<(Device<Context>, DeviceDescriptor, DeviceHandle<Context>), Error> {
279 let devices = context.devices()?;
280
281 if devices.is_empty() {
282 warn!("Unable to enumerate USB devices");
283 return Err(Error::DeviceListNotReadable);
284 }
285 for device in devices.iter() {
286 let device_desc = match device.device_descriptor() {
287 Ok(d) => d,
288 Err(err) => {
289 debug!("{:#?}", err);
290 continue;
291 }
292 };
293 debug!(
294 "vender_id: {:x}, product_id: {:x}",
295 device_desc.vendor_id(),
296 device_desc.product_id()
297 );
298 if device_desc.vendor_id() == VENDOR_ID && device_desc.product_id() == pid {
299 match device.open() {
300 Ok(handle) => {
301 let timeout = Duration::from_secs(1);
302 let languages = handle.read_languages(timeout)?;
303
304 if languages.len() > 0 {
305 let language = languages[0];
306 match handle.read_serial_number_string(language, &device_desc, timeout)
307 {
308 Ok(s) => {
309 if s == serial {
310 info!("Connected to printer (serial: {})", serial);
311 return Ok((device, device_desc, handle));
312 } else {
313 continue;
314 }
315 }
316 Err(err) => {
317 debug!("Cannot read device serial number: {:?}", err);
318 continue;
319 }
320 }
321 } else {
322 continue;
323 }
324 }
325 Err(err) => {
326 debug!("Unable to open USB device: {:?}", err);
327 continue;
328 }
329 }
330 }
331 }
332 error!("No printer found with serial number: {}", serial);
333 Err(Error::DeviceOffline)
334 }
335
336 fn find_endpoint(
337 device: &mut Device<Context>,
338 device_desc: &DeviceDescriptor,
339 direction: Direction,
340 transfer_type: TransferType,
341 ) -> Option<Endpoint> {
342 for n in 0..device_desc.num_configurations() {
343 let config_desc = match device.config_descriptor(n) {
344 Ok(c) => c,
345 Err(_) => continue,
346 };
347 for interface in config_desc.interfaces() {
348 for interface_desc in interface.descriptors() {
349 for endpoint_desc in interface_desc.endpoint_descriptors() {
350 if endpoint_desc.direction() == direction
351 && endpoint_desc.transfer_type() == transfer_type
352 {
353 return Some(Endpoint {
354 config: config_desc.number(),
355 iface: interface_desc.interface_number(),
356 setting: interface_desc.setting_number(),
357 address: endpoint_desc.address(),
358 });
359 }
360 }
361 }
362 }
363 }
364 None
365 }
366
367 fn write(&self, buf: Vec<u8>) -> Result<(), Error> {
368 // 動的タイムアウト計算
369 // - ベースタイムアウト: 5秒
370 // - データサイズ依存: 1MB/sの転送速度を仮定
371 // - 安全マージン: 2倍
372 let base_timeout_secs = 5;
373 let transfer_rate_bytes_per_sec = 1_000_000; // 1MB/s
374 let safety_margin = 2.0;
375
376 let data_dependent_timeout =
377 (buf.len() as f64 / transfer_rate_bytes_per_sec as f64) * safety_margin;
378 let total_timeout_secs = base_timeout_secs as f64 + data_dependent_timeout;
379
380 // 最小10秒、最大60秒の範囲でクランプ
381 let timeout_secs = total_timeout_secs.max(10.0).min(60.0);
382 let timeout = Duration::from_secs(timeout_secs as u64);
383
384 debug!(
385 "USB transfer timeout set to {:.1}s for {} bytes",
386 timeout_secs,
387 buf.len()
388 );
389 let result = self
390 .handle
391 .write_bulk(self.endpoint_out.address, &buf, timeout);
392 match result {
393 Ok(n) => {
394 if n == buf.len() {
395 debug!(
396 "Successfully wrote {} bytes to endpoint {:#x}",
397 n, self.endpoint_out.address
398 );
399 Ok(())
400 } else {
401 warn!(
402 "USB write incomplete: {} of {} bytes transferred (possible timeout)",
403 n,
404 buf.len()
405 );
406 Err(Error::InvalidResponse(n))
407 }
408 }
409 Err(e) => Err(Error::UsbError(e)),
410 }
411 }
412
413 fn read_status(&self) -> Result<Status, Error> {
414 self.read_status_with_timeout(Duration::from_millis(1000))
415 }
416
417 fn read_status_with_timeout(&self, timeout: Duration) -> Result<Status, Error> {
418 let mut buf: [u8; 32] = [0x00; 32];
419 let mut counter = 0;
420
421 debug!("reading from endpoint_in {:#?}", self.endpoint_in);
422 while counter < 100000 {
423 match self
424 .handle
425 .read_bulk(self.endpoint_in.address, &mut buf, timeout)
426 {
427 // TODO: Check the first 4bytes match to [0x80, 0x20, 0x42, 0x34]
428 // TODO: Check the error status
429 //
430 // buf is pouplated with 32 bytes of data
431 Ok(32) => {
432 let status = Status::from_buf(buf);
433 debug!("Raw status code: {:X?}", buf);
434 debug!("Parsed Status struct: {:?}", status);
435 return Ok(status);
436 }
437 Ok(x) => {
438 debug!("Waiting {counter} {x}");
439 std::thread::sleep(std::time::Duration::from_millis(50));
440 }
441 Err(e) => return Err(Error::UsbError(e)),
442 };
443 counter = counter + 1;
444 }
445 Err(Error::ReadStatusTimeout)
446 }
447
448 fn wait_for_print_completion(&self) -> Result<(), Error> {
449 let mut attempts = 0;
450 const MAX_ATTEMPTS: u32 = 100; // 約5秒のタイムアウト
451
452 debug!("Waiting for print completion...");
453
454 loop {
455 let status = self.read_status_with_timeout(Duration::from_millis(1000))?;
456 debug!(
457 "Print completion check: status_type={:?}, phase={:?}, error={:?}",
458 status.status_type, status.phase, status.error
459 );
460
461 // エラー状態の即座検出
462 if !status.error.is_no_error() {
463 error!("Print operation failed: {:?}", status.error);
464 return Err(Error::PrinterError(status.error));
465 }
466
467 match (status.status_type, status.phase) {
468 // エラー状態の即座検出
469 (StatusType::Error, _) => {
470 error!("Printer reported error status");
471 return Err(Error::PrinterError(status.error));
472 }
473
474 // 印刷完了 -> 受信待機への遷移を待つ
475 (StatusType::Completed, Phase::Printing) => {
476 info!("Print finished, verifying printer state");
477 // 完了後、受信状態への遷移を確認
478 std::thread::sleep(Duration::from_millis(100));
479 let final_status = self.read_status_with_timeout(Duration::from_millis(500))?;
480 if matches!(final_status.phase, Phase::Receiving) {
481 info!("Print completed, printer ready for next job");
482 return Ok(());
483 }
484 debug!(
485 "Still waiting for transition to receiving state, current phase: {:?}",
486 final_status.phase
487 );
488 }
489
490 // 既に受信状態に戻っている(即座完了)
491 (StatusType::PhaseChange, Phase::Receiving) => {
492 info!("Printer ready (already in receiving state)");
493 return Ok(());
494 }
495
496 // まだ印刷中
497 (StatusType::PhaseChange, Phase::Printing) => {
498 debug!("Print in progress, continuing to monitor");
499 // 短い待機で継続監視
500 std::thread::sleep(Duration::from_millis(50));
501 }
502
503 // 予期しない状態
504 _ => {
505 debug!("Unexpected status during print completion: {:#?}", status);
506 std::thread::sleep(Duration::from_millis(100));
507 }
508 }
509
510 attempts += 1;
511 if attempts >= MAX_ATTEMPTS {
512 error!(
513 "Print completion timed out after {} attempts ({}s)",
514 attempts,
515 attempts * 50 / 1000
516 );
517 return Err(Error::PrintTimeout);
518 }
519 }
520 }
521
522 fn initialize(&self) -> Vec<u8> {
523 let mut buf: Vec<u8> = Vec::new();
524 buf.append(&mut [0x00; 400].to_vec());
525 buf.append(&mut [0x1B, 0x40].to_vec());
526 buf
527 }
528
529 fn set_media(&self, buf: &mut std::vec::Vec<u8>, raster_count: u32) {
530 buf.extend_from_slice(&[0x1B, 0x69, 0x7A]); // ESC i z
531
532 // n1: 有効フラグ (用紙種類+幅+長さ+ラスター数)
533 let valid_flags = 0x02 | 0x04 | 0x08 | 0x40;
534 buf.push(valid_flags);
535
536 // n2: 用紙種類 (長尺:0x0A, ダイカット:0x0C)
537 let media_type = match self.config.media {
538 Media::Continuous(_) => 0x0A,
539 Media::DieCut(_) => 0x0B,
540 };
541 buf.push(media_type);
542
543 // n3, n4: 用紙幅・長さ (mm)
544 let spec = self.config.media.spec();
545 buf.push(spec.width_mm());
546 buf.push(spec.length_mm());
547
548 // n5-n8: ラスター数 (リトルエンディアン)
549 let raster_bytes = raster_count.to_le_bytes();
550 buf.extend_from_slice(&raster_bytes);
551
552 // n9: 先頭ページフラグ (0=先頭ページ)
553 buf.push(0x00);
554
555 // n10: 固定値
556 buf.push(0x00);
557 }
558
559 fn print_label(&self, images: impl Iterator<Item = Matrix>) -> Result<(), Error> {
560 let mut preamble: Vec<u8> = self.initialize();
561 preamble.append(&mut [0x1B, 0x69, 0x61, 0x01].to_vec()); // Set raster command mode
562 preamble.append(&mut [0x1B, 0x69, 0x21, 0x00].to_vec()); // Set auto status notificatoin mode
563 //
564 // Apply config values
565 match self.config.clone().build() {
566 Ok(mut buf) => preamble.append(&mut buf),
567 Err(err) => return Err(err),
568 }
569
570 // QL-800では圧縮モードがサポートされていないため、常に非圧縮とする
571 let use_compression = if matches!(self.config.model, Model::QL800) && self.config.compress {
572 warn!("QL-800 does not support compression mode, using uncompressed mode instead");
573 false
574 } else {
575 self.config.compress
576 };
577
578 if use_compression {
579 preamble.append(&mut [0x4D, 0x02].to_vec()); // Set to pack bits compression mode
580 } else {
581 preamble.append(&mut [0x4D, 0x00].to_vec()); // Set to no compression mode
582 }
583
584 debug!("{:?}", self.config);
585
586 let mut start_flag: bool = true;
587 let mut color = false;
588
589 let mut iter = images.into_iter().peekable();
590
591 loop {
592 let mut buf: Vec<u8> = Vec::new();
593
594 match iter.next() {
595 Some(image) => {
596 if start_flag {
597 buf.append(&mut preamble);
598 }
599
600 // ESC i z 印刷情報司令
601 let raster_count = if self.config.two_colors {
602 (image.len() / 2) as u32
603 } else {
604 image.len() as u32
605 };
606 self.set_media(&mut buf, raster_count);
607 if start_flag {
608 buf.append(&mut [0x00, 0x00].to_vec());
609 start_flag = false;
610 } else {
611 buf.append(&mut [0x01, 0x00].to_vec());
612 }
613
614 // Add raster line image data
615 if self.config.two_colors {
616 for mut row in image {
617 if color {
618 // Black raster line (color code 0x01)
619 buf.append(&mut [0x77, 0x01, 90].to_vec());
620 buf.append(&mut row);
621 color = !color;
622 } else {
623 // Red raster line (color code 0x02)
624 buf.append(&mut [0x77, 0x02, 90].to_vec());
625 buf.append(&mut row);
626 color = !color;
627 }
628 }
629 } else {
630 if use_compression {
631 for row in image {
632 let mut packed = Self::pack_bits(&row);
633 let len = packed.len() as u8;
634 buf.append(&mut [0x67, 0x00, len].to_vec());
635 buf.append(&mut packed);
636 }
637 } else {
638 for mut row in image {
639 buf.append(&mut [0x67, 0x00, 90].to_vec());
640 buf.append(&mut row);
641 }
642 }
643 }
644
645 if iter.peek().is_some() {
646 buf.push(0x0C); // FF : Print
647 self.write(buf)?;
648 info!("Print command sent, waiting for completion...");
649
650 // 改善されたステータス待機(中間ページ)
651 self.wait_for_print_completion()?;
652 info!("Page printed successfully");
653 } else {
654 buf.push(0x1A); // Control-Z : Print then Eject
655 self.write(buf)?;
656 info!("Final print command sent, ejecting media...");
657
658 // 改善されたステータス待機
659 self.wait_for_print_completion()?;
660 info!("Print job completed successfully");
661
662 self.invalidate()?;
663 }
664 }
665 None => {
666 break;
667 }
668 }
669 }
670 Ok(())
671 }
672
673 /// TIFF PackBits圧縮アルゴリズム(Brother QL仕様準拠)
674 ///
675 /// 仕様:
676 /// - 同一データ連続:個数-1を負数で指定 + データ1バイト
677 /// - 異なるデータ連続:個数-1を正数で指定 + 全データ
678 /// - 90バイト超過時は非圧縮として91バイト送信
679 fn pack_bits(data: &[u8]) -> Vec<u8> {
680 // 入力データが90バイト固定でない場合はそのまま返す
681 if data.len() != 90 {
682 return data.to_vec();
683 }
684
685 let mut packed = Vec::new();
686 let mut i = 0;
687
688 while i < data.len() {
689 // Run-length encoding (RLE)のチェック
690 let mut run_length = 1;
691 let run_value = data[i];
692
693 // 同じ値の連続をカウント(最大128個まで)
694 while i + run_length < data.len()
695 && run_length < 128
696 && data[i + run_length] == run_value
697 {
698 run_length += 1;
699 }
700
701 // RLEが効果的な場合(2個以上の連続)
702 if run_length >= 2 {
703 // 負数で圧縮指示: -(count-1)
704 packed.push((-(run_length as i8 - 1)) as u8);
705 packed.push(run_value);
706 i += run_length;
707 } else {
708 // リテラル実行のチェック
709 let start_pos = i;
710 let mut literal_length = 1;
711
712 // リテラル実行の最適な長さを決定
713 while i + literal_length < data.len() && literal_length < 128 {
714 // 次の位置で2個以上同じ値が続く場合は、ここでリテラル実行を終了
715 if i + literal_length + 1 < data.len()
716 && data[i + literal_length] == data[i + literal_length + 1]
717 {
718 break;
719 }
720 literal_length += 1;
721 }
722
723 // リテラル実行: 正数で非圧縮指示
724 packed.push((literal_length - 1) as u8);
725 packed.extend_from_slice(&data[start_pos..start_pos + literal_length]);
726 i += literal_length;
727 }
728 }
729
730 // 重要な最適化: 90バイト超過時は非圧縮として91バイト返す
731 if packed.len() > 90 {
732 warn!(
733 "Data compression ineffective, sending uncompressed ({} bytes)",
734 data.len()
735 );
736 let mut result = Vec::with_capacity(91);
737 result.push(89); // 90-1 = 89(90バイトの非圧縮指示)
738 result.extend_from_slice(data);
739 result
740 } else {
741 debug!(
742 "Compression reduced data from {} to {} bytes ({:.1}% reduction)",
743 data.len(),
744 packed.len(),
745 (1.0 - packed.len() as f64 / data.len() as f64) * 100.0
746 );
747 packed
748 }
749 }
750
751 fn request_status(&self) -> Result<(), Error> {
752 let mut buf: Vec<u8> = self.initialize();
753 buf.append(&mut [0x1b, 0x69, 0x53].to_vec());
754 self.write(buf)
755 }
756
757 fn invalidate(&self) -> Result<(), Error> {
758 let buf: Vec<u8> = self.initialize();
759 self.write(buf)
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766
767 #[test]
768 fn test_pack_bits_compression() {
769 // テスト1: 効果的な圧縮(同一データ連続)
770 let all_zeros = vec![0u8; 90];
771 let compressed = Printer::pack_bits(&all_zeros);
772 println!(
773 "All zeros: {} -> {} bytes",
774 all_zeros.len(),
775 compressed.len()
776 );
777 assert!(compressed.len() < all_zeros.len(), "圧縮が効果的でない");
778
779 // テスト2: 非効果的な圧縮(ランダムデータ)
780 let random_data: Vec<u8> = (0..90).map(|i| (i * 37 + 17) as u8).collect();
781 let compressed_random = Printer::pack_bits(&random_data);
782 println!(
783 "Random data: {} -> {} bytes",
784 random_data.len(),
785 compressed_random.len()
786 );
787
788 // テスト3: 91バイト制限の確認
789 if compressed_random.len() > 90 {
790 println!("91バイト制限により非圧縮データが返される");
791 assert_eq!(compressed_random.len(), 91); // 89 + 90バイトの元データ
792 assert_eq!(compressed_random[0], 89); // 非圧縮指示
793 }
794
795 // テスト4: 混合パターン(部分的な圧縮効果)
796 let mut mixed_data = vec![0u8; 30];
797 mixed_data.extend(vec![255u8; 30]);
798 mixed_data.extend((0..30).map(|i| i as u8));
799 let compressed_mixed = Printer::pack_bits(&mixed_data);
800 println!(
801 "Mixed data: {} -> {} bytes",
802 mixed_data.len(),
803 compressed_mixed.len()
804 );
805 }
806
807 #[test]
808 fn test_pack_bits_edge_cases() {
809 // エッジケース1: 空のデータ
810 let empty_data = vec![];
811 let compressed_empty = Printer::pack_bits(&empty_data);
812 assert_eq!(compressed_empty, empty_data);
813
814 // エッジケース2: 90バイト以外のサイズ
815 let wrong_size = vec![42u8; 50];
816 let compressed_wrong = Printer::pack_bits(&wrong_size);
817 assert_eq!(compressed_wrong, wrong_size);
818
819 // エッジケース3: 単一バイトの繰り返し(最大圧縮)
820 let single_byte = vec![42u8; 90];
821 let compressed_single = Printer::pack_bits(&single_byte);
822 assert_eq!(compressed_single.len(), 2); // 長さ指示 + データ
823 assert_eq!(compressed_single[0], (-(90i8 - 1)) as u8); // -89
824 assert_eq!(compressed_single[1], 42);
825 }
826}
827
828///
829/// Status received from the printer encoded to Rust friendly type.
830///
831#[derive(Debug)]
832#[allow(dead_code)]
833pub struct Status {
834 model: Model,
835 error: PrinterError,
836 media: Option<Media>,
837 mode: u8,
838 status_type: StatusType,
839 phase: Phase,
840 notification: Notification,
841 id: u8,
842}
843
844impl Status {
845 fn from_buf(buf: [u8; 32]) -> Self {
846 Status {
847 model: Model::from_code(buf[4]),
848 error: PrinterError::from_buf(buf),
849 media: Media::from_buf(buf),
850 mode: buf[15],
851 status_type: StatusType::from_code(buf[18]),
852 phase: Phase::from_buf(buf),
853 notification: Notification::from_code(buf[22]),
854 id: buf[14],
855 }
856 }
857
858 pub fn check_media(self, expected_media: Media) -> Result<(), Error> {
859 match self.media {
860 Some(actual_media) => {
861 if actual_media == expected_media {
862 Ok(())
863 } else {
864 Err(Error::MediaMismatch {
865 expected: expected_media,
866 actual: actual_media,
867 })
868 }
869 }
870 None => Err(Error::NoMediaInstalled),
871 }
872 }
873}
874
875// StatusType
876
877#[derive(Debug, PartialEq, Clone, Copy)]
878enum StatusType {
879 ReplyToRequest,
880 Completed,
881 Error,
882 Offline,
883 Notification,
884 PhaseChange,
885 Unknown,
886}
887
888impl StatusType {
889 fn from_code(code: u8) -> StatusType {
890 match code {
891 0x00 => Self::ReplyToRequest,
892 0x01 => Self::Completed,
893 0x02 => Self::Error,
894 0x04 => Self::Offline,
895 0x05 => Self::Notification,
896 0x06 => Self::PhaseChange,
897 _ => Self::Unknown,
898 }
899 }
900}
901// Phase
902
903#[derive(Debug, PartialEq, Clone, Copy)]
904pub enum Phase {
905 Receiving,
906 Printing,
907 Waiting(u16),
908 // Printing(u16),
909}
910
911impl Phase {
912 fn from_buf(buf: [u8; 32]) -> Self {
913 match buf[19] {
914 0x00 => Self::Receiving,
915 0x01 => Self::Printing,
916 _ => Self::Waiting(0),
917 }
918 }
919}
920
921// Notification
922
923#[derive(Debug)]
924enum Notification {
925 NotAvailable,
926 CoolingStarted,
927 CoolingFinished,
928}
929
930impl Notification {
931 fn from_code(code: u8) -> Self {
932 match code {
933 0x03 => Self::CoolingStarted,
934 0x04 => Self::CoolingFinished,
935 _ => Self::NotAvailable,
936 }
937 }
938}
939
940/// Config
941///
942#[derive(Debug, Clone, Copy)]
943enum AutoCut {
944 Enabled(u8),
945 Disabled,
946}
947
948#[derive(Debug, Clone)]
949pub struct Config {
950 model: Model,
951 serial: String,
952 media: Media,
953 auto_cut: AutoCut,
954 two_colors: bool,
955 cut_at_end: bool,
956 high_resolution: bool,
957 feed: u16,
958 compress: bool,
959}
960
961impl Config {
962 /// Initialize configuration data with default values.
963 ///
964 /// This method receives model and media. They are not modifiable after the initialization.
965 ///
966 /// # Example
967 ///
968 /// ```rust,no_run
969 /// use ptouch::{Config, ContinuousType, Media, Model};
970 ///
971 /// let media = Media::Continuous(ContinuousType::Continuous29);
972 /// let model = Model::QL800;
973 /// let config = Config::new(model, "serial".to_string(), media);
974 /// ```
975 ///
976 pub fn new(model: Model, serial: String, media: Media) -> Config {
977 Config {
978 model,
979 serial,
980 media,
981 auto_cut: AutoCut::Enabled(1),
982 two_colors: false,
983 cut_at_end: true,
984 high_resolution: false,
985 feed: media.get_default_feed_dots(),
986 compress: false,
987 }
988 }
989
990 /// Enable auto cut after printing specified number of labels.
991 ///
992 /// # Arguments
993 /// * `size` - Number of labels to print before auto-cutting (1-255)
994 ///
995 /// # Example
996 /// ```rust,no_run
997 /// # use ptouch::{Config, Model, Media, ContinuousType};
998 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
999 /// Media::Continuous(ContinuousType::Continuous62))
1000 /// .enable_auto_cut(3); // Cut after every 3 labels
1001 /// ```
1002 pub fn enable_auto_cut(self, size: u8) -> Self {
1003 Config {
1004 auto_cut: AutoCut::Enabled(size),
1005 ..self
1006 }
1007 }
1008
1009 /// Disable automatic cutting of labels.
1010 ///
1011 /// When disabled, labels will need to be manually torn or cut.
1012 ///
1013 /// # Example
1014 /// ```rust,no_run
1015 /// # use ptouch::{Config, Model, Media, ContinuousType};
1016 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
1017 /// Media::Continuous(ContinuousType::Continuous62))
1018 /// .disable_auto_cut();
1019 /// ```
1020 pub fn disable_auto_cut(self) -> Self {
1021 Config {
1022 auto_cut: AutoCut::Disabled,
1023 ..self
1024 }
1025 }
1026
1027 /// Control whether to cut the tape at the end of a print job.
1028 ///
1029 /// # Arguments
1030 /// * `flag` - `true` to cut at end, `false` to leave uncut
1031 ///
1032 /// # Example
1033 /// ```rust,no_run
1034 /// # use ptouch::{Config, Model, Media, ContinuousType};
1035 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
1036 /// Media::Continuous(ContinuousType::Continuous62))
1037 /// .cut_at_end(true); // Cut at the end of job
1038 /// ```
1039 pub fn cut_at_end(self, flag: bool) -> Self {
1040 Config {
1041 cut_at_end: flag,
1042 ..self
1043 }
1044 }
1045
1046 /// Enable or disable high resolution printing.
1047 ///
1048 /// High resolution doubles the vertical resolution from 300 DPI to 600 DPI.
1049 /// When enabled, image height should be doubled accordingly.
1050 ///
1051 /// # Arguments
1052 /// * `high` - `true` for 600 DPI, `false` for 300 DPI
1053 ///
1054 /// # Example
1055 /// ```rust,no_run
1056 /// # use ptouch::{Config, Model, Media, ContinuousType};
1057 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
1058 /// Media::Continuous(ContinuousType::Continuous62))
1059 /// .high_resolution(true); // Enable 600 DPI
1060 /// ```
1061 pub fn high_resolution(self, high: bool) -> Self {
1062 Config {
1063 high_resolution: high,
1064 ..self
1065 }
1066 }
1067
1068 /// Set the feeding length in dots.
1069 ///
1070 /// Controls how much tape is fed before printing starts.
1071 /// Different media types have different valid ranges.
1072 ///
1073 /// # Arguments
1074 /// * `feed` - Feed length in dots
1075 ///
1076 /// # Example
1077 /// ```rust,no_run
1078 /// # use ptouch::{Config, Model, Media, ContinuousType};
1079 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
1080 /// Media::Continuous(ContinuousType::Continuous62))
1081 /// .set_feed_in_dots(150); // Set feed to 150 dots
1082 /// ```
1083 pub fn set_feed_in_dots(self, feed: u16) -> Self {
1084 Config { feed, ..self }
1085 }
1086
1087 /// Enable or disable two-color printing (black and red).
1088 ///
1089 /// Only supported on QL-820NWB with compatible red/black tape.
1090 /// When enabled, use `print_two_color()` method instead of `print()`.
1091 ///
1092 /// # Arguments
1093 /// * `two_colors` - `true` to enable two-color printing
1094 ///
1095 /// # Example
1096 /// ```rust,no_run
1097 /// # use ptouch::{Config, Model, Media, ContinuousType};
1098 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
1099 /// Media::Continuous(ContinuousType::Continuous62Red))
1100 /// .two_colors(true); // Enable red and black printing
1101 /// ```
1102 pub fn two_colors(self, two_colors: bool) -> Self {
1103 Config { two_colors, ..self }
1104 }
1105
1106 /// Enable or disable data compression.
1107 ///
1108 /// Uses PackBits compression to reduce USB transfer size.
1109 /// Automatically disabled for QL-800 model due to hardware limitations.
1110 ///
1111 /// # Arguments
1112 /// * `flag` - `true` to enable compression
1113 ///
1114 /// # Example
1115 /// ```rust,no_run
1116 /// # use ptouch::{Config, Model, Media, ContinuousType};
1117 /// let config = Config::new(Model::QL820NWB, "serial".to_string(),
1118 /// Media::Continuous(ContinuousType::Continuous62))
1119 /// .compress(true); // Enable compression
1120 /// ```
1121 pub fn compress(self, flag: bool) -> Self {
1122 Config {
1123 compress: flag,
1124 ..self
1125 }
1126 }
1127
1128 fn build(self) -> Result<Vec<u8>, Error> {
1129 let mut buf: Vec<u8> = Vec::new();
1130
1131 // Set feeding values in dots
1132 {
1133 match self.media.check_feed_value(self.feed) {
1134 Ok(feed) => {
1135 buf.append(&mut [0x1B, 0x69, 0x64].to_vec());
1136 buf.append(&mut feed.to_vec());
1137 }
1138 Err(msg) => return Err(Error::InvalidConfig(msg)),
1139 }
1140 }
1141 // Set auto cut settings
1142 {
1143 let mut various_mode: u8 = 0b0000_0000;
1144 let mut auto_cut_num: u8 = 1;
1145
1146 if let AutoCut::Enabled(n) = self.auto_cut {
1147 various_mode = various_mode | 0b0100_0000;
1148 auto_cut_num = n;
1149 }
1150
1151 debug!("Auto-cut mode configured: {:#04x}", various_mode);
1152 debug!("Auto-cut frequency: {} pages", auto_cut_num);
1153
1154 buf.append(&mut [0x1B, 0x69, 0x4D, various_mode].to_vec()); // ESC i M : Set various mode
1155 buf.append(&mut [0x1B, 0x69, 0x41, auto_cut_num].to_vec()); // ESC i A : Set auto cut number
1156 }
1157 // Set expanded mode
1158 {
1159 let mut expanded_mode: u8 = 0b00000000;
1160
1161 if self.two_colors {
1162 expanded_mode = expanded_mode | 0b0000_0001;
1163 }
1164
1165 if self.cut_at_end {
1166 expanded_mode = expanded_mode | 0b0000_1000;
1167 };
1168
1169 if self.high_resolution {
1170 expanded_mode = expanded_mode | 0b0100_0000;
1171 }
1172
1173 debug!("Print mode settings: {:#04x}", expanded_mode);
1174
1175 buf.append(&mut [0x1B, 0x69, 0x4B, expanded_mode].to_vec()); // ESC i K : Set expanded mode
1176 }
1177 Ok(buf)
1178 }
1179}