@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.
Files changed (100) hide show
  1. package/DelicityThermalPrinter.podspec +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +649 -0
  4. package/android/build.gradle +122 -0
  5. package/android/src/main/AndroidManifest.xml +38 -0
  6. package/android/src/main/java/com/delicity/thermalprinter/Logger.kt +50 -0
  7. package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterEngine.kt +528 -0
  8. package/android/src/main/java/com/delicity/thermalprinter/ThermalPrinterPlugin.kt +334 -0
  9. package/android/src/main/java/com/delicity/thermalprinter/adapters/BleAdapter.kt +125 -0
  10. package/android/src/main/java/com/delicity/thermalprinter/adapters/BrotherAdapter.kt +206 -0
  11. package/android/src/main/java/com/delicity/thermalprinter/adapters/EpsonAdapter.kt +384 -0
  12. package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosAdapter.kt +160 -0
  13. package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosCommands.kt +42 -0
  14. package/android/src/main/java/com/delicity/thermalprinter/adapters/EscPosTextEncoder.kt +138 -0
  15. package/android/src/main/java/com/delicity/thermalprinter/adapters/PrinterAdapter.kt +95 -0
  16. package/android/src/main/java/com/delicity/thermalprinter/adapters/RawTcpAdapter.kt +96 -0
  17. package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkContract.kt +158 -0
  18. package/android/src/main/java/com/delicity/thermalprinter/adapters/SdkReflect.kt +104 -0
  19. package/android/src/main/java/com/delicity/thermalprinter/adapters/StarAdapter.kt +322 -0
  20. package/android/src/main/java/com/delicity/thermalprinter/adapters/UsbAdapter.kt +248 -0
  21. package/android/src/main/java/com/delicity/thermalprinter/adapters/ZebraAdapter.kt +207 -0
  22. package/android/src/main/java/com/delicity/thermalprinter/discovery/AdapterPriority.kt +39 -0
  23. package/android/src/main/java/com/delicity/thermalprinter/discovery/BleScanner.kt +70 -0
  24. package/android/src/main/java/com/delicity/thermalprinter/discovery/BluetoothClassicScanner.kt +112 -0
  25. package/android/src/main/java/com/delicity/thermalprinter/discovery/DiscoveryManager.kt +136 -0
  26. package/android/src/main/java/com/delicity/thermalprinter/discovery/TcpScanner.kt +96 -0
  27. package/android/src/main/java/com/delicity/thermalprinter/image/ImageCache.kt +88 -0
  28. package/android/src/main/java/com/delicity/thermalprinter/image/ImageProcessor.kt +220 -0
  29. package/android/src/main/java/com/delicity/thermalprinter/image/TextRasterizer.kt +99 -0
  30. package/android/src/main/java/com/delicity/thermalprinter/model/Models.kt +206 -0
  31. package/android/src/main/java/com/delicity/thermalprinter/model/PrintItem.kt +100 -0
  32. package/android/src/main/java/com/delicity/thermalprinter/store/PrinterStore.kt +71 -0
  33. package/android/src/main/java/com/delicity/thermalprinter/transport/BleGattClient.kt +201 -0
  34. package/android/src/main/java/com/delicity/thermalprinter/transport/BluetoothSppTransport.kt +110 -0
  35. package/android/src/main/java/com/delicity/thermalprinter/transport/ByteTransport.kt +18 -0
  36. package/android/src/main/java/com/delicity/thermalprinter/transport/TcpTransport.kt +83 -0
  37. package/dist/esm/adapters/dedup.d.ts +26 -0
  38. package/dist/esm/adapters/dedup.js +66 -0
  39. package/dist/esm/adapters/dedup.js.map +1 -0
  40. package/dist/esm/adapters/priority.d.ts +29 -0
  41. package/dist/esm/adapters/priority.js +55 -0
  42. package/dist/esm/adapters/priority.js.map +1 -0
  43. package/dist/esm/core/enums.d.ts +61 -0
  44. package/dist/esm/core/enums.js +25 -0
  45. package/dist/esm/core/enums.js.map +1 -0
  46. package/dist/esm/core/errors.d.ts +16 -0
  47. package/dist/esm/core/errors.js +53 -0
  48. package/dist/esm/core/errors.js.map +1 -0
  49. package/dist/esm/core/escpos-text.d.ts +33 -0
  50. package/dist/esm/core/escpos-text.js +239 -0
  51. package/dist/esm/core/escpos-text.js.map +1 -0
  52. package/dist/esm/core/imaging.d.ts +91 -0
  53. package/dist/esm/core/imaging.js +184 -0
  54. package/dist/esm/core/imaging.js.map +1 -0
  55. package/dist/esm/core/models.d.ts +131 -0
  56. package/dist/esm/core/models.js +2 -0
  57. package/dist/esm/core/models.js.map +1 -0
  58. package/dist/esm/core/options.d.ts +154 -0
  59. package/dist/esm/core/options.js +2 -0
  60. package/dist/esm/core/options.js.map +1 -0
  61. package/dist/esm/core/text.d.ts +138 -0
  62. package/dist/esm/core/text.js +14 -0
  63. package/dist/esm/core/text.js.map +1 -0
  64. package/dist/esm/definitions.d.ts +155 -0
  65. package/dist/esm/definitions.js +2 -0
  66. package/dist/esm/definitions.js.map +1 -0
  67. package/dist/esm/index.d.ts +15 -0
  68. package/dist/esm/index.js +18 -0
  69. package/dist/esm/index.js.map +1 -0
  70. package/dist/esm/web.d.ts +63 -0
  71. package/dist/esm/web.js +112 -0
  72. package/dist/esm/web.js.map +1 -0
  73. package/dist/plugin.cjs.js +224 -0
  74. package/dist/plugin.cjs.js.map +1 -0
  75. package/dist/plugin.js +227 -0
  76. package/dist/plugin.js.map +1 -0
  77. package/ios/Plugin/Adapters/BrotherAdapter.swift +139 -0
  78. package/ios/Plugin/Adapters/EpsonAdapter.swift +131 -0
  79. package/ios/Plugin/Adapters/EscPosAdapter.swift +106 -0
  80. package/ios/Plugin/Adapters/EscPosCommands.swift +32 -0
  81. package/ios/Plugin/Adapters/EscPosTextEncoder.swift +115 -0
  82. package/ios/Plugin/Adapters/PrinterAdapter.swift +44 -0
  83. package/ios/Plugin/Adapters/RawTcpAdapter.swift +70 -0
  84. package/ios/Plugin/Adapters/StarAdapter.swift +305 -0
  85. package/ios/Plugin/Adapters/ZebraAdapter.swift +119 -0
  86. package/ios/Plugin/Discovery/AdapterPriority.swift +21 -0
  87. package/ios/Plugin/Discovery/BonjourScanner.swift +51 -0
  88. package/ios/Plugin/Discovery/DiscoveryManager.swift +86 -0
  89. package/ios/Plugin/Image/ImageCache.swift +73 -0
  90. package/ios/Plugin/Image/ImageProcessor.swift +168 -0
  91. package/ios/Plugin/Image/TextRasterizer.swift +81 -0
  92. package/ios/Plugin/Logger.swift +33 -0
  93. package/ios/Plugin/Model/Models.swift +174 -0
  94. package/ios/Plugin/Model/PrintItem.swift +111 -0
  95. package/ios/Plugin/Store/PrinterStore.swift +51 -0
  96. package/ios/Plugin/ThermalPrinterEngine.swift +395 -0
  97. package/ios/Plugin/ThermalPrinterPlugin.m +22 -0
  98. package/ios/Plugin/ThermalPrinterPlugin.swift +258 -0
  99. package/ios/Plugin/Transport/TcpTransport.swift +89 -0
  100. package/package.json +96 -0
