Move sendPacketWaitResponse to base class

This commit is contained in:
MultiMote 2024-11-12 10:39:44 +03:00
parent 6f4b4fc99e
commit 7988d46d0c
4 changed files with 111 additions and 139 deletions

@ -15,4 +15,14 @@ export default [
pluginJs.configs.recommended, pluginJs.configs.recommended,
...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.recommendedTypeChecked,
{ ignores: ["dist/*", "dumps/*", "**/*.{mjs,js}"] }, { ignores: ["dist/*", "dumps/*", "**/*.{mjs,js}"] },
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
caughtErrorsIgnorePattern: "^_",
},
],
},
},
]; ];

@ -1,9 +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 { ConnectionInfo, NiimbotAbstractClient } from ".";
import { NiimbotPacket } from "../packets/packet"; import { NiimbotPacket } from "../packets/packet";
import { ConnectResult, PrinterErrorCode, PrintError, ResponseCommandId } from "../packets"; import { ConnectResult, ResponseCommandId } from "../packets";
import { Utils, Validators } from "../utils"; import { Utils } from "../utils";
class BleConfiguration { class BleConfiguration {
public static readonly SERVICE: string = "e7810a71-73ae-499d-8c15-faa9aef0c3f2"; public static readonly SERVICE: string = "e7810a71-73ae-499d-8c15-faa9aef0c3f2";
@ -35,7 +34,6 @@ class BleConfiguration {
export class NiimbotBluetoothClient extends NiimbotAbstractClient { export class NiimbotBluetoothClient extends NiimbotAbstractClient {
private gattServer?: BluetoothRemoteGATTServer = undefined; private gattServer?: BluetoothRemoteGATTServer = undefined;
private channel?: BluetoothRemoteGATTCharacteristic = undefined; private channel?: BluetoothRemoteGATTCharacteristic = undefined;
private mutex: Mutex = new Mutex();
public async connect(): Promise<ConnectionInfo> { public async connect(): Promise<ConnectionInfo> {
await this.disconnect(); await this.disconnect();
@ -114,57 +112,6 @@ export class NiimbotBluetoothClient extends NiimbotAbstractClient {
this.info = {}; 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) { public async sendRaw(data: Uint8Array, force?: boolean) {
const send = async () => { const send = async () => {
if (this.channel === undefined) { if (this.channel === undefined) {

@ -1,4 +1,5 @@
import { EventEmitter } from "eventemitter3"; import { EventEmitter } from "eventemitter3";
import { Mutex } from "async-mutex";
import { import {
Abstraction, Abstraction,
AutoShutdownTime, AutoShutdownTime,
@ -6,11 +7,21 @@ import {
ConnectResult, ConnectResult,
LabelType, LabelType,
NiimbotPacket, NiimbotPacket,
PrinterErrorCode,
PrintError,
ResponseCommandId,
} from "../packets"; } from "../packets";
import { PrinterModelMeta, getPrinterMetaById } from "../printer_models"; 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 { findPrintTask, PrintTaskName } from "../print_tasks";
import { Utils, Validators } from "../utils";
/** /**
* Represents the connection result information. * Represents the connection result information.
@ -40,7 +51,6 @@ export interface PrinterInfo {
hardwareVersion?: string; hardwareVersion?: string;
} }
/** /**
* Abstract class representing a client with common functionality for interacting with a printer. * Abstract class representing a client with common functionality for interacting with a printer.
* Hardware interface must be defined after extending this class. * 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 heartbeatTimer?: NodeJS.Timeout;
private heartbeatFails: number = 0; private heartbeatFails: number = 0;
private heartbeatIntervalMs: number = 2_000; private heartbeatIntervalMs: number = 2_000;
protected mutex: Mutex = new Mutex();
protected debug: boolean = false;
/** @see https://github.com/MultiMote/niimblue/issues/5 */ /** @see https://github.com/MultiMote/niimblue/issues/5 */
protected packetIntervalMs: number = 10; protected packetIntervalMs: number = 10;
@ -80,16 +92,64 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap>
public abstract isConnected(): boolean; public abstract isConnected(): boolean;
/** /**
* Send packet and wait for response. * Send packet and wait for response for {@link timeoutMs} milliseconds.
* If packet.responsePacketCommandId is defined, it will wait for packet with this command id. *
* 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. * Send raw bytes to the printer port.
* *
* @param data Bytes to send. * @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>; 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)); 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> { protected async initialNegotiate(): Promise<void> {
const cfg = this.info; const cfg = this.info;
cfg.connectResult = await this.abstraction.connectResult(); cfg.connectResult = await this.abstraction.connectResult();
@ -112,20 +174,19 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap>
} }
} }
/** /**
* Fetches printer information and stores it. * Fetches printer information and stores it.
*/ */
public async fetchPrinterInfo(): Promise<PrinterInfo> { public async fetchPrinterInfo(): Promise<PrinterInfo> {
this.info.modelId = await this.abstraction.getPrinterModel(); this.info.modelId = await this.abstraction.getPrinterModel();
this.info.serial = await this.abstraction.getPrinterSerialNumber().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.mac = (await this.abstraction.getPrinterBluetoothMacAddress().catch(console.error)) ?? undefined;
this.info.charge = await this.abstraction.getBatteryChargeLevel().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.autoShutdownTime = (await this.abstraction.getAutoShutDownTime().catch(console.error)) ?? undefined;
this.info.labelType = await this.abstraction.getLabelType().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.hardwareVersion = (await this.abstraction.getHardwareVersion().catch(console.error)) ?? undefined;
this.info.softwareVersion = await this.abstraction.getSoftwareVersion().catch(console.error) ?? undefined; this.info.softwareVersion = (await this.abstraction.getSoftwareVersion().catch(console.error)) ?? undefined;
this.emit("printerinfofetched", new PrinterInfoFetchedEvent(this.info)); this.emit("printerinfofetched", new PrinterInfoFetchedEvent(this.info));
return this.info; return this.info;
@ -216,6 +277,13 @@ export abstract class NiimbotAbstractClient extends EventEmitter<ClientEventMap>
public setPacketInterval(milliseconds: number) { public setPacketInterval(milliseconds: number) {
this.packetIntervalMs = milliseconds; this.packetIntervalMs = milliseconds;
} }
/**
* Enable some debug information logging.
*/
public setDebug(value: boolean) {
this.debug = value;
}
} }
export * from "./bluetooth_impl"; export * from "./bluetooth_impl";

@ -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 { ConnectionInfo, NiimbotAbstractClient } from ".";
import { NiimbotPacket } from "../packets/packet"; import { NiimbotPacket } from "../packets/packet";
import { ConnectResult, PrinterErrorCode, PrintError, ResponseCommandId } from "../packets"; import { ConnectResult } from "../packets";
import { Utils, Validators } from "../utils"; import { Utils } from "../utils";
/** /**
* Uses [Web Serial API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API) * 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 port?: SerialPort = undefined;
private writer?: WritableStreamDefaultWriter<Uint8Array> = undefined; private writer?: WritableStreamDefaultWriter<Uint8Array> = undefined;
private reader?: ReadableStreamDefaultReader<Uint8Array> = undefined; private reader?: ReadableStreamDefaultReader<Uint8Array> = undefined;
private mutex: Mutex = new Mutex();
public async connect(): Promise<ConnectionInfo> { public async connect(): Promise<ConnectionInfo> {
await this.disconnect(); await this.disconnect();
@ -77,7 +69,9 @@ export class NiimbotSerialClient extends NiimbotAbstractClient {
try { try {
const result = await this.reader!.read(); const result = await this.reader!.read();
if (result.value) { 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); const newBuf = new Uint8Array(buf.length + result.value.length);
newBuf.set(buf, 0); newBuf.set(buf, 0);
@ -89,7 +83,7 @@ export class NiimbotSerialClient extends NiimbotAbstractClient {
console.log("done"); console.log("done");
break; break;
} }
} catch (e) { } catch (_e) {
break; break;
} }
@ -105,8 +99,10 @@ export class NiimbotSerialClient extends NiimbotAbstractClient {
buf = new Uint8Array(); buf = new Uint8Array();
} }
} catch (e) { } catch (_e) {
// console.info(`Incomplete packet, ignoring:${Utils.bufToHex(buf)}`); if (this.debug) {
console.info(`Incomplete packet, ignoring:${Utils.bufToHex(buf)}`);
}
} }
} }
} }
@ -132,65 +128,16 @@ export class NiimbotSerialClient extends NiimbotAbstractClient {
} }
public isConnected(): boolean { public isConnected(): boolean {
return this.port !== undefined && this.writer !== undefined; return this.port !== undefined && this.writer !== undefined && this.reader !== 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);
});
});
} }
public async sendRaw(data: Uint8Array, force?: boolean) { public async sendRaw(data: Uint8Array, force?: boolean) {
const send = async () => { const send = async () => {
if (this.writer === undefined) { if (!this.isConnected()) {
throw new Error("Port is not writable"); throw new Error("Port is not readable/writable");
} }
await Utils.sleep(this.packetIntervalMs); await Utils.sleep(this.packetIntervalMs);
await this.writer.write(data); await this.writer!.write(data);
this.emit("rawpacketsent", new RawPacketSentEvent(data)); this.emit("rawpacketsent", new RawPacketSentEvent(data));
}; };