@amitkhare/capacitor-cat-printer 0.5.0
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/LICENSE +21 -0
- package/README.md +324 -0
- package/android/build.gradle +44 -0
- package/android/src/main/AndroidManifest.xml +21 -0
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterCore.java +661 -0
- package/android/src/main/java/khare/catprinter/plugin/CatPrinterPlugin.java +348 -0
- package/android/src/main/java/khare/catprinter/plugin/ImageProcessor.java +213 -0
- package/android/src/main/java/khare/catprinter/plugin/PrinterProtocol.java +243 -0
- package/dist/esm/definitions.d.ts +189 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +8 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +13 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +28 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +51 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
package khare.catprinter.plugin;
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint;
|
|
4
|
+
import android.bluetooth.BluetoothAdapter;
|
|
5
|
+
import android.bluetooth.BluetoothDevice;
|
|
6
|
+
import android.bluetooth.BluetoothGatt;
|
|
7
|
+
import android.bluetooth.BluetoothGattCallback;
|
|
8
|
+
import android.bluetooth.BluetoothGattCharacteristic;
|
|
9
|
+
import android.bluetooth.BluetoothGattDescriptor;
|
|
10
|
+
import android.bluetooth.BluetoothGattService;
|
|
11
|
+
import android.bluetooth.BluetoothManager;
|
|
12
|
+
import android.bluetooth.BluetoothProfile;
|
|
13
|
+
import android.bluetooth.le.BluetoothLeScanner;
|
|
14
|
+
import android.bluetooth.le.ScanCallback;
|
|
15
|
+
import android.bluetooth.le.ScanResult;
|
|
16
|
+
import android.bluetooth.le.ScanSettings;
|
|
17
|
+
import android.content.Context;
|
|
18
|
+
import android.graphics.Bitmap;
|
|
19
|
+
import android.graphics.BitmapFactory;
|
|
20
|
+
import android.os.Handler;
|
|
21
|
+
import android.os.Looper;
|
|
22
|
+
import android.util.Base64;
|
|
23
|
+
import android.util.Log;
|
|
24
|
+
|
|
25
|
+
import java.util.ArrayList;
|
|
26
|
+
import java.util.List;
|
|
27
|
+
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
28
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Core BLE printer functionality.
|
|
32
|
+
* Handles scanning, connection, and printing operations.
|
|
33
|
+
*
|
|
34
|
+
* Note: BLE permissions are handled by CatPrinterPlugin before calling these methods.
|
|
35
|
+
*/
|
|
36
|
+
@SuppressLint("MissingPermission")
|
|
37
|
+
public class CatPrinterCore {
|
|
38
|
+
private static final String TAG = "CatPrinterCore";
|
|
39
|
+
private static final int MTU_SIZE = 200;
|
|
40
|
+
private static final int WRITE_DELAY_MS = 20;
|
|
41
|
+
|
|
42
|
+
private final Context context;
|
|
43
|
+
private final Handler mainHandler;
|
|
44
|
+
|
|
45
|
+
private BluetoothAdapter bluetoothAdapter;
|
|
46
|
+
private BluetoothGatt gatt;
|
|
47
|
+
private BluetoothGattCharacteristic txCharacteristic;
|
|
48
|
+
|
|
49
|
+
private String connectedAddress;
|
|
50
|
+
private int paperWidth = PrinterProtocol.WIDTH_58MM;
|
|
51
|
+
|
|
52
|
+
private final AtomicBoolean isScanning = new AtomicBoolean(false);
|
|
53
|
+
private final AtomicBoolean isPaused = new AtomicBoolean(false);
|
|
54
|
+
private final AtomicBoolean isPrinting = new AtomicBoolean(false);
|
|
55
|
+
|
|
56
|
+
private final ConcurrentLinkedQueue<byte[]> writeQueue = new ConcurrentLinkedQueue<>();
|
|
57
|
+
private final AtomicBoolean isWriting = new AtomicBoolean(false);
|
|
58
|
+
|
|
59
|
+
// Callbacks
|
|
60
|
+
private ScanResultCallback scanCallback;
|
|
61
|
+
private ConnectionCallback connectionCallback;
|
|
62
|
+
private PrintCallback printCallback;
|
|
63
|
+
|
|
64
|
+
public interface ScanResultCallback {
|
|
65
|
+
void onDeviceFound(String name, String address, int rssi);
|
|
66
|
+
void onScanComplete(List<BleDeviceInfo> devices);
|
|
67
|
+
void onScanError(String error);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public interface ConnectionCallback {
|
|
71
|
+
void onConnected();
|
|
72
|
+
void onDisconnected();
|
|
73
|
+
void onError(String error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public interface PrintCallback {
|
|
77
|
+
void onProgress(int percent, String status, String message);
|
|
78
|
+
void onComplete();
|
|
79
|
+
void onError(String error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public static class BleDeviceInfo {
|
|
83
|
+
public String name;
|
|
84
|
+
public String address;
|
|
85
|
+
public int rssi;
|
|
86
|
+
|
|
87
|
+
public BleDeviceInfo(String name, String address, int rssi) {
|
|
88
|
+
this.name = name;
|
|
89
|
+
this.address = address;
|
|
90
|
+
this.rssi = rssi;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public CatPrinterCore(Context context) {
|
|
95
|
+
this.context = context;
|
|
96
|
+
this.mainHandler = new Handler(Looper.getMainLooper());
|
|
97
|
+
initBluetooth();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private void initBluetooth() {
|
|
101
|
+
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
|
|
102
|
+
if (bluetoothManager != null) {
|
|
103
|
+
bluetoothAdapter = bluetoothManager.getAdapter();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private BluetoothLeScanner getScanner() {
|
|
108
|
+
if (bluetoothAdapter == null) {
|
|
109
|
+
initBluetooth();
|
|
110
|
+
}
|
|
111
|
+
if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
|
|
112
|
+
return bluetoothAdapter.getBluetoothLeScanner();
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ==================== SCANNING ====================
|
|
118
|
+
|
|
119
|
+
private ScanCallback activeScanCallback;
|
|
120
|
+
|
|
121
|
+
public void startScan(int durationMs, ScanResultCallback callback) {
|
|
122
|
+
this.scanCallback = callback;
|
|
123
|
+
|
|
124
|
+
if (bluetoothAdapter == null) {
|
|
125
|
+
initBluetooth();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
|
129
|
+
callback.onScanError("Bluetooth is not enabled. Please enable Bluetooth and try again.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
final BluetoothLeScanner bleScanner = getScanner();
|
|
134
|
+
if (bleScanner == null) {
|
|
135
|
+
callback.onScanError("BLE scanner not available. Please enable Bluetooth and try again.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isScanning.get()) {
|
|
140
|
+
stopScan();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
final List<BleDeviceInfo> foundDevices = new ArrayList<>();
|
|
144
|
+
|
|
145
|
+
activeScanCallback = new ScanCallback() {
|
|
146
|
+
@Override
|
|
147
|
+
public void onScanResult(int callbackType, ScanResult result) {
|
|
148
|
+
BluetoothDevice device = result.getDevice();
|
|
149
|
+
String name = device.getName();
|
|
150
|
+
String address = device.getAddress();
|
|
151
|
+
int rssi = result.getRssi();
|
|
152
|
+
|
|
153
|
+
// Show all devices - use address as name if no name available
|
|
154
|
+
if (name == null || name.isEmpty()) {
|
|
155
|
+
name = address;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if already in list
|
|
159
|
+
boolean exists = false;
|
|
160
|
+
synchronized (foundDevices) {
|
|
161
|
+
for (BleDeviceInfo info : foundDevices) {
|
|
162
|
+
if (info.address.equals(address)) {
|
|
163
|
+
exists = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!exists) {
|
|
169
|
+
BleDeviceInfo info = new BleDeviceInfo(name, address, rssi);
|
|
170
|
+
foundDevices.add(info);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!exists) {
|
|
175
|
+
final String finalName = name;
|
|
176
|
+
mainHandler.post(() -> callback.onDeviceFound(finalName, address, rssi));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Override
|
|
181
|
+
public void onScanFailed(int errorCode) {
|
|
182
|
+
isScanning.set(false);
|
|
183
|
+
activeScanCallback = null;
|
|
184
|
+
String errorMsg = "Scan failed: ";
|
|
185
|
+
switch (errorCode) {
|
|
186
|
+
case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
|
|
187
|
+
errorMsg += "Already started";
|
|
188
|
+
break;
|
|
189
|
+
case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
|
|
190
|
+
errorMsg += "App registration failed";
|
|
191
|
+
break;
|
|
192
|
+
case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
|
|
193
|
+
errorMsg += "Feature unsupported";
|
|
194
|
+
break;
|
|
195
|
+
case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
|
|
196
|
+
errorMsg += "Internal error";
|
|
197
|
+
break;
|
|
198
|
+
default:
|
|
199
|
+
errorMsg += "Error code " + errorCode;
|
|
200
|
+
}
|
|
201
|
+
final String finalErrorMsg = errorMsg;
|
|
202
|
+
mainHandler.post(() -> callback.onScanError(finalErrorMsg));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Use low latency scan settings for faster discovery
|
|
207
|
+
ScanSettings scanSettings = new ScanSettings.Builder()
|
|
208
|
+
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
|
209
|
+
.build();
|
|
210
|
+
|
|
211
|
+
isScanning.set(true);
|
|
212
|
+
Log.d(TAG, "Starting BLE scan with low latency mode...");
|
|
213
|
+
bleScanner.startScan(null, scanSettings, activeScanCallback);
|
|
214
|
+
|
|
215
|
+
// Stop scan after duration
|
|
216
|
+
mainHandler.postDelayed(() -> {
|
|
217
|
+
if (isScanning.get() && activeScanCallback != null) {
|
|
218
|
+
try {
|
|
219
|
+
bleScanner.stopScan(activeScanCallback);
|
|
220
|
+
} catch (Exception e) {
|
|
221
|
+
Log.w(TAG, "Error stopping scan", e);
|
|
222
|
+
}
|
|
223
|
+
isScanning.set(false);
|
|
224
|
+
activeScanCallback = null;
|
|
225
|
+
|
|
226
|
+
synchronized (foundDevices) {
|
|
227
|
+
callback.onScanComplete(new ArrayList<>(foundDevices));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}, durationMs);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public void stopScan() {
|
|
234
|
+
if (isScanning.get() && activeScanCallback != null) {
|
|
235
|
+
BluetoothLeScanner bleScanner = getScanner();
|
|
236
|
+
if (bleScanner != null) {
|
|
237
|
+
try {
|
|
238
|
+
bleScanner.stopScan(activeScanCallback);
|
|
239
|
+
} catch (Exception e) {
|
|
240
|
+
Log.w(TAG, "Error stopping scan", e);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
isScanning.set(false);
|
|
244
|
+
activeScanCallback = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ==================== CONNECTION ====================
|
|
249
|
+
|
|
250
|
+
public void connect(String address, int width, ConnectionCallback callback) {
|
|
251
|
+
this.connectionCallback = callback;
|
|
252
|
+
this.paperWidth = width;
|
|
253
|
+
|
|
254
|
+
if (bluetoothAdapter == null) {
|
|
255
|
+
callback.onError("Bluetooth not available");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Disconnect existing connection
|
|
260
|
+
if (gatt != null) {
|
|
261
|
+
disconnect();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
|
|
265
|
+
if (device == null) {
|
|
266
|
+
callback.onError("Device not found: " + address);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public void disconnect() {
|
|
274
|
+
if (gatt != null) {
|
|
275
|
+
gatt.disconnect();
|
|
276
|
+
gatt.close();
|
|
277
|
+
gatt = null;
|
|
278
|
+
}
|
|
279
|
+
txCharacteristic = null;
|
|
280
|
+
connectedAddress = null;
|
|
281
|
+
isPaused.set(false);
|
|
282
|
+
writeQueue.clear();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public boolean isConnected() {
|
|
286
|
+
return gatt != null && txCharacteristic != null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public String getConnectedAddress() {
|
|
290
|
+
return connectedAddress;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public int getPaperWidth() {
|
|
294
|
+
return paperWidth;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
|
|
298
|
+
@Override
|
|
299
|
+
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
|
300
|
+
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
301
|
+
Log.d(TAG, "Connected to GATT server");
|
|
302
|
+
gatt.discoverServices();
|
|
303
|
+
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
304
|
+
Log.d(TAG, "Disconnected from GATT server");
|
|
305
|
+
connectedAddress = null;
|
|
306
|
+
txCharacteristic = null;
|
|
307
|
+
if (connectionCallback != null) {
|
|
308
|
+
mainHandler.post(() -> connectionCallback.onDisconnected());
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@Override
|
|
314
|
+
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
|
315
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
316
|
+
// Find the printer service and characteristics
|
|
317
|
+
for (BluetoothGattService service : gatt.getServices()) {
|
|
318
|
+
BluetoothGattCharacteristic tx = service.getCharacteristic(PrinterProtocol.TX_CHAR_UUID);
|
|
319
|
+
BluetoothGattCharacteristic rx = service.getCharacteristic(PrinterProtocol.RX_CHAR_UUID);
|
|
320
|
+
|
|
321
|
+
if (tx != null && rx != null) {
|
|
322
|
+
txCharacteristic = tx;
|
|
323
|
+
connectedAddress = gatt.getDevice().getAddress();
|
|
324
|
+
|
|
325
|
+
// Enable notifications on RX for flow control
|
|
326
|
+
gatt.setCharacteristicNotification(rx, true);
|
|
327
|
+
BluetoothGattDescriptor descriptor = rx.getDescriptor(PrinterProtocol.CCCD_UUID);
|
|
328
|
+
if (descriptor != null) {
|
|
329
|
+
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
|
330
|
+
gatt.writeDescriptor(descriptor);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (connectionCallback != null) {
|
|
334
|
+
mainHandler.post(() -> connectionCallback.onConnected());
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Characteristics not found
|
|
341
|
+
if (connectionCallback != null) {
|
|
342
|
+
mainHandler.post(() -> connectionCallback.onError("Printer characteristics not found"));
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
if (connectionCallback != null) {
|
|
346
|
+
mainHandler.post(() -> connectionCallback.onError("Service discovery failed"));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@Override
|
|
352
|
+
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
|
353
|
+
byte[] data = characteristic.getValue();
|
|
354
|
+
if (data != null) {
|
|
355
|
+
if (PrinterProtocol.isFlowPause(data)) {
|
|
356
|
+
isPaused.set(true);
|
|
357
|
+
Log.d(TAG, "Flow control: PAUSE");
|
|
358
|
+
} else if (PrinterProtocol.isFlowResume(data)) {
|
|
359
|
+
isPaused.set(false);
|
|
360
|
+
Log.d(TAG, "Flow control: RESUME");
|
|
361
|
+
processWriteQueue();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@Override
|
|
367
|
+
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
|
368
|
+
isWriting.set(false);
|
|
369
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
370
|
+
processWriteQueue();
|
|
371
|
+
} else {
|
|
372
|
+
Log.e(TAG, "Write failed with status: " + status);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// ==================== WRITING ====================
|
|
378
|
+
|
|
379
|
+
private void queueWrite(byte[] data) {
|
|
380
|
+
// Split into MTU-sized chunks
|
|
381
|
+
int offset = 0;
|
|
382
|
+
while (offset < data.length) {
|
|
383
|
+
int chunkSize = Math.min(MTU_SIZE, data.length - offset);
|
|
384
|
+
byte[] chunk = new byte[chunkSize];
|
|
385
|
+
System.arraycopy(data, offset, chunk, 0, chunkSize);
|
|
386
|
+
writeQueue.add(chunk);
|
|
387
|
+
offset += chunkSize;
|
|
388
|
+
}
|
|
389
|
+
processWriteQueue();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private void processWriteQueue() {
|
|
393
|
+
if (isPaused.get() || isWriting.get() || writeQueue.isEmpty()) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (gatt == null || txCharacteristic == null) {
|
|
398
|
+
writeQueue.clear();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
byte[] data = writeQueue.poll();
|
|
403
|
+
if (data != null) {
|
|
404
|
+
isWriting.set(true);
|
|
405
|
+
txCharacteristic.setValue(data);
|
|
406
|
+
txCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
|
|
407
|
+
|
|
408
|
+
boolean success = gatt.writeCharacteristic(txCharacteristic);
|
|
409
|
+
if (!success) {
|
|
410
|
+
isWriting.set(false);
|
|
411
|
+
Log.e(TAG, "Failed to initiate write");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Small delay between writes
|
|
415
|
+
mainHandler.postDelayed(this::processWriteQueue, WRITE_DELAY_MS);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private void flushQueue() {
|
|
420
|
+
// Wait for queue to empty with timeout
|
|
421
|
+
int maxWaitMs = 60000; // 60 second timeout for long images
|
|
422
|
+
int waited = 0;
|
|
423
|
+
int sleepInterval = 50;
|
|
424
|
+
|
|
425
|
+
while ((!writeQueue.isEmpty() || isWriting.get()) && waited < maxWaitMs) {
|
|
426
|
+
try {
|
|
427
|
+
Thread.sleep(sleepInterval);
|
|
428
|
+
waited += sleepInterval;
|
|
429
|
+
|
|
430
|
+
// If paused, wait but don't count against timeout
|
|
431
|
+
if (isPaused.get()) {
|
|
432
|
+
waited = Math.max(0, waited - sleepInterval);
|
|
433
|
+
}
|
|
434
|
+
} catch (InterruptedException e) {
|
|
435
|
+
Thread.currentThread().interrupt();
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (waited >= maxWaitMs) {
|
|
441
|
+
Log.w(TAG, "Flush timeout - queue may not be empty");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ==================== PRINTING ====================
|
|
446
|
+
|
|
447
|
+
public void printImage(String base64Image, float energy, int quality,
|
|
448
|
+
int feedAfter, int threshold, boolean dither, PrintCallback callback) {
|
|
449
|
+
this.printCallback = callback;
|
|
450
|
+
|
|
451
|
+
if (!isConnected()) {
|
|
452
|
+
callback.onError("Not connected to printer");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isPrinting.get()) {
|
|
457
|
+
callback.onError("Already printing");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
isPrinting.set(true);
|
|
462
|
+
|
|
463
|
+
new Thread(() -> {
|
|
464
|
+
try {
|
|
465
|
+
reportProgress(0, "processing", "Decoding image");
|
|
466
|
+
|
|
467
|
+
// Decode base64 image
|
|
468
|
+
byte[] imageBytes = Base64.decode(base64Image, Base64.DEFAULT);
|
|
469
|
+
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
|
470
|
+
|
|
471
|
+
if (bitmap == null) {
|
|
472
|
+
throw new Exception("Failed to decode image");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
Log.d(TAG, "Original image size: " + bitmap.getWidth() + "x" + bitmap.getHeight());
|
|
476
|
+
|
|
477
|
+
reportProgress(10, "processing", "Resizing image");
|
|
478
|
+
|
|
479
|
+
// Resize to paper width
|
|
480
|
+
bitmap = ImageProcessor.resizeToWidth(bitmap, paperWidth);
|
|
481
|
+
|
|
482
|
+
Log.d(TAG, "Resized image size: " + bitmap.getWidth() + "x" + bitmap.getHeight());
|
|
483
|
+
|
|
484
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
485
|
+
|
|
486
|
+
// Convert to 1-bit monochrome (use dithering for better quality)
|
|
487
|
+
byte[] monoData;
|
|
488
|
+
if (dither) {
|
|
489
|
+
monoData = ImageProcessor.toMonochromeDithered(bitmap, threshold);
|
|
490
|
+
} else {
|
|
491
|
+
monoData = ImageProcessor.toMonochrome(bitmap, threshold);
|
|
492
|
+
}
|
|
493
|
+
int height = bitmap.getHeight();
|
|
494
|
+
|
|
495
|
+
Log.d(TAG, "Printing " + height + " lines");
|
|
496
|
+
|
|
497
|
+
bitmap.recycle();
|
|
498
|
+
|
|
499
|
+
// Print the bitmap
|
|
500
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter);
|
|
501
|
+
|
|
502
|
+
} catch (Exception e) {
|
|
503
|
+
Log.e(TAG, "Print error", e);
|
|
504
|
+
isPrinting.set(false);
|
|
505
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
506
|
+
}
|
|
507
|
+
}).start();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
public void printText(String text, int fontSize, String align, boolean bold,
|
|
511
|
+
float lineSpacing, float energy, int quality,
|
|
512
|
+
int feedAfter, PrintCallback callback) {
|
|
513
|
+
this.printCallback = callback;
|
|
514
|
+
|
|
515
|
+
if (!isConnected()) {
|
|
516
|
+
callback.onError("Not connected to printer");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (isPrinting.get()) {
|
|
521
|
+
callback.onError("Already printing");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
isPrinting.set(true);
|
|
526
|
+
|
|
527
|
+
new Thread(() -> {
|
|
528
|
+
try {
|
|
529
|
+
reportProgress(0, "processing", "Rendering text");
|
|
530
|
+
|
|
531
|
+
// Render text to bitmap
|
|
532
|
+
Bitmap bitmap = ImageProcessor.renderText(text, paperWidth, fontSize,
|
|
533
|
+
align, bold, lineSpacing);
|
|
534
|
+
|
|
535
|
+
reportProgress(20, "processing", "Converting to monochrome");
|
|
536
|
+
|
|
537
|
+
// Convert to 1-bit (use lower threshold for text - sharper)
|
|
538
|
+
byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
|
|
539
|
+
int height = bitmap.getHeight();
|
|
540
|
+
|
|
541
|
+
bitmap.recycle();
|
|
542
|
+
|
|
543
|
+
// Print the bitmap
|
|
544
|
+
printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter);
|
|
545
|
+
|
|
546
|
+
} catch (Exception e) {
|
|
547
|
+
Log.e(TAG, "Print error", e);
|
|
548
|
+
isPrinting.set(false);
|
|
549
|
+
mainHandler.post(() -> callback.onError(e.getMessage()));
|
|
550
|
+
}
|
|
551
|
+
}).start();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private void printBitmapData(byte[] data, int width, int height,
|
|
555
|
+
float energy, int quality, int feedAfter) {
|
|
556
|
+
try {
|
|
557
|
+
int bytesPerLine = width / 8;
|
|
558
|
+
int totalLines = height;
|
|
559
|
+
|
|
560
|
+
reportProgress(30, "printing", "Initializing printer");
|
|
561
|
+
|
|
562
|
+
// Calculate energy and speed values
|
|
563
|
+
// Reference: energy = int(args.energy * 0xffff)
|
|
564
|
+
int energyValue = (int) (energy * 0xffff);
|
|
565
|
+
// Reference: speed = 4 * (quality + 5), quality 1-4 -> speed 24-36
|
|
566
|
+
int speedValue = 4 * (quality + 5);
|
|
567
|
+
|
|
568
|
+
// === PREPARE PHASE (matches reference _prepare method) ===
|
|
569
|
+
// 1. Get device state
|
|
570
|
+
queueWrite(PrinterProtocol.cmdGetDeviceState());
|
|
571
|
+
|
|
572
|
+
// 2. Start printing (use standard command, not new)
|
|
573
|
+
queueWrite(PrinterProtocol.cmdStartPrinting());
|
|
574
|
+
|
|
575
|
+
// 3. Set DPI
|
|
576
|
+
queueWrite(PrinterProtocol.cmdSetDpi200());
|
|
577
|
+
|
|
578
|
+
// 4. Set speed (if specified)
|
|
579
|
+
if (speedValue > 0) {
|
|
580
|
+
queueWrite(PrinterProtocol.cmdSetSpeed(speedValue));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 5. Set energy (if specified)
|
|
584
|
+
if (energyValue > 0) {
|
|
585
|
+
queueWrite(PrinterProtocol.cmdSetEnergy(energyValue));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 6. Apply energy
|
|
589
|
+
queueWrite(PrinterProtocol.cmdApplyEnergy());
|
|
590
|
+
|
|
591
|
+
// 7. Update device
|
|
592
|
+
queueWrite(PrinterProtocol.cmdUpdateDevice());
|
|
593
|
+
|
|
594
|
+
// 8. Flush before starting lattice (important!)
|
|
595
|
+
flushQueue();
|
|
596
|
+
|
|
597
|
+
// 9. Start lattice
|
|
598
|
+
queueWrite(PrinterProtocol.cmdStartLattice());
|
|
599
|
+
|
|
600
|
+
// === PRINT BITMAP DATA ===
|
|
601
|
+
for (int line = 0; line < totalLines; line++) {
|
|
602
|
+
int offset = line * bytesPerLine;
|
|
603
|
+
byte[] lineData = new byte[bytesPerLine];
|
|
604
|
+
System.arraycopy(data, offset, lineData, 0, bytesPerLine);
|
|
605
|
+
|
|
606
|
+
queueWrite(PrinterProtocol.cmdDrawBitmap(lineData));
|
|
607
|
+
|
|
608
|
+
// Report progress every 50 lines
|
|
609
|
+
if (line % 50 == 0) {
|
|
610
|
+
int progress = 30 + (int) ((line / (float) totalLines) * 60);
|
|
611
|
+
reportProgress(progress, "printing", "Printing line " + line + "/" + totalLines);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
reportProgress(90, "feeding", "Finishing");
|
|
616
|
+
|
|
617
|
+
// === FINISH PHASE (matches reference _finish method) ===
|
|
618
|
+
// 1. End lattice
|
|
619
|
+
queueWrite(PrinterProtocol.cmdEndLattice());
|
|
620
|
+
|
|
621
|
+
// 2. Set speed to 8 for feeding
|
|
622
|
+
queueWrite(PrinterProtocol.cmdSetSpeed(8));
|
|
623
|
+
|
|
624
|
+
// 3. Feed paper
|
|
625
|
+
queueWrite(PrinterProtocol.cmdFeedPaper(feedAfter));
|
|
626
|
+
|
|
627
|
+
// 4. Get device state
|
|
628
|
+
queueWrite(PrinterProtocol.cmdGetDeviceState());
|
|
629
|
+
|
|
630
|
+
// 5. Final flush
|
|
631
|
+
flushQueue();
|
|
632
|
+
|
|
633
|
+
isPrinting.set(false);
|
|
634
|
+
reportProgress(100, "done", "Complete");
|
|
635
|
+
|
|
636
|
+
if (printCallback != null) {
|
|
637
|
+
mainHandler.post(() -> printCallback.onComplete());
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
} catch (Exception e) {
|
|
641
|
+
isPrinting.set(false);
|
|
642
|
+
throw new RuntimeException(e);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
public void feedPaper(int pixels, PrintCallback callback) {
|
|
647
|
+
if (!isConnected()) {
|
|
648
|
+
callback.onError("Not connected to printer");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
queueWrite(PrinterProtocol.cmdFeedPaper(pixels));
|
|
653
|
+
callback.onComplete();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private void reportProgress(int percent, String status, String message) {
|
|
657
|
+
if (printCallback != null) {
|
|
658
|
+
mainHandler.post(() -> printCallback.onProgress(percent, status, message));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|