@amitkhare/capacitor-cat-printer 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -229
- package/android/build.gradle +4 -0
- package/android/src/main/java/khare/catprinter/plugin/BarcodeGenerator.java +188 -0
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterCore.java +697 -14
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterPlugin.java +350 -4
- package/android/src/main/java/khare/catprinter/plugin/ImageProcessor.java +358 -24
- package/android/src/main/java/khare/catprinter/plugin/PrinterProtocol.java +92 -0
- package/dist/esm/definitions.d.ts +476 -1
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.js +30 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +13 -2
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +33 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +65 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +65 -0
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -48,10 +48,13 @@ public class CatPrinterCore {
|
|
|
48
48
|
|
|
49
49
|
private String connectedAddress;
|
|
50
50
|
private int paperWidth = PrinterProtocol.WIDTH_58MM;
|
|
51
|
+
private boolean dryRunMode = false;
|
|
52
|
+
private int connectionTimeout = 10000; // Default 10 seconds
|
|
51
53
|
|
|
52
54
|
private final AtomicBoolean isScanning = new AtomicBoolean(false);
|
|
53
55
|
private final AtomicBoolean isPaused = new AtomicBoolean(false);
|
|
54
56
|
private final AtomicBoolean isPrinting = new AtomicBoolean(false);
|
|
57
|
+
private final AtomicBoolean isCancelled = new AtomicBoolean(false);
|
|
55
58
|
|
|
56
59
|
private final ConcurrentLinkedQueue<byte[]> writeQueue = new ConcurrentLinkedQueue<>();
|
|
57
60
|
private final AtomicBoolean isWriting = new AtomicBoolean(false);
|
|
@@ -247,9 +250,21 @@ public class CatPrinterCore {
|
|
|
247
250
|
|
|
248
251
|
// ==================== CONNECTION ====================
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
private Runnable connectionTimeoutRunnable;
|
|
254
|
+
|
|
255
|
+
public void connect(String address, int width, int timeout, boolean dryRun, ConnectionCallback callback) {
|
|
251
256
|
this.connectionCallback = callback;
|
|
252
257
|
this.paperWidth = width;
|
|
258
|
+
this.connectionTimeout = timeout;
|
|
259
|
+
this.dryRunMode = dryRun;
|
|
260
|
+
|
|
261
|
+
// In dry-run mode, simulate connection
|
|
262
|
+
if (dryRun) {
|
|
263
|
+
Log.d(TAG, "Dry-run mode: simulating connection to " + address);
|
|
264
|
+
connectedAddress = address;
|
|
265
|
+
mainHandler.post(() -> callback.onConnected());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
253
268
|
|
|
254
269
|
if (bluetoothAdapter == null) {
|
|
255
270
|
callback.onError("Bluetooth not available");
|
|
@@ -267,10 +282,31 @@ public class CatPrinterCore {
|
|
|
267
282
|
return;
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
// Set up connection timeout
|
|
286
|
+
connectionTimeoutRunnable = () -> {
|
|
287
|
+
if (gatt != null && txCharacteristic == null) {
|
|
288
|
+
Log.e(TAG, "Connection timeout after " + timeout + "ms");
|
|
289
|
+
disconnect();
|
|
290
|
+
mainHandler.post(() -> callback.onError("Connection timeout"));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
mainHandler.postDelayed(connectionTimeoutRunnable, timeout);
|
|
294
|
+
|
|
270
295
|
gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
|
|
271
296
|
}
|
|
272
297
|
|
|
298
|
+
// Overload for backward compatibility
|
|
299
|
+
public void connect(String address, int width, ConnectionCallback callback) {
|
|
300
|
+
connect(address, width, 10000, false, callback);
|
|
301
|
+
}
|
|
302
|
+
|
|
273
303
|
public void disconnect() {
|
|
304
|
+
// Cancel connection timeout if pending
|
|
305
|
+
if (connectionTimeoutRunnable != null) {
|
|
306
|
+
mainHandler.removeCallbacks(connectionTimeoutRunnable);
|
|
307
|
+
connectionTimeoutRunnable = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
274
310
|
if (gatt != null) {
|
|
275
311
|
gatt.disconnect();
|
|
276
312
|
gatt.close();
|
|
@@ -278,14 +314,23 @@ public class CatPrinterCore {
|
|
|
278
314
|
}
|
|
279
315
|
txCharacteristic = null;
|
|
280
316
|
connectedAddress = null;
|
|
317
|
+
dryRunMode = false;
|
|
281
318
|
isPaused.set(false);
|
|
282
319
|
writeQueue.clear();
|
|
283
320
|
}
|
|
284
321
|
|
|
285
322
|
public boolean isConnected() {
|
|
323
|
+
// In dry-run mode, we're always "connected" if address is set
|
|
324
|
+
if (dryRunMode && connectedAddress != null) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
286
327
|
return gatt != null && txCharacteristic != null;
|
|
287
328
|
}
|
|
288
329
|
|
|
330
|
+
public boolean isDryRunMode() {
|
|
331
|
+
return dryRunMode;
|
|
332
|
+
}
|
|
333
|
+
|
|
289
334
|
public String getConnectedAddress() {
|
|
290
335
|
return connectedAddress;
|
|
291
336
|
}
|
|
@@ -319,6 +364,12 @@ public class CatPrinterCore {
|
|
|
319
364
|
BluetoothGattCharacteristic rx = service.getCharacteristic(PrinterProtocol.RX_CHAR_UUID);
|
|
320
365
|
|
|
321
366
|
if (tx != null && rx != null) {
|
|
367
|
+
// Cancel connection timeout
|
|
368
|
+
if (connectionTimeoutRunnable != null) {
|
|
369
|
+
mainHandler.removeCallbacks(connectionTimeoutRunnable);
|
|
370
|
+
connectionTimeoutRunnable = null;
|
|
371
|
+
}
|
|
372
|
+
|
|
322
373
|
txCharacteristic = tx;
|
|
323
374
|
connectedAddress = gatt.getDevice().getAddress();
|
|
324
375
|
|
|
@@ -390,6 +441,13 @@ public class CatPrinterCore {
|
|
|
390
441
|
}
|
|
391
442
|
|
|
392
443
|
private void processWriteQueue() {
|
|
444
|
+
// Check if cancelled - stop processing
|
|
445
|
+
if (isCancelled.get()) {
|
|
446
|
+
writeQueue.clear();
|
|
447
|
+
isWriting.set(false);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
393
451
|
if (isPaused.get() || isWriting.get() || writeQueue.isEmpty()) {
|
|
394
452
|
return;
|
|
395
453
|
}
|
|
@@ -423,6 +481,12 @@ public class CatPrinterCore {
|
|
|
423
481
|
int sleepInterval = 50;
|
|
424
482
|
|
|
425
483
|
while ((!writeQueue.isEmpty() || isWriting.get()) && waited < maxWaitMs) {
|
|
484
|
+
// Check if cancelled - stop waiting
|
|
485
|
+
if (isCancelled.get()) {
|
|
486
|
+
writeQueue.clear();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
426
490
|
try {
|
|
427
491
|
Thread.sleep(sleepInterval);
|
|
428
492
|
waited += sleepInterval;
|
|
@@ -445,7 +509,9 @@ public class CatPrinterCore {
|
|
|
445
509
|
// ==================== PRINTING ====================
|
|
446
510
|
|
|
447
511
|
public void printImage(String base64Image, float energy, int quality,
|
|
448
|
-
int feedAfter, int threshold, boolean dither,
|
|
512
|
+
int feedAfter, int threshold, boolean dither,
|
|
513
|
+
boolean flipH, boolean flipV, boolean compress,
|
|
514
|
+
PrintCallback callback) {
|
|
449
515
|
this.printCallback = callback;
|
|
450
516
|
|
|
451
517
|
if (!isConnected()) {
|
|
@@ -459,11 +525,18 @@ public class CatPrinterCore {
|
|
|
459
525
|
}
|
|
460
526
|
|
|
461
527
|
isPrinting.set(true);
|
|
528
|
+
isCancelled.set(false); // Reset cancel flag
|
|
462
529
|
|
|
463
530
|
new Thread(() -> {
|
|
464
531
|
try {
|
|
465
532
|
reportProgress(0, "processing", "Decoding image");
|
|
466
533
|
|
|
534
|
+
// Check for cancellation
|
|
535
|
+
if (isCancelled.get()) {
|
|
536
|
+
handleCancellation();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
467
540
|
// Decode base64 image
|
|
468
541
|
byte[] imageBytes = Base64.decode(base64Image, Base64.DEFAULT);
|
|
469
542
|
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
|
@@ -476,6 +549,12 @@ public class CatPrinterCore {
|
|
|
476
549
|
|
|
477
550
|
reportProgress(10, "processing", "Resizing image");
|
|
478
551
|
|
|
552
|
+
// Check for cancellation
|
|
553
|
+
if (isCancelled.get()) {
|
|
554
|
+
handleCancellation();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
479
558
|
// Resize to paper width
|
|
480
559
|
bitmap = ImageProcessor.resizeToWidth(bitmap, paperWidth);
|
|
481
560
|
|
|
@@ -492,12 +571,22 @@ public class CatPrinterCore {
|
|
|
492
571
|
}
|
|
493
572
|
int height = bitmap.getHeight();
|
|
494
573
|
|
|
495
|
-
|
|
574
|
+
// Apply flipping if requested
|
|
575
|
+
if (flipH) {
|
|
576
|
+
reportProgress(25, "processing", "Flipping horizontally");
|
|
577
|
+
ImageProcessor.flipHorizontal(monoData, paperWidth, height);
|
|
578
|
+
}
|
|
579
|
+
if (flipV) {
|
|
580
|
+
reportProgress(27, "processing", "Flipping vertically");
|
|
581
|
+
ImageProcessor.flipVertical(monoData, paperWidth, height);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
Log.d(TAG, "Printing " + height + " lines" + (dryRunMode ? " (DRY RUN)" : ""));
|
|
496
585
|
|
|
497
586
|
bitmap.recycle();
|
|
498
587
|
|
|
499
|
-
// Print the bitmap
|
|
500
|
-
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter);
|
|
588
|
+
// Print the bitmap (with compression flag for newer printers)
|
|
589
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter, compress);
|
|
501
590
|
|
|
502
591
|
} catch (Exception e) {
|
|
503
592
|
Log.e(TAG, "Print error", e);
|
|
@@ -507,9 +596,16 @@ public class CatPrinterCore {
|
|
|
507
596
|
}).start();
|
|
508
597
|
}
|
|
509
598
|
|
|
599
|
+
// Overload for backward compatibility
|
|
600
|
+
public void printImage(String base64Image, float energy, int quality,
|
|
601
|
+
int feedAfter, int threshold, boolean dither, PrintCallback callback) {
|
|
602
|
+
printImage(base64Image, energy, quality, feedAfter, threshold, dither, false, false, false, callback);
|
|
603
|
+
}
|
|
604
|
+
|
|
510
605
|
public void printText(String text, int fontSize, String align, boolean bold,
|
|
511
606
|
float lineSpacing, float energy, int quality,
|
|
512
|
-
int feedAfter,
|
|
607
|
+
int feedAfter, boolean wordWrap, boolean rtl,
|
|
608
|
+
PrintCallback callback) {
|
|
513
609
|
this.printCallback = callback;
|
|
514
610
|
|
|
515
611
|
if (!isConnected()) {
|
|
@@ -523,14 +619,21 @@ public class CatPrinterCore {
|
|
|
523
619
|
}
|
|
524
620
|
|
|
525
621
|
isPrinting.set(true);
|
|
622
|
+
isCancelled.set(false); // Reset cancel flag
|
|
526
623
|
|
|
527
624
|
new Thread(() -> {
|
|
528
625
|
try {
|
|
529
626
|
reportProgress(0, "processing", "Rendering text");
|
|
530
627
|
|
|
531
|
-
//
|
|
628
|
+
// Check for cancellation
|
|
629
|
+
if (isCancelled.get()) {
|
|
630
|
+
handleCancellation();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Render text to bitmap with word wrap and RTL support
|
|
532
635
|
Bitmap bitmap = ImageProcessor.renderText(text, paperWidth, fontSize,
|
|
533
|
-
align, bold, lineSpacing);
|
|
636
|
+
align, bold, lineSpacing, wordWrap, rtl);
|
|
534
637
|
|
|
535
638
|
reportProgress(20, "processing", "Converting to monochrome");
|
|
536
639
|
|
|
@@ -538,10 +641,12 @@ public class CatPrinterCore {
|
|
|
538
641
|
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
539
642
|
int height = bitmap.getHeight();
|
|
540
643
|
|
|
644
|
+
Log.d(TAG, "Printing text: " + height + " lines" + (dryRunMode ? " (DRY RUN)" : ""));
|
|
645
|
+
|
|
541
646
|
bitmap.recycle();
|
|
542
647
|
|
|
543
648
|
// Print the bitmap
|
|
544
|
-
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter);
|
|
649
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter, false);
|
|
545
650
|
|
|
546
651
|
} catch (Exception e) {
|
|
547
652
|
Log.e(TAG, "Print error", e);
|
|
@@ -551,13 +656,49 @@ public class CatPrinterCore {
|
|
|
551
656
|
}).start();
|
|
552
657
|
}
|
|
553
658
|
|
|
659
|
+
// Overload for backward compatibility
|
|
660
|
+
public void printText(String text, int fontSize, String align, boolean bold,
|
|
661
|
+
float lineSpacing, float energy, int quality,
|
|
662
|
+
int feedAfter, PrintCallback callback) {
|
|
663
|
+
printText(text, fontSize, align, bold, lineSpacing, energy, quality, feedAfter, true, false, callback);
|
|
664
|
+
}
|
|
665
|
+
|
|
554
666
|
private void printBitmapData(byte[] data, int width, int height,
|
|
555
|
-
float energy, int quality, int feedAfter) {
|
|
667
|
+
float energy, int quality, int feedAfter, boolean compress) {
|
|
556
668
|
try {
|
|
557
669
|
int bytesPerLine = width / 8;
|
|
558
670
|
int totalLines = height;
|
|
559
671
|
|
|
560
|
-
reportProgress(30, "printing", "Initializing printer");
|
|
672
|
+
reportProgress(30, "printing", dryRunMode ? "Simulating print..." : "Initializing printer");
|
|
673
|
+
|
|
674
|
+
// In dry-run mode, simulate the print process without sending data
|
|
675
|
+
if (dryRunMode) {
|
|
676
|
+
for (int line = 0; line < totalLines; line++) {
|
|
677
|
+
// Check for cancellation
|
|
678
|
+
if (isCancelled.get()) {
|
|
679
|
+
handleCancellation();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Simulate processing time
|
|
684
|
+
if (line % 100 == 0) {
|
|
685
|
+
Thread.sleep(10);
|
|
686
|
+
int progress = 30 + (int) ((line / (float) totalLines) * 60);
|
|
687
|
+
reportProgress(progress, "printing", "Simulating line " + line + "/" + totalLines);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
reportProgress(90, "feeding", "Simulation complete");
|
|
692
|
+
Thread.sleep(100);
|
|
693
|
+
|
|
694
|
+
isPrinting.set(false);
|
|
695
|
+
reportProgress(100, "done", "Dry run complete");
|
|
696
|
+
|
|
697
|
+
if (printCallback != null) {
|
|
698
|
+
mainHandler.post(() -> printCallback.onComplete());
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
561
702
|
|
|
562
703
|
// Calculate energy and speed values
|
|
563
704
|
// Reference: energy = int(args.energy * 0xffff)
|
|
@@ -565,12 +706,21 @@ public class CatPrinterCore {
|
|
|
565
706
|
// Reference: speed = 4 * (quality + 5), quality 1-4 -> speed 24-36
|
|
566
707
|
int speedValue = 4 * (quality + 5);
|
|
567
708
|
|
|
709
|
+
// Check if this is a new-kind printer that supports compression
|
|
710
|
+
String model = PrinterProtocol.detectModel(getConnectedDeviceName());
|
|
711
|
+
boolean isNewKind = PrinterProtocol.isNewKind(model);
|
|
712
|
+
boolean useCompression = compress && isNewKind;
|
|
713
|
+
|
|
568
714
|
// === PREPARE PHASE (matches reference _prepare method) ===
|
|
569
715
|
// 1. Get device state
|
|
570
716
|
queueWrite(PrinterProtocol.cmdGetDeviceState());
|
|
571
717
|
|
|
572
|
-
// 2. Start printing (use
|
|
573
|
-
|
|
718
|
+
// 2. Start printing (use new command for new-kind printers)
|
|
719
|
+
if (isNewKind) {
|
|
720
|
+
queueWrite(PrinterProtocol.cmdStartPrintingNew());
|
|
721
|
+
} else {
|
|
722
|
+
queueWrite(PrinterProtocol.cmdStartPrinting());
|
|
723
|
+
}
|
|
574
724
|
|
|
575
725
|
// 3. Set DPI
|
|
576
726
|
queueWrite(PrinterProtocol.cmdSetDpi200());
|
|
@@ -599,11 +749,22 @@ public class CatPrinterCore {
|
|
|
599
749
|
|
|
600
750
|
// === PRINT BITMAP DATA ===
|
|
601
751
|
for (int line = 0; line < totalLines; line++) {
|
|
752
|
+
// Check for cancellation
|
|
753
|
+
if (isCancelled.get()) {
|
|
754
|
+
handleCancellation();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
602
758
|
int offset = line * bytesPerLine;
|
|
603
759
|
byte[] lineData = new byte[bytesPerLine];
|
|
604
760
|
System.arraycopy(data, offset, lineData, 0, bytesPerLine);
|
|
605
761
|
|
|
606
|
-
|
|
762
|
+
if (useCompression) {
|
|
763
|
+
// Use compressed bitmap command for newer printers
|
|
764
|
+
queueWrite(PrinterProtocol.cmdDrawCompressedBitmap(lineData));
|
|
765
|
+
} else {
|
|
766
|
+
queueWrite(PrinterProtocol.cmdDrawBitmap(lineData));
|
|
767
|
+
}
|
|
607
768
|
|
|
608
769
|
// Report progress every 50 lines
|
|
609
770
|
if (line % 50 == 0) {
|
|
@@ -653,9 +814,531 @@ public class CatPrinterCore {
|
|
|
653
814
|
callback.onComplete();
|
|
654
815
|
}
|
|
655
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Cancel an ongoing print job
|
|
819
|
+
*/
|
|
820
|
+
public void cancelPrint(PrintCallback callback) {
|
|
821
|
+
Log.d(TAG, "Cancel requested, isPrinting=" + isPrinting.get());
|
|
822
|
+
|
|
823
|
+
// Always set cancelled flag and clear queue, even if not currently printing
|
|
824
|
+
isCancelled.set(true);
|
|
825
|
+
writeQueue.clear();
|
|
826
|
+
|
|
827
|
+
if (isPrinting.get()) {
|
|
828
|
+
// Let the print loop handle the cancellation
|
|
829
|
+
Log.d(TAG, "Cancelling active print job...");
|
|
830
|
+
} else {
|
|
831
|
+
// Reset the flag if nothing was printing
|
|
832
|
+
isCancelled.set(false);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
callback.onComplete();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
public void retractPaper(int pixels, PrintCallback callback) {
|
|
839
|
+
if (!isConnected()) {
|
|
840
|
+
callback.onError("Not connected to printer");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
queueWrite(PrinterProtocol.cmdRetractPaper(pixels));
|
|
845
|
+
callback.onComplete();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
public void getDeviceInfo(PrintCallback callback) {
|
|
849
|
+
if (!isConnected()) {
|
|
850
|
+
callback.onError("Not connected to printer");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Send device info command
|
|
855
|
+
queueWrite(PrinterProtocol.cmdGetDeviceInfo());
|
|
856
|
+
// Note: Response comes via onCharacteristicChanged, but we don't have
|
|
857
|
+
// a way to capture it yet. For now, just confirm command was sent.
|
|
858
|
+
callback.onComplete();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Get the connected device name for model detection
|
|
863
|
+
*/
|
|
864
|
+
public String getConnectedDeviceName() {
|
|
865
|
+
if (gatt != null && gatt.getDevice() != null) {
|
|
866
|
+
return gatt.getDevice().getName();
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Get detected printer model info
|
|
873
|
+
*/
|
|
874
|
+
public PrinterModelInfo getPrinterModel() {
|
|
875
|
+
String deviceName = getConnectedDeviceName();
|
|
876
|
+
String model = PrinterProtocol.detectModel(deviceName);
|
|
877
|
+
boolean isNewKind = PrinterProtocol.isNewKind(model);
|
|
878
|
+
boolean hasFeedingProblems = PrinterProtocol.hasFeedingProblems(model);
|
|
879
|
+
|
|
880
|
+
return new PrinterModelInfo(
|
|
881
|
+
model,
|
|
882
|
+
isNewKind,
|
|
883
|
+
hasFeedingProblems,
|
|
884
|
+
paperWidth,
|
|
885
|
+
deviceName != null ? deviceName : ""
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
public static class PrinterModelInfo {
|
|
890
|
+
public String model;
|
|
891
|
+
public boolean isNewKind;
|
|
892
|
+
public boolean hasFeedingProblems;
|
|
893
|
+
public int paperWidth;
|
|
894
|
+
public String deviceName;
|
|
895
|
+
|
|
896
|
+
public PrinterModelInfo(String model, boolean isNewKind, boolean hasFeedingProblems,
|
|
897
|
+
int paperWidth, String deviceName) {
|
|
898
|
+
this.model = model;
|
|
899
|
+
this.isNewKind = isNewKind;
|
|
900
|
+
this.hasFeedingProblems = hasFeedingProblems;
|
|
901
|
+
this.paperWidth = paperWidth;
|
|
902
|
+
this.deviceName = deviceName;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
656
906
|
private void reportProgress(int percent, String status, String message) {
|
|
657
907
|
if (printCallback != null) {
|
|
658
908
|
mainHandler.post(() -> printCallback.onProgress(percent, status, message));
|
|
659
909
|
}
|
|
660
910
|
}
|
|
911
|
+
|
|
912
|
+
private void handleCancellation() {
|
|
913
|
+
Log.d(TAG, "Print job cancelled");
|
|
914
|
+
writeQueue.clear();
|
|
915
|
+
isWriting.set(false);
|
|
916
|
+
isPrinting.set(false);
|
|
917
|
+
|
|
918
|
+
// Try to send stop command to printer (best effort)
|
|
919
|
+
if (gatt != null && txCharacteristic != null && !dryRunMode) {
|
|
920
|
+
try {
|
|
921
|
+
byte[] stopCmd = PrinterProtocol.cmdEndLattice();
|
|
922
|
+
txCharacteristic.setValue(stopCmd);
|
|
923
|
+
txCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
|
|
924
|
+
gatt.writeCharacteristic(txCharacteristic);
|
|
925
|
+
} catch (Exception e) {
|
|
926
|
+
Log.w(TAG, "Failed to send stop command", e);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
isCancelled.set(false);
|
|
931
|
+
|
|
932
|
+
reportProgress(0, "error", "Print cancelled");
|
|
933
|
+
|
|
934
|
+
if (printCallback != null) {
|
|
935
|
+
mainHandler.post(() -> printCallback.onError("Print cancelled by user"));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// ==================== NEW FEATURES ====================
|
|
940
|
+
|
|
941
|
+
// Auto-reconnect settings
|
|
942
|
+
private boolean autoReconnectEnabled = false;
|
|
943
|
+
private int autoReconnectMaxAttempts = 3;
|
|
944
|
+
private int autoReconnectDelayMs = 2000;
|
|
945
|
+
private int autoReconnectAttempts = 0;
|
|
946
|
+
private String lastConnectedAddress = null;
|
|
947
|
+
private int lastPaperWidth = PrinterProtocol.WIDTH_58MM;
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Set auto-reconnect options
|
|
951
|
+
*/
|
|
952
|
+
public void setAutoReconnect(boolean enabled, int maxAttempts, int delayMs) {
|
|
953
|
+
this.autoReconnectEnabled = enabled;
|
|
954
|
+
this.autoReconnectMaxAttempts = maxAttempts;
|
|
955
|
+
this.autoReconnectDelayMs = delayMs;
|
|
956
|
+
this.autoReconnectAttempts = 0;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Print text with full styling support
|
|
961
|
+
*/
|
|
962
|
+
public void printTextStyled(String text, int fontSize, String align, boolean bold,
|
|
963
|
+
boolean italic, boolean underline, boolean strikethrough,
|
|
964
|
+
float lineSpacing, float energy, int quality,
|
|
965
|
+
int feedAfter, boolean wordWrap, boolean rtl,
|
|
966
|
+
String fontPath, PrintCallback callback) {
|
|
967
|
+
this.printCallback = callback;
|
|
968
|
+
|
|
969
|
+
if (!isConnected()) {
|
|
970
|
+
callback.onError("Not connected to printer");
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (isPrinting.get()) {
|
|
975
|
+
callback.onError("Already printing");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
isPrinting.set(true);
|
|
980
|
+
isCancelled.set(false);
|
|
981
|
+
|
|
982
|
+
new Thread(() -> {
|
|
983
|
+
try {
|
|
984
|
+
reportProgress(0, "processing", "Rendering styled text");
|
|
985
|
+
|
|
986
|
+
if (isCancelled.get()) {
|
|
987
|
+
handleCancellation();
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
Bitmap bitmap = ImageProcessor.renderText(text, paperWidth, fontSize,
|
|
992
|
+
align, bold, italic, underline, strikethrough,
|
|
993
|
+
lineSpacing, wordWrap, rtl, fontPath);
|
|
994
|
+
|
|
995
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
996
|
+
|
|
997
|
+
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
998
|
+
int height = bitmap.getHeight();
|
|
999
|
+
|
|
1000
|
+
bitmap.recycle();
|
|
1001
|
+
|
|
1002
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter, false);
|
|
1003
|
+
|
|
1004
|
+
} catch (Exception e) {
|
|
1005
|
+
Log.e(TAG, "Print error", e);
|
|
1006
|
+
isPrinting.set(false);
|
|
1007
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
1008
|
+
}
|
|
1009
|
+
}).start();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Print rich text with mixed formatting (different styles per segment)
|
|
1014
|
+
*/
|
|
1015
|
+
public void printRichText(String[] texts, boolean[] bolds, boolean[] italics,
|
|
1016
|
+
boolean[] underlines, boolean[] strikethroughs, int[] fontSizes,
|
|
1017
|
+
int defaultFontSize, String align, float lineSpacing,
|
|
1018
|
+
float energy, int quality, int feedAfter, boolean rtl,
|
|
1019
|
+
String fontPath, PrintCallback callback) {
|
|
1020
|
+
this.printCallback = callback;
|
|
1021
|
+
|
|
1022
|
+
if (!isConnected()) {
|
|
1023
|
+
callback.onError("Not connected to printer");
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (isPrinting.get()) {
|
|
1028
|
+
callback.onError("Already printing");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
isPrinting.set(true);
|
|
1033
|
+
isCancelled.set(false);
|
|
1034
|
+
|
|
1035
|
+
new Thread(() -> {
|
|
1036
|
+
try {
|
|
1037
|
+
reportProgress(0, "processing", "Rendering rich text");
|
|
1038
|
+
|
|
1039
|
+
if (isCancelled.get()) {
|
|
1040
|
+
handleCancellation();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
Bitmap bitmap = ImageProcessor.renderRichText(texts, bolds, italics,
|
|
1045
|
+
underlines, strikethroughs, fontSizes, defaultFontSize,
|
|
1046
|
+
paperWidth, align, lineSpacing, rtl, fontPath);
|
|
1047
|
+
|
|
1048
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
1049
|
+
|
|
1050
|
+
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
1051
|
+
int height = bitmap.getHeight();
|
|
1052
|
+
|
|
1053
|
+
bitmap.recycle();
|
|
1054
|
+
|
|
1055
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter, false);
|
|
1056
|
+
|
|
1057
|
+
} catch (Exception e) {
|
|
1058
|
+
Log.e(TAG, "Rich text print error", e);
|
|
1059
|
+
isPrinting.set(false);
|
|
1060
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
1061
|
+
}
|
|
1062
|
+
}).start();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Print image with brightness/contrast/invert adjustments
|
|
1067
|
+
*/
|
|
1068
|
+
public void printImageAdjusted(String base64Image, float energy, int quality,
|
|
1069
|
+
int feedAfter, int threshold, boolean dither,
|
|
1070
|
+
boolean flipH, boolean flipV, boolean compress,
|
|
1071
|
+
int brightness, int contrast, boolean invert,
|
|
1072
|
+
PrintCallback callback) {
|
|
1073
|
+
this.printCallback = callback;
|
|
1074
|
+
|
|
1075
|
+
if (!isConnected()) {
|
|
1076
|
+
callback.onError("Not connected to printer");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (isPrinting.get()) {
|
|
1081
|
+
callback.onError("Already printing");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
isPrinting.set(true);
|
|
1086
|
+
isCancelled.set(false);
|
|
1087
|
+
|
|
1088
|
+
new Thread(() -> {
|
|
1089
|
+
try {
|
|
1090
|
+
reportProgress(0, "processing", "Decoding image");
|
|
1091
|
+
|
|
1092
|
+
if (isCancelled.get()) {
|
|
1093
|
+
handleCancellation();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
byte[] imageBytes = Base64.decode(base64Image, Base64.DEFAULT);
|
|
1098
|
+
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
|
1099
|
+
|
|
1100
|
+
if (bitmap == null) {
|
|
1101
|
+
throw new Exception("Failed to decode image");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
reportProgress(10, "processing", "Resizing image");
|
|
1105
|
+
bitmap = ImageProcessor.resizeToWidth(bitmap, paperWidth);
|
|
1106
|
+
|
|
1107
|
+
// Apply brightness/contrast adjustments
|
|
1108
|
+
if (brightness != 0 || contrast != 0) {
|
|
1109
|
+
reportProgress(15, "processing", "Adjusting brightness/contrast");
|
|
1110
|
+
bitmap = ImageProcessor.adjustBrightnessContrast(bitmap, brightness, contrast);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Apply invert
|
|
1114
|
+
if (invert) {
|
|
1115
|
+
reportProgress(17, "processing", "Inverting colors");
|
|
1116
|
+
bitmap = ImageProcessor.invertColors(bitmap);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
1120
|
+
|
|
1121
|
+
byte[] monoData;
|
|
1122
|
+
if (dither) {
|
|
1123
|
+
monoData = ImageProcessor.toMonochromeDithered(bitmap, threshold);
|
|
1124
|
+
} else {
|
|
1125
|
+
monoData = ImageProcessor.toMonochrome(bitmap, threshold);
|
|
1126
|
+
}
|
|
1127
|
+
int height = bitmap.getHeight();
|
|
1128
|
+
|
|
1129
|
+
if (flipH) {
|
|
1130
|
+
ImageProcessor.flipHorizontal(monoData, paperWidth, height);
|
|
1131
|
+
}
|
|
1132
|
+
if (flipV) {
|
|
1133
|
+
ImageProcessor.flipVertical(monoData, paperWidth, height);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
bitmap.recycle();
|
|
1137
|
+
|
|
1138
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter, compress);
|
|
1139
|
+
|
|
1140
|
+
} catch (Exception e) {
|
|
1141
|
+
Log.e(TAG, "Print error", e);
|
|
1142
|
+
isPrinting.set(false);
|
|
1143
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
1144
|
+
}
|
|
1145
|
+
}).start();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Print a barcode
|
|
1150
|
+
*/
|
|
1151
|
+
public void printBarcode(String data, String type, int height, boolean showText,
|
|
1152
|
+
String align, float energy, int feedAfter, PrintCallback callback) {
|
|
1153
|
+
this.printCallback = callback;
|
|
1154
|
+
|
|
1155
|
+
if (!isConnected()) {
|
|
1156
|
+
callback.onError("Not connected to printer");
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (isPrinting.get()) {
|
|
1161
|
+
callback.onError("Already printing");
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
isPrinting.set(true);
|
|
1166
|
+
isCancelled.set(false);
|
|
1167
|
+
|
|
1168
|
+
new Thread(() -> {
|
|
1169
|
+
try {
|
|
1170
|
+
reportProgress(0, "processing", "Generating barcode");
|
|
1171
|
+
|
|
1172
|
+
Bitmap bitmap = BarcodeGenerator.createAlignedBarcode(data, type, paperWidth,
|
|
1173
|
+
height, showText, align);
|
|
1174
|
+
|
|
1175
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
1176
|
+
|
|
1177
|
+
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
1178
|
+
int bitmapHeight = bitmap.getHeight();
|
|
1179
|
+
|
|
1180
|
+
bitmap.recycle();
|
|
1181
|
+
|
|
1182
|
+
printBitmapData(monoData, paperWidth, bitmapHeight, energy, 3, feedAfter, false);
|
|
1183
|
+
|
|
1184
|
+
} catch (Exception e) {
|
|
1185
|
+
Log.e(TAG, "Barcode print error", e);
|
|
1186
|
+
isPrinting.set(false);
|
|
1187
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
1188
|
+
}
|
|
1189
|
+
}).start();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Print a QR code
|
|
1194
|
+
*/
|
|
1195
|
+
public void printQRCode(String data, int size, String errorCorrection, String align,
|
|
1196
|
+
float energy, int feedAfter, PrintCallback callback) {
|
|
1197
|
+
this.printCallback = callback;
|
|
1198
|
+
|
|
1199
|
+
if (!isConnected()) {
|
|
1200
|
+
callback.onError("Not connected to printer");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (isPrinting.get()) {
|
|
1205
|
+
callback.onError("Already printing");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
isPrinting.set(true);
|
|
1210
|
+
isCancelled.set(false);
|
|
1211
|
+
|
|
1212
|
+
new Thread(() -> {
|
|
1213
|
+
try {
|
|
1214
|
+
reportProgress(0, "processing", "Generating QR code");
|
|
1215
|
+
|
|
1216
|
+
Bitmap bitmap = BarcodeGenerator.createAlignedQRCode(data, paperWidth, size,
|
|
1217
|
+
errorCorrection, align);
|
|
1218
|
+
|
|
1219
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
1220
|
+
|
|
1221
|
+
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
1222
|
+
int height = bitmap.getHeight();
|
|
1223
|
+
|
|
1224
|
+
bitmap.recycle();
|
|
1225
|
+
|
|
1226
|
+
printBitmapData(monoData, paperWidth, height, energy, 3, feedAfter, false);
|
|
1227
|
+
|
|
1228
|
+
} catch (Exception e) {
|
|
1229
|
+
Log.e(TAG, "QR code print error", e);
|
|
1230
|
+
isPrinting.set(false);
|
|
1231
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
1232
|
+
}
|
|
1233
|
+
}).start();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Print a formatted table
|
|
1238
|
+
*/
|
|
1239
|
+
public void printTable(String[][] rows, int[] columnWidths, String[] alignments,
|
|
1240
|
+
boolean[] boldRows, boolean showLines, String lineChar,
|
|
1241
|
+
int fontSize, float energy, int feedAfter, PrintCallback callback) {
|
|
1242
|
+
this.printCallback = callback;
|
|
1243
|
+
|
|
1244
|
+
if (!isConnected()) {
|
|
1245
|
+
callback.onError("Not connected to printer");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (isPrinting.get()) {
|
|
1250
|
+
callback.onError("Already printing");
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
isPrinting.set(true);
|
|
1255
|
+
isCancelled.set(false);
|
|
1256
|
+
|
|
1257
|
+
new Thread(() -> {
|
|
1258
|
+
try {
|
|
1259
|
+
reportProgress(0, "processing", "Rendering table");
|
|
1260
|
+
|
|
1261
|
+
Bitmap bitmap = ImageProcessor.renderTable(rows, columnWidths, alignments,
|
|
1262
|
+
boldRows, paperWidth, fontSize, showLines, lineChar);
|
|
1263
|
+
|
|
1264
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
1265
|
+
|
|
1266
|
+
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
1267
|
+
int height = bitmap.getHeight();
|
|
1268
|
+
|
|
1269
|
+
bitmap.recycle();
|
|
1270
|
+
|
|
1271
|
+
printBitmapData(monoData, paperWidth, height, energy, 3, feedAfter, false);
|
|
1272
|
+
|
|
1273
|
+
} catch (Exception e) {
|
|
1274
|
+
Log.e(TAG, "Table print error", e);
|
|
1275
|
+
isPrinting.set(false);
|
|
1276
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
1277
|
+
}
|
|
1278
|
+
}).start();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Get print preview as base64 image
|
|
1283
|
+
*/
|
|
1284
|
+
public PreviewResult getPreview(String type, Object options) {
|
|
1285
|
+
try {
|
|
1286
|
+
Bitmap bitmap = null;
|
|
1287
|
+
|
|
1288
|
+
// Generate bitmap based on type (simplified - actual implementation would parse options)
|
|
1289
|
+
// This is a placeholder - the actual implementation would be in the plugin layer
|
|
1290
|
+
|
|
1291
|
+
if (bitmap != null) {
|
|
1292
|
+
String base64 = ImageProcessor.bitmapToBase64(bitmap);
|
|
1293
|
+
int width = bitmap.getWidth();
|
|
1294
|
+
int height = bitmap.getHeight();
|
|
1295
|
+
bitmap.recycle();
|
|
1296
|
+
return new PreviewResult(base64, width, height);
|
|
1297
|
+
}
|
|
1298
|
+
} catch (Exception e) {
|
|
1299
|
+
Log.e(TAG, "Preview error", e);
|
|
1300
|
+
}
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
public static class PreviewResult {
|
|
1305
|
+
public String imageBase64;
|
|
1306
|
+
public int width;
|
|
1307
|
+
public int height;
|
|
1308
|
+
|
|
1309
|
+
public PreviewResult(String imageBase64, int width, int height) {
|
|
1310
|
+
this.imageBase64 = imageBase64;
|
|
1311
|
+
this.width = width;
|
|
1312
|
+
this.height = height;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Get printer status (battery, paper, firmware)
|
|
1318
|
+
* Note: Most cat printers don't report this info, so values may be null
|
|
1319
|
+
*/
|
|
1320
|
+
public PrinterStatus getPrinterStatus() {
|
|
1321
|
+
PrinterStatus status = new PrinterStatus();
|
|
1322
|
+
status.connected = isConnected();
|
|
1323
|
+
status.paperWidth = paperWidth;
|
|
1324
|
+
|
|
1325
|
+
// These would need to be populated from device responses
|
|
1326
|
+
// Most cat printers don't report battery/paper status
|
|
1327
|
+
status.batteryLevel = null;
|
|
1328
|
+
status.isCharging = null;
|
|
1329
|
+
status.hasPaper = null;
|
|
1330
|
+
status.firmwareVersion = null;
|
|
1331
|
+
|
|
1332
|
+
return status;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
public static class PrinterStatus {
|
|
1336
|
+
public boolean connected;
|
|
1337
|
+
public Integer batteryLevel;
|
|
1338
|
+
public Boolean isCharging;
|
|
1339
|
+
public Boolean hasPaper;
|
|
1340
|
+
public String firmwareVersion;
|
|
1341
|
+
public int paperWidth;
|
|
1342
|
+
public String rawData;
|
|
1343
|
+
}
|
|
661
1344
|
}
|