@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,305 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
#if canImport(StarIO10)
|
|
5
|
+
import StarIO10
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
/// Adapter Star iOS basé sur StarXpand SDK (StarIO10).
|
|
9
|
+
///
|
|
10
|
+
/// ⭐ Star est le mieux supporté sur iOS (MFi) et le seul fabricant auto-installable :
|
|
11
|
+
/// via Swift Package Manager (`https://github.com/star-micronics/StarXpand-SDK-iOS`).
|
|
12
|
+
/// On utilise la compilation conditionnelle `#if canImport(StarIO10)` :
|
|
13
|
+
/// - si l'app a ajouté le package StarXpand -> code TYPÉ compilé et actif,
|
|
14
|
+
/// - sinon -> stub inerte, `isAvailable()` renvoie false, l'adapter est ignoré.
|
|
15
|
+
///
|
|
16
|
+
/// INSTALLATION : voir docs/SDK_INTEGRATION.md (§ Star iOS).
|
|
17
|
+
final class StarAdapter: PrinterAdapter {
|
|
18
|
+
|
|
19
|
+
let id: AdapterId = .star
|
|
20
|
+
|
|
21
|
+
/// Connexions ouvertes indexées par printerId (StarPrinter est une classe -> AnyObject).
|
|
22
|
+
private var connections: [String: AnyObject] = [:]
|
|
23
|
+
private let lock = NSLock()
|
|
24
|
+
|
|
25
|
+
#if canImport(StarIO10)
|
|
26
|
+
/// Délégué de découverte retenu le temps du scan.
|
|
27
|
+
private var discoveryDelegate: StarDiscoveryDelegate?
|
|
28
|
+
private var discoveryManager: StarDeviceDiscoveryManager?
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
func isAvailable() -> Bool {
|
|
32
|
+
#if canImport(StarIO10)
|
|
33
|
+
return true
|
|
34
|
+
#else
|
|
35
|
+
return false
|
|
36
|
+
#endif
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func canHandle(_ profile: PrinterProfile) -> Bool { isAvailable() && profile.adapter == .star }
|
|
40
|
+
|
|
41
|
+
func supportsTextItems() -> Bool { isAvailable() }
|
|
42
|
+
|
|
43
|
+
// MARK: Découverte
|
|
44
|
+
|
|
45
|
+
func discover(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async {
|
|
46
|
+
#if canImport(StarIO10)
|
|
47
|
+
guard let manager = try? StarDeviceDiscoveryManagerFactory.create(
|
|
48
|
+
interfaceTypes: [.lan, .bluetooth, .bluetoothLE, .usb]
|
|
49
|
+
) else { return }
|
|
50
|
+
manager.discoveryTime = min(max(timeoutMs, 1000), 30000)
|
|
51
|
+
|
|
52
|
+
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
|
|
53
|
+
let delegate = StarDiscoveryDelegate(
|
|
54
|
+
onFound: { printer in
|
|
55
|
+
let settings = printer.connectionSettings
|
|
56
|
+
let transport = Self.transport(for: settings.interfaceType)
|
|
57
|
+
let model = printer.information?.model.map { String(describing: $0) }
|
|
58
|
+
onFound(DiscoveredPrinter(
|
|
59
|
+
id: "star:\(settings.interfaceType):\(settings.identifier)",
|
|
60
|
+
name: model ?? "Star Printer",
|
|
61
|
+
brand: "Star",
|
|
62
|
+
model: model,
|
|
63
|
+
transport: transport,
|
|
64
|
+
adapter: .star,
|
|
65
|
+
address: settings.identifier,
|
|
66
|
+
discoveredBy: ["star"]
|
|
67
|
+
))
|
|
68
|
+
},
|
|
69
|
+
onFinish: { [weak self] in
|
|
70
|
+
self?.lock.lock(); let resumed = self?.discoveryDelegate != nil; self?.discoveryDelegate = nil; self?.lock.unlock()
|
|
71
|
+
if resumed { cont.resume() }
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
self.lock.lock(); self.discoveryDelegate = delegate; self.discoveryManager = manager; self.lock.unlock()
|
|
75
|
+
manager.delegate = delegate
|
|
76
|
+
do { try manager.startDiscovery() } catch {
|
|
77
|
+
self.lock.lock(); self.discoveryDelegate = nil; self.lock.unlock()
|
|
78
|
+
cont.resume()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
manager.stopDiscovery()
|
|
82
|
+
#endif
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: Connexion
|
|
86
|
+
|
|
87
|
+
func connect(_ profile: PrinterProfile, timeoutMs: Int) async throws {
|
|
88
|
+
#if canImport(StarIO10)
|
|
89
|
+
if isConnected(profile.id) { return }
|
|
90
|
+
let printer = StarPrinter(Self.connectionSettings(for: profile))
|
|
91
|
+
do {
|
|
92
|
+
try await printer.open()
|
|
93
|
+
} catch {
|
|
94
|
+
throw PrinterError(.CONNECTION_FAILED, "Connexion Star échouée: \(profile.address)", detail: "\(error)", retryable: true)
|
|
95
|
+
}
|
|
96
|
+
lock.lock(); connections[profile.id] = printer; lock.unlock()
|
|
97
|
+
#else
|
|
98
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Star (StarIO10) absent")
|
|
99
|
+
#endif
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func isConnected(_ printerId: String) -> Bool {
|
|
103
|
+
lock.lock(); defer { lock.unlock() }
|
|
104
|
+
return connections[printerId] != nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func disconnect(_ printerId: String) async {
|
|
108
|
+
#if canImport(StarIO10)
|
|
109
|
+
lock.lock(); let p = connections.removeValue(forKey: printerId) as? StarPrinter; lock.unlock()
|
|
110
|
+
if let p = p { try? await p.close() }
|
|
111
|
+
#endif
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: Impression
|
|
115
|
+
|
|
116
|
+
func printImage(_ profile: PrinterProfile, image: UIImage, options: RenderOptions) async throws -> Int {
|
|
117
|
+
#if canImport(StarIO10)
|
|
118
|
+
let printer = try requireConnected(profile)
|
|
119
|
+
let printerBuilder = StarXpandCommand.PrinterBuilder()
|
|
120
|
+
.styleAlignment(Self.alignment(for: options.align))
|
|
121
|
+
.actionPrintImage(StarXpandCommand.Printer.ImageParameter(image: image, width: max(8, profile.capabilities.printableDots)))
|
|
122
|
+
if options.feedLines > 0 { _ = printerBuilder.actionFeedLine(options.feedLines) }
|
|
123
|
+
if options.cut && profile.capabilities.supportsCut { _ = printerBuilder.actionCut(.partial) }
|
|
124
|
+
|
|
125
|
+
let document = StarXpandCommand.DocumentBuilder().addPrinter(printerBuilder)
|
|
126
|
+
if options.openCashDrawer && profile.capabilities.supportsCashDrawer {
|
|
127
|
+
_ = document.addDrawer(StarXpandCommand.DrawerBuilder().actionOpen(StarXpandCommand.Drawer.OpenParameter()))
|
|
128
|
+
}
|
|
129
|
+
let commands = StarXpandCommand.StarXpandCommandBuilder().addDocument(document).getCommands()
|
|
130
|
+
|
|
131
|
+
var sent = 0
|
|
132
|
+
for _ in 0..<max(1, options.copies) {
|
|
133
|
+
try await send(printer, commands)
|
|
134
|
+
sent += commands.count
|
|
135
|
+
}
|
|
136
|
+
return sent
|
|
137
|
+
#else
|
|
138
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Star (StarIO10) absent")
|
|
139
|
+
#endif
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func printItems(_ profile: PrinterProfile, items: [PrintItem], defaultCodePage: String, cut: Bool, feedLines: Int) async throws -> Int {
|
|
143
|
+
#if canImport(StarIO10)
|
|
144
|
+
let printer = try requireConnected(profile)
|
|
145
|
+
let pb = StarXpandCommand.PrinterBuilder()
|
|
146
|
+
for item in items { Self.map(item, into: pb, profile: profile) }
|
|
147
|
+
if feedLines > 0 { _ = pb.actionFeedLine(feedLines) }
|
|
148
|
+
if cut && profile.capabilities.supportsCut { _ = pb.actionCut(.partial) }
|
|
149
|
+
let commands = StarXpandCommand.StarXpandCommandBuilder()
|
|
150
|
+
.addDocument(StarXpandCommand.DocumentBuilder().addPrinter(pb))
|
|
151
|
+
.getCommands()
|
|
152
|
+
try await send(printer, commands)
|
|
153
|
+
return commands.count
|
|
154
|
+
#else
|
|
155
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Star (StarIO10) absent")
|
|
156
|
+
#endif
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MARK: Statut
|
|
160
|
+
|
|
161
|
+
func getStatus(_ profile: PrinterProfile) async throws -> PrinterStatus {
|
|
162
|
+
#if canImport(StarIO10)
|
|
163
|
+
lock.lock(); let p = connections[profile.id] as? StarPrinter; lock.unlock()
|
|
164
|
+
guard let printer = p else {
|
|
165
|
+
return PrinterStatus(id: profile.id, connection: "disconnected", online: false, paper: "unknown")
|
|
166
|
+
}
|
|
167
|
+
do {
|
|
168
|
+
let st = try await printer.getStatus()
|
|
169
|
+
let paperEmpty = st.paperEmpty
|
|
170
|
+
let coverOpen = st.coverOpen
|
|
171
|
+
return PrinterStatus(
|
|
172
|
+
id: profile.id, connection: "connected", online: !paperEmpty && !coverOpen,
|
|
173
|
+
paper: paperEmpty ? "empty" : (st.paperNearEmpty ? "near_end" : "ok"),
|
|
174
|
+
coverOpen: coverOpen,
|
|
175
|
+
errorCode: paperEmpty ? .PAPER_EMPTY : (coverOpen ? .COVER_OPEN : nil),
|
|
176
|
+
rawStatus: "\(st)"
|
|
177
|
+
)
|
|
178
|
+
} catch {
|
|
179
|
+
return PrinterStatus(id: profile.id, connection: "error", online: false, paper: "unknown", rawStatus: "\(error)")
|
|
180
|
+
}
|
|
181
|
+
#else
|
|
182
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Star (StarIO10) absent")
|
|
183
|
+
#endif
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// MARK: Helpers (typés, sous canImport)
|
|
187
|
+
|
|
188
|
+
#if canImport(StarIO10)
|
|
189
|
+
private func requireConnected(_ profile: PrinterProfile) throws -> StarPrinter {
|
|
190
|
+
lock.lock(); let p = connections[profile.id] as? StarPrinter; lock.unlock()
|
|
191
|
+
guard let printer = p else { throw PrinterError(.CONNECTION_FAILED, "Star non connecté: \(profile.id)") }
|
|
192
|
+
return printer
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func send(_ printer: StarPrinter, _ commands: String) async throws {
|
|
196
|
+
do { try await printer.print(command: commands) }
|
|
197
|
+
catch { throw PrinterError(.PRINT_FAILED, "Impression Star échouée", detail: "\(error)", retryable: true) }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static func connectionSettings(for profile: PrinterProfile) -> StarConnectionSettings {
|
|
201
|
+
let iface: InterfaceType
|
|
202
|
+
switch profile.transport {
|
|
203
|
+
case .wifi, .ethernet: iface = .lan
|
|
204
|
+
case .bluetooth: iface = .bluetooth
|
|
205
|
+
case .ble: iface = .bluetoothLE
|
|
206
|
+
case .usb: iface = .usb
|
|
207
|
+
}
|
|
208
|
+
let identifier = iface == .lan ? String(profile.address.split(separator: ":").first ?? "") : profile.address
|
|
209
|
+
return StarConnectionSettings(interfaceType: iface, identifier: identifier)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private static func transport(for iface: InterfaceType) -> Transport {
|
|
213
|
+
switch iface {
|
|
214
|
+
case .lan: return .wifi
|
|
215
|
+
case .bluetooth: return .bluetooth
|
|
216
|
+
case .bluetoothLE: return .ble
|
|
217
|
+
case .usb: return .usb
|
|
218
|
+
@unknown default: return .wifi
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static func alignment(for align: String) -> StarXpandCommand.Printer.Alignment {
|
|
223
|
+
switch align {
|
|
224
|
+
case "center": return .center
|
|
225
|
+
case "right": return .right
|
|
226
|
+
default: return .left
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Mappe un PrintItem vers le PrinterBuilder StarXpand (best effort par type).
|
|
231
|
+
private static func map(_ item: PrintItem, into pb: StarXpandCommand.PrinterBuilder, profile: PrinterProfile) {
|
|
232
|
+
switch item {
|
|
233
|
+
case let .text(value, style):
|
|
234
|
+
if let a = style.align { _ = pb.styleAlignment(alignment(for: a)) }
|
|
235
|
+
_ = pb.styleBold(style.bold)
|
|
236
|
+
_ = pb.styleInvert(style.invert)
|
|
237
|
+
_ = pb.styleUnderLine(style.underline != "none")
|
|
238
|
+
_ = pb.styleMagnification(StarXpandCommand.MagnificationParameter(
|
|
239
|
+
width: min(max(style.widthMultiplier, 1), 6), height: min(max(style.heightMultiplier, 1), 6)))
|
|
240
|
+
_ = pb.actionPrintText(style.newline ? value + "\n" : value)
|
|
241
|
+
_ = pb.styleBold(false).styleInvert(false).styleUnderLine(false)
|
|
242
|
+
.styleMagnification(StarXpandCommand.MagnificationParameter(width: 1, height: 1))
|
|
243
|
+
case let .feed(lines):
|
|
244
|
+
_ = pb.actionFeedLine(max(1, lines))
|
|
245
|
+
case let .cut(mode, _):
|
|
246
|
+
_ = pb.actionCut(mode == "full" ? .full : .partial)
|
|
247
|
+
case let .divider(char, columns, align, bold):
|
|
248
|
+
let cols = columns ?? (profile.capabilities.printableDots <= 420 ? 32 : 48)
|
|
249
|
+
if let a = align { _ = pb.styleAlignment(alignment(for: a)) }
|
|
250
|
+
_ = pb.styleBold(bold)
|
|
251
|
+
_ = pb.actionPrintText(String(repeating: char, count: min(max(cols, 1), 96)) + "\n")
|
|
252
|
+
_ = pb.styleBold(false)
|
|
253
|
+
case let .qrcode(value, size, ec, align):
|
|
254
|
+
_ = pb.styleAlignment(alignment(for: align))
|
|
255
|
+
_ = pb.actionPrintQRCode(StarXpandCommand.Printer.QRCodeParameter(content: value)
|
|
256
|
+
.setLevel(qrLevel(for: ec))
|
|
257
|
+
.setCellSize(min(max(size, 1), 16)))
|
|
258
|
+
case let .barcode(value, symbology, height, _, hri, align):
|
|
259
|
+
_ = pb.styleAlignment(alignment(for: align))
|
|
260
|
+
_ = pb.actionPrintBarcode(StarXpandCommand.Printer.BarcodeParameter(content: value, symbology: barcodeSymbology(for: symbology))
|
|
261
|
+
.setHeight(Double(min(max(height, 1), 255)))
|
|
262
|
+
.setPrintHri(hri != "none"))
|
|
263
|
+
case .cashDrawer, .image, .raw:
|
|
264
|
+
// tiroir -> openCashDrawer ; image -> printImage ; raw -> non supporté par StarXpand.
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static func qrLevel(for ec: String) -> StarXpandCommand.Printer.QRCodeLevel {
|
|
270
|
+
switch ec.uppercased() {
|
|
271
|
+
case "L": return .l
|
|
272
|
+
case "Q": return .q
|
|
273
|
+
case "H": return .h
|
|
274
|
+
default: return .m
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static func barcodeSymbology(for s: String) -> StarXpandCommand.Printer.BarcodeSymbology {
|
|
279
|
+
switch s.uppercased() {
|
|
280
|
+
case "CODE39": return .code39
|
|
281
|
+
case "CODE93": return .code93
|
|
282
|
+
case "EAN13", "JAN13": return .jan13
|
|
283
|
+
case "EAN8", "JAN8": return .jan8
|
|
284
|
+
case "ITF": return .itf
|
|
285
|
+
case "UPCA": return .upcA
|
|
286
|
+
case "UPCE": return .upcE
|
|
287
|
+
case "NW7", "CODABAR": return .nw7
|
|
288
|
+
default: return .code128
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
#endif
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#if canImport(StarIO10)
|
|
295
|
+
/// Délégué de découverte StarXpand (retenu par l'adapter le temps du scan).
|
|
296
|
+
private final class StarDiscoveryDelegate: NSObject, StarDeviceDiscoveryManagerDelegate {
|
|
297
|
+
private let onFound: (StarPrinter) -> Void
|
|
298
|
+
private let onFinish: () -> Void
|
|
299
|
+
init(onFound: @escaping (StarPrinter) -> Void, onFinish: @escaping () -> Void) {
|
|
300
|
+
self.onFound = onFound; self.onFinish = onFinish
|
|
301
|
+
}
|
|
302
|
+
func manager(_ manager: StarDeviceDiscoveryManager, didFind printer: StarPrinter) { onFound(printer) }
|
|
303
|
+
func managerDidFinishDiscovery(_ manager: StarDeviceDiscoveryManager) { onFinish() }
|
|
304
|
+
}
|
|
305
|
+
#endif
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
// Le SDK Link-OS iOS est livré en xcframework manuel (licence Zebra, non
|
|
5
|
+
// redistribuable, pas de pod/SPM officiel). Nom de module possible `ZSDK_API`.
|
|
6
|
+
// Import absent -> stub inerte. Voir docs/SDK_INTEGRATION.md (§ Zebra iOS).
|
|
7
|
+
#if canImport(ZSDK_API)
|
|
8
|
+
import ZSDK_API
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
/// Adapter Zebra iOS (Link-OS SDK / ZSDK).
|
|
12
|
+
///
|
|
13
|
+
/// ⚠️ Comme sur Android : Zebra = ZPL/CPCL, JAMAIS ESC/POS. Le SDK convertit un
|
|
14
|
+
/// CGImage en ZPL et l'imprime via GraphicsUtil. Transports : TCP + Bluetooth MFi.
|
|
15
|
+
/// API ObjC à VÉRIFIER sur device avec le xcframework réel.
|
|
16
|
+
final class ZebraAdapter: PrinterAdapter {
|
|
17
|
+
|
|
18
|
+
let id: AdapterId = .zebra
|
|
19
|
+
private var connections: [String: AnyObject] = [:]
|
|
20
|
+
private let lock = NSLock()
|
|
21
|
+
|
|
22
|
+
func isAvailable() -> Bool {
|
|
23
|
+
#if canImport(ZSDK_API)
|
|
24
|
+
return true
|
|
25
|
+
#else
|
|
26
|
+
return NSClassFromString("TcpPrinterConnection") != nil || NSClassFromString("ZebraPrinterFactory") != nil
|
|
27
|
+
#endif
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func canHandle(_ profile: PrinterProfile) -> Bool { isAvailable() && profile.adapter == .zebra }
|
|
31
|
+
|
|
32
|
+
func discover(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async {
|
|
33
|
+
// NetworkDiscoverer.localBroadcast(...) disponible mais bloquant/ObjC ;
|
|
34
|
+
// les Zebra réseau restent trouvées par le scan TCP générique.
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func connect(_ profile: PrinterProfile, timeoutMs: Int) async throws {
|
|
38
|
+
#if canImport(ZSDK_API)
|
|
39
|
+
if isConnected(profile.id) { return }
|
|
40
|
+
let (host, port) = Self.hostPort(profile.address)
|
|
41
|
+
guard let conn = TcpPrinterConnection(address: host, andWithPort: port) else {
|
|
42
|
+
throw PrinterError(.CONNECTION_FAILED, "Init connexion Zebra échouée")
|
|
43
|
+
}
|
|
44
|
+
conn.setMaxTimeoutForRead(Int32(timeoutMs))
|
|
45
|
+
if !conn.open() {
|
|
46
|
+
throw PrinterError(.CONNECTION_FAILED, "Connexion Zebra échouée: \(profile.address)", retryable: true)
|
|
47
|
+
}
|
|
48
|
+
lock.lock(); connections[profile.id] = conn; lock.unlock()
|
|
49
|
+
#else
|
|
50
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Zebra Link-OS absent")
|
|
51
|
+
#endif
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func isConnected(_ printerId: String) -> Bool {
|
|
55
|
+
lock.lock(); defer { lock.unlock() }
|
|
56
|
+
return connections[printerId] != nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func disconnect(_ printerId: String) async {
|
|
60
|
+
#if canImport(ZSDK_API)
|
|
61
|
+
lock.lock(); let c = connections.removeValue(forKey: printerId) as? ZebraPrinterConnection; lock.unlock()
|
|
62
|
+
c?.close()
|
|
63
|
+
#endif
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func printImage(_ profile: PrinterProfile, image: UIImage, options: RenderOptions) async throws -> Int {
|
|
67
|
+
#if canImport(ZSDK_API)
|
|
68
|
+
lock.lock(); let c = connections[profile.id] as? ZebraPrinterConnection; lock.unlock()
|
|
69
|
+
guard let conn = c else { throw PrinterError(.CONNECTION_FAILED, "Zebra non connecté: \(profile.id)") }
|
|
70
|
+
guard let cg = image.cgImage else { throw PrinterError(.IMAGE_INVALID, "CGImage indisponible") }
|
|
71
|
+
do {
|
|
72
|
+
let printer = try ZebraPrinterFactory.getInstance(conn)
|
|
73
|
+
let tools = printer.getGraphicsUtil()
|
|
74
|
+
for _ in 0..<max(1, options.copies) {
|
|
75
|
+
try tools?.print(cg, atX: 0, atY: 0, withWidth: cg.width, withHeight: cg.height, andIsInsideFormat: false)
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
throw PrinterError(.PRINT_FAILED, "Impression Zebra échouée", detail: "\(error)", retryable: true)
|
|
79
|
+
}
|
|
80
|
+
return cg.width * cg.height / 8
|
|
81
|
+
#else
|
|
82
|
+
throw PrinterError(.SDK_NOT_AVAILABLE, "SDK Zebra Link-OS absent")
|
|
83
|
+
#endif
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func getStatus(_ profile: PrinterProfile) async throws -> PrinterStatus {
|
|
87
|
+
#if canImport(ZSDK_API)
|
|
88
|
+
lock.lock(); let c = connections[profile.id] as? ZebraPrinterConnection; lock.unlock()
|
|
89
|
+
guard let conn = c else {
|
|
90
|
+
return PrinterStatus(id: profile.id, connection: "disconnected", online: false, paper: "unknown")
|
|
91
|
+
}
|
|
92
|
+
do {
|
|
93
|
+
let printer = try ZebraPrinterFactory.getInstance(conn)
|
|
94
|
+
let st = try printer.getCurrentStatus()
|
|
95
|
+
let paperOut = st.isPaperOut
|
|
96
|
+
let headOpen = st.isHeadOpen
|
|
97
|
+
return PrinterStatus(
|
|
98
|
+
id: profile.id, connection: "connected", online: st.isReadyToPrint,
|
|
99
|
+
paper: paperOut ? "empty" : "ok", coverOpen: headOpen,
|
|
100
|
+
errorCode: paperOut ? .PAPER_EMPTY : (headOpen ? .COVER_OPEN : nil)
|
|
101
|
+
)
|
|
102
|
+
} catch {
|
|
103
|
+
return PrinterStatus(id: profile.id, connection: "error", online: false, paper: "unknown", rawStatus: "\(error)")
|
|
104
|
+
}
|
|
105
|
+
#else
|
|
106
|
+
return PrinterStatus(id: profile.id, connection: isConnected(profile.id) ? "connected" : "disconnected",
|
|
107
|
+
online: isConnected(profile.id), paper: "unknown")
|
|
108
|
+
#endif
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// MARK: Helpers
|
|
112
|
+
|
|
113
|
+
private static func hostPort(_ address: String) -> (String, Int) {
|
|
114
|
+
let parts = address.split(separator: ":")
|
|
115
|
+
let host = parts.first.map(String.init) ?? address
|
|
116
|
+
let port = parts.count > 1 ? (Int(parts[1]) ?? 9100) : 9100
|
|
117
|
+
return (host, port)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Moteur de priorité d'adapter (miroir iOS de priority.ts / AdapterPriority.kt).
|
|
4
|
+
enum AdapterPriority {
|
|
5
|
+
static func score(_ p: DiscoveredPrinter) -> Int {
|
|
6
|
+
let brand = (p.brand ?? "").lowercased()
|
|
7
|
+
let isZebra = brand.contains("zebra") || p.adapter == .zebra
|
|
8
|
+
if isZebra { return p.adapter == .zebra ? 1000 : -1000 }
|
|
9
|
+
|
|
10
|
+
if p.adapter == .epson { return 900 }
|
|
11
|
+
if p.adapter == .star { return 890 }
|
|
12
|
+
if p.adapter == .brother { return 880 }
|
|
13
|
+
|
|
14
|
+
if p.adapter == .escpos {
|
|
15
|
+
return p.transport == .bluetooth ? 620 : 600
|
|
16
|
+
}
|
|
17
|
+
if p.transport == .ble { return 500 }
|
|
18
|
+
if p.adapter == .rawTcp { return 300 }
|
|
19
|
+
return 100
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
|
|
4
|
+
/// Découverte réseau iOS.
|
|
5
|
+
///
|
|
6
|
+
/// Recommandé : Bonjour/mDNS via NWBrowser pour `_pdl-datastream._tcp` (port 9100)
|
|
7
|
+
/// et `_printer._tcp` / `_ipp._tcp`. C'est la méthode propre sur iOS (pas de scan
|
|
8
|
+
/// d'IP brute, qui est mal vu et lent). Les imprimantes réseau modernes publient
|
|
9
|
+
/// ces services.
|
|
10
|
+
///
|
|
11
|
+
/// ⚠️ Info.plist : nécessite NSLocalNetworkUsageDescription + NSBonjourServices
|
|
12
|
+
/// listant les services recherchés.
|
|
13
|
+
final class BonjourScanner {
|
|
14
|
+
|
|
15
|
+
private var browsers: [NWBrowser] = []
|
|
16
|
+
|
|
17
|
+
func scan(timeoutMs: Int, onFound: @escaping (DiscoveredPrinter) -> Void) async {
|
|
18
|
+
let services = ["_pdl-datastream._tcp", "_printer._tcp", "_ipp._tcp"]
|
|
19
|
+
let queue = DispatchQueue(label: "thermalprinter.bonjour")
|
|
20
|
+
|
|
21
|
+
for service in services {
|
|
22
|
+
let params = NWParameters()
|
|
23
|
+
params.includePeerToPeer = false
|
|
24
|
+
let browser = NWBrowser(for: .bonjour(type: service, domain: nil), using: params)
|
|
25
|
+
browser.browseResultsChangedHandler = { results, _ in
|
|
26
|
+
for result in results {
|
|
27
|
+
if case let .service(name, _, _, _) = result.endpoint {
|
|
28
|
+
// La résolution IP se fait à la connexion via NWConnection(endpoint:).
|
|
29
|
+
let printer = DiscoveredPrinter(
|
|
30
|
+
id: "wifi:\(name)",
|
|
31
|
+
name: name,
|
|
32
|
+
brand: nil, model: nil,
|
|
33
|
+
transport: .wifi,
|
|
34
|
+
adapter: .escpos, // arbitré ensuite par la priorité
|
|
35
|
+
address: "\(name)._\(service)", // résolu via NWEndpoint à la connexion
|
|
36
|
+
capabilities: Capabilities(),
|
|
37
|
+
discoveredBy: ["escpos", "rawTcp"]
|
|
38
|
+
)
|
|
39
|
+
onFound(printer)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
browser.start(queue: queue)
|
|
44
|
+
browsers.append(browser)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try? await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
|
|
48
|
+
browsers.forEach { $0.cancel() }
|
|
49
|
+
browsers.removeAll()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Orchestre la découverte agrégée iOS (miroir de DiscoveryManager.kt).
|
|
4
|
+
///
|
|
5
|
+
/// Sources iOS :
|
|
6
|
+
/// - SDK Epson / Star / Brother / Zebra (via adapters, si liés)
|
|
7
|
+
/// - Bonjour/mDNS réseau (BonjourScanner)
|
|
8
|
+
/// - BLE (optionnel, allowlist)
|
|
9
|
+
///
|
|
10
|
+
/// PAS de Bluetooth Classic générique sur iOS (voir README "Limites iOS").
|
|
11
|
+
final class DiscoveryManager {
|
|
12
|
+
|
|
13
|
+
struct Options {
|
|
14
|
+
let sources: Set<String>?
|
|
15
|
+
let timeoutMs: Int
|
|
16
|
+
let networkCidr: String?
|
|
17
|
+
let tcpPorts: [Int]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private let adapters: [PrinterAdapter]
|
|
21
|
+
init(adapters: [PrinterAdapter]) { self.adapters = adapters }
|
|
22
|
+
|
|
23
|
+
func discover(_ options: Options, emitPartial: @escaping (DiscoveredPrinter) -> Void) async -> (printers: [DiscoveredPrinter], failed: [String]) {
|
|
24
|
+
let buffer = SyncBuffer()
|
|
25
|
+
var failed: [String] = []
|
|
26
|
+
|
|
27
|
+
func enabled(_ src: String) -> Bool { options.sources == nil || options.sources!.contains(src) }
|
|
28
|
+
|
|
29
|
+
let collect: (DiscoveredPrinter) -> Void = { p in
|
|
30
|
+
buffer.add(p)
|
|
31
|
+
emitPartial(p)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await withTaskGroup(of: Void.self) { group in
|
|
35
|
+
// SDK fabricants
|
|
36
|
+
for adapter in adapters {
|
|
37
|
+
let src: String?
|
|
38
|
+
switch adapter.id {
|
|
39
|
+
case .epson: src = "epson"
|
|
40
|
+
case .star: src = "star"
|
|
41
|
+
case .brother: src = "brother"
|
|
42
|
+
case .zebra: src = "zebra"
|
|
43
|
+
default: src = nil
|
|
44
|
+
}
|
|
45
|
+
if let src = src, enabled(src), adapter.isAvailable() {
|
|
46
|
+
group.addTask { await adapter.discover(timeoutMs: options.timeoutMs, onFound: collect) }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Bonjour réseau
|
|
50
|
+
if enabled("tcp") {
|
|
51
|
+
group.addTask { await BonjourScanner().scan(timeoutMs: options.timeoutMs, onFound: collect) }
|
|
52
|
+
}
|
|
53
|
+
// BLE (optionnel)
|
|
54
|
+
// if enabled("ble") { group.addTask { await BleScanner().scan(...) } }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (merge(buffer.snapshot()), failed)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private func merge(_ incoming: [DiscoveredPrinter]) -> [DiscoveredPrinter] {
|
|
61
|
+
var byId: [String: DiscoveredPrinter] = [:]
|
|
62
|
+
for p in incoming {
|
|
63
|
+
if let existing = byId[p.id] {
|
|
64
|
+
var winner = AdapterPriority.score(p) > AdapterPriority.score(existing) ? p : existing
|
|
65
|
+
winner.discoveredBy = existing.discoveredBy.union(p.discoveredBy)
|
|
66
|
+
winner.lastSeenAt = max(existing.lastSeenAt, p.lastSeenAt)
|
|
67
|
+
winner.isConnected = existing.isConnected || p.isConnected
|
|
68
|
+
byId[p.id] = winner
|
|
69
|
+
} else {
|
|
70
|
+
byId[p.id] = p
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return byId.values.sorted {
|
|
74
|
+
let sa = AdapterPriority.score($0), sb = AdapterPriority.score($1)
|
|
75
|
+
return sa != sb ? sa > sb : $0.name < $1.name
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Petit buffer thread-safe pour collecter les résultats concurrents.
|
|
81
|
+
final class SyncBuffer {
|
|
82
|
+
private var items: [DiscoveredPrinter] = []
|
|
83
|
+
private let lock = NSLock()
|
|
84
|
+
func add(_ p: DiscoveredPrinter) { lock.lock(); items.append(p); lock.unlock() }
|
|
85
|
+
func snapshot() -> [DiscoveredPrinter] { lock.lock(); defer { lock.unlock() }; return items }
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
|
|
4
|
+
/// Cache local des images à imprimer (miroir de ImageCache.kt).
|
|
5
|
+
/// Emplacement : caches/thermal-images/, clé = SHA-256(url), quota 32 Mo.
|
|
6
|
+
final class ImageCache {
|
|
7
|
+
|
|
8
|
+
private let dir: URL
|
|
9
|
+
private let maxBytes: Int64 = 32 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
init() {
|
|
12
|
+
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
13
|
+
dir = caches.appendingPathComponent("thermal-images", isDirectory: true)
|
|
14
|
+
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Télécharge l'URL (si absente) et renvoie le chemin local.
|
|
18
|
+
func fetch(_ urlString: String, timeoutMs: Int = 10000) async throws -> String {
|
|
19
|
+
guard let url = URL(string: urlString) else {
|
|
20
|
+
throw PrinterError(.IMAGE_INVALID, "URL invalide: \(urlString)")
|
|
21
|
+
}
|
|
22
|
+
let dest = dir.appendingPathComponent(sha256(urlString) + ".img")
|
|
23
|
+
if FileManager.default.fileExists(atPath: dest.path) {
|
|
24
|
+
Logger.shared.log("image", "cache hit", ["url": urlString])
|
|
25
|
+
return dest.path
|
|
26
|
+
}
|
|
27
|
+
var request = URLRequest(url: url)
|
|
28
|
+
request.timeoutInterval = Double(timeoutMs) / 1000.0
|
|
29
|
+
do {
|
|
30
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
31
|
+
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
|
|
32
|
+
throw PrinterError(.IMAGE_INVALID, "HTTP \(http.statusCode) pour \(urlString)")
|
|
33
|
+
}
|
|
34
|
+
try data.write(to: dest)
|
|
35
|
+
Logger.shared.log("image", "downloaded", ["url": urlString, "bytes": data.count])
|
|
36
|
+
enforceQuota()
|
|
37
|
+
return dest.path
|
|
38
|
+
} catch let e as PrinterError {
|
|
39
|
+
throw e
|
|
40
|
+
} catch {
|
|
41
|
+
throw PrinterError(.IMAGE_INVALID, "Téléchargement image échoué", detail: error.localizedDescription, retryable: true)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func enforceQuota() {
|
|
46
|
+
let keys: [URLResourceKey] = [.contentModificationDateKey, .fileSizeKey]
|
|
47
|
+
guard let files = try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: keys) else { return }
|
|
48
|
+
let sorted = files.sorted {
|
|
49
|
+
let d0 = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
|
50
|
+
let d1 = (try? $1.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
|
51
|
+
return d0 < d1
|
|
52
|
+
}
|
|
53
|
+
var total = sorted.reduce(Int64(0)) { acc, u in
|
|
54
|
+
acc + Int64((try? u.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0)
|
|
55
|
+
}
|
|
56
|
+
var i = 0
|
|
57
|
+
while total > maxBytes && i < sorted.count {
|
|
58
|
+
let size = Int64((try? sorted[i].resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0)
|
|
59
|
+
try? FileManager.default.removeItem(at: sorted[i])
|
|
60
|
+
total -= size
|
|
61
|
+
i += 1
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func clear() {
|
|
66
|
+
try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)
|
|
67
|
+
.forEach { try? FileManager.default.removeItem(at: $0) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private func sha256(_ s: String) -> String {
|
|
71
|
+
SHA256.hash(data: Data(s.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
72
|
+
}
|
|
73
|
+
}
|