@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,138 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import com.delicity.thermalprinter.model.PrintItem
4
+ import com.delicity.thermalprinter.model.TextStyle
5
+ import java.io.ByteArrayOutputStream
6
+ import android.util.Base64
7
+
8
+ /**
9
+ * Encodeur ESC/POS texte (miroir Kotlin de src/core/escpos-text.ts).
10
+ * Transforme une liste de PrintItem en flux d'octets ESC/POS.
11
+ *
12
+ * Les items `image` sont signalés via [imageIndexes] et NON encodés ici :
13
+ * ils sont rendus par le pipeline image (ImageProcessor) et insérés par le moteur.
14
+ */
15
+ object EscPosTextEncoder {
16
+
17
+ private const val ESC = 0x1B
18
+ private const val GS = 0x1D
19
+ private const val LF = 0x0A
20
+
21
+ private val CODE_PAGE_TO_ESC_T = mapOf(
22
+ "CP437" to 0, "CP850" to 2, "CP858" to 19, "WPC1252" to 16, "CP852" to 18, "CP866" to 17,
23
+ )
24
+
25
+ private val BARCODE_M = mapOf(
26
+ "UPC_A" to 65, "UPC_E" to 66, "EAN13" to 67, "EAN8" to 68,
27
+ "CODE39" to 69, "ITF" to 70, "CODABAR" to 71, "CODE93" to 72, "CODE128" to 73,
28
+ )
29
+
30
+ data class Encoded(val bytes: ByteArray, val imageIndexes: List<Int>)
31
+
32
+ fun encodeString(value: String): ByteArray {
33
+ val out = ByteArrayOutputStream()
34
+ value.codePoints().forEach { cp -> out.write(if (cp <= 0xFF) cp else 0x3F) }
35
+ return out.toByteArray()
36
+ }
37
+
38
+ fun sizeByte(w: Int, h: Int): Int {
39
+ val ww = (w.coerceIn(1, 8)) - 1
40
+ val hh = (h.coerceIn(1, 8)) - 1
41
+ return (ww shl 4) or hh
42
+ }
43
+
44
+ private fun openStyle(out: ByteArrayOutputStream, s: TextStyle, defaultCodePage: String) {
45
+ val cp = s.codePageId ?: (CODE_PAGE_TO_ESC_T[s.codePage ?: defaultCodePage] ?: 16)
46
+ out.write(byteArrayOf(ESC.toByte(), 0x74, (cp and 0xFF).toByte()))
47
+ val align = when (s.align) { "center" -> 1; "right" -> 2; else -> 0 }
48
+ out.write(byteArrayOf(ESC.toByte(), 0x61, align.toByte()))
49
+ out.write(byteArrayOf(ESC.toByte(), 0x4D, if (s.font == "B") 1 else 0))
50
+ out.write(byteArrayOf(ESC.toByte(), 0x45, if (s.bold) 1 else 0))
51
+ out.write(byteArrayOf(ESC.toByte(), 0x47, if (s.doubleStrike) 1 else 0))
52
+ val ul = when (s.underline) { "single" -> 1; "double" -> 2; else -> 0 }
53
+ out.write(byteArrayOf(ESC.toByte(), 0x2D, ul.toByte()))
54
+ out.write(byteArrayOf(GS.toByte(), 0x42, if (s.invert) 1 else 0))
55
+ out.write(byteArrayOf(ESC.toByte(), 0x7B, if (s.upsideDown) 1 else 0))
56
+ out.write(byteArrayOf(ESC.toByte(), 0x56, if (s.rotate90) 1 else 0))
57
+ out.write(byteArrayOf(GS.toByte(), 0x21, sizeByte(s.widthMultiplier, s.heightMultiplier).toByte()))
58
+ s.letterSpacing?.let { out.write(byteArrayOf(ESC.toByte(), 0x20, (it and 0xFF).toByte())) }
59
+ if (s.lineSpacing != null) out.write(byteArrayOf(ESC.toByte(), 0x33, (s.lineSpacing and 0xFF).toByte()))
60
+ else out.write(byteArrayOf(ESC.toByte(), 0x32))
61
+ }
62
+
63
+ private fun reset(out: ByteArrayOutputStream) = out.write(byteArrayOf(ESC.toByte(), 0x40))
64
+
65
+ private fun qrCode(out: ByteArrayOutputStream, item: PrintItem.QrCode) {
66
+ val align = when (item.align) { "left" -> 0; "right" -> 2; else -> 1 }
67
+ out.write(byteArrayOf(ESC.toByte(), 0x61, align.toByte()))
68
+ out.write(byteArrayOf(GS.toByte(), 0x28, 0x6B, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00))
69
+ val size = item.size.coerceIn(1, 16)
70
+ out.write(byteArrayOf(GS.toByte(), 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, size.toByte()))
71
+ val ec = when (item.ec) { "L" -> 48; "Q" -> 50; "H" -> 51; else -> 49 }
72
+ out.write(byteArrayOf(GS.toByte(), 0x28, 0x6B, 0x03, 0x00, 0x31, 0x45, ec.toByte()))
73
+ val data = encodeString(item.value)
74
+ val len = data.size + 3
75
+ out.write(byteArrayOf(GS.toByte(), 0x28, 0x6B, (len and 0xFF).toByte(), ((len shr 8) and 0xFF).toByte(), 0x31, 0x50, 0x30))
76
+ out.write(data)
77
+ out.write(byteArrayOf(GS.toByte(), 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30))
78
+ }
79
+
80
+ private fun barcode(out: ByteArrayOutputStream, item: PrintItem.Barcode) {
81
+ val align = when (item.align) { "left" -> 0; "right" -> 2; else -> 1 }
82
+ out.write(byteArrayOf(ESC.toByte(), 0x61, align.toByte()))
83
+ val hri = when (item.hri) { "above" -> 1; "both" -> 3; "none" -> 0; else -> 2 }
84
+ out.write(byteArrayOf(GS.toByte(), 0x48, hri.toByte()))
85
+ out.write(byteArrayOf(GS.toByte(), 0x68, item.height.coerceIn(1, 255).toByte()))
86
+ out.write(byteArrayOf(GS.toByte(), 0x77, item.width.coerceIn(2, 6).toByte()))
87
+ val m = BARCODE_M[item.symbology] ?: 73
88
+ var data = encodeString(item.value)
89
+ if (item.symbology == "CODE128" && !(data.isNotEmpty() && data[0].toInt() == 0x7B)) {
90
+ data = byteArrayOf(0x7B, 0x42) + data
91
+ }
92
+ out.write(byteArrayOf(GS.toByte(), 0x6B, m.toByte(), data.size.toByte()))
93
+ out.write(data)
94
+ }
95
+
96
+ fun encode(items: List<PrintItem>, defaultCodePage: String = "WPC1252", columns: Int = 48): Encoded {
97
+ val out = ByteArrayOutputStream()
98
+ val imageIndexes = mutableListOf<Int>()
99
+ reset(out)
100
+ items.forEachIndexed { index, item ->
101
+ when (item) {
102
+ is PrintItem.Text -> {
103
+ openStyle(out, item.style, defaultCodePage)
104
+ out.write(encodeString(item.value))
105
+ if (item.style.newline) out.write(LF)
106
+ reset(out)
107
+ }
108
+ is PrintItem.Feed -> out.write(byteArrayOf(ESC.toByte(), 0x64, item.lines.coerceIn(0, 255).toByte()))
109
+ is PrintItem.Divider -> {
110
+ val ch = (item.char.firstOrNull() ?: '-').code
111
+ val n = item.columns ?: columns
112
+ val align = when (item.align) { "center" -> 1; "right" -> 2; else -> 0 }
113
+ out.write(byteArrayOf(ESC.toByte(), 0x61, align.toByte()))
114
+ if (item.bold) out.write(byteArrayOf(ESC.toByte(), 0x45, 1))
115
+ repeat(n) { out.write(ch) }
116
+ out.write(LF)
117
+ reset(out)
118
+ }
119
+ is PrintItem.QrCode -> qrCode(out, item)
120
+ is PrintItem.Barcode -> barcode(out, item)
121
+ is PrintItem.CashDrawer -> out.write(
122
+ if (item.pin == 5) byteArrayOf(ESC.toByte(), 0x70, 0x01, 0x19, 0xFA.toByte())
123
+ else byteArrayOf(ESC.toByte(), 0x70, 0x00, 0x19, 0xFA.toByte()),
124
+ )
125
+ is PrintItem.Cut -> {
126
+ if (item.feedBefore > 0) out.write(byteArrayOf(ESC.toByte(), 0x64, (item.feedBefore and 0xFF).toByte()))
127
+ out.write(if (item.mode == "full") byteArrayOf(GS.toByte(), 0x56, 0x00) else byteArrayOf(GS.toByte(), 0x56, 0x01))
128
+ }
129
+ is PrintItem.Raw -> {
130
+ val clean = if (item.bytesBase64.contains("base64,")) item.bytesBase64.substringAfter("base64,") else item.bytesBase64
131
+ out.write(Base64.decode(clean, Base64.DEFAULT))
132
+ }
133
+ is PrintItem.Image -> imageIndexes.add(index)
134
+ }
135
+ }
136
+ return Encoded(out.toByteArray(), imageIndexes)
137
+ }
138
+ }
@@ -0,0 +1,95 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import android.graphics.Bitmap
4
+ import com.delicity.thermalprinter.model.AdapterId
5
+ import com.delicity.thermalprinter.model.DiscoveredPrinter
6
+ import com.delicity.thermalprinter.model.PrinterProfile
7
+ import com.delicity.thermalprinter.model.PrinterStatus
8
+ import com.delicity.thermalprinter.model.RenderOptions
9
+
10
+ /**
11
+ * Contrat commun à tous les adapters natifs Android.
12
+ *
13
+ * Un adapter encapsule TOUT ce qui est spécifique à une famille d'imprimantes :
14
+ * - sa découverte (via SDK ou scan générique),
15
+ * - sa connexion / reconnexion,
16
+ * - la conversion d'un Bitmap déjà binarisé en commandes natives,
17
+ * - l'envoi,
18
+ * - la lecture de statut.
19
+ *
20
+ * Le moteur (ThermalPrinterEngine) orchestre les adapters et applique la priorité.
21
+ * Toutes les méthodes longues sont `suspend` (exécutées sur Dispatchers.IO).
22
+ */
23
+ interface PrinterAdapter {
24
+
25
+ val id: AdapterId
26
+
27
+ /** True si le SDK requis est présent dans l'app (sinon l'adapter est ignoré). */
28
+ fun isAvailable(): Boolean
29
+
30
+ /**
31
+ * Lance une découverte propre à cet adapter. Les résultats partiels peuvent
32
+ * être émis via [onFound] au fil de l'eau. La méthode se termine quand le
33
+ * scan est fini ou que [timeoutMs] est atteint.
34
+ */
35
+ suspend fun discover(
36
+ timeoutMs: Long,
37
+ onFound: (DiscoveredPrinter) -> Unit,
38
+ )
39
+
40
+ /** Indique si cet adapter sait gérer ce profil (transport + identité). */
41
+ fun canHandle(profile: PrinterProfile): Boolean
42
+
43
+ /**
44
+ * True si l'adapter sait imprimer des items texte NATIVEMENT (encodeur ESC/POS
45
+ * ou builder texte du SDK). Si false, le moteur effectue un repli automatique :
46
+ * il rend les items en image (TextRasterizer) puis appelle [printBitmap].
47
+ * Voir docs/SDK_INTEGRATION.md (printText sur marques SDK).
48
+ */
49
+ fun supportsTextItems(): Boolean = false
50
+
51
+ /** Ouvre une connexion. Idempotent : ne rien faire si déjà connecté. */
52
+ suspend fun connect(profile: PrinterProfile, timeoutMs: Long)
53
+
54
+ /** True si une connexion active existe pour cette imprimante. */
55
+ fun isConnected(printerId: String): Boolean
56
+
57
+ /** Ferme la connexion. */
58
+ suspend fun disconnect(printerId: String)
59
+
60
+ /**
61
+ * Imprime un bitmap.
62
+ *
63
+ * IMPORTANT : `bitmap` est DÉJÀ redimensionné à la largeur cible et
64
+ * binarisé/dithered par le moteur (ImageProcessor). Pour les adapters ESC/POS,
65
+ * il suffit de l'encoder en raster GS v 0. Pour les SDK fabricants, on passe
66
+ * le bitmap à l'API d'impression image du SDK.
67
+ *
68
+ * @return nombre d'octets envoyés (best effort).
69
+ */
70
+ suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int
71
+
72
+ /**
73
+ * Imprime une liste d'items texte stylés (+ QR/code-barres/feed/cut...).
74
+ * Pour ESC/POS : encodage via EscPosTextEncoder. Pour les SDK fabricants :
75
+ * mapping vers le builder du SDK (voir docs/SDK_INTEGRATION.md).
76
+ *
77
+ * Implémentation par défaut : non supportée (les adapters SDK la surchargent
78
+ * avec leur builder texte ; ESC/POS et rawTcp l'implémentent via l'encodeur).
79
+ *
80
+ * @return nombre d'octets envoyés (best effort).
81
+ */
82
+ suspend fun printItems(
83
+ profile: PrinterProfile,
84
+ items: List<com.delicity.thermalprinter.model.PrintItem>,
85
+ defaultCodePage: String,
86
+ cut: Boolean,
87
+ feedLines: Int,
88
+ ): Int = throw com.delicity.thermalprinter.model.PrinterException(
89
+ com.delicity.thermalprinter.model.ErrorCode.SDK_NOT_AVAILABLE,
90
+ "printText non implémenté pour cet adapter (${id.value}) — voir docs/SDK_INTEGRATION.md",
91
+ )
92
+
93
+ /** Lit le statut. Renvoie supportsStatus=false via online/paper=unknown si non supporté. */
94
+ suspend fun getStatus(profile: PrinterProfile): PrinterStatus
95
+ }
@@ -0,0 +1,96 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import android.graphics.Bitmap
4
+ import com.delicity.thermalprinter.image.ImageProcessor
5
+ import com.delicity.thermalprinter.model.AdapterId
6
+ import com.delicity.thermalprinter.model.DiscoveredPrinter
7
+ import com.delicity.thermalprinter.model.ErrorCode
8
+ import com.delicity.thermalprinter.model.PrinterException
9
+ import com.delicity.thermalprinter.model.PrinterProfile
10
+ import com.delicity.thermalprinter.model.PrinterStatus
11
+ import com.delicity.thermalprinter.model.RenderOptions
12
+ import com.delicity.thermalprinter.model.Transport
13
+ import com.delicity.thermalprinter.transport.TcpTransport
14
+ import java.util.concurrent.ConcurrentHashMap
15
+
16
+ /**
17
+ * Adapter "filet de sécurité" réseau : envoie un raster ESC/POS sur un socket TCP
18
+ * brut sans rien présumer du dialecte de statut. Utilisé quand une imprimante
19
+ * réseau n'est identifiée par aucun SDK et n'a pas confirmé l'ESC/POS.
20
+ *
21
+ * Priorité la plus basse (voir priority.ts). Ne lit jamais de statut.
22
+ */
23
+ class RawTcpAdapter : PrinterAdapter {
24
+
25
+ override val id = AdapterId.RAW_TCP
26
+ private val connections = ConcurrentHashMap<String, TcpTransport>()
27
+
28
+ override fun isAvailable(): Boolean = true
29
+
30
+ override fun supportsTextItems(): Boolean = true
31
+
32
+ override suspend fun discover(timeoutMs: Long, onFound: (DiscoveredPrinter) -> Unit) {
33
+ // Délégué au TcpScanner.
34
+ }
35
+
36
+ override fun canHandle(profile: PrinterProfile): Boolean =
37
+ profile.adapter == AdapterId.RAW_TCP &&
38
+ profile.transport in setOf(Transport.WIFI, Transport.ETHERNET)
39
+
40
+ override suspend fun connect(profile: PrinterProfile, timeoutMs: Long) {
41
+ if (isConnected(profile.id)) return
42
+ val idx = profile.address.lastIndexOf(':')
43
+ val host = if (idx > 0) profile.address.substring(0, idx) else profile.address
44
+ val port = if (idx > 0) profile.address.substring(idx + 1).toIntOrNull() ?: 9100 else 9100
45
+ val t = TcpTransport(host, port)
46
+ t.open(timeoutMs)
47
+ connections[profile.id] = t
48
+ }
49
+
50
+ override fun isConnected(printerId: String): Boolean = connections[printerId]?.isOpen == true
51
+
52
+ override suspend fun disconnect(printerId: String) {
53
+ connections.remove(printerId)?.close()
54
+ }
55
+
56
+ override suspend fun printBitmap(profile: PrinterProfile, bitmap: Bitmap, options: RenderOptions): Int {
57
+ val t = connections[profile.id]
58
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "rawTcp non connecté")
59
+ val mono = ImageProcessor.toMono(bitmap, options)
60
+ val raster = ImageProcessor.encodeEscPosRaster(mono)
61
+ val job = EscPosCommands.buildJob(raster, options.align, options.feedLines, options.cut, options.openCashDrawer)
62
+ t.write(job)
63
+ return job.size
64
+ }
65
+
66
+ override suspend fun printItems(
67
+ profile: PrinterProfile,
68
+ items: List<com.delicity.thermalprinter.model.PrintItem>,
69
+ defaultCodePage: String,
70
+ cut: Boolean,
71
+ feedLines: Int,
72
+ ): Int {
73
+ val t = connections[profile.id]
74
+ ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "rawTcp non connecté")
75
+ val columns = if (profile.capabilities.printableDots <= 420) 32 else 48
76
+ val encoded = EscPosTextEncoder.encode(items, defaultCodePage, columns)
77
+ val out = java.io.ByteArrayOutputStream()
78
+ out.write(encoded.bytes)
79
+ if (feedLines > 0) out.write(EscPosCommands.feed(feedLines))
80
+ if (cut) out.write(EscPosCommands.CUT_PARTIAL)
81
+ val job = out.toByteArray()
82
+ t.write(job)
83
+ return job.size
84
+ }
85
+
86
+ override suspend fun getStatus(profile: PrinterProfile): PrinterStatus {
87
+ val connected = isConnected(profile.id)
88
+ return PrinterStatus(
89
+ id = profile.id,
90
+ connection = if (connected) "connected" else "disconnected",
91
+ online = connected,
92
+ paper = "unknown",
93
+ rawStatus = "rawTcp: statut non supporté",
94
+ )
95
+ }
96
+ }
@@ -0,0 +1,158 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ /**
4
+ * Contrat de réflexion : énumère la **surface exacte** (classes, constructeurs,
5
+ * méthodes, champs) que chaque adapter SDK appelle par réflexion. Sert à deux choses :
6
+ *
7
+ * 1. En CI (faux SDK présent) : un test vérifie que le faux satisfait le contrat
8
+ * → garde les faux fidèles à ce que l'adapter attend.
9
+ * 2. Avec le VRAI SDK (binaire déposé dans l'app) : appeler [verify] confirme que
10
+ * l'API réelle correspond à notre réflexion — **sans imprimante**. Tout symbole
11
+ * manquant est remonté (au lieu d'échouer silencieusement à l'exécution).
12
+ *
13
+ * Usage (dans l'app, après dépôt du binaire) :
14
+ * ```
15
+ * val missing = SdkContract.verify(SdkContract.EPSON)
16
+ * if (missing.isNotEmpty()) Log.e("SDK", "API Epson divergente: $missing")
17
+ * ```
18
+ * Voir docs/TESTING_SDK.md.
19
+ */
20
+ object SdkContract {
21
+
22
+ data class Method(val name: String, val params: List<String> = emptyList())
23
+ data class Requirement(
24
+ val className: String,
25
+ val constructors: List<List<String>> = emptyList(),
26
+ val methods: List<Method> = emptyList(),
27
+ val fields: List<String> = emptyList(),
28
+ )
29
+
30
+ /** Vérifie une liste de requirements ; renvoie les symboles manquants (vide = OK). */
31
+ fun verify(requirements: List<Requirement>): List<String> {
32
+ val missing = mutableListOf<String>()
33
+ for (r in requirements) {
34
+ val cls = try {
35
+ Class.forName(r.className)
36
+ } catch (e: Throwable) {
37
+ missing.add("class ${r.className}")
38
+ continue
39
+ }
40
+ for (c in r.constructors) {
41
+ try {
42
+ cls.getConstructor(*c.map(::typeOf).toTypedArray())
43
+ } catch (e: Throwable) {
44
+ missing.add("${r.className}.<init>(${c.joinToString()})")
45
+ }
46
+ }
47
+ for (m in r.methods) {
48
+ try {
49
+ cls.getMethod(m.name, *m.params.map(::typeOf).toTypedArray())
50
+ } catch (e: Throwable) {
51
+ missing.add("${r.className}#${m.name}(${m.params.joinToString()})")
52
+ }
53
+ }
54
+ for (f in r.fields) {
55
+ try {
56
+ cls.getField(f)
57
+ } catch (e: Throwable) {
58
+ missing.add("${r.className}.$f")
59
+ }
60
+ }
61
+ }
62
+ return missing
63
+ }
64
+
65
+ /** Vérifie toutes les marches dont le SDK est présent (les absentes sont ignorées). */
66
+ fun verifyAll(): Map<String, List<String>> = buildMap {
67
+ if (SdkReflect.exists("com.epson.epos2.printer.Printer")) put("epson", verify(EPSON))
68
+ if (SdkReflect.exists("com.zebra.sdk.comm.Connection")) put("zebra", verify(ZEBRA))
69
+ if (SdkReflect.exists("com.brother.sdk.lmprinter.PrinterDriverGenerator")) put("brother", verify(BROTHER))
70
+ }
71
+
72
+ private fun typeOf(name: String): Class<*> = when (name) {
73
+ "int" -> Int::class.javaPrimitiveType!!
74
+ "long" -> Long::class.javaPrimitiveType!!
75
+ "double" -> Double::class.javaPrimitiveType!!
76
+ "float" -> Float::class.javaPrimitiveType!!
77
+ "boolean" -> Boolean::class.javaPrimitiveType!!
78
+ else -> Class.forName(name)
79
+ }
80
+
81
+ private const val BITMAP = "android.graphics.Bitmap"
82
+ private const val CONTEXT = "android.content.Context"
83
+ private const val BT_ADAPTER = "android.bluetooth.BluetoothAdapter"
84
+
85
+ // ------------------------------------------------------------------ Epson
86
+ val EPSON = listOf(
87
+ Requirement(
88
+ "com.epson.epos2.printer.Printer",
89
+ constructors = listOf(listOf("int", "int", CONTEXT)),
90
+ methods = listOf(
91
+ Method("connect", listOf("java.lang.String", "int")),
92
+ Method("disconnect"),
93
+ Method("clearCommandBuffer"),
94
+ Method("beginTransaction"),
95
+ Method("endTransaction"),
96
+ Method("addImage", listOf(BITMAP, "int", "int", "int", "int", "int", "int", "int", "double", "int")),
97
+ Method("addCut", listOf("int")),
98
+ Method("addPulse", listOf("int", "int")),
99
+ Method("sendData", listOf("int")),
100
+ Method("getStatus"),
101
+ Method("addText", listOf("java.lang.String")),
102
+ Method("addTextAlign", listOf("int")),
103
+ Method("addTextStyle", listOf("int", "int", "int", "int")),
104
+ Method("addTextSize", listOf("int", "int")),
105
+ Method("addFeedLine", listOf("int")),
106
+ Method("addSymbol", listOf("java.lang.String", "int", "int", "int", "int", "int")),
107
+ Method("addBarcode", listOf("java.lang.String", "int", "int", "int", "int", "int")),
108
+ ),
109
+ fields = listOf(
110
+ "TRUE", "FALSE", "MODEL_ANK", "PARAM_DEFAULT", "COLOR_1", "MODE_MONO", "COMPRESS_AUTO",
111
+ "HALFTONE_DITHER", "HALFTONE_ERROR_DIFFUSION", "HALFTONE_THRESHOLD",
112
+ "CUT_FEED", "DRAWER_2PIN", "PULSE_100", "PAPER_EMPTY", "PAPER_NEAR_END",
113
+ "ALIGN_LEFT", "ALIGN_CENTER", "ALIGN_RIGHT", "LEVEL_M", "FONT_A",
114
+ ),
115
+ ),
116
+ Requirement("com.epson.epos2.printer.PrinterStatusInfo", fields = listOf("connection", "online", "paper", "coverOpen")),
117
+ Requirement(
118
+ "com.epson.epos2.discovery.Discovery",
119
+ methods = listOf(
120
+ Method("start", listOf(CONTEXT, "com.epson.epos2.discovery.FilterOption", "com.epson.epos2.discovery.DiscoveryListener")),
121
+ Method("stop"),
122
+ ),
123
+ fields = listOf("TYPE_PRINTER"),
124
+ ),
125
+ Requirement("com.epson.epos2.discovery.FilterOption", constructors = listOf(emptyList()), methods = listOf(Method("setDeviceType", listOf("int")))),
126
+ Requirement("com.epson.epos2.discovery.DeviceInfo", methods = listOf(Method("getTarget"), Method("getDeviceName"))),
127
+ Requirement("com.epson.epos2.discovery.DiscoveryListener"),
128
+ )
129
+
130
+ // ------------------------------------------------------------------ Zebra
131
+ val ZEBRA = listOf(
132
+ Requirement("com.zebra.sdk.comm.Connection", methods = listOf(Method("open"), Method("close"), Method("isConnected"))),
133
+ Requirement("com.zebra.sdk.comm.TcpConnection", constructors = listOf(listOf("java.lang.String", "int"))),
134
+ Requirement("com.zebra.sdk.comm.BluetoothConnection", constructors = listOf(listOf("java.lang.String"))),
135
+ Requirement("com.zebra.sdk.printer.ZebraPrinterFactory", methods = listOf(Method("getInstance", listOf("com.zebra.sdk.comm.Connection")))),
136
+ Requirement("com.zebra.sdk.graphics.ZebraImageFactory", methods = listOf(Method("getImage", listOf(BITMAP)))),
137
+ Requirement("com.zebra.sdk.graphics.ZebraImageI"),
138
+ Requirement("com.zebra.sdk.printer.discovery.NetworkDiscoverer", methods = listOf(Method("findPrinters", listOf("com.zebra.sdk.printer.discovery.DiscoveryHandler")))),
139
+ Requirement("com.zebra.sdk.printer.discovery.BluetoothDiscoverer", methods = listOf(Method("findPrinters", listOf(CONTEXT, "com.zebra.sdk.printer.discovery.DiscoveryHandler")))),
140
+ Requirement("com.zebra.sdk.printer.discovery.DiscoveryHandler"),
141
+ )
142
+
143
+ // ---------------------------------------------------------------- Brother
144
+ val BROTHER = listOf(
145
+ Requirement("com.brother.sdk.lmprinter.PrinterDriverGenerator", methods = listOf(Method("openChannel", listOf("com.brother.sdk.lmprinter.Channel")))),
146
+ Requirement(
147
+ "com.brother.sdk.lmprinter.Channel",
148
+ methods = listOf(
149
+ Method("newWifiChannel", listOf("java.lang.String")),
150
+ Method("newBluetoothChannel", listOf("java.lang.String", BT_ADAPTER)),
151
+ Method("newBluetoothLowEnergyChannel", listOf("java.lang.String", CONTEXT, BT_ADAPTER)),
152
+ ),
153
+ ),
154
+ Requirement("com.brother.sdk.lmprinter.PrinterDriver", methods = listOf(Method("printImage", listOf(BITMAP, "com.brother.sdk.lmprinter.setting.PrintImageSettings")), Method("closeChannel"))),
155
+ Requirement("com.brother.sdk.lmprinter.setting.PrinterModel"),
156
+ Requirement("com.brother.sdk.lmprinter.setting.PrintImageSettings"),
157
+ )
158
+ }
@@ -0,0 +1,104 @@
1
+ package com.delicity.thermalprinter.adapters
2
+
3
+ import java.lang.reflect.InvocationHandler
4
+ import java.lang.reflect.Method
5
+ import java.lang.reflect.Proxy
6
+
7
+ /**
8
+ * Boîte à outils de réflexion pour piloter les SDK fabricants NON redistribuables
9
+ * (Epson ePOS2, Brother, Zebra Link-OS) SANS dépendance de compilation.
10
+ *
11
+ * Pourquoi la réflexion ? Les licences de ces SDK interdisent leur redistribution
12
+ * (Maven Central / CocoaPods), donc on ne peut pas compiler le plugin contre leurs
13
+ * types. La réflexion permet :
14
+ * - de compiler/publier le plugin SANS les binaires,
15
+ * - d'activer automatiquement l'adapter quand l'app fournit le binaire
16
+ * (voir docs/SDK_INTEGRATION.md).
17
+ *
18
+ * ⚠️ Le code réflexif n'est pas vérifié par le compilateur : toute évolution
19
+ * d'API du SDK doit être testée sur device avec le binaire réel.
20
+ */
21
+ object SdkReflect {
22
+
23
+ fun classOrNull(name: String): Class<*>? = try {
24
+ Class.forName(name)
25
+ } catch (e: Throwable) {
26
+ null
27
+ }
28
+
29
+ fun exists(name: String): Boolean = classOrNull(name) != null
30
+
31
+ /** Instancie une classe via un constructeur dont on donne les types de paramètres. */
32
+ fun newInstance(className: String, paramTypes: Array<Class<*>>, args: Array<Any?>): Any {
33
+ val c = classOrNull(className) ?: error("Classe absente: $className")
34
+ return c.getConstructor(*paramTypes).newInstance(*args)
35
+ }
36
+
37
+ /** Appelle une méthode d'instance. */
38
+ fun call(target: Any, method: String, paramTypes: Array<Class<*>> = emptyArray(), args: Array<Any?> = emptyArray()): Any? =
39
+ target.javaClass.getMethod(method, *paramTypes).invoke(target, *args)
40
+
41
+ /** Appelle une méthode statique. */
42
+ fun callStatic(className: String, method: String, paramTypes: Array<Class<*>> = emptyArray(), args: Array<Any?> = emptyArray()): Any? {
43
+ val c = classOrNull(className) ?: error("Classe absente: $className")
44
+ return c.getMethod(method, *paramTypes).invoke(null, *args)
45
+ }
46
+
47
+ /** Lit une constante (champ static) — typiquement les `int` du SDK. */
48
+ fun staticInt(className: String, field: String, fallback: Int = 0): Int = try {
49
+ classOrNull(className)?.getField(field)?.getInt(null) ?: fallback
50
+ } catch (e: Throwable) {
51
+ fallback
52
+ }
53
+
54
+ fun staticField(className: String, field: String): Any? = try {
55
+ classOrNull(className)?.getField(field)?.get(null)
56
+ } catch (e: Throwable) {
57
+ null
58
+ }
59
+
60
+ /** Lit un champ d'instance (public). */
61
+ fun field(target: Any, name: String): Any? = try {
62
+ target.javaClass.getField(name).get(target)
63
+ } catch (e: Throwable) {
64
+ null
65
+ }
66
+
67
+ fun intField(target: Any, name: String, fallback: Int = 0): Int =
68
+ (field(target, name) as? Int) ?: fallback
69
+
70
+ /** Valeur d'enum par nom. */
71
+ @Suppress("UNCHECKED_CAST")
72
+ fun enumValue(enumClass: String, name: String): Any? {
73
+ val c = classOrNull(enumClass) ?: return null
74
+ return try {
75
+ java.lang.Enum.valueOf(c as Class<out Enum<*>>, name)
76
+ } catch (e: Throwable) {
77
+ null
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Crée un proxy dynamique implémentant une interface listener du SDK.
83
+ * [handlers] mappe un nom de méthode -> lambda recevant les arguments.
84
+ */
85
+ fun proxy(interfaceName: String, handlers: Map<String, (Array<Any?>) -> Any?>): Any {
86
+ val iface = classOrNull(interfaceName) ?: error("Interface absente: $interfaceName")
87
+ return Proxy.newProxyInstance(iface.classLoader, arrayOf(iface), object : InvocationHandler {
88
+ override fun invoke(proxy: Any?, method: Method, args: Array<out Any?>?): Any? {
89
+ val a = (args ?: emptyArray()).map { it }.toTypedArray()
90
+ return handlers[method.name]?.invoke(a) ?: defaultFor(method.returnType)
91
+ }
92
+ })
93
+ }
94
+
95
+ private fun defaultFor(t: Class<*>): Any? = when (t) {
96
+ Boolean::class.javaPrimitiveType -> false
97
+ Int::class.javaPrimitiveType -> 0
98
+ Long::class.javaPrimitiveType -> 0L
99
+ Double::class.javaPrimitiveType -> 0.0
100
+ Float::class.javaPrimitiveType -> 0f
101
+ Void.TYPE -> null
102
+ else -> null
103
+ }
104
+ }