@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
- guard let (accessory, protocolString) = self.findPrinterAccessory(preferredName: preferredName) else {
161
+ // Convert text to data
162
+ guard let data = (textToPrint + "\n\n").data(using: .utf8) else {
153
163
  DispatchQueue.main.async {
154
- call.reject("No compatible Bluetooth printer found. Make sure it is paired, connected, and its protocol is listed in UISupportedExternalAccessoryProtocols.")
164
+ call.reject("Failed to encode text as UTF-8.")
155
165
  }
156
166
  return
157
167
  }
158
168
 
159
- // Minimal text ESC/POS data conversion.
160
- // Formatting tags like [C] or <b> are not parsed yet and will
161
- // be sent as plain text.
162
- guard let data = (textToPrint + "\n\n").data(using: .utf8) else {
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("Failed to encode text as UTF-8.")
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
- do {
170
- try self.sendPrintData(data, to: accessory, using: protocolString)
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
- } catch {
257
+ } else {
180
258
  DispatchQueue.main.async {
181
- call.reject("Bluetooth print failed: \(error.localizedDescription)")
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 printers: [[String: String]] = accessories.compactMap { accessory in
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
- call.resolve([
204
- "printers": printers,
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruhiverse/thermal-printer-plugin",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Capacitor plugin for thermal printing via USB and Bluetooth",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",