diff --git a/eslint.config.mjs b/eslint.config.mjs index 2ee9ae4..a52edb9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,4 +15,14 @@ export default [ pluginJs.configs.recommended, ...tseslint.configs.recommendedTypeChecked, { ignores: ["dist/*", "dumps/*", "**/*.{mjs,js}"] }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, ]; diff --git a/src/client/bluetooth_impl.ts b/src/client/bluetooth_impl.ts index 194f1e7..0f411a0 100644 --- a/src/client/bluetooth_impl.ts +++ b/src/client/bluetooth_impl.ts @@ -1,9 +1,8 @@ -import { Mutex } from "async-mutex"; import { ConnectEvent, DisconnectEvent, PacketReceivedEvent, RawPacketReceivedEvent, RawPacketSentEvent } from "../events"; import { ConnectionInfo, NiimbotAbstractClient } from "."; import { NiimbotPacket } from "../packets/packet"; -import { ConnectResult, PrinterErrorCode, PrintError, ResponseCommandId } from "../packets"; -import { Utils, Validators } from "../utils"; +import { ConnectResult, ResponseCommandId } from "../packets"; +import { Utils } from "../utils"; class BleConfiguration { public static readonly SERVICE: string = "e7810a71-73ae-499d-8c15-faa9aef0c3f2"; @@ -35,7 +34,6 @@ class BleConfiguration { export class NiimbotBluetoothClient extends NiimbotAbstractClient { private gattServer?: BluetoothRemoteGATTServer = undefined; private channel?: BluetoothRemoteGATTCharacteristic = undefined; - private mutex: Mutex = new Mutex(); public async connect(): Promise<ConnectionInfo> { await this.disconnect(); @@ -114,57 +112,6 @@ export class NiimbotBluetoothClient extends NiimbotAbstractClient { this.info = {}; } - /** - * Send packet and wait for response. - * If packet.responsePacketCommandId is defined, it will wait for packet with this command id. - */ - public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs?: number): Promise<NiimbotPacket> { - return this.mutex.runExclusive(async () => { - await this.sendPacket(packet, true); - - if (packet.oneWay) { - return new NiimbotPacket(ResponseCommandId.Invalid, []); // or undefined is better? - } - - // what if response received at this point? - - return new Promise((resolve, reject) => { - let timeout: NodeJS.Timeout | undefined = undefined; - - const listener = (evt: PacketReceivedEvent) => { - const pktIn = evt.packet; - const cmdIn = pktIn.command as ResponseCommandId; - - if ( - packet.validResponseIds.length === 0 || - packet.validResponseIds.includes(cmdIn) || - [ResponseCommandId.In_PrintError, ResponseCommandId.In_NotSupported].includes(cmdIn) - ) { - clearTimeout(timeout); - this.off("packetreceived", listener); - - if (cmdIn === ResponseCommandId.In_PrintError) { - Validators.u8ArrayLengthEquals(pktIn.data, 1); - const errorName = PrinterErrorCode[pktIn.data[0]] ?? "unknown"; - reject(new PrintError(`Print error ${pktIn.data[0]}: ${errorName}`, pktIn.data[0])); - } else if (cmdIn === ResponseCommandId.In_NotSupported) { - reject(new PrintError("Feature not supported", 0)); - } else { - resolve(pktIn); - } - } - }; - - timeout = setTimeout(() => { - this.off("packetreceived", listener); - reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`)); - }, timeoutMs ?? 1000); - - this.on("packetreceived", listener); - }); - }); - } - public async sendRaw(data: Uint8Array, force?: boolean) { const send = async () => { if (this.channel === undefined) { diff --git a/src/client/index.ts b/src/client/index.ts index 3315675..57b2f8a 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "eventemitter3"; +import { Mutex } from "async-mutex"; import { Abstraction, AutoShutdownTime, @@ -6,11 +7,21 @@ import { ConnectResult, LabelType, NiimbotPacket, + PrinterErrorCode, + PrintError, + ResponseCommandId, } from "../packets"; import { PrinterModelMeta, getPrinterMetaById } from "../printer_models"; -import { ClientEventMap, PacketSentEvent, PrinterInfoFetchedEvent, HeartbeatEvent, HeartbeatFailedEvent } from "../events"; +import { + ClientEventMap, + PacketSentEvent, + PrinterInfoFetchedEvent, + HeartbeatEvent, + HeartbeatFailedEvent, + PacketReceivedEvent, +} from "../events"; import { findPrintTask, PrintTaskName } from "../print_tasks"; - +import { Utils, Validators } from "../utils"; /** * Represents the connection result information. @@ -40,7 +51,6 @@ export interface PrinterInfo { hardwareVersion?: string; } - /** * Abstract class representing a client with common functionality for interacting with a printer. * Hardware interface must be defined after extending this class. @@ -53,6 +63,8 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> private heartbeatTimer?: NodeJS.Timeout; private heartbeatFails: number = 0; private heartbeatIntervalMs: number = 2_000; + protected mutex: Mutex = new Mutex(); + protected debug: boolean = false; /** @see https://github.com/MultiMote/niimblue/issues/5 */ protected packetIntervalMs: number = 10; @@ -80,16 +92,64 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> public abstract isConnected(): boolean; /** - * Send packet and wait for response. - * If packet.responsePacketCommandId is defined, it will wait for packet with this command id. + * Send packet and wait for response for {@link timeoutMs} milliseconds. + * + * If {@link NiimbotPacket.validResponseIds() validResponseIds} is defined, it will wait for packet with this command id. + * + * @throws {@link PrintError} when {@link ResponseCommandId.In_PrintError} or {@link ResponseCommandId.In_NotSupported} received. + * + * @returns {NiimbotPacket} Printer response object. */ - public abstract sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs?: number): Promise<NiimbotPacket>; + public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs: number = 1000): Promise<NiimbotPacket> { + return this.mutex.runExclusive(async () => { + await this.sendPacket(packet, true); + + if (packet.oneWay) { + return new NiimbotPacket(ResponseCommandId.Invalid, []); // or undefined is better? + } + + return new Promise((resolve, reject) => { + let timeout: NodeJS.Timeout | undefined = undefined; + + const listener = (evt: PacketReceivedEvent) => { + const pktIn = evt.packet; + const cmdIn = pktIn.command as ResponseCommandId; + + if ( + packet.validResponseIds.length === 0 || + packet.validResponseIds.includes(cmdIn) || + [ResponseCommandId.In_PrintError, ResponseCommandId.In_NotSupported].includes(cmdIn) + ) { + clearTimeout(timeout); + this.off("packetreceived", listener); + + if (cmdIn === ResponseCommandId.In_PrintError) { + Validators.u8ArrayLengthEquals(pktIn.data, 1); + const errorName = PrinterErrorCode[pktIn.data[0]] ?? "unknown"; + reject(new PrintError(`Print error ${pktIn.data[0]}: ${errorName}`, pktIn.data[0])); + } else if (cmdIn === ResponseCommandId.In_NotSupported) { + reject(new PrintError("Feature not supported", 0)); + } else { + resolve(pktIn); + } + } + }; + + timeout = setTimeout(() => { + this.off("packetreceived", listener); + reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`)); + }, timeoutMs ?? 1000); + + this.on("packetreceived", listener); + }); + }); + } /** * Send raw bytes to the printer port. * * @param data Bytes to send. - * @param force Ignore mutex lock. You should avoid using it. + * @param force Ignore mutex lock. It used internally and you should avoid using it. */ public abstract sendRaw(data: Uint8Array, force?: boolean): Promise<void>; @@ -98,7 +158,9 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> this.emit("packetsent", new PacketSentEvent(packet)); } - /** Send "connect" packet and fetch the protocol version */ + /** + * Send "connect" packet and fetch the protocol version. + **/ protected async initialNegotiate(): Promise<void> { const cfg = this.info; cfg.connectResult = await this.abstraction.connectResult(); @@ -112,20 +174,19 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> } } - /** * Fetches printer information and stores it. */ public async fetchPrinterInfo(): Promise<PrinterInfo> { this.info.modelId = await this.abstraction.getPrinterModel(); - this.info.serial = await this.abstraction.getPrinterSerialNumber().catch(console.error) ?? undefined; - this.info.mac = await this.abstraction.getPrinterBluetoothMacAddress().catch(console.error) ?? undefined; - this.info.charge = await this.abstraction.getBatteryChargeLevel().catch(console.error) ?? undefined; - this.info.autoShutdownTime = await this.abstraction.getAutoShutDownTime().catch(console.error) ?? undefined; - this.info.labelType = await this.abstraction.getLabelType().catch(console.error) ?? undefined; - this.info.hardwareVersion = await this.abstraction.getHardwareVersion().catch(console.error) ?? undefined; - this.info.softwareVersion = await this.abstraction.getSoftwareVersion().catch(console.error) ?? undefined; + this.info.serial = (await this.abstraction.getPrinterSerialNumber().catch(console.error)) ?? undefined; + this.info.mac = (await this.abstraction.getPrinterBluetoothMacAddress().catch(console.error)) ?? undefined; + this.info.charge = (await this.abstraction.getBatteryChargeLevel().catch(console.error)) ?? undefined; + this.info.autoShutdownTime = (await this.abstraction.getAutoShutDownTime().catch(console.error)) ?? undefined; + this.info.labelType = (await this.abstraction.getLabelType().catch(console.error)) ?? undefined; + this.info.hardwareVersion = (await this.abstraction.getHardwareVersion().catch(console.error)) ?? undefined; + this.info.softwareVersion = (await this.abstraction.getSoftwareVersion().catch(console.error)) ?? undefined; this.emit("printerinfofetched", new PrinterInfoFetchedEvent(this.info)); return this.info; @@ -216,6 +277,13 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap> public setPacketInterval(milliseconds: number) { this.packetIntervalMs = milliseconds; } + + /** + * Enable some debug information logging. + */ + public setDebug(value: boolean) { + this.debug = value; + } } export * from "./bluetooth_impl"; diff --git a/src/client/serial_impl.ts b/src/client/serial_impl.ts index 0fdd5ea..192c39f 100644 --- a/src/client/serial_impl.ts +++ b/src/client/serial_impl.ts @@ -1,15 +1,8 @@ -import { Mutex } from "async-mutex"; -import { - ConnectEvent, - DisconnectEvent, - PacketReceivedEvent, - RawPacketReceivedEvent, - RawPacketSentEvent, -} from "../events"; +import { ConnectEvent, DisconnectEvent, PacketReceivedEvent, RawPacketReceivedEvent, RawPacketSentEvent } from "../events"; import { ConnectionInfo, NiimbotAbstractClient } from "."; import { NiimbotPacket } from "../packets/packet"; -import { ConnectResult, PrinterErrorCode, PrintError, ResponseCommandId } from "../packets"; -import { Utils, Validators } from "../utils"; +import { ConnectResult } from "../packets"; +import { Utils } from "../utils"; /** * Uses [Web Serial API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API) @@ -20,7 +13,6 @@ export class NiimbotSerialClient extends NiimbotAbstractClient { private port?: SerialPort = undefined; private writer?: WritableStreamDefaultWriter<Uint8Array> = undefined; private reader?: ReadableStreamDefaultReader<Uint8Array> = undefined; - private mutex: Mutex = new Mutex(); public async connect(): Promise<ConnectionInfo> { await this.disconnect(); @@ -77,7 +69,9 @@ export class NiimbotSerialClient extends NiimbotAbstractClient { try { const result = await this.reader!.read(); if (result.value) { - // console.info(`<< serial chunk ${Utils.bufToHex(result.value)}`); + if (this.debug) { + console.info(`<< serial chunk ${Utils.bufToHex(result.value)}`); + } const newBuf = new Uint8Array(buf.length + result.value.length); newBuf.set(buf, 0); @@ -89,7 +83,7 @@ export class NiimbotSerialClient extends NiimbotAbstractClient { console.log("done"); break; } - } catch (e) { + } catch (_e) { break; } @@ -105,8 +99,10 @@ export class NiimbotSerialClient extends NiimbotAbstractClient { buf = new Uint8Array(); } - } catch (e) { - // console.info(`Incomplete packet, ignoring:${Utils.bufToHex(buf)}`); + } catch (_e) { + if (this.debug) { + console.info(`Incomplete packet, ignoring:${Utils.bufToHex(buf)}`); + } } } } @@ -132,65 +128,16 @@ export class NiimbotSerialClient extends NiimbotAbstractClient { } public isConnected(): boolean { - return this.port !== undefined && this.writer !== undefined; - } - - public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs: number = 1000): Promise<NiimbotPacket> { - if (!this.port?.readable || !this.port?.writable) { - throw new Error("Port is not readable/writable"); - } - - return this.mutex.runExclusive(async () => { - await this.sendPacket(packet, true); - - if (packet.oneWay) { - return new NiimbotPacket(ResponseCommandId.Invalid, []); // or undefined is better? - } - - return new Promise((resolve, reject) => { - let timeout: NodeJS.Timeout | undefined = undefined; - - const listener = (evt: PacketReceivedEvent) => { - const pktIn = evt.packet; - const cmdIn = pktIn.command as ResponseCommandId; - - if ( - packet.validResponseIds.length === 0 || - packet.validResponseIds.includes(cmdIn) || - [ResponseCommandId.In_PrintError, ResponseCommandId.In_NotSupported].includes(cmdIn) - ) { - clearTimeout(timeout); - this.off("packetreceived", listener); - - if (cmdIn === ResponseCommandId.In_PrintError) { - Validators.u8ArrayLengthEquals(pktIn.data, 1); - const errorName = PrinterErrorCode[pktIn.data[0]] ?? "unknown"; - reject(new PrintError(`Print error ${pktIn.data[0]}: ${errorName}`, pktIn.data[0])); - } else if (cmdIn === ResponseCommandId.In_NotSupported) { - reject(new PrintError("Feature not supported", 0)); - } else { - resolve(pktIn); - } - } - }; - - timeout = setTimeout(() => { - this.off("packetreceived", listener); - reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`)); - }, timeoutMs ?? 1000); - - this.on("packetreceived", listener); - }); - }); + return this.port !== undefined && this.writer !== undefined && this.reader !== undefined; } public async sendRaw(data: Uint8Array, force?: boolean) { const send = async () => { - if (this.writer === undefined) { - throw new Error("Port is not writable"); + if (!this.isConnected()) { + throw new Error("Port is not readable/writable"); } await Utils.sleep(this.packetIntervalMs); - await this.writer.write(data); + await this.writer!.write(data); this.emit("rawpacketsent", new RawPacketSentEvent(data)); };