@ruhiverse/thermal-printer-plugin 1.0.2 → 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
- 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
+ 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
- do {
170
- try self.sendPrintData(data, to: accessory, using: protocolString)
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
- } catch {
256
+ } else {
180
257
  DispatchQueue.main.async {
181
- call.reject("Bluetooth print failed: \(error.localizedDescription)")
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 printers: [[String: String]] = accessories.compactMap { accessory in
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
- call.resolve([
204
- "printers": printers,
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruhiverse/thermal-printer-plugin",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",