@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,395 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Cœur applicatif iOS (miroir de ThermalPrinterEngine.kt). Indépendant de Capacitor.
|
|
5
|
+
final class ThermalPrinterEngine {
|
|
6
|
+
|
|
7
|
+
private let store = PrinterStore()
|
|
8
|
+
private let imageCache = ImageCache()
|
|
9
|
+
|
|
10
|
+
/// Registry d'adapters (l'ordre n'importe pas, priorité gérée ailleurs).
|
|
11
|
+
private lazy var adapters: [PrinterAdapter] = [
|
|
12
|
+
EpsonAdapter(), StarAdapter(), BrotherAdapter(), ZebraAdapter(),
|
|
13
|
+
EscPosAdapter(), RawTcpAdapter(),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
private var lastDiscovered: [DiscoveredPrinter] = []
|
|
17
|
+
private let lock = NSLock()
|
|
18
|
+
|
|
19
|
+
/// Émetteur d'états de job (branché par le plugin sur notifyListeners).
|
|
20
|
+
var onJobUpdate: ((JobUpdate) -> Void)?
|
|
21
|
+
|
|
22
|
+
/// Émetteur de changement de statut (branché par le plugin sur 'statusChange').
|
|
23
|
+
var onStatusChange: ((PrinterStatus) -> Void)?
|
|
24
|
+
|
|
25
|
+
/// Registre des moniteurs de statut actifs (Phase 6).
|
|
26
|
+
private var monitors: [String: Task<Void, Never>] = [:]
|
|
27
|
+
private let monitorLock = NSLock()
|
|
28
|
+
|
|
29
|
+
private func emitJob(_ jobId: String, _ printerId: String, _ state: String,
|
|
30
|
+
holdReason: String? = nil, progress: Double? = nil,
|
|
31
|
+
errorCode: ErrorCode? = nil, message: String? = nil) {
|
|
32
|
+
onJobUpdate?(JobUpdate(jobId: jobId, printerId: printerId, state: state,
|
|
33
|
+
holdReason: holdReason, progress: progress, errorCode: errorCode, message: message))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: Découverte
|
|
37
|
+
|
|
38
|
+
func discover(_ options: DiscoveryManager.Options, emitPartial: @escaping (DiscoveredPrinter) -> Void) async -> (printers: [DiscoveredPrinter], failed: [String]) {
|
|
39
|
+
Logger.shared.log("discovery", "start")
|
|
40
|
+
let manager = DiscoveryManager(adapters: adapters)
|
|
41
|
+
var (printers, failed) = await manager.discover(options, emitPartial: emitPartial)
|
|
42
|
+
let defaultId = store.getDefault()?.id
|
|
43
|
+
for i in printers.indices {
|
|
44
|
+
printers[i].isDefault = printers[i].id == defaultId
|
|
45
|
+
printers[i].isConnected = adapterFor(printers[i].adapter)?.isConnected(printers[i].id) ?? false
|
|
46
|
+
}
|
|
47
|
+
lock.lock(); lastDiscovered = printers; lock.unlock()
|
|
48
|
+
Logger.shared.log("discovery", "complete", ["count": printers.count])
|
|
49
|
+
return (printers, failed)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MARK: Connexion
|
|
53
|
+
|
|
54
|
+
func connect(_ printerId: String, timeoutMs: Int, forceAdapter: AdapterId?, setAsDefault: Bool = false) async throws -> Bool {
|
|
55
|
+
let profile = try resolveProfile(printerId, forceAdapter: forceAdapter)
|
|
56
|
+
guard let adapter = adapterFor(profile.adapter) else {
|
|
57
|
+
throw PrinterError(.UNSUPPORTED_PRINTER, "Aucun adapter pour \(profile.adapter.rawValue)")
|
|
58
|
+
}
|
|
59
|
+
guard adapter.isAvailable() else {
|
|
60
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "Adapter \(profile.adapter.rawValue) indisponible")
|
|
61
|
+
}
|
|
62
|
+
try await adapter.connect(profile, timeoutMs: timeoutMs)
|
|
63
|
+
let connected = adapter.isConnected(printerId)
|
|
64
|
+
// setAsDefault UNIQUEMENT si la connexion a réussi.
|
|
65
|
+
if connected && setAsDefault {
|
|
66
|
+
store.upsert(profile)
|
|
67
|
+
store.setDefault(printerId)
|
|
68
|
+
Logger.shared.log("connect", "set-default-after-connect", ["id": printerId])
|
|
69
|
+
}
|
|
70
|
+
return connected
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func disconnect(_ printerId: String) async {
|
|
74
|
+
guard let profile = store.get(printerId) ?? ephemeral(for: printerId),
|
|
75
|
+
let adapter = adapterFor(profile.adapter) else { return }
|
|
76
|
+
await adapter.disconnect(printerId)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private static let reconnectAttempts = 3
|
|
80
|
+
|
|
81
|
+
/// Reconnexion auto avec **backoff exponentiel** (300ms, 600ms, 1200ms…, plafonné).
|
|
82
|
+
/// Les erreurs non-retryables court-circuitent.
|
|
83
|
+
private func ensureConnected(_ profile: PrinterProfile, timeoutMs: Int) async throws {
|
|
84
|
+
guard let adapter = adapterFor(profile.adapter) else {
|
|
85
|
+
throw PrinterError(.UNSUPPORTED_PRINTER, "Adapter introuvable")
|
|
86
|
+
}
|
|
87
|
+
if adapter.isConnected(profile.id) { return }
|
|
88
|
+
|
|
89
|
+
var backoff: UInt64 = 300
|
|
90
|
+
var lastError: Error?
|
|
91
|
+
for attempt in 1...Self.reconnectAttempts {
|
|
92
|
+
do {
|
|
93
|
+
Logger.shared.log("connect", "auto-reconnect", ["id": profile.id, "attempt": attempt])
|
|
94
|
+
try await adapter.connect(profile, timeoutMs: timeoutMs)
|
|
95
|
+
if adapter.isConnected(profile.id) {
|
|
96
|
+
if attempt > 1 { Logger.shared.log("connect", "reconnect-recovered", ["id": profile.id, "attempt": attempt]) }
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
lastError = PrinterError(.CONNECTION_FAILED, "Connexion non établie", retryable: true)
|
|
100
|
+
} catch let e as PrinterError {
|
|
101
|
+
lastError = e
|
|
102
|
+
if !e.retryable { throw e }
|
|
103
|
+
} catch {
|
|
104
|
+
lastError = error
|
|
105
|
+
}
|
|
106
|
+
if attempt < Self.reconnectAttempts {
|
|
107
|
+
Logger.shared.log("connect", "backoff", ["id": profile.id, "delayMs": backoff])
|
|
108
|
+
try? await Task.sleep(nanoseconds: backoff * 1_000_000)
|
|
109
|
+
backoff = min(backoff * 2, 3000)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw lastError ?? PrinterError(.CONNECTION_FAILED, "Reconnexion échouée (\(Self.reconnectAttempts) tentatives)", retryable: true)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: Impression
|
|
116
|
+
|
|
117
|
+
struct PrintRequest {
|
|
118
|
+
let printerId: String?
|
|
119
|
+
let filePath: String?
|
|
120
|
+
let url: String?
|
|
121
|
+
let base64: String?
|
|
122
|
+
let render: RenderOptions?
|
|
123
|
+
let timeoutMs: Int
|
|
124
|
+
let autoReconnect: Bool
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
struct PrintTextRequest {
|
|
128
|
+
let printerId: String?
|
|
129
|
+
let items: [PrintItem]
|
|
130
|
+
let defaultCodePage: String
|
|
131
|
+
let cut: Bool
|
|
132
|
+
let feedLines: Int
|
|
133
|
+
let timeoutMs: Int
|
|
134
|
+
let autoReconnect: Bool
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
struct PrintOutcome {
|
|
138
|
+
let printerId: String
|
|
139
|
+
let adapter: AdapterId
|
|
140
|
+
let jobId: String
|
|
141
|
+
let state: String
|
|
142
|
+
let bytesSent: Int
|
|
143
|
+
let durationMs: Int
|
|
144
|
+
let status: PrinterStatus?
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func printImage(_ req: PrintRequest) async throws -> PrintOutcome {
|
|
148
|
+
let started = Date()
|
|
149
|
+
let jobId = UUID().uuidString
|
|
150
|
+
let profile = try resolveTargetProfile(req.printerId)
|
|
151
|
+
emitJob(jobId, profile.id, "pending")
|
|
152
|
+
guard let adapter = adapterFor(profile.adapter) else {
|
|
153
|
+
let e = PrinterError(.UNSUPPORTED_PRINTER, "Adapter introuvable")
|
|
154
|
+
emitJob(jobId, profile.id, "failed", errorCode: e.code, message: e.message); throw e
|
|
155
|
+
}
|
|
156
|
+
do {
|
|
157
|
+
if !adapter.isConnected(profile.id) {
|
|
158
|
+
guard req.autoReconnect else { throw PrinterError(.CONNECTION_FAILED, "Imprimante non connectée") }
|
|
159
|
+
try await ensureConnected(profile, timeoutMs: req.timeoutMs)
|
|
160
|
+
}
|
|
161
|
+
try await preflightHold(adapter, profile, jobId)
|
|
162
|
+
|
|
163
|
+
let image = try await loadImage(req)
|
|
164
|
+
let render = resolveRenderOptions(profile, req.render)
|
|
165
|
+
let resized = render.resize ? try ImageProcessor.resizeToWidth(image, targetWidth: render.widthDots) : image
|
|
166
|
+
|
|
167
|
+
emitJob(jobId, profile.id, "printing", progress: 0.1)
|
|
168
|
+
let bytes = try await adapter.printImage(profile, image: resized, options: render)
|
|
169
|
+
let status = try? await adapter.getStatus(profile)
|
|
170
|
+
let duration = Int(Date().timeIntervalSince(started) * 1000)
|
|
171
|
+
emitJob(jobId, profile.id, "completed", progress: 1.0)
|
|
172
|
+
return PrintOutcome(printerId: profile.id, adapter: profile.adapter, jobId: jobId, state: "completed", bytesSent: bytes, durationMs: duration, status: status)
|
|
173
|
+
} catch let e as PrinterError {
|
|
174
|
+
emitJob(jobId, profile.id, "failed", errorCode: e.code, message: e.message); throw e
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func printText(_ req: PrintTextRequest) async throws -> PrintOutcome {
|
|
179
|
+
let started = Date()
|
|
180
|
+
let jobId = UUID().uuidString
|
|
181
|
+
let profile = try resolveTargetProfile(req.printerId)
|
|
182
|
+
emitJob(jobId, profile.id, "pending")
|
|
183
|
+
guard let adapter = adapterFor(profile.adapter) else {
|
|
184
|
+
let e = PrinterError(.UNSUPPORTED_PRINTER, "Adapter introuvable")
|
|
185
|
+
emitJob(jobId, profile.id, "failed", errorCode: e.code, message: e.message); throw e
|
|
186
|
+
}
|
|
187
|
+
do {
|
|
188
|
+
if !adapter.isConnected(profile.id) {
|
|
189
|
+
guard req.autoReconnect else { throw PrinterError(.CONNECTION_FAILED, "Imprimante non connectée") }
|
|
190
|
+
try await ensureConnected(profile, timeoutMs: req.timeoutMs)
|
|
191
|
+
}
|
|
192
|
+
try await preflightHold(adapter, profile, jobId)
|
|
193
|
+
|
|
194
|
+
emitJob(jobId, profile.id, "printing", progress: 0.1)
|
|
195
|
+
let bytes: Int
|
|
196
|
+
if adapter.supportsTextItems() {
|
|
197
|
+
bytes = try await adapter.printItems(profile, items: req.items, defaultCodePage: req.defaultCodePage, cut: req.cut, feedLines: req.feedLines)
|
|
198
|
+
} else {
|
|
199
|
+
// Repli : rendre les items en image puis imprimer via le SDK image (Brother/Zebra).
|
|
200
|
+
let width = profile.capabilities.printableDots > 0 ? profile.capabilities.printableDots : 576
|
|
201
|
+
let image = TextRasterizer.render(req.items, widthDots: width)
|
|
202
|
+
var render = RenderOptions(widthDots: width)
|
|
203
|
+
render.resize = false
|
|
204
|
+
render.cut = req.cut
|
|
205
|
+
render.feedLines = req.feedLines
|
|
206
|
+
bytes = try await adapter.printImage(profile, image: image, options: render)
|
|
207
|
+
}
|
|
208
|
+
let status = try? await adapter.getStatus(profile)
|
|
209
|
+
let duration = Int(Date().timeIntervalSince(started) * 1000)
|
|
210
|
+
emitJob(jobId, profile.id, "completed", progress: 1.0)
|
|
211
|
+
return PrintOutcome(printerId: profile.id, adapter: profile.adapter, jobId: jobId, state: "completed", bytesSent: bytes, durationMs: duration, status: status)
|
|
212
|
+
} catch let e as PrinterError {
|
|
213
|
+
emitJob(jobId, profile.id, "failed", errorCode: e.code, message: e.message); throw e
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Lit le statut avant impression ; émet HOLD + lève si papier/capot bloquant.
|
|
218
|
+
private func preflightHold(_ adapter: PrinterAdapter, _ profile: PrinterProfile, _ jobId: String) async throws {
|
|
219
|
+
guard profile.capabilities.supportsStatus else { return }
|
|
220
|
+
guard let st = try? await adapter.getStatus(profile) else { return }
|
|
221
|
+
if st.paper == "empty" {
|
|
222
|
+
emitJob(jobId, profile.id, "hold", holdReason: "paper_empty")
|
|
223
|
+
throw PrinterError(.PAPER_EMPTY, "Plus de papier", retryable: true)
|
|
224
|
+
}
|
|
225
|
+
if st.coverOpen == true {
|
|
226
|
+
emitJob(jobId, profile.id, "hold", holdReason: "cover_open")
|
|
227
|
+
throw PrinterError(.COVER_OPEN, "Capot ouvert", retryable: true)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// MARK: Profils
|
|
232
|
+
|
|
233
|
+
func savedProfiles() -> [PrinterProfile] { store.all() }
|
|
234
|
+
func defaultProfile() -> PrinterProfile? { store.getDefault() }
|
|
235
|
+
func removeProfile(_ id: String) { store.remove(id) }
|
|
236
|
+
|
|
237
|
+
func setDefault(_ printerId: String) throws -> PrinterProfile {
|
|
238
|
+
if store.get(printerId) != nil {
|
|
239
|
+
guard let updated = store.setDefault(printerId) else {
|
|
240
|
+
throw PrinterError(.PRINTER_NOT_FOUND, "Profil introuvable")
|
|
241
|
+
}
|
|
242
|
+
return updated
|
|
243
|
+
}
|
|
244
|
+
guard let d = ephemeralDiscovered(printerId) else {
|
|
245
|
+
throw PrinterError(.PRINTER_NOT_FOUND, "Imprimante inconnue: \(printerId)")
|
|
246
|
+
}
|
|
247
|
+
var profile = toEphemeralProfile(d)
|
|
248
|
+
profile.isDefault = true
|
|
249
|
+
store.upsert(profile)
|
|
250
|
+
return store.setDefault(printerId) ?? profile
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func getStatus(_ printerId: String?) async throws -> PrinterStatus {
|
|
254
|
+
let profile = try resolveTargetProfile(printerId)
|
|
255
|
+
guard let adapter = adapterFor(profile.adapter) else {
|
|
256
|
+
throw PrinterError(.UNSUPPORTED_PRINTER, "Adapter introuvable")
|
|
257
|
+
}
|
|
258
|
+
return try await adapter.getStatus(profile)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// MARK: Monitoring de statut (Phase 6)
|
|
262
|
+
|
|
263
|
+
/// Démarre un polling périodique du statut et émet `statusChange` uniquement
|
|
264
|
+
/// quand l'état pertinent change (connexion/online/papier/capot). Idempotent.
|
|
265
|
+
func startStatusMonitor(_ printerId: String, intervalMs: Int) {
|
|
266
|
+
stopStatusMonitor(printerId)
|
|
267
|
+
let interval = min(max(intervalMs, 1000), 300_000)
|
|
268
|
+
let task = Task { [weak self] in
|
|
269
|
+
var lastKey: String?
|
|
270
|
+
var lastBlocked = false
|
|
271
|
+
while !Task.isCancelled {
|
|
272
|
+
guard let self = self else { return }
|
|
273
|
+
let status: PrinterStatus
|
|
274
|
+
do {
|
|
275
|
+
status = try await self.getStatus(printerId)
|
|
276
|
+
} catch let e as PrinterError {
|
|
277
|
+
status = PrinterStatus(id: printerId, connection: "error", online: false, paper: "unknown", errorCode: e.code, rawStatus: e.message)
|
|
278
|
+
} catch {
|
|
279
|
+
status = PrinterStatus(id: printerId, connection: "error", online: false, paper: "unknown", rawStatus: "\(error)")
|
|
280
|
+
}
|
|
281
|
+
// "Bloqué" = condition qui mettrait un job en hold (papier/capot/offline).
|
|
282
|
+
let blocked = status.paper == "empty" || status.coverOpen == true || !status.online
|
|
283
|
+
let key = "\(status.connection)|\(status.online)|\(status.paper)|\(String(describing: status.coverOpen))|\(String(describing: status.errorCode))"
|
|
284
|
+
if key != lastKey {
|
|
285
|
+
lastKey = key
|
|
286
|
+
self.onStatusChange?(status)
|
|
287
|
+
if lastBlocked && !blocked {
|
|
288
|
+
// Reprise après hold (papier rechargé / capot fermé / retour online).
|
|
289
|
+
Logger.shared.log("status", "recovered", ["id": printerId])
|
|
290
|
+
}
|
|
291
|
+
Logger.shared.log("status", "change", ["id": printerId, "paper": status.paper])
|
|
292
|
+
}
|
|
293
|
+
lastBlocked = blocked
|
|
294
|
+
try? await Task.sleep(nanoseconds: UInt64(interval) * 1_000_000)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
monitorLock.lock(); monitors[printerId] = task; monitorLock.unlock()
|
|
298
|
+
Logger.shared.log("status", "monitor-start", ["id": printerId, "intervalMs": interval])
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Arrête le moniteur d'une imprimante (no-op si absent).
|
|
302
|
+
func stopStatusMonitor(_ printerId: String) {
|
|
303
|
+
monitorLock.lock(); let t = monitors.removeValue(forKey: printerId); monitorLock.unlock()
|
|
304
|
+
t?.cancel()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Arrête tous les moniteurs.
|
|
308
|
+
func stopAllMonitors() {
|
|
309
|
+
monitorLock.lock(); let all = monitors; monitors.removeAll(); monitorLock.unlock()
|
|
310
|
+
all.values.forEach { $0.cancel() }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// État courant de chaque adapter/SDK (cf. getActiveSdks).
|
|
314
|
+
func activeSdks() -> [[String: Any]] {
|
|
315
|
+
let star = adapters.first { $0 is StarAdapter }?.isAvailable() ?? false
|
|
316
|
+
let epson = adapters.first { $0 is EpsonAdapter }?.isAvailable() ?? false
|
|
317
|
+
let brother = adapters.first { $0 is BrotherAdapter }?.isAvailable() ?? false
|
|
318
|
+
let zebra = adapters.first { $0 is ZebraAdapter }?.isAvailable() ?? false
|
|
319
|
+
return [
|
|
320
|
+
["adapter": "escpos", "label": "ESC/POS générique", "available": true, "requiresSdk": false, "transports": ["wifi", "ethernet"]],
|
|
321
|
+
["adapter": "star", "label": "Star StarXpand", "available": star, "requiresSdk": true, "transports": ["wifi", "bluetooth", "ble"]],
|
|
322
|
+
["adapter": "epson", "label": "Epson ePOS2", "available": epson, "requiresSdk": true, "transports": ["wifi", "bluetooth"]],
|
|
323
|
+
["adapter": "brother", "label": "Brother", "available": brother, "requiresSdk": true, "transports": ["wifi", "bluetooth", "ble"]],
|
|
324
|
+
["adapter": "zebra", "label": "Zebra Link-OS", "available": zebra, "requiresSdk": true, "transports": ["wifi", "bluetooth"]],
|
|
325
|
+
["adapter": "rawTcp", "label": "TCP brut", "available": true, "requiresSdk": false, "transports": ["wifi", "ethernet"]],
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
func debugLog() -> [[String: Any]] { Logger.shared.snapshot() }
|
|
330
|
+
|
|
331
|
+
// MARK: Helpers
|
|
332
|
+
|
|
333
|
+
private func adapterFor(_ id: AdapterId) -> PrinterAdapter? { adapters.first { $0.id == id } }
|
|
334
|
+
|
|
335
|
+
private func ephemeralDiscovered(_ id: String) -> DiscoveredPrinter? {
|
|
336
|
+
lock.lock(); defer { lock.unlock() }
|
|
337
|
+
return lastDiscovered.first { $0.id == id }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private func ephemeral(for id: String) -> PrinterProfile? {
|
|
341
|
+
ephemeralDiscovered(id).map(toEphemeralProfile)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private func resolveTargetProfile(_ printerId: String?) throws -> PrinterProfile {
|
|
345
|
+
guard let id = printerId else {
|
|
346
|
+
guard let def = store.getDefault() else {
|
|
347
|
+
throw PrinterError(.PRINTER_NOT_FOUND, "Aucune imprimante par défaut")
|
|
348
|
+
}
|
|
349
|
+
return def
|
|
350
|
+
}
|
|
351
|
+
return try resolveProfile(id, forceAdapter: nil)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private func resolveProfile(_ printerId: String, forceAdapter: AdapterId?) throws -> PrinterProfile {
|
|
355
|
+
if var p = store.get(printerId) {
|
|
356
|
+
if let f = forceAdapter { p.adapter = f }
|
|
357
|
+
return p
|
|
358
|
+
}
|
|
359
|
+
guard let d = ephemeralDiscovered(printerId) else {
|
|
360
|
+
throw PrinterError(.PRINTER_NOT_FOUND, "Imprimante inconnue: \(printerId)")
|
|
361
|
+
}
|
|
362
|
+
var base = toEphemeralProfile(d)
|
|
363
|
+
if let f = forceAdapter { base.adapter = f }
|
|
364
|
+
return base
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private func toEphemeralProfile(_ d: DiscoveredPrinter) -> PrinterProfile {
|
|
368
|
+
PrinterProfile(
|
|
369
|
+
id: d.id, adapter: d.adapter, transport: d.transport, address: d.address,
|
|
370
|
+
brand: d.brand, model: d.model, name: d.name,
|
|
371
|
+
capabilities: d.capabilities ?? Capabilities()
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private func loadImage(_ req: PrintRequest) async throws -> UIImage {
|
|
376
|
+
if let path = req.filePath, !path.isEmpty { return try ImageProcessor.decodeFile(path) }
|
|
377
|
+
if let url = req.url, !url.isEmpty {
|
|
378
|
+
let local = try await imageCache.fetch(url)
|
|
379
|
+
return try ImageProcessor.decodeFile(local)
|
|
380
|
+
}
|
|
381
|
+
if let b64 = req.base64, !b64.isEmpty { return try ImageProcessor.decodeBase64(b64) }
|
|
382
|
+
throw PrinterError(.IMAGE_INVALID, "Aucune source image fournie")
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private func resolveRenderOptions(_ profile: PrinterProfile, _ req: RenderOptions?) -> RenderOptions {
|
|
386
|
+
var width = req?.widthDots ?? 0
|
|
387
|
+
if width <= 0 { width = profile.capabilities.printableDots }
|
|
388
|
+
if width <= 0 {
|
|
389
|
+
width = profile.capabilities.paperWidthMm == 58 ? 384 : (profile.capabilities.paperWidthMm == 112 ? 832 : 576)
|
|
390
|
+
}
|
|
391
|
+
var opts = req ?? RenderOptions(widthDots: width)
|
|
392
|
+
opts.widthDots = width
|
|
393
|
+
return opts
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <Capacitor/Capacitor.h>
|
|
3
|
+
|
|
4
|
+
// Enregistrement du plugin auprès du runtime Capacitor (Objective-C bridge).
|
|
5
|
+
// Doit refléter les méthodes déclarées dans pluginMethods (ThermalPrinterPlugin.swift).
|
|
6
|
+
CAP_PLUGIN(ThermalPrinterPlugin, "ThermalPrinter",
|
|
7
|
+
CAP_PLUGIN_METHOD(discoverPrinters, CAPPluginReturnPromise);
|
|
8
|
+
CAP_PLUGIN_METHOD(connectPrinter, CAPPluginReturnPromise);
|
|
9
|
+
CAP_PLUGIN_METHOD(disconnectPrinter, CAPPluginReturnPromise);
|
|
10
|
+
CAP_PLUGIN_METHOD(setDefaultPrinter, CAPPluginReturnPromise);
|
|
11
|
+
CAP_PLUGIN_METHOD(getDefaultPrinter, CAPPluginReturnPromise);
|
|
12
|
+
CAP_PLUGIN_METHOD(getSavedPrinters, CAPPluginReturnPromise);
|
|
13
|
+
CAP_PLUGIN_METHOD(removePrinter, CAPPluginReturnPromise);
|
|
14
|
+
CAP_PLUGIN_METHOD(printImage, CAPPluginReturnPromise);
|
|
15
|
+
CAP_PLUGIN_METHOD(printText, CAPPluginReturnPromise);
|
|
16
|
+
CAP_PLUGIN_METHOD(getPrinterStatus, CAPPluginReturnPromise);
|
|
17
|
+
CAP_PLUGIN_METHOD(requestPermissions, CAPPluginReturnPromise);
|
|
18
|
+
CAP_PLUGIN_METHOD(checkPermissions, CAPPluginReturnPromise);
|
|
19
|
+
CAP_PLUGIN_METHOD(startStatusMonitor, CAPPluginReturnPromise);
|
|
20
|
+
CAP_PLUGIN_METHOD(stopStatusMonitor, CAPPluginReturnPromise);
|
|
21
|
+
CAP_PLUGIN_METHOD(getDebugLog, CAPPluginReturnPromise);
|
|
22
|
+
)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
/// Pont Capacitor iOS (bridge JS <-> Swift). Conforme Capacitor 7 (CAPBridgedPlugin).
|
|
5
|
+
///
|
|
6
|
+
/// Mappe l'API publique (definitions.ts) vers ThermalPrinterEngine. Les opérations
|
|
7
|
+
/// async tournent dans des Task ; les PrinterError sont converties en rejets
|
|
8
|
+
/// avec code normalisé.
|
|
9
|
+
@objc(ThermalPrinterPlugin)
|
|
10
|
+
public class ThermalPrinterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
+
|
|
12
|
+
public let identifier = "ThermalPrinterPlugin"
|
|
13
|
+
public let jsName = "ThermalPrinter"
|
|
14
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
15
|
+
CAPPluginMethod(name: "discoverPrinters", returnType: CAPPluginReturnPromise),
|
|
16
|
+
CAPPluginMethod(name: "connectPrinter", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "disconnectPrinter", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "setDefaultPrinter", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "getDefaultPrinter", returnType: CAPPluginReturnPromise),
|
|
20
|
+
CAPPluginMethod(name: "getSavedPrinters", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "removePrinter", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "printImage", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "printText", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "getPrinterStatus", returnType: CAPPluginReturnPromise),
|
|
25
|
+
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
|
|
27
|
+
CAPPluginMethod(name: "startStatusMonitor", returnType: CAPPluginReturnPromise),
|
|
28
|
+
CAPPluginMethod(name: "stopStatusMonitor", returnType: CAPPluginReturnPromise),
|
|
29
|
+
CAPPluginMethod(name: "getActiveSdks", returnType: CAPPluginReturnPromise),
|
|
30
|
+
CAPPluginMethod(name: "getDebugLog", returnType: CAPPluginReturnPromise),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
private let engine = ThermalPrinterEngine()
|
|
34
|
+
|
|
35
|
+
override public func load() {
|
|
36
|
+
// Relaye les états de job vers le JS (event printJobStatus).
|
|
37
|
+
engine.onJobUpdate = { [weak self] update in
|
|
38
|
+
self?.notifyListeners("printJobStatus", data: ["job": update.toDict()])
|
|
39
|
+
}
|
|
40
|
+
// Relaye les changements de statut vers le JS (event statusChange).
|
|
41
|
+
engine.onStatusChange = { [weak self] status in
|
|
42
|
+
self?.notifyListeners("statusChange", data: ["status": status.toDict()])
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// MARK: Permissions
|
|
47
|
+
//
|
|
48
|
+
// iOS n'a pas de permission runtime pour le Bluetooth Classic (inexistant) ni
|
|
49
|
+
// pour le réseau local au sens "demande explicite" : la pop-up Local Network
|
|
50
|
+
// apparaît à la première connexion. Le BLE déclenche une autorisation gérée par
|
|
51
|
+
// CoreBluetooth si on instancie un CBCentralManager (à activer avec BleAdapter).
|
|
52
|
+
|
|
53
|
+
@objc func checkPermissions(_ call: CAPPluginCall) {
|
|
54
|
+
call.resolve(permissionStatus())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@objc override public func requestPermissions(_ call: CAPPluginCall) {
|
|
58
|
+
// Rien à demander explicitement ici (voir note ci-dessus).
|
|
59
|
+
call.resolve(permissionStatus())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func permissionStatus() -> [String: Any] {
|
|
63
|
+
[
|
|
64
|
+
"bluetooth": "unavailable", // BT Classic générique impossible sur iOS
|
|
65
|
+
"bluetoothScan": "unavailable",
|
|
66
|
+
"bluetoothConnect": "unavailable",
|
|
67
|
+
"location": "granted",
|
|
68
|
+
"localNetwork": "prompt", // pop-up système à la 1re connexion locale
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// MARK: Découverte
|
|
73
|
+
|
|
74
|
+
@objc func discoverPrinters(_ call: CAPPluginCall) {
|
|
75
|
+
let sources: Set<String>? = (call.getArray("sources") as? [String]).map { Set($0) }
|
|
76
|
+
let options = DiscoveryManager.Options(
|
|
77
|
+
sources: sources,
|
|
78
|
+
timeoutMs: call.getInt("timeoutMs") ?? 8000,
|
|
79
|
+
networkCidr: call.getString("networkCidr"),
|
|
80
|
+
tcpPorts: (call.getArray("tcpPorts") as? [Int]) ?? [9100]
|
|
81
|
+
)
|
|
82
|
+
let emitPartial = call.getBool("emitPartialResults") ?? true
|
|
83
|
+
|
|
84
|
+
Task {
|
|
85
|
+
let result = await engine.discover(options) { [weak self] p in
|
|
86
|
+
if emitPartial { self?.notifyListeners("printerFound", data: ["printer": p.toDict()]) }
|
|
87
|
+
}
|
|
88
|
+
let printersDict = result.printers.map { $0.toDict() }
|
|
89
|
+
self.notifyListeners("discoveryComplete", data: ["printers": printersDict, "failedSources": result.failed])
|
|
90
|
+
call.resolve(["printers": printersDict])
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MARK: Connexion
|
|
95
|
+
|
|
96
|
+
@objc func connectPrinter(_ call: CAPPluginCall) {
|
|
97
|
+
guard let printerId = call.getString("printerId") else {
|
|
98
|
+
return reject(call, .PRINTER_NOT_FOUND, "printerId requis")
|
|
99
|
+
}
|
|
100
|
+
let timeout = call.getInt("timeoutMs") ?? 10000
|
|
101
|
+
let force = call.getString("forceAdapter").map { AdapterId.from($0) }
|
|
102
|
+
let setAsDefault = call.getBool("setAsDefault") ?? false
|
|
103
|
+
Task { await self.guarded(call) {
|
|
104
|
+
let connected = try await self.engine.connect(printerId, timeoutMs: timeout, forceAdapter: force, setAsDefault: setAsDefault)
|
|
105
|
+
call.resolve(["connected": connected])
|
|
106
|
+
} }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@objc func disconnectPrinter(_ call: CAPPluginCall) {
|
|
110
|
+
let id = call.getString("printerId") ?? ""
|
|
111
|
+
Task { await self.engine.disconnect(id); call.resolve() }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: Profils
|
|
115
|
+
|
|
116
|
+
@objc func setDefaultPrinter(_ call: CAPPluginCall) {
|
|
117
|
+
guard let id = call.getString("printerId") else { return reject(call, .PRINTER_NOT_FOUND, "printerId requis") }
|
|
118
|
+
do {
|
|
119
|
+
let profile = try engine.setDefault(id)
|
|
120
|
+
call.resolve(["profile": profile.toDict()])
|
|
121
|
+
} catch { rejectError(call, error) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@objc func getDefaultPrinter(_ call: CAPPluginCall) {
|
|
125
|
+
if let p = engine.defaultProfile() { call.resolve(["profile": p.toDict()]) }
|
|
126
|
+
else { call.resolve(["profile": NSNull()]) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@objc func getSavedPrinters(_ call: CAPPluginCall) {
|
|
130
|
+
call.resolve(["profiles": engine.savedProfiles().map { $0.toDict() }])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@objc func removePrinter(_ call: CAPPluginCall) {
|
|
134
|
+
engine.removeProfile(call.getString("printerId") ?? "")
|
|
135
|
+
call.resolve()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MARK: Impression / statut
|
|
139
|
+
|
|
140
|
+
@objc func printImage(_ call: CAPPluginCall) {
|
|
141
|
+
guard let image = call.getObject("image") else { return reject(call, .IMAGE_INVALID, "image requise") }
|
|
142
|
+
var render: RenderOptions?
|
|
143
|
+
if let r = call.getObject("render") {
|
|
144
|
+
render = RenderOptions(
|
|
145
|
+
widthDots: r["widthDots"] as? Int ?? 0,
|
|
146
|
+
resize: r["resize"] as? Bool ?? true,
|
|
147
|
+
grayscale: r["grayscale"] as? Bool ?? true,
|
|
148
|
+
threshold: r["threshold"] as? Int ?? 128,
|
|
149
|
+
dithering: r["dithering"] as? String ?? "floyd_steinberg",
|
|
150
|
+
align: r["align"] as? String ?? "center",
|
|
151
|
+
invert: r["invert"] as? Bool ?? false,
|
|
152
|
+
cut: r["cut"] as? Bool ?? true,
|
|
153
|
+
feedLines: r["feedLines"] as? Int ?? 3,
|
|
154
|
+
openCashDrawer: r["openCashDrawer"] as? Bool ?? false,
|
|
155
|
+
copies: r["copies"] as? Int ?? 1
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
let req = ThermalPrinterEngine.PrintRequest(
|
|
159
|
+
printerId: call.getString("printerId"),
|
|
160
|
+
filePath: image["filePath"] as? String,
|
|
161
|
+
url: image["url"] as? String,
|
|
162
|
+
base64: image["base64"] as? String,
|
|
163
|
+
render: render,
|
|
164
|
+
timeoutMs: call.getInt("timeoutMs") ?? 15000,
|
|
165
|
+
autoReconnect: call.getBool("autoReconnect") ?? true
|
|
166
|
+
)
|
|
167
|
+
Task { await self.guarded(call) {
|
|
168
|
+
let out = try await self.engine.printImage(req)
|
|
169
|
+
call.resolve(self.printResultDict(out))
|
|
170
|
+
} }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@objc func printText(_ call: CAPPluginCall) {
|
|
174
|
+
guard let rawItems = call.getArray("items") as? [[String: Any]] else {
|
|
175
|
+
return reject(call, .IMAGE_INVALID, "items requis")
|
|
176
|
+
}
|
|
177
|
+
let req = ThermalPrinterEngine.PrintTextRequest(
|
|
178
|
+
printerId: call.getString("printerId"),
|
|
179
|
+
items: PrintItem.parseList(rawItems),
|
|
180
|
+
defaultCodePage: call.getString("defaultCodePage") ?? "WPC1252",
|
|
181
|
+
cut: call.getBool("cut") ?? false,
|
|
182
|
+
feedLines: call.getInt("feedLines") ?? 3,
|
|
183
|
+
timeoutMs: call.getInt("timeoutMs") ?? 15000,
|
|
184
|
+
autoReconnect: call.getBool("autoReconnect") ?? true
|
|
185
|
+
)
|
|
186
|
+
Task { await self.guarded(call) {
|
|
187
|
+
let out = try await self.engine.printText(req)
|
|
188
|
+
call.resolve(self.printResultDict(out))
|
|
189
|
+
} }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func printResultDict(_ out: ThermalPrinterEngine.PrintOutcome) -> [String: Any] {
|
|
193
|
+
var result: [String: Any] = [
|
|
194
|
+
"success": out.state == "completed",
|
|
195
|
+
"printerId": out.printerId,
|
|
196
|
+
"adapter": out.adapter.rawValue,
|
|
197
|
+
"jobId": out.jobId,
|
|
198
|
+
"state": out.state,
|
|
199
|
+
"bytesSent": out.bytesSent,
|
|
200
|
+
"durationMs": out.durationMs,
|
|
201
|
+
]
|
|
202
|
+
if let status = out.status { result["status"] = status.toDict() }
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@objc func getPrinterStatus(_ call: CAPPluginCall) {
|
|
207
|
+
Task { await self.guarded(call) {
|
|
208
|
+
let status = try await self.engine.getStatus(call.getString("printerId"))
|
|
209
|
+
call.resolve(status.toDict())
|
|
210
|
+
} }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// MARK: Monitoring (Phase 6)
|
|
214
|
+
|
|
215
|
+
@objc func startStatusMonitor(_ call: CAPPluginCall) {
|
|
216
|
+
guard let printerId = call.getString("printerId") else {
|
|
217
|
+
return reject(call, .PRINTER_NOT_FOUND, "printerId requis")
|
|
218
|
+
}
|
|
219
|
+
engine.startStatusMonitor(printerId, intervalMs: call.getInt("intervalMs") ?? 5000)
|
|
220
|
+
call.resolve()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@objc func stopStatusMonitor(_ call: CAPPluginCall) {
|
|
224
|
+
guard let printerId = call.getString("printerId") else {
|
|
225
|
+
return reject(call, .PRINTER_NOT_FOUND, "printerId requis")
|
|
226
|
+
}
|
|
227
|
+
engine.stopStatusMonitor(printerId)
|
|
228
|
+
call.resolve()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@objc func getActiveSdks(_ call: CAPPluginCall) {
|
|
232
|
+
call.resolve(["sdks": engine.activeSdks()])
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@objc func getDebugLog(_ call: CAPPluginCall) {
|
|
236
|
+
call.resolve(["log": engine.debugLog()])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// MARK: Plomberie erreurs
|
|
240
|
+
|
|
241
|
+
private func guarded(_ call: CAPPluginCall, _ block: () async throws -> Void) async {
|
|
242
|
+
do { try await block() } catch { rejectError(call, error) }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private func reject(_ call: CAPPluginCall, _ code: ErrorCode, _ message: String) {
|
|
246
|
+
call.reject(message, code.rawValue, nil, ["code": code.rawValue, "retryable": false])
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private func rejectError(_ call: CAPPluginCall, _ error: Error) {
|
|
250
|
+
if let pe = error as? PrinterError {
|
|
251
|
+
Logger.shared.error("plugin", "\(pe.code.rawValue): \(pe.message)")
|
|
252
|
+
call.reject(pe.message, pe.code.rawValue, nil, ["code": pe.code.rawValue, "detail": pe.detail ?? "", "retryable": pe.retryable])
|
|
253
|
+
} else {
|
|
254
|
+
Logger.shared.error("plugin", "UNKNOWN: \(error.localizedDescription)")
|
|
255
|
+
call.reject(error.localizedDescription, ErrorCode.UNKNOWN.rawValue, error)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|