@ruhiverse/thermal-printer-plugin 1.0.3 → 1.0.4
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,277 @@ 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
|
+
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
|
+
}
|
|
165
228
|
}
|
|
166
|
-
return
|
|
167
229
|
}
|
|
168
230
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Not found in known peripherals, scan for it
|
|
245
|
+
printData = data
|
|
246
|
+
targetPrintAddress = address
|
|
247
|
+
printCompletion = { success, error in
|
|
248
|
+
self.targetPrintAddress = nil
|
|
249
|
+
if success {
|
|
171
250
|
DispatchQueue.main.async {
|
|
172
251
|
call.resolve([
|
|
173
252
|
"success": true,
|
|
174
|
-
"message": "Bluetooth print data sent to printer.",
|
|
175
|
-
"printerName": accessory.name,
|
|
176
|
-
"protocol": protocolString,
|
|
253
|
+
"message": "Bluetooth print data sent to printer (BLE).",
|
|
177
254
|
])
|
|
178
255
|
}
|
|
179
|
-
}
|
|
256
|
+
} else {
|
|
180
257
|
DispatchQueue.main.async {
|
|
181
|
-
call.reject("
|
|
258
|
+
call.reject("BLE print failed: \(error ?? "Unknown error")")
|
|
182
259
|
}
|
|
183
260
|
}
|
|
184
261
|
}
|
|
262
|
+
|
|
263
|
+
// Start scanning
|
|
264
|
+
if centralManager.state == .poweredOn {
|
|
265
|
+
centralManager.scanForPeripherals(withServices: nil, options: nil)
|
|
266
|
+
|
|
267
|
+
// Timeout after 10 seconds
|
|
268
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
|
|
269
|
+
centralManager.stopScan()
|
|
270
|
+
if self.printCompletion != nil {
|
|
271
|
+
self.targetPrintAddress = nil
|
|
272
|
+
DispatchQueue.main.async {
|
|
273
|
+
call.reject("Could not find printer with address: \(address)")
|
|
274
|
+
}
|
|
275
|
+
self.printCompletion = nil
|
|
276
|
+
self.printData = nil
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
DispatchQueue.main.async {
|
|
281
|
+
call.reject("Bluetooth is not powered on")
|
|
282
|
+
}
|
|
283
|
+
}
|
|
185
284
|
}
|
|
186
285
|
|
|
187
286
|
@objc func listBluetoothPrinters(_ call: CAPPluginCall) {
|
|
287
|
+
var printers: [[String: String]] = []
|
|
288
|
+
|
|
289
|
+
// First, get MFi/ExternalAccessory printers
|
|
188
290
|
let manager = EAAccessoryManager.shared()
|
|
189
291
|
let accessories = manager.connectedAccessories
|
|
190
292
|
|
|
191
|
-
let
|
|
293
|
+
let mfiPrinters: [[String: String]] = accessories.compactMap { accessory in
|
|
192
294
|
guard accessory.protocolStrings.contains(where: { supportedPrinterProtocols.contains($0) }) else {
|
|
193
295
|
return nil
|
|
194
296
|
}
|
|
195
297
|
return [
|
|
196
298
|
"name": accessory.name,
|
|
197
|
-
// iOS doesn't expose a MAC address for accessories; we
|
|
198
|
-
// return the serial number or a generated identifier.
|
|
199
299
|
"address": accessory.serialNumber.isEmpty ? "\(accessory.connectionID)" : accessory.serialNumber,
|
|
200
300
|
]
|
|
201
301
|
}
|
|
302
|
+
printers.append(contentsOf: mfiPrinters)
|
|
202
303
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
304
|
+
// Then scan for BLE printers
|
|
305
|
+
discoveredPeripherals.removeAll()
|
|
306
|
+
|
|
307
|
+
if centralManager == nil {
|
|
308
|
+
centralManager = CBCentralManager(delegate: self, queue: nil)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
guard let centralManager = centralManager else {
|
|
312
|
+
call.resolve(["printers": printers])
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Wait a bit for central manager to be ready, then scan
|
|
317
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
318
|
+
if centralManager.state == .poweredOn {
|
|
319
|
+
// Scan for all peripherals (we'll filter by name/characteristics)
|
|
320
|
+
centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
|
|
321
|
+
|
|
322
|
+
// Stop scanning after 5 seconds
|
|
323
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
|
324
|
+
centralManager.stopScan()
|
|
325
|
+
|
|
326
|
+
// Add discovered BLE printers
|
|
327
|
+
let blePrinters: [[String: String]] = self.discoveredPeripherals.compactMap { peripheral in
|
|
328
|
+
guard let name = peripheral.name, !name.isEmpty else {
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
331
|
+
// Filter for common printer names or accept all if we found them
|
|
332
|
+
let printerKeywords = ["printer", "print", "thermal", "pos", "epson", "star", "receipt"]
|
|
333
|
+
let isLikelyPrinter = printerKeywords.contains { keyword in
|
|
334
|
+
name.lowercased().contains(keyword)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Include if it's a likely printer or if we have no other printers found
|
|
338
|
+
if isLikelyPrinter || printers.isEmpty {
|
|
339
|
+
return [
|
|
340
|
+
"name": name,
|
|
341
|
+
"address": peripheral.identifier.uuidString,
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
return nil
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
printers.append(contentsOf: blePrinters)
|
|
348
|
+
call.resolve(["printers": printers])
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
// Bluetooth not available, return what we have
|
|
352
|
+
call.resolve(["printers": printers])
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// MARK: - CBCentralManagerDelegate
|
|
358
|
+
|
|
359
|
+
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
360
|
+
// State updated, scanning will start when ready
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
364
|
+
// Avoid duplicates
|
|
365
|
+
if !discoveredPeripherals.contains(where: { $0.identifier == peripheral.identifier }) {
|
|
366
|
+
discoveredPeripherals.append(peripheral)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// If we're looking for a specific printer to print to
|
|
370
|
+
if let targetAddress = targetPrintAddress,
|
|
371
|
+
peripheral.identifier.uuidString == targetAddress {
|
|
372
|
+
central.stopScan()
|
|
373
|
+
connectedPeripheral = peripheral
|
|
374
|
+
peripheral.delegate = self
|
|
375
|
+
central.connect(peripheral, options: nil)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
380
|
+
peripheral.delegate = self
|
|
381
|
+
peripheral.discoverServices(nil)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
385
|
+
printCompletion?(false, error?.localizedDescription ?? "Failed to connect")
|
|
386
|
+
printCompletion = nil
|
|
387
|
+
printData = nil
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
391
|
+
connectedPeripheral = nil
|
|
392
|
+
writeCharacteristic = nil
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// MARK: - CBPeripheralDelegate
|
|
396
|
+
|
|
397
|
+
public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
398
|
+
guard let services = peripheral.services else { return }
|
|
399
|
+
|
|
400
|
+
for service in services {
|
|
401
|
+
peripheral.discoverCharacteristics(nil, for: service)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
406
|
+
guard let characteristics = service.characteristics else { return }
|
|
407
|
+
|
|
408
|
+
for characteristic in characteristics {
|
|
409
|
+
// Look for write characteristics
|
|
410
|
+
if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
|
|
411
|
+
writeCharacteristic = characteristic
|
|
412
|
+
|
|
413
|
+
// Send print data if we have it
|
|
414
|
+
if let data = printData {
|
|
415
|
+
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
416
|
+
printCompletion?(true, nil)
|
|
417
|
+
printCompletion = nil
|
|
418
|
+
printData = nil
|
|
419
|
+
|
|
420
|
+
// Disconnect after a short delay
|
|
421
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
422
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
break
|
|
426
|
+
}
|
|
427
|
+
}
|
|
206
428
|
}
|
|
207
429
|
|
|
208
430
|
@objc func listUsbPrinters(_ call: CAPPluginCall) {
|