@delicity/capacitor-thermal-printer 7.0.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/DelicityThermalPrinter.podspec +28 -0
- package/LICENSE +21 -0
- package/README.md +649 -0
- package/android/build.gradle +122 -0
- package/android/src/main/AndroidManifest.xml +38 -0
- package/android/src/main/java/com/delicity/thermalprinter/Logger.kt +50 -0
- package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterEngine.kt +528 -0
- package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterPlugin.kt +334 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/BleAdapter.kt +125 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/BrotherAdapter.kt +206 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EpsonAdapter.kt +384 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosAdapter.kt +160 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosCommands.kt +42 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosTextEncoder.kt +138 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/PrinterAdapter.kt +95 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/RawTcpAdapter.kt +96 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkContract.kt +158 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkReflect.kt +104 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/StarAdapter.kt +322 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/UsbAdapter.kt +248 -0
- package/android/src/main/java/com/delicity/thermalprinter/adapters/ZebraAdapter.kt +207 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/AdapterPriority.kt +39 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/BleScanner.kt +70 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/BluetoothClassicScanner.kt +112 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/DiscoveryManager.kt +136 -0
- package/android/src/main/java/com/delicity/thermalprinter/discovery/TcpScanner.kt +96 -0
- package/android/src/main/java/com/delicity/thermalprinter/image/ImageCache.kt +88 -0
- package/android/src/main/java/com/delicity/thermalprinter/image/ImageProcessor.kt +220 -0
- package/android/src/main/java/com/delicity/thermalprinter/image/TextRasterizer.kt +99 -0
- package/android/src/main/java/com/delicity/thermalprinter/model/Models.kt +206 -0
- package/android/src/main/java/com/delicity/thermalprinter/model/PrintItem.kt +100 -0
- package/android/src/main/java/com/delicity/thermalprinter/store/PrinterStore.kt +71 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/BleGattClient.kt +201 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/BluetoothSppTransport.kt +110 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/ByteTransport.kt +18 -0
- package/android/src/main/java/com/delicity/thermalprinter/transport/TcpTransport.kt +83 -0
- package/dist/esm/adapters/dedup.d.ts +26 -0
- package/dist/esm/adapters/dedup.js +66 -0
- package/dist/esm/adapters/dedup.js.map +1 -0
- package/dist/esm/adapters/priority.d.ts +29 -0
- package/dist/esm/adapters/priority.js +55 -0
- package/dist/esm/adapters/priority.js.map +1 -0
- package/dist/esm/core/enums.d.ts +61 -0
- package/dist/esm/core/enums.js +25 -0
- package/dist/esm/core/enums.js.map +1 -0
- package/dist/esm/core/errors.d.ts +16 -0
- package/dist/esm/core/errors.js +53 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/escpos-text.d.ts +33 -0
- package/dist/esm/core/escpos-text.js +239 -0
- package/dist/esm/core/escpos-text.js.map +1 -0
- package/dist/esm/core/imaging.d.ts +91 -0
- package/dist/esm/core/imaging.js +184 -0
- package/dist/esm/core/imaging.js.map +1 -0
- package/dist/esm/core/models.d.ts +131 -0
- package/dist/esm/core/models.js +2 -0
- package/dist/esm/core/models.js.map +1 -0
- package/dist/esm/core/options.d.ts +154 -0
- package/dist/esm/core/options.js +2 -0
- package/dist/esm/core/options.js.map +1 -0
- package/dist/esm/core/text.d.ts +138 -0
- package/dist/esm/core/text.js +14 -0
- package/dist/esm/core/text.js.map +1 -0
- package/dist/esm/definitions.d.ts +155 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +63 -0
- package/dist/esm/web.js +112 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +224 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +227 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Adapters/BrotherAdapter.swift +139 -0
- package/ios/Plugin/Adapters/EpsonAdapter.swift +131 -0
- package/ios/Plugin/Adapters/EscPosAdapter.swift +106 -0
- package/ios/Plugin/Adapters/EscPosCommands.swift +32 -0
- package/ios/Plugin/Adapters/EscPosTextEncoder.swift +115 -0
- package/ios/Plugin/Adapters/PrinterAdapter.swift +44 -0
- package/ios/Plugin/Adapters/RawTcpAdapter.swift +70 -0
- package/ios/Plugin/Adapters/StarAdapter.swift +305 -0
- package/ios/Plugin/Adapters/ZebraAdapter.swift +119 -0
- package/ios/Plugin/Discovery/AdapterPriority.swift +21 -0
- package/ios/Plugin/Discovery/BonjourScanner.swift +51 -0
- package/ios/Plugin/Discovery/DiscoveryManager.swift +86 -0
- package/ios/Plugin/Image/ImageCache.swift +73 -0
- package/ios/Plugin/Image/ImageProcessor.swift +168 -0
- package/ios/Plugin/Image/TextRasterizer.swift +81 -0
- package/ios/Plugin/Logger.swift +33 -0
- package/ios/Plugin/Model/Models.swift +174 -0
- package/ios/Plugin/Model/PrintItem.swift +111 -0
- package/ios/Plugin/Store/PrinterStore.swift +51 -0
- package/ios/Plugin/ThermalPrinterEngine.swift +395 -0
- package/ios/Plugin/ThermalPrinterPlugin.m +22 -0
- package/ios/Plugin/ThermalPrinterPlugin.swift +258 -0
- package/ios/Plugin/Transport/TcpTransport.swift +89 -0
- package/package.json +96 -0
package/README.md
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# @delicity/capacitor-thermal-printer
|
|
2
|
+
|
|
3
|
+
> Capacitor **7** plugin for **thermal / receipt / label printing** — **multi-brand** (ESC/POS, Epson, Star, Brother, Zebra), **image-based**, with **aggregated discovery**, **deduplication**, **automatic reconnection** and a **single JavaScript API**.
|
|
4
|
+
|
|
5
|
+
For **any Capacitor app that needs to print** — point of sale, receipts, order tickets, shipping/label printing, kitchen slips, etc. The user taps "Add a printer", picks one from a list, runs a test print, and never has to touch the phone's Bluetooth/Wi-Fi settings again.
|
|
6
|
+
|
|
7
|
+
**Requires Capacitor 7** · Android (`compileSdk 35`, JDK 21) · iOS 14+ / Xcode 16+.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
1. [Philosophy](#philosophy)
|
|
14
|
+
2. [Architecture](#architecture)
|
|
15
|
+
3. [Installation](#installation)
|
|
16
|
+
4. [Manufacturer SDKs](#manufacturer-sdks)
|
|
17
|
+
5. [Permissions](#permissions)
|
|
18
|
+
6. [Public API](#public-api)
|
|
19
|
+
7. [Types](#types)
|
|
20
|
+
8. [Image printing flow](#image-printing-flow)
|
|
21
|
+
9. [Image processing](#image-processing)
|
|
22
|
+
10. [Aggregated discovery & adapter priority](#aggregated-discovery--adapter-priority)
|
|
23
|
+
11. [Default printer & reconnection](#default-printer--reconnection)
|
|
24
|
+
12. [Normalized errors](#normalized-errors)
|
|
25
|
+
13. [Android / iOS differences](#android--ios-differences)
|
|
26
|
+
14. [Image cache & logs/diagnostics](#image-cache--logsdiagnostics)
|
|
27
|
+
15. [Full example](#full-example)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Philosophy
|
|
32
|
+
|
|
33
|
+
- **The app generates an image of what to print** (PNG/bitmap — receipt, ticket, label). It never sends structured text to the SDKs.
|
|
34
|
+
- The plugin **receives an image**, **normalizes** it (resize → grayscale → 1-bit + dithering), **converts** it to the adapter's format, and **sends** it.
|
|
35
|
+
- **One JS API.** Internally, an **adapter-based architecture** routes to the right implementation.
|
|
36
|
+
- **There is no universal protocol**: each family has its adapter. Adapter priority guarantees the best choice.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ App (Ionic/JS/TS) │
|
|
43
|
+
│ discoverPrinters / connect / setDefault / printImage ... │
|
|
44
|
+
└───────────────────────────────┬───────────────────────────────┘
|
|
45
|
+
│ Single API (definitions.ts)
|
|
46
|
+
┌───────────────┴───────────────┐
|
|
47
|
+
│ Capacitor Bridge │
|
|
48
|
+
┌────────┴─────────┐ ┌──────────┴─────────┐
|
|
49
|
+
│ Android (Kotlin) │ │ iOS (Swift) │
|
|
50
|
+
│ ThermalPrinter… │ │ ThermalPrinter… │
|
|
51
|
+
└────────┬─────────┘ └──────────┬─────────┘
|
|
52
|
+
│ ThermalPrinterEngine │ ThermalPrinterEngine
|
|
53
|
+
┌─────────────┼──────────────┐ ┌───────────┼──────────────┐
|
|
54
|
+
│ Discovery │ Adapters │ │ Discovery │ Adapters │
|
|
55
|
+
│ Manager │ (registry) │ │ Manager │ (registry) │
|
|
56
|
+
└─────────────┴──────────────┘ └───────────┴──────────────┘
|
|
57
|
+
│ │
|
|
58
|
+
┌───────┴────────────────────────────────────┴─────────┐
|
|
59
|
+
│ EscPos · Epson · Star · Brother · Zebra · RawTcp · BLE │
|
|
60
|
+
│ Transport: TCP9100 / SPP(Android) / NWConnection(iOS) │
|
|
61
|
+
│ Image: decode → resize → grayscale → dither → raster │
|
|
62
|
+
│ Store: profiles + default printer (persisted) │
|
|
63
|
+
└────────────────────────────────────────────────────────┘
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> 📁 Repo layout, internal architecture, tests and the contribution guide live in
|
|
67
|
+
> [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install @delicity/capacitor-thermal-printer
|
|
73
|
+
npx cap sync
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Capacitor 7 requirements**: Android `compileSdk 35` / JDK 21 ; iOS 14+ / Xcode 16+.
|
|
77
|
+
|
|
78
|
+
## Manufacturer SDKs
|
|
79
|
+
|
|
80
|
+
The plugin supports **Star, Epson, Brother, Zebra** via their native SDK, **optionally**:
|
|
81
|
+
it compiles and works **without any SDK** (generic ESC/POS over TCP/Bluetooth/USB/BLE),
|
|
82
|
+
and each brand **activates automatically** as soon as its binary is present.
|
|
83
|
+
|
|
84
|
+
> **Why isn't it 100% automatic on `npm install`?**
|
|
85
|
+
> Only SDKs published to a standard package repository download by themselves.
|
|
86
|
+
> The others are distributed only through the manufacturer's portal and their
|
|
87
|
+
> **license forbids redistribution** — so they cannot be put on Maven Central /
|
|
88
|
+
> CocoaPods, nor committed here. The consuming app downloads the binary itself
|
|
89
|
+
> (accepting the license); the plugin provides all the code to use it.
|
|
90
|
+
|
|
91
|
+
| Brand | Android | iOS | What to do in the app |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| **Star** | ✅ auto (Maven Central) | ✅ auto (SPM) | Add the `StarXpand-SDK-iOS` SPM package (iOS). Android: nothing. |
|
|
94
|
+
| **Brother** | ⛔ manual `.aar` | ✅ auto (CocoaPods) | `pod 'BRLMPrinterKit'` (iOS); drop `BrotherPrintLibrary.aar` (Android). |
|
|
95
|
+
| **Epson** | ⛔ manual `.jar`+`.so` | ⛔ manual xcframework | Drop `ePOS2.jar` (Android) / `libepos2.xcframework` (iOS). |
|
|
96
|
+
| **Zebra** | ⚠️ private Maven (token) or `.jar` | ⛔ manual xcframework | Zebra token or `ZSDK_ANDROID_API.jar`; `ZSDK_API.xcframework` (iOS). |
|
|
97
|
+
|
|
98
|
+
**Official download links:**
|
|
99
|
+
- **Star**: [StarXpand-SDK-Android](https://github.com/star-micronics/StarXpand-SDK-Android) · [StarXpand-SDK-iOS](https://github.com/star-micronics/StarXpand-SDK-iOS) (nothing to download: Maven Central / SPM)
|
|
100
|
+
- **Epson**: [Epson Developers](https://epson.com/developers-products) · [MFi / ePOS SDK](https://global.epson.com/products_and_drivers/tm/en/mfi.html)
|
|
101
|
+
- **Brother**: [Mobile SDK (download)](https://support.brother.com/g/s/es/dev/en/mobilesdk/download/index.html) · US: [Brother Developer Program](https://developerprogram.brother-usa.com/sdk-download) · iOS pod: [BRLMPrinterKit](https://cocoapods.org/pods/BRLMPrinterKit)
|
|
102
|
+
- **Zebra**: [Link-OS Multiplatform SDK](https://developer.zebra.com/products/printers/link-os-multiplatform-sdk) · [Downloads & support](https://www.zebra.com/us/en/support-downloads/software/printer-software/link-os-multiplatform-sdk.html)
|
|
103
|
+
|
|
104
|
+
> Epson / Brother / Zebra: a free developer account + license acceptance are required.
|
|
105
|
+
|
|
106
|
+
Step-by-step setup (where to drop each binary, Zebra private Maven repo, iOS module
|
|
107
|
+
names, git-ignored test folder): **[`docs/SDK_INTEGRATION.md`](docs/SDK_INTEGRATION.md)**.
|
|
108
|
+
|
|
109
|
+
### Know which SDKs are active (runtime)
|
|
110
|
+
|
|
111
|
+
`getActiveSdks()` reports, at the current moment, which adapters/SDKs are available:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { ThermalPrinter } from '@delicity/capacitor-thermal-printer';
|
|
115
|
+
|
|
116
|
+
const { sdks } = await ThermalPrinter.getActiveSdks();
|
|
117
|
+
// [
|
|
118
|
+
// { adapter: 'escpos', label: 'Generic ESC/POS', available: true, requiresSdk: false, transports: ['wifi','ethernet','bluetooth','usb'] },
|
|
119
|
+
// { adapter: 'star', label: 'Star StarXpand', available: true, requiresSdk: true, transports: ['wifi','bluetooth','ble','usb'] },
|
|
120
|
+
// { adapter: 'epson', label: 'Epson ePOS2', available: false, requiresSdk: true, transports: ['wifi','bluetooth','usb'] },
|
|
121
|
+
// ...
|
|
122
|
+
// ]
|
|
123
|
+
const active = sdks.filter(s => s.available).map(s => s.label);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Useful for a "Printer diagnostics" screen, or to only show the brands actually
|
|
127
|
+
available on the device.
|
|
128
|
+
|
|
129
|
+
## Permissions
|
|
130
|
+
|
|
131
|
+
### Android (plugin `AndroidManifest.xml`, already provided)
|
|
132
|
+
|
|
133
|
+
| Permission | Use | API |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `BLUETOOTH_SCAN` (`neverForLocation`) | BT/BLE scan | 31+ |
|
|
136
|
+
| `BLUETOOTH_CONNECT` | SPP/GATT connection | 31+ |
|
|
137
|
+
| `BLUETOOTH`, `BLUETOOTH_ADMIN` | scan/connection | ≤30 |
|
|
138
|
+
| `ACCESS_FINE_LOCATION` | BLE scan | ≤30 |
|
|
139
|
+
| `INTERNET`, `ACCESS_NETWORK_STATE`, `ACCESS_WIFI_STATE` | TCP 9100 + network detection | all |
|
|
140
|
+
| `CHANGE_WIFI_MULTICAST_STATE` | mDNS | all |
|
|
141
|
+
| `android.hardware.usb.host` (feature) | USB | optional |
|
|
142
|
+
|
|
143
|
+
Call `requestPermissions()` before the first scan.
|
|
144
|
+
|
|
145
|
+
### iOS (host app `Info.plist`)
|
|
146
|
+
|
|
147
|
+
```xml
|
|
148
|
+
<key>NSLocalNetworkUsageDescription</key>
|
|
149
|
+
<string>Discover and print to printers on the local network.</string>
|
|
150
|
+
<key>NSBonjourServices</key>
|
|
151
|
+
<array>
|
|
152
|
+
<string>_pdl-datastream._tcp</string>
|
|
153
|
+
<string>_printer._tcp</string>
|
|
154
|
+
<string>_ipp._tcp</string>
|
|
155
|
+
</array>
|
|
156
|
+
<!-- If BLE is enabled: -->
|
|
157
|
+
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
158
|
+
<string>Connect to compatible Bluetooth printers.</string>
|
|
159
|
+
<!-- If using an MFi SDK (Epson/Star/Zebra Bluetooth): declare the protocols -->
|
|
160
|
+
<key>UISupportedExternalAccessoryProtocols</key>
|
|
161
|
+
<array>
|
|
162
|
+
<string>com.epson.escpos</string>
|
|
163
|
+
<string>jp.star-m.starpro</string>
|
|
164
|
+
</array>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Public API
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { ThermalPrinter } from '@delicity/capacitor-thermal-printer';
|
|
171
|
+
|
|
172
|
+
ThermalPrinter.discoverPrinters(options?) // → { printers: DiscoveredPrinter[] }
|
|
173
|
+
ThermalPrinter.connectPrinter({ printerId, timeoutMs?, forceAdapter?, setAsDefault? }) // → { connected }
|
|
174
|
+
ThermalPrinter.disconnectPrinter({ printerId }) // → void
|
|
175
|
+
ThermalPrinter.setDefaultPrinter({ printerId }) // → { profile }
|
|
176
|
+
ThermalPrinter.getDefaultPrinter() // → { profile | null }
|
|
177
|
+
ThermalPrinter.getSavedPrinters() // → { profiles }
|
|
178
|
+
ThermalPrinter.removePrinter({ printerId }) // → void
|
|
179
|
+
ThermalPrinter.printImage(options) // → PrintResult (await = printed)
|
|
180
|
+
ThermalPrinter.printText({ items, ... }) // → PrintResult (await = printed)
|
|
181
|
+
ThermalPrinter.getPrinterStatus({ printerId? }) // → PrinterStatus
|
|
182
|
+
ThermalPrinter.requestPermissions() / checkPermissions() // → PermissionStatus
|
|
183
|
+
ThermalPrinter.startStatusMonitor({ printerId, intervalMs? }) // background status polling
|
|
184
|
+
ThermalPrinter.stopStatusMonitor({ printerId })
|
|
185
|
+
ThermalPrinter.getActiveSdks() // → { sdks: SdkStatus[] }
|
|
186
|
+
ThermalPrinter.getDebugLog() // → { log: DebugLogEntry[] }
|
|
187
|
+
|
|
188
|
+
// Events
|
|
189
|
+
ThermalPrinter.addListener('printerFound', e => ...) // incremental scan results
|
|
190
|
+
ThermalPrinter.addListener('discoveryComplete', e => ...)
|
|
191
|
+
ThermalPrinter.addListener('statusChange', e => ...) // PrinterStatus (paper/cover/connection)
|
|
192
|
+
ThermalPrinter.addListener('printJobStatus', e => ...) // JobState: pending/printing/hold/completed/failed
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
> **`connectPrinter({ setAsDefault: true })`** sets the default printer **only if
|
|
196
|
+
> the connection succeeds** (`connect` + `setDefaultPrinter` in one step, without
|
|
197
|
+
> persisting an unreachable printer).
|
|
198
|
+
|
|
199
|
+
### Print completion / `await`
|
|
200
|
+
|
|
201
|
+
`printImage` and `printText` are **async and resolve when physical printing is done**
|
|
202
|
+
(best-effort) — so you can `await` them. Details:
|
|
203
|
+
|
|
204
|
+
- **Manufacturer SDKs**: the promise waits for the SDK's **completion callback** (max reliability).
|
|
205
|
+
- **ESC/POS TCP/SPP**: a **one-way** channel → the promise resolves once all bytes are
|
|
206
|
+
**written and flushed**. A **status pre-check** runs before sending: paper empty /
|
|
207
|
+
cover open → job set to `hold` + rejection `PAPER_EMPTY` / `COVER_OPEN`
|
|
208
|
+
(`retryable: true`).
|
|
209
|
+
|
|
210
|
+
## Types
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
type PrinterTransport = 'wifi' | 'ethernet' | 'bluetooth' | 'ble' | 'usb';
|
|
214
|
+
type PrinterAdapterId = 'escpos' | 'epson' | 'star' | 'brother' | 'zebra' | 'rawTcp';
|
|
215
|
+
|
|
216
|
+
interface PrinterCapabilities {
|
|
217
|
+
paperWidthMm: number; // 58 | 80 | 112…
|
|
218
|
+
printableDots: number; // 384 (58mm) | 576 (80mm) | 832 (112mm)
|
|
219
|
+
dpi: number; // 203 most of the time
|
|
220
|
+
supportsCut: boolean;
|
|
221
|
+
supportsCashDrawer: boolean;
|
|
222
|
+
supportsStatus: boolean;
|
|
223
|
+
supportsRasterImage: boolean;
|
|
224
|
+
supportsQrCode?: boolean;
|
|
225
|
+
supportsBarcode?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface DiscoveredPrinter {
|
|
229
|
+
id: string; // stable id: "wifi:192.168.1.50", "bluetooth:AA:BB:.."
|
|
230
|
+
name: string;
|
|
231
|
+
brand?: string; model?: string;
|
|
232
|
+
transport: PrinterTransport;
|
|
233
|
+
adapter: PrinterAdapterId; // resolved by priority
|
|
234
|
+
address: string; // "ip:port" | MAC | UUID
|
|
235
|
+
capabilities?: Partial<PrinterCapabilities>;
|
|
236
|
+
discoveredBy?: PrinterAdapterId[];
|
|
237
|
+
lastSeenAt: number;
|
|
238
|
+
isDefault: boolean;
|
|
239
|
+
isConnected: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface PrinterProfile {
|
|
243
|
+
id: string;
|
|
244
|
+
adapter: PrinterAdapterId;
|
|
245
|
+
transport: PrinterTransport;
|
|
246
|
+
address: string;
|
|
247
|
+
brand?: string; model?: string;
|
|
248
|
+
name: string;
|
|
249
|
+
capabilities: PrinterCapabilities;
|
|
250
|
+
defaultPrintOptions?: PrintRenderOptions;
|
|
251
|
+
adapterMeta?: Record<string, string | number | boolean>;
|
|
252
|
+
isDefault: boolean;
|
|
253
|
+
createdAt: number; updatedAt: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- States / statuses ----
|
|
257
|
+
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
258
|
+
type PaperStatus = 'ok' | 'near_end' | 'empty' | 'unknown';
|
|
259
|
+
type JobState = 'pending' | 'printing' | 'hold' | 'completed' | 'failed' | 'canceled';
|
|
260
|
+
type HoldReason = 'paper_empty' | 'paper_near_end' | 'cover_open' | 'buffer_full' | 'offline' | 'unknown';
|
|
261
|
+
|
|
262
|
+
interface PrinterStatus {
|
|
263
|
+
id: string;
|
|
264
|
+
connection: ConnectionState;
|
|
265
|
+
online: boolean;
|
|
266
|
+
paper: PaperStatus;
|
|
267
|
+
coverOpen?: boolean;
|
|
268
|
+
errorCode?: PrintErrorCode;
|
|
269
|
+
rawStatus?: string;
|
|
270
|
+
checkedAt: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface PrintJobStatus {
|
|
274
|
+
jobId: string;
|
|
275
|
+
printerId: string;
|
|
276
|
+
state: JobState;
|
|
277
|
+
holdReason?: HoldReason;
|
|
278
|
+
progress?: number; // 0..1 (best-effort)
|
|
279
|
+
errorCode?: PrintErrorCode;
|
|
280
|
+
message?: string;
|
|
281
|
+
updatedAt: number;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface PrintResult {
|
|
285
|
+
success: boolean;
|
|
286
|
+
printerId: string;
|
|
287
|
+
adapter: PrinterAdapterId;
|
|
288
|
+
jobId: string; // correlated with printJobStatus events
|
|
289
|
+
state: JobState; // 'completed' on success
|
|
290
|
+
bytesSent?: number;
|
|
291
|
+
durationMs?: number;
|
|
292
|
+
status?: PrinterStatus;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---- Image print options ----
|
|
296
|
+
type DitheringAlgorithm = 'none' | 'floyd_steinberg' | 'atkinson';
|
|
297
|
+
type ImageAlign = 'left' | 'center' | 'right';
|
|
298
|
+
|
|
299
|
+
interface ImageSource { filePath?: string; url?: string; base64?: string; } // exactly one key
|
|
300
|
+
|
|
301
|
+
interface PrintRenderOptions {
|
|
302
|
+
widthDots?: number; // otherwise derived from the profile (384/576/832)
|
|
303
|
+
resize?: boolean; // default true; false = image already at the right width
|
|
304
|
+
grayscale?: boolean; // default true; false = image already 1-bit (simple threshold)
|
|
305
|
+
threshold?: number; // default 128 (when dithering 'none' or grayscale false)
|
|
306
|
+
dithering?: DitheringAlgorithm; // default 'floyd_steinberg'
|
|
307
|
+
align?: ImageAlign; // default 'center'
|
|
308
|
+
invert?: boolean;
|
|
309
|
+
cut?: boolean; // default true
|
|
310
|
+
feedLines?: number; // default 3
|
|
311
|
+
openCashDrawer?: boolean;
|
|
312
|
+
copies?: number; // default 1
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
interface PrintImageOptions {
|
|
316
|
+
printerId?: string; // otherwise the default printer
|
|
317
|
+
image: ImageSource;
|
|
318
|
+
render?: PrintRenderOptions;
|
|
319
|
+
timeoutMs?: number; // default 15000
|
|
320
|
+
autoReconnect?: boolean; // default true
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---- SDK status ----
|
|
324
|
+
interface SdkStatus {
|
|
325
|
+
adapter: PrinterAdapterId;
|
|
326
|
+
label: string;
|
|
327
|
+
available: boolean; // detected & usable right now
|
|
328
|
+
requiresSdk: boolean; // true for brand SDKs, false for built-in adapters
|
|
329
|
+
transports: PrinterTransport[];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---- Events ----
|
|
333
|
+
interface PrinterFoundEvent { printer: DiscoveredPrinter; }
|
|
334
|
+
interface DiscoveryCompleteEvent { printers: DiscoveredPrinter[]; failedSources?: string[]; }
|
|
335
|
+
interface StatusChangeEvent { status: PrinterStatus; }
|
|
336
|
+
interface PrintJobStatusEvent { job: PrintJobStatus; }
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### `printText` types
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
type TextAlign = 'left' | 'center' | 'right';
|
|
343
|
+
type Underline = 'none' | 'single' | 'double';
|
|
344
|
+
type EscPosFont = 'A' | 'B';
|
|
345
|
+
type CodePage = 'CP437' | 'CP850' | 'CP858' | 'WPC1252' | 'CP852' | 'CP866'; // Latin-1/Western: WPC1252
|
|
346
|
+
type BarcodeSymbology = 'UPC_A'|'UPC_E'|'EAN13'|'EAN8'|'CODE39'|'ITF'|'CODABAR'|'CODE93'|'CODE128';
|
|
347
|
+
type HriPosition = 'none' | 'above' | 'below' | 'both';
|
|
348
|
+
type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
|
|
349
|
+
|
|
350
|
+
interface TextStyle {
|
|
351
|
+
align?: TextAlign;
|
|
352
|
+
bold?: boolean;
|
|
353
|
+
underline?: Underline;
|
|
354
|
+
font?: EscPosFont;
|
|
355
|
+
widthMultiplier?: number; // 1..8
|
|
356
|
+
heightMultiplier?: number; // 1..8
|
|
357
|
+
doubleStrike?: boolean;
|
|
358
|
+
invert?: boolean; // white on black
|
|
359
|
+
upsideDown?: boolean;
|
|
360
|
+
rotate90?: boolean;
|
|
361
|
+
letterSpacing?: number; // dots
|
|
362
|
+
lineSpacing?: number; // dots (otherwise default)
|
|
363
|
+
codePage?: CodePage;
|
|
364
|
+
codePageId?: number; // raw ESC t n override
|
|
365
|
+
newline?: boolean; // default true
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
type PrintItem =
|
|
369
|
+
| { type: 'text'; value: string; style?: TextStyle }
|
|
370
|
+
| { type: 'feed'; lines?: number }
|
|
371
|
+
| { type: 'cut'; mode?: 'full' | 'partial'; feedBefore?: number }
|
|
372
|
+
| { type: 'divider'; char?: string; columns?: number; style?: Pick<TextStyle,'align'|'bold'|'font'> }
|
|
373
|
+
| { type: 'qrcode'; value: string; size?: number; errorCorrection?: QrErrorCorrection; align?: TextAlign }
|
|
374
|
+
| { type: 'barcode'; value: string; symbology: BarcodeSymbology; height?: number; width?: number; hri?: HriPosition; align?: TextAlign }
|
|
375
|
+
| { type: 'cashDrawer'; pin?: 2 | 5 }
|
|
376
|
+
| { type: 'image'; image: ImageSource; render?: PrintRenderOptions }
|
|
377
|
+
| { type: 'raw'; bytesBase64: string };
|
|
378
|
+
|
|
379
|
+
interface PrintTextOptions {
|
|
380
|
+
printerId?: string;
|
|
381
|
+
items: PrintItem[];
|
|
382
|
+
defaultCodePage?: CodePage; // Western/Latin-1: 'WPC1252'
|
|
383
|
+
cut?: boolean; // default false
|
|
384
|
+
feedLines?: number; // default 3
|
|
385
|
+
timeoutMs?: number;
|
|
386
|
+
autoReconnect?: boolean;
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Image printing flow
|
|
391
|
+
|
|
392
|
+
`printImage` performs exactly:
|
|
393
|
+
|
|
394
|
+
1. Resolve the target printer (otherwise the **default printer**).
|
|
395
|
+
2. Check whether it is connected.
|
|
396
|
+
3. If not, **automatic reconnection** (when `autoReconnect`, default `true`).
|
|
397
|
+
4. **Open the image** (`filePath` > `url` (cached) > `base64`).
|
|
398
|
+
5. **Resize** to the exact width (`widthDots` or the profile's capabilities).
|
|
399
|
+
6. **Grayscale** (BT.601 luminance), flatten onto a white background (transparent PNG).
|
|
400
|
+
7. **Dithering** (Floyd-Steinberg by default, Atkinson, or threshold).
|
|
401
|
+
8. **Convert to the adapter** (`GS v 0` raster for ESC/POS, `addImage` for SDKs, ZPL for Zebra).
|
|
402
|
+
9. **Send** (in transport-sized chunks).
|
|
403
|
+
10. **Feed + cut** (if supported) + optional cash drawer.
|
|
404
|
+
11. **Normalized result** + best-effort status read.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
await ThermalPrinter.printImage({
|
|
408
|
+
// printerId omitted → default printer
|
|
409
|
+
image: { filePath: '/data/.../receipt.png' }, // recommended in production
|
|
410
|
+
render: { dithering: 'floyd_steinberg', cut: true, feedLines: 3, align: 'center' },
|
|
411
|
+
timeoutMs: 15000,
|
|
412
|
+
autoReconnect: true,
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Concrete image-printing examples
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
// 1) Local file (RECOMMENDED in production) — most reliable/performant
|
|
420
|
+
await ThermalPrinter.printImage({ image: { filePath: '/data/user/0/app/files/receipt.png' } });
|
|
421
|
+
|
|
422
|
+
// 2) Remote URL — downloaded and cached by the plugin
|
|
423
|
+
await ThermalPrinter.printImage({
|
|
424
|
+
image: { url: 'https://api.example.com/receipts/123/render.png' },
|
|
425
|
+
render: { dithering: 'atkinson', cut: true },
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// 3) base64 (handy for tests, less performant)
|
|
429
|
+
await ThermalPrinter.printImage({ image: { base64: 'iVBORw0KGgoAAAANS...' } });
|
|
430
|
+
|
|
431
|
+
// 4) Image ALREADY rendered server-side at the right width and as 1-bit black/white:
|
|
432
|
+
// disable resize + grayscale → pixel-perfect, faster send.
|
|
433
|
+
await ThermalPrinter.printImage({
|
|
434
|
+
image: { filePath: '/data/.../receipt_576px_1bit.png' },
|
|
435
|
+
render: { resize: false, grayscale: false, cut: true },
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// 5) Target a specific printer + 2 copies + cash drawer
|
|
439
|
+
await ThermalPrinter.printImage({
|
|
440
|
+
printerId: 'wifi:192.168.1.50',
|
|
441
|
+
image: { filePath: '/data/.../receipt.png' },
|
|
442
|
+
render: { widthDots: 576, copies: 2, openCashDrawer: true },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// 6) await = printed (best-effort); typed error handling
|
|
446
|
+
try {
|
|
447
|
+
const res = await ThermalPrinter.printImage({ image: { filePath } });
|
|
448
|
+
console.log('Printed', res.jobId, res.bytesSent, 'bytes in', res.durationMs, 'ms');
|
|
449
|
+
} catch (e) {
|
|
450
|
+
if ((e as PrinterError).code === PrintErrorCode.PAPER_EMPTY) alert('Out of paper');
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
> **`resize`/`grayscale` are optional**: if your server already produces a PNG at the
|
|
455
|
+
> exact width (`576px`/`384px`) and 1-bit black/white, pass
|
|
456
|
+
> `render: { resize: false, grayscale: false }`. The plugin then applies a simple
|
|
457
|
+
> threshold (no dithering) and does not alter the geometry.
|
|
458
|
+
|
|
459
|
+
## Text printing (`printText`)
|
|
460
|
+
|
|
461
|
+
`printText` accepts an **ordered array of typed items**. Ideal for purely textual
|
|
462
|
+
output, with no server-side pre-rendering.
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
await ThermalPrinter.printText({
|
|
466
|
+
defaultCodePage: 'WPC1252', // Western/Latin-1 accents
|
|
467
|
+
items: [
|
|
468
|
+
{ type: 'text', value: 'MY STORE', style: { align: 'center', bold: true, widthMultiplier: 2, heightMultiplier: 2 } },
|
|
469
|
+
{ type: 'text', value: '12 Main Street', style: { align: 'center' } },
|
|
470
|
+
{ type: 'divider', char: '-' },
|
|
471
|
+
{ type: 'text', value: 'Order #1042', style: { bold: true } },
|
|
472
|
+
{ type: 'text', value: 'Item A...........12.00' },
|
|
473
|
+
{ type: 'text', value: 'Item B........... 2.00' },
|
|
474
|
+
{ type: 'divider' },
|
|
475
|
+
{ type: 'text', value: 'TOTAL 14.00', style: { align: 'right', bold: true, widthMultiplier: 2 } },
|
|
476
|
+
{ type: 'feed', lines: 1 },
|
|
477
|
+
{ type: 'qrcode', value: 'https://example.com/order/1042', size: 6, align: 'center' },
|
|
478
|
+
{ type: 'barcode', value: '4006381333931', symbology: 'EAN13', hri: 'below' },
|
|
479
|
+
{ type: 'cut', mode: 'partial', feedBefore: 3 },
|
|
480
|
+
],
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Supported styles (ESC/POS) and SDK mapping
|
|
485
|
+
|
|
486
|
+
| Style / item | ESC/POS (escpos, rawTcp) | Epson ePOS2 | Star StarXpand | Brother | Zebra (ZPL) |
|
|
487
|
+
|---|:--:|:--:|:--:|:--:|:--:|
|
|
488
|
+
| `align` (left/center/right) | ✅ `ESC a` | ✅ | ✅ | ✅ | ✅ (field) |
|
|
489
|
+
| `bold` | ✅ `ESC E` | ✅ | ✅ | ✅ | ⚠️ via font |
|
|
490
|
+
| `underline` (single/double) | ✅ `ESC -` | ✅ | ✅ | ⚠️ | ❌ |
|
|
491
|
+
| `font` A/B | ✅ `ESC M` | ✅ | ✅ | ⚠️ | ⚠️ |
|
|
492
|
+
| `widthMultiplier`/`heightMultiplier` (1..8) | ✅ `GS !` | ✅ | ✅ | ✅ | ✅ (size) |
|
|
493
|
+
| `doubleStrike` | ✅ `ESC G` | ✅ | ⚠️ | ❌ | ❌ |
|
|
494
|
+
| `invert` (white/black) | ✅ `GS B` | ✅ | ✅ | ⚠️ | ✅ (reverse) |
|
|
495
|
+
| `upsideDown` | ✅ `ESC {` | ✅ | ⚠️ | ❌ | ✅ |
|
|
496
|
+
| `rotate90` | ✅ `ESC V` | ✅ | ⚠️ | ⚠️ | ✅ |
|
|
497
|
+
| `letterSpacing` | ✅ `ESC SP` | ✅ | ⚠️ | ❌ | ⚠️ |
|
|
498
|
+
| `lineSpacing` | ✅ `ESC 3` | ✅ | ✅ | ⚠️ | ✅ |
|
|
499
|
+
| `codePage` (accents) | ✅ `ESC t` | ✅ | ✅ | ✅ | ✅ |
|
|
500
|
+
| `qrcode` | ✅ `GS ( k` | ✅ native | ✅ native | ✅ native | ✅ `^BQ` |
|
|
501
|
+
| `barcode` (EAN/CODE128…) | ✅ `GS k` | ✅ native | ✅ native | ✅ native | ✅ `^BC`… |
|
|
502
|
+
| `divider` / `feed` / `cut` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
503
|
+
| `cashDrawer` | ✅ `ESC p` | ✅ | ✅ | ⚠️ | ⚠️ |
|
|
504
|
+
| `image` (inline) | ✅ raster | ✅ addImage | ✅ actionPrintImage | ✅ printImage | ✅ ^GF |
|
|
505
|
+
| `raw` (raw bytes) | ✅ | ⚠️ | ⚠️ | ❌ | ⚠️ (raw ZPL) |
|
|
506
|
+
|
|
507
|
+
> ✅ supported · ⚠️ partial/model-dependent equivalent · ❌ not available.
|
|
508
|
+
> Styles not supported by an SDK are **ignored gracefully** (never a hard failure).
|
|
509
|
+
> The reference ESC/POS encoder lives in `src/core/escpos-text.ts` (tested), mirrored
|
|
510
|
+
> in Kotlin (`EscPosTextEncoder.kt`) and Swift (`EscPosTextEncoder.swift`).
|
|
511
|
+
|
|
512
|
+
> **`printText` per brand.** It works on all brands: ESC/POS and **Star** (both
|
|
513
|
+
> platforms) and **Epson Android** map text to a native builder; **Epson iOS,
|
|
514
|
+
> Brother, Zebra** fall back automatically to **rendering the items to an image**
|
|
515
|
+
> (`TextRasterizer`) printed via the SDK's image path. See `docs/SDK_INTEGRATION.md`.
|
|
516
|
+
|
|
517
|
+
## Client-side events & status
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
// Job tracking: pending → printing → completed | hold | failed
|
|
521
|
+
const jobSub = await ThermalPrinter.addListener('printJobStatus', ({ job }) => {
|
|
522
|
+
switch (job.state) {
|
|
523
|
+
case 'printing': showSpinner(job.progress); break;
|
|
524
|
+
case 'hold': toast(job.holdReason === 'paper_empty' ? 'Add paper' : 'Cover open'); break;
|
|
525
|
+
case 'completed': hideSpinner(); break;
|
|
526
|
+
case 'failed': alert(`Failed: ${job.errorCode}`); break;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Printer status (connection, paper, cover)
|
|
531
|
+
const statusSub = await ThermalPrinter.addListener('statusChange', ({ status }) => {
|
|
532
|
+
updateBadge(status.online, status.paper); // 'ok' | 'near_end' | 'empty' | 'unknown'
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ... later
|
|
536
|
+
await jobSub.remove();
|
|
537
|
+
await statusSub.remove();
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## Image processing
|
|
541
|
+
|
|
542
|
+
- **Reference widths @203 dpi**: `58mm → 384 px`, `80mm → 576 px`, `112mm → 832 px`. Some 80mm models print `640 px`: **always prefer the profile/SDK `printableDots`** when known.
|
|
543
|
+
- **Pipeline**: proportional resize to the target width → grayscale → binarization.
|
|
544
|
+
- **Dithering**:
|
|
545
|
+
- `none` (threshold): crisp for text/lines.
|
|
546
|
+
- `floyd_steinberg` (**default**): logos/photos.
|
|
547
|
+
- `atkinson`: more contrast, pleasant on receipts.
|
|
548
|
+
- **ESC/POS raster**: `GS v 0` command (`0x1D 0x76 0x30 m xL xH yL yH data`), width padded to a multiple of 8, MSB = leftmost pixel. Testable reference implementation in `src/core/imaging.ts`, mirrored in Kotlin (`ImageProcessor.kt`) and Swift (`ImageProcessor.swift`).
|
|
549
|
+
|
|
550
|
+
## Aggregated discovery & adapter priority
|
|
551
|
+
|
|
552
|
+
Several sources run **in parallel**: Epson/Star/Brother/Zebra SDKs, TCP 9100 scan, Bluetooth Classic (Android), BLE (allowlisted services), USB (Android). Results are **merged** by stable `id` and **deduplicated**.
|
|
553
|
+
|
|
554
|
+
**Priority rules** (`priority.ts` / `AdapterPriority.kt` / `.swift`):
|
|
555
|
+
|
|
556
|
+
| Case | Selected adapter | Score |
|
|
557
|
+
|---|---|---|
|
|
558
|
+
| Printer recognized by an official SDK | `epson` / `star` / `brother` | 880–900 |
|
|
559
|
+
| **Zebra** | **`zebra` only** (ESC/POS banned) | 1000 / −1000 |
|
|
560
|
+
| ESC/POS confirmed over Bluetooth (Android) | `escpos` | 620 |
|
|
561
|
+
| ESC/POS confirmed over TCP | `escpos` | 600 |
|
|
562
|
+
| BLE with a usable service | (BLE) | 500 |
|
|
563
|
+
| Unidentified network device | `rawTcp` | 300 |
|
|
564
|
+
|
|
565
|
+
## Default printer & reconnection
|
|
566
|
+
|
|
567
|
+
- After a **successful test print**, the app calls `setDefaultPrinter({ printerId })`: the plugin **persists a `PrinterProfile`** (id, adapter, transport, address, brand, model, paper width, `printableDots`, dpi, cut options, reconnection metadata).
|
|
568
|
+
- On **startup** or **before printing**, the plugin reloads this profile.
|
|
569
|
+
- **Reconnection is not a permanent connection**: it is attempted **just before `printImage`** (step 3). This avoids keeping a socket/Bluetooth link open needlessly and improves reliability for occasional printing. It uses **exponential backoff** (up to 3 attempts) and detects recovery after a `hold` (paper reloaded / cover closed / back online).
|
|
570
|
+
|
|
571
|
+
## Normalized errors
|
|
572
|
+
|
|
573
|
+
Every rejected promise carries a **stable code** (`error.code`):
|
|
574
|
+
|
|
575
|
+
`PRINTER_NOT_FOUND`, `PRINTER_OFFLINE`, `CONNECTION_FAILED`, `PERMISSION_DENIED`, `BLUETOOTH_DISABLED`, `WIFI_NOT_CONNECTED`, `PAIRING_REQUIRED`, `UNSUPPORTED_TRANSPORT`, `UNSUPPORTED_PRINTER`, `IMAGE_INVALID`, `IMAGE_TOO_LARGE`, `PRINT_FAILED`, `PAPER_EMPTY`, `COVER_OPEN`, `SDK_NOT_AVAILABLE`, `TIMEOUT`, `UNKNOWN`.
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
import { PrinterError, PrintErrorCode } from '@delicity/capacitor-thermal-printer';
|
|
579
|
+
try { await ThermalPrinter.printImage({ image: { filePath } }); }
|
|
580
|
+
catch (e) {
|
|
581
|
+
const err = e as PrinterError; // { code, message, detail, retryable }
|
|
582
|
+
if (err.code === PrintErrorCode.PAPER_EMPTY) showPaperAlert();
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## Android / iOS differences
|
|
587
|
+
|
|
588
|
+
### Android — broad hardware coverage
|
|
589
|
+
- Modern Bluetooth permissions (12+) handled.
|
|
590
|
+
- **Bluetooth Classic / SPP**: supported → covers the very common generic ESC/POS printers. ✅
|
|
591
|
+
- BLE supported (UUID allowlist recommended).
|
|
592
|
+
- Retrieval of **already-paired devices** (instant, no scan).
|
|
593
|
+
- TCP 9100 (Wi-Fi/Ethernet). ✅
|
|
594
|
+
- USB host (optional).
|
|
595
|
+
|
|
596
|
+
### iOS — Apple constraints
|
|
597
|
+
- ❌ **No generic Bluetooth Classic / SPP.** A generic "no-name" BT printer **is not addressable** unless it exposes a usable BLE service.
|
|
598
|
+
- ✅ **MFi manufacturer SDKs** (Epson/Star/Brother/Zebra): this is **the** path for Bluetooth on iOS.
|
|
599
|
+
- ✅ **Wi-Fi TCP** (port 9100) via `Network.framework` → triggers the **Local Network** prompt.
|
|
600
|
+
- ❌ **No generic BLE GATT exposed by the plugin on iOS.** BLE goes through the MFi
|
|
601
|
+
SDKs (Star/Epson/Brother). Attempting a `ble`/`bluetooth`/`usb` transport on the
|
|
602
|
+
generic ESC/POS adapter returns an explicit `UNSUPPORTED_TRANSPORT` error.
|
|
603
|
+
- ❌ No USB host for this use case.
|
|
604
|
+
|
|
605
|
+
> **Never promise** universal Bluetooth compatibility on iOS. In practice: **Wi-Fi for everyone, Bluetooth via the manufacturer SDK**.
|
|
606
|
+
|
|
607
|
+
## Image cache & logs/diagnostics
|
|
608
|
+
|
|
609
|
+
- **Cache**: `url` images are downloaded into `cache/thermal-images/` (key = URL hash, 32 MB quota, LRU eviction). The `filePath` mode remains the most reliable.
|
|
610
|
+
- **Logs**: in-memory ring buffer (500 lines) + Logcat/os_log. Retrievable via `getDebugLog()` for a "Diagnostics" screen attachable to support tickets. Never raw image data (only dimensions/byte counts).
|
|
611
|
+
|
|
612
|
+
> Implementation status, tests and development setup live in
|
|
613
|
+
> [`CONTRIBUTING.md`](CONTRIBUTING.md) · roadmap in [`ROADMAP.md`](ROADMAP.md).
|
|
614
|
+
|
|
615
|
+
## Full example
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
import { ThermalPrinter, PrinterError } from '@delicity/capacitor-thermal-printer';
|
|
619
|
+
|
|
620
|
+
// 1) Discovery (with incremental results)
|
|
621
|
+
const sub = await ThermalPrinter.addListener('printerFound', e => {
|
|
622
|
+
console.log('Found:', e.printer.name, e.printer.adapter);
|
|
623
|
+
});
|
|
624
|
+
await ThermalPrinter.requestPermissions();
|
|
625
|
+
const { printers } = await ThermalPrinter.discoverPrinters({ timeoutMs: 8000 });
|
|
626
|
+
await sub.remove();
|
|
627
|
+
|
|
628
|
+
// 2) Connect, set as default IF it succeeds, then test print
|
|
629
|
+
const target = printers[0];
|
|
630
|
+
await ThermalPrinter.connectPrinter({ printerId: target.id, setAsDefault: true });
|
|
631
|
+
await ThermalPrinter.printImage({ printerId: target.id, image: { base64: testReceiptBase64 } });
|
|
632
|
+
|
|
633
|
+
// 3) Later: simple print (default printer + auto reconnection)
|
|
634
|
+
await ThermalPrinter.printImage({ image: { filePath: '/data/.../receipt.png' } });
|
|
635
|
+
|
|
636
|
+
// 4) Or styled text printing
|
|
637
|
+
await ThermalPrinter.printText({
|
|
638
|
+
items: [
|
|
639
|
+
{ type: 'text', value: 'Thank you!', style: { align: 'center', bold: true } },
|
|
640
|
+
{ type: 'cut' },
|
|
641
|
+
],
|
|
642
|
+
});
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## License
|
|
648
|
+
|
|
649
|
+
MIT © Delicity
|