@nitra/zebra 7.0.0 → 7.0.1

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 CHANGED
@@ -18,19 +18,21 @@ npx cap sync
18
18
 
19
19
  ## API
20
20
 
21
- ### `print(value: string): Promise<boolean>`
21
+ ### `print(zpl: string | PrintOptions): Promise<{ sent?: boolean }>`
22
22
 
23
23
  Відправляє ZPL-команду на принтер Zebra.
24
24
 
25
- | Платформа | Поведінка |
26
- | ----------- | --------------------------------------------------------------------------------------------------------------- |
27
- | **Web** | Запитує порт через Web Serial API, відкриває з'єднання (9600 baud) і відправляє рядок ZPL на вибраний пристрій. |
28
- | **iOS** | Нативна реалізація (через плагін). |
29
- | **Android** | Нативна реалізація (через плагін). |
25
+ | Платформа | Поведінка |
26
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
+ | **Web** | Запитує порт через Web Serial API, відкриває з'єднання (9600 baud) і відправляє рядок ZPL на вибраний пристрій. |
28
+ | **iOS** | Нативна реалізація (через плагін). |
29
+ | **Android** | Відправка ZPL по **Bluetooth Classic (SPP)**. Потрібно спочатку викликати `setPrinterAddress({ address: "MAC" })` або передати `address` (Bluetooth MAC) у виклику. |
30
30
 
