@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,78 @@
1
+ import CoreBluetooth
2
+ import Foundation
3
+
4
+ enum EscPosBluetoothRules {
5
+ static let printerServices: [CBUUID] = [
6
+ CBUUID(string: "18F0"),
7
+ CBUUID(string: "E7810A71-73AE-499D-8C15-FAA9AEF0C3F2"),
8
+ CBUUID(string: "49535343-FE7D-4AE5-8FA9-9FAFD205E455"),
9
+ CBUUID(string: "FF00"),
10
+ CBUUID(string: "FFE0"),
11
+ CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"),
12
+ ]
13
+
14
+ static let printerCharacteristics: [CBUUID] = [
15
+ CBUUID(string: "2AF1"),
16
+ CBUUID(string: "BEF8D6C9-9C21-4C9E-B632-BD58C1009F9F"),
17
+ CBUUID(string: "49535343-8841-43F4-A8D4-ECBE34729BB3"),
18
+ CBUUID(string: "FF02"),
19
+ CBUUID(string: "FFE1"),
20
+ CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
21
+ ]
22
+
23
+ static let standardServices: [CBUUID] = [
24
+ CBUUID(string: "1800"),
25
+ CBUUID(string: "1801"),
26
+ CBUUID(string: "180A"),
27
+ CBUUID(string: "180F"),
28
+ ]
29
+
30
+ static let printerNameFragments = ["print", "thermal", "receipt"]
31
+ static let printerNamePrefixes = ["mpt", "mtp", "rpp", "xp-", "pt-", "zj-", "pos", "goojprt", "peripage"]
32
+
33
+ static func isPrinterCandidate(name: String?, advertisedServices: [CBUUID], connectable: Bool?) -> Bool {
34
+ if connectable == false {
35
+ return false
36
+ }
37
+ if advertisedServices.contains(where: { printerServices.contains($0) }) {
38
+ return true
39
+ }
40
+ guard let name = name?.lowercased(), !name.isEmpty else {
41
+ return false
42
+ }
43
+ return printerNameFragments.contains { name.contains($0) } || printerNamePrefixes.contains { name.hasPrefix($0) }
44
+ }
45
+
46
+ static func writeType(for properties: CBCharacteristicProperties) -> CBCharacteristicWriteType? {
47
+ if properties.contains(.writeWithoutResponse) {
48
+ return .withoutResponse
49
+ }
50
+ if properties.contains(.write) {
51
+ return .withResponse
52
+ }
53
+ return nil
54
+ }
55
+
56
+ static func selectCharacteristic(in services: [CBService]) -> (CBCharacteristic, CBCharacteristicWriteType)? {
57
+ let characteristics = services.flatMap { $0.characteristics ?? [] }
58
+ for uuid in printerCharacteristics {
59
+ if let match = characteristics.first(where: { $0.uuid == uuid }), let type = writeType(for: match.properties) {
60
+ return (match, type)
61
+ }
62
+ }
63
+ for uuid in printerServices {
64
+ for service in services where service.uuid == uuid {
65
+ for characteristic in service.characteristics ?? [] {
66
+ if let type = writeType(for: characteristic.properties) {
67
+ return (characteristic, type)
68
+ }
69
+ }
70
+ }
71
+ }
72
+ let vendorWritable = services
73
+ .filter { !standardServices.contains($0.uuid) }
74
+ .flatMap { $0.characteristics ?? [] }
75
+ .compactMap { characteristic in writeType(for: characteristic.properties).map { (characteristic, $0) } }
76
+ return vendorWritable.count == 1 ? vendorWritable[0] : nil
77
+ }
78
+ }
@@ -0,0 +1,115 @@
1
+ import Foundation
2
+
3
+ struct EscPosCharsetEncoding: Equatable {
4
+ let name: String
5
+ let command: [UInt8]
6
+
7
+ init(name: String, escPosCharsetId: Int) {
8
+ self.name = name
9
+ self.command = [0x1B, 0x74, UInt8(truncatingIfNeeded: escPosCharsetId)]
10
+ }
11
+
12
+ func encode(_ text: JavaString) throws -> [UInt8] {
13
+ let encoding = try EscPosCharsetEncoding.stringEncoding(for: name)
14
+ if let data = text.string.data(using: encoding, allowLossyConversion: false), !EscPosCharsetEncoding.hasLoneSurrogate(text) {
15
+ return EscPosCharsetEncoding.dropByteOrderMark([UInt8](data), encoding: encoding)
16
+ }
17
+ var bytes: [UInt8] = []
18
+ var index = 0
19
+ let units = text.units
20
+ while index < units.count {
21
+ let unit = units[index]
22
+ var codePointUnits = [unit]
23
+ if UTF16.isLeadSurrogate(unit), index + 1 < units.count, UTF16.isTrailSurrogate(units[index + 1]) {
24
+ codePointUnits.append(units[index + 1])
25
+ }
26
+ index += codePointUnits.count
27
+ let isLoneSurrogate = codePointUnits.count == 1 && (UTF16.isLeadSurrogate(unit) || UTF16.isTrailSurrogate(unit))
28
+ if !isLoneSurrogate,
29
+ let data = String(decoding: codePointUnits, as: UTF16.self).data(using: encoding, allowLossyConversion: false) {
30
+ bytes.append(contentsOf: EscPosCharsetEncoding.dropByteOrderMark([UInt8](data), encoding: encoding))
31
+ } else {
32
+ bytes.append(contentsOf: EscPosCharsetEncoding.replacement(for: encoding))
33
+ }
34
+ }
35
+ return bytes
36
+ }
37
+
38
+ static func stringEncoding(for charsetName: String) throws -> String.Encoding {
39
+ for candidate in candidates(for: charsetName) {
40
+ let cfEncoding = CFStringConvertIANACharSetNameToEncoding(candidate as CFString)
41
+ if cfEncoding != kCFStringEncodingInvalidId {
42
+ return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(cfEncoding))
43
+ }
44
+ }
45
+ throw EscPosError.encoding(charsetName)
46
+ }
47
+
48
+ private static func candidates(for charsetName: String) -> [String] {
49
+ let trimmed = charsetName.trimmingCharacters(in: .whitespaces)
50
+ guard !trimmed.isEmpty else { return [] }
51
+ let lower = trimmed.lowercased()
52
+ var names = [trimmed]
53
+ if let digits = suffixDigits(lower, prefixes: ["cp", "ibm", "ibm-", "cp-"]) {
54
+ if digits.count == 4, digits.hasPrefix("12") {
55
+ names.append("windows-\(digits)")
56
+ } else {
57
+ names.append("IBM\(digits)")
58
+ names.append("cp\(digits)")
59
+ }
60
+ }
61
+ if let digits = suffixDigits(lower, prefixes: ["windows-", "windows"]) {
62
+ names.append("windows-\(digits)")
63
+ }
64
+ if let digits = suffixDigits(lower, prefixes: ["iso8859_", "iso8859-", "iso-8859_", "iso_8859-", "iso_8859_"]) {
65
+ names.append("ISO-8859-\(digits)")
66
+ }
67
+ if lower == "utf8" { names.append("UTF-8") }
68
+ if lower == "ascii" || lower == "us-ascii" { names.append("US-ASCII") }
69
+ return names
70
+ }
71
+
72
+ private static func suffixDigits(_ name: String, prefixes: [String]) -> String? {
73
+ for prefix in prefixes where name.hasPrefix(prefix) {
74
+ let rest = String(name.dropFirst(prefix.count))
75
+ if !rest.isEmpty, rest.allSatisfy({ $0.isASCII && $0.isNumber }) {
76
+ return rest
77
+ }
78
+ }
79
+ return nil
80
+ }
81
+
82
+ private static func hasLoneSurrogate(_ text: JavaString) -> Bool {
83
+ var index = 0
84
+ let units = text.units
85
+ while index < units.count {
86
+ let unit = units[index]
87
+ if UTF16.isLeadSurrogate(unit) {
88
+ if index + 1 < units.count, UTF16.isTrailSurrogate(units[index + 1]) {
89
+ index += 2
90
+ continue
91
+ }
92
+ return true
93
+ }
94
+ if UTF16.isTrailSurrogate(unit) {
95
+ return true
96
+ }
97
+ index += 1
98
+ }
99
+ return false
100
+ }
101
+
102
+ private static func dropByteOrderMark(_ bytes: [UInt8], encoding: String.Encoding) -> [UInt8] {
103
+ if encoding == .utf8, bytes.starts(with: [0xEF, 0xBB, 0xBF]) {
104
+ return Array(bytes.dropFirst(3))
105
+ }
106
+ return bytes
107
+ }
108
+
109
+ private static func replacement(for encoding: String.Encoding) -> [UInt8] {
110
+ if let data = "?".data(using: encoding, allowLossyConversion: false) {
111
+ return dropByteOrderMark([UInt8](data), encoding: encoding)
112
+ }
113
+ return [0x3F]
114
+ }
115
+ }
@@ -0,0 +1,287 @@
1
+ import CoreBluetooth
2
+ import Foundation
3
+
4
+ protocol EscPosPeripheralDriver: AnyObject {
5
+ var isAvailable: Bool { get }
6
+ var isConnected: Bool { get }
7
+ var canSendWriteWithoutResponse: Bool { get }
8
+ var services: [CBService] { get }
9
+ func attach(_ delegate: CBPeripheralDelegate)
10
+ func maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int
11
+ func connect()
12
+ func cancelConnection()
13
+ func discoverServices()
14
+ func discoverCharacteristics(for service: CBService)
15
+ func write(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType)
16
+ }
17
+
18
+ final class EscPosCBPeripheralDriver: EscPosPeripheralDriver {
19
+ let peripheral: CBPeripheral
20
+ private unowned let central: EscPosBluetoothCentral
21
+
22
+ init(peripheral: CBPeripheral, central: EscPosBluetoothCentral) {
23
+ self.peripheral = peripheral
24
+ self.central = central
25
+ }
26
+
27
+ var isAvailable: Bool { central.isPoweredOn }
28
+
29
+ var isConnected: Bool { peripheral.state == .connected }
30
+
31
+ var canSendWriteWithoutResponse: Bool { peripheral.canSendWriteWithoutResponse }
32
+
33
+ var services: [CBService] { peripheral.services ?? [] }
34
+
35
+ func attach(_ delegate: CBPeripheralDelegate) {
36
+ peripheral.delegate = delegate
37
+ }
38
+
39
+ func maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int {
40
+ peripheral.maximumWriteValueLength(for: type)
41
+ }
42
+
43
+ func connect() {
44
+ central.startConnecting(peripheral)
45
+ }
46
+
47
+ func cancelConnection() {
48
+ central.cancelConnection(peripheral)
49
+ }
50
+
51
+ func discoverServices() {
52
+ peripheral.discoverServices(nil)
53
+ }
54
+
55
+ func discoverCharacteristics(for service: CBService) {
56
+ peripheral.discoverCharacteristics(nil, for: service)
57
+ }
58
+
59
+ func write(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType) {
60
+ peripheral.writeValue(data, for: characteristic, type: type)
61
+ }
62
+ }
63
+
64
+ final class EscPosCoreBluetoothLink: NSObject, EscPosBluetoothLink, CBPeripheralDelegate {
65
+ weak var events: EscPosBluetoothLinkEvents?
66
+ let driver: EscPosPeripheralDriver
67
+ private var session: Int?
68
+ private var pendingConnect: Int?
69
+ private var closing: Int?
70
+ private var queuedConnect: Int?
71
+ private var pendingDiscovery: Int?
72
+ private var pendingWrites: [Int] = []
73
+ private var characteristic: CBCharacteristic?
74
+ private var pendingCharacteristicDiscoveries = 0
75
+
76
+ init(driver: EscPosPeripheralDriver) {
77
+ self.driver = driver
78
+ super.init()
79
+ }
80
+
81
+ var isCancelling: Bool { closing != nil }
82
+
83
+ var isConnected: Bool { closing == nil && driver.isConnected }
84
+
85
+ var canSendWriteWithoutResponse: Bool { driver.canSendWriteWithoutResponse }
86
+
87
+ func maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int {
88
+ driver.maximumWriteValueLength(for: type)
89
+ }
90
+
91
+ func connect(attempt: Int) {
92
+ resetSession()
93
+ if closing != nil {
94
+ queuedConnect = attempt
95
+ return
96
+ }
97
+ startConnecting(attempt)
98
+ }
99
+
100
+ func adopt(attempt: Int) {
101
+ resetSession()
102
+ if closing != nil {
103
+ queuedConnect = attempt
104
+ return
105
+ }
106
+ session = attempt
107
+ driver.attach(self)
108
+ }
109
+
110
+ func cancel() {
111
+ let open = session ?? pendingConnect
112
+ queuedConnect = nil
113
+ resetSession()
114
+ pendingConnect = nil
115
+ guard let open = open else { return }
116
+ closing = open
117
+ driver.cancelConnection()
118
+ }
119
+
120
+ func discoverPrinterCharacteristic(attempt: Int) {
121
+ guard session == attempt else { return }
122
+ characteristic = nil
123
+ pendingCharacteristicDiscoveries = 0
124
+ pendingDiscovery = attempt
125
+ driver.discoverServices()
126
+ }
127
+
128
+ func write(_ data: Data, type: CBCharacteristicWriteType, token: Int) {
129
+ guard session != nil, let characteristic = characteristic else {
130
+ if type == .withResponse {
131
+ events?.linkDidWrite(token: token, error: EscPosError.connection("Unable to send data to device."))
132
+ }
133
+ return
134
+ }
135
+ if type == .withResponse {
136
+ pendingWrites.append(token)
137
+ }
138
+ driver.write(data, for: characteristic, type: type)
139
+ }
140
+
141
+ func handleConnected() {
142
+ if closing != nil {
143
+ return
144
+ }
145
+ guard let attempt = pendingConnect else { return }
146
+ pendingConnect = nil
147
+ session = attempt
148
+ events?.linkDidConnect(attempt: attempt)
149
+ }
150
+
151
+ func handleFailedToConnect(_ error: Error?) {
152
+ if let ended = closing {
153
+ finishCancellation { $0.linkDidFailToConnect(attempt: ended, error: error) }
154
+ return
155
+ }
156
+ guard let attempt = pendingConnect else { return }
157
+ pendingConnect = nil
158
+ events?.linkDidFailToConnect(attempt: attempt, error: error)
159
+ }
160
+
161
+ func handleDisconnected(_ error: Error?) {
162
+ if let ended = closing {
163
+ finishCancellation { $0.linkDidDisconnect(attempt: ended, error: error) }
164
+ return
165
+ }
166
+ guard let live = session else { return }
167
+ resetSession()
168
+ events?.linkDidDisconnect(attempt: live, error: error)
169
+ }
170
+
171
+ func handleUnavailable() {
172
+ let cancelled = closing
173
+ let live = session
174
+ let pending = pendingConnect
175
+ let queued = queuedConnect
176
+ resetSession()
177
+ closing = nil
178
+ pendingConnect = nil
179
+ queuedConnect = nil
180
+ guard let events = events else { return }
181
+ if let cancelled = cancelled {
182
+ events.linkDidDisconnect(attempt: cancelled, error: nil)
183
+ }
184
+ if let live = live {
185
+ events.linkDidDisconnect(attempt: live, error: nil)
186
+ }
187
+ if let pending = pending {
188
+ events.linkDidFailToConnect(attempt: pending, error: nil)
189
+ }
190
+ if let queued = queued {
191
+ events.linkDidFailToConnect(attempt: queued, error: nil)
192
+ }
193
+ }
194
+
195
+ func handleServicesDiscovered(error: Error?) {
196
+ guard session != nil, let attempt = pendingDiscovery else { return }
197
+ if let error = error {
198
+ pendingDiscovery = nil
199
+ events?.linkDidDiscover(attempt: attempt, writeType: nil, error: error)
200
+ return
201
+ }
202
+ let services = driver.services
203
+ if services.isEmpty {
204
+ finishDiscovery()
205
+ return
206
+ }
207
+ pendingCharacteristicDiscoveries = services.count
208
+ for service in services {
209
+ driver.discoverCharacteristics(for: service)
210
+ }
211
+ }
212
+
213
+ func handleCharacteristicsDiscovered() {
214
+ guard session != nil, pendingDiscovery != nil else { return }
215
+ pendingCharacteristicDiscoveries -= 1
216
+ if pendingCharacteristicDiscoveries == 0 {
217
+ finishDiscovery()
218
+ }
219
+ }
220
+
221
+ func handleWriteResponse(error: Error?) {
222
+ guard session != nil, !pendingWrites.isEmpty else { return }
223
+ events?.linkDidWrite(token: pendingWrites.removeFirst(), error: error)
224
+ }
225
+
226
+ func handleReadyToSend() {
227
+ guard let attempt = session else { return }
228
+ events?.linkIsReadyToSend(attempt: attempt)
229
+ }
230
+
231
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
232
+ handleServicesDiscovered(error: error)
233
+ }
234
+
235
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
236
+ handleCharacteristicsDiscovered()
237
+ }
238
+
239
+ func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
240
+ handleWriteResponse(error: error)
241
+ }
242
+
243
+ func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
244
+ handleReadyToSend()
245
+ }
246
+
247
+ private func startConnecting(_ attempt: Int) {
248
+ guard driver.isAvailable else {
249
+ events?.linkDidFailToConnect(attempt: attempt, error: nil)
250
+ return
251
+ }
252
+ pendingConnect = attempt
253
+ driver.attach(self)
254
+ driver.connect()
255
+ }
256
+
257
+ private func finishCancellation(_ report: (EscPosBluetoothLinkEvents) -> Void) {
258
+ closing = nil
259
+ let next = queuedConnect
260
+ queuedConnect = nil
261
+ if let events = events {
262
+ report(events)
263
+ }
264
+ if let next = next {
265
+ startConnecting(next)
266
+ }
267
+ }
268
+
269
+ private func finishDiscovery() {
270
+ guard let attempt = pendingDiscovery else { return }
271
+ pendingDiscovery = nil
272
+ if let (found, type) = EscPosBluetoothRules.selectCharacteristic(in: driver.services) {
273
+ characteristic = found
274
+ events?.linkDidDiscover(attempt: attempt, writeType: type, error: nil)
275
+ } else {
276
+ events?.linkDidDiscover(attempt: attempt, writeType: nil, error: nil)
277
+ }
278
+ }
279
+
280
+ private func resetSession() {
281
+ session = nil
282
+ pendingDiscovery = nil
283
+ pendingWrites = []
284
+ characteristic = nil
285
+ pendingCharacteristicDiscoveries = 0
286
+ }
287
+ }
@@ -0,0 +1,47 @@
1
+ import Foundation
2
+
3
+ class EscPosDeviceConnection: NSObject {
4
+ private(set) var data: [UInt8] = []
5
+ var pacesSends = true
6
+
7
+ var isConnected: Bool { false }
8
+
9
+ @discardableResult
10
+ func connect() throws -> EscPosDeviceConnection { self }
11
+
12
+ @discardableResult
13
+ func disconnect() -> EscPosDeviceConnection {
14
+ data = []
15
+ return self
16
+ }
17
+
18
+ func transmit(_ bytes: [UInt8]) throws {}
19
+
20
+ func write(_ bytes: [UInt8]) {
21
+ data.append(contentsOf: bytes)
22
+ }
23
+
24
+ func send(_ addWaitingTime: Int = 0) throws {
25
+ guard isConnected else {
26
+ data = []
27
+ throw EscPosError.connection("Unable to send data to device.")
28
+ }
29
+ let payload = data
30
+ do {
31
+ try transmit(payload)
32
+ } catch {
33
+ data = []
34
+ disconnect()
35
+ throw error
36
+ }
37
+ let waitingTime = addWaitingTime + payload.count / 16
38
+ data = []
39
+ if pacesSends && waitingTime > 0 {
40
+ Thread.sleep(forTimeInterval: Double(waitingTime) / 1000.0)
41
+ }
42
+ }
43
+
44
+ func clearBuffer() {
45
+ data = []
46
+ }
47
+ }