diff --git a/.changeset/ninety-needles-hug.md b/.changeset/ninety-needles-hug.md new file mode 100644 index 00000000..ccef607d --- /dev/null +++ b/.changeset/ninety-needles-hug.md @@ -0,0 +1,6 @@ +--- +"@node-escpos/core": minor +"@node-escpos/network-adapter": minor +--- + +Improvement for core and network adapter diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 986edd3a..86eac674 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -905,15 +905,26 @@ export class Printer extends EventEmitter { * @return {Promise} promise returning given status */ getStatus(StatusClass: StatusClassConstructor): Promise { - return new Promise((resolve) => { - this.adapter.read((data) => { - const byte = data.readInt8(0); - resolve(new StatusClass(byte)); + return new Promise((resolve, reject) => { + this.adapter.read((data: Buffer) => { + try { + if(data.length === 0) { + return reject(new Error("Get status timeout")); + } + const byte = data.readInt8(0); + resolve(new StatusClass(byte)); + } catch (err) { + if (typeof err === "string") { + console.error(err); + reject(new Error(err)); + } else { + console.error(err); + reject(err); + } + } }); - StatusClass.commands().forEach((c) => { - this.buffer.write(c); - }); + this.adapter.write(StatusClass.commands().join('')); }); } @@ -922,26 +933,25 @@ export class Printer extends EventEmitter { * @return {Promise} */ getStatuses(): Promise { - return new Promise((resolve, reject) => { - this.adapter.read((data) => { - const buffer: number[] = []; - for (let i = 0; i < data.byteLength; i++) buffer.push(data.readInt8(i)); - if (buffer.length < 4) return reject(); - - const statuses = [ - new PrinterStatus(buffer[0]), - new RollPaperSensorStatus(buffer[1]), - new OfflineCauseStatus(buffer[2]), - new ErrorCauseStatus(buffer[3]), - ]; - resolve(statuses); - }); - - [PrinterStatus, RollPaperSensorStatus, OfflineCauseStatus, ErrorCauseStatus].forEach((statusClass) => { - statusClass.commands().forEach((command) => { - this.adapter.write(command); - }); - }); + return new Promise (async (resolve, reject) => { + const results:DeviceStatus[] = []; + + try { + results.push(await this.getStatus(PrinterStatus)); + results.push(await this.getStatus(RollPaperSensorStatus)); + results.push(await this.getStatus(OfflineCauseStatus)); + results.push(await this.getStatus(ErrorCauseStatus)); + + resolve(results); + } catch (err) { + if (typeof err === "string") { + console.error(err); + reject(new Error(err)); + } else { + console.error(err); + reject(err); + } + } }); } @@ -1091,3 +1101,17 @@ export class Printer extends EventEmitter { export default Printer; export { default as Image } from "./image"; export const command = _; +export { + ErrorCauseStatus, + OfflineCauseStatus, + PrinterStatus, + RollPaperSensorStatus, +} from "./statuses"; +export type { + DeviceStatus, + StatusClassConstructor, + StatusJSON, + StatusJSONElement, + StatusJSONElementSingle, + StatusJSONElementMultiple, +} from "./statuses"; \ No newline at end of file diff --git a/packages/core/src/statuses.ts b/packages/core/src/statuses.ts index 5265ba27..c109f013 100644 --- a/packages/core/src/statuses.ts +++ b/packages/core/src/statuses.ts @@ -6,23 +6,23 @@ enum Status { Error = "error", } -interface StatusJSONElementSingle { +export interface StatusJSONElementSingle { bit: number value: 0 | 1 label: string status: Status } -interface StatusJSONElementMultiple { +export interface StatusJSONElementMultiple { bit: string value: string label: string status: Status } -type StatusJSONElement = StatusJSONElementSingle | StatusJSONElementMultiple; +export type StatusJSONElement = StatusJSONElementSingle | StatusJSONElementMultiple; -interface StatusJSON { +export interface StatusJSON { className: T byte: number bits: string diff --git a/packages/core/test/index.test.ts b/packages/core/test/index.test.ts index cff2c287..082faa9f 100644 --- a/packages/core/test/index.test.ts +++ b/packages/core/test/index.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, it } from 'vitest' -import { splitForCode128, genCode128forXprinter } from '../src/utils'; +import { describe, expect, it, vi } from 'vitest' +import { splitForCode128, genCode128forXprinter } from '../src/utils'; describe('should', () => { + it('exported', () => { expect(1).toEqual(1) }); diff --git a/packages/core/test/status.test.ts b/packages/core/test/status.test.ts new file mode 100644 index 00000000..2c46a057 --- /dev/null +++ b/packages/core/test/status.test.ts @@ -0,0 +1,154 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { Adapter } from '@node-escpos/adapter'; +import { ErrorCauseStatus, OfflineCauseStatus, Printer, PrinterStatus, RollPaperSensorStatus } from '..//src'; + +class MockAdapter extends Adapter<[]> { + open = vi.fn() + write = vi.fn() + close = vi.fn() + read = vi.fn() +} + +describe('should', () => { + const adapter = new MockAdapter(); + const dataWrote = vi.fn(); + + // flag to return empty data on getting OnOfflineCauseStatus + let returnEmptyDataOnOfflineCauseStatus = false; + + beforeEach(() => { + let readResolver: (value: string|PromiseLike) => void; + let readRejecter: (reason?: any) => void; + returnEmptyDataOnOfflineCauseStatus = false; + + adapter.read.mockImplementation(async (callback?: (data: Buffer) => void) => { + // promise to wait for data writing + const promise = new Promise((resolve, reject) => { + // save resolve and reject + readResolver = resolve; + readRejecter = reject; + }); + const result = await promise; + if (callback) callback(Buffer.from(result)); + }); + + adapter.write.mockImplementation((data: string | Buffer, callback?: (error: Error | null) => void) => { + const normalizedData = data.toString(); + dataWrote(normalizedData) + + // return different data depending on the command received + switch(normalizedData) { + case PrinterStatus.commands().join(''): + readResolver('\x16'); + break; + case RollPaperSensorStatus.commands().join(''): + readResolver('\x17'); + break; + case OfflineCauseStatus.commands().join(''): + if (returnEmptyDataOnOfflineCauseStatus) { + readResolver(""); + } else { + readResolver('\x18'); + } + break; + case ErrorCauseStatus.commands().join(''): + readResolver('\x19'); + break; + default: + readRejecter(new Error("Unknown data wrote")); + break; + } + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('adapter receive correct data from getStatus', async () => { + const printer = new Printer(adapter, {}) + await printer.getStatus(PrinterStatus); + + expect(adapter.write).toHaveBeenCalledOnce(); + + expect(dataWrote).toBeCalledWith(PrinterStatus.commands().join('')); + }) + + it('data return from adapter can create correct status with correct byte', async () => { + const printer = new Printer(adapter, {}) + const printerStatus = await printer.getStatus(PrinterStatus); + + expect(adapter.read).toHaveBeenCalledOnce(); + + expect(printerStatus.byte).toEqual(22); + }) + + it('getStatuses return all statues with correct byte', async () => { + const printer = new Printer(adapter, {}) + const printerStatuses = await printer.getStatuses(); + + expect(adapter.write).toHaveBeenCalledTimes(4); + expect(adapter.read).toHaveBeenCalledTimes(4); + expect(printerStatuses.length).toEqual(4); + + printerStatuses + .map((status) => status.toJSON()) + .forEach((json) => { + switch(json.className) { + case PrinterStatus.name: + expect(json.byte).toEqual(22); + break; + case RollPaperSensorStatus.name: + expect(json.byte).toEqual(23); + break; + case OfflineCauseStatus.name: + expect(json.byte).toEqual(24); + break; + case ErrorCauseStatus.name: + expect(json.byte).toEqual(25); + break; + default: + expect(false, "unexpected DeviceStatus class:" + json.className).toBeTruthy(); + break; + } + }) + }) + + it('getStatus throw error when receive empty buffer from adapter', async () => { + returnEmptyDataOnOfflineCauseStatus = true; + let receivedError: Error | null = null; + + const printer = new Printer(adapter, {}) + try { + await printer.getStatus(OfflineCauseStatus); + } catch (err) { + if (err instanceof Error) { + receivedError = err; + } + } + + expect(adapter.write).toHaveBeenCalledOnce(); + expect(adapter.read).toHaveBeenCalledOnce(); + expect(receivedError).not.toBeNull(); + expect(receivedError?.message).toEqual("Get status timeout"); + }) + + it('getStatuses throw error when receive empty buffer from adapter', async () => { + returnEmptyDataOnOfflineCauseStatus = true; + let receivedError: Error | null = null; + + const printer = new Printer(adapter, {}) + try { + await printer.getStatuses(); + } catch (err) { + if (err instanceof Error) { + receivedError = err; + } + } + + expect(adapter.write).toHaveBeenCalledTimes(3); + expect(adapter.read).toHaveBeenCalledTimes(3); + expect(receivedError).not.toBeNull(); + expect(receivedError?.message).toEqual("Get status timeout"); + }) +}); \ No newline at end of file diff --git a/packages/network-adapter/src/index.ts b/packages/network-adapter/src/index.ts index 838d519a..7f37bddd 100644 --- a/packages/network-adapter/src/index.ts +++ b/packages/network-adapter/src/index.ts @@ -8,18 +8,22 @@ import { Adapter } from "@node-escpos/adapter"; export default class Network extends Adapter<[device: net.Socket]> { private readonly address: string; private readonly port: number; - private readonly timeout: number; + private readonly connectTimeout: number; + private readonly readTimeout: number; private readonly device: net.Socket; /** * @param {[type]} address * @param {[type]} port + * @param {[type]} connectTimeout + * @param {[type]} readTimeout */ - constructor(address: string, port = 9100, timeout = 30000) { + constructor(address: string, port = 9100, connectTimeout = 30000, readTimeout = 1000) { super(); this.address = address; this.port = port; - this.timeout = timeout; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; this.device = new net.Socket(); } @@ -33,9 +37,9 @@ export default class Network extends Adapter<[device: net.Socket]> { const connection_timeout = setTimeout(() => { this.device.destroy(); callback && callback( - new Error(`printer connection timeout after ${this.timeout}ms`), this.device, + new Error(`printer connection timeout after ${this.connectTimeout}ms`), this.device, ); - }, this.timeout); + }, this.connectTimeout); // connect to net printer by socket (port, ip) this.device.on("error", (err) => { @@ -43,10 +47,10 @@ export default class Network extends Adapter<[device: net.Socket]> { }).on("data", (buf) => { // eslint-disable-next-line no-console console.log("printer say:", buf); - }).connect(this.port, this.address, (err?: Error | null) => { - clearInterval(connection_timeout); + }).connect(this.port, this.address, () => { + clearTimeout(connection_timeout); this.emit("connect", this.device); - callback && callback(err ?? null, this.device); + callback && callback(null, this.device); }); return this; } @@ -67,9 +71,21 @@ export default class Network extends Adapter<[device: net.Socket]> { } read(callback?: (data: Buffer) => void) { - this.device.on("data", (buf) => { + let timeoutId:NodeJS.Timeout|undefined = undefined; + + // listener to pass to socket.once and socket.off + const eventListener = (buf: Buffer) => { + if(timeoutId !== undefined) clearTimeout(timeoutId); if (callback) callback(buf); - }); + } + + // pass empty buffer to callback function when timeout + timeoutId = setTimeout(() => { + this.device.off("data", eventListener); + if (callback) callback(Buffer.from("")); + }, this.readTimeout); + + this.device.once("data", eventListener); return this; } diff --git a/packages/network-adapter/test/index.test.ts b/packages/network-adapter/test/index.test.ts index 401553c6..d32f6698 100644 --- a/packages/network-adapter/test/index.test.ts +++ b/packages/network-adapter/test/index.test.ts @@ -1,7 +1,166 @@ -import { describe, expect, it } from 'vitest' +import net from "net"; +import { describe, expect, it, vi } from 'vitest'; +import Network from '../src/'; +import { DeviceStatus, Printer, PrinterStatus, StatusJSONElement } from '../../core'; + describe('should', () => { - it('exported', () => { - expect(1).toEqual(1) - }) + const testServerIP = "127.0.0.1"; + const testServerPort = 65178; + + it('get status from test server', async () => { + const server = net.createServer((s) => { + s.on('data', (d) => { + s.write('\x16') + }); + }).listen(testServerPort,testServerIP); + + const device = new Network(testServerIP, testServerPort, 3000, 1000); + + const getStatusTask = new Promise((resolve, reject) => { + device.open(async function(err: Error | null, dev: net.Socket) { + if(err) return reject(err); + + try{ + const options = { encoding: "BIG5"}; + const printer = new Printer(device, options); + const status = await printer.getStatus(PrinterStatus); + resolve(status); + } catch(err) { + return reject(err); + } + }); + }); + + const result = await getStatusTask; + + expect(result.byte).toEqual(22); + + server.close(); + }); + + it('fail get status from test server with delay', async () => { + const server = net.createServer((s) => { + s.on('data', (d) => { + // write data after 500 ms + setTimeout(() => s.write('\x16'), 500); + }); + }).listen(testServerPort,testServerIP); + + const device = new Network(testServerIP, testServerPort, 3000, 100); + + const getStatusTask = new Promise((resolve, reject) => { + device.open(async function(err: Error | null, dev: net.Socket) { + if(err) return reject(err); + + try{ + const options = { encoding: "BIG5"}; + const printer = new Printer(device, options); + const status = await printer.getStatus(PrinterStatus); + resolve(status); + } catch(err) { + return reject(err); + } + }); + }); + + let error:any; + + try{ + await getStatusTask; + } catch(err) { + error = err; + } + + expect(error).not.null; + + server.close(); + }); + + it('get statuses from test server', async () => { + const server = net.createServer((s) => { + s.on('data', (d) => { + if(d.toString() === "\x10\x04\x01") { + s.write('\x16') + } else { + s.write('\x12') + } + }); + }).listen(testServerPort,testServerIP); + + const device = new Network(testServerIP, testServerPort, 3000, 1000); + + const getStatusTask = new Promise((resolve, reject) => { + device.open(async function(err: Error | null, dev: net.Socket) { + if(err) return reject(err); + + try{ + const options = { encoding: "BIG5"}; + const printer = new Printer(device, options); + const statuses = await printer.getStatuses(); + resolve(statuses); + } catch(err) { + return reject(err); + } + }); + }); + + const result = await getStatusTask; + + expect(result).toHaveLength(4) + + result.forEach((r) => { + if(r instanceof PrinterStatus) { + expect(r.byte).toEqual(22); + } else { + expect(r.byte).toEqual(18); + } + }); + + server.close(); + }); + + it('fail get statuses from test server with delay', async () => { + const server = net.createServer((s) => { + s.on('data', (d) => { + if(d.toString() === "\x10\x04\x01") { + s.write('\x16') + } else if(d.toString() === "\x10\x04\x02") { + // write data after 500 ms + setTimeout(() => s.write('\x12'), 500); + } else { + s.write('\x12') + } + }); + }).listen(testServerPort,testServerIP); + + const device = new Network(testServerIP, testServerPort, 3000, 100); + + const getStatusTask = new Promise((resolve, reject) => { + device.open(async function(err: Error | null, dev: net.Socket) { + if(err) return reject(err); + + try{ + const options = { encoding: "BIG5"}; + const printer = new Printer(device, options); + const statuses = await printer.getStatuses(); + resolve(statuses); + } catch(err) { + return reject(err); + } + }); + }); + + let error:any; + + try{ + await getStatusTask; + } catch(err) { + error = err; + } + + expect(error).not.null; + + server.close(); + }); })