31
- **Параметри:**
31
+ **Параметри (об'єкт для Android):**
32
32
 
33
- - `value` — рядок з ZPL-командами (наприклад, `^XA^FO50,50^A0N,28,28^FDHello^FS^XZ`).
33
+ - `zpl` — рядок ZPL-команд (наприклад, `^XA^FO50,50^A0N,28,28^FDHello^FS^XZ`).
34
+ - `address` — (Android, опційно) Bluetooth MAC-адреса принтера (наприклад `00:11:22:33:44:55`); якщо не передано, використовується збережена через `setPrinterAddress()`.
35
+ - `port` — ігнорується (для сумісності API).
34
36
 
35
37
  **Приклад:**
36
38
 
@@ -41,6 +43,33 @@ const zpl = "^XA^FO50,50^A0N,28,28^FDHello World^FS^XZ";
41
43
  await Zebra.print(zpl);
42
44
  ```
43
45
 
46
+ ### `setPrinterAddress(options: { address: string, port?: number }): Promise<{ address }>` (Android)
47
+
48
+ Зберігає Bluetooth MAC-адресу принтера Zebra для подальших викликів `print(zpl)` без передачі адреси.
49
+
50
+ **Параметри:**
51
+
52
+ - `address` — Bluetooth MAC-адреса принтера (наприклад `00:11:22:33:44:55`).
53
+ - `port` — ігнорується (для сумісності API).
54
+
55
+ **Приклад (Android):**
56
+
57
+ ```javascript
58
+ await Zebra.setPrinterAddress({ address: "00:11:22:33:44:55" });
59
+ await Zebra.print(zpl);
60
+ ```
61
+
62
+ ### `getPairedDevices(): Promise<{ devices: { address: string, name: string }[] }>` (Android)
63
+
64
+ Повертає список спарених Bluetooth-пристроїв. Корисно для вибору принтера в налаштуваннях. На Android 12+ потрібен дозвіл **BLUETOOTH_CONNECT** (запитати до виклику).
65
+
66
+ **Приклад (Android):**
67
+
68
+ ```javascript
69
+ const { devices } = await Zebra.getPairedDevices();
70
+ // devices: [{ address: "00:11:22:33:44:55", name: "Zebra ZD420" }, ...]
71
+ ```
72
+
44
73
  ## Веб (Web Serial API)
45
74
 
46
75
  На веб-платформі плагін використовує Web Serial API:
@@ -51,6 +80,17 @@ await Zebra.print(zpl);
51
80
 
52
81
  **Обмеження:** Web Serial API працює лише в підтримуваних браузерах (Chrome, Edge, Opera) і зазвичай лише через HTTPS або localhost.
53
82
 
83
+ ## Android (Bluetooth Classic)
84
+
85
+ На Android плагін відправляє ZPL на принтер Zebra по **Bluetooth Classic** (профіль SPP):
86
+
87
+ 1. Спаріть принтер у налаштуваннях Bluetooth пристрою.
88
+ 2. Викличте `getPairedDevices()` щоб отримати список пристроїв (опційно) або введіть MAC-адресу вручну.
89
+ 3. Викличте `setPrinterAddress({ address: "MAC_ПРИНТЕРА" })` один раз (наприклад, з налаштувань) або передайте `address` у виклику `print({ zpl, address })`.
90
+ 4. Викличте `print(zpl)` або `print({ zpl })` — ZPL буде відправлено на принтер по Bluetooth.
91
+
92
+ Потрібні дозволи **BLUETOOTH**, **BLUETOOTH_ADMIN** (до API 30), **BLUETOOTH_CONNECT** (API 31+). На Android 12+ дозвіл `BLUETOOTH_CONNECT` потрібно запитати під час виконання (наприклад перед `getPairedDevices()` або `print()`).
93
+
54
94
  ## Структура пакету
55
95
 
56
96
  - `dist/` — зібраний JS-плагін (rolldown)
@@ -1,2 +1,5 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
3
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
4
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
2
5
  </manifest>
@@ -1,11 +1,227 @@
1
1
  package dev.nitra.zebra;
2
2
 
3
+ import android.Manifest;
4
+ import android.bluetooth.BluetoothAdapter;
5
+ import android.bluetooth.BluetoothDevice;
6
+ import android.bluetooth.BluetoothSocket;
7
+ import android.content.Context;
8
+ import android.content.SharedPreferences;
9
+ import android.content.pm.PackageManager;
10
+ import android.os.Build;
11
+
12
+ import androidx.core.app.ActivityCompat;
13
+
3
14
  import com.getcapacitor.Logger;
4
15
 
16
+ import java.io.IOException;
17
+ import java.io.OutputStream;
18
+ import java.nio.charset.StandardCharsets;
19
+ import java.util.ArrayList;
20
+ import java.util.List;
21
+ import java.util.Set;
22
+ import java.util.UUID;
23
+ import java.util.concurrent.ExecutorService;
24
+ import java.util.concurrent.Executors;
25
+
26
+ /**
27
+ * Реалізація друку ZPL на принтері Zebra через Bluetooth Classic (SPP).
28
+ * Зберігає MAC-адресу принтера та відправляє ZPL по SPP.
29
+ */
5
30
  public class Zebra {
6
31
 
7
- public String echo(String value) {
8
- Logger.info("Echo", value);
9
- return value;
32
+ /** UUID профілю SPP (Serial Port Profile) для Bluetooth Classic. */
33
+ private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
34
+
35
+ private static final String PREFS_NAME = "ZebraPrinter";
36
+ private static final String KEY_ADDRESS = "printer_address";
37
+
38
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
39
+
40
+ /**
41
+ * Зберігає Bluetooth MAC-адресу принтера для подальшого використання в print().
42
+ *
43
+ * @param address MAC-адреса пристрою (наприклад "00:11:22:33:44:55")
44
+ */
45
+ public void setPrinterAddress(Context context, String address, int port) {
46
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
47
+ String normalized = address != null ? address.trim() : "";
48
+ prefs.edit().putString(KEY_ADDRESS, normalized).apply();
49
+ Logger.info("Zebra", "Printer address saved: " + normalized);
50
+ }
51
+
52
+ /**
53
+ * Повертає збережену MAC-адресу принтера або null.
54
+ */
55
+ public String getStoredAddress(Context context) {
56
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
57
+ return prefs.getString(KEY_ADDRESS, null);
58
+ }
59
+
60
+ /**
61
+ * Повертає список спарених Bluetooth-пристроїв (для вибору принтера).
62
+ */
63
+ public List<BluetoothDeviceInfo> getPairedDevices(Context context) {
64
+ List<BluetoothDeviceInfo> result = new ArrayList<>();
65
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
66
+ if (adapter == null) {
67
+ return result;
68
+ }
69
+ if (!adapter.isEnabled()) {
70
+ return result;
71
+ }
72
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
73
+ return result;
74
+ }
75
+ Set<BluetoothDevice> paired = adapter.getBondedDevices();
76
+ if (paired == null) {
77
+ return result;
78
+ }
79
+ for (BluetoothDevice device : paired) {
80
+ String name = device.getName();
81
+ String address = device.getAddress();
82
+ if (address != null && !address.isEmpty()) {
83
+ result.add(new BluetoothDeviceInfo(
84
+ address,
85
+ name != null ? name : ""
86
+ ));
87
+ }
88
+ }
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * Відправляє ZPL на принтер Zebra по Bluetooth Classic (SPP).
94
+ *
95
+ * @param address MAC-адреса принтера (якщо null, використовується збережена адреса)
96
+ * @param port ігнорується (для сумісності API)
97
+ * @param zpl рядок ZPL для друку
98
+ * @param callback результат успіху або помилки
99
+ */
100
+ public void printZpl(Context context, String address, int port, String zpl, PrintCallback callback) {
101
+ final String addr = (address != null && !address.isEmpty()) ? address.trim() : getStoredAddress(context);
102
+
103
+ if (addr == null || addr.isEmpty()) {
104
+ callback.onError("ADDRESS_MISSING", new IllegalArgumentException("Адреса принтера не вказана. Викличте setPrinterAddress() з Bluetooth MAC або передайте address у print()."));
105
+ return;
106
+ }
107
+
108
+ String zplTrimmed = zpl != null ? zpl.trim() : "";
109
+ if (zplTrimmed.isEmpty()) {
110
+ callback.onError("ZPL_EMPTY", new IllegalArgumentException("ZPL команда порожня."));
111
+ return;
112
+ }
113
+
114
+ executor.execute(() -> {
115
+ BluetoothSocket socket = null;
116
+ OutputStream out = null;
117
+ try {
118
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
119
+ if (adapter == null) {
120
+ callback.onError("BLUETOOTH_NOT_SUPPORTED", new IllegalStateException("Bluetooth не підтримується на цьому пристрої."));
121
+ return;
122
+ }
123
+ if (!adapter.isEnabled()) {
124
+ callback.onError("BLUETOOTH_DISABLED", new IllegalStateException("Увімкніть Bluetooth в налаштуваннях."));
125
+ return;
126
+ }
127
+
128
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
129
+ callback.onError("PERMISSION_DENIED", new SecurityException("Відсутній дозвіл BLUETOOTH_CONNECT"));
130
+ return;
131
+ }
132
+
133
+ BluetoothDevice device;
134
+ try {
135
+ device = adapter.getRemoteDevice(addr);
136
+ } catch (IllegalArgumentException e) {
137
+ callback.onError("INVALID_ADDRESS", new IllegalArgumentException("Некоректна MAC-адреса: " + addr));
138
+ return;
139
+ }
140
+
141
+ try {
142
+ socket = device.createRfcommSocketToServiceRecord(SPP_UUID);
143
+ socket.connect();
144
+ } catch (IOException e) {
145
+ // Частина пристроїв (зокрема Zebra) потребує fallback через RFCOMM канал 1
146
+ Logger.info("Zebra", "SPP UUID connect failed, trying fallback RFCOMM channel 1");
147
+ if (socket != null) {
148
+ try {
149
+ socket.close();
150
+ } catch (IOException ignored) {
151
+ }
152
+ socket = null;
153
+ }
154
+ socket = createRfcommSocket(device, 1);
155
+ socket.connect();
156
+ }
157
+
158
+ out = socket.getOutputStream();
159
+ byte[] data = zplTrimmed.getBytes(StandardCharsets.UTF_8);
160
+ out.write(data);
161
+ out.flush();
162
+
163
+ Logger.info("Zebra", "ZPL sent successfully (" + data.length + " bytes) to " + addr);
164
+ callback.onSuccess(addr, data.length);
165
+ } catch (IOException e) {
166
+ Logger.error("Zebra", "Print failed: " + e.getMessage(), e);
167
+ callback.onError("CONNECTION_FAILED", e);
168
+ } finally {
169
+ if (out != null) {
170
+ try {
171
+ out.close();
172
+ } catch (IOException ignored) {
173
+ }
174
+ }
175
+ if (socket != null) {
176
+ try {
177
+ socket.close();
178
+ } catch (IOException ignored) {
179
+ }
180
+ }
181
+ }
182
+ });
183
+ }
184
+
185
+ @SuppressWarnings("JavaReflectionMemberAccess")
186
+ private static BluetoothSocket createRfcommSocket(BluetoothDevice device, int channel) throws IOException {
187
+ try {
188
+ return (BluetoothSocket) device.getClass()
189
+ .getMethod("createRfcommSocket", int.class)
190
+ .invoke(device, channel);
191
+ } catch (Exception e) {
192
+ throw new IOException("Fallback createRfcommSocket failed: " + e.getMessage(), e);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Інформація про Bluetooth-пристрій (для списку спарених).
198
+ */
199
+ public static class BluetoothDeviceInfo {
200
+ public final String address;
201
+ public final String name;
202
+
203
+ public BluetoothDeviceInfo(String address, String name) {
204
+ this.address = address;
205
+ this.name = name;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Колбек результату друку.
211
+ */
212
+ public interface PrintCallback {
213
+ /**
214
+ * Успішне відправлення даних.
215
+ * @param address MAC-адреса, на яку відправлено.
216
+ * @param bytesSent кількість відправлених байт.
217
+ */
218
+ void onSuccess(String address, int bytesSent);
219
+
220
+ /**
221
+ * Помилка друку.
222
+ * @param code Короткий код помилки.
223
+ * @param error Об'єкт виключення з деталями.
224
+ */
225
+ void onError(String code, Throwable error);
10
226
  }
11
227
  }
@@ -1,22 +1,128 @@
1
1
  package dev.nitra.zebra;
2
2
 
3
+ import android.Manifest;
3
4
  import com.getcapacitor.JSObject;
5
+ import com.getcapacitor.PermissionState;
4
6
  import com.getcapacitor.Plugin;
5
7
  import com.getcapacitor.PluginCall;
6
8
  import com.getcapacitor.PluginMethod;
7
9
  import com.getcapacitor.annotation.CapacitorPlugin;
10
+ import com.getcapacitor.annotation.Permission;
11
+ import com.getcapacitor.annotation.PermissionCallback;
8
12
 
9
- @CapacitorPlugin(name = "Zebra")
13
+ import org.json.JSONArray;
14
+ import org.json.JSONObject;
15
+
16
+ import java.util.List;
17
+
18
+ @CapacitorPlugin(
19
+ name = "Zebra",
20
+ permissions = {
21
+ @Permission(
22
+ strings = { Manifest.permission.BLUETOOTH_CONNECT },
23
+ alias = "bt_connect"
24
+ )
25
+ }
26
+ )
10
27
  public class ZebraPlugin extends Plugin {
11
28
 
12
- private Zebra implementation = new Zebra();
29
+ private final Zebra implementation = new Zebra();
30
+
31
+ @PluginMethod
32
+ public void setPrinterAddress(PluginCall call) {
33
+ String address = call.getString("address");
34
+ Integer port = call.getInt("port", 0);
35
+
36
+ if (address == null || address.trim().isEmpty()) {
37
+ call.reject("Параметр 'address' обов'язковий (Bluetooth MAC-адреса принтера).");
38
+ return;
39
+ }
40
+
41
+ implementation.setPrinterAddress(getContext(), address.trim(), port != null ? port : 0);
42
+ JSObject ret = new JSObject();
43
+ ret.put("address", address.trim());
44
+ call.resolve(ret);
45
+ }
13
46
 
14
47
  @PluginMethod
15
- public void echo(PluginCall call) {
16
- String value = call.getString("value");
48
+ public void getPairedDevices(PluginCall call) {
49
+ if (getPermissionState("bt_connect") != PermissionState.GRANTED) {
50
+ requestPermissionForAlias("bt_connect", call, "getPairedDevicesCallback");
51
+ } else {
52
+ loadPairedDevices(call);
53
+ }
54
+ }
55
+
56
+ @PermissionCallback
57
+ private void getPairedDevicesCallback(PluginCall call) {
58
+ if (getPermissionState("bt_connect") != PermissionState.GRANTED) {
59
+ call.reject("Permission is required to access Bluetooth devices.");
60
+ return;
61
+ }
62
+ loadPairedDevices(call);
63
+ }
17
64
 
65
+ void loadPairedDevices(PluginCall call) {
66
+ List<Zebra.BluetoothDeviceInfo> devices = implementation.getPairedDevices(getContext());
67
+ JSONArray arr = new JSONArray();
68
+ for (Zebra.BluetoothDeviceInfo d : devices) {
69
+ JSONObject o = new JSONObject();
70
+ try {
71
+ o.put("address", d.address);
72
+ o.put("name", d.name);
73
+ } catch (Exception ignored) {
74
+ }
75
+ arr.put(o);
76
+ }
18
77
  JSObject ret = new JSObject();
19
- ret.put("value", implementation.echo(value));
78
+ ret.put("devices", arr);
20
79
  call.resolve(ret);
21
80
  }
81
+
82
+ @PluginMethod
83
+ public void print(PluginCall call) {
84
+ // Підтримка лише print({ zpl, address?, port? })
85
+ String zpl = call.getString("zpl");
86
+ String address = call.getString("address");
87
+ Integer port = call.getInt("port", 0);
88
+
89
+ implementation.printZpl(
90
+ getContext(),
91
+ address,
92
+ port != null ? port : 0,
93
+ zpl != null ? zpl : "",
94
+ new Zebra.PrintCallback() {
95
+ @Override
96
+ public void onSuccess(String address, int bytesSent) {
97
+ runOnMainThread(() -> {
98
+ JSObject ret = new JSObject();
99
+ ret.put("success", true);
100
+ ret.put("address", address);
101
+ ret.put("bytesSent", bytesSent);
102
+ call.resolve(ret);
103
+ });
104
+ }
105
+
106
+ @Override
107
+ public void onError(String code, Throwable error) {
108
+ runOnMainThread(() -> {
109
+ JSObject ret = new JSObject();
110
+ ret.put("success", false);
111
+ ret.put("code", code);
112
+ ret.put("message", error.getMessage());
113
+ // Також робимо reject для стандартної обробки помилок Capacitor
114
+ call.reject(error.getMessage(), code, new Exception(error), ret);
115
+ });
116
+ }
117
+ }
118
+ );
119
+ }
120
+
121
+ private void runOnMainThread(Runnable runnable) {
122
+ if (getActivity() != null) {
123
+ getActivity().runOnUiThread(runnable);
124
+ } else {
125
+ runnable.run();
126
+ }
127
+ }
22
128
  }
@@ -0,0 +1,26 @@
1
+ //#region src/index.d.ts
2
+ type PairedDevice = {
3
+ address: string;
4
+ name: string;
5
+ };
6
+ type PrintResult = {
7
+ success?: boolean;
8
+ address?: string;
9
+ bytesSent?: number;
10
+ devices?: PairedDevice[];
11
+ };
12
+ interface ZebraPlugin {
13
+ print(zpl: string): Promise<PrintResult>;
14
+ setPrinterAddress(options: {
15
+ address: string;
16
+ port?: number;
17
+ }): Promise<{
18
+ address: string;
19
+ }>;
20
+ getPairedDevices(): Promise<{
21
+ devices: PairedDevice[];
22
+ }>;
23
+ }
24
+ declare const Zebra: ZebraPlugin;
25
+ //#endregion
26
+ export { PairedDevice, PrintResult, Zebra, ZebraPlugin };