@mmote/niimbluelib
Version:
Library for the communication with NIIMBOT printers
242 lines (241 loc) • 9.88 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NiimbotAbstractClient = void 0;
const eventemitter3_1 = require("eventemitter3");
const async_mutex_1 = require("async-mutex");
const packets_1 = require("../packets");
const printer_models_1 = require("../printer_models");
const events_1 = require("../events");
const print_tasks_1 = require("../print_tasks");
const utils_1 = require("../utils");
const dto_1 = require("../packets/dto");
/**
* Abstract class representing a client with common functionality for interacting with a printer.
* Hardware interface must be defined after extending this class.
*
* @category Client
*/
class NiimbotAbstractClient extends eventemitter3_1.EventEmitter {
constructor() {
super();
this.info = {};
this.heartbeatFails = 0;
this.heartbeatIntervalMs = 2_000;
this.mutex = new async_mutex_1.Mutex();
this.debug = false;
this.packetBuf = new Uint8Array();
/** @see https://github.com/MultiMote/niimblue/issues/5 */
this.packetIntervalMs = 10;
this.abstraction = new packets_1.Abstraction(this);
this.on("connect", () => this.startHeartbeat());
this.on("disconnect", () => {
this.stopHeartbeat();
this.packetBuf = new Uint8Array();
});
}
/**
* 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} Packet object.
*/
async sendPacketWaitResponse(packet, timeoutMs = 1000) {
return this.mutex.runExclusive(async () => {
await this.sendPacket(packet, true);
if (packet.oneWay) {
return new packets_1.NiimbotPacket(packets_1.ResponseCommandId.In_Invalid, []); // or undefined is better?
}
return this.waitForPacket(packet.validResponseIds, true, timeoutMs);
});
}
/**
* Send wait for response for {@link timeoutMs} milliseconds.
*
* If {@link ids} is set, it will wait for packet with this command ids.
*
* @throws {@link PrintError} when {@link ResponseCommandId.In_PrintError} or {@link ResponseCommandId.In_NotSupported} received and {@link catchErrorPackets} is true.
*
* @returns {NiimbotPacket} Packet object.
*/
async waitForPacket(ids = [], catchErrorPackets = true, timeoutMs = 1000) {
return new Promise((resolve, reject) => {
let timeout = undefined;
const listener = (evt) => {
const pktIn = evt.packet;
const cmdIn = pktIn.command;
if (ids.length === 0 ||
ids.includes(cmdIn) ||
(catchErrorPackets && [packets_1.ResponseCommandId.In_PrintError, packets_1.ResponseCommandId.In_NotSupported].includes(cmdIn))) {
clearTimeout(timeout);
this.off("packetreceived", listener);
if (cmdIn === packets_1.ResponseCommandId.In_PrintError) {
utils_1.Validators.u8ArrayLengthEquals(pktIn.data, 1);
const errorName = packets_1.PrinterErrorCode[pktIn.data[0]] ?? "unknown";
reject(new dto_1.PrintError(`Print error ${pktIn.data[0]}: ${errorName}`, pktIn.data[0]));
}
else if (cmdIn === packets_1.ResponseCommandId.In_NotSupported) {
reject(new dto_1.PrintError("Feature not supported", 0));
}
else {
resolve(pktIn);
}
}
};
timeout = setTimeout(() => {
this.off("packetreceived", listener);
reject(new Error(`Timeout waiting response (waited for ${utils_1.Utils.bufToHex(ids, ", ")})`));
}, timeoutMs ?? 1000);
this.on("packetreceived", listener);
});
}
/**
* Convert raw bytes to packet objects and fire events. Defragmentation included.
* @param data Bytes to process.
*/
processRawPacket(data) {
if (data.byteLength === 0) {
return;
}
if (data instanceof DataView) {
data = new Uint8Array(data.buffer);
}
this.packetBuf = utils_1.Utils.u8ArrayAppend(this.packetBuf, data);
if (this.packetBuf.length > 1 && !utils_1.Utils.hasSubarrayAtPos(this.packetBuf, packets_1.NiimbotPacket.HEAD, 0)) {
console.warn("Dropping invalid buffer", utils_1.Utils.bufToHex(this.packetBuf));
this.packetBuf = new Uint8Array();
}
try {
const packets = packets_1.PacketParser.parsePacketBundle(this.packetBuf);
if (packets.length > 0) {
this.emit("rawpacketreceived", new events_1.RawPacketReceivedEvent(this.packetBuf));
packets.forEach((p) => {
this.emit("packetreceived", new events_1.PacketReceivedEvent(p));
});
this.packetBuf = new Uint8Array();
}
}
catch (_e) {
if (this.debug) {
console.info(`Incomplete packet, ignoring:${utils_1.Utils.bufToHex(this.packetBuf)}`, _e);
}
}
}
async sendPacket(packet, force) {
await this.sendRaw(packet.toBytes(), force);
this.emit("packetsent", new events_1.PacketSentEvent(packet));
}
/**
* Send "connect" packet and fetch the protocol version.
**/
async initialNegotiate() {
const cfg = this.info;
cfg.connectResult = await this.abstraction.connectResult();
cfg.protocolVersion = 0;
if (cfg.connectResult === packets_1.ConnectResult.ConnectedNew) {
cfg.protocolVersion = 1;
}
else if (cfg.connectResult === packets_1.ConnectResult.ConnectedV3) {
const statusData = await this.abstraction.getPrinterStatusData();
cfg.protocolVersion = statusData.protocolVersion;
}
}
/**
* Fetches printer information and stores it.
*/
async fetchPrinterInfo() {
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.emit("printerinfofetched", new events_1.PrinterInfoFetchedEvent(this.info));
return this.info;
}
/**
* Get the stored information about the printer.
*/
getPrinterInfo() {
return this.info;
}
/**
* Set interval for {@link startHeartbeat}.
*
* @param intervalMs Heartbeat interval, default is 1000ms
*/
setHeartbeatInterval(intervalMs) {
this.heartbeatIntervalMs = intervalMs;
}
/**
* Starts the heartbeat timer, "heartbeat" is emitted after packet received.
*
* If you need to change interval, call {@link setHeartbeatInterval} before.
*/
startHeartbeat() {
this.heartbeatFails = 0;
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
this.abstraction
.heartbeat()
.then((data) => {
this.heartbeatFails = 0;
this.emit("heartbeat", new events_1.HeartbeatEvent(data));
})
.catch((e) => {
console.error(e);
this.heartbeatFails++;
this.emit("heartbeatfailed", new events_1.HeartbeatFailedEvent(this.heartbeatFails));
});
}, this.heartbeatIntervalMs);
}
/**
* Stops the heartbeat by clearing the interval timer.
*/
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
/**
* Checks if the heartbeat timer has been started.
*/
isHeartbeatStarted() {
return this.heartbeatTimer === undefined;
}
/**
* Get printer capabilities based on the printer model. Model library is hardcoded.
**/
getModelMetadata() {
if (this.info.modelId === undefined) {
return undefined;
}
return (0, printer_models_1.getPrinterMetaById)(this.info.modelId);
}
/**
* Determine print task version if any.
**/
getPrintTaskType() {
const meta = this.getModelMetadata();
if (meta === undefined) {
return undefined;
}
return (0, print_tasks_1.findPrintTask)(meta.model, this.getPrinterInfo().protocolVersion);
}
/**
* Set the interval between packets in milliseconds.
*/
setPacketInterval(milliseconds) {
this.packetIntervalMs = milliseconds;
}
/**
* Enable some debug information logging.
*/
setDebug(value) {
this.debug = value;
}
}
exports.NiimbotAbstractClient = NiimbotAbstractClient;