@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,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
|
+
}
|