@risleylima/escpos 0.0.13 → 0.1.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.
@@ -1,12 +1,19 @@
1
1
  'use strict';
2
2
  const { SerialPort } = require('serialport');
3
- const EventEmitter = require('events');
4
3
  const Adapter = require('../adapter');
5
4
 
6
5
  const debug = require('debug')('escpos:serial-adapter');
7
6
 
8
7
  const scope = {
9
8
  port: null,
9
+ /**
10
+ * Verify that a serial port exists
11
+ * @private
12
+ * @async
13
+ * @param {String} port - Serial port path to verify
14
+ * @returns {Promise<String>} Verified port path
15
+ * @throws {Error} If port does not exist
16
+ */
10
17
  verifyPort: async (port) => {
11
18
  let ports = await SerialPort.list();
12
19
  if (!ports.find((i) => i.path === port)) {
@@ -16,110 +23,151 @@ const scope = {
16
23
  }
17
24
  }
18
25
 
19
- const Serial = new EventEmitter();
20
-
21
- Serial.connect = (port, options) => {
22
- return new Promise((resolve, reject) => {
23
- let connectListener = () => {
24
- Serial.removeListener('close', connectListener);
25
- scope.verifyPort(port).catch(e => reject(e)).then((portVerified) => {
26
- if (portVerified) {
27
- scope.port = new SerialPort(Object.assign(options || {}, { path: portVerified, autoOpen: true }), (err) => {
28
- if (err) {
29
- debug('Error Opening the Selected Port: ', err);
30
- if (scope.port) {
31
- scope.port.close(async () => {
32
- await connectListener();
33
- })
34
- }
35
- }
36
-
37
- let clearPort = () => {
38
- Serial.emit('disconnect', scope.port);
39
- scope.port.removeListener('close', clearPort);
40
- scope.port = null;
41
- }
42
-
43
- scope.port.on('close', clearPort);
44
-
45
- debug('Device Connected and Open!');
46
- Serial.emit('connect', scope.port);
47
-
48
- resolve(true);
49
- });
50
- }
51
- });
26
+ // Create Adapter instance first, so it's the same object used internally and exported
27
+ const Serial = new Adapter();
28
+
29
+ /**
30
+ * Connect to a serial port printer
31
+ * @async
32
+ * @param {String} port - Serial port path (e.g., '/dev/ttyUSB0' or 'COM3')
33
+ * @param {Object} [options] - Serial port options (baudRate, dataBits, etc.)
34
+ * @returns {Promise<Boolean>} True if connection successful
35
+ * @throws {Error} If port does not exist or cannot be opened
36
+ * @fires Serial#connect
37
+ * @fires Serial#close (if reconnecting)
38
+ */
39
+ Serial.connect = async (port, options) => {
40
+ // Close existing connection if any
41
+ if (scope.port) {
42
+ try {
43
+ await scope.port.close();
44
+ } catch (e) {
45
+ debug('Error closing existing port: ', e);
52
46
  }
47
+ Serial.emit('close');
48
+ }
53
49
 
54
- Serial.on('close', connectListener);
50
+ // Verify port exists
51
+ const portVerified = await scope.verifyPort(port);
52
+
53
+ // Create SerialPort instance (v13: no callback, autoOpen: false)
54
+ scope.port = new SerialPort(Object.assign(options || {}, {
55
+ path: portVerified,
56
+ autoOpen: false
57
+ }));
58
+
59
+ // Handle errors via events
60
+ scope.port.on('error', (err) => {
61
+ debug('Error on Serial Port: ', err);
62
+ });
55
63
 
64
+ // Handle close event
65
+ let clearPort = () => {
66
+ Serial.emit('disconnect', scope.port);
67
+ scope.port.removeListener('close', clearPort);
68
+ scope.port = null;
69
+ };
70
+ scope.port.on('close', clearPort);
71
+
72
+ // Open the port manually (v13: returns Promise)
73
+ try {
74
+ await scope.port.open();
75
+ debug('Device Connected and Open!');
76
+ Serial.emit('connect', scope.port);
77
+ return true;
78
+ } catch (err) {
79
+ debug('Error Opening the Selected Port: ', err);
56
80
  if (scope.port) {
57
- scope.port.close((e) => {
58
- Serial.emit('close');
59
- });
60
- } else {
61
- Serial.emit('close');
81
+ try {
82
+ await scope.port.close();
83
+ } catch (closeErr) {
84
+ debug('Error closing port after open failure: ', closeErr);
85
+ }
62
86
  }
63
- });
87
+ throw err;
88
+ }
64
89
  }
65
90
 
66
- Serial.open = () => {
67
- return new Promise((resolve, reject) => {
68
- if (!scope.port.isOpen) {
69
- scope.port.open((err) => {
70
- if (err)
71
- reject(err)
72
- debug('Device Opened!');
73
- resolve(scope.port.isOpen);
74
- });
75
- } else {
76
- debug('Device is already Opened!');
77
- resolve(true)
91
+ /**
92
+ * Open the serial port if it's closed
93
+ * @async
94
+ * @returns {Promise<Boolean>} True if port is open (or was already open)
95
+ * @throws {Error} If port cannot be opened
96
+ */
97
+ Serial.open = async () => {
98
+ if (!scope.port.isOpen) {
99
+ try {
100
+ await scope.port.open();
101
+ debug('Device Opened!');
102
+ return scope.port.isOpen;
103
+ } catch (err) {
104
+ throw err;
78
105
  }
79
- });
106
+ } else {
107
+ debug('Device is already Opened!');
108
+ return true;
109
+ }
80
110
  };
81
111
 
82
- Serial.write = (data) => {
83
- return new Promise((resolve, reject) => {
84
- scope.port.write(data, (e) => {
85
- if (e)
86
- reject(e);
87
- scope.port.drain(() => {
88
- resolve(true);
89
- })
90
- });
91
- });
112
+ /**
113
+ * Write data to the serial port
114
+ * @async
115
+ * @param {Buffer} data - Data buffer to send to printer
116
+ * @returns {Promise<Boolean>} True if write successful
117
+ * @throws {Error} If write fails
118
+ */
119
+ Serial.write = async (data) => {
120
+ try {
121
+ await scope.port.write(data);
122
+ await scope.port.drain();
123
+ return true;
124
+ } catch (e) {
125
+ throw e;
126
+ }
92
127
  };
93
128
 
94
- Serial.close = (timeout) => {
129
+ /**
130
+ * Close the serial port connection
131
+ * @async
132
+ * @param {Number} [timeout] - Timeout in milliseconds before closing (default: 50ms)
133
+ * @returns {Promise<Boolean>} True if port closed successfully
134
+ * @fires Serial#close
135
+ */
136
+ Serial.close = async (timeout) => {
95
137
  let time = Number(timeout);
96
138
  if (Number.isNaN(time)) {
97
- time = 50
139
+ time = 50;
98
140
  }
99
141
 
100
- return new Promise((resolve, reject) => {
101
- scope.port.flush((e) => {
102
- setTimeout(() => {
103
- if (e)
104
- debug('Error while Flush Device: ', e);
105
- scope.port.drain(() => {
106
- scope.port.close((eClosing) => {
107
- if (eClosing)
108
- debug('Error while Close Device: ', eClosing);
109
- Serial.emit('close');
110
- resolve(true);
111
- });
112
- })
113
- }, time);
114
- });
115
- });
116
-
142
+ try {
143
+ await scope.port.flush();
144
+ await new Promise(resolve => setTimeout(resolve, time));
145
+ await scope.port.drain();
146
+ await scope.port.close();
147
+ // Emit event synchronously - this ensures listeners are called immediately
148
+ Serial.emit('close');
149
+ return true;
150
+ } catch (e) {
151
+ debug('Error while closing device: ', e);
152
+ // Emit event synchronously even on error - this ensures listeners are called immediately
153
+ Serial.emit('close');
154
+ return true; // Still resolve to allow cleanup
155
+ }
117
156
  }
118
157
 
158
+ /**
159
+ * Disconnect from the serial port (calls close internally)
160
+ * @param {Number} [timeout] - Timeout in milliseconds before closing (default: 50ms)
161
+ * @returns {Promise<Boolean>} True if disconnection successful
162
+ */
119
163
  Serial.disconnect = (timeout) => {
120
164
  return Serial.close(timeout);
121
165
  }
122
166
 
167
+ /**
168
+ * Read data from the serial port
169
+ * @returns {Promise<Buffer>} Data received from the port
170
+ */
123
171
  Serial.read = () => {
124
172
  return new Promise((resolve, reject) => {
125
173
  let dataHandler = (data) => {
@@ -130,4 +178,5 @@ Serial.read = () => {
130
178
  });
131
179
  };
132
180
 
133
- module.exports = new Adapter(Serial);
181
+ // Serial is already an Adapter instance, so export it directly
182
+ module.exports = Serial;
@@ -1,5 +1,4 @@
1
1
  'use strict'
2
- const { EventEmitter } = require('stream');
3
2
  const Adapter = require('../adapter');
4
3
  const usb = require('usb');
5
4
  const os = require('os');
@@ -23,16 +22,28 @@ const IFACE_CLASS = {
23
22
  HUB: 0x09
24
23
  };
25
24
 
26
- const USB = new EventEmitter();
25
+ // Create Adapter instance first, so it's the same object used internally and exported
26
+ const USB = new Adapter();
27
27
 
28
+ /**
29
+ * List all available USB printer devices
30
+ * @async
31
+ * @returns {Promise<Array>} Array of USB printer devices with manufacturer and product information
32
+ */
28
33
  USB.listUSB = async () => {
29
34
  const devices = usb.getDeviceList().filter((device) => {
30
35
  try {
31
- return device.configDescriptor.interfaces.filter((iface) => {
32
- return iface.filter((conf) => {
33
- return conf.bInterfaceClass === IFACE_CLASS.PRINTER;
34
- }).length;
35
- }).length;
36
+ // In v2, we need to check configDescriptor for interface class
37
+ const configDescriptor = device.configDescriptor;
38
+ if (!configDescriptor || !configDescriptor.interfaces) {
39
+ return false;
40
+ }
41
+ // configDescriptor.interfaces is an array of arrays (alternate settings)
42
+ return configDescriptor.interfaces.some((ifaceArray) => {
43
+ return ifaceArray.some((iface) => {
44
+ return iface.bInterfaceClass === IFACE_CLASS.PRINTER;
45
+ });
46
+ });
36
47
  } catch (e) {
37
48
  debug('Error while get device info: ', e);
38
49
  return false;
@@ -41,21 +52,30 @@ USB.listUSB = async () => {
41
52
 
42
53
  let retorno = [];
43
54
 
44
- const getDescriptor = (device, type) => new Promise((resolve, reject) => {
55
+ /**
56
+ * Get string descriptor from USB device
57
+ * @private
58
+ * @async
59
+ * @param {Object} device - USB device object
60
+ * @param {Number} type - Descriptor type index
61
+ * @returns {Promise<String|Boolean>} Descriptor string or false on error
62
+ */
63
+ const getDescriptor = async (device, type) => {
45
64
  try {
46
- device.open();
47
- device.getStringDescriptor(type, (err, data) => {
48
- if (err) {
49
- reject(new Error('Error while read selected Description: ', e));
50
- }
51
- device.close();
52
- resolve(data);
53
- });
65
+ await device.open();
66
+ const data = await device.getStringDescriptor(type);
67
+ await device.close();
68
+ return data;
54
69
  } catch (e) {
55
- debug(new Error('Error while read device description: ', e));
56
- resolve(false);
70
+ debug('Error while read device description: ', e);
71
+ try {
72
+ await device.close();
73
+ } catch (closeErr) {
74
+ // Ignore close errors
75
+ }
76
+ return false;
57
77
  }
58
- });
78
+ };
59
79
 
60
80
  for (let device of devices) {
61
81
  device.manufacturer = await getDescriptor(device, device.deviceDescriptor.iManufacturer);
@@ -68,6 +88,15 @@ USB.listUSB = async () => {
68
88
  return retorno;
69
89
  };
70
90
 
91
+ /**
92
+ * Connect to a USB printer device
93
+ * @async
94
+ * @param {Number} [vid] - Vendor ID (optional, if not provided, uses first available printer)
95
+ * @param {Number} [pid] - Product ID (optional, if not provided, uses first available printer)
96
+ * @returns {Promise<Boolean>} True if connection successful
97
+ * @throws {Error} If printer cannot be found
98
+ * @fires USB#connect
99
+ */
71
100
  USB.connect = async (vid, pid) => {
72
101
  scope.device = null;
73
102
  scope.endpoint = null;
@@ -96,27 +125,52 @@ USB.connect = async (vid, pid) => {
96
125
  return true;
97
126
  };
98
127
 
128
+ /**
129
+ * Open the USB device and claim the printer interface
130
+ * @async
131
+ * @returns {Promise<Boolean>} True if device opened successfully
132
+ * @throws {Error} If interfaces cannot be accessed or endpoint not found
133
+ * @fires USB#connect
134
+ */
99
135
  USB.open = async () => {
100
- scope.device.open();
101
- for (let iface of scope.device.interfaces) {
136
+ await scope.device.open();
137
+
138
+ // In v2, device.interfaces is a direct array of Interface objects
139
+ // We need to iterate through all interfaces to find the printer interface
140
+ const interfaces = scope.device.interfaces;
141
+ if (!interfaces || interfaces.length === 0) {
142
+ throw new Error('Cannot access device interfaces');
143
+ }
144
+
145
+ for (let interfaceObj of interfaces) {
102
146
  if (scope.endpoint) {
103
147
  break;
104
148
  }
105
149
 
150
+ // Check if this interface is a printer interface
151
+ const descriptor = interfaceObj.descriptor;
152
+ if (descriptor && descriptor.bInterfaceClass !== IFACE_CLASS.PRINTER) {
153
+ continue;
154
+ }
155
+
156
+ // Claim interface (required on all platforms)
106
157
  if ("win32" !== os.platform()) {
107
- if (iface.isKernelDriverActive()) {
158
+ // On Linux/macOS, detach kernel driver first if active
159
+ if (interfaceObj.isKernelDriverActive()) {
108
160
  try {
109
- iface.detachKernelDriver();
161
+ await interfaceObj.detachKernelDriver();
110
162
  } catch (e) {
111
- throw new Error("[ERROR] Could not detatch kernel driver: %s", e);
163
+ throw new Error(`[ERROR] Could not detach kernel driver: ${e.message}`);
112
164
  }
113
165
  }
114
- iface.claim(); // must be called before using any endpoints of this interface.
115
166
  }
116
- for (let endpoint of iface.endpoints) {
167
+ // Claim interface (required on all platforms before using endpoints)
168
+ await interfaceObj.claim();
169
+
170
+ for (let endpoint of interfaceObj.endpoints) {
117
171
  if (scope.endpoint) {
118
172
  break;
119
- } else if (endpoint.direction == 'out') {
173
+ } else if (endpoint.direction === 'out') {
120
174
  scope.endpoint = endpoint;
121
175
  USB.emit('connect', scope.device);
122
176
  debug('Device Opened!');
@@ -130,40 +184,100 @@ USB.open = async () => {
130
184
  return true;
131
185
  };
132
186
 
187
+ /**
188
+ * Close the USB device connection and release interfaces
189
+ * @async
190
+ * @returns {Promise<Boolean>} True if device closed successfully
191
+ * @fires USB#close
192
+ */
133
193
  USB.close = async () => {
194
+ const device = scope.device; // Save device reference before cleanup
195
+
134
196
  if (scope.device) {
135
- await scope.device.close();
197
+ try {
198
+ // Release interfaces before closing
199
+ // Only release the interface we actually claimed
200
+ if (scope.endpoint && scope.endpoint.interface) {
201
+ const interfaceObj = scope.endpoint.interface;
202
+ try {
203
+ // Check if interface is still valid and was claimed
204
+ if (interfaceObj && typeof interfaceObj.release === 'function') {
205
+ await interfaceObj.release();
206
+ }
207
+ } catch (e) {
208
+ debug('Error releasing interface: ', e);
209
+ }
210
+ } else {
211
+ // Fallback: try to release all interfaces
212
+ const interfaces = scope.device.interfaces;
213
+ if (interfaces && interfaces.length > 0) {
214
+ for (let interfaceObj of interfaces) {
215
+ try {
216
+ // Only release if we claimed it (kernel driver was detached)
217
+ if (interfaceObj && typeof interfaceObj.release === 'function' && !interfaceObj.isKernelDriverActive()) {
218
+ await interfaceObj.release();
219
+ }
220
+ } catch (e) {
221
+ debug('Error releasing interface: ', e);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ await scope.device.close();
227
+ } catch (e) {
228
+ debug('Error closing device: ', e);
229
+ }
136
230
  }
137
231
 
138
- USB.emit('close', scope.device);
139
- debug('Device Closed!');
232
+ // Clear endpoint before emitting event
140
233
  scope.endpoint = null;
234
+
235
+ // Emit event synchronously - this ensures listeners are called immediately
236
+ USB.emit('close', device);
237
+ debug('Device Closed!');
141
238
 
142
239
  return true;
143
240
  }
144
241
 
242
+ /**
243
+ * Disconnect from the USB device (calls close internally)
244
+ * @async
245
+ * @returns {Promise<Boolean>} True if disconnection successful
246
+ * @fires USB#disconnect
247
+ */
145
248
  USB.disconnect = async () => {
249
+ const device = scope.device; // Save device reference before cleanup
250
+
146
251
  if (scope.device) {
147
252
  await USB.close().catch(e => { debug(e); return true });
148
253
  }
149
- USB.emit('disconnect', scope.device);
150
- debug('Device Disconnected!');
254
+
255
+ // Clear scope before emitting event
151
256
  scope.endpoint = null;
152
257
  scope.device = null;
258
+
259
+ // Emit event synchronously - this ensures listeners are called immediately
260
+ USB.emit('disconnect', device);
261
+ debug('Device Disconnected!');
153
262
 
154
263
  return true;
155
264
  }
156
265
 
157
-
158
- USB.write = (data) => {
159
- return new Promise((resolve, reject) => {
160
- scope.endpoint.transfer(data, (e) => {
161
- if (e) {
162
- reject(e);
163
- }
164
- resolve(true);
165
- });
166
- })
266
+ /**
267
+ * Write data to the USB printer
268
+ * @async
269
+ * @param {Buffer} data - Data buffer to send to printer
270
+ * @returns {Promise<Boolean>} True if write successful
271
+ * @throws {Error} If write fails
272
+ */
273
+ USB.write = async (data) => {
274
+ try {
275
+ await scope.endpoint.transfer(data);
276
+ return true;
277
+ } catch (e) {
278
+ throw e;
279
+ }
167
280
  }
168
281
 
169
- module.exports = new Adapter(USB);
282
+ // USB is already an Adapter instance, so export it directly
283
+ module.exports = USB;
@@ -0,0 +1,67 @@
1
+ # Tests - EscPos
2
+
3
+ This directory contains the test suite for the EscPos library.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ tests/
9
+ ├── unit/ # Isolated unit tests
10
+ │ ├── printer/ # Printer class tests
11
+ │ ├── adapters/ # Adapter tests (USB/Serial)
12
+ │ ├── image/ # Image processing tests
13
+ │ └── utils/ # Utility functions tests
14
+ └── integration/ # Integration tests
15
+ └── printer-flow/ # Complete flow tests
16
+ ```
17
+
18
+ ## Running Tests
19
+
20
+ ```bash
21
+ # Run all tests
22
+ npm test
23
+
24
+ # Run in watch mode
25
+ npm run test:watch
26
+
27
+ # Run with coverage
28
+ npm run test:coverage
29
+ ```
30
+
31
+ ## Coverage
32
+
33
+ The tests cover:
34
+ - ✅ Buffer operations
35
+ - ✅ Text formatting
36
+ - ✅ ESC/POS commands
37
+ - ✅ Image processing
38
+ - ✅ Barcodes
39
+ - ✅ Adapters (with mocks)
40
+ - ✅ Complete print flows
41
+
42
+ ## Mocks
43
+
44
+ The tests use mocks for:
45
+ - **USB Adapter**: Mock of `usb` module
46
+ - **Serial Adapter**: Mock of `serialport` module
47
+ - **Image**: Mock of `get-pixels` module
48
+
49
+ This allows running tests without requiring real hardware.
50
+
51
+ ## Adding New Tests
52
+
53
+ 1. Create the test file in `tests/unit/` or `tests/integration/`
54
+ 2. Use the `*.test.js` convention
55
+ 3. Follow Jest's describe/it pattern
56
+ 4. Use mocks for external dependencies
57
+
58
+ ## Example
59
+
60
+ ```javascript
61
+ describe('MyFeature', () => {
62
+ it('should do something', () => {
63
+ // test here
64
+ });
65
+ });
66
+ ```
67
+