@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,334 @@
1
+ package com.delicity.thermalprinter
2
+
3
+ import android.Manifest
4
+ import android.os.Build
5
+ import com.getcapacitor.JSArray
6
+ import com.getcapacitor.JSObject
7
+ import com.getcapacitor.Plugin
8
+ import com.getcapacitor.PluginCall
9
+ import com.getcapacitor.PluginMethod
10
+ import com.getcapacitor.annotation.CapacitorPlugin
11
+ import com.getcapacitor.annotation.Permission
12
+ import com.getcapacitor.annotation.PermissionCallback
13
+ import com.delicity.thermalprinter.discovery.DiscoveryManager
14
+ import com.delicity.thermalprinter.model.PrinterException
15
+ import com.delicity.thermalprinter.model.RenderOptions
16
+ import kotlinx.coroutines.CoroutineScope
17
+ import kotlinx.coroutines.Dispatchers
18
+ import kotlinx.coroutines.SupervisorJob
19
+ import kotlinx.coroutines.launch
20
+ import kotlinx.coroutines.withContext
21
+
22
+ /**
23
+ * Pont Capacitor (bridge JS <-> natif). Mappe l'API publique ThermalPrinterPlugin
24
+ * (definitions.ts) vers le ThermalPrinterEngine.
25
+ *
26
+ * Toutes les opérations longues s'exécutent sur Dispatchers.IO ; les erreurs
27
+ * PrinterException sont converties en rejets Capacitor avec le code normalisé.
28
+ */
29
+ @CapacitorPlugin(
30
+ name = "ThermalPrinter",
31
+ permissions = [
32
+ Permission(alias = "bluetoothScan", strings = [Manifest.permission.BLUETOOTH_SCAN]),
33
+ Permission(alias = "bluetoothConnect", strings = [Manifest.permission.BLUETOOTH_CONNECT]),
34
+ Permission(alias = "location", strings = [Manifest.permission.ACCESS_FINE_LOCATION]),
35
+ ],
36
+ )
37
+ class ThermalPrinterPlugin : Plugin() {
38
+
39
+ private lateinit var engine: ThermalPrinterEngine
40
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
41
+
42
+ override fun load() {
43
+ engine = ThermalPrinterEngine(context)
44
+ // Relaye les états de job vers le JS (event printJobStatus).
45
+ engine.onJobUpdate = { u ->
46
+ val job = JSObject()
47
+ .put("jobId", u.jobId)
48
+ .put("printerId", u.printerId)
49
+ .put("state", u.state)
50
+ .put("updatedAt", System.currentTimeMillis())
51
+ u.holdReason?.let { job.put("holdReason", it) }
52
+ u.progress?.let { job.put("progress", it) }
53
+ u.errorCode?.let { job.put("errorCode", it.name) }
54
+ u.message?.let { job.put("message", it) }
55
+ notifyListeners("printJobStatus", JSObject().put("job", job))
56
+ }
57
+ // Relaye les changements de statut vers le JS (event statusChange).
58
+ engine.onStatusChange = { status ->
59
+ notifyListeners("statusChange", JSObject().put("status", status.toJson()))
60
+ }
61
+ Logger.log("plugin", "loaded")
62
+ }
63
+
64
+ // ---------------------------------------------------------------------
65
+ // Permissions
66
+ // ---------------------------------------------------------------------
67
+
68
+ @PluginMethod
69
+ override fun checkPermissions(call: PluginCall) {
70
+ call.resolve(buildPermissionStatus())
71
+ }
72
+
73
+ @PluginMethod
74
+ override fun requestPermissions(call: PluginCall) {
75
+ // Sur API < 31, les permissions BT runtime n'existent pas -> rien à demander.
76
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
77
+ call.resolve(buildPermissionStatus())
78
+ return
79
+ }
80
+ requestPermissionForAliases(arrayOf("bluetoothScan", "bluetoothConnect"), call, "permsCallback")
81
+ }
82
+
83
+ @PermissionCallback
84
+ private fun permsCallback(call: PluginCall) {
85
+ call.resolve(buildPermissionStatus())
86
+ }
87
+
88
+ private fun buildPermissionStatus(): JSObject {
89
+ fun state(alias: String): String =
90
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && alias.startsWith("bluetooth")) "granted"
91
+ else getPermissionState(alias)?.toString() ?: "prompt"
92
+ return JSObject().apply {
93
+ put("bluetooth", state("bluetoothConnect"))
94
+ put("bluetoothScan", state("bluetoothScan"))
95
+ put("bluetoothConnect", state("bluetoothConnect"))
96
+ put("location", state("location"))
97
+ put("localNetwork", "granted") // pas de permission runtime réseau sur Android
98
+ }
99
+ }
100
+
101
+ // ---------------------------------------------------------------------
102
+ // Découverte
103
+ // ---------------------------------------------------------------------
104
+
105
+ @PluginMethod
106
+ fun discoverPrinters(call: PluginCall) = exec(call) {
107
+ val sources = call.getArray("sources", null)?.let { arr ->
108
+ (0 until arr.length()).map { arr.getString(it) }.toSet()
109
+ }
110
+ val options = DiscoveryManager.Options(
111
+ sources = sources,
112
+ timeoutMs = (call.getInt("timeoutMs")?.toLong()) ?: 8000L,
113
+ includePaired = call.getBoolean("includePaired", true) ?: true,
114
+ networkCidr = call.getString("networkCidr"),
115
+ tcpPorts = call.getArray("tcpPorts", null)?.let { arr ->
116
+ (0 until arr.length()).mapNotNull { arr.optInt(it) }
117
+ } ?: listOf(9100),
118
+ )
119
+ val emitPartial = call.getBoolean("emitPartialResults", true) ?: true
120
+
121
+ val (printers, failed) = engine.discover(options) { p ->
122
+ if (emitPartial) notifyListeners("printerFound", JSObject().put("printer", p.toJson()))
123
+ }
124
+
125
+ val arr = JSArray()
126
+ printers.forEach { arr.put(it.toJson()) }
127
+ val failedArr = JSArray()
128
+ failed.forEach { failedArr.put(it) }
129
+ val result = JSObject().put("printers", arr)
130
+
131
+ // Event de fin de découverte.
132
+ notifyListeners(
133
+ "discoveryComplete",
134
+ JSObject().put("printers", arr).put("failedSources", failedArr),
135
+ )
136
+ result
137
+ }
138
+
139
+ // ---------------------------------------------------------------------
140
+ // Connexion
141
+ // ---------------------------------------------------------------------
142
+
143
+ @PluginMethod
144
+ fun connectPrinter(call: PluginCall) = exec(call) {
145
+ val printerId = call.getString("printerId")
146
+ ?: throw PrinterException(com.delicity.thermalprinter.model.ErrorCode.PRINTER_NOT_FOUND, "printerId requis")
147
+ val timeout = (call.getInt("timeoutMs")?.toLong()) ?: 10000L
148
+ val force = call.getString("forceAdapter")?.let { com.delicity.thermalprinter.model.AdapterId.from(it) }
149
+ val setAsDefault = call.getBoolean("setAsDefault", false) ?: false
150
+ val connected = engine.connect(printerId, timeout, force, setAsDefault)
151
+ JSObject().put("connected", connected)
152
+ }
153
+
154
+ @PluginMethod
155
+ fun disconnectPrinter(call: PluginCall) = exec(call) {
156
+ engine.disconnect(call.getString("printerId") ?: "")
157
+ JSObject()
158
+ }
159
+
160
+ // ---------------------------------------------------------------------
161
+ // Profils / défaut
162
+ // ---------------------------------------------------------------------
163
+
164
+ @PluginMethod
165
+ fun setDefaultPrinter(call: PluginCall) = exec(call) {
166
+ val id = call.getString("printerId")
167
+ ?: throw PrinterException(com.delicity.thermalprinter.model.ErrorCode.PRINTER_NOT_FOUND, "printerId requis")
168
+ val profile = engine.setDefault(id)
169
+ JSObject().put("profile", profile.toJson())
170
+ }
171
+
172
+ @PluginMethod
173
+ fun getDefaultPrinter(call: PluginCall) = exec(call) {
174
+ val profile = engine.defaultProfile()
175
+ JSObject().put("profile", profile?.toJson() ?: JSObject.NULL)
176
+ }
177
+
178
+ @PluginMethod
179
+ fun getSavedPrinters(call: PluginCall) = exec(call) {
180
+ val arr = JSArray()
181
+ engine.savedProfiles().forEach { arr.put(it.toJson()) }
182
+ JSObject().put("profiles", arr)
183
+ }
184
+
185
+ @PluginMethod
186
+ fun removePrinter(call: PluginCall) = exec(call) {
187
+ engine.removeProfile(call.getString("printerId") ?: "")
188
+ JSObject()
189
+ }
190
+
191
+ // ---------------------------------------------------------------------
192
+ // Impression / statut
193
+ // ---------------------------------------------------------------------
194
+
195
+ @PluginMethod
196
+ fun printImage(call: PluginCall) = exec(call) {
197
+ val image = call.getObject("image")
198
+ ?: throw PrinterException(com.delicity.thermalprinter.model.ErrorCode.IMAGE_INVALID, "image requise")
199
+ val render = call.getObject("render")?.let { r ->
200
+ RenderOptions(
201
+ widthDots = r.optInt("widthDots", 0),
202
+ resize = r.optBoolean("resize", true),
203
+ grayscale = r.optBoolean("grayscale", true),
204
+ threshold = r.optInt("threshold", 128),
205
+ dithering = r.optString("dithering", "floyd_steinberg"),
206
+ align = r.optString("align", "center"),
207
+ invert = r.optBoolean("invert", false),
208
+ cut = r.optBoolean("cut", true),
209
+ feedLines = r.optInt("feedLines", 3),
210
+ openCashDrawer = r.optBoolean("openCashDrawer", false),
211
+ copies = r.optInt("copies", 1),
212
+ )
213
+ }
214
+ val req = ThermalPrinterEngine.PrintRequest(
215
+ printerId = call.getString("printerId"),
216
+ filePath = image.optString("filePath", null),
217
+ url = image.optString("url", null),
218
+ base64 = image.optString("base64", null),
219
+ render = render,
220
+ timeoutMs = (call.getInt("timeoutMs")?.toLong()) ?: 15000L,
221
+ autoReconnect = call.getBoolean("autoReconnect", true) ?: true,
222
+ )
223
+ val out = engine.printImage(req)
224
+ printResultJson(out)
225
+ }
226
+
227
+ @PluginMethod
228
+ fun printText(call: PluginCall) = exec(call) {
229
+ val itemsArr = call.getArray("items")
230
+ ?: throw PrinterException(com.delicity.thermalprinter.model.ErrorCode.IMAGE_INVALID, "items requis")
231
+ val items = com.delicity.thermalprinter.model.PrintItem.parseList(itemsArr)
232
+ val req = ThermalPrinterEngine.PrintTextRequest(
233
+ printerId = call.getString("printerId"),
234
+ items = items,
235
+ defaultCodePage = call.getString("defaultCodePage") ?: "WPC1252",
236
+ cut = call.getBoolean("cut", false) ?: false,
237
+ feedLines = call.getInt("feedLines") ?: 3,
238
+ timeoutMs = (call.getInt("timeoutMs")?.toLong()) ?: 15000L,
239
+ autoReconnect = call.getBoolean("autoReconnect", true) ?: true,
240
+ )
241
+ val out = engine.printText(req)
242
+ printResultJson(out)
243
+ }
244
+
245
+ private fun printResultJson(out: ThermalPrinterEngine.PrintOutcome): JSObject = JSObject().apply {
246
+ put("success", out.state == "completed")
247
+ put("printerId", out.printerId)
248
+ put("adapter", out.adapter.value)
249
+ put("jobId", out.jobId)
250
+ put("state", out.state)
251
+ put("bytesSent", out.bytesSent)
252
+ put("durationMs", out.durationMs)
253
+ out.status?.let { put("status", it.toJson()) }
254
+ }
255
+
256
+ @PluginMethod
257
+ fun getPrinterStatus(call: PluginCall) = exec(call) {
258
+ engine.getStatus(call.getString("printerId")).toJson()
259
+ }
260
+
261
+ // ---------------------------------------------------------------------
262
+ // Monitoring (Phase 6) — stubs branchés à compléter
263
+ // ---------------------------------------------------------------------
264
+
265
+ @PluginMethod
266
+ fun startStatusMonitor(call: PluginCall) = exec(call) {
267
+ val printerId = call.getString("printerId")
268
+ ?: throw PrinterException(com.delicity.thermalprinter.model.ErrorCode.PRINTER_NOT_FOUND, "printerId requis")
269
+ val interval = (call.getInt("intervalMs")?.toLong()) ?: 5000L
270
+ engine.startStatusMonitor(printerId, interval)
271
+ JSObject()
272
+ }
273
+
274
+ @PluginMethod
275
+ fun stopStatusMonitor(call: PluginCall) = exec(call) {
276
+ val printerId = call.getString("printerId")
277
+ ?: throw PrinterException(com.delicity.thermalprinter.model.ErrorCode.PRINTER_NOT_FOUND, "printerId requis")
278
+ engine.stopStatusMonitor(printerId)
279
+ JSObject()
280
+ }
281
+
282
+ @PluginMethod
283
+ fun getActiveSdks(call: PluginCall) = exec(call) {
284
+ val arr = JSArray()
285
+ engine.activeSdks().forEach { s ->
286
+ val transports = JSArray()
287
+ s.transports.forEach { transports.put(it) }
288
+ arr.put(
289
+ JSObject()
290
+ .put("adapter", s.adapter)
291
+ .put("label", s.label)
292
+ .put("available", s.available)
293
+ .put("requiresSdk", s.requiresSdk)
294
+ .put("transports", transports),
295
+ )
296
+ }
297
+ JSObject().put("sdks", arr)
298
+ }
299
+
300
+ @PluginMethod
301
+ fun getDebugLog(call: PluginCall) {
302
+ call.resolve(JSObject().put("log", engine.debugLog()))
303
+ }
304
+
305
+ // ---------------------------------------------------------------------
306
+ // Plomberie : exécution async + normalisation d'erreurs
307
+ // ---------------------------------------------------------------------
308
+
309
+ private fun exec(call: PluginCall, block: suspend () -> JSObject) {
310
+ scope.launch {
311
+ try {
312
+ val result = block()
313
+ withContext(Dispatchers.Main) { call.resolve(result) }
314
+ } catch (e: PrinterException) {
315
+ Logger.error("plugin", "${e.code}: ${e.message}", e)
316
+ withContext(Dispatchers.Main) {
317
+ val data = JSObject().put("code", e.code.name).put("detail", e.detail).put("retryable", e.retryable)
318
+ call.reject(e.message, e.code.name, e, data)
319
+ }
320
+ } catch (e: Exception) {
321
+ Logger.error("plugin", "UNKNOWN: ${e.message}", e)
322
+ withContext(Dispatchers.Main) {
323
+ call.reject(e.message ?: "Erreur inconnue", "UNKNOWN", e)
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ override fun handleOnDestroy() {
330
+ super.handleOnDestroy()
331
+ engine.stopAllMonitors()
332
+ scope.coroutineContext[kotlinx.coroutines.Job]?.cancel()
333
+ }
334
+ }
@@ -0,0 +1,125 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import android.bluetooth.BluetoothManager
4
+ import android.content.Context
5
+ import android.graphics.Bitmap
6
+ import com.delicity.thermalprinter.image.ImageProcessor
7
+ import com.delicity.thermalprinter.model.AdapterId
8
+ import com.delicity.thermalprinter.model.DiscoveredPrinter
9
+ import com.delicity.thermalprinter.model.ErrorCode
10
+ import com.delicity.thermalprinter.model.PrinterException
11
+ import com.delicity.thermalprinter.model.PrinterProfile
12
+ import com.delicity.thermalprinter.model.PrinterStatus
13
+ import com.delicity.thermalprinter.model.RenderOptions
14
+ import com.delicity.thermalprinter.model.Transport
15
+ import com.delicity.thermalprinter.transport.BleGattClient
16
+ import java.util.concurrent.ConcurrentHashMap
17
+
18
+ /**
19
+ * Adapter BLE (Bluetooth Low Energy) générique pour imprimantes ESC/POS exposant
20
+ * un service GATT d'écriture "série".
21
+ *
22
+ * La connexion GATT + l'écriture par paquets sont déléguées à [BleGattClient]
23
+ * (négociation MTU, allowlist d'UUID, fallback characteristic inscriptible). Le
24
+ * scan BLE concret est fait par BleScanner (DiscoveryManager).
25
+ *
26
+ * Recommandation : valider chaque modèle (allowlist d'UUID dans BleGattClient).
27
+ * Pour le BT classique ESC/POS générique sur Android, préférer SPP (EscPosAdapter).
28
+ */
29
+ class BleAdapter(private val context: Context) : PrinterAdapter {
30
+
31
+ override val id = AdapterId.ESCPOS // BLE transporte de l'ESC/POS dans la majorité des cas
32
+
33
+ private val connections = ConcurrentHashMap<String, BleGattClient>()
34
+
35
+ private val btAdapter by lazy {
36
+ (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
37
+ }
38
+
39
+ override fun isAvailable(): Boolean =
40
+ context.packageManager.hasSystemFeature(android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE)
41
+
42
+ override fun supportsTextItems(): Boolean = true
43
+
44
+ override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
45
+ // Délégué à BleScanner (DiscoveryManager).
46
+ }
47
+
48
+ override fun canHandle(profile: PrinterProfile): Boolean = profile.transport == Transport.BLE
49
+
50
+ override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
51
+ ensureBle()
52
+ if (isConnected(profile.id)) return
53
+ val adapter = btAdapter ?: throw PrinterException(ErrorCode.BLUETOOTH_DISABLED, "Bluetooth indisponible")
54
+ val device = try {
55
+ adapter.getRemoteDevice(profile.address)
56
+ } catch (e: IllegalArgumentException) {
57
+ throw PrinterException(ErrorCode.PRINTER_NOT_FOUND, "Adresse BLE invalide: ${profile.address}")
58
+ }
59
+ val client = BleGattClient(context, device)
60
+ client.open(timeoutMs)
61
+ connections[profile.id] = client
62
+ }
63
+
64
+ override fun isConnected(printerId: String): Boolean = connections[printerId]?.isOpen == true
65
+
66
+ override suspend fun disconnect(printerId: String) {
67
+ connections.remove(printerId)?.close()
68
+ }
69
+
70
+ override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
71
+ val client = requireClient(profile)
72
+ val mono = ImageProcessor.toMono(bitmap, options)
73
+ val raster = ImageProcessor.encodeEscPosRaster(mono)
74
+ val job = EscPosCommands.buildJob(
75
+ rasterData = raster,
76
+ align = options.align,
77
+ feedLines = options.feedLines,
78
+ cut = options.cut && profile.capabilities.supportsCut,
79
+ openDrawer = options.openCashDrawer && profile.capabilities.supportsCashDrawer,
80
+ )
81
+ var sent = 0
82
+ repeat(options.copies.coerceAtLeast(1)) {
83
+ client.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 client = requireClient(profile)
97
+ val columns = if (profile.capabilities.printableDots <= 420) 32 else 48
98
+ val encoded = EscPosTextEncoder.encode(items, defaultCodePage, columns)
99
+ val out = java.io.ByteArrayOutputStream()
100
+ out.write(encoded.bytes)
101
+ if (feedLines > 0) out.write(EscPosCommands.feed(feedLines))
102
+ if (cut && profile.capabilities.supportsCut) out.write(EscPosCommands.CUT_PARTIAL)
103
+ val job = out.toByteArray()
104
+ client.write(job)
105
+ return job.size
106
+ }
107
+
108
+ override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
109
+ val open = isConnected(profile.id)
110
+ return PrinterStatus(
111
+ id = profile.id,
112
+ connection = if (open) "connected" else "disconnected",
113
+ online = open,
114
+ paper = "unknown",
115
+ rawStatus = "BLE: statut temps réel non lu (notify spécifique au modèle)",
116
+ )
117
+ }
118
+
119
+ private fun requireClient(profile: PrinterProfile): BleGattClient =
120
+ connections[profile.id] ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "BLE non connecté: ${profile.id}")
121
+
122
+ private fun ensureBle() {
123
+ if (!isAvailable()) throw PrinterException(ErrorCode.UNSUPPORTED_TRANSPORT, "BLE indisponible sur cet appareil")
124
+ }
125
+ }
@@ -0,0 +1,206 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import android.bluetooth.BluetoothManager
4
+ import android.content.Context
5
+ import android.graphics.Bitmap
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 java.util.concurrent.ConcurrentHashMap
15
+
16
+ /**
17
+ * Adapter Brother basé sur le Brother Print SDK v4 (`com.brother.sdk.lmprinter`),
18
+ * piloté par RÉFLEXION.
19
+ *
20
+ * Brother est orienté étiquettes (QL / TD / RJ / PJ / PT) : les réglages d'impression
21
+ * (`*PrintSettings`) dépendent du MODÈLE. L'adapter dérive un `PrinterModel` depuis
22
+ * `profile.model` (ex "RJ-3150" -> PrinterModel.RJ_3150). Si le modèle est inconnu,
23
+ * une erreur explicite invite à le préciser.
24
+ *
25
+ * Le SDK n'est pas redistribuable (licence Brother) : déposer
26
+ * `BrotherPrintLibrary.aar` (portail Brother). Voir docs/SDK_INTEGRATION.md (§ Brother).
27
+ *
28
+ * ⚠️ API model-dépendante : à vérifier sur device avec le binaire réel.
29
+ */
30
+ class BrotherAdapter(private val context: Context) : PrinterAdapter {
31
+
32
+ override val id = AdapterId.BROTHER
33
+
34
+ private val cache = ConcurrentHashMap<String, Any>() // printerId -> PrinterDriver
35
+
36
+ private val btAdapter by lazy {
37
+ (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
38
+ }
39
+
40
+ override fun isAvailable(): Boolean = EpsonAdapter.classExists(DRIVER_GENERATOR)
41
+
42
+ // -------------------------------------------------------------------------
43
+ // Découverte (BRLMPrinterSearcher — réseau)
44
+ // -------------------------------------------------------------------------
45
+
46
+ override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
47
+ if (!isAvailable()) return
48
+ // L'API de recherche Brother varie selon la version ; on tente la recherche
49
+ // réseau si disponible, sinon on s'appuie sur le scan TCP générique.
50
+ runCatching {
51
+ val option = SdkReflect.newInstance(NET_SEARCH_OPTION, emptyArray(), emptyArray())
52
+ runCatching {
53
+ SdkReflect.call(option, "setSearchDuration", arrayOf(Int::class.javaPrimitiveType!!), arrayOf((timeoutMs / 1000).toInt()))
54
+ }
55
+ val callback = SdkReflect.proxy(SEARCH_RESULT_CALLBACK, mapOf(
56
+ "onChannelInfo" to { args -> handleChannel(args.getOrNull(0), onFound); null },
57
+ "onSearchResult" to { null },
58
+ ))
59
+ SdkReflect.callStatic(
60
+ SEARCHER, "startNetworkSearch",
61
+ arrayOf(Context::class.java, SdkReflect.classOrNull(NET_SEARCH_OPTION)!!, SdkReflect.classOrNull(SEARCH_RESULT_CALLBACK)!!),
62
+ arrayOf(context, option, callback),
63
+ )
64
+ }
65
+ }
66
+
67
+ private fun handleChannel(channel: Any?, onFound: (DiscoveredPrinter) -> Unit) {
68
+ if (channel == null) return
69
+ val address = (SdkReflect.call(channel, "getChannelInfo") as? String)
70
+ ?: (SdkReflect.field(channel, "channelInfo") as? String) ?: return
71
+ val name = (SdkReflect.call(channel, "getExtraInfo") as? String) ?: "Brother"
72
+ onFound(
73
+ DiscoveredPrinter(
74
+ id = "brother:$address",
75
+ name = name,
76
+ brand = "Brother",
77
+ transport = Transport.WIFI,
78
+ adapter = AdapterId.BROTHER,
79
+ address = address,
80
+ discoveredBy = mutableSetOf(AdapterId.BROTHER),
81
+ ),
82
+ )
83
+ }
84
+
85
+ override fun canHandle(profile: PrinterProfile): Boolean =
86
+ isAvailable() && profile.adapter == AdapterId.BROTHER
87
+
88
+ // -------------------------------------------------------------------------
89
+ // Connexion (openChannel -> PrinterDriver)
90
+ // -------------------------------------------------------------------------
91
+
92
+ override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
93
+ ensureSdk()
94
+ if (isConnected(profile.id)) return
95
+ val channel = buildChannel(profile)
96
+ val result = SdkReflect.callStatic(
97
+ DRIVER_GENERATOR, "openChannel",
98
+ arrayOf(SdkReflect.classOrNull(CHANNEL)!!), arrayOf(channel),
99
+ ) ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "openChannel Brother null")
100
+
101
+ val driver = SdkReflect.call(result, "getDriver")
102
+ if (driver == null) {
103
+ val err = runCatching { SdkReflect.call(result, "getError")?.let { SdkReflect.call(it, "getCode") } }.getOrNull()
104
+ throw PrinterException(ErrorCode.CONNECTION_FAILED, "Connexion Brother échouée (${err ?: "openChannel"})", retryable = true)
105
+ }
106
+ cache[profile.id] = driver
107
+ }
108
+
109
+ override fun isConnected(printerId: String): Boolean = cache.containsKey(printerId)
110
+
111
+ override suspend fun disconnect(printerId: String) {
112
+ cache.remove(printerId)?.let { runCatching { SdkReflect.call(it, "closeChannel") } }
113
+ }
114
+
115
+ // -------------------------------------------------------------------------
116
+ // Impression image
117
+ // -------------------------------------------------------------------------
118
+
119
+ override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
120
+ val driver = cache[profile.id]
121
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Brother non connecté: ${profile.id}")
122
+ val settings = buildPrintSettings(profile)
123
+ try {
124
+ repeat(options.copies.coerceAtLeast(1)) {
125
+ SdkReflect.call(
126
+ driver, "printImage",
127
+ arrayOf(Bitmap::class.java, SdkReflect.classOrNull(PRINT_IMAGE_SETTINGS)!!),
128
+ arrayOf(bitmap, settings),
129
+ )
130
+ }
131
+ } catch (e: Throwable) {
132
+ throw PrinterException(ErrorCode.PRINT_FAILED, "Impression Brother échouée", e.message, retryable = true)
133
+ }
134
+ return bitmap.width * bitmap.height / 8
135
+ }
136
+
137
+ // -------------------------------------------------------------------------
138
+ // Statut
139
+ // -------------------------------------------------------------------------
140
+
141
+ override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
142
+ val driver = cache[profile.id]
143
+ ?: return PrinterStatus(profile.id, "disconnected", online = false, paper = "unknown")
144
+ return try {
145
+ val result = SdkReflect.call(driver, "getPrinterStatus")
146
+ val online = result != null
147
+ PrinterStatus(profile.id, if (online) "connected" else "disconnected", online = online, paper = "unknown", rawStatus = result?.toString())
148
+ } catch (e: Throwable) {
149
+ PrinterStatus(profile.id, "connected", online = true, paper = "unknown", rawStatus = e.message)
150
+ }
151
+ }
152
+
153
+ // -------------------------------------------------------------------------
154
+ // Helpers
155
+ // -------------------------------------------------------------------------
156
+
157
+ private fun buildChannel(profile: PrinterProfile): Any = when (profile.transport) {
158
+ Transport.WIFI, Transport.ETHERNET -> SdkReflect.callStatic(
159
+ CHANNEL, "newWifiChannel", arrayOf(String::class.java), arrayOf(profile.address.substringBefore(":")),
160
+ ) ?: error("newWifiChannel null")
161
+ Transport.BLUETOOTH -> SdkReflect.callStatic(
162
+ CHANNEL, "newBluetoothChannel",
163
+ arrayOf(String::class.java, android.bluetooth.BluetoothAdapter::class.java),
164
+ arrayOf(profile.address, btAdapter),
165
+ ) ?: error("newBluetoothChannel null")
166
+ Transport.BLE -> SdkReflect.callStatic(
167
+ CHANNEL, "newBluetoothLowEnergyChannel",
168
+ arrayOf(String::class.java, Context::class.java, android.bluetooth.BluetoothAdapter::class.java),
169
+ arrayOf(profile.address, context, btAdapter),
170
+ ) ?: error("newBluetoothLowEnergyChannel null")
171
+ else -> throw PrinterException(ErrorCode.UNSUPPORTED_TRANSPORT, "Transport Brother non supporté: ${profile.transport.value}")
172
+ }
173
+
174
+ /**
175
+ * Construit les *PrintSettings selon la famille du modèle (QL/PJ/RJ/TD/PT).
176
+ * `PrinterModel` est dérivé du nom (ex "RJ-3150" -> RJ_3150).
177
+ */
178
+ private fun buildPrintSettings(profile: PrinterProfile): Any {
179
+ val model = profile.model
180
+ ?: throw PrinterException(ErrorCode.UNSUPPORTED_PRINTER, "Modèle Brother requis (ex 'RJ-3150') pour les réglages d'impression")
181
+ val enumName = model.uppercase().replace("-", "_").replace(" ", "_")
182
+ val family = Regex("^(QL|PJ|RJ|TD|PT)").find(enumName)?.value
183
+ ?: throw PrinterException(ErrorCode.UNSUPPORTED_PRINTER, "Famille Brother inconnue pour '$model'")
184
+ val printerModel = SdkReflect.enumValue(PRINTER_MODEL, enumName)
185
+ ?: throw PrinterException(ErrorCode.UNSUPPORTED_PRINTER, "PrinterModel Brother inconnu: $enumName")
186
+ val settingsClass = "$SETTING_PKG.${family}PrintSettings"
187
+ return SdkReflect.newInstance(
188
+ settingsClass, arrayOf(SdkReflect.classOrNull(PRINTER_MODEL)!!), arrayOf(printerModel),
189
+ )
190
+ }
191
+
192
+ private fun ensureSdk() {
193
+ if (!isAvailable()) throw PrinterException(ErrorCode.SDK_NOT_AVAILABLE, "SDK Brother absent")
194
+ }
195
+
196
+ companion object {
197
+ private const val DRIVER_GENERATOR = "com.brother.sdk.lmprinter.PrinterDriverGenerator"
198
+ private const val CHANNEL = "com.brother.sdk.lmprinter.Channel"
199
+ private const val SEARCHER = "com.brother.sdk.lmprinter.PrinterSearcher"
200
+ private const val NET_SEARCH_OPTION = "com.brother.sdk.lmprinter.NetworkSearchOption"
201
+ private const val SEARCH_RESULT_CALLBACK = "com.brother.sdk.lmprinter.PrinterSearcher\$SearchResultCallback"
202
+ private const val SETTING_PKG = "com.brother.sdk.lmprinter.setting"
203
+ private const val PRINT_IMAGE_SETTINGS = "com.brother.sdk.lmprinter.setting.PrintImageSettings"
204
+ private const val PRINTER_MODEL = "com.brother.sdk.lmprinter.setting.PrinterModel"
205
+ }
206
+ }