@@ -0,0 +1,384 @@
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 java.util.concurrent.ConcurrentHashMap
14
+ import kotlinx.coroutines.delay
15
+
16
+ /**
17
+ * Adapter Epson basé sur le SDK ePOS2 (`com.epson.epos2`), piloté par RÉFLEXION.
18
+ *
19
+ * Le SDK ePOS2 n'est PAS redistribuable (licence Epson) : il n'est donc pas une
20
+ * dépendance de compilation. L'app doit déposer `ePOS2.jar` (+ `.so`) — voir
21
+ * docs/SDK_INTEGRATION.md (§ Epson). Tant que le binaire est absent,
22
+ * `isAvailable()` renvoie false et l'adapter est ignoré (fallback ESC/POS).
23
+ *
24
+ * Chemin principal : impression IMAGE (réception rendue en bitmap). Le texte stylé
25
+ * SDK (printItems) n'est pas mappé ici -> utiliser printImage pour les marques SDK.
26
+ */
27
+ class EpsonAdapter(private val context: Context) : PrinterAdapter {
28
+
29
+ override val id = AdapterId.EPSON
30
+
31
+ private val cache = ConcurrentHashMap<String, Any>() // printerId -> com.epson.epos2.printer.Printer
32
+
33
+ override fun isAvailable(): Boolean = classExists(PRINTER)
34
+
35
+ override fun supportsTextItems(): Boolean = isAvailable()
36
+
37
+ // -------------------------------------------------------------------------
38
+ // Découverte (Discovery.start + DiscoveryListener via proxy)
39
+ // -------------------------------------------------------------------------
40
+
41
+ override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
42
+ if (!isAvailable()) return
43
+ val filter = SdkReflect.newInstance(FILTER_OPTION, emptyArray(), emptyArray())
44
+ runCatching {
45
+ SdkReflect.call(filter, "setDeviceType", arrayOf(Int::class.javaPrimitiveType!!),
46
+ arrayOf(SdkReflect.staticInt(DISCOVERY, "TYPE_PRINTER", 0)))
47
+ }
48
+ val listener = SdkReflect.proxy(DISCOVERY_LISTENER, mapOf(
49
+ "onDiscovery" to { args ->
50
+ val info = args.getOrNull(0)
51
+ if (info != null) {
52
+ val target = SdkReflect.call(info, "getTarget") as? String ?: ""
53
+ val name = SdkReflect.call(info, "getDeviceName") as? String ?: "Epson"
54
+ val transport = when {
55
+ target.startsWith("BT:") -> Transport.BLUETOOTH
56
+ target.startsWith("USB:") -> Transport.USB
57
+ else -> Transport.WIFI
58
+ }
59
+ onFound(
60
+ DiscoveredPrinter(
61
+ id = "epson:$target",
62
+ name = name,
63
+ brand = "Epson",
64
+ model = name,
65
+ transport = transport,
66
+ adapter = AdapterId.EPSON,
67
+ address = target,
68
+ discoveredBy = mutableSetOf(AdapterId.EPSON),
69
+ ),
70
+ )
71
+ }
72
+ null
73
+ },
74
+ ))
75
+ try {
76
+ SdkReflect.callStatic(
77
+ DISCOVERY, "start",
78
+ arrayOf(Context::class.java, SdkReflect.classOrNull(FILTER_OPTION)!!, SdkReflect.classOrNull(DISCOVERY_LISTENER)!!),
79
+ arrayOf(context, filter, listener),
80
+ )
81
+ delay(timeoutMs)
82
+ } catch (e: Throwable) {
83
+ throw PrinterException(ErrorCode.UNKNOWN, "Découverte Epson échouée", e.message)
84
+ } finally {
85
+ runCatching { SdkReflect.callStatic(DISCOVERY, "stop") }
86
+ }
87
+ }
88
+
89
+ override fun canHandle(profile: PrinterProfile): Boolean =
90
+ isAvailable() && profile.adapter == AdapterId.EPSON
91
+
92
+ // -------------------------------------------------------------------------
93
+ // Connexion
94
+ // -------------------------------------------------------------------------
95
+
96
+ override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
97
+ ensureSdk()
98
+ if (isConnected(profile.id)) return
99
+ val series = seriesConstFor(profile.model)
100
+ val lang = SdkReflect.staticInt(PRINTER, "MODEL_ANK", 0)
101
+ val printer = SdkReflect.newInstance(
102
+ PRINTER,
103
+ arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!, Context::class.java),
104
+ arrayOf(series, lang, context),
105
+ )
106
+ try {
107
+ SdkReflect.call(
108
+ printer, "connect",
109
+ arrayOf(String::class.java, Int::class.javaPrimitiveType!!),
110
+ arrayOf(targetFor(profile), SdkReflect.staticInt(PRINTER, "PARAM_DEFAULT", -2)),
111
+ )
112
+ } catch (e: Throwable) {
113
+ throw PrinterException(ErrorCode.CONNECTION_FAILED, "Connexion Epson échouée: ${profile.address}", e.message, retryable = true)
114
+ }
115
+ cache[profile.id] = printer
116
+ }
117
+
118
+ override fun isConnected(printerId: String): Boolean = cache.containsKey(printerId)
119
+
120
+ override suspend fun disconnect(printerId: String) {
121
+ cache.remove(printerId)?.let { printer ->
122
+ runCatching { SdkReflect.call(printer, "disconnect") }
123
+ runCatching { SdkReflect.call(printer, "clearCommandBuffer") }
124
+ }
125
+ }
126
+
127
+ // -------------------------------------------------------------------------
128
+ // Impression image
129
+ // -------------------------------------------------------------------------
130
+
131
+ override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
132
+ val printer = cache[profile.id]
133
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Epson non connecté: ${profile.id}")
134
+ val intT = Int::class.javaPrimitiveType!!
135
+ val dblT = Double::class.javaPrimitiveType!!
136
+ try {
137
+ repeat(options.copies.coerceAtLeast(1)) {
138
+ SdkReflect.call(printer, "beginTransaction")
139
+ SdkReflect.call(
140
+ printer, "addImage",
141
+ arrayOf(Bitmap::class.java, intT, intT, intT, intT, intT, intT, intT, dblT, intT),
142
+ arrayOf(
143
+ bitmap, 0, 0, bitmap.width, bitmap.height,
144
+ SdkReflect.staticInt(PRINTER, "COLOR_1", 1),
145
+ SdkReflect.staticInt(PRINTER, "MODE_MONO", 0),
146
+ halftoneFor(options.dithering),
147
+ 1.0,
148
+ SdkReflect.staticInt(PRINTER, "COMPRESS_AUTO", 0),
149
+ ),
150
+ )
151
+ if (options.cut && profile.capabilities.supportsCut) {
152
+ SdkReflect.call(printer, "addCut", arrayOf(intT), arrayOf(SdkReflect.staticInt(PRINTER, "CUT_FEED", 1)))
153
+ }
154
+ if (options.openCashDrawer && profile.capabilities.supportsCashDrawer) {
155
+ SdkReflect.call(
156
+ printer, "addPulse", arrayOf(intT, intT),
157
+ arrayOf(SdkReflect.staticInt(PRINTER, "DRAWER_2PIN", 0), SdkReflect.staticInt(PRINTER, "PULSE_100", 0)),
158
+ )
159
+ }
160
+ SdkReflect.call(printer, "sendData", arrayOf(intT), arrayOf(SdkReflect.staticInt(PRINTER, "PARAM_DEFAULT", -2)))
161
+ SdkReflect.call(printer, "endTransaction")
162
+ runCatching { SdkReflect.call(printer, "clearCommandBuffer") }
163
+ }
164
+ } catch (e: Throwable) {
165
+ runCatching { SdkReflect.call(printer, "clearCommandBuffer") }
166
+ throw PrinterException(ErrorCode.PRINT_FAILED, "Impression Epson échouée", e.message, retryable = true)
167
+ }
168
+ return bitmap.width * bitmap.height / 8
169
+ }
170
+
171
+ // -------------------------------------------------------------------------
172
+ // Impression texte stylé (builder ePOS2 natif)
173
+ // -------------------------------------------------------------------------
174
+
175
+ override suspend fun printItems(
176
+ profile: PrinterProfile,
177
+ items: List<com.delicity.thermalprinter.model.PrintItem>,
178
+ defaultCodePage: String,
179
+ cut: Boolean,
180
+ feedLines: Int,
181
+ ): Int {
182
+ val printer = cache[profile.id]
183
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Epson non connecté: ${profile.id}")
184
+ try {
185
+ SdkReflect.call(printer, "beginTransaction")
186
+ for (item in items) mapTextItem(printer, item, profile)
187
+ if (feedLines > 0) callInt(printer, "addFeedLine", feedLines)
188
+ if (cut && profile.capabilities.supportsCut) callInt(printer, "addCut", SdkReflect.staticInt(PRINTER, "CUT_FEED", 1))
189
+ callInt(printer, "sendData", SdkReflect.staticInt(PRINTER, "PARAM_DEFAULT", -2))
190
+ SdkReflect.call(printer, "endTransaction")
191
+ runCatching { SdkReflect.call(printer, "clearCommandBuffer") }
192
+ } catch (e: Throwable) {
193
+ runCatching { SdkReflect.call(printer, "clearCommandBuffer") }
194
+ throw PrinterException(ErrorCode.PRINT_FAILED, "Impression texte Epson échouée", e.message, retryable = true)
195
+ }
196
+ return items.size
197
+ }
198
+
199
+ private fun mapTextItem(printer: Any, item: com.delicity.thermalprinter.model.PrintItem, profile: PrinterProfile) {
200
+ val P = com.delicity.thermalprinter.model.PrintItem
201
+ val tru = SdkReflect.staticInt(PRINTER, "TRUE", 1)
202
+ val fls = SdkReflect.staticInt(PRINTER, "FALSE", 0)
203
+ when (item) {
204
+ is com.delicity.thermalprinter.model.PrintItem.Text -> {
205
+ val s = item.style
206
+ callInt(printer, "addTextAlign", alignConst(s.align))
207
+ SdkReflect.call(
208
+ printer, "addTextStyle",
209
+ arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!),
210
+ arrayOf(if (s.invert) tru else fls, if (s.underline != "none") tru else fls, if (s.bold) tru else fls, SdkReflect.staticInt(PRINTER, "COLOR_1", 1)),
211
+ )
212
+ SdkReflect.call(
213
+ printer, "addTextSize",
214
+ arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!),
215
+ arrayOf(s.widthMultiplier.coerceIn(1, 8), s.heightMultiplier.coerceIn(1, 8)),
216
+ )
217
+ callStr(printer, "addText", if (s.newline) item.value + "\n" else item.value)
218
+ }
219
+ is com.delicity.thermalprinter.model.PrintItem.Feed -> callInt(printer, "addFeedLine", item.lines.coerceIn(1, 255))
220
+ is com.delicity.thermalprinter.model.PrintItem.Cut ->
221
+ callInt(printer, "addCut", SdkReflect.staticInt(PRINTER, if (item.mode == "full") "CUT_NO_FEED" else "CUT_FEED", 1))
222
+ is com.delicity.thermalprinter.model.PrintItem.Divider -> {
223
+ val cols = item.columns ?: if (profile.capabilities.printableDots <= 420) 32 else 48
224
+ callInt(printer, "addTextAlign", alignConst(item.align))
225
+ callStr(printer, "addText", item.char.take(1).ifEmpty { "-" }.repeat(cols.coerceIn(1, 96)) + "\n")
226
+ }
227
+ is com.delicity.thermalprinter.model.PrintItem.QrCode -> {
228
+ callInt(printer, "addTextAlign", alignConst(item.align))
229
+ val i = Int::class.javaPrimitiveType!!
230
+ SdkReflect.call(
231
+ printer, "addSymbol",
232
+ arrayOf(String::class.java, i, i, i, i, i),
233
+ arrayOf(item.value, SdkReflect.staticInt(PRINTER, "SYMBOL_QRCODE_MODEL_2", 0), qrLevelConst(item.ec), item.size.coerceIn(1, 16), item.size.coerceIn(1, 16), 0),
234
+ )
235
+ }
236
+ is com.delicity.thermalprinter.model.PrintItem.Barcode -> {
237
+ callInt(printer, "addTextAlign", alignConst(item.align))
238
+ val i = Int::class.javaPrimitiveType!!
239
+ SdkReflect.call(
240
+ printer, "addBarcode",
241
+ arrayOf(String::class.java, i, i, i, i, i),
242
+ arrayOf(
243
+ item.value, barcodeConst(item.symbology),
244
+ SdkReflect.staticInt(PRINTER, if (item.hri == "none") "HRI_NONE" else "HRI_BELOW", 0),
245
+ SdkReflect.staticInt(PRINTER, "FONT_A", 0),
246
+ item.width.coerceIn(2, 6), item.height.coerceIn(1, 255),
247
+ ),
248
+ )
249
+ }
250
+ is com.delicity.thermalprinter.model.PrintItem.CashDrawer ->
251
+ SdkReflect.call(
252
+ printer, "addPulse",
253
+ arrayOf(Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!),
254
+ arrayOf(SdkReflect.staticInt(PRINTER, "DRAWER_2PIN", 0), SdkReflect.staticInt(PRINTER, "PULSE_100", 0)),
255
+ )
256
+ is com.delicity.thermalprinter.model.PrintItem.Image, is com.delicity.thermalprinter.model.PrintItem.Raw -> Unit
257
+ }
258
+ @Suppress("UNUSED_EXPRESSION") P
259
+ }
260
+
261
+ private fun callInt(target: Any, method: String, value: Int) =
262
+ SdkReflect.call(target, method, arrayOf(Int::class.javaPrimitiveType!!), arrayOf(value))
263
+
264
+ private fun callStr(target: Any, method: String, value: String) =
265
+ SdkReflect.call(target, method, arrayOf(String::class.java), arrayOf(value))
266
+
267
+ private fun alignConst(align: String?): Int = when (align) {
268
+ "center" -> SdkReflect.staticInt(PRINTER, "ALIGN_CENTER", 1)
269
+ "right" -> SdkReflect.staticInt(PRINTER, "ALIGN_RIGHT", 2)
270
+ else -> SdkReflect.staticInt(PRINTER, "ALIGN_LEFT", 0)
271
+ }
272
+
273
+ private fun qrLevelConst(ec: String): Int = when (ec.uppercase()) {
274
+ "L" -> SdkReflect.staticInt(PRINTER, "LEVEL_L", 0)
275
+ "Q" -> SdkReflect.staticInt(PRINTER, "LEVEL_Q", 2)
276
+ "H" -> SdkReflect.staticInt(PRINTER, "LEVEL_H", 3)
277
+ else -> SdkReflect.staticInt(PRINTER, "LEVEL_M", 1)
278
+ }
279
+
280
+ private fun barcodeConst(symbology: String): Int = when (symbology.uppercase()) {
281
+ "CODE39" -> SdkReflect.staticInt(PRINTER, "BARCODE_CODE39", 0)
282
+ "CODE93" -> SdkReflect.staticInt(PRINTER, "BARCODE_CODE93", 0)
283
+ "EAN13" -> SdkReflect.staticInt(PRINTER, "BARCODE_EAN13", 0)
284
+ "EAN8" -> SdkReflect.staticInt(PRINTER, "BARCODE_EAN8", 0)
285
+ "ITF" -> SdkReflect.staticInt(PRINTER, "BARCODE_ITF", 0)
286
+ "UPCA" -> SdkReflect.staticInt(PRINTER, "BARCODE_UPC_A", 0)
287
+ "UPCE" -> SdkReflect.staticInt(PRINTER, "BARCODE_UPC_E", 0)
288
+ "CODABAR" -> SdkReflect.staticInt(PRINTER, "BARCODE_CODABAR", 0)
289
+ else -> SdkReflect.staticInt(PRINTER, "BARCODE_CODE128", 0)
290
+ }
291
+
292
+ // -------------------------------------------------------------------------
293
+ // Statut
294
+ // -------------------------------------------------------------------------
295
+
296
+ override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
297
+ val printer = cache[profile.id]
298
+ ?: return PrinterStatus(profile.id, "disconnected", online = false, paper = "unknown")
299
+ return try {
300
+ val info = SdkReflect.call(printer, "getStatus")
301
+ ?: return PrinterStatus(profile.id, "connected", online = true, paper = "unknown")
302
+ val trueVal = SdkReflect.staticInt(PRINTER, "TRUE", 1)
303
+ val paperEmptyVal = SdkReflect.staticInt(PRINTER, "PAPER_EMPTY", 2)
304
+ val paperNearVal = SdkReflect.staticInt(PRINTER, "PAPER_NEAR_END", 1)
305
+ val connection = SdkReflect.intField(info, "connection", -1)
306
+ val online = SdkReflect.intField(info, "online", -1)
307
+ val paper = SdkReflect.intField(info, "paper", -1)
308
+ val coverOpen = SdkReflect.intField(info, "coverOpen", -1)
309
+ PrinterStatus(
310
+ id = profile.id,
311
+ connection = if (connection == trueVal) "connected" else "disconnected",
312
+ online = online == trueVal,
313
+ paper = when (paper) {
314
+ paperEmptyVal -> "empty"
315
+ paperNearVal -> "near_end"
316
+ else -> "ok"
317
+ },
318
+ coverOpen = coverOpen == trueVal,
319
+ errorCode = if (paper == paperEmptyVal) ErrorCode.PAPER_EMPTY else if (coverOpen == trueVal) ErrorCode.COVER_OPEN else null,
320
+ )
321
+ } catch (e: Throwable) {
322
+ PrinterStatus(profile.id, "error", online = false, paper = "unknown", rawStatus = e.message)
323
+ }
324
+ }
325
+
326
+ // -------------------------------------------------------------------------
327
+ // Helpers
328
+ // -------------------------------------------------------------------------
329
+
330
+ /** Cible ePOS2 selon le transport ("TCP:ip" / "BT:mac" / "USB:..."). */
331
+ private fun targetFor(profile: PrinterProfile): String {
332
+ if (profile.address.contains(":") &&
333
+ (profile.address.startsWith("TCP:") || profile.address.startsWith("BT:") || profile.address.startsWith("USB:"))
334
+ ) {
335
+ return profile.address
336
+ }
337
+ return when (profile.transport) {
338
+ Transport.WIFI, Transport.ETHERNET -> "TCP:${profile.address.substringBefore(":")}"
339
+ Transport.BLUETOOTH -> "BT:${profile.address}"
340
+ Transport.USB -> "USB:${profile.address}"
341
+ Transport.BLE -> "BT:${profile.address}"
342
+ }
343
+ }
344
+
345
+ /** Constante de série modèle (ex "TM_M30") lue par réflexion, fallback TM_m30/TM_T88. */
346
+ private fun seriesConstFor(model: String?): Int {
347
+ val candidates = buildList {
348
+ model?.uppercase()?.replace(" ", "")?.replace("-", "")?.let { m ->
349
+ Regex("TM[_]?([A-Z0-9]+)").find(m)?.let { add("TM_${it.groupValues[1]}") }
350
+ }
351
+ add("TM_M30")
352
+ add("TM_T88VI")
353
+ add("TM_T20")
354
+ }
355
+ for (name in candidates) {
356
+ val v = SdkReflect.staticInt(PRINTER, name, Int.MIN_VALUE)
357
+ if (v != Int.MIN_VALUE) return v
358
+ }
359
+ return 0
360
+ }
361
+
362
+ private fun halftoneFor(dithering: String): Int = when (dithering) {
363
+ "none" -> SdkReflect.staticInt(PRINTER, "HALFTONE_THRESHOLD", 1)
364
+ "atkinson", "floyd_steinberg" -> SdkReflect.staticInt(PRINTER, "HALFTONE_ERROR_DIFFUSION", 2)
365
+ else -> SdkReflect.staticInt(PRINTER, "HALFTONE_DITHER", 0)
366
+ }
367
+
368
+ private fun ensureSdk() {
369
+ if (!isAvailable()) throw PrinterException(ErrorCode.SDK_NOT_AVAILABLE, "SDK Epson ePOS2 absent")
370
+ }
371
+
372
+ companion object {
373
+ private const val PRINTER = "com.epson.epos2.printer.Printer"
374
+ private const val FILTER_OPTION = "com.epson.epos2.discovery.FilterOption"
375
+ private const val DISCOVERY = "com.epson.epos2.discovery.Discovery"
376
+ private const val DISCOVERY_LISTENER = "com.epson.epos2.discovery.DiscoveryListener"
377
+
378
+ fun classExists(name: String): Boolean = try {
379
+ Class.forName(name); true
380
+ } catch (e: Throwable) {
381
+ false
382
+ }
383
+ }
384
+ }
@@ -0,0 +1,160 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import android.bluetooth.BluetoothAdapter
4
+ import android.graphics.Bitmap
5
+ import com.delicity.thermalprinter.image.ImageProcessor
6
+ import com.delicity.thermalprinter.model.AdapterId
7
+ import com.delicity.thermalprinter.model.DiscoveredPrinter
8
+ import com.delicity.thermalprinter.model.ErrorCode
9
+ import com.delicity.thermalprinter.model.PrinterException
10
+ import com.delicity.thermalprinter.model.PrinterProfile
11
+ import com.delicity.thermalprinter.model.PrinterStatus
12
+ import com.delicity.thermalprinter.model.RenderOptions
13
+ import com.delicity.thermalprinter.model.Transport
14
+ import com.delicity.thermalprinter.transport.BluetoothSppTransport
15
+ import com.delicity.thermalprinter.transport.ByteTransport
16
+ import com.delicity.thermalprinter.transport.TcpTransport
17
+ import java.util.concurrent.ConcurrentHashMap
18
+
19
+ /**
20
+ * Adapter ESC/POS générique.
21
+ *
22
+ * Couvre la grande majorité des imprimantes thermiques bas/moyen de gamme via :
23
+ * - TCP 9100 (Wi-Fi / Ethernet)
24
+ * - Bluetooth Classic SPP (Android)
25
+ * - (BLE délégué à BleAdapter)
26
+ *
27
+ * La découverte propre à ESC/POS n'existe pas en tant que telle : ce sont les
28
+ * sources génériques (TcpScanner, BluetoothScanner) qui produisent des
29
+ * DiscoveredPrinter avec adapter=ESCPOS. Cet adapter se concentre sur la
30
+ * connexion + l'impression raster.
31
+ */
32
+ class EscPosAdapter(
33
+ private val btAdapter: BluetoothAdapter?,
34
+ ) : PrinterAdapter {
35
+
36
+ override val id = AdapterId.ESCPOS
37
+
38
+ /** Connexions vivantes indexées par printerId. */
39
+ private val connections = ConcurrentHashMap<String, ByteTransport>()
40
+
41
+ override fun isAvailable(): Boolean = true // pas de SDK requis
42
+
43
+ override fun supportsTextItems(): Boolean = true
44
+
45
+ override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
46
+ // Délégué aux scanners génériques (voir DiscoveryManager). No-op ici.
47
+ }
48
+
49
+ override fun canHandle(profile: PrinterProfile): Boolean =
50
+ profile.adapter == AdapterId.ESCPOS &&
51
+ profile.transport in setOf(Transport.WIFI, Transport.ETHERNET, Transport.BLUETOOTH)
52
+
53
+ override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
54
+ if (isConnected(profile.id)) return
55
+ val transport = buildTransport(profile)
56
+ transport.open(timeoutMs)
57
+ connections[profile.id] = transport
58
+ }
59
+
60
+ override fun isConnected(printerId: String): Boolean = connections[printerId]?.isOpen == true
61
+
62
+ override suspend fun disconnect(printerId: String) {
63
+ connections.remove(printerId)?.close()
64
+ }
65
+
66
+ override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
67
+ val transport = connections[profile.id]
68
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "ESC/POS non connecté: ${profile.id}")
69
+
70
+ // 1-bit + dithering -> raster GS v 0
71
+ val mono = ImageProcessor.toMono(bitmap, options)
72
+ val raster = ImageProcessor.encodeEscPosRaster(mono)
73
+ val job = EscPosCommands.buildJob(
74
+ rasterData = raster,
75
+ align = options.align,
76
+ feedLines = options.feedLines,
77
+ cut = options.cut && profile.capabilities.supportsCut,
78
+ openDrawer = options.openCashDrawer && profile.capabilities.supportsCashDrawer,
79
+ )
80
+
81
+ var sent = 0
82
+ repeat(options.copies.coerceAtLeast(1)) {
83
+ transport.write(job)
84
+ sent += job.size
85
+ }
86
+ return sent
87
+ }
88
+
89
+ override suspend fun printItems(
90
+ profile: PrinterProfile,
91
+ items: List<com.delicity.thermalprinter.model.PrintItem>,
92
+ defaultCodePage: String,
93
+ cut: Boolean,
94
+ feedLines: Int,
95
+ ): Int {
96
+ val transport = connections[profile.id]
97
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "ESC/POS non connecté: ${profile.id}")
98
+ // Colonnes selon largeur (police A ~ 12 dots/char): 384->32, 576->48.
99
+ val columns = if (profile.capabilities.printableDots <= 420) 32 else 48
100
+ val encoded = EscPosTextEncoder.encode(items, defaultCodePage, columns)
101
+ val out = java.io.ByteArrayOutputStream()
102
+ out.write(encoded.bytes)
103
+ if (feedLines > 0) out.write(EscPosCommands.feed(feedLines))
104
+ if (cut && profile.capabilities.supportsCut) out.write(EscPosCommands.CUT_PARTIAL)
105
+ val job = out.toByteArray()
106
+ transport.write(job)
107
+ return job.size
108
+ }
109
+
110
+ override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
111
+ val transport = connections[profile.id]
112
+ if (transport == null || !transport.isOpen) {
113
+ return PrinterStatus(
114
+ id = profile.id, connection = "disconnected", online = false, paper = "unknown",
115
+ )
116
+ }
117
+ // DLE EOT 4 -> statut papier (si l'imprimante le supporte sur ce transport).
118
+ return try {
119
+ transport.write(EscPosCommands.realtimeStatus(4))
120
+ val buf = ByteArray(8)
121
+ val n = transport.read(buf, 1500)
122
+ if (n <= 0) {
123
+ PrinterStatus(profile.id, "connected", online = true, paper = "unknown", rawStatus = "no-response")
124
+ } else {
125
+ val b = buf[0].toInt()
126
+ // bit 5/6 (0x60) indiquent fin papier sur la plupart des modèles ESC/POS
127
+ val paperEmpty = (b and 0x60) != 0
128
+ PrinterStatus(
129
+ id = profile.id, connection = "connected", online = true,
130
+ paper = if (paperEmpty) "empty" else "ok",
131
+ errorCode = if (paperEmpty) ErrorCode.PAPER_EMPTY else null,
132
+ rawStatus = "0x%02X".format(b),
133
+ )
134
+ }
135
+ } catch (e: Exception) {
136
+ PrinterStatus(profile.id, "error", online = false, paper = "unknown", rawStatus = e.message)
137
+ }
138
+ }
139
+
140
+ private fun buildTransport(profile: PrinterProfile): ByteTransport = when (profile.transport) {
141
+ Transport.WIFI, Transport.ETHERNET -> {
142
+ val (host, port) = splitHostPort(profile.address, 9100)
143
+ TcpTransport(host, port)
144
+ }
145
+ Transport.BLUETOOTH -> BluetoothSppTransport(btAdapter, profile.address)
146
+ else -> throw PrinterException(
147
+ ErrorCode.UNSUPPORTED_TRANSPORT,
148
+ "ESC/POS ne gère pas le transport ${profile.transport.value}",
149
+ )
150
+ }
151
+
152
+ private fun splitHostPort(addr: String, defaultPort: Int): Pair<String, Int> {
153
+ val idx = addr.lastIndexOf(':')
154
+ return if (idx > 0 && addr.indexOf(':') == idx) {
155
+ addr.substring(0, idx) to (addr.substring(idx + 1).toIntOrNull() ?: defaultPort)
156
+ } else {
157
+ addr to defaultPort
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,42 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import java.io.ByteArrayOutputStream
4
+
5
+ /** Constantes et helpers de commandes ESC/POS. */
6
+ object EscPosCommands {
7
+ val INIT = byteArrayOf(0x1B, 0x40) // ESC @
8
+ val ALIGN_LEFT = byteArrayOf(0x1B, 0x61, 0x00)
9
+ val ALIGN_CENTER = byteArrayOf(0x1B, 0x61, 0x01)
10
+ val ALIGN_RIGHT = byteArrayOf(0x1B, 0x61, 0x02)
11
+ val CUT_PARTIAL = byteArrayOf(0x1D, 0x56, 0x01) // GS V 1
12
+ val DRAWER_PIN2 = byteArrayOf(0x1B, 0x70, 0x00, 0x19.toByte(), 0xFA.toByte())
13
+
14
+ fun feed(lines: Int): ByteArray = byteArrayOf(0x1B, 0x64, lines.coerceIn(0, 255).toByte())
15
+
16
+ fun alignOf(align: String): ByteArray = when (align) {
17
+ "center" -> ALIGN_CENTER
18
+ "right" -> ALIGN_RIGHT
19
+ else -> ALIGN_LEFT
20
+ }
21
+
22
+ /** DLE EOT n : statut temps réel (1 imprimante, 2 offline, 3 erreur, 4 papier). */
23
+ fun realtimeStatus(n: Int): ByteArray = byteArrayOf(0x10, 0x04, n.toByte())
24
+
25
+ /** Assemble un job complet autour du raster déjà encodé. */
26
+ fun buildJob(
27
+ rasterData: ByteArray,
28
+ align: String,
29
+ feedLines: Int,
30
+ cut: Boolean,
31
+ openDrawer: Boolean,
32
+ ): ByteArray {
33
+ val out = ByteArrayOutputStream()
34
+ out.write(INIT)
35
+ out.write(alignOf(align))
36
+ out.write(rasterData)
37
+ out.write(feed(feedLines))
38
+ if (cut) out.write(CUT_PARTIAL)
39
+ if (openDrawer) out.write(DRAWER_PIN2)
40
+ return out.toByteArray()
41
+ }
42
+ }