@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.
Files changed (35) hide show
  1. package/BricksSoftCapacitorThermalPrinter.podspec +17 -0
  2. package/LICENSE +201 -0
  3. package/Package.swift +13 -0
  4. package/README.md +80 -2
  5. package/android/build.gradle +40 -0
  6. package/android/src/main/AndroidManifest.xml +8 -0
  7. package/android/src/main/java/com/brickssoft/thermalprinter/ThermalPrinterPlugin.java +463 -0
  8. package/dist/esm/definitions.d.ts +73 -0
  9. package/dist/esm/definitions.js +1 -0
  10. package/dist/esm/index.d.ts +3 -0
  11. package/dist/esm/index.js +23 -0
  12. package/dist/esm/web.d.ts +13 -0
  13. package/dist/esm/web.js +33 -0
  14. package/dist/plugin.cjs.js +65 -0
  15. package/dist/plugin.js +68 -0
  16. package/ios/Sources/ThermalPrinter/EscPosBarcode.swift +121 -0
  17. package/ios/Sources/ThermalPrinter/EscPosBitmap.swift +178 -0
  18. package/ios/Sources/ThermalPrinter/EscPosBluetooth.swift +341 -0
  19. package/ios/Sources/ThermalPrinter/EscPosBluetoothConnection.swift +298 -0
  20. package/ios/Sources/ThermalPrinter/EscPosBluetoothRules.swift +78 -0
  21. package/ios/Sources/ThermalPrinter/EscPosCharsetEncoding.swift +115 -0
  22. package/ios/Sources/ThermalPrinter/EscPosCoreBluetoothLink.swift +287 -0
  23. package/ios/Sources/ThermalPrinter/EscPosDeviceConnection.swift +47 -0
  24. package/ios/Sources/ThermalPrinter/EscPosJavaCompat.swift +213 -0
  25. package/ios/Sources/ThermalPrinter/EscPosKnownPrinterStore.swift +44 -0
  26. package/ios/Sources/ThermalPrinter/EscPosPrinter.swift +55 -0
  27. package/ios/Sources/ThermalPrinter/EscPosPrinterCommands.swift +367 -0
  28. package/ios/Sources/ThermalPrinter/EscPosPrinterSize.swift +53 -0
  29. package/ios/Sources/ThermalPrinter/EscPosQRCode.swift +70 -0
  30. package/ios/Sources/ThermalPrinter/EscPosQueue.swift +30 -0
  31. package/ios/Sources/ThermalPrinter/EscPosTcpConnection.swift +265 -0
  32. package/ios/Sources/ThermalPrinter/PrinterTextParser.swift +622 -0
  33. package/ios/Sources/ThermalPrinter/ThermalPrinterPlugin.swift +85 -0
  34. package/ios/Sources/ThermalPrinter/ThermalPrinterService.swift +380 -0
  35. 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
+ }