@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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.adapters
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import com.delicity.thermalprinter.model.AdapterId
|
|
6
|
+
import com.delicity.thermalprinter.model.DiscoveredPrinter
|
|
7
|
+
import com.delicity.thermalprinter.model.ErrorCode
|
|
8
|
+
import com.delicity.thermalprinter.model.PrinterException
|
|
9
|
+
import com.delicity.thermalprinter.model.PrinterProfile
|
|
10
|
+
import com.delicity.thermalprinter.model.PrinterStatus
|
|
11
|
+
import com.delicity.thermalprinter.model.RenderOptions
|
|
12
|
+
import com.delicity.thermalprinter.model.Transport
|
|
13
|
+
import com.starmicronics.stario10.InterfaceType
|
|
14
|
+
import com.starmicronics.stario10.StarConnectionSettings
|
|
15
|
+
import com.starmicronics.stario10.StarDeviceDiscoveryManager
|
|
16
|
+
import com.starmicronics.stario10.StarDeviceDiscoveryManagerFactory
|
|
17
|
+
import com.starmicronics.stario10.StarPrinter
|
|
18
|
+
import com.starmicronics.stario10.starxpandcommand.DocumentBuilder
|
|
19
|
+
import com.starmicronics.stario10.starxpandcommand.DrawerBuilder
|
|
20
|
+
import com.starmicronics.stario10.starxpandcommand.PrinterBuilder
|
|
21
|
+
import com.starmicronics.stario10.starxpandcommand.StarXpandCommandBuilder
|
|
22
|
+
import com.starmicronics.stario10.starxpandcommand.drawer.Channel
|
|
23
|
+
import com.starmicronics.stario10.starxpandcommand.drawer.OpenParameter
|
|
24
|
+
import com.starmicronics.stario10.starxpandcommand.printer.Alignment
|
|
25
|
+
import com.starmicronics.stario10.starxpandcommand.printer.BarcodeParameter
|
|
26
|
+
import com.starmicronics.stario10.starxpandcommand.printer.BarcodeSymbology
|
|
27
|
+
import com.starmicronics.stario10.starxpandcommand.printer.CutType
|
|
28
|
+
import com.starmicronics.stario10.starxpandcommand.printer.ImageParameter
|
|
29
|
+
import com.starmicronics.stario10.starxpandcommand.printer.MagnificationParameter
|
|
30
|
+
import com.starmicronics.stario10.starxpandcommand.printer.QRCodeLevel
|
|
31
|
+
import com.starmicronics.stario10.starxpandcommand.printer.QRCodeParameter
|
|
32
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
33
|
+
import kotlin.coroutines.resume
|
|
34
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adapter Star basé sur le StarXpand SDK (`com.starmicronics:stario10`).
|
|
38
|
+
*
|
|
39
|
+
* ⭐ Star est le SEUL fabricant dont le SDK est 100 % auto-téléchargé (Maven
|
|
40
|
+
* Central) : cet adapter utilise donc des appels TYPÉS directs (pas de réflexion).
|
|
41
|
+
* Si la dépendance Maven est retirée du build, `isAvailable()` renvoie false par
|
|
42
|
+
* réflexion et l'adapter est ignoré.
|
|
43
|
+
*
|
|
44
|
+
* StarXpand expose :
|
|
45
|
+
* - StarDeviceDiscoveryManager (LAN, Bluetooth, BLE, USB),
|
|
46
|
+
* - StarXpandCommandBuilder + PrinterBuilder.actionPrintImage(...) pour l'image,
|
|
47
|
+
* - PrinterBuilder.actionPrintText / QRCode / Barcode pour le texte stylé,
|
|
48
|
+
* - getStatusAsync() pour papier/capot/massicot.
|
|
49
|
+
*/
|
|
50
|
+
class StarAdapter(private val context: Context) : PrinterAdapter {
|
|
51
|
+
|
|
52
|
+
override val id = AdapterId.STAR
|
|
53
|
+
|
|
54
|
+
/** Connexions ouvertes indexées par printerId. */
|
|
55
|
+
private val connections = ConcurrentHashMap<String, StarPrinter>()
|
|
56
|
+
|
|
57
|
+
override fun isAvailable(): Boolean =
|
|
58
|
+
EpsonAdapter.classExists("com.starmicronics.stario10.StarPrinter")
|
|
59
|
+
|
|
60
|
+
override fun supportsTextItems(): Boolean = true
|
|
61
|
+
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
// Découverte
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
|
|
67
|
+
if (!isAvailable()) return
|
|
68
|
+
val manager = StarDeviceDiscoveryManagerFactory.create(
|
|
69
|
+
listOf(
|
|
70
|
+
InterfaceType.Lan,
|
|
71
|
+
InterfaceType.Bluetooth,
|
|
72
|
+
InterfaceType.BluetoothLE,
|
|
73
|
+
InterfaceType.Usb,
|
|
74
|
+
),
|
|
75
|
+
context,
|
|
76
|
+
)
|
|
77
|
+
manager.discoveryTime = timeoutMs.toInt().coerceIn(1000, 30000)
|
|
78
|
+
|
|
79
|
+
suspendCancellableCoroutine<Unit> { cont ->
|
|
80
|
+
manager.callback = object : StarDeviceDiscoveryManager.Callback {
|
|
81
|
+
override fun onPrinterFound(printer: StarPrinter) {
|
|
82
|
+
val settings = printer.connectionSettings
|
|
83
|
+
val transport = transportFor(settings.interfaceType)
|
|
84
|
+
val model = printer.information?.model?.name
|
|
85
|
+
onFound(
|
|
86
|
+
DiscoveredPrinter(
|
|
87
|
+
id = "star:${settings.interfaceType}:${settings.identifier}",
|
|
88
|
+
name = model ?: "Star Printer",
|
|
89
|
+
brand = "Star",
|
|
90
|
+
model = model,
|
|
91
|
+
transport = transport,
|
|
92
|
+
adapter = AdapterId.STAR,
|
|
93
|
+
address = settings.identifier,
|
|
94
|
+
discoveredBy = mutableSetOf(AdapterId.STAR),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun onDiscoveryFinished() {
|
|
100
|
+
if (cont.isActive) cont.resume(Unit)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
cont.invokeOnCancellation { runCatching { manager.stopDiscovery() } }
|
|
104
|
+
runCatching { manager.startDiscovery() }
|
|
105
|
+
.onFailure { if (cont.isActive) cont.resume(Unit) }
|
|
106
|
+
}
|
|
107
|
+
runCatching { manager.stopDiscovery() }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override fun canHandle(profile: PrinterProfile): Boolean =
|
|
111
|
+
isAvailable() && profile.adapter == AdapterId.STAR
|
|
112
|
+
|
|
113
|
+
// -------------------------------------------------------------------------
|
|
114
|
+
// Connexion
|
|
115
|
+
// -------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
|
|
118
|
+
ensureSdk()
|
|
119
|
+
if (isConnected(profile.id)) return
|
|
120
|
+
val printer = StarPrinter(connectionSettingsFor(profile), context)
|
|
121
|
+
try {
|
|
122
|
+
printer.openAsync().await()
|
|
123
|
+
} catch (e: Exception) {
|
|
124
|
+
throw PrinterException(ErrorCode.CONNECTION_FAILED, "Connexion Star échouée: ${profile.address}", e.message, retryable = true)
|
|
125
|
+
}
|
|
126
|
+
connections[profile.id] = printer
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun isConnected(printerId: String): Boolean = connections.containsKey(printerId)
|
|
130
|
+
|
|
131
|
+
override suspend fun disconnect(printerId: String) {
|
|
132
|
+
connections.remove(printerId)?.let { runCatching { it.closeAsync().await() } }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// -------------------------------------------------------------------------
|
|
136
|
+
// Impression
|
|
137
|
+
// -------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
|
|
140
|
+
val printer = requireConnected(profile)
|
|
141
|
+
val printerDoc = PrinterBuilder()
|
|
142
|
+
.styleAlignment(alignmentFor(options.align))
|
|
143
|
+
.actionPrintImage(ImageParameter(bitmap, profile.capabilities.printableDots.coerceAtLeast(8)))
|
|
144
|
+
if (options.feedLines > 0) printerDoc.actionFeedLine(options.feedLines)
|
|
145
|
+
if (options.cut && profile.capabilities.supportsCut) printerDoc.actionCut(CutType.Partial)
|
|
146
|
+
|
|
147
|
+
val document = DocumentBuilder().addPrinter(printerDoc)
|
|
148
|
+
if (options.openCashDrawer && profile.capabilities.supportsCashDrawer) {
|
|
149
|
+
document.addDrawer(DrawerBuilder().actionOpen(OpenParameter().setChannel(Channel.No1)))
|
|
150
|
+
}
|
|
151
|
+
val commands = StarXpandCommandBuilder().addDocument(document).getCommands()
|
|
152
|
+
|
|
153
|
+
var sent = 0
|
|
154
|
+
repeat(options.copies.coerceAtLeast(1)) {
|
|
155
|
+
sendCommands(printer, commands, profile)
|
|
156
|
+
sent += commands.length
|
|
157
|
+
}
|
|
158
|
+
return sent
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
override suspend fun printItems(
|
|
162
|
+
profile: PrinterProfile,
|
|
163
|
+
items: List<com.delicity.thermalprinter.model.PrintItem>,
|
|
164
|
+
defaultCodePage: String,
|
|
165
|
+
cut: Boolean,
|
|
166
|
+
feedLines: Int,
|
|
167
|
+
): Int {
|
|
168
|
+
val printer = requireConnected(profile)
|
|
169
|
+
val pb = PrinterBuilder()
|
|
170
|
+
for (item in items) mapItem(pb, item, profile)
|
|
171
|
+
if (feedLines > 0) pb.actionFeedLine(feedLines)
|
|
172
|
+
if (cut && profile.capabilities.supportsCut) pb.actionCut(CutType.Partial)
|
|
173
|
+
val commands = StarXpandCommandBuilder()
|
|
174
|
+
.addDocument(DocumentBuilder().addPrinter(pb))
|
|
175
|
+
.getCommands()
|
|
176
|
+
sendCommands(printer, commands, profile)
|
|
177
|
+
return commands.length
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Mappe un PrintItem vers le builder StarXpand (best effort par type). */
|
|
181
|
+
private fun mapItem(pb: PrinterBuilder, item: com.delicity.thermalprinter.model.PrintItem, profile: PrinterProfile) {
|
|
182
|
+
when (item) {
|
|
183
|
+
is com.delicity.thermalprinter.model.PrintItem.Text -> {
|
|
184
|
+
item.style.align?.let { pb.styleAlignment(alignmentFor(it)) }
|
|
185
|
+
pb.styleBold(item.style.bold)
|
|
186
|
+
pb.styleInvert(item.style.invert)
|
|
187
|
+
pb.styleUnderLine(item.style.underline != "none")
|
|
188
|
+
pb.styleMagnification(MagnificationParameter(item.style.widthMultiplier.coerceIn(1, 6), item.style.heightMultiplier.coerceIn(1, 6)))
|
|
189
|
+
if (item.style.newline) pb.actionPrintText(item.value + "\n") else pb.actionPrintText(item.value)
|
|
190
|
+
// reset styles pour ne pas contaminer les items suivants
|
|
191
|
+
pb.styleBold(false).styleInvert(false).styleUnderLine(false)
|
|
192
|
+
.styleMagnification(MagnificationParameter(1, 1))
|
|
193
|
+
}
|
|
194
|
+
is com.delicity.thermalprinter.model.PrintItem.Feed -> pb.actionFeedLine(item.lines.coerceAtLeast(1))
|
|
195
|
+
is com.delicity.thermalprinter.model.PrintItem.Cut ->
|
|
196
|
+
pb.actionCut(if (item.mode == "full") CutType.Full else CutType.Partial)
|
|
197
|
+
is com.delicity.thermalprinter.model.PrintItem.Divider -> {
|
|
198
|
+
val cols = item.columns ?: if (profile.capabilities.printableDots <= 420) 32 else 48
|
|
199
|
+
item.align?.let { pb.styleAlignment(alignmentFor(it)) }
|
|
200
|
+
pb.styleBold(item.bold)
|
|
201
|
+
pb.actionPrintText(item.char.repeat(cols.coerceIn(1, 96)) + "\n")
|
|
202
|
+
pb.styleBold(false)
|
|
203
|
+
}
|
|
204
|
+
is com.delicity.thermalprinter.model.PrintItem.QrCode -> {
|
|
205
|
+
pb.styleAlignment(alignmentFor(item.align))
|
|
206
|
+
pb.actionPrintQRCode(
|
|
207
|
+
QRCodeParameter(item.value)
|
|
208
|
+
.setLevel(qrLevelFor(item.ec))
|
|
209
|
+
.setCellSize(item.size.coerceIn(1, 16)),
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
is com.delicity.thermalprinter.model.PrintItem.Barcode -> {
|
|
213
|
+
pb.styleAlignment(alignmentFor(item.align))
|
|
214
|
+
pb.actionPrintBarcode(
|
|
215
|
+
BarcodeParameter(item.value, barcodeSymbologyFor(item.symbology))
|
|
216
|
+
.setHeight(item.height.coerceIn(1, 255).toDouble())
|
|
217
|
+
.setPrintHri(item.hri != "none"),
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
is com.delicity.thermalprinter.model.PrintItem.CashDrawer -> {
|
|
221
|
+
// Le tiroir est géré au niveau document ; ignoré ici (voir openCashDrawer).
|
|
222
|
+
}
|
|
223
|
+
is com.delicity.thermalprinter.model.PrintItem.Image -> {
|
|
224
|
+
// Les images inline dans printText ne sont pas pré-rendues ici ;
|
|
225
|
+
// l'app doit utiliser printImage pour un rendu maîtrisé.
|
|
226
|
+
}
|
|
227
|
+
is com.delicity.thermalprinter.model.PrintItem.Raw -> {
|
|
228
|
+
// StarXpand n'expose pas d'injection ESC/POS brute fiable -> ignoré.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private suspend fun sendCommands(printer: StarPrinter, commands: String, profile: PrinterProfile) {
|
|
234
|
+
try {
|
|
235
|
+
printer.printAsync(commands).await()
|
|
236
|
+
} catch (e: Exception) {
|
|
237
|
+
throw PrinterException(ErrorCode.PRINT_FAILED, "Impression Star échouée", e.message, retryable = true)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
// Statut
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
|
|
246
|
+
val printer = connections[profile.id]
|
|
247
|
+
?: return PrinterStatus(profile.id, "disconnected", online = false, paper = "unknown")
|
|
248
|
+
return try {
|
|
249
|
+
val st = printer.getStatusAsync().await()
|
|
250
|
+
val paperEmpty = runCatching { st.paperEmpty }.getOrDefault(false)
|
|
251
|
+
val paperNear = runCatching { st.paperNearEmpty }.getOrDefault(false)
|
|
252
|
+
val coverOpen = runCatching { st.coverOpen }.getOrDefault(false)
|
|
253
|
+
PrinterStatus(
|
|
254
|
+
id = profile.id,
|
|
255
|
+
connection = "connected",
|
|
256
|
+
online = !paperEmpty && !coverOpen,
|
|
257
|
+
paper = if (paperEmpty) "empty" else if (paperNear) "near_end" else "ok",
|
|
258
|
+
coverOpen = coverOpen,
|
|
259
|
+
errorCode = if (paperEmpty) ErrorCode.PAPER_EMPTY else if (coverOpen) ErrorCode.COVER_OPEN else null,
|
|
260
|
+
rawStatus = st.toString(),
|
|
261
|
+
)
|
|
262
|
+
} catch (e: Exception) {
|
|
263
|
+
PrinterStatus(profile.id, "error", online = false, paper = "unknown", rawStatus = e.message)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// -------------------------------------------------------------------------
|
|
268
|
+
// Helpers
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
private fun requireConnected(profile: PrinterProfile): StarPrinter =
|
|
272
|
+
connections[profile.id] ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Star non connecté: ${profile.id}")
|
|
273
|
+
|
|
274
|
+
private fun connectionSettingsFor(profile: PrinterProfile): StarConnectionSettings {
|
|
275
|
+
val iface = when (profile.transport) {
|
|
276
|
+
Transport.WIFI, Transport.ETHERNET -> InterfaceType.Lan
|
|
277
|
+
Transport.BLUETOOTH -> InterfaceType.Bluetooth
|
|
278
|
+
Transport.BLE -> InterfaceType.BluetoothLE
|
|
279
|
+
Transport.USB -> InterfaceType.Usb
|
|
280
|
+
}
|
|
281
|
+
// Pour le LAN, l'identifier Star est l'IP (sans port).
|
|
282
|
+
val identifier = if (iface == InterfaceType.Lan) profile.address.substringBefore(":") else profile.address
|
|
283
|
+
return StarConnectionSettings(iface, identifier)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private fun transportFor(iface: InterfaceType): Transport = when (iface) {
|
|
287
|
+
InterfaceType.Lan -> Transport.WIFI
|
|
288
|
+
InterfaceType.Bluetooth -> Transport.BLUETOOTH
|
|
289
|
+
InterfaceType.BluetoothLE -> Transport.BLE
|
|
290
|
+
InterfaceType.Usb -> Transport.USB
|
|
291
|
+
else -> Transport.WIFI
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private fun alignmentFor(align: String?): Alignment = when (align) {
|
|
295
|
+
"center" -> Alignment.Center
|
|
296
|
+
"right" -> Alignment.Right
|
|
297
|
+
else -> Alignment.Left
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private fun qrLevelFor(ec: String): QRCodeLevel = when (ec.uppercase()) {
|
|
301
|
+
"L" -> QRCodeLevel.L
|
|
302
|
+
"Q" -> QRCodeLevel.Q
|
|
303
|
+
"H" -> QRCodeLevel.H
|
|
304
|
+
else -> QRCodeLevel.M
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private fun barcodeSymbologyFor(symbology: String): BarcodeSymbology = when (symbology.uppercase()) {
|
|
308
|
+
"CODE39" -> BarcodeSymbology.Code39
|
|
309
|
+
"CODE93" -> BarcodeSymbology.Code93
|
|
310
|
+
"EAN13", "JAN13" -> BarcodeSymbology.Jan13
|
|
311
|
+
"EAN8", "JAN8" -> BarcodeSymbology.Jan8
|
|
312
|
+
"ITF" -> BarcodeSymbology.Itf
|
|
313
|
+
"UPCA" -> BarcodeSymbology.UpcA
|
|
314
|
+
"UPCE" -> BarcodeSymbology.UpcE
|
|
315
|
+
"NW7", "CODABAR" -> BarcodeSymbology.Nw7
|
|
316
|
+
else -> BarcodeSymbology.Code128
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private fun ensureSdk() {
|
|
320
|
+
if (!isAvailable()) throw PrinterException(ErrorCode.SDK_NOT_AVAILABLE, "SDK Star absent")
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.adapters
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.content.IntentFilter
|
|
8
|
+
import android.graphics.Bitmap
|
|
9
|
+
import android.hardware.usb.UsbConstants
|
|
10
|
+
import android.hardware.usb.UsbDevice
|
|
11
|
+
import android.hardware.usb.UsbDeviceConnection
|
|
12
|
+
import android.hardware.usb.UsbEndpoint
|
|
13
|
+
import android.hardware.usb.UsbInterface
|
|
14
|
+
import android.hardware.usb.UsbManager
|
|
15
|
+
import android.os.Build
|
|
16
|
+
import com.delicity.thermalprinter.image.ImageProcessor
|
|
17
|
+
import com.delicity.thermalprinter.model.AdapterId
|
|
18
|
+
import com.delicity.thermalprinter.model.DiscoveredPrinter
|
|
19
|
+
import com.delicity.thermalprinter.model.ErrorCode
|
|
20
|
+
import com.delicity.thermalprinter.model.PrinterException
|
|
21
|
+
import com.delicity.thermalprinter.model.PrinterProfile
|
|
22
|
+
import com.delicity.thermalprinter.model.PrinterStatus
|
|
23
|
+
import com.delicity.thermalprinter.model.RenderOptions
|
|
24
|
+
import com.delicity.thermalprinter.model.Transport
|
|
25
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
26
|
+
import kotlin.coroutines.resume
|
|
27
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Adapter USB (Android host) pour imprimantes ESC/POS branchées en USB.
|
|
31
|
+
*
|
|
32
|
+
* Détecte les périphériques de classe imprimante (USB class 7), demande la
|
|
33
|
+
* permission runtime, réclame l'interface et écrit le raster ESC/POS sur
|
|
34
|
+
* l'endpoint bulk OUT via bulkTransfer (par paquets).
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ ANDROID UNIQUEMENT (iOS n'expose pas l'USB host pour ce cas).
|
|
37
|
+
*/
|
|
38
|
+
class UsbAdapter(private val context: Context) : PrinterAdapter {
|
|
39
|
+
|
|
40
|
+
override val id = AdapterId.ESCPOS
|
|
41
|
+
private val usbManager get() = context.getSystemService(Context.USB_SERVICE) as UsbManager
|
|
42
|
+
|
|
43
|
+
/** Connexions ouvertes indexées par printerId ("usb:vendorId:productId"). */
|
|
44
|
+
private val connections = ConcurrentHashMap<String, UsbLink>()
|
|
45
|
+
|
|
46
|
+
private data class UsbLink(
|
|
47
|
+
val connection: UsbDeviceConnection,
|
|
48
|
+
val iface: UsbInterface,
|
|
49
|
+
val endpointOut: UsbEndpoint,
|
|
50
|
+
val endpointIn: UsbEndpoint?,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
override fun isAvailable(): Boolean =
|
|
54
|
+
context.packageManager.hasSystemFeature(android.content.pm.PackageManager.FEATURE_USB_HOST)
|
|
55
|
+
|
|
56
|
+
override fun supportsTextItems(): Boolean = true
|
|
57
|
+
|
|
58
|
+
override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
|
|
59
|
+
if (!isAvailable()) return
|
|
60
|
+
usbManager.deviceList.values.forEach { device ->
|
|
61
|
+
val isPrinter = (0 until device.interfaceCount).any { device.getInterface(it).interfaceClass == UsbConstants.USB_CLASS_PRINTER }
|
|
62
|
+
if (isPrinter) {
|
|
63
|
+
onFound(
|
|
64
|
+
DiscoveredPrinter(
|
|
65
|
+
id = "usb:${device.vendorId}:${device.productId}",
|
|
66
|
+
name = device.productName ?: "USB Printer",
|
|
67
|
+
brand = device.manufacturerName,
|
|
68
|
+
transport = Transport.USB,
|
|
69
|
+
adapter = AdapterId.ESCPOS,
|
|
70
|
+
address = "${device.vendorId}:${device.productId}",
|
|
71
|
+
discoveredBy = mutableSetOf(AdapterId.ESCPOS),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun canHandle(profile: PrinterProfile): Boolean = profile.transport == Transport.USB
|
|
79
|
+
|
|
80
|
+
override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
|
|
81
|
+
ensureUsb()
|
|
82
|
+
if (isConnected(profile.id)) return
|
|
83
|
+
|
|
84
|
+
val device = findDevice(profile.address)
|
|
85
|
+
?: throw PrinterException(ErrorCode.PRINTER_NOT_FOUND, "Périphérique USB introuvable: ${profile.address}")
|
|
86
|
+
|
|
87
|
+
if (!usbManager.hasPermission(device)) {
|
|
88
|
+
val granted = requestPermission(device, timeoutMs)
|
|
89
|
+
if (!granted) throw PrinterException(ErrorCode.PERMISSION_DENIED, "Permission USB refusée", retryable = true)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
val iface = (0 until device.interfaceCount)
|
|
93
|
+
.map { device.getInterface(it) }
|
|
94
|
+
.firstOrNull { it.interfaceClass == UsbConstants.USB_CLASS_PRINTER }
|
|
95
|
+
?: throw PrinterException(ErrorCode.UNSUPPORTED_PRINTER, "Aucune interface imprimante USB")
|
|
96
|
+
|
|
97
|
+
var endpointOut: UsbEndpoint? = null
|
|
98
|
+
var endpointIn: UsbEndpoint? = null
|
|
99
|
+
for (i in 0 until iface.endpointCount) {
|
|
100
|
+
val ep = iface.getEndpoint(i)
|
|
101
|
+
if (ep.type == UsbConstants.USB_ENDPOINT_XFER_BULK) {
|
|
102
|
+
if (ep.direction == UsbConstants.USB_DIR_OUT) endpointOut = ep
|
|
103
|
+
if (ep.direction == UsbConstants.USB_DIR_IN) endpointIn = ep
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (endpointOut == null) throw PrinterException(ErrorCode.UNSUPPORTED_PRINTER, "Endpoint USB bulk OUT introuvable")
|
|
107
|
+
|
|
108
|
+
val connection = usbManager.openDevice(device)
|
|
109
|
+
?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Ouverture USB échouée", retryable = true)
|
|
110
|
+
if (!connection.claimInterface(iface, true)) {
|
|
111
|
+
connection.close()
|
|
112
|
+
throw PrinterException(ErrorCode.CONNECTION_FAILED, "claimInterface USB échoué", retryable = true)
|
|
113
|
+
}
|
|
114
|
+
connections[profile.id] = UsbLink(connection, iface, endpointOut, endpointIn)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override fun isConnected(printerId: String): Boolean = connections.containsKey(printerId)
|
|
118
|
+
|
|
119
|
+
override suspend fun disconnect(printerId: String) {
|
|
120
|
+
connections.remove(printerId)?.let { link ->
|
|
121
|
+
runCatching { link.connection.releaseInterface(link.iface) }
|
|
122
|
+
runCatching { link.connection.close() }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
|
|
127
|
+
val link = requireLink(profile)
|
|
128
|
+
val mono = ImageProcessor.toMono(bitmap, options)
|
|
129
|
+
val raster = ImageProcessor.encodeEscPosRaster(mono)
|
|
130
|
+
val job = EscPosCommands.buildJob(
|
|
131
|
+
rasterData = raster,
|
|
132
|
+
align = options.align,
|
|
133
|
+
feedLines = options.feedLines,
|
|
134
|
+
cut = options.cut && profile.capabilities.supportsCut,
|
|
135
|
+
openDrawer = options.openCashDrawer && profile.capabilities.supportsCashDrawer,
|
|
136
|
+
)
|
|
137
|
+
var sent = 0
|
|
138
|
+
repeat(options.copies.coerceAtLeast(1)) { sent += writeBulk(link, job) }
|
|
139
|
+
return sent
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override suspend fun printItems(
|
|
143
|
+
profile: PrinterProfile,
|
|
144
|
+
items: List<com.delicity.thermalprinter.model.PrintItem>,
|
|
145
|
+
defaultCodePage: String,
|
|
146
|
+
cut: Boolean,
|
|
147
|
+
feedLines: Int,
|
|
148
|
+
): Int {
|
|
149
|
+
val link = requireLink(profile)
|
|
150
|
+
val columns = if (profile.capabilities.printableDots <= 420) 32 else 48
|
|
151
|
+
val encoded = EscPosTextEncoder.encode(items, defaultCodePage, columns)
|
|
152
|
+
val out = java.io.ByteArrayOutputStream()
|
|
153
|
+
out.write(encoded.bytes)
|
|
154
|
+
if (feedLines > 0) out.write(EscPosCommands.feed(feedLines))
|
|
155
|
+
if (cut && profile.capabilities.supportsCut) out.write(EscPosCommands.CUT_PARTIAL)
|
|
156
|
+
return writeBulk(link, out.toByteArray())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
|
|
160
|
+
val link = connections[profile.id]
|
|
161
|
+
?: return PrinterStatus(profile.id, "disconnected", online = false, paper = "unknown")
|
|
162
|
+
// DLE EOT 4 -> statut papier si un endpoint IN est exposé (rare en USB).
|
|
163
|
+
val epIn = link.endpointIn
|
|
164
|
+
?: return PrinterStatus(profile.id, "connected", online = true, paper = "unknown", rawStatus = "no-in-endpoint")
|
|
165
|
+
return try {
|
|
166
|
+
writeBulk(link, EscPosCommands.realtimeStatus(4))
|
|
167
|
+
val buf = ByteArray(8)
|
|
168
|
+
val n = link.connection.bulkTransfer(epIn, buf, buf.size, 1500)
|
|
169
|
+
if (n <= 0) {
|
|
170
|
+
PrinterStatus(profile.id, "connected", online = true, paper = "unknown", rawStatus = "no-response")
|
|
171
|
+
} else {
|
|
172
|
+
val b = buf[0].toInt()
|
|
173
|
+
val paperEmpty = (b and 0x60) != 0
|
|
174
|
+
PrinterStatus(
|
|
175
|
+
profile.id, "connected", online = true,
|
|
176
|
+
paper = if (paperEmpty) "empty" else "ok",
|
|
177
|
+
errorCode = if (paperEmpty) ErrorCode.PAPER_EMPTY else null,
|
|
178
|
+
rawStatus = "0x%02X".format(b),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
} catch (e: Exception) {
|
|
182
|
+
PrinterStatus(profile.id, "error", online = false, paper = "unknown", rawStatus = e.message)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
// Helpers
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
private fun requireLink(profile: PrinterProfile): UsbLink =
|
|
191
|
+
connections[profile.id] ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "USB non connecté: ${profile.id}")
|
|
192
|
+
|
|
193
|
+
/** Écrit un buffer sur l'endpoint bulk OUT par paquets (taille = maxPacketSize). */
|
|
194
|
+
private fun writeBulk(link: UsbLink, data: ByteArray): Int {
|
|
195
|
+
val ep = link.endpointOut
|
|
196
|
+
val chunk = if (ep.maxPacketSize > 0) ep.maxPacketSize else 16384
|
|
197
|
+
var offset = 0
|
|
198
|
+
while (offset < data.size) {
|
|
199
|
+
val len = minOf(chunk, data.size - offset)
|
|
200
|
+
val slice = if (offset == 0 && len == data.size) data else data.copyOfRange(offset, offset + len)
|
|
201
|
+
val n = link.connection.bulkTransfer(ep, slice, len, 5000)
|
|
202
|
+
if (n < 0) throw PrinterException(ErrorCode.PRINT_FAILED, "Écriture USB échouée (bulkTransfer=$n)", retryable = true)
|
|
203
|
+
offset += len
|
|
204
|
+
}
|
|
205
|
+
return data.size
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun findDevice(address: String): UsbDevice? {
|
|
209
|
+
val parts = address.split(":")
|
|
210
|
+
if (parts.size < 2) return null
|
|
211
|
+
val vid = parts[0].toIntOrNull() ?: return null
|
|
212
|
+
val pid = parts[1].toIntOrNull() ?: return null
|
|
213
|
+
return usbManager.deviceList.values.firstOrNull { it.vendorId == vid && it.productId == pid }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Demande la permission USB runtime et suspend jusqu'à la réponse (ou timeout). */
|
|
217
|
+
private suspend fun requestPermission(device: UsbDevice, timeoutMs: Long): Boolean =
|
|
218
|
+
suspendCancellableCoroutine { cont ->
|
|
219
|
+
val action = "$ACTION_USB_PERMISSION.${device.deviceId}"
|
|
220
|
+
val receiver = object : BroadcastReceiver() {
|
|
221
|
+
override fun onReceive(ctx: Context, intent: Intent) {
|
|
222
|
+
if (intent.action != action) return
|
|
223
|
+
runCatching { context.unregisterReceiver(this) }
|
|
224
|
+
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
|
225
|
+
if (cont.isActive) cont.resume(granted)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
val filter = IntentFilter(action)
|
|
229
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
230
|
+
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
231
|
+
} else {
|
|
232
|
+
@Suppress("UnspecifiedRegisterReceiverFlag")
|
|
233
|
+
context.registerReceiver(receiver, filter)
|
|
234
|
+
}
|
|
235
|
+
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
|
|
236
|
+
val pi = PendingIntent.getBroadcast(context, 0, Intent(action).setPackage(context.packageName), flags)
|
|
237
|
+
cont.invokeOnCancellation { runCatching { context.unregisterReceiver(receiver) } }
|
|
238
|
+
usbManager.requestPermission(device, pi)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private fun ensureUsb() {
|
|
242
|
+
if (!isAvailable()) throw PrinterException(ErrorCode.UNSUPPORTED_TRANSPORT, "USB host indisponible")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
companion object {
|
|
246
|
+
private const val ACTION_USB_PERMISSION = "com.delicity.thermalprinter.USB_PERMISSION"
|
|
247
|
+
}
|
|
248
|
+
}
|