@ruhiverse/thermal-printer-plugin 1.0.3 → 1.0.5
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.
|
@@ -8,7 +8,7 @@ import CoreBluetooth
|
|
|
8
8
|
* Supports USB and Bluetooth printing via ESC/POS commands
|
|
9
9
|
*/
|
|
10
10
|
@objc(ThermalPrinterPlugin)
|
|
11
|
-
public class ThermalPrinterPlugin: CAPPlugin {
|
|
11
|
+
public class ThermalPrinterPlugin: CAPPlugin, CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
12
12
|
|
|
13
13
|
/// Protocol identifiers for supported MFi ESC/POS printers.
|
|
14
14
|
/// Update this list with the protocol strings for your specific printer model.
|
|
@@ -17,6 +17,23 @@ public class ThermalPrinterPlugin: CAPPlugin {
|
|
|
17
17
|
"com.starmicronics.starprnt",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
// CoreBluetooth properties
|
|
21
|
+
private var centralManager: CBCentralManager?
|
|
22
|
+
private var discoveredPeripherals: [CBPeripheral] = []
|
|
23
|
+
private var connectedPeripheral: CBPeripheral?
|
|
24
|
+
private var writeCharacteristic: CBCharacteristic?
|
|
25
|
+
private var scanCompletion: (([CBPeripheral]) -> Void)?
|
|
26
|
+
private var printCompletion: ((Bool, String?) -> Void)?
|
|
27
|
+
private var printData: Data?
|
|
28
|
+
private var targetPrintAddress: String?
|
|
29
|
+
|
|
30
|
+
// Common BLE service UUIDs for thermal printers (Serial Port Profile)
|
|
31
|
+
private let serialPortServiceUUID = CBUUID(string: "00001101-0000-1000-8000-00805F9B34FB") // SPP
|
|
32
|
+
private let commonPrinterServiceUUIDs = [
|
|
33
|
+
CBUUID(string: "00001101-0000-1000-8000-00805F9B34FB"), // SPP
|
|
34
|
+
CBUUID(string: "E7810A71-73AE-499D-8C15-DAA9080B0E"), // Some thermal printers
|
|
35
|
+
]
|
|
36
|
+
|
|
20
37
|
/// Find a connected External Accessory that matches the given (optional) name
|
|
21
38
|
/// and supports one of the known printer protocols.
|
|
22
39
|
private func findPrinterAccessory(preferredName: String?) -> (EAAccessory, String)? {
|
|
@@ -137,72 +154,278 @@ public class ThermalPrinterPlugin: CAPPlugin {
|
|
|
137
154
|
return
|
|
138
155
|
}
|
|
139
156
|
|
|
140
|
-
// iOS Bluetooth printing using Core Bluetooth
|
|
141
|
-
// This is a basic implementation - you may need to configure
|
|
142
|
-
// Bluetooth permissions in Info.plist
|
|
143
|
-
|
|
144
157
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
145
|
-
// On many MFi / ExternalAccessory-based thermal printers, the
|
|
146
|
-
// Bluetooth connection is abstracted behind the ExternalAccessory
|
|
147
|
-
// protocols (e.g., com.epson.escpos, com.starmicronics.starprnt).
|
|
148
|
-
// We reuse the same ExternalAccessory-based implementation here.
|
|
149
|
-
|
|
150
158
|
let preferredName = call.getString("name")
|
|
159
|
+
let address = call.getString("address")
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
// Convert text to data
|
|
162
|
+
guard let data = (textToPrint + "\n\n").data(using: .utf8) else {
|
|
153
163
|
DispatchQueue.main.async {
|
|
154
|
-
call.reject("
|
|
164
|
+
call.reject("Failed to encode text as UTF-8.")
|
|
155
165
|
}
|
|
156
166
|
return
|
|
157
167
|
}
|
|
158
168
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
// Try ExternalAccessory first (for MFi printers)
|
|
170
|
+
if let (accessory, protocolString) = self.findPrinterAccessory(preferredName: preferredName) {
|
|
171
|
+
do {
|
|
172
|
+
try self.sendPrintData(data, to: accessory, using: protocolString)
|
|
173
|
+
DispatchQueue.main.async {
|
|
174
|
+
call.resolve([
|
|
175
|
+
"success": true,
|
|
176
|
+
"message": "Bluetooth print data sent to printer (MFi).",
|
|
177
|
+
"printerName": accessory.name,
|
|
178
|
+
"protocol": protocolString,
|
|
179
|
+
])
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
} catch {
|
|
183
|
+
// Fall through to BLE if MFi fails
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try CoreBluetooth if address is provided or MFi didn't work
|
|
188
|
+
if let address = address, !address.isEmpty {
|
|
189
|
+
self.printViaBLE(address: address, data: data, call: call)
|
|
190
|
+
} else {
|
|
163
191
|
DispatchQueue.main.async {
|
|
164
|
-
call.reject("
|
|
192
|
+
call.reject("No compatible Bluetooth printer found. Make sure it is paired/connected. If using BLE, provide the printer address from listBluetoothPrinters().")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private func printViaBLE(address: String, data: Data, call: CAPPluginCall) {
|
|
199
|
+
if centralManager == nil {
|
|
200
|
+
centralManager = CBCentralManager(delegate: self, queue: nil)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
guard let centralManager = centralManager else {
|
|
204
|
+
DispatchQueue.main.async {
|
|
205
|
+
call.reject("Bluetooth not available")
|
|
206
|
+
}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Find peripheral by identifier
|
|
211
|
+
if let uuid = UUID(uuidString: address) {
|
|
212
|
+
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid])
|
|
213
|
+
if let peripheral = peripherals.first {
|
|
214
|
+
// Found in known peripherals, connect directly
|
|
215
|
+
printData = data
|
|
216
|
+
printCompletion = { success, error in
|
|
217
|
+
if success {
|
|
218
|
+
DispatchQueue.main.async {
|
|
219
|
+
call.resolve([
|
|
220
|
+
"success": true,
|
|
221
|
+
"message": "Bluetooth print data sent to printer (BLE).",
|
|
222
|
+
])
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
DispatchQueue.main.async {
|
|
226
|
+
call.reject("BLE print failed: \(error ?? "Unknown error")")
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
connectedPeripheral = peripheral
|
|
232
|
+
peripheral.delegate = self
|
|
233
|
+
|
|
234
|
+
if centralManager.state == .poweredOn {
|
|
235
|
+
centralManager.connect(peripheral, options: nil)
|
|
236
|
+
} else {
|
|
237
|
+
DispatchQueue.main.async {
|
|
238
|
+
call.reject("Bluetooth is not powered on")
|
|
239
|
+
}
|
|
165
240
|
}
|
|
166
241
|
return
|
|
167
242
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Not found in known peripherals, scan for it
|
|
246
|
+
printData = data
|
|
247
|
+
targetPrintAddress = address
|
|
248
|
+
printCompletion = { success, error in
|
|
249
|
+
self.targetPrintAddress = nil
|
|
250
|
+
if success {
|
|
171
251
|
DispatchQueue.main.async {
|
|
172
252
|
call.resolve([
|
|
173
253
|
"success": true,
|
|
174
|
-
"message": "Bluetooth print data sent to printer.",
|
|
175
|
-
"printerName": accessory.name,
|
|
176
|
-
"protocol": protocolString,
|
|
254
|
+
"message": "Bluetooth print data sent to printer (BLE).",
|
|
177
255
|
])
|
|
178
256
|
}
|
|
179
|
-
}
|
|
257
|
+
} else {
|
|
180
258
|
DispatchQueue.main.async {
|
|
181
|
-
call.reject("
|
|
259
|
+
call.reject("BLE print failed: \(error ?? "Unknown error")")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Start scanning
|
|
265
|
+
if centralManager.state == .poweredOn {
|
|
266
|
+
centralManager.scanForPeripherals(withServices: nil, options: nil)
|
|
267
|
+
|
|
268
|
+
// Timeout after 10 seconds
|
|
269
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
|
|
270
|
+
centralManager.stopScan()
|
|
271
|
+
if self.printCompletion != nil {
|
|
272
|
+
self.targetPrintAddress = nil
|
|
273
|
+
DispatchQueue.main.async {
|
|
274
|
+
call.reject("Could not find printer with address: \(address)")
|
|
275
|
+
}
|
|
276
|
+
self.printCompletion = nil
|
|
277
|
+
self.printData = nil
|
|
182
278
|
}
|
|
183
279
|
}
|
|
280
|
+
} else {
|
|
281
|
+
DispatchQueue.main.async {
|
|
282
|
+
call.reject("Bluetooth is not powered on")
|
|
283
|
+
}
|
|
184
284
|
}
|
|
185
285
|
}
|
|
186
286
|
|
|
187
287
|
@objc func listBluetoothPrinters(_ call: CAPPluginCall) {
|
|
288
|
+
var printers: [[String: String]] = []
|
|
289
|
+
|
|
290
|
+
// First, get MFi/ExternalAccessory printers
|
|
188
291
|
let manager = EAAccessoryManager.shared()
|
|
189
292
|
let accessories = manager.connectedAccessories
|
|
190
293
|
|
|
191
|
-
let
|
|
294
|
+
let mfiPrinters: [[String: String]] = accessories.compactMap { accessory in
|
|
192
295
|
guard accessory.protocolStrings.contains(where: { supportedPrinterProtocols.contains($0) }) else {
|
|
193
296
|
return nil
|
|
194
297
|
}
|
|
195
298
|
return [
|
|
196
299
|
"name": accessory.name,
|
|
197
|
-
// iOS doesn't expose a MAC address for accessories; we
|
|
198
|
-
// return the serial number or a generated identifier.
|
|
199
300
|
"address": accessory.serialNumber.isEmpty ? "\(accessory.connectionID)" : accessory.serialNumber,
|
|
200
301
|
]
|
|
201
302
|
}
|
|
303
|
+
printers.append(contentsOf: mfiPrinters)
|
|
202
304
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
305
|
+
// Then scan for BLE printers
|
|
306
|
+
discoveredPeripherals.removeAll()
|
|
307
|
+
|
|
308
|
+
if centralManager == nil {
|
|
309
|
+
centralManager = CBCentralManager(delegate: self, queue: nil)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
guard let centralManager = centralManager else {
|
|
313
|
+
call.resolve(["printers": printers])
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Wait a bit for central manager to be ready, then scan
|
|
318
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
319
|
+
if centralManager.state == .poweredOn {
|
|
320
|
+
// Scan for all peripherals (we'll filter by name/characteristics)
|
|
321
|
+
centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
|
|
322
|
+
|
|
323
|
+
// Stop scanning after 5 seconds
|
|
324
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
|
325
|
+
centralManager.stopScan()
|
|
326
|
+
|
|
327
|
+
// Add discovered BLE printers
|
|
328
|
+
let blePrinters: [[String: String]] = self.discoveredPeripherals.compactMap { peripheral in
|
|
329
|
+
guard let name = peripheral.name, !name.isEmpty else {
|
|
330
|
+
return nil
|
|
331
|
+
}
|
|
332
|
+
// Filter for common printer names or accept all if we found them
|
|
333
|
+
let printerKeywords = ["printer", "print", "thermal", "pos", "epson", "star", "receipt"]
|
|
334
|
+
let isLikelyPrinter = printerKeywords.contains { keyword in
|
|
335
|
+
name.lowercased().contains(keyword)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Include if it's a likely printer or if we have no other printers found
|
|
339
|
+
if isLikelyPrinter || printers.isEmpty {
|
|
340
|
+
return [
|
|
341
|
+
"name": name,
|
|
342
|
+
"address": peripheral.identifier.uuidString,
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
return nil
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
printers.append(contentsOf: blePrinters)
|
|
349
|
+
call.resolve(["printers": printers])
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
// Bluetooth not available, return what we have
|
|
353
|
+
call.resolve(["printers": printers])
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// MARK: - CBCentralManagerDelegate
|
|
359
|
+
|
|
360
|
+
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
361
|
+
// State updated, scanning will start when ready
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
365
|
+
// Avoid duplicates
|
|
366
|
+
if !discoveredPeripherals.contains(where: { $0.identifier == peripheral.identifier }) {
|
|
367
|
+
discoveredPeripherals.append(peripheral)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// If we're looking for a specific printer to print to
|
|
371
|
+
if let targetAddress = targetPrintAddress,
|
|
372
|
+
peripheral.identifier.uuidString == targetAddress {
|
|
373
|
+
central.stopScan()
|
|
374
|
+
connectedPeripheral = peripheral
|
|
375
|
+
peripheral.delegate = self
|
|
376
|
+
central.connect(peripheral, options: nil)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
381
|
+
peripheral.delegate = self
|
|
382
|
+
peripheral.discoverServices(nil)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
386
|
+
printCompletion?(false, error?.localizedDescription ?? "Failed to connect")
|
|
387
|
+
printCompletion = nil
|
|
388
|
+
printData = nil
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
392
|
+
connectedPeripheral = nil
|
|
393
|
+
writeCharacteristic = nil
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// MARK: - CBPeripheralDelegate
|
|
397
|
+
|
|
398
|
+
public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
399
|
+
guard let services = peripheral.services else { return }
|
|
400
|
+
|
|
401
|
+
for service in services {
|
|
402
|
+
peripheral.discoverCharacteristics(nil, for: service)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
407
|
+
guard let characteristics = service.characteristics else { return }
|
|
408
|
+
|
|
409
|
+
for characteristic in characteristics {
|
|
410
|
+
// Look for write characteristics
|
|
411
|
+
if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
|
|
412
|
+
writeCharacteristic = characteristic
|
|
413
|
+
|
|
414
|
+
// Send print data if we have it
|
|
415
|
+
if let data = printData {
|
|
416
|
+
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
417
|
+
printCompletion?(true, nil)
|
|
418
|
+
printCompletion = nil
|
|
419
|
+
printData = nil
|
|
420
|
+
|
|
421
|
+
// Disconnect after a short delay
|
|
422
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
423
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
break
|
|
427
|
+
}
|
|
428
|
+
}
|
|
206
429
|
}
|
|
207
430
|
|
|
208
431
|
@objc func listUsbPrinters(_ call: CAPPluginCall) {
|