@bricks-soft/capacitor-thermal-printer 0.0.0-stage → 8.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/BricksSoftCapacitorThermalPrinter.podspec +17 -0
- package/LICENSE +201 -0
- package/Package.swift +13 -0
- package/README.md +80 -2
- package/android/build.gradle +40 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/brickssoft/thermalprinter/ThermalPrinterPlugin.java +463 -0
- package/dist/esm/definitions.d.ts +73 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +23 -0
- package/dist/esm/web.d.ts +13 -0
- package/dist/esm/web.js +33 -0
- package/dist/plugin.cjs.js +65 -0
- package/dist/plugin.js +68 -0
- package/ios/Sources/ThermalPrinter/EscPosBarcode.swift +121 -0
- package/ios/Sources/ThermalPrinter/EscPosBitmap.swift +178 -0
- package/ios/Sources/ThermalPrinter/EscPosBluetooth.swift +341 -0
- package/ios/Sources/ThermalPrinter/EscPosBluetoothConnection.swift +298 -0
- package/ios/Sources/ThermalPrinter/EscPosBluetoothRules.swift +78 -0
- package/ios/Sources/ThermalPrinter/EscPosCharsetEncoding.swift +115 -0
- package/ios/Sources/ThermalPrinter/EscPosCoreBluetoothLink.swift +287 -0
- package/ios/Sources/ThermalPrinter/EscPosDeviceConnection.swift +47 -0
- package/ios/Sources/ThermalPrinter/EscPosJavaCompat.swift +213 -0
- package/ios/Sources/ThermalPrinter/EscPosKnownPrinterStore.swift +44 -0
- package/ios/Sources/ThermalPrinter/EscPosPrinter.swift +55 -0
- package/ios/Sources/ThermalPrinter/EscPosPrinterCommands.swift +367 -0
- package/ios/Sources/ThermalPrinter/EscPosPrinterSize.swift +53 -0
- package/ios/Sources/ThermalPrinter/EscPosQRCode.swift +70 -0
- package/ios/Sources/ThermalPrinter/EscPosQueue.swift +30 -0
- package/ios/Sources/ThermalPrinter/EscPosTcpConnection.swift +265 -0
- package/ios/Sources/ThermalPrinter/PrinterTextParser.swift +622 -0
- package/ios/Sources/ThermalPrinter/ThermalPrinterPlugin.swift +85 -0
- package/ios/Sources/ThermalPrinter/ThermalPrinterService.swift +380 -0
- package/package.json +61 -4
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import CoreBluetooth
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
protocol EscPosCentralManaging: AnyObject {
|
|
5
|
+
var state: CBManagerState { get }
|
|
6
|
+
func scanForPeripherals(withServices serviceUUIDs: [CBUUID]?, options: [String: Any]?)
|
|
7
|
+
func stopScan()
|
|
8
|
+
func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheral]
|
|
9
|
+
func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheral]
|
|
10
|
+
func connect(_ peripheral: CBPeripheral, options: [String: Any]?)
|
|
11
|
+
func cancelPeripheralConnection(_ peripheral: CBPeripheral)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
extension CBCentralManager: EscPosCentralManaging {}
|
|
15
|
+
|
|
16
|
+
protocol EscPosDiscoveredPeripheral: AnyObject {
|
|
17
|
+
var identifier: UUID { get }
|
|
18
|
+
var name: String? { get }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extension CBPeripheral: EscPosDiscoveredPeripheral {}
|
|
22
|
+
|
|
23
|
+
struct EscPosBluetoothDevice {
|
|
24
|
+
let peripheral: EscPosDiscoveredPeripheral
|
|
25
|
+
let name: String?
|
|
26
|
+
let services: [CBUUID]
|
|
27
|
+
let isPrinterCandidate: Bool
|
|
28
|
+
|
|
29
|
+
var identifier: UUID { peripheral.identifier }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
final class EscPosBluetoothCentral: NSObject, CBCentralManagerDelegate {
|
|
33
|
+
static var scanDuration: TimeInterval = 3
|
|
34
|
+
static var stateTimeout: TimeInterval = 10
|
|
35
|
+
static var authorizationTimeout: TimeInterval = 60
|
|
36
|
+
|
|
37
|
+
static func fallbackName(for identifier: UUID) -> String {
|
|
38
|
+
"BLE Printer \(identifier.uuidString.prefix(4))"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let queue = EscPosQueue(label: "de.paystory.thermal-printer.bluetooth")
|
|
42
|
+
private let authorizationSource: () -> CBManagerAuthorization
|
|
43
|
+
private let makeManager: (EscPosBluetoothCentral) -> EscPosCentralManaging
|
|
44
|
+
private let store: EscPosKnownPrinterStore
|
|
45
|
+
private var manager: EscPosCentralManaging?
|
|
46
|
+
private var stateEvent: DispatchSemaphore?
|
|
47
|
+
private var authorizationRequests: [(CBManagerAuthorization?) -> Void] = []
|
|
48
|
+
private var scanEvent: DispatchSemaphore?
|
|
49
|
+
private var scanMatch: ((EscPosBluetoothDevice) -> Bool)?
|
|
50
|
+
private var scanResults: [EscPosBluetoothDevice] = []
|
|
51
|
+
private var lastScanResults: [EscPosBluetoothDevice] = []
|
|
52
|
+
private var displayNames: [UUID: String] = [:]
|
|
53
|
+
private var connections: [UUID: EscPosBluetoothConnection] = [:]
|
|
54
|
+
private var links: [UUID: EscPosCoreBluetoothLink] = [:]
|
|
55
|
+
private var linkedPeripherals: [UUID: CBPeripheral] = [:]
|
|
56
|
+
|
|
57
|
+
init(
|
|
58
|
+
authorization: @escaping () -> CBManagerAuthorization = { CBManager.authorization },
|
|
59
|
+
store: EscPosKnownPrinterStore = EscPosKnownPrinterStore(),
|
|
60
|
+
makeManager: ((EscPosBluetoothCentral) -> EscPosCentralManaging)? = nil
|
|
61
|
+
) {
|
|
62
|
+
self.authorizationSource = authorization
|
|
63
|
+
self.store = store
|
|
64
|
+
self.makeManager = makeManager ?? { central in
|
|
65
|
+
CBCentralManager(delegate: central, queue: central.queue.queue, options: [CBCentralManagerOptionShowPowerAlertKey: false])
|
|
66
|
+
}
|
|
67
|
+
super.init()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var authorization: CBManagerAuthorization {
|
|
71
|
+
authorizationSource()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func requestAuthorization(_ completion: @escaping (CBManagerAuthorization?) -> Void) {
|
|
75
|
+
queue.sync {
|
|
76
|
+
let current = authorization
|
|
77
|
+
guard current == .notDetermined else {
|
|
78
|
+
completion(current)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
authorizationRequests.append(completion)
|
|
82
|
+
ensureManager()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func cancelAuthorizationRequests() {
|
|
87
|
+
queue.sync { finishAuthorizationRequests(nil) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func waitForAuthorization() -> CBManagerAuthorization {
|
|
91
|
+
let answered = DispatchSemaphore(value: 0)
|
|
92
|
+
requestAuthorization { _ in answered.signal() }
|
|
93
|
+
_ = answered.wait(timeout: .now() + EscPosBluetoothCentral.authorizationTimeout)
|
|
94
|
+
return authorization
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func finishAuthorizationRequests(_ answer: CBManagerAuthorization?) {
|
|
98
|
+
let requests = authorizationRequests
|
|
99
|
+
authorizationRequests = []
|
|
100
|
+
requests.forEach { $0(answer) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func waitForState() -> CBManagerState {
|
|
104
|
+
let deadline = Date().addingTimeInterval(EscPosBluetoothCentral.stateTimeout)
|
|
105
|
+
while true {
|
|
106
|
+
let (state, event) = queue.sync { () -> (CBManagerState, DispatchSemaphore) in
|
|
107
|
+
let current = ensureManager().state
|
|
108
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
109
|
+
stateEvent = semaphore
|
|
110
|
+
return (current, semaphore)
|
|
111
|
+
}
|
|
112
|
+
if state != .unknown && state != .resetting {
|
|
113
|
+
return state
|
|
114
|
+
}
|
|
115
|
+
if Date() >= deadline {
|
|
116
|
+
return state
|
|
117
|
+
}
|
|
118
|
+
_ = event.wait(timeout: .now() + 0.5)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func handleStateUpdate() {
|
|
123
|
+
queue.sync {
|
|
124
|
+
stateEvent?.signal()
|
|
125
|
+
let current = authorization
|
|
126
|
+
if current != .notDetermined {
|
|
127
|
+
finishAuthorizationRequests(current)
|
|
128
|
+
}
|
|
129
|
+
guard let manager = manager, manager.state != .poweredOn else { return }
|
|
130
|
+
for link in links.values {
|
|
131
|
+
link.handleUnavailable()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func knownDevices() -> [EscPosBluetoothDevice] {
|
|
137
|
+
queue.sync {
|
|
138
|
+
guard let manager = manager else { return [] }
|
|
139
|
+
var devices: [EscPosBluetoothDevice] = []
|
|
140
|
+
var seen = Set<UUID>()
|
|
141
|
+
func add(_ peripheral: CBPeripheral) {
|
|
142
|
+
guard !seen.contains(peripheral.identifier) else { return }
|
|
143
|
+
seen.insert(peripheral.identifier)
|
|
144
|
+
devices.append(EscPosBluetoothDevice(peripheral: peripheral, name: displayName(for: peripheral), services: [], isPrinterCandidate: true))
|
|
145
|
+
}
|
|
146
|
+
for (identifier, link) in links where link.isConnected {
|
|
147
|
+
if let peripheral = linkedPeripherals[identifier] {
|
|
148
|
+
add(peripheral)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let remembered = store.load().keys.compactMap { UUID(uuidString: $0) }
|
|
152
|
+
if !remembered.isEmpty {
|
|
153
|
+
manager.retrievePeripherals(withIdentifiers: remembered).forEach(add)
|
|
154
|
+
}
|
|
155
|
+
manager.retrieveConnectedPeripherals(withServices: EscPosBluetoothRules.printerServices).forEach(add)
|
|
156
|
+
return devices
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func scan(duration: TimeInterval, until match: ((EscPosBluetoothDevice) -> Bool)? = nil) -> [EscPosBluetoothDevice] {
|
|
161
|
+
let event: DispatchSemaphore? = queue.sync {
|
|
162
|
+
guard let manager = manager, manager.state == .poweredOn else { return nil }
|
|
163
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
164
|
+
scanResults = []
|
|
165
|
+
scanMatch = match
|
|
166
|
+
scanEvent = semaphore
|
|
167
|
+
manager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
|
|
168
|
+
return semaphore
|
|
169
|
+
}
|
|
170
|
+
guard let semaphore = event else { return [] }
|
|
171
|
+
_ = semaphore.wait(timeout: .now() + duration)
|
|
172
|
+
return queue.sync {
|
|
173
|
+
manager?.stopScan()
|
|
174
|
+
scanEvent = nil
|
|
175
|
+
scanMatch = nil
|
|
176
|
+
lastScanResults = scanResults
|
|
177
|
+
return scanResults
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func listDevices() -> [EscPosBluetoothDevice] {
|
|
182
|
+
var devices = knownDevices()
|
|
183
|
+
var seen = Set(devices.map(\.identifier))
|
|
184
|
+
for device in scan(duration: EscPosBluetoothCentral.scanDuration) where device.isPrinterCandidate && !seen.contains(device.identifier) {
|
|
185
|
+
seen.insert(device.identifier)
|
|
186
|
+
devices.append(device)
|
|
187
|
+
}
|
|
188
|
+
return devices
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func findPeripheral(id: String, address: String?) -> CBPeripheral? {
|
|
192
|
+
let keys = [id, address ?? ""].filter { !$0.isEmpty }
|
|
193
|
+
let byIdentifier: CBPeripheral? = queue.sync {
|
|
194
|
+
guard let manager = manager else { return nil }
|
|
195
|
+
for key in keys {
|
|
196
|
+
if let uuid = UUID(uuidString: key), let peripheral = manager.retrievePeripherals(withIdentifiers: [uuid]).first {
|
|
197
|
+
return peripheral
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return nil
|
|
201
|
+
}
|
|
202
|
+
if let peripheral = byIdentifier {
|
|
203
|
+
return peripheral
|
|
204
|
+
}
|
|
205
|
+
let matchesName: (EscPosBluetoothDevice) -> Bool = { device in
|
|
206
|
+
device.name == id || device.peripheral.name == id
|
|
207
|
+
}
|
|
208
|
+
if let peripheral = queue.sync({ lastScanResults.first(where: matchesName)?.peripheral as? CBPeripheral }) {
|
|
209
|
+
return peripheral
|
|
210
|
+
}
|
|
211
|
+
if let peripheral = knownDevices().first(where: matchesName)?.peripheral as? CBPeripheral {
|
|
212
|
+
return peripheral
|
|
213
|
+
}
|
|
214
|
+
return scan(duration: EscPosBluetoothCentral.scanDuration, until: matchesName).first(where: matchesName)?.peripheral as? CBPeripheral
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func candidatesForFirst() -> [CBPeripheral] {
|
|
218
|
+
let known = knownDevices()
|
|
219
|
+
if !known.isEmpty {
|
|
220
|
+
return known.compactMap { $0.peripheral as? CBPeripheral }
|
|
221
|
+
}
|
|
222
|
+
return scan(duration: EscPosBluetoothCentral.scanDuration).filter(\.isPrinterCandidate).compactMap { $0.peripheral as? CBPeripheral }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var isPoweredOn: Bool {
|
|
226
|
+
queue.sync { manager?.state == .poweredOn }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func connection(for peripheral: CBPeripheral) -> EscPosBluetoothConnection {
|
|
230
|
+
queue.sync {
|
|
231
|
+
if connections[peripheral.identifier] == nil {
|
|
232
|
+
linkedPeripherals[peripheral.identifier] = peripheral
|
|
233
|
+
}
|
|
234
|
+
return connection(for: peripheral.identifier, driver: EscPosCBPeripheralDriver(peripheral: peripheral, central: self)) { [weak self] in
|
|
235
|
+
self?.remember(peripheral)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func connection(for identifier: UUID, driver: @autoclosure () -> EscPosPeripheralDriver, onConnected: (() -> Void)? = nil) -> EscPosBluetoothConnection {
|
|
241
|
+
queue.sync {
|
|
242
|
+
if let existing = connections[identifier] {
|
|
243
|
+
return existing
|
|
244
|
+
}
|
|
245
|
+
let link = EscPosCoreBluetoothLink(driver: driver())
|
|
246
|
+
links[identifier] = link
|
|
247
|
+
let created = EscPosBluetoothConnection(link: link, queue: queue, onConnected: onConnected)
|
|
248
|
+
connections[identifier] = created
|
|
249
|
+
return created
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func startConnecting(_ peripheral: CBPeripheral) {
|
|
254
|
+
queue.sync { manager?.connect(peripheral, options: nil) }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func cancelConnection(_ peripheral: CBPeripheral) {
|
|
258
|
+
queue.sync { manager?.cancelPeripheralConnection(peripheral) }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func remember(_ peripheral: CBPeripheral) {
|
|
262
|
+
let name = queue.sync { displayName(for: peripheral) ?? peripheral.name ?? EscPosBluetoothCentral.fallbackName(for: peripheral.identifier) }
|
|
263
|
+
store.remember(identifier: peripheral.identifier, name: name)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
func record(_ peripheral: EscPosDiscoveredPeripheral, advertisementData: [String: Any]) {
|
|
267
|
+
guard scanEvent != nil else { return }
|
|
268
|
+
let connectable = (advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber)?.boolValue
|
|
269
|
+
if connectable == false {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
let index = scanResults.firstIndex { $0.identifier == peripheral.identifier }
|
|
273
|
+
let earlierServices = index.map { scanResults[$0].services } ?? []
|
|
274
|
+
let advertised = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
|
|
275
|
+
let services = earlierServices + advertised.filter { !earlierServices.contains($0) }
|
|
276
|
+
let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
|
277
|
+
let advertisedName = [localName, peripheral.name].compactMap { $0 }.first { !$0.isEmpty }
|
|
278
|
+
let isPrinterCandidate = index.map { scanResults[$0].isPrinterCandidate } == true
|
|
279
|
+
|| EscPosBluetoothRules.isPrinterCandidate(name: advertisedName, advertisedServices: services, connectable: connectable)
|
|
280
|
+
guard advertisedName != nil || isPrinterCandidate else { return }
|
|
281
|
+
if let advertisedName = advertisedName {
|
|
282
|
+
displayNames[peripheral.identifier] = advertisedName
|
|
283
|
+
} else if displayNames[peripheral.identifier] == nil {
|
|
284
|
+
displayNames[peripheral.identifier] = EscPosBluetoothCentral.fallbackName(for: peripheral.identifier)
|
|
285
|
+
}
|
|
286
|
+
let device = EscPosBluetoothDevice(
|
|
287
|
+
peripheral: peripheral,
|
|
288
|
+
name: displayName(for: peripheral),
|
|
289
|
+
services: services,
|
|
290
|
+
isPrinterCandidate: isPrinterCandidate
|
|
291
|
+
)
|
|
292
|
+
if let index = index {
|
|
293
|
+
scanResults[index] = device
|
|
294
|
+
} else {
|
|
295
|
+
scanResults.append(device)
|
|
296
|
+
}
|
|
297
|
+
if let match = scanMatch, match(device) {
|
|
298
|
+
scanEvent?.signal()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func displayName(for peripheral: EscPosDiscoveredPeripheral) -> String? {
|
|
303
|
+
if let remembered = store.load()[peripheral.identifier.uuidString], !remembered.isEmpty {
|
|
304
|
+
return remembered
|
|
305
|
+
}
|
|
306
|
+
if let name = displayNames[peripheral.identifier], !name.isEmpty {
|
|
307
|
+
return name
|
|
308
|
+
}
|
|
309
|
+
return peripheral.name
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@discardableResult
|
|
313
|
+
private func ensureManager() -> EscPosCentralManaging {
|
|
314
|
+
if let manager = manager {
|
|
315
|
+
return manager
|
|
316
|
+
}
|
|
317
|
+
let created = makeManager(self)
|
|
318
|
+
manager = created
|
|
319
|
+
return created
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
323
|
+
handleStateUpdate()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
327
|
+
record(peripheral, advertisementData: advertisementData)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
331
|
+
links[peripheral.identifier]?.handleConnected()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
335
|
+
links[peripheral.identifier]?.handleFailedToConnect(error)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
339
|
+
links[peripheral.identifier]?.handleDisconnected(error)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import CoreBluetooth
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
protocol EscPosBluetoothLinkEvents: AnyObject {
|
|
5
|
+
func linkDidConnect(attempt: Int)
|
|
6
|
+
func linkDidFailToConnect(attempt: Int, error: Error?)
|
|
7
|
+
func linkDidDisconnect(attempt: Int, error: Error?)
|
|
8
|
+
func linkDidDiscover(attempt: Int, writeType: CBCharacteristicWriteType?, error: Error?)
|
|
9
|
+
func linkDidWrite(token: Int, error: Error?)
|
|
10
|
+
func linkIsReadyToSend(attempt: Int)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protocol EscPosBluetoothLink: AnyObject {
|
|
14
|
+
var events: EscPosBluetoothLinkEvents? { get set }
|
|
15
|
+
var isConnected: Bool { get }
|
|
16
|
+
var canSendWriteWithoutResponse: Bool { get }
|
|
17
|
+
func maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int
|
|
18
|
+
func connect(attempt: Int)
|
|
19
|
+
func adopt(attempt: Int)
|
|
20
|
+
func cancel()
|
|
21
|
+
func discoverPrinterCharacteristic(attempt: Int)
|
|
22
|
+
func write(_ data: Data, type: CBCharacteristicWriteType, token: Int)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
struct EscPosBluetoothTimeouts {
|
|
26
|
+
var connect: TimeInterval = 10
|
|
27
|
+
var discovery: TimeInterval = 10
|
|
28
|
+
var write: TimeInterval = 10
|
|
29
|
+
var readinessPoll: TimeInterval = 0.05
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
final class EscPosBluetoothConnection: EscPosDeviceConnection, EscPosBluetoothLinkEvents {
|
|
33
|
+
private enum OperationKind {
|
|
34
|
+
case connect
|
|
35
|
+
case discovery
|
|
36
|
+
case write
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private final class Operation {
|
|
40
|
+
let kind: OperationKind
|
|
41
|
+
let token: Int
|
|
42
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
43
|
+
var finished = false
|
|
44
|
+
var error: Error?
|
|
45
|
+
|
|
46
|
+
init(kind: OperationKind, token: Int) {
|
|
47
|
+
self.kind = kind
|
|
48
|
+
self.token = token
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private struct LinkFailure: Error {}
|
|
53
|
+
|
|
54
|
+
let link: EscPosBluetoothLink
|
|
55
|
+
private let queue: EscPosQueue
|
|
56
|
+
private let timeouts: EscPosBluetoothTimeouts
|
|
57
|
+
private let onConnected: (() -> Void)?
|
|
58
|
+
private var attempt = 0
|
|
59
|
+
private var lastToken = 0
|
|
60
|
+
private var linkUp = false
|
|
61
|
+
private var tearingDown = false
|
|
62
|
+
private var writeType: CBCharacteristicWriteType?
|
|
63
|
+
private var current: Operation?
|
|
64
|
+
private var readiness: DispatchSemaphore?
|
|
65
|
+
private var ignoredCallbacks = 0
|
|
66
|
+
|
|
67
|
+
init(link: EscPosBluetoothLink, queue: EscPosQueue, timeouts: EscPosBluetoothTimeouts = EscPosBluetoothTimeouts(), onConnected: (() -> Void)? = nil) {
|
|
68
|
+
self.link = link
|
|
69
|
+
self.queue = queue
|
|
70
|
+
self.timeouts = timeouts
|
|
71
|
+
self.onConnected = onConnected
|
|
72
|
+
super.init()
|
|
73
|
+
queue.sync { link.events = self }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override var isConnected: Bool {
|
|
77
|
+
queue.sync { linkUp && link.isConnected && writeType != nil }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var ignoredCallbackCount: Int {
|
|
81
|
+
queue.sync { ignoredCallbacks }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@discardableResult
|
|
85
|
+
override func connect() throws -> EscPosDeviceConnection {
|
|
86
|
+
if isConnected {
|
|
87
|
+
return self
|
|
88
|
+
}
|
|
89
|
+
do {
|
|
90
|
+
let connecting = queue.sync { () -> Operation? in
|
|
91
|
+
attempt = nextToken()
|
|
92
|
+
writeType = nil
|
|
93
|
+
if link.isConnected && !tearingDown {
|
|
94
|
+
linkUp = true
|
|
95
|
+
link.adopt(attempt: attempt)
|
|
96
|
+
return nil
|
|
97
|
+
}
|
|
98
|
+
linkUp = false
|
|
99
|
+
let operation = begin(.connect, token: attempt)
|
|
100
|
+
link.connect(attempt: attempt)
|
|
101
|
+
return operation
|
|
102
|
+
}
|
|
103
|
+
if let operation = connecting {
|
|
104
|
+
try waitFor(operation, timeout: timeouts.connect)
|
|
105
|
+
}
|
|
106
|
+
let discovery = queue.sync { () -> Operation in
|
|
107
|
+
let operation = begin(.discovery, token: attempt)
|
|
108
|
+
link.discoverPrinterCharacteristic(attempt: attempt)
|
|
109
|
+
return operation
|
|
110
|
+
}
|
|
111
|
+
try waitFor(discovery, timeout: timeouts.discovery)
|
|
112
|
+
guard isConnected else {
|
|
113
|
+
throw LinkFailure()
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
invalidate()
|
|
117
|
+
throw EscPosError.connection("Unable to connect to bluetooth device.")
|
|
118
|
+
}
|
|
119
|
+
onConnected?()
|
|
120
|
+
clearBuffer()
|
|
121
|
+
return self
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@discardableResult
|
|
125
|
+
override func disconnect() -> EscPosDeviceConnection {
|
|
126
|
+
invalidate()
|
|
127
|
+
return super.disconnect()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override func transmit(_ bytes: [UInt8]) throws {
|
|
131
|
+
let target: (CBCharacteristicWriteType, Int)? = queue.sync {
|
|
132
|
+
guard linkUp, link.isConnected, let type = writeType else { return nil }
|
|
133
|
+
return (type, max(20, link.maximumWriteValueLength(for: type)))
|
|
134
|
+
}
|
|
135
|
+
guard let (type, chunkSize) = target else {
|
|
136
|
+
throw EscPosError.connection("Unable to send data to device.")
|
|
137
|
+
}
|
|
138
|
+
var offset = 0
|
|
139
|
+
while offset < bytes.count {
|
|
140
|
+
let end = min(offset + chunkSize, bytes.count)
|
|
141
|
+
let chunk = Data(bytes[offset..<end])
|
|
142
|
+
if type == .withoutResponse {
|
|
143
|
+
try waitUntilReadyToWrite()
|
|
144
|
+
try queue.sync {
|
|
145
|
+
guard linkUp else { throw EscPosError.connection("Bluetooth device disconnected.") }
|
|
146
|
+
link.write(chunk, type: .withoutResponse, token: nextToken())
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
let operation = try queue.sync { () -> Operation in
|
|
150
|
+
guard linkUp else { throw EscPosError.connection("Bluetooth device disconnected.") }
|
|
151
|
+
let operation = begin(.write, token: nextToken())
|
|
152
|
+
link.write(chunk, type: .withResponse, token: operation.token)
|
|
153
|
+
return operation
|
|
154
|
+
}
|
|
155
|
+
do {
|
|
156
|
+
try waitFor(operation, timeout: timeouts.write)
|
|
157
|
+
} catch let failure as EscPosError {
|
|
158
|
+
throw failure
|
|
159
|
+
} catch {
|
|
160
|
+
throw EscPosError.connection(error.localizedDescription)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
offset = end
|
|
164
|
+
}
|
|
165
|
+
if !queue.sync({ linkUp }) {
|
|
166
|
+
throw EscPosError.connection("Bluetooth device disconnected.")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func linkDidConnect(attempt connected: Int) {
|
|
171
|
+
guard connected == attempt, let operation = current, operation.kind == .connect, operation.token == connected else {
|
|
172
|
+
ignoredCallbacks += 1
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
linkUp = true
|
|
176
|
+
finish(operation, error: nil)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func linkDidFailToConnect(attempt failed: Int, error: Error?) {
|
|
180
|
+
guard failed == attempt, let operation = current, operation.kind == .connect, operation.token == failed else {
|
|
181
|
+
ignoredCallbacks += 1
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
finish(operation, error: error ?? LinkFailure())
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func linkDidDisconnect(attempt ended: Int, error: Error?) {
|
|
188
|
+
guard ended == attempt else {
|
|
189
|
+
tearingDown = false
|
|
190
|
+
ignoredCallbacks += 1
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
linkUp = false
|
|
194
|
+
writeType = nil
|
|
195
|
+
readiness?.signal()
|
|
196
|
+
if let operation = current {
|
|
197
|
+
finish(operation, error: error ?? LinkFailure())
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func linkDidDiscover(attempt discovered: Int, writeType found: CBCharacteristicWriteType?, error: Error?) {
|
|
202
|
+
guard discovered == attempt, let operation = current, operation.kind == .discovery, operation.token == discovered else {
|
|
203
|
+
ignoredCallbacks += 1
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if error == nil, let found = found {
|
|
207
|
+
writeType = found
|
|
208
|
+
finish(operation, error: nil)
|
|
209
|
+
} else {
|
|
210
|
+
finish(operation, error: error ?? LinkFailure())
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func linkDidWrite(token: Int, error: Error?) {
|
|
215
|
+
guard let operation = current, operation.kind == .write, operation.token == token else {
|
|
216
|
+
ignoredCallbacks += 1
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
finish(operation, error: error)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func linkIsReadyToSend(attempt ready: Int) {
|
|
223
|
+
guard ready == attempt else {
|
|
224
|
+
ignoredCallbacks += 1
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
readiness?.signal()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func nextToken() -> Int {
|
|
231
|
+
lastToken += 1
|
|
232
|
+
return lastToken
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func begin(_ kind: OperationKind, token: Int) -> Operation {
|
|
236
|
+
let operation = Operation(kind: kind, token: token)
|
|
237
|
+
current = operation
|
|
238
|
+
return operation
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private func finish(_ operation: Operation, error: Error?) {
|
|
242
|
+
guard !operation.finished else { return }
|
|
243
|
+
operation.finished = true
|
|
244
|
+
operation.error = error
|
|
245
|
+
if current === operation {
|
|
246
|
+
current = nil
|
|
247
|
+
}
|
|
248
|
+
operation.semaphore.signal()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private func waitFor(_ operation: Operation, timeout: TimeInterval) throws {
|
|
252
|
+
if operation.semaphore.wait(timeout: .now() + timeout) == .timedOut {
|
|
253
|
+
queue.sync {
|
|
254
|
+
finish(operation, error: EscPosError.connection("Timed out while waiting for the Bluetooth device."))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if let error = queue.sync({ operation.error }) {
|
|
258
|
+
throw error
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func waitUntilReadyToWrite() throws {
|
|
263
|
+
let deadline = Date().addingTimeInterval(timeouts.write)
|
|
264
|
+
while true {
|
|
265
|
+
let (ready, connected, semaphore) = queue.sync { () -> (Bool, Bool, DispatchSemaphore) in
|
|
266
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
267
|
+
readiness = semaphore
|
|
268
|
+
return (link.canSendWriteWithoutResponse, linkUp, semaphore)
|
|
269
|
+
}
|
|
270
|
+
if !connected {
|
|
271
|
+
throw EscPosError.connection("Bluetooth device disconnected.")
|
|
272
|
+
}
|
|
273
|
+
if ready {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if Date() >= deadline {
|
|
277
|
+
throw EscPosError.connection("Timed out while sending data to device.")
|
|
278
|
+
}
|
|
279
|
+
_ = semaphore.wait(timeout: .now() + timeouts.readinessPoll)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private func invalidate() {
|
|
284
|
+
queue.sync {
|
|
285
|
+
if link.isConnected {
|
|
286
|
+
tearingDown = true
|
|
287
|
+
}
|
|
288
|
+
attempt = nextToken()
|
|
289
|
+
linkUp = false
|
|
290
|
+
writeType = nil
|
|
291
|
+
if let operation = current {
|
|
292
|
+
finish(operation, error: LinkFailure())
|
|
293
|
+
}
|
|
294
|
+
readiness?.signal()
|
|
295
|
+
link.cancel()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|