@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,100 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.model
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Modèle d'items texte (miroir Kotlin de src/core/text.ts).
|
|
8
|
+
* Parsé depuis le tableau JSON envoyé par printText([...]).
|
|
9
|
+
*/
|
|
10
|
+
data class TextStyle(
|
|
11
|
+
val align: String? = null,
|
|
12
|
+
val bold: Boolean = false,
|
|
13
|
+
val underline: String = "none",
|
|
14
|
+
val font: String = "A",
|
|
15
|
+
val widthMultiplier: Int = 1,
|
|
16
|
+
val heightMultiplier: Int = 1,
|
|
17
|
+
val doubleStrike: Boolean = false,
|
|
18
|
+
val invert: Boolean = false,
|
|
19
|
+
val upsideDown: Boolean = false,
|
|
20
|
+
val rotate90: Boolean = false,
|
|
21
|
+
val letterSpacing: Int? = null,
|
|
22
|
+
val lineSpacing: Int? = null,
|
|
23
|
+
val codePage: String? = null,
|
|
24
|
+
val codePageId: Int? = null,
|
|
25
|
+
val newline: Boolean = true,
|
|
26
|
+
) {
|
|
27
|
+
companion object {
|
|
28
|
+
fun fromJson(o: JSONObject?): TextStyle {
|
|
29
|
+
if (o == null) return TextStyle()
|
|
30
|
+
return TextStyle(
|
|
31
|
+
align = o.optString("align").ifEmpty { null },
|
|
32
|
+
bold = o.optBoolean("bold", false),
|
|
33
|
+
underline = o.optString("underline", "none"),
|
|
34
|
+
font = o.optString("font", "A"),
|
|
35
|
+
widthMultiplier = o.optInt("widthMultiplier", 1),
|
|
36
|
+
heightMultiplier = o.optInt("heightMultiplier", 1),
|
|
37
|
+
doubleStrike = o.optBoolean("doubleStrike", false),
|
|
38
|
+
invert = o.optBoolean("invert", false),
|
|
39
|
+
upsideDown = o.optBoolean("upsideDown", false),
|
|
40
|
+
rotate90 = o.optBoolean("rotate90", false),
|
|
41
|
+
letterSpacing = if (o.has("letterSpacing")) o.optInt("letterSpacing") else null,
|
|
42
|
+
lineSpacing = if (o.has("lineSpacing")) o.optInt("lineSpacing") else null,
|
|
43
|
+
codePage = o.optString("codePage").ifEmpty { null },
|
|
44
|
+
codePageId = if (o.has("codePageId")) o.optInt("codePageId") else null,
|
|
45
|
+
newline = o.optBoolean("newline", true),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sealed class PrintItem {
|
|
52
|
+
data class Text(val value: String, val style: TextStyle) : PrintItem()
|
|
53
|
+
data class Feed(val lines: Int) : PrintItem()
|
|
54
|
+
data class Cut(val mode: String, val feedBefore: Int) : PrintItem()
|
|
55
|
+
data class Divider(val char: String, val columns: Int?, val align: String?, val bold: Boolean) : PrintItem()
|
|
56
|
+
data class QrCode(val value: String, val size: Int, val ec: String, val align: String) : PrintItem()
|
|
57
|
+
data class Barcode(
|
|
58
|
+
val value: String, val symbology: String, val height: Int, val width: Int, val hri: String, val align: String,
|
|
59
|
+
) : PrintItem()
|
|
60
|
+
data class CashDrawer(val pin: Int) : PrintItem()
|
|
61
|
+
data class Image(val filePath: String?, val url: String?, val base64: String?, val render: JSONObject?) : PrintItem()
|
|
62
|
+
data class Raw(val bytesBase64: String) : PrintItem()
|
|
63
|
+
|
|
64
|
+
companion object {
|
|
65
|
+
fun parseList(arr: JSONArray): List<PrintItem> =
|
|
66
|
+
(0 until arr.length()).mapNotNull { parse(arr.optJSONObject(it)) }
|
|
67
|
+
|
|
68
|
+
private fun parse(o: JSONObject?): PrintItem? {
|
|
69
|
+
if (o == null) return null
|
|
70
|
+
return when (o.optString("type")) {
|
|
71
|
+
"text" -> Text(o.optString("value"), TextStyle.fromJson(o.optJSONObject("style")))
|
|
72
|
+
"feed" -> Feed(o.optInt("lines", 1))
|
|
73
|
+
"cut" -> Cut(o.optString("mode", "partial"), o.optInt("feedBefore", 0))
|
|
74
|
+
"divider" -> Divider(
|
|
75
|
+
o.optString("char", "-"),
|
|
76
|
+
if (o.has("columns")) o.optInt("columns") else null,
|
|
77
|
+
o.optJSONObject("style")?.optString("align")?.ifEmpty { null },
|
|
78
|
+
o.optJSONObject("style")?.optBoolean("bold", false) ?: false,
|
|
79
|
+
)
|
|
80
|
+
"qrcode" -> QrCode(o.optString("value"), o.optInt("size", 6), o.optString("errorCorrection", "M"), o.optString("align", "center"))
|
|
81
|
+
"barcode" -> Barcode(
|
|
82
|
+
o.optString("value"), o.optString("symbology", "CODE128"),
|
|
83
|
+
o.optInt("height", 80), o.optInt("width", 3),
|
|
84
|
+
o.optString("hri", "below"), o.optString("align", "center"),
|
|
85
|
+
)
|
|
86
|
+
"cashDrawer" -> CashDrawer(o.optInt("pin", 2))
|
|
87
|
+
"image" -> o.optJSONObject("image").let { img ->
|
|
88
|
+
Image(
|
|
89
|
+
img?.optString("filePath")?.ifEmpty { null },
|
|
90
|
+
img?.optString("url")?.ifEmpty { null },
|
|
91
|
+
img?.optString("base64")?.ifEmpty { null },
|
|
92
|
+
o.optJSONObject("render"),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
"raw" -> Raw(o.optString("bytesBase64"))
|
|
96
|
+
else -> null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.store
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import com.delicity.thermalprinter.model.PrinterProfile
|
|
6
|
+
import org.json.JSONArray
|
|
7
|
+
import org.json.JSONObject
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Persistance des profils d'imprimantes (SharedPreferences + JSON).
|
|
11
|
+
*
|
|
12
|
+
* Conçu pour la reconnexion automatique : tout le nécessaire pour rejoindre une
|
|
13
|
+
* imprimante sans re-découverte est stocké dans le PrinterProfile.
|
|
14
|
+
*
|
|
15
|
+
* NB : les profils ne contiennent pas de secret. Si un jour des credentials
|
|
16
|
+
* réseau sont stockés, migrer vers EncryptedSharedPreferences.
|
|
17
|
+
*/
|
|
18
|
+
class PrinterStore(context: Context) {
|
|
19
|
+
|
|
20
|
+
private val prefs: SharedPreferences =
|
|
21
|
+
context.getSharedPreferences("delicity.thermalprinter", Context.MODE_PRIVATE)
|
|
22
|
+
|
|
23
|
+
@Synchronized
|
|
24
|
+
fun all(): List<PrinterProfile> {
|
|
25
|
+
val raw = prefs.getString(KEY_PROFILES, "[]") ?: "[]"
|
|
26
|
+
val arr = JSONArray(raw)
|
|
27
|
+
return (0 until arr.length()).map { PrinterProfile.fromJson(arr.getJSONObject(it)) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun get(id: String): PrinterProfile? = all().firstOrNull { it.id == id }
|
|
31
|
+
|
|
32
|
+
fun getDefault(): PrinterProfile? = all().firstOrNull { it.isDefault }
|
|
33
|
+
|
|
34
|
+
@Synchronized
|
|
35
|
+
fun upsert(profile: PrinterProfile) {
|
|
36
|
+
val list = all().toMutableList()
|
|
37
|
+
val idx = list.indexOfFirst { it.id == profile.id }
|
|
38
|
+
profile.updatedAt = System.currentTimeMillis()
|
|
39
|
+
if (idx >= 0) list[idx] = profile else list.add(profile)
|
|
40
|
+
persist(list)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Synchronized
|
|
44
|
+
fun setDefault(id: String): PrinterProfile? {
|
|
45
|
+
val list = all().toMutableList()
|
|
46
|
+
var target: PrinterProfile? = null
|
|
47
|
+
list.forEach {
|
|
48
|
+
it.isDefault = it.id == id
|
|
49
|
+
if (it.isDefault) target = it
|
|
50
|
+
}
|
|
51
|
+
persist(list)
|
|
52
|
+
return target
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Synchronized
|
|
56
|
+
fun remove(id: String) {
|
|
57
|
+
persist(all().filterNot { it.id == id })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun persist(list: List<PrinterProfile>) {
|
|
61
|
+
val arr = JSONArray()
|
|
62
|
+
list.forEach { arr.put(it.toJson()) }
|
|
63
|
+
prefs.edit().putString(KEY_PROFILES, arr.toString()).apply()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
companion object {
|
|
67
|
+
private const val KEY_PROFILES = "profiles_v1"
|
|
68
|
+
|
|
69
|
+
fun emptyJson(): JSONObject = JSONObject()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.transport
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.bluetooth.BluetoothDevice
|
|
5
|
+
import android.bluetooth.BluetoothGatt
|
|
6
|
+
import android.bluetooth.BluetoothGattCallback
|
|
7
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
8
|
+
import android.bluetooth.BluetoothProfile
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import com.delicity.thermalprinter.model.ErrorCode
|
|
12
|
+
import com.delicity.thermalprinter.model.PrinterException
|
|
13
|
+
import java.util.UUID
|
|
14
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
15
|
+
import kotlin.coroutines.Continuation
|
|
16
|
+
import kotlin.coroutines.resume
|
|
17
|
+
import kotlin.coroutines.resumeWithException
|
|
18
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
19
|
+
import kotlinx.coroutines.withTimeout
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Client GATT BLE suspend pour imprimantes ESC/POS exposant une "UART série" BLE.
|
|
23
|
+
*
|
|
24
|
+
* Flux : connectGatt -> (MTU) -> discoverServices -> localiser la characteristic
|
|
25
|
+
* d'écriture (allowlist d'UUID connus, sinon 1re characteristic WRITE/WRITE_NO_RESPONSE)
|
|
26
|
+
* -> écriture du raster ESC/POS par paquets <= (MTU-3).
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ Il n'existe pas de profil BLE standard d'impression : on s'appuie sur une
|
|
29
|
+
* allowlist de services/characteristics validés (cf. [WRITE_TARGETS]). Pour un
|
|
30
|
+
* modèle non listé, on tente la 1re characteristic inscriptible trouvée.
|
|
31
|
+
*/
|
|
32
|
+
@SuppressLint("MissingPermission")
|
|
33
|
+
class BleGattClient(
|
|
34
|
+
private val context: Context,
|
|
35
|
+
private val device: BluetoothDevice,
|
|
36
|
+
) {
|
|
37
|
+
private var gatt: BluetoothGatt? = null
|
|
38
|
+
private var writeChar: BluetoothGattCharacteristic? = null
|
|
39
|
+
@Volatile private var mtu: Int = 23
|
|
40
|
+
@Volatile var isOpen: Boolean = false
|
|
41
|
+
private set
|
|
42
|
+
|
|
43
|
+
// Continuations one-shot par opération (le GATT sérialise les callbacks).
|
|
44
|
+
private val connectCont = AtomicReference<Continuation<Unit>?>()
|
|
45
|
+
private val mtuCont = AtomicReference<Continuation<Unit>?>()
|
|
46
|
+
private val servicesCont = AtomicReference<Continuation<Unit>?>()
|
|
47
|
+
private val writeCont = AtomicReference<Continuation<Unit>?>()
|
|
48
|
+
|
|
49
|
+
private val callback = object : BluetoothGattCallback() {
|
|
50
|
+
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
|
|
51
|
+
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
|
|
52
|
+
connectCont.getAndSet(null)?.resume(Unit)
|
|
53
|
+
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
54
|
+
isOpen = false
|
|
55
|
+
val e = PrinterException(ErrorCode.CONNECTION_FAILED, "BLE déconnecté (status=$status)", retryable = true)
|
|
56
|
+
connectCont.getAndSet(null)?.resumeWithException(e)
|
|
57
|
+
writeCont.getAndSet(null)?.resumeWithException(e)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun onMtuChanged(g: BluetoothGatt, newMtu: Int, status: Int) {
|
|
62
|
+
mtu = if (status == BluetoothGatt.GATT_SUCCESS) newMtu else mtu
|
|
63
|
+
mtuCont.getAndSet(null)?.resume(Unit)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
|
|
67
|
+
servicesCont.getAndSet(null)?.let { cont ->
|
|
68
|
+
if (status == BluetoothGatt.GATT_SUCCESS) cont.resume(Unit)
|
|
69
|
+
else cont.resumeWithException(PrinterException(ErrorCode.CONNECTION_FAILED, "discoverServices BLE échoué"))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Deprecated("Deprecated in Java")
|
|
74
|
+
override fun onCharacteristicWrite(g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int) {
|
|
75
|
+
writeCont.getAndSet(null)?.let { cont ->
|
|
76
|
+
if (status == BluetoothGatt.GATT_SUCCESS) cont.resume(Unit)
|
|
77
|
+
else cont.resumeWithException(PrinterException(ErrorCode.PRINT_FAILED, "Écriture BLE échouée (status=$status)", retryable = true))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
suspend fun open(timeoutMs: Long) {
|
|
83
|
+
withTimeout(timeoutMs.coerceAtLeast(3000)) {
|
|
84
|
+
suspendCancellableCoroutine<Unit> { cont ->
|
|
85
|
+
connectCont.set(cont)
|
|
86
|
+
cont.invokeOnCancellation { runCatching { gatt?.close() } }
|
|
87
|
+
gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
88
|
+
device.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE)
|
|
89
|
+
} else {
|
|
90
|
+
device.connectGatt(context, false, callback)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// MTU max (512) pour réduire le nombre de paquets ; best effort.
|
|
94
|
+
runCatching {
|
|
95
|
+
suspendCancellableCoroutine<Unit> { cont ->
|
|
96
|
+
mtuCont.set(cont)
|
|
97
|
+
if (gatt?.requestMtu(512) != true) mtuCont.getAndSet(null)?.resume(Unit)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
suspendCancellableCoroutine<Unit> { cont ->
|
|
101
|
+
servicesCont.set(cont)
|
|
102
|
+
if (gatt?.discoverServices() != true) {
|
|
103
|
+
servicesCont.getAndSet(null)?.resumeWithException(
|
|
104
|
+
PrinterException(ErrorCode.CONNECTION_FAILED, "discoverServices BLE non démarré"),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
writeChar = locateWriteCharacteristic()
|
|
109
|
+
?: throw PrinterException(ErrorCode.UNSUPPORTED_PRINTER, "Aucune characteristic BLE inscriptible (allowlist UUID requise)")
|
|
110
|
+
isOpen = true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Écrit [data] par paquets <= (MTU-3), en attendant l'ACK entre chaque paquet. */
|
|
115
|
+
suspend fun write(data: ByteArray) {
|
|
116
|
+
val g = gatt ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "GATT non ouvert")
|
|
117
|
+
val ch = writeChar ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Characteristic d'écriture absente")
|
|
118
|
+
val packet = (mtu - 3).coerceIn(20, 512)
|
|
119
|
+
val noResponse = (ch.properties and BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
|
|
120
|
+
val writeType = if (noResponse) {
|
|
121
|
+
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
122
|
+
} else {
|
|
123
|
+
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
124
|
+
}
|
|
125
|
+
var offset = 0
|
|
126
|
+
while (offset < data.size) {
|
|
127
|
+
val len = minOf(packet, data.size - offset)
|
|
128
|
+
val slice = data.copyOfRange(offset, offset + len)
|
|
129
|
+
suspendCancellableCoroutine<Unit> { cont ->
|
|
130
|
+
writeCont.set(cont)
|
|
131
|
+
val ok = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
132
|
+
g.writeCharacteristic(ch, slice, writeType) == BluetoothGatt.GATT_SUCCESS
|
|
133
|
+
} else {
|
|
134
|
+
@Suppress("DEPRECATION")
|
|
135
|
+
run {
|
|
136
|
+
ch.writeType = writeType
|
|
137
|
+
ch.value = slice
|
|
138
|
+
g.writeCharacteristic(ch)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!ok) writeCont.getAndSet(null)?.resumeWithException(
|
|
142
|
+
PrinterException(ErrorCode.PRINT_FAILED, "writeCharacteristic BLE refusé", retryable = true),
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
offset += len
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fun close() {
|
|
150
|
+
isOpen = false
|
|
151
|
+
runCatching { gatt?.disconnect() }
|
|
152
|
+
runCatching { gatt?.close() }
|
|
153
|
+
gatt = null
|
|
154
|
+
writeChar = null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun locateWriteCharacteristic(): BluetoothGattCharacteristic? {
|
|
158
|
+
val g = gatt ?: return null
|
|
159
|
+
// 1) Allowlist : services/characteristics connus.
|
|
160
|
+
for ((svc, chr) in WRITE_TARGETS) {
|
|
161
|
+
g.getService(svc)?.getCharacteristic(chr)?.let { return it }
|
|
162
|
+
}
|
|
163
|
+
// 2) Fallback : 1re characteristic inscriptible trouvée.
|
|
164
|
+
for (service in g.services) {
|
|
165
|
+
for (c in service.characteristics) {
|
|
166
|
+
val p = c.properties
|
|
167
|
+
if ((p and BluetoothGattCharacteristic.PROPERTY_WRITE) != 0 ||
|
|
168
|
+
(p and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0
|
|
169
|
+
) {
|
|
170
|
+
return c
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
companion object {
|
|
178
|
+
private fun u(s: String) = UUID.fromString(s)
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Allowlist (service -> characteristic d'écriture) des "UART série" BLE
|
|
182
|
+
* couramment exposées par les imprimantes thermiques. À enrichir au fil des
|
|
183
|
+
* modèles validés.
|
|
184
|
+
*/
|
|
185
|
+
val WRITE_TARGETS: List<Pair<UUID, UUID>> = listOf(
|
|
186
|
+
// Nordic UART Service (NUS)
|
|
187
|
+
u("6e400001-b5a3-f393-e0a9-e50e24dcca9e") to u("6e400002-b5a3-f393-e0a9-e50e24dcca9e"),
|
|
188
|
+
// Microchip / ISSC Transparent UART
|
|
189
|
+
u("49535343-fe7d-4ae5-8fa9-9fafd205e455") to u("49535343-8841-43f4-a8d4-ecbe34729bb3"),
|
|
190
|
+
// Générique "FFE0/FFE1" (modules HM-10 et clones)
|
|
191
|
+
u("0000ffe0-0000-1000-8000-00805f9b34fb") to u("0000ffe1-0000-1000-8000-00805f9b34fb"),
|
|
192
|
+
// Générique "FF00/FF02"
|
|
193
|
+
u("0000ff00-0000-1000-8000-00805f9b34fb") to u("0000ff02-0000-1000-8000-00805f9b34fb"),
|
|
194
|
+
// Imprimantes type "18F0/2AF1"
|
|
195
|
+
u("000018f0-0000-1000-8000-00805f9b34fb") to u("00002af1-0000-1000-8000-00805f9b34fb"),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
/** Services BLE à mettre en avant lors du scan (filtre de découverte). */
|
|
199
|
+
val ADVERTISED_SERVICES: List<UUID> = WRITE_TARGETS.map { it.first }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.transport
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.bluetooth.BluetoothAdapter
|
|
5
|
+
import android.bluetooth.BluetoothDevice
|
|
6
|
+
import android.bluetooth.BluetoothSocket
|
|
7
|
+
import com.delicity.thermalprinter.model.ErrorCode
|
|
8
|
+
import com.delicity.thermalprinter.model.PrinterException
|
|
9
|
+
import java.io.InputStream
|
|
10
|
+
import java.io.OutputStream
|
|
11
|
+
import java.util.UUID
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Transport Bluetooth Classic / SPP (Serial Port Profile).
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ ANDROID UNIQUEMENT. iOS n'expose pas le SPP générique (voir limites README).
|
|
17
|
+
*
|
|
18
|
+
* Le SPP est le canal des imprimantes ESC/POS Bluetooth "génériques" très répandues
|
|
19
|
+
* en restauration (modèles chinois à bas coût). UUID SPP standard :
|
|
20
|
+
* 00001101-0000-1000-8000-00805F9B34FB
|
|
21
|
+
*
|
|
22
|
+
* Requiert BLUETOOTH_CONNECT (API 31+) accordée AVANT l'appel.
|
|
23
|
+
*/
|
|
24
|
+
@SuppressLint("MissingPermission")
|
|
25
|
+
class BluetoothSppTransport(
|
|
26
|
+
private val adapter: BluetoothAdapter?,
|
|
27
|
+
private val macAddress: String,
|
|
28
|
+
) : ByteTransport {
|
|
29
|
+
|
|
30
|
+
private var socket: BluetoothSocket? = null
|
|
31
|
+
private var out: OutputStream? = null
|
|
32
|
+
private var input: InputStream? = null
|
|
33
|
+
|
|
34
|
+
override val isOpen: Boolean
|
|
35
|
+
get() = socket?.isConnected == true
|
|
36
|
+
|
|
37
|
+
override fun open(timeoutMs: Long) {
|
|
38
|
+
if (isOpen) return
|
|
39
|
+
val ad = adapter ?: throw PrinterException(ErrorCode.BLUETOOTH_DISABLED, "Bluetooth indisponible")
|
|
40
|
+
if (!ad.isEnabled) throw PrinterException(ErrorCode.BLUETOOTH_DISABLED, "Bluetooth désactivé")
|
|
41
|
+
|
|
42
|
+
val device: BluetoothDevice = try {
|
|
43
|
+
ad.getRemoteDevice(macAddress)
|
|
44
|
+
} catch (e: IllegalArgumentException) {
|
|
45
|
+
throw PrinterException(ErrorCode.PRINTER_NOT_FOUND, "MAC invalide: $macAddress", e.message)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Si non appairé, l'app doit déclencher l'appairage en amont.
|
|
49
|
+
if (device.bondState != BluetoothDevice.BOND_BONDED) {
|
|
50
|
+
throw PrinterException(ErrorCode.PAIRING_REQUIRED, "Appareil non appairé: $macAddress", retryable = false)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
ad.cancelDiscovery() // la découverte ralentit/échoue la connexion
|
|
55
|
+
val sock = device.createRfcommSocketToServiceRecord(SPP_UUID)
|
|
56
|
+
sock.connect() // bloquant ; pas de vrai timeout natif -> orchestré par coroutine
|
|
57
|
+
socket = sock
|
|
58
|
+
out = sock.outputStream
|
|
59
|
+
input = sock.inputStream
|
|
60
|
+
} catch (e: SecurityException) {
|
|
61
|
+
throw PrinterException(ErrorCode.PERMISSION_DENIED, "Permission BLUETOOTH_CONNECT manquante", e.message)
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
// Fallback "insecure" pour certaines imprimantes capricieuses.
|
|
64
|
+
try {
|
|
65
|
+
val sock = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID)
|
|
66
|
+
sock.connect()
|
|
67
|
+
socket = sock
|
|
68
|
+
out = sock.outputStream
|
|
69
|
+
input = sock.inputStream
|
|
70
|
+
} catch (e2: Exception) {
|
|
71
|
+
throw PrinterException(ErrorCode.CONNECTION_FAILED, "Connexion SPP échouée $macAddress", e2.message, retryable = true)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun write(bytes: ByteArray) {
|
|
77
|
+
val o = out ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Socket SPP non ouvert")
|
|
78
|
+
try {
|
|
79
|
+
var offset = 0
|
|
80
|
+
val chunk = 2048 // les buffers SPP sont petits
|
|
81
|
+
while (offset < bytes.size) {
|
|
82
|
+
val len = minOf(chunk, bytes.size - offset)
|
|
83
|
+
o.write(bytes, offset, len)
|
|
84
|
+
o.flush()
|
|
85
|
+
offset += len
|
|
86
|
+
// micro-pause anti-overflow sur imprimantes lentes
|
|
87
|
+
if (bytes.size > 16_384) Thread.sleep(8)
|
|
88
|
+
}
|
|
89
|
+
} catch (e: Exception) {
|
|
90
|
+
throw PrinterException(ErrorCode.PRINT_FAILED, "Écriture SPP échouée", e.message, retryable = true)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override fun read(buffer: ByteArray, timeoutMs: Long): Int {
|
|
95
|
+
val i = input ?: return -1
|
|
96
|
+
return try { i.read(buffer) } catch (e: Exception) { -1 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun close() {
|
|
100
|
+
try { out?.flush() } catch (_: Exception) {}
|
|
101
|
+
try { input?.close() } catch (_: Exception) {}
|
|
102
|
+
try { out?.close() } catch (_: Exception) {}
|
|
103
|
+
try { socket?.close() } catch (_: Exception) {}
|
|
104
|
+
socket = null; out = null; input = null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
companion object {
|
|
108
|
+
val SPP_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.transport
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canal d'octets bas niveau vers une imprimante (TCP, SPP, BLE).
|
|
5
|
+
* Les adapters ESC/POS écrivent des commandes brutes via ce contrat.
|
|
6
|
+
*/
|
|
7
|
+
interface ByteTransport {
|
|
8
|
+
val isOpen: Boolean
|
|
9
|
+
fun open(timeoutMs: Long)
|
|
10
|
+
fun write(bytes: ByteArray)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lecture optionnelle (statut temps réel ESC/POS DLE EOT).
|
|
14
|
+
* Retourne le nombre d'octets lus, ou -1 si non supporté/aucune donnée.
|
|
15
|
+
*/
|
|
16
|
+
fun read(buffer: ByteArray, timeoutMs: Long): Int = -1
|
|
17
|
+
fun close()
|
|
18
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.delicity.thermalprinter.transport
|
|
2
|
+
|
|
3
|
+
import com.delicity.thermalprinter.model.ErrorCode
|
|
4
|
+
import com.delicity.thermalprinter.model.PrinterException
|
|
5
|
+
import java.io.InputStream
|
|
6
|
+
import java.io.OutputStream
|
|
7
|
+
import java.net.InetSocketAddress
|
|
8
|
+
import java.net.Socket
|
|
9
|
+
import java.net.SocketTimeoutException
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Transport TCP brut (RAW / port 9100). Utilisé par EscPosAdapter et RawTcpAdapter
|
|
13
|
+
* en Wi-Fi/Ethernet.
|
|
14
|
+
*
|
|
15
|
+
* @param host adresse IP/hostname
|
|
16
|
+
* @param port port (défaut 9100)
|
|
17
|
+
*/
|
|
18
|
+
class TcpTransport(
|
|
19
|
+
private val host: String,
|
|
20
|
+
private val port: Int = 9100,
|
|
21
|
+
) : ByteTransport {
|
|
22
|
+
|
|
23
|
+
private var socket: Socket? = null
|
|
24
|
+
private var out: OutputStream? = null
|
|
25
|
+
private var input: InputStream? = null
|
|
26
|
+
|
|
27
|
+
override val isOpen: Boolean
|
|
28
|
+
get() = socket?.isConnected == true && socket?.isClosed == false
|
|
29
|
+
|
|
30
|
+
override fun open(timeoutMs: Long) {
|
|
31
|
+
if (isOpen) return
|
|
32
|
+
try {
|
|
33
|
+
val s = Socket()
|
|
34
|
+
s.tcpNoDelay = true
|
|
35
|
+
s.connect(InetSocketAddress(host, port), timeoutMs.toInt())
|
|
36
|
+
s.soTimeout = timeoutMs.toInt()
|
|
37
|
+
socket = s
|
|
38
|
+
out = s.getOutputStream()
|
|
39
|
+
input = s.getInputStream()
|
|
40
|
+
} catch (e: SocketTimeoutException) {
|
|
41
|
+
throw PrinterException(ErrorCode.TIMEOUT, "Timeout connexion TCP $host:$port", e.message, retryable = true)
|
|
42
|
+
} catch (e: Exception) {
|
|
43
|
+
throw PrinterException(ErrorCode.CONNECTION_FAILED, "Connexion TCP échouée $host:$port", e.message, retryable = true)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun write(bytes: ByteArray) {
|
|
48
|
+
val o = out ?: throw PrinterException(ErrorCode.CONNECTION_FAILED, "Socket TCP non ouvert")
|
|
49
|
+
try {
|
|
50
|
+
// Envoi par chunks pour ménager les petits buffers d'imprimante.
|
|
51
|
+
var offset = 0
|
|
52
|
+
val chunk = 4096
|
|
53
|
+
while (offset < bytes.size) {
|
|
54
|
+
val len = minOf(chunk, bytes.size - offset)
|
|
55
|
+
o.write(bytes, offset, len)
|
|
56
|
+
o.flush()
|
|
57
|
+
offset += len
|
|
58
|
+
}
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
throw PrinterException(ErrorCode.PRINT_FAILED, "Écriture TCP échouée", e.message, retryable = true)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override fun read(buffer: ByteArray, timeoutMs: Long): Int {
|
|
65
|
+
val i = input ?: return -1
|
|
66
|
+
return try {
|
|
67
|
+
socket?.soTimeout = timeoutMs.toInt()
|
|
68
|
+
i.read(buffer)
|
|
69
|
+
} catch (e: SocketTimeoutException) {
|
|
70
|
+
-1
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
-1
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun close() {
|
|
77
|
+
try { out?.flush() } catch (_: Exception) {}
|
|
78
|
+
try { input?.close() } catch (_: Exception) {}
|
|
79
|
+
try { out?.close() } catch (_: Exception) {}
|
|
80
|
+
try { socket?.close() } catch (_: Exception) {}
|
|
81
|
+
socket = null; out = null; input = null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PrinterTransport } from '../core/enums';
|
|
2
|
+
import type { DiscoveredPrinter } from '../core/models';
|
|
3
|
+
/**
|
|
4
|
+
* Construit un identifiant interne stable pour une imprimante.
|
|
5
|
+
*
|
|
6
|
+
* On veut le MÊME id quelle que soit la source de découverte afin de fusionner
|
|
7
|
+
* les doublons (ex: une TM-m30 vue à la fois par le SDK Epson et par le scan TCP).
|
|
8
|
+
*
|
|
9
|
+
* Clé de stabilité par transport :
|
|
10
|
+
* - réseau (wifi/ethernet) : adresse IP normalisée (sans port) — l'imprimante a
|
|
11
|
+
* une seule IP même si plusieurs ports répondent.
|
|
12
|
+
* - bluetooth/ble : adresse MAC (Android) ou UUID périphérique (iOS).
|
|
13
|
+
* - usb : vendorId:productId.
|
|
14
|
+
*
|
|
15
|
+
* Le préfixe transport évite les collisions improbables entre familles.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildStableId(transport: PrinterTransport, rawAddress: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Fusionne une liste de découvertes brutes en une liste dédoublonnée.
|
|
20
|
+
* En cas de doublon (même id), on conserve l'entrée au meilleur adapter
|
|
21
|
+
* (déjà résolu par `resolveBestAdapter`) et on fusionne `discoveredBy`.
|
|
22
|
+
*
|
|
23
|
+
* @param incoming découvertes brutes (chaque source peut produire des doublons)
|
|
24
|
+
* @param adapterRank fonction de classement (score) déjà appliquée au champ adapter
|
|
25
|
+
*/
|
|
26
|
+
export declare function mergeDiscoveries(incoming: DiscoveredPrinter[], adapterRank: (p: DiscoveredPrinter) => number): DiscoveredPrinter[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construit un identifiant interne stable pour une imprimante.
|
|
3
|
+
*
|
|
4
|
+
* On veut le MÊME id quelle que soit la source de découverte afin de fusionner
|
|
5
|
+
* les doublons (ex: une TM-m30 vue à la fois par le SDK Epson et par le scan TCP).
|
|
6
|
+
*
|
|
7
|
+
* Clé de stabilité par transport :
|
|
8
|
+
* - réseau (wifi/ethernet) : adresse IP normalisée (sans port) — l'imprimante a
|
|
9
|
+
* une seule IP même si plusieurs ports répondent.
|
|
10
|
+
* - bluetooth/ble : adresse MAC (Android) ou UUID périphérique (iOS).
|
|
11
|
+
* - usb : vendorId:productId.
|
|
12
|
+
*
|
|
13
|
+
* Le préfixe transport évite les collisions improbables entre familles.
|
|
14
|
+
*/
|
|
15
|
+
export function buildStableId(transport, rawAddress) {
|
|
16
|
+
const norm = normalizeAddress(transport, rawAddress);
|
|
17
|
+
return `${transport}:${norm}`;
|
|
18
|
+
}
|
|
19
|
+
function normalizeAddress(transport, address) {
|
|
20
|
+
switch (transport) {
|
|
21
|
+
case 'wifi':
|
|
22
|
+
case 'ethernet':
|
|
23
|
+
// retire un éventuel :port
|
|
24
|
+
return address.replace(/:\d+$/, '').trim().toLowerCase();
|
|
25
|
+
case 'bluetooth':
|
|
26
|
+
case 'ble':
|
|
27
|
+
return address.trim().toUpperCase();
|
|
28
|
+
case 'usb':
|
|
29
|
+
return address.trim().toLowerCase();
|
|
30
|
+
default:
|
|
31
|
+
return address.trim().toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fusionne une liste de découvertes brutes en une liste dédoublonnée.
|
|
36
|
+
* En cas de doublon (même id), on conserve l'entrée au meilleur adapter
|
|
37
|
+
* (déjà résolu par `resolveBestAdapter`) et on fusionne `discoveredBy`.
|
|
38
|
+
*
|
|
39
|
+
* @param incoming découvertes brutes (chaque source peut produire des doublons)
|
|
40
|
+
* @param adapterRank fonction de classement (score) déjà appliquée au champ adapter
|
|
41
|
+
*/
|
|
42
|
+
export function mergeDiscoveries(incoming, adapterRank) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const byId = new Map();
|
|
45
|
+
for (const printer of incoming) {
|
|
46
|
+
const existing = byId.get(printer.id);
|
|
47
|
+
if (!existing) {
|
|
48
|
+
byId.set(printer.id, Object.assign(Object.assign({}, printer), { discoveredBy: dedupeSources(printer) }));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Fusionner les sources de découverte.
|
|
52
|
+
const mergedSources = Array.from(new Set([...((_a = existing.discoveredBy) !== null && _a !== void 0 ? _a : []), ...((_b = printer.discoveredBy) !== null && _b !== void 0 ? _b : []), printer.adapter, existing.adapter]));
|
|
53
|
+
// Garder l'entrée au meilleur adapter.
|
|
54
|
+
const winner = adapterRank(printer) > adapterRank(existing) ? printer : existing;
|
|
55
|
+
byId.set(printer.id, Object.assign(Object.assign({}, winner), {
|
|
56
|
+
// capacités: union (on garde le plus d'infos possible)
|
|
57
|
+
capabilities: Object.assign(Object.assign(Object.assign({}, existing.capabilities), printer.capabilities), winner.capabilities), lastSeenAt: Math.max(existing.lastSeenAt, printer.lastSeenAt), discoveredBy: mergedSources, isDefault: existing.isDefault || printer.isDefault, isConnected: existing.isConnected || printer.isConnected }));
|
|
58
|
+
}
|
|
59
|
+
return Array.from(byId.values());
|
|
60
|
+
}
|
|
61
|
+
function dedupeSources(p) {
|
|
62
|
+
var _a;
|
|
63
|
+
const base = (_a = p.discoveredBy) !== null && _a !== void 0 ? _a : [];
|
|
64
|
+
return Array.from(new Set([...base, p.adapter]));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=dedup.js.map
|