mirror of
https://github.com/MultiMote/niimbluelib.git
synced 2025-03-15 11:01:01 +03:00
Add code, npm publishing
This commit is contained in:
parent
0cb12cda1e
commit
a5c572aa00
.github/workflows
.gitignoreREADME.mdclean-dist.mjseslint.config.mjspackage.jsonsrc
tsconfig.jsonutils
yarn.lock
33
.github/workflows/publish-npmjs.yml
vendored
Normal file
33
.github/workflows/publish-npmjs.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Publish Package to npmjs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- npm-test
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
scope: '@mmote'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: npm publish --provenance --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/.vscode
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/yarn-error.log
|
89
README.md
Normal file
89
README.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
## NiimBlueLib [](https://npmjs.com/package/@mmote/niimbluelib)
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> This project is intended for informational and educational purposes only.
|
||||||
|
> The project is not affiliated with or endorsed by the original software or hardware vendor,
|
||||||
|
> and is not intended to be used for commercial purposes without the consent of the vendor.
|
||||||
|
|
||||||
|
NiimBlueLib is a library for the communication with NIIMBOT printers.
|
||||||
|
|
||||||
|
Project is in Alpha state. Use only exact version when you add it to your project. API can be changed anytime.
|
||||||
|
|
||||||
|
### Usage example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Utils, RequestCommandId, ResponseCommandId, NiimbotBluetoothClient, ImageEncoder } from "@mmote/niimbluelib";
|
||||||
|
|
||||||
|
const client = new NiimbotBluetoothClient();
|
||||||
|
|
||||||
|
client.addEventListener("packetsent", (e) => {
|
||||||
|
console.log(`>> ${Utils.bufToHex(e.packet.toBytes())} (${RequestCommandId[e.packet.command]})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.addEventListener("packetreceived", (e) => {
|
||||||
|
console.log(`<< ${Utils.bufToHex(e.packet.toBytes())} (${ResponseCommandId[e.packet.command]})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.addEventListener("connect", () => {
|
||||||
|
console.log("connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
client.addEventListener("disconnect", () => {
|
||||||
|
console.log("disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
client.addEventListener("printprogress", (e) => {
|
||||||
|
console.log(`Page ${e.page}/${e.pagesTotal}, Page print ${e.pagePrintProgress}%, Page feed ${e.pageFeedProgress}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// label props
|
||||||
|
const props = {
|
||||||
|
width: 240,
|
||||||
|
height: 96,
|
||||||
|
printDirection: "left",
|
||||||
|
};
|
||||||
|
const quantity = 1;
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = props.width;
|
||||||
|
canvas.height = props.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
|
||||||
|
// fill background
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
// draw diagonal line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 0);
|
||||||
|
ctx.lineTo(canvas.width, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
// draw border
|
||||||
|
ctx.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
|
||||||
|
|
||||||
|
const image = ImageEncoder.encodeCanvas(canvas, props.printDirection);
|
||||||
|
|
||||||
|
await client.abstraction.print(client.getPrintTaskVersion(), image, { quantity });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.abstraction.waitUntilPrintFinished(quantity);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.abstraction.printEnd();
|
||||||
|
await client.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
Eslint not included. Install it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --no-save --no-package-lock eslint@9.x globals @eslint/js typescript-eslint
|
||||||
|
```
|
3
clean-dist.mjs
Normal file
3
clean-dist.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
fs.rmSync("dist", { recursive: true, force: true });
|
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ files: ["**/*.{ts}"] },
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
project: "tsconfig.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
{ ignores: ["dist/*", "dumps/*", "**/*.{mjs,js}"] },
|
||||||
|
];
|
45
package.json
Normal file
45
package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "@mmote/niimbluelib",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Library for the communication with NIIMBOT printers",
|
||||||
|
"keywords": [
|
||||||
|
"reverse-engineering",
|
||||||
|
"thermal-printer",
|
||||||
|
"label-printer",
|
||||||
|
"niimbot",
|
||||||
|
"niimbot-d110",
|
||||||
|
"niimbot-b1",
|
||||||
|
"bluetooth",
|
||||||
|
"serial"
|
||||||
|
],
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"/dist"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MultiMote/niimbluelib.git"
|
||||||
|
},
|
||||||
|
"author": "MultiMote",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"clean-build": "yarn clean && yarn build",
|
||||||
|
"build": "tsc --build",
|
||||||
|
"parse-dump": "yarn build && node utils/parse-dump.mjs",
|
||||||
|
"clean": "node clean-dist.mjs",
|
||||||
|
"gen-printer-models": "node utils/gen-printer-models.js > src/printer_models.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.2",
|
||||||
|
"@types/w3c-web-serial": "^1.0.6",
|
||||||
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
|
"typescript-event-target": "^1.1.1"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
|
}
|
159
src/client/bluetooth_impl.ts
Normal file
159
src/client/bluetooth_impl.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { Mutex } from "async-mutex";
|
||||||
|
import { ConnectionInfo, NiimbotAbstractClient, ConnectResult, NiimbotPacket, ResponseCommandId, Utils } from "..";
|
||||||
|
import {
|
||||||
|
ConnectEvent,
|
||||||
|
DisconnectEvent,
|
||||||
|
PacketReceivedEvent,
|
||||||
|
RawPacketReceivedEvent,
|
||||||
|
RawPacketSentEvent,
|
||||||
|
} from "./events";
|
||||||
|
|
||||||
|
class BleConfiguration {
|
||||||
|
public static readonly SERVICE: string = "e7810a71-73ae-499d-8c15-faa9aef0c3f2";
|
||||||
|
public static readonly CHARACTERISTIC: string = "bef8d6c9-9c21-4c9e-b632-bd58c1009f9f";
|
||||||
|
public static readonly FILTER: BluetoothLEScanFilter[] = [
|
||||||
|
{ namePrefix: "D" },
|
||||||
|
{ namePrefix: "B" },
|
||||||
|
{ services: [BleConfiguration.SERVICE] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Uses Web Bluetooth API */
|
||||||
|
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();
|
||||||
|
|
||||||
|
const options: RequestDeviceOptions = {
|
||||||
|
filters: BleConfiguration.FILTER,
|
||||||
|
};
|
||||||
|
const device: BluetoothDevice = await navigator.bluetooth.requestDevice(options);
|
||||||
|
|
||||||
|
if (device.gatt === undefined) {
|
||||||
|
throw new Error("Device has no Bluetooth Generic Attribute Profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnectListener = () => {
|
||||||
|
this.gattServer = undefined;
|
||||||
|
this.channel = undefined;
|
||||||
|
this.info = {};
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.dispatchTypedEvent("disconnect", new DisconnectEvent());
|
||||||
|
device.removeEventListener("gattserverdisconnected", disconnectListener);
|
||||||
|
};
|
||||||
|
|
||||||
|
device.addEventListener("gattserverdisconnected", disconnectListener);
|
||||||
|
|
||||||
|
const gattServer: BluetoothRemoteGATTServer = await device.gatt.connect();
|
||||||
|
|
||||||
|
const service: BluetoothRemoteGATTService = await gattServer.getPrimaryService(BleConfiguration.SERVICE);
|
||||||
|
|
||||||
|
const channel: BluetoothRemoteGATTCharacteristic = await service.getCharacteristic(BleConfiguration.CHARACTERISTIC);
|
||||||
|
|
||||||
|
channel.addEventListener("characteristicvaluechanged", (event: Event) => {
|
||||||
|
const target = event.target as BluetoothRemoteGATTCharacteristic;
|
||||||
|
const data = new Uint8Array(target.value!.buffer);
|
||||||
|
const packet = NiimbotPacket.fromBytes(data);
|
||||||
|
|
||||||
|
this.dispatchTypedEvent("rawpacketreceived", new RawPacketReceivedEvent(data));
|
||||||
|
this.dispatchTypedEvent("packetreceived", new PacketReceivedEvent(packet));
|
||||||
|
|
||||||
|
if (!(packet.command in ResponseCommandId)) {
|
||||||
|
console.warn(`Unknown response command: 0x${Utils.numberToHex(packet.command)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await channel.startNotifications();
|
||||||
|
|
||||||
|
this.gattServer = gattServer;
|
||||||
|
this.channel = channel;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.initialNegotiate();
|
||||||
|
await this.fetchPrinterInfo();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Unable to fetch printer info.");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ConnectionInfo = {
|
||||||
|
deviceName: device.name,
|
||||||
|
result: this.info.connectResult ?? ConnectResult.FirmwareErrors,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dispatchTypedEvent("connect", new ConnectEvent(result));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isConnected(): boolean {
|
||||||
|
return this.gattServer !== undefined && this.channel !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
public async disconnect() {
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.gattServer?.disconnect();
|
||||||
|
this.gattServer = undefined;
|
||||||
|
this.channel = undefined;
|
||||||
|
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) => {
|
||||||
|
if (
|
||||||
|
packet.validResponseIds.length === 0 ||
|
||||||
|
packet.validResponseIds.includes(evt.packet.command as ResponseCommandId)
|
||||||
|
) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.removeEventListener("packetreceived", listener);
|
||||||
|
resolve(evt.packet);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.removeEventListener("packetreceived", listener);
|
||||||
|
reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`));
|
||||||
|
}, timeoutMs ?? 1000);
|
||||||
|
|
||||||
|
this.addEventListener("packetreceived", listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendRaw(data: Uint8Array, force?: boolean) {
|
||||||
|
const send = async () => {
|
||||||
|
if (this.channel === undefined) {
|
||||||
|
throw new Error("Channel is closed");
|
||||||
|
}
|
||||||
|
await Utils.sleep(this.packetIntervalMs);
|
||||||
|
await this.channel.writeValueWithoutResponse(data);
|
||||||
|
this.dispatchTypedEvent("rawpacketsent", new RawPacketSentEvent(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
await send();
|
||||||
|
} else {
|
||||||
|
await this.mutex.runExclusive(send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
src/client/events.ts
Normal file
94
src/client/events.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { ConnectionInfo, PrinterInfo, HeartbeatData, NiimbotPacket } from "..";
|
||||||
|
|
||||||
|
export class ConnectEvent extends Event {
|
||||||
|
info: ConnectionInfo;
|
||||||
|
constructor(info: ConnectionInfo) {
|
||||||
|
super("connect");
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DisconnectEvent extends Event {
|
||||||
|
constructor() {
|
||||||
|
super("disconnect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PacketReceivedEvent extends Event {
|
||||||
|
packet: NiimbotPacket;
|
||||||
|
constructor(packet: NiimbotPacket) {
|
||||||
|
super("packetreceived");
|
||||||
|
this.packet = packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PacketSentEvent extends Event {
|
||||||
|
packet: NiimbotPacket;
|
||||||
|
constructor(packet: NiimbotPacket) {
|
||||||
|
super("packetsent");
|
||||||
|
this.packet = packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawPacketSentEvent extends Event {
|
||||||
|
data: Uint8Array;
|
||||||
|
constructor(data: Uint8Array) {
|
||||||
|
super("rawpacketsent");
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RawPacketReceivedEvent extends Event {
|
||||||
|
data: Uint8Array;
|
||||||
|
constructor(data: Uint8Array) {
|
||||||
|
super("rawpacketreceived");
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeartbeatEvent extends Event {
|
||||||
|
data: HeartbeatData;
|
||||||
|
constructor(data: HeartbeatData) {
|
||||||
|
super("heartbeat");
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrinterInfoFetchedEvent extends Event {
|
||||||
|
info: PrinterInfo;
|
||||||
|
constructor(info: PrinterInfo) {
|
||||||
|
super("printerinfofetched");
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrintProgressEvent extends Event {
|
||||||
|
/** 0 – n */
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
pagesTotal: number;
|
||||||
|
/** 0 – 100 */
|
||||||
|
pagePrintProgress: number;
|
||||||
|
/** 0 – 100 */
|
||||||
|
pageFeedProgress: number;
|
||||||
|
|
||||||
|
constructor(page: number, pagesTotal: number, pagePrintProgress: number, pageFeedProgress: number) {
|
||||||
|
super("printprogress");
|
||||||
|
this.page = page;
|
||||||
|
this.pagesTotal = pagesTotal;
|
||||||
|
this.pagePrintProgress = pagePrintProgress;
|
||||||
|
this.pageFeedProgress = pageFeedProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientEventMap {
|
||||||
|
connect: ConnectEvent;
|
||||||
|
disconnect: DisconnectEvent;
|
||||||
|
rawpacketsent: RawPacketSentEvent;
|
||||||
|
rawpacketreceived: RawPacketReceivedEvent;
|
||||||
|
packetreceived: PacketReceivedEvent;
|
||||||
|
packetsent: PacketSentEvent;
|
||||||
|
heartbeat: HeartbeatEvent;
|
||||||
|
printerinfofetched: PrinterInfoFetchedEvent;
|
||||||
|
printprogress: PrintProgressEvent;
|
||||||
|
}
|
155
src/client/index.ts
Normal file
155
src/client/index.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { TypedEventTarget } from "typescript-event-target";
|
||||||
|
import {
|
||||||
|
AutoShutdownTime,
|
||||||
|
BatteryChargeLevel,
|
||||||
|
ConnectResult,
|
||||||
|
getPrintTaskVersion,
|
||||||
|
LabelType,
|
||||||
|
NiimbotPacket,
|
||||||
|
PrintTaskVersion,
|
||||||
|
ClientEventMap,
|
||||||
|
HeartbeatEvent,
|
||||||
|
PacketSentEvent,
|
||||||
|
PrinterInfoFetchedEvent,
|
||||||
|
Abstraction,
|
||||||
|
getPrinterMetaById,
|
||||||
|
PrinterModelMeta,
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
export type ConnectionInfo = {
|
||||||
|
deviceName?: string;
|
||||||
|
result: ConnectResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PrinterInfo {
|
||||||
|
connectResult?: ConnectResult;
|
||||||
|
protocolVersion?: number;
|
||||||
|
model_id?: number;
|
||||||
|
serial?: string;
|
||||||
|
mac?: string;
|
||||||
|
charge?: BatteryChargeLevel;
|
||||||
|
autoShutdownTime?: AutoShutdownTime;
|
||||||
|
labelType?: LabelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class NiimbotAbstractClient extends TypedEventTarget<ClientEventMap> {
|
||||||
|
public readonly abstraction: Abstraction;
|
||||||
|
protected info: PrinterInfo = {};
|
||||||
|
private heartbeatTimer?: NodeJS.Timeout;
|
||||||
|
/** https://github.com/MultiMote/niimblue/issues/5 */
|
||||||
|
protected packetIntervalMs: number = 10;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.abstraction = new Abstraction(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect to printer port */
|
||||||
|
public abstract connect(): Promise<ConnectionInfo>;
|
||||||
|
|
||||||
|
/** Disconnect from printer port */
|
||||||
|
public abstract disconnect(): Promise<void>;
|
||||||
|
|
||||||
|
public abstract isConnected(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send packet and wait for response.
|
||||||
|
* If packet.responsePacketCommandId is defined, it will wait for packet with this command id.
|
||||||
|
*/
|
||||||
|
public abstract sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs?: number): Promise<NiimbotPacket>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send raw bytes to the printer port.
|
||||||
|
*
|
||||||
|
* @param data Bytes to send.
|
||||||
|
* @param force Ignore mutex lock. You should avoid using it.
|
||||||
|
*/
|
||||||
|
public abstract sendRaw(data: Uint8Array, force?: boolean): Promise<void>;
|
||||||
|
|
||||||
|
public async sendPacket(packet: NiimbotPacket, force?: boolean) {
|
||||||
|
await this.sendRaw(packet.toBytes(), force);
|
||||||
|
this.dispatchTypedEvent("packetsent", new PacketSentEvent(packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send "connect" packet and fetch the protocol version */
|
||||||
|
protected async initialNegotiate(): Promise<void> {
|
||||||
|
const cfg = this.info;
|
||||||
|
cfg.connectResult = await this.abstraction.connectResult();
|
||||||
|
cfg.protocolVersion = 0;
|
||||||
|
|
||||||
|
if (cfg.connectResult === ConnectResult.ConnectedNew) {
|
||||||
|
cfg.protocolVersion = 1;
|
||||||
|
} else if (cfg.connectResult === ConnectResult.ConnectedV3) {
|
||||||
|
const statusData = await this.abstraction.getPrinterStatusData();
|
||||||
|
cfg.protocolVersion = statusData.protocolVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchPrinterInfo(): Promise<PrinterInfo> {
|
||||||
|
// console.log(await this.abstraction.getPrinterStatusData());
|
||||||
|
this.info.model_id = await this.abstraction.getPrinterModel();
|
||||||
|
this.info.serial = await this.abstraction.getPrinterSerialNumber();
|
||||||
|
this.info.mac = await this.abstraction.getPrinterBluetoothMacAddress();
|
||||||
|
this.info.charge = await this.abstraction.getBatteryChargeLevel();
|
||||||
|
this.info.autoShutdownTime = await this.abstraction.getAutoShutDownTime();
|
||||||
|
this.info.labelType = await this.abstraction.getLabelType();
|
||||||
|
this.dispatchTypedEvent("printerinfofetched", new PrinterInfoFetchedEvent(this.info));
|
||||||
|
return this.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPrinterInfo(): PrinterInfo {
|
||||||
|
return this.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the heartbeat timer, "heartbeat" is emitted after packet received.
|
||||||
|
*
|
||||||
|
* @param interval Heartbeat interval, default is 1000ms
|
||||||
|
*/
|
||||||
|
public startHeartbeat(intervalMs: number = 1000): void {
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
this.abstraction
|
||||||
|
.heartbeat()
|
||||||
|
.then((data) => {
|
||||||
|
this.dispatchTypedEvent("heartbeat", new HeartbeatEvent(data));
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopHeartbeat(): void {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isHeartbeatStarted(): boolean {
|
||||||
|
return this.heartbeatTimer === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get printer capabilities based on the printer model. Model library is hardcoded. */
|
||||||
|
public getModelMetadata(): PrinterModelMeta | undefined {
|
||||||
|
if (this.info.model_id === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getPrinterMetaById(this.info.model_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine print task version if any */
|
||||||
|
public getPrintTaskVersion(): PrintTaskVersion | undefined {
|
||||||
|
const meta = this.getModelMetadata();
|
||||||
|
|
||||||
|
if (meta === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPrintTaskVersion(meta.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPacketInterval(milliseconds: number) {
|
||||||
|
this.packetIntervalMs = milliseconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./events";
|
||||||
|
export * from "./bluetooth_impl";
|
||||||
|
export * from "./serial_impl";
|
184
src/client/serial_impl.ts
Normal file
184
src/client/serial_impl.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { Mutex } from "async-mutex";
|
||||||
|
import { ConnectionInfo, NiimbotAbstractClient, ConnectResult, NiimbotPacket, ResponseCommandId, Utils } from "..";
|
||||||
|
import {
|
||||||
|
ConnectEvent,
|
||||||
|
DisconnectEvent,
|
||||||
|
PacketReceivedEvent,
|
||||||
|
RawPacketReceivedEvent,
|
||||||
|
RawPacketSentEvent,
|
||||||
|
} from "./events";
|
||||||
|
|
||||||
|
/** Uses Web Serial API */
|
||||||
|
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();
|
||||||
|
|
||||||
|
const _port: SerialPort = await navigator.serial.requestPort();
|
||||||
|
|
||||||
|
_port.addEventListener("disconnect", () => {
|
||||||
|
this.port = undefined;
|
||||||
|
console.log("serial disconnect event");
|
||||||
|
this.dispatchTypedEvent("disconnect", new DisconnectEvent());
|
||||||
|
});
|
||||||
|
|
||||||
|
await _port.open({ baudRate: 115200 });
|
||||||
|
|
||||||
|
if (_port.readable === null) {
|
||||||
|
throw new Error("Port is not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_port.writable === null) {
|
||||||
|
throw new Error("Port is not writable");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.port = _port;
|
||||||
|
const info = _port.getInfo();
|
||||||
|
this.writer = _port.writable.getWriter();
|
||||||
|
this.reader = _port.readable.getReader();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
void (async () => {
|
||||||
|
await this.waitSerialData();
|
||||||
|
})();
|
||||||
|
}, 1); // todo: maybe some other way exists
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.initialNegotiate();
|
||||||
|
await this.fetchPrinterInfo();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Unable to fetch printer info (is it turned on?).");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ConnectionInfo = {
|
||||||
|
deviceName: `Serial (VID:${info.usbVendorId?.toString(16)} PID:${info.usbProductId?.toString(16)})`,
|
||||||
|
result: this.info.connectResult ?? ConnectResult.FirmwareErrors,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dispatchTypedEvent("connect", new ConnectEvent(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitSerialData() {
|
||||||
|
let buf = new Uint8Array();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const result = await this.reader!.read();
|
||||||
|
if (result.value) {
|
||||||
|
// console.info(`<< serial chunk ${Utils.bufToHex(result.value)}`);
|
||||||
|
|
||||||
|
const newBuf = new Uint8Array(buf.length + result.value.length);
|
||||||
|
newBuf.set(buf, 0);
|
||||||
|
newBuf.set(result.value, buf.length);
|
||||||
|
buf = newBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.done) {
|
||||||
|
console.log("done");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packets: NiimbotPacket[] = NiimbotPacket.fromBytesMultiPacket(buf);
|
||||||
|
|
||||||
|
if (packets.length > 0) {
|
||||||
|
this.dispatchTypedEvent("rawpacketreceived", new RawPacketReceivedEvent(buf));
|
||||||
|
|
||||||
|
packets.forEach((p) => {
|
||||||
|
this.dispatchTypedEvent("packetreceived", new PacketReceivedEvent(p));
|
||||||
|
});
|
||||||
|
|
||||||
|
buf = new Uint8Array();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.info(`Incomplete packet, ignoring:${Utils.bufToHex(buf)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnect() {
|
||||||
|
this.stopHeartbeat();
|
||||||
|
|
||||||
|
if (this.writer !== undefined) {
|
||||||
|
this.writer.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reader !== undefined) {
|
||||||
|
this.reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.port !== undefined) {
|
||||||
|
await this.port.close();
|
||||||
|
this.dispatchTypedEvent("disconnect", new DisconnectEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.port = undefined;
|
||||||
|
this.writer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (
|
||||||
|
packet.validResponseIds.length === 0 ||
|
||||||
|
packet.validResponseIds.includes(evt.packet.command as ResponseCommandId)
|
||||||
|
) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.removeEventListener("packetreceived", listener);
|
||||||
|
resolve(evt.packet);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.removeEventListener("packetreceived", listener);
|
||||||
|
reject(new Error(`Timeout waiting response (waited for ${Utils.bufToHex(packet.validResponseIds, ", ")})`));
|
||||||
|
}, timeoutMs ?? 1000);
|
||||||
|
|
||||||
|
this.addEventListener("packetreceived", listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendRaw(data: Uint8Array, force?: boolean) {
|
||||||
|
const send = async () => {
|
||||||
|
if (this.writer === undefined) {
|
||||||
|
throw new Error("Port is not writable");
|
||||||
|
}
|
||||||
|
await Utils.sleep(this.packetIntervalMs);
|
||||||
|
await this.writer.write(data);
|
||||||
|
this.dispatchTypedEvent("rawpacketsent", new RawPacketSentEvent(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
await send();
|
||||||
|
} else {
|
||||||
|
await this.mutex.runExclusive(send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
src/image_encoder.ts
Normal file
116
src/image_encoder.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { Utils } from ".";
|
||||||
|
|
||||||
|
export type ImageRow = {
|
||||||
|
dataType: "void" | "pixels";
|
||||||
|
rowNumber: number;
|
||||||
|
repeat: number;
|
||||||
|
blackPixelsCount: number;
|
||||||
|
rowData?: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EncodedImage = {
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
rowsData: ImageRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrintDirection = "left" | "top";
|
||||||
|
|
||||||
|
export class ImageEncoder {
|
||||||
|
/** printDirection = "left" rotates image for 90 degrees clockwise */
|
||||||
|
public static encodeCanvas(canvas: HTMLCanvasElement, printDirection: PrintDirection = "left"): EncodedImage {
|
||||||
|
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
|
||||||
|
const iData: ImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const rowsData: ImageRow[] = [];
|
||||||
|
|
||||||
|
let cols: number = canvas.width;
|
||||||
|
let rows: number = canvas.height;
|
||||||
|
|
||||||
|
if (printDirection === "left") {
|
||||||
|
cols = canvas.height;
|
||||||
|
rows = canvas.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cols % 8 !== 0) {
|
||||||
|
throw new Error("Column count must be multiple of 8");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
let isVoid: boolean = true;
|
||||||
|
let blackPixelsCount: number = 0;
|
||||||
|
const rowData = new Uint8Array(cols / 8);
|
||||||
|
|
||||||
|
for (let colOct = 0; colOct < cols / 8; colOct++) {
|
||||||
|
let pixelsOctet: number = 0;
|
||||||
|
for (let colBit = 0; colBit < 8; colBit++) {
|
||||||
|
if (ImageEncoder.isPixelNonWhite(iData, colOct * 8 + colBit, row, printDirection)) {
|
||||||
|
pixelsOctet |= 1 << (7 - colBit);
|
||||||
|
isVoid = false;
|
||||||
|
blackPixelsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowData[colOct] = pixelsOctet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPart: ImageRow = {
|
||||||
|
dataType: isVoid ? "void" : "pixels",
|
||||||
|
rowNumber: row,
|
||||||
|
repeat: 1,
|
||||||
|
rowData: isVoid ? undefined : rowData,
|
||||||
|
blackPixelsCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check previous row and increment repeats instead of adding new row if data is same
|
||||||
|
if (rowsData.length === 0) {
|
||||||
|
rowsData.push(newPart);
|
||||||
|
} else {
|
||||||
|
const lastPacket: ImageRow = rowsData[rowsData.length - 1];
|
||||||
|
let same: boolean = newPart.dataType === lastPacket.dataType;
|
||||||
|
|
||||||
|
if (same && newPart.dataType === "pixels") {
|
||||||
|
same = Utils.u8ArraysEqual(newPart.rowData!, lastPacket.rowData!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (same) {
|
||||||
|
lastPacket.repeat++;
|
||||||
|
} else {
|
||||||
|
rowsData.push(newPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cols, rows, rowsData };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** printDirection = "left" rotates image to 90 degrees clockwise */
|
||||||
|
public static isPixelNonWhite(iData: ImageData, x: number, y: number, printDirection: PrintDirection = "left"): boolean {
|
||||||
|
let idx = y * iData.width + x;
|
||||||
|
|
||||||
|
if (printDirection === "left") {
|
||||||
|
idx = (iData.height - 1 - x) * iData.width + y;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx *= 4;
|
||||||
|
return iData.data[idx] !== 255 || iData.data[idx + 1] !== 255 || iData.data[idx + 2] !== 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data Pixels encoded by {@link encodeCanvas} (byte is 8 pixels)
|
||||||
|
* @returns Array of indexes where every index stored in two bytes (big endian)
|
||||||
|
*/
|
||||||
|
public static indexPixels(data: Uint8Array): Uint8Array {
|
||||||
|
const result: number[] = [];
|
||||||
|
|
||||||
|
for (let bytePos = 0; bytePos < data.byteLength; bytePos++) {
|
||||||
|
const b: number = data[bytePos];
|
||||||
|
for (let bitPos = 0; bitPos < 8; bitPos++) {
|
||||||
|
// iterate from most significant bit of byte
|
||||||
|
if (b & (1 << (7 - bitPos))) {
|
||||||
|
result.push(...Utils.u16ToBytes(bytePos * 8 + bitPos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(result);
|
||||||
|
}
|
||||||
|
}
|
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./client";
|
||||||
|
export * from "./packets";
|
||||||
|
export * from "./image_encoder";
|
||||||
|
export * from "./utils";
|
||||||
|
export * from "./printer_models";
|
342
src/packets/abstraction.ts
Normal file
342
src/packets/abstraction.ts
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
import {
|
||||||
|
AutoShutdownTime,
|
||||||
|
BatteryChargeLevel,
|
||||||
|
ConnectResult,
|
||||||
|
HeartbeatType,
|
||||||
|
LabelType,
|
||||||
|
NiimbotPacket,
|
||||||
|
PacketGenerator,
|
||||||
|
PrinterInfoType,
|
||||||
|
PrintOptions,
|
||||||
|
ResponseCommandId,
|
||||||
|
SoundSettingsItemType,
|
||||||
|
SoundSettingsType,
|
||||||
|
EncodedImage,
|
||||||
|
NiimbotAbstractClient,
|
||||||
|
Utils,
|
||||||
|
Validators,
|
||||||
|
PrintTaskVersion,
|
||||||
|
PrintProgressEvent,
|
||||||
|
SequentialDataReader
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
export class PrintError extends Error {
|
||||||
|
public readonly reasonId: number;
|
||||||
|
|
||||||
|
constructor(message: string, reasonId: number) {
|
||||||
|
super(message);
|
||||||
|
this.reasonId = reasonId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintStatus {
|
||||||
|
/** 0 – n */
|
||||||
|
page: number;
|
||||||
|
/** 0 – 100 */
|
||||||
|
pagePrintProgress: number;
|
||||||
|
/** 0 – 100 */
|
||||||
|
pageFeedProgress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RfidInfo {
|
||||||
|
tagPresent: boolean;
|
||||||
|
uuid: string;
|
||||||
|
barCode: string;
|
||||||
|
serialNumber: string;
|
||||||
|
allPaper: number;
|
||||||
|
usedPaper: number;
|
||||||
|
consumablesType: LabelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** closingState inverted on some printers */
|
||||||
|
export interface HeartbeatData {
|
||||||
|
paperState: number;
|
||||||
|
rfidReadState: number;
|
||||||
|
lidClosed: boolean;
|
||||||
|
powerLevel: BatteryChargeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoundSettings {
|
||||||
|
category: SoundSettingsType;
|
||||||
|
item: SoundSettingsItemType;
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrinterStatusData {
|
||||||
|
supportColor: number;
|
||||||
|
protocolVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Not sure for name. */
|
||||||
|
export class Abstraction {
|
||||||
|
private readonly DEFAULT_TIMEOUT: number = 1_000;
|
||||||
|
private client: NiimbotAbstractClient;
|
||||||
|
private timeout: number = this.DEFAULT_TIMEOUT;
|
||||||
|
private statusPollTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
constructor(client: NiimbotAbstractClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTimeout(): number {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTimeout(value: number) {
|
||||||
|
this.timeout = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDefaultTimeout() {
|
||||||
|
this.timeout = this.DEFAULT_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send packet and wait for response */
|
||||||
|
private async send(packet: NiimbotPacket): Promise<NiimbotPacket> {
|
||||||
|
return this.client.sendPacketWaitResponse(packet, this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPrintStatus(): Promise<PrintStatus> {
|
||||||
|
const packet = await this.send(PacketGenerator.printStatus());
|
||||||
|
|
||||||
|
if (packet.command === ResponseCommandId.In_PrintError) {
|
||||||
|
Validators.u8ArrayLengthEquals(packet.data, 1);
|
||||||
|
throw new PrintError(`Print error (${ResponseCommandId[packet.command]} packet received)`, packet.data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Validators.u8ArrayLengthAtLeast(packet.data, 4); // can be 8, 10, but ignore it for now
|
||||||
|
|
||||||
|
const r = new SequentialDataReader(packet.data);
|
||||||
|
const page = r.readI16();
|
||||||
|
const pagePrintProgress = r.readI8();
|
||||||
|
const pageFeedProgress = r.readI8();
|
||||||
|
|
||||||
|
if (packet.dataLength === 10) {
|
||||||
|
r.skip(2);
|
||||||
|
const error = r.readI8();
|
||||||
|
|
||||||
|
if (error !== 0) {
|
||||||
|
throw new PrintError(`Print error (${ResponseCommandId[packet.command]} packet flag)`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { page, pagePrintProgress, pageFeedProgress };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectResult(): Promise<ConnectResult> {
|
||||||
|
const packet = await this.send(PacketGenerator.connect());
|
||||||
|
Validators.u8ArrayLengthAtLeast(packet.data, 1);
|
||||||
|
return packet.data[0] as ConnectResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPrinterStatusData(): Promise<PrinterStatusData> {
|
||||||
|
let protocolVersion = 0;
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterStatusData());
|
||||||
|
let supportColor = 0;
|
||||||
|
|
||||||
|
if (packet.dataLength > 12) {
|
||||||
|
supportColor = packet.data[10];
|
||||||
|
|
||||||
|
const n = packet.data[11] * 100 + packet.data[12];
|
||||||
|
if (n >= 204 && n < 300) {
|
||||||
|
protocolVersion = 3;
|
||||||
|
}
|
||||||
|
if (n >= 301) {
|
||||||
|
protocolVersion = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
supportColor,
|
||||||
|
protocolVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPrinterModel(): Promise<number> {
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterInfo(PrinterInfoType.PrinterModelId));
|
||||||
|
Validators.u8ArrayLengthEquals(packet.data, 2);
|
||||||
|
return Utils.bytesToI16(packet.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read paper nfc tag info */
|
||||||
|
public async rfidInfo(): Promise<RfidInfo> {
|
||||||
|
const packet = await this.send(PacketGenerator.rfidInfo());
|
||||||
|
|
||||||
|
const info: RfidInfo = {
|
||||||
|
tagPresent: false,
|
||||||
|
uuid: "",
|
||||||
|
barCode: "",
|
||||||
|
serialNumber: "",
|
||||||
|
allPaper: -1,
|
||||||
|
usedPaper: -1,
|
||||||
|
consumablesType: LabelType.Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (packet.dataLength === 1) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = new SequentialDataReader(packet.data);
|
||||||
|
info.tagPresent = true;
|
||||||
|
info.uuid = Utils.bufToHex(r.readBytes(8), "");
|
||||||
|
info.barCode = r.readVString();
|
||||||
|
info.serialNumber = r.readVString();
|
||||||
|
info.allPaper = r.readI16();
|
||||||
|
info.usedPaper = r.readI16();
|
||||||
|
info.consumablesType = r.readI8() as LabelType;
|
||||||
|
r.end();
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async heartbeat(): Promise<HeartbeatData> {
|
||||||
|
const packet = await this.send(PacketGenerator.heartbeat(HeartbeatType.Advanced1));
|
||||||
|
|
||||||
|
const info: HeartbeatData = {
|
||||||
|
paperState: -1,
|
||||||
|
rfidReadState: -1,
|
||||||
|
lidClosed: false,
|
||||||
|
powerLevel: BatteryChargeLevel.Charge0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// originally expected packet length is bound to model id, but we make it more robust and simple
|
||||||
|
const len = packet.dataLength;
|
||||||
|
const r = new SequentialDataReader(packet.data);
|
||||||
|
|
||||||
|
if (len === 10) {
|
||||||
|
// d110
|
||||||
|
r.skip(8);
|
||||||
|
info.lidClosed = r.readBool();
|
||||||
|
info.powerLevel = r.readI8();
|
||||||
|
} else if (len === 20) {
|
||||||
|
r.skip(18);
|
||||||
|
info.paperState = r.readI8();
|
||||||
|
info.rfidReadState = r.readI8();
|
||||||
|
} else if (len === 19) {
|
||||||
|
r.skip(15);
|
||||||
|
info.lidClosed = r.readBool();
|
||||||
|
info.powerLevel = r.readI8();
|
||||||
|
info.paperState = r.readI8();
|
||||||
|
info.rfidReadState = r.readI8();
|
||||||
|
} else if (len === 13) {
|
||||||
|
// b1
|
||||||
|
r.skip(9);
|
||||||
|
info.lidClosed = r.readBool();
|
||||||
|
info.powerLevel = r.readI8();
|
||||||
|
info.paperState = r.readI8();
|
||||||
|
info.rfidReadState = r.readI8();
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid heartbeat length");
|
||||||
|
}
|
||||||
|
r.end();
|
||||||
|
|
||||||
|
const model: number | undefined = this.client.getPrinterInfo().model_id;
|
||||||
|
|
||||||
|
if (model !== undefined && ![512, 514, 513, 2304, 1792, 3584, 5120, 2560, 3840, 4352, 272].includes(model)) {
|
||||||
|
info.lidClosed = !info.lidClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBatteryChargeLevel(): Promise<BatteryChargeLevel> {
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterInfo(PrinterInfoType.BatteryChargeLevel));
|
||||||
|
Validators.u8ArrayLengthEquals(packet.data, 1);
|
||||||
|
return packet.data[0] as BatteryChargeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAutoShutDownTime(): Promise<AutoShutdownTime> {
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterInfo(PrinterInfoType.AutoShutdownTime));
|
||||||
|
Validators.u8ArrayLengthEquals(packet.data, 1);
|
||||||
|
return packet.data[0] as AutoShutdownTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setAutoShutDownTime(time: AutoShutdownTime): Promise<void> {
|
||||||
|
await this.send(PacketGenerator.setAutoShutDownTime(time));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLabelType(): Promise<LabelType> {
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterInfo(PrinterInfoType.LabelType));
|
||||||
|
Validators.u8ArrayLengthEquals(packet.data, 1);
|
||||||
|
return packet.data[0] as LabelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPrinterSerialNumber(): Promise<string> {
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterInfo(PrinterInfoType.SerialNumber));
|
||||||
|
Validators.u8ArrayLengthAtLeast(packet.data, 1);
|
||||||
|
return Utils.u8ArrayToString(packet.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPrinterBluetoothMacAddress(): Promise<string> {
|
||||||
|
const packet = await this.send(PacketGenerator.getPrinterInfo(PrinterInfoType.BluetoothAddress));
|
||||||
|
Validators.u8ArrayLengthAtLeast(packet.data, 1);
|
||||||
|
return Utils.bufToHex(packet.data.reverse(), ":");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isSoundEnabled(soundType: SoundSettingsItemType): Promise<boolean> {
|
||||||
|
const packet = await this.send(PacketGenerator.getSoundSettings(soundType));
|
||||||
|
Validators.u8ArrayLengthEquals(packet.data, 3);
|
||||||
|
const value = !!packet.data[2];
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setSoundEnabled(soundType: SoundSettingsItemType, value: boolean): Promise<void> {
|
||||||
|
await this.send(PacketGenerator.setSoundSettings(soundType, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear settings */
|
||||||
|
public async printerReset(): Promise<void> {
|
||||||
|
await this.send(PacketGenerator.printerReset());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async print(
|
||||||
|
taskVersion: PrintTaskVersion,
|
||||||
|
image: EncodedImage,
|
||||||
|
options?: PrintOptions,
|
||||||
|
timeout?: number
|
||||||
|
): Promise<void> {
|
||||||
|
this.setTimeout(timeout ?? 10_000);
|
||||||
|
const packets: NiimbotPacket[] = PacketGenerator.generatePrintSequence(taskVersion, image, options);
|
||||||
|
try {
|
||||||
|
for (const element of packets) {
|
||||||
|
await this.send(element);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.setDefaultTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll printer every {@link pollIntervalMs} and resolve when printer pages equals {@link pagesToPrint}, pagePrintProgress=100, pageFeedProgress=100.
|
||||||
|
*
|
||||||
|
* printprogress event is firing during this process.
|
||||||
|
*
|
||||||
|
* @param pagesToPrint Total pages to print.
|
||||||
|
* @param pollIntervalMs Poll interval in milliseconds.
|
||||||
|
*/
|
||||||
|
public async waitUntilPrintFinished(pagesToPrint: number, pollIntervalMs: number = 300): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.statusPollTimer = setInterval(() => {
|
||||||
|
this.getPrintStatus()
|
||||||
|
.then((status: PrintStatus) => {
|
||||||
|
this.client.dispatchTypedEvent(
|
||||||
|
"printprogress",
|
||||||
|
new PrintProgressEvent(status.page, pagesToPrint, status.pagePrintProgress, status.pageFeedProgress)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status.page === pagesToPrint && status.pagePrintProgress === 100 && status.pageFeedProgress === 100) {
|
||||||
|
clearInterval(this.statusPollTimer);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: unknown) => {
|
||||||
|
clearInterval(this.statusPollTimer);
|
||||||
|
reject(e as Error);
|
||||||
|
});
|
||||||
|
}, pollIntervalMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async printEnd(): Promise<void> {
|
||||||
|
await this.send(PacketGenerator.printEnd());
|
||||||
|
}
|
||||||
|
}
|
74
src/packets/data_reader.ts
Normal file
74
src/packets/data_reader.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { Utils } from "..";
|
||||||
|
|
||||||
|
/** Utility class to sequentially fetch data from byte array. EOF checks included. */
|
||||||
|
export class SequentialDataReader {
|
||||||
|
private bytes: Uint8Array;
|
||||||
|
private offset: number;
|
||||||
|
|
||||||
|
constructor(bytes: Uint8Array) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check available bytes bytes and throw exception if EOF met */
|
||||||
|
private willRead(count: number) {
|
||||||
|
// console.log(`willRead ${count} (offset becomes ${this.offset+count} / ${this.bytes.length})`)
|
||||||
|
if (this.offset + count > this.bytes.length) {
|
||||||
|
throw new Error("Tried to read too much data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Skip bytes */
|
||||||
|
skip(len: number): void {
|
||||||
|
this.willRead(len);
|
||||||
|
this.offset += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read fixed length bytes */
|
||||||
|
readBytes(len: number): Uint8Array {
|
||||||
|
this.willRead(len);
|
||||||
|
const part = this.bytes.slice(this.offset, this.offset + len);
|
||||||
|
this.offset += len;
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read variable length bytes */
|
||||||
|
readVBytes(): Uint8Array {
|
||||||
|
const len = this.readI8();
|
||||||
|
const part: Uint8Array = this.readBytes(len);
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read variable length string */
|
||||||
|
readVString(): string {
|
||||||
|
const part: Uint8Array = this.readVBytes();
|
||||||
|
return Utils.u8ArrayToString(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read 8 bit int (big endian) */
|
||||||
|
readI8(): number {
|
||||||
|
this.willRead(1);
|
||||||
|
const result = this.bytes[this.offset];
|
||||||
|
this.offset += 1;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBool(): boolean {
|
||||||
|
return this.readI8() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read 16 bit int (big endian) */
|
||||||
|
readI16(): number {
|
||||||
|
this.willRead(2);
|
||||||
|
const part = this.bytes.slice(this.offset, this.offset + 2);
|
||||||
|
this.offset += 2;
|
||||||
|
return Utils.bytesToI16(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check EOF condition */
|
||||||
|
end() {
|
||||||
|
if (this.offset != this.bytes.length) {
|
||||||
|
throw new Error("Extra data left");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
165
src/packets/index.ts
Normal file
165
src/packets/index.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
export enum RequestCommandId {
|
||||||
|
Invalid = -1,
|
||||||
|
Connect = 0xc1,
|
||||||
|
CancelPrint = 0xda,
|
||||||
|
Heartbeat = 0xdc,
|
||||||
|
LabelPositioningCalibration = 0x8e, //-114,
|
||||||
|
PageEnd = 0xe3,
|
||||||
|
PrinterLog = 0x05,
|
||||||
|
PageStart = 0x03,
|
||||||
|
PrintBitmapRow = 0x85, // -123
|
||||||
|
PrintBitmapRowIndexed = 0x83, // -125, indexed if black pixels < 6
|
||||||
|
PrintClear = 0x20,
|
||||||
|
PrintEmptyRow = 0x84, // -124
|
||||||
|
PrintEnd = 0xf3,
|
||||||
|
PrinterInfo = 0x40, // See PrinterInfoType
|
||||||
|
PrinterConfig = 0xaf,
|
||||||
|
PrinterStatusData = 0xa5,
|
||||||
|
PrinterReset = 0x28,
|
||||||
|
PrintQuantity = 0x15,
|
||||||
|
PrintStart = 0x01,
|
||||||
|
PrintStatus = 0xa3,
|
||||||
|
RfidInfo = 0x1a,
|
||||||
|
RfidInfo2 = 0x1c,
|
||||||
|
RfidSuccessTimes = 0x54,
|
||||||
|
SetAutoShutdownTime = 0x27, ///
|
||||||
|
SetDensity = 0x21,
|
||||||
|
SetLabelType = 0x23 /* D11 - 1,5, for D110 able to set 1,2,3,5; see LabelType */,
|
||||||
|
SetPageSize = 0x13, // 2, 4 or 6 bytes
|
||||||
|
SoundSettings = 0x58,
|
||||||
|
AntiFake = 0x0b, // some info request (niimbot app), 01 long 02 short
|
||||||
|
WriteRFID = 0x70, // same as GetVolumeLevel???
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ResponseCommandId {
|
||||||
|
Invalid = -1,
|
||||||
|
In_NotSupported = 0x00,
|
||||||
|
In_Connect = 0xc2,
|
||||||
|
In_AntiFake = 0x0c,
|
||||||
|
In_HeartbeatAdvanced1 = 0xdd,
|
||||||
|
In_HeartbeatBasic = 0xde,
|
||||||
|
In_HeartbeatUnknown = 0xdf,
|
||||||
|
In_HeartbeatAdvanced2 = 0xd9,
|
||||||
|
In_PageStart = 0x04,
|
||||||
|
In_PrintClear = 0x30,
|
||||||
|
/** sent by printer after {@link RequestCommandId.PageEnd} with {@link ResponseCommandId.In_PageEnd} */
|
||||||
|
In_PrinterCheckLine = 0xd3,
|
||||||
|
In_PrintEnd = 0xf4,
|
||||||
|
In_PrinterConfig = 0xbf,
|
||||||
|
In_PrinterInfoAutoShutDownTime = 0x47,
|
||||||
|
In_PrinterInfoBluetoothAddress = 0x4d,
|
||||||
|
In_PrinterInfoSpeed = 0x42,
|
||||||
|
In_PrinterInfoDensity = 0x41,
|
||||||
|
In_PrinterInfoLanguage = 0x46,
|
||||||
|
In_PrinterInfoChargeLevel = 0x4a,
|
||||||
|
In_PrinterInfoHardWareVersion = 0x4c,
|
||||||
|
In_PrinterInfoLabelType = 0x43,
|
||||||
|
In_PrinterInfoPrinterCode = 0x48,
|
||||||
|
In_PrinterInfoSerialNumber = 0x4b,
|
||||||
|
In_PrinterInfoSoftWareVersion = 0x49,
|
||||||
|
In_PrinterInfoArea = 0x4f,
|
||||||
|
In_PrinterStatusData = 0xb5,
|
||||||
|
In_PrinterReset = 0x38,
|
||||||
|
In_PrintStatus = 0xb3,
|
||||||
|
In_PrintError = 0xdb, // For example, sent on SetPageSize when page print is not started
|
||||||
|
In_PrintQuantity = 0x16,
|
||||||
|
In_PrintStart = 0x02,
|
||||||
|
In_RfidInfo = 0x1b,
|
||||||
|
In_RfidSuccessTimes = 0x64,
|
||||||
|
In_SetAutoShutdownTime = 0x37,
|
||||||
|
In_SetDensity = 0x31,
|
||||||
|
In_SetLabelType = 0x33,
|
||||||
|
In_SetPageSize = 0x14,
|
||||||
|
In_SoundSettings = 0x68,
|
||||||
|
In_PageEnd = 0xe4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PrinterInfoType {
|
||||||
|
Density = 1,
|
||||||
|
Speed = 2,
|
||||||
|
LabelType = 3,
|
||||||
|
Language = 6,
|
||||||
|
AutoShutdownTime = 7,
|
||||||
|
/** See {@link PrinterId} */
|
||||||
|
PrinterModelId = 8,
|
||||||
|
SoftWareVersion = 9,
|
||||||
|
BatteryChargeLevel = 10,
|
||||||
|
SerialNumber = 11,
|
||||||
|
HardWareVersion = 12,
|
||||||
|
BluetoothAddress = 13,
|
||||||
|
PrintMode = 14,
|
||||||
|
Area = 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SoundSettingsType {
|
||||||
|
SetSound = 0x01,
|
||||||
|
GetSoundState = 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SoundSettingsItemType {
|
||||||
|
BluetoothConnectionSound = 0x01,
|
||||||
|
PowerSound = 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LabelType {
|
||||||
|
Invalid = 0,
|
||||||
|
/** Default for D11 and similar */
|
||||||
|
WithGaps = 1,
|
||||||
|
Black = 2,
|
||||||
|
Continuous = 3,
|
||||||
|
Perforated = 4,
|
||||||
|
Transparent = 5,
|
||||||
|
PvcTag = 6,
|
||||||
|
BlackMarkGap = 10,
|
||||||
|
HeatShrinkTube = 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HeartbeatType {
|
||||||
|
Advanced1 = 1,
|
||||||
|
Basic = 2,
|
||||||
|
Unknown = 3,
|
||||||
|
Advanced2 = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AutoShutdownTime {
|
||||||
|
Shutdown15min = 1,
|
||||||
|
Shutdown30min = 2,
|
||||||
|
Shutdown45min = 3,
|
||||||
|
Shutdown60min = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Battery charge level */
|
||||||
|
export enum BatteryChargeLevel {
|
||||||
|
Charge0 = 0,
|
||||||
|
Charge25 = 1,
|
||||||
|
Charge50 = 2,
|
||||||
|
Charge75 = 3,
|
||||||
|
Charge100 = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConnectResult {
|
||||||
|
Disconnect = 0,
|
||||||
|
Connected = 1,
|
||||||
|
ConnectedNew = 2,
|
||||||
|
ConnectedV3 = 3,
|
||||||
|
FirmwareErrors = 90,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PrintTaskVersion {
|
||||||
|
/** Used in D11 */
|
||||||
|
V1 = 1,
|
||||||
|
/** Used in B21, D110new */
|
||||||
|
V2,
|
||||||
|
/** Used in B16 */
|
||||||
|
V3,
|
||||||
|
/** Used in B1 */
|
||||||
|
V4,
|
||||||
|
/** Not used */
|
||||||
|
V5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./packet";
|
||||||
|
export * from "./packet_generator";
|
||||||
|
export * from "./abstraction";
|
||||||
|
export * from "./data_reader";
|
||||||
|
export * from "./print_task_versions";
|
194
src/packets/packet.ts
Normal file
194
src/packets/packet.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { Validators } from "../utils";
|
||||||
|
import { RequestCommandId, ResponseCommandId } from ".";
|
||||||
|
|
||||||
|
export class NiimbotPacket {
|
||||||
|
public static readonly HEAD = new Uint8Array([0x55, 0x55]);
|
||||||
|
public static readonly TAIL = new Uint8Array([0xaa, 0xaa]);
|
||||||
|
|
||||||
|
private _command: RequestCommandId | ResponseCommandId;
|
||||||
|
private _data: Uint8Array;
|
||||||
|
private _validResponseIds: ResponseCommandId[];
|
||||||
|
|
||||||
|
/** There can be no response after this request. */
|
||||||
|
private _oneWay: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
command: RequestCommandId | ResponseCommandId,
|
||||||
|
data: Uint8Array | number[],
|
||||||
|
validResponseIds: ResponseCommandId[] = []
|
||||||
|
) {
|
||||||
|
this._command = command;
|
||||||
|
this._data = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||||
|
this._validResponseIds = validResponseIds;
|
||||||
|
this._oneWay = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Data length (header, command, dataLen, checksum, tail are excluded). */
|
||||||
|
public get dataLength(): number {
|
||||||
|
return this._data.length;
|
||||||
|
}
|
||||||
|
public get length(): number {
|
||||||
|
return (
|
||||||
|
NiimbotPacket.HEAD.length + // head
|
||||||
|
1 + // cmd
|
||||||
|
1 + // dataLength
|
||||||
|
this.dataLength +
|
||||||
|
1 + // checksum
|
||||||
|
NiimbotPacket.TAIL.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set oneWay(value: boolean) {
|
||||||
|
this._oneWay = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get oneWay(): boolean {
|
||||||
|
return this._oneWay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get validResponseIds(): ResponseCommandId[] {
|
||||||
|
return this._validResponseIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get command(): RequestCommandId | ResponseCommandId {
|
||||||
|
return this._command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get data(): Uint8Array {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get checksum(): number {
|
||||||
|
let checksum = 0;
|
||||||
|
checksum ^= this._command;
|
||||||
|
checksum ^= this._data.length;
|
||||||
|
this._data.forEach((i: number) => (checksum ^= i));
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [0x55, 0x55, CMD, DATA_LEN, DA =//= TA, CHECKSUM, 0xAA, 0xAA]
|
||||||
|
public toBytes(): Uint8Array {
|
||||||
|
const buf = new ArrayBuffer(
|
||||||
|
NiimbotPacket.HEAD.length + // head
|
||||||
|
1 + // cmd
|
||||||
|
1 + // dataLength
|
||||||
|
this._data.length +
|
||||||
|
1 + // checksum
|
||||||
|
NiimbotPacket.TAIL.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const arr = new Uint8Array(buf);
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
arr.set(NiimbotPacket.HEAD, pos);
|
||||||
|
pos += NiimbotPacket.HEAD.length;
|
||||||
|
|
||||||
|
arr[pos] = this._command;
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
arr[pos] = this._data.length;
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
arr.set(this._data, pos);
|
||||||
|
pos += this._data.length;
|
||||||
|
|
||||||
|
arr[pos] = this.checksum;
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
arr.set(NiimbotPacket.TAIL, pos);
|
||||||
|
|
||||||
|
if (this._command === RequestCommandId.Connect) {
|
||||||
|
// const newArr = new Uint8Array(arr.length + 1);
|
||||||
|
// newArr[0] = 3;
|
||||||
|
// newArr.set(arr, 1);
|
||||||
|
return new Uint8Array([3, ...arr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromBytes(buf: Uint8Array): NiimbotPacket {
|
||||||
|
const head = new Uint8Array(buf.slice(0, 2));
|
||||||
|
const tail = new Uint8Array(buf.slice(buf.length - 2));
|
||||||
|
const minPacketSize =
|
||||||
|
NiimbotPacket.HEAD.length + // head
|
||||||
|
1 + // cmd
|
||||||
|
1 + // dataLength
|
||||||
|
1 + // checksum
|
||||||
|
NiimbotPacket.TAIL.length;
|
||||||
|
|
||||||
|
if (buf.length < minPacketSize) {
|
||||||
|
throw new Error(`Packet is too small (${buf.length} < ${minPacketSize})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
Validators.u8ArraysEqual(head, NiimbotPacket.HEAD, "Invalid packet head");
|
||||||
|
|
||||||
|
Validators.u8ArraysEqual(tail, NiimbotPacket.TAIL, "Invalid packet tail");
|
||||||
|
|
||||||
|
const cmd: number = buf[2];
|
||||||
|
const dataLen: number = buf[3];
|
||||||
|
|
||||||
|
if (buf.length !== minPacketSize + dataLen) {
|
||||||
|
throw new Error(`Invalid packet size (${buf.length} < ${minPacketSize + dataLen})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Uint8Array = new Uint8Array(buf.slice(4, 4 + dataLen));
|
||||||
|
const checksum: number = buf[4 + dataLen];
|
||||||
|
const packet = new NiimbotPacket(cmd, data);
|
||||||
|
|
||||||
|
if (packet.checksum !== checksum) {
|
||||||
|
throw new Error("Invalid packet checksum");
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse data containing one or more packets */
|
||||||
|
public static fromBytesMultiPacket(buf: Uint8Array): NiimbotPacket[] {
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
|
let head1pos = -1;
|
||||||
|
let head2pos = -1;
|
||||||
|
let tail1pos = -1;
|
||||||
|
let tail2pos = -1;
|
||||||
|
|
||||||
|
// split data to chunks by head and tail bytes
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
const v = buf[i];
|
||||||
|
if (v === NiimbotPacket.HEAD[0]) {
|
||||||
|
if (head1pos === -1) {
|
||||||
|
head1pos = i;
|
||||||
|
head2pos = -1;
|
||||||
|
} else {
|
||||||
|
head2pos = i;
|
||||||
|
}
|
||||||
|
} else if (v === NiimbotPacket.TAIL[0]) {
|
||||||
|
if (head1pos !== -1 && head2pos !== -1) {
|
||||||
|
if (tail1pos === -1) {
|
||||||
|
tail1pos = i;
|
||||||
|
tail2pos = -1;
|
||||||
|
} else {
|
||||||
|
tail2pos = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (head1pos !== -1 && head2pos !== -1 && tail1pos !== -1 && tail2pos !== -1) {
|
||||||
|
chunks.push(buf.slice(head1pos, tail2pos + 1));
|
||||||
|
head1pos = -1;
|
||||||
|
head2pos = -1;
|
||||||
|
tail1pos = -1;
|
||||||
|
tail2pos = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunksDataLen: number = chunks.reduce((acc: number, b: Uint8Array) => acc + b.length, 0);
|
||||||
|
|
||||||
|
if (buf.length !== chunksDataLen) {
|
||||||
|
throw new Error("Splitted chunks data length not equals buffer length");
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.map((c) => this.fromBytes(c));
|
||||||
|
}
|
||||||
|
}
|
335
src/packets/packet_generator.ts
Normal file
335
src/packets/packet_generator.ts
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import {
|
||||||
|
AutoShutdownTime,
|
||||||
|
HeartbeatType,
|
||||||
|
LabelType,
|
||||||
|
NiimbotPacket,
|
||||||
|
PrinterInfoType,
|
||||||
|
RequestCommandId,
|
||||||
|
ResponseCommandId,
|
||||||
|
SoundSettingsItemType,
|
||||||
|
SoundSettingsType,
|
||||||
|
PrintTaskVersion,
|
||||||
|
} from ".";
|
||||||
|
import { EncodedImage, ImageEncoder, ImageRow } from "../image_encoder";
|
||||||
|
import { Utils } from "../utils";
|
||||||
|
|
||||||
|
export type PrintOptions = {
|
||||||
|
labelType?: LabelType;
|
||||||
|
density?: number;
|
||||||
|
quantity?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PacketGenerator {
|
||||||
|
public static generic(
|
||||||
|
requestId: RequestCommandId,
|
||||||
|
data: Uint8Array | number[],
|
||||||
|
responseIds: ResponseCommandId[] = []
|
||||||
|
): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(requestId, data, responseIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static connect(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.Connect, [1], [ResponseCommandId.In_Connect]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getPrinterStatusData(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PrinterStatusData, [1], [ResponseCommandId.In_PrinterStatusData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static rfidInfo(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.RfidInfo, [1], [ResponseCommandId.In_RfidInfo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setAutoShutDownTime(time: AutoShutdownTime): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.SetAutoShutdownTime, [time], [ResponseCommandId.In_SetAutoShutdownTime]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getPrinterInfo(type: PrinterInfoType): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.PrinterInfo,
|
||||||
|
[type],
|
||||||
|
[
|
||||||
|
ResponseCommandId.In_PrinterInfoArea,
|
||||||
|
ResponseCommandId.In_PrinterInfoAutoShutDownTime,
|
||||||
|
ResponseCommandId.In_PrinterInfoBluetoothAddress,
|
||||||
|
ResponseCommandId.In_PrinterInfoChargeLevel,
|
||||||
|
ResponseCommandId.In_PrinterInfoDensity,
|
||||||
|
ResponseCommandId.In_PrinterInfoHardWareVersion,
|
||||||
|
ResponseCommandId.In_PrinterInfoLabelType,
|
||||||
|
ResponseCommandId.In_PrinterInfoLanguage,
|
||||||
|
ResponseCommandId.In_PrinterInfoPrinterCode,
|
||||||
|
ResponseCommandId.In_PrinterInfoSerialNumber,
|
||||||
|
ResponseCommandId.In_PrinterInfoSoftWareVersion,
|
||||||
|
ResponseCommandId.In_PrinterInfoSpeed,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setSoundSettings(soundType: SoundSettingsItemType, on: boolean): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.SoundSettings,
|
||||||
|
[SoundSettingsType.SetSound, soundType, on ? 1 : 0],
|
||||||
|
[ResponseCommandId.In_SoundSettings]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getSoundSettings(soundType: SoundSettingsItemType): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.SoundSettings,
|
||||||
|
[SoundSettingsType.GetSoundState, soundType, 1],
|
||||||
|
[ResponseCommandId.In_SoundSettings]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static heartbeat(type: HeartbeatType): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.Heartbeat,
|
||||||
|
[type],
|
||||||
|
[
|
||||||
|
ResponseCommandId.In_HeartbeatBasic,
|
||||||
|
ResponseCommandId.In_HeartbeatUnknown,
|
||||||
|
ResponseCommandId.In_HeartbeatAdvanced1,
|
||||||
|
ResponseCommandId.In_HeartbeatAdvanced2,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setDensity(value: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.SetDensity, [value], [ResponseCommandId.In_SetDensity]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setLabelType(value: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.SetLabelType, [value], [ResponseCommandId.In_SetLabelType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setPageSizeV1(rows: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.SetPageSize,
|
||||||
|
[...Utils.u16ToBytes(rows)],
|
||||||
|
[ResponseCommandId.In_SetPageSize]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B1 behavior: strange, first print is blank or printer prints many copies (use {@link setPageSizeV2} instead)
|
||||||
|
*
|
||||||
|
* D110 behavior: ordinary.
|
||||||
|
*
|
||||||
|
* @param rows Height in pixels
|
||||||
|
* @param cols Width in pixels
|
||||||
|
*/
|
||||||
|
public static setPageSizeV2(rows: number, cols: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.SetPageSize,
|
||||||
|
[...Utils.u16ToBytes(rows), ...Utils.u16ToBytes(cols)],
|
||||||
|
[ResponseCommandId.In_SetPageSize]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param rows Height in pixels
|
||||||
|
* @param cols Width in pixels
|
||||||
|
* @param copiesCount Page instances
|
||||||
|
*/
|
||||||
|
public static setPageSizeV3(rows: number, cols: number, copiesCount: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.SetPageSize,
|
||||||
|
[...Utils.u16ToBytes(rows), ...Utils.u16ToBytes(cols), ...Utils.u16ToBytes(copiesCount)],
|
||||||
|
[ResponseCommandId.In_SetPageSize]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setPageSizeV5(rows: number, cols: number, copiesCount: number, someSize: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.SetPageSize,
|
||||||
|
[
|
||||||
|
...Utils.u16ToBytes(rows),
|
||||||
|
...Utils.u16ToBytes(cols),
|
||||||
|
...Utils.u16ToBytes(copiesCount),
|
||||||
|
...Utils.u16ToBytes(someSize),
|
||||||
|
],
|
||||||
|
[ResponseCommandId.In_SetPageSize]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static setPrintQuantity(quantity: number): NiimbotPacket {
|
||||||
|
const [h, l] = Utils.u16ToBytes(quantity);
|
||||||
|
return new NiimbotPacket(RequestCommandId.PrintQuantity, [h, l]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printStatus(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.PrintStatus,
|
||||||
|
[1],
|
||||||
|
[ResponseCommandId.In_PrintStatus, ResponseCommandId.In_PrintError]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public static printerReset(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PrinterReset, [1], [ResponseCommandId.In_PrinterReset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B1 behavior: after {@link pageEnd} paper stops at printhead position, on {@link printEnd} paper moved further.
|
||||||
|
*
|
||||||
|
* D110 behavior: ordinary.
|
||||||
|
* */
|
||||||
|
public static printStart(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PrintStart, [1], [ResponseCommandId.In_PrintStart]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printStartV3(totalPages: number): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.PrintStart,
|
||||||
|
[...Utils.u16ToBytes(totalPages)],
|
||||||
|
[ResponseCommandId.In_PrintStart]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B1 behavior: when {@link totalPages} > 1 after {@link pageEnd} paper stops at printhead position and waits for next page.
|
||||||
|
* When last page ({@link totalPages}) printed paper moved further.
|
||||||
|
*
|
||||||
|
* D110 behavior: ordinary.
|
||||||
|
*
|
||||||
|
* @param totalPages Declare how many pages will be printed
|
||||||
|
*/
|
||||||
|
public static printStartV4(totalPages: number, pageColor: number = 0): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.PrintStart,
|
||||||
|
[...Utils.u16ToBytes(totalPages), 0x00, 0x00, 0x00, 0x00, pageColor],
|
||||||
|
[ResponseCommandId.In_PrintStart]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printStartV5(totalPages: number, pageColor: number = 0, quality: number = 0): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(
|
||||||
|
RequestCommandId.PrintStart,
|
||||||
|
[...Utils.u16ToBytes(totalPages), 0x00, 0x00, 0x00, 0x00, pageColor, quality],
|
||||||
|
[ResponseCommandId.In_PrintStart]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printEnd(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PrintEnd, [1], [ResponseCommandId.In_PrintEnd]);
|
||||||
|
}
|
||||||
|
public static pageStart(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PageStart, [1], [ResponseCommandId.In_PageStart]);
|
||||||
|
}
|
||||||
|
public static pageEnd(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PageEnd, [1], [ResponseCommandId.In_PageEnd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printEmptySpace(pos: number, repeats: number): NiimbotPacket {
|
||||||
|
const packet = new NiimbotPacket(RequestCommandId.PrintEmptyRow, [...Utils.u16ToBytes(pos), repeats]);
|
||||||
|
packet.oneWay = true;
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printBitmapRow(pos: number, repeats: number, data: Uint8Array): NiimbotPacket {
|
||||||
|
const blackPixelCount: number = Utils.countSetBits(data);
|
||||||
|
|
||||||
|
const packet = new NiimbotPacket(RequestCommandId.PrintBitmapRow, [
|
||||||
|
...Utils.u16ToBytes(pos),
|
||||||
|
// Black pixel count. Not sure what role it plays in printing.
|
||||||
|
// There is two formats of this part
|
||||||
|
// 1. <count> <count> <count> (sum must equals number of pixels, every number calculated by algorithm based on printhead resolution)
|
||||||
|
// 2. <0> <countH> <countL> (big endian)
|
||||||
|
0,
|
||||||
|
...Utils.u16ToBytes(blackPixelCount),
|
||||||
|
repeats,
|
||||||
|
...data,
|
||||||
|
]);
|
||||||
|
packet.oneWay = true;
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Printer powers off if black pixel count > 6 */
|
||||||
|
public static printBitmapRowIndexed(pos: number, repeats: number, data: Uint8Array): NiimbotPacket {
|
||||||
|
const blackPixelCount: number = Utils.countSetBits(data);
|
||||||
|
const indexes: Uint8Array = ImageEncoder.indexPixels(data);
|
||||||
|
|
||||||
|
if (blackPixelCount > 6) {
|
||||||
|
throw new Error(`Black pixel count > 6 (${blackPixelCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = new NiimbotPacket(RequestCommandId.PrintBitmapRowIndexed, [
|
||||||
|
...Utils.u16ToBytes(pos),
|
||||||
|
0,
|
||||||
|
...Utils.u16ToBytes(blackPixelCount),
|
||||||
|
repeats,
|
||||||
|
...indexes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
packet.oneWay = true;
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static printClear(): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.PrintClear, [1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static writeRfid(data: Uint8Array): NiimbotPacket {
|
||||||
|
return new NiimbotPacket(RequestCommandId.WriteRFID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static writeImageData(image: EncodedImage): NiimbotPacket[] {
|
||||||
|
return image.rowsData.map((p: ImageRow) => {
|
||||||
|
if (p.dataType === "pixels") {
|
||||||
|
if (p.blackPixelsCount > 6) {
|
||||||
|
return PacketGenerator.printBitmapRow(p.rowNumber, p.repeat, p.rowData!);
|
||||||
|
} else {
|
||||||
|
return PacketGenerator.printBitmapRowIndexed(p.rowNumber, p.repeat, p.rowData!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return PacketGenerator.printEmptySpace(p.rowNumber, p.repeat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You should send PrintEnd manually after this sequence after print is finished
|
||||||
|
*/
|
||||||
|
public static generatePrintSequenceV3(image: EncodedImage, options?: PrintOptions): NiimbotPacket[] {
|
||||||
|
return [
|
||||||
|
PacketGenerator.setLabelType(options?.labelType ?? LabelType.WithGaps),
|
||||||
|
PacketGenerator.setDensity(options?.density ?? 2),
|
||||||
|
PacketGenerator.printStart(),
|
||||||
|
PacketGenerator.printClear(),
|
||||||
|
PacketGenerator.pageStart(),
|
||||||
|
PacketGenerator.setPageSizeV2(image.rows, image.cols),
|
||||||
|
PacketGenerator.setPrintQuantity(options?.quantity ?? 1),
|
||||||
|
...PacketGenerator.writeImageData(image),
|
||||||
|
PacketGenerator.pageEnd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You should send PrintEnd manually after this sequence after print is finished
|
||||||
|
*/
|
||||||
|
public static generatePrintSequenceV4(image: EncodedImage, options?: PrintOptions): NiimbotPacket[] {
|
||||||
|
return [
|
||||||
|
PacketGenerator.setDensity(options?.density ?? 2),
|
||||||
|
PacketGenerator.setLabelType(options?.labelType ?? LabelType.WithGaps),
|
||||||
|
PacketGenerator.printStartV4(options?.quantity ?? 1),
|
||||||
|
PacketGenerator.pageStart(),
|
||||||
|
PacketGenerator.setPageSizeV3(image.rows, image.cols, options?.quantity ?? 1),
|
||||||
|
...PacketGenerator.writeImageData(image),
|
||||||
|
PacketGenerator.pageEnd(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static generatePrintSequence(
|
||||||
|
printTaskVersion: PrintTaskVersion,
|
||||||
|
image: EncodedImage,
|
||||||
|
options?: PrintOptions
|
||||||
|
): NiimbotPacket[] {
|
||||||
|
switch (printTaskVersion) {
|
||||||
|
case PrintTaskVersion.V3:
|
||||||
|
return PacketGenerator.generatePrintSequenceV3(image, options);
|
||||||
|
case PrintTaskVersion.V4:
|
||||||
|
return PacketGenerator.generatePrintSequenceV4(image, options);
|
||||||
|
default:
|
||||||
|
throw new Error(`PrintTaskVersion ${printTaskVersion} Not implemented`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/packets/print_task_versions.ts
Normal file
17
src/packets/print_task_versions.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { PrintTaskVersion, PrinterModel as M } from "..";
|
||||||
|
|
||||||
|
export const getPrintTaskVersion = (model: M): PrintTaskVersion | undefined => {
|
||||||
|
switch (model) {
|
||||||
|
case M.D11:
|
||||||
|
case M.D11_H:
|
||||||
|
case M.D11S:
|
||||||
|
return PrintTaskVersion.V1;
|
||||||
|
case M.D110:
|
||||||
|
case M.D110_M:
|
||||||
|
return PrintTaskVersion.V3;
|
||||||
|
case M.B1:
|
||||||
|
return PrintTaskVersion.V4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
730
src/printer_models.ts
Normal file
730
src/printer_models.ts
Normal file
@ -0,0 +1,730 @@
|
|||||||
|
/* AUTO-GENERATED FILE. DO NOT EDIT! */
|
||||||
|
/* use 'yarn gen-printer-models' to generate */
|
||||||
|
|
||||||
|
import { PrintDirection, LabelType as LT } from ".";
|
||||||
|
|
||||||
|
export enum PrinterModel {
|
||||||
|
UNKNOWN = "UNKNOWN",
|
||||||
|
A20 = "A20",
|
||||||
|
A203 = "A203",
|
||||||
|
A63 = "A63",
|
||||||
|
A8 = "A8",
|
||||||
|
A8_P = "A8_P",
|
||||||
|
B1 = "B1",
|
||||||
|
B11 = "B11",
|
||||||
|
B16 = "B16",
|
||||||
|
B18 = "B18",
|
||||||
|
B18S = "B18S",
|
||||||
|
B203 = "B203",
|
||||||
|
B21 = "B21",
|
||||||
|
B21_PRO = "B21_PRO",
|
||||||
|
B21_C2B = "B21_C2B",
|
||||||
|
B21_L2B = "B21_L2B",
|
||||||
|
B21S = "B21S",
|
||||||
|
B21S_C2B = "B21S_C2B",
|
||||||
|
B3 = "B3",
|
||||||
|
B31 = "B31",
|
||||||
|
B32 = "B32",
|
||||||
|
B32R = "B32R",
|
||||||
|
B3S = "B3S",
|
||||||
|
B3S_P = "B3S_P",
|
||||||
|
B50 = "B50",
|
||||||
|
B50W = "B50W",
|
||||||
|
BETTY = "BETTY",
|
||||||
|
D101 = "D101",
|
||||||
|
D11 = "D11",
|
||||||
|
D11_H = "D11_H",
|
||||||
|
D110 = "D110",
|
||||||
|
D110_M = "D110_M",
|
||||||
|
D11S = "D11S",
|
||||||
|
D41 = "D41",
|
||||||
|
D61 = "D61",
|
||||||
|
DXX = "DXX",
|
||||||
|
ET10 = "ET10",
|
||||||
|
FUST = "FUST",
|
||||||
|
HI_D110 = "HI_D110",
|
||||||
|
HI_NB_D11 = "HI_NB_D11",
|
||||||
|
JC_M90 = "JC_M90",
|
||||||
|
JCB3S = "JCB3S",
|
||||||
|
K3 = "K3",
|
||||||
|
K3_W = "K3_W",
|
||||||
|
M2_H = "M2_H",
|
||||||
|
MP3K = "MP3K",
|
||||||
|
MP3K_W = "MP3K_W",
|
||||||
|
N1 = "N1",
|
||||||
|
P1 = "P1",
|
||||||
|
P18 = "P18",
|
||||||
|
P1S = "P1S",
|
||||||
|
S1 = "S1",
|
||||||
|
S3 = "S3",
|
||||||
|
S6 = "S6",
|
||||||
|
S6_P = "S6_P",
|
||||||
|
T2S = "T2S",
|
||||||
|
T6 = "T6",
|
||||||
|
T7 = "T7",
|
||||||
|
T8 = "T8",
|
||||||
|
T8S = "T8S",
|
||||||
|
TP2M_H = "TP2M_H",
|
||||||
|
Z401 = "Z401",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PrinterModelMeta {
|
||||||
|
model: PrinterModel;
|
||||||
|
id: [number, ...number[]];
|
||||||
|
dpi: number;
|
||||||
|
printDirection: PrintDirection;
|
||||||
|
printheadPixels: number;
|
||||||
|
paperTypes: number[];
|
||||||
|
densityMin: number;
|
||||||
|
densityMax: number;
|
||||||
|
densityDefault: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modelsLibrary: PrinterModelMeta[] = [
|
||||||
|
{
|
||||||
|
model: PrinterModel.A20,
|
||||||
|
id: [2817],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 400,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.A203,
|
||||||
|
id: [2818],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 400,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.A63,
|
||||||
|
id: [2054],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 851,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent, LT.Black],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.A8,
|
||||||
|
id: [256],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 600,
|
||||||
|
paperTypes: [LT.Black, LT.WithGaps, LT.Continuous],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.A8_P,
|
||||||
|
id: [273],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 616,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B1,
|
||||||
|
id: [4096],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B11,
|
||||||
|
id: [51457],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated, LT.Transparent],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B16,
|
||||||
|
id: [1792],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 96,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B18,
|
||||||
|
id: [3584],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 120,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent, LT.BlackMarkGap, LT.HeatShrinkTube],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B18S,
|
||||||
|
id: [3585],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 120,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent, LT.BlackMarkGap, LT.HeatShrinkTube],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B203,
|
||||||
|
id: [2816],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 400,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B21,
|
||||||
|
id: [768],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B21_PRO,
|
||||||
|
id: [785],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 591,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B21_C2B,
|
||||||
|
id: [771, 775],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Continuous, LT.Transparent, LT.Black],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B21_L2B,
|
||||||
|
id: [769],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B21S,
|
||||||
|
id: [777],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B21S_C2B,
|
||||||
|
id: [776],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B3,
|
||||||
|
id: [52993],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 600,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B31,
|
||||||
|
id: [5632],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 600,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B32,
|
||||||
|
id: [2049],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 851,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B32R,
|
||||||
|
id: [2050],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 851,
|
||||||
|
paperTypes: [LT.WithGaps],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B3S,
|
||||||
|
id: [256, 260, 262],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 576,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B3S_P,
|
||||||
|
id: [272],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 576,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B50,
|
||||||
|
id: [51713],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 400,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.B50W,
|
||||||
|
id: [51714],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.BETTY,
|
||||||
|
id: [2561],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 192,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.D101,
|
||||||
|
id: [2560],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 192,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.D11,
|
||||||
|
id: [512],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 96,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.D11_H,
|
||||||
|
id: [528],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 178,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.D110,
|
||||||
|
id: [2304, 2305],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 96,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.D110_M,
|
||||||
|
id: [2320],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 120,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.D11S,
|
||||||
|
id: [514],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 96,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.ET10,
|
||||||
|
id: [5376],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 1600,
|
||||||
|
paperTypes: [LT.Continuous],
|
||||||
|
densityMin: 3,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.FUST,
|
||||||
|
id: [513],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 96,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.HI_D110,
|
||||||
|
id: [2305],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 120,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.HI_NB_D11,
|
||||||
|
id: [512],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 120,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.JC_M90,
|
||||||
|
id: [51461],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.JCB3S,
|
||||||
|
id: [256],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 576,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.K3,
|
||||||
|
id: [4864],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 656,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.K3_W,
|
||||||
|
id: [4865],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 656,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.M2_H,
|
||||||
|
id: [4608],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 591,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent, LT.Black, LT.BlackMarkGap],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.MP3K,
|
||||||
|
id: [4866],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 656,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.MP3K_W,
|
||||||
|
id: [4867],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 656,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.N1,
|
||||||
|
id: [3586],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 120,
|
||||||
|
paperTypes: [LT.WithGaps, LT.HeatShrinkTube, LT.Transparent, LT.BlackMarkGap],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 3,
|
||||||
|
densityDefault: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.P1,
|
||||||
|
id: [1024],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 697,
|
||||||
|
paperTypes: [LT.PvcTag],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.P18,
|
||||||
|
id: [1026],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 662,
|
||||||
|
paperTypes: [LT.PvcTag],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.P1S,
|
||||||
|
id: [1025],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "left",
|
||||||
|
printheadPixels: 662,
|
||||||
|
paperTypes: [LT.PvcTag],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.S1,
|
||||||
|
id: [51458],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.S3,
|
||||||
|
id: [51460],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.S6,
|
||||||
|
id: [261, 259, 258, 257],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 576,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.S6_P,
|
||||||
|
id: [274],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 600,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.T2S,
|
||||||
|
id: [53250],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 832,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 20,
|
||||||
|
densityDefault: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.T6,
|
||||||
|
id: [51715],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.T7,
|
||||||
|
id: [51717],
|
||||||
|
dpi: 203,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 384,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.T8,
|
||||||
|
id: [51718],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 567,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Continuous, LT.Perforated],
|
||||||
|
densityMin: 6,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.T8S,
|
||||||
|
id: [2053],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 851,
|
||||||
|
paperTypes: [LT.WithGaps],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.TP2M_H,
|
||||||
|
id: [4609],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 591,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Black, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 5,
|
||||||
|
densityDefault: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: PrinterModel.Z401,
|
||||||
|
id: [2051],
|
||||||
|
dpi: 300,
|
||||||
|
printDirection: "top",
|
||||||
|
printheadPixels: 851,
|
||||||
|
paperTypes: [LT.WithGaps, LT.Transparent],
|
||||||
|
densityMin: 1,
|
||||||
|
densityMax: 15,
|
||||||
|
densityDefault: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getPrinterMetaById = (id: number): PrinterModelMeta | undefined => {
|
||||||
|
return modelsLibrary.find((o) => o.id.includes(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPrinterMetaByModel = (model: PrinterModel): PrinterModelMeta | undefined => {
|
||||||
|
return modelsLibrary.find((o) => o.model === model);
|
||||||
|
};
|
107
src/utils.ts
Normal file
107
src/utils.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
export class Utils {
|
||||||
|
public static numberToHex(n: number): string {
|
||||||
|
const hex = n.toString(16);
|
||||||
|
return hex.length === 1 ? `0${hex}` : hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bufToHex(buf: DataView | Uint8Array | number[], separator: string = " "): string {
|
||||||
|
const arr: number[] = buf instanceof DataView ? this.dataViewToNumberArray(buf) : Array.from(buf);
|
||||||
|
return arr.map((n) => Utils.numberToHex(n)).join(separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static hexToBuf(str: string): Uint8Array {
|
||||||
|
const match = str.match(/[\da-f]{2}/gi);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(
|
||||||
|
match.map((h) => {
|
||||||
|
return parseInt(h, 16);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static dataViewToNumberArray(dw: DataView): number[] {
|
||||||
|
const a: number[] = [];
|
||||||
|
for (let i = 0; i < dw.byteLength; i++) {
|
||||||
|
a.push(dw.getUint8(i));
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static dataViewToU8Array(dw: DataView): Uint8Array {
|
||||||
|
return Uint8Array.from(this.dataViewToNumberArray(dw));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static u8ArrayToString(arr: Uint8Array): string {
|
||||||
|
return new TextDecoder().decode(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Count non-zero bits in the byte array */
|
||||||
|
public static countSetBits(arr: Uint8Array): number {
|
||||||
|
// not so efficient, but readable
|
||||||
|
let count: number = 0;
|
||||||
|
|
||||||
|
arr.forEach((value) => {
|
||||||
|
// shift until value becomes zero
|
||||||
|
while (value > 0) {
|
||||||
|
// check last bit
|
||||||
|
if ((value & 1) === 1) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
value >>= 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Big endian */
|
||||||
|
public static u16ToBytes(n: number): [number, number] {
|
||||||
|
const h = (n >> 8) & 0xff;
|
||||||
|
const l = n % 256 & 0xff;
|
||||||
|
return [h, l];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Big endian */
|
||||||
|
public static bytesToI16(arr: Uint8Array): number {
|
||||||
|
Validators.u8ArrayLengthEquals(arr, 2);
|
||||||
|
return arr[0] * 256 + arr[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static u8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {
|
||||||
|
return a.length === b.length && a.every((el, i) => el === b[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static sleep(ms: number): Promise<undefined> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isBluetoothSupported(): boolean {
|
||||||
|
return typeof navigator.bluetooth?.requestDevice !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isSerialSupported(): boolean {
|
||||||
|
return typeof navigator.serial?.requestPort !== "undefined";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Validators {
|
||||||
|
public static u8ArraysEqual(a: Uint8Array, b: Uint8Array, message?: string): void {
|
||||||
|
if (!Utils.u8ArraysEqual(a, b)) {
|
||||||
|
throw new Error(message ?? "Arrays must be equal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static u8ArrayLengthEquals(a: Uint8Array, len: number, message?: string): void {
|
||||||
|
if (a.length !== len) {
|
||||||
|
throw new Error(message ?? `Array length must be ${len}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static u8ArrayLengthAtLeast(a: Uint8Array, len: number, message?: string): void {
|
||||||
|
if (a.length < len) {
|
||||||
|
throw new Error(message ?? `Array length must be at least ${len}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
"lib": ["ES2021", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
93
utils/gen-printer-models.js
Normal file
93
utils/gen-printer-models.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// https://print.niimbot.com/api/hardware/list
|
||||||
|
|
||||||
|
fetch("https://oss-print.niimbot.com/public_resources/static_resources/devices.json")
|
||||||
|
.then((resp) => resp.json())
|
||||||
|
.then((items) => {
|
||||||
|
items.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const dir_d = {
|
||||||
|
270: "left",
|
||||||
|
180: "top",
|
||||||
|
90: "left",
|
||||||
|
0: "top",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ppmm_d = {
|
||||||
|
203: 8,
|
||||||
|
300: 11.81,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labeltypes_d = {
|
||||||
|
"1": "LT.WithGaps",
|
||||||
|
"2": "LT.Black",
|
||||||
|
"3": "LT.Continuous",
|
||||||
|
"4": "LT.Perforated",
|
||||||
|
"5": "LT.Transparent",
|
||||||
|
"6": "LT.PvcTag",
|
||||||
|
"10": "LT.BlackMarkGap",
|
||||||
|
"11": "LT.HeatShrinkTube",
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("/* AUTO-GENERATED FILE. DO NOT EDIT! */");
|
||||||
|
console.log("/* use 'yarn gen-printer-models' to generate */\n");
|
||||||
|
console.log('import { PrintDirection, LabelType as LT } from ".";\n');
|
||||||
|
|
||||||
|
console.log("export enum PrinterModel {");
|
||||||
|
console.log(' UNKNOWN = "UNKNOWN",');
|
||||||
|
for (const item of items) {
|
||||||
|
const name = item.name.toUpperCase().replaceAll("-", "_");
|
||||||
|
console.log(` ${name} = "${name}",`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("};");
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
export interface PrinterModelMeta {
|
||||||
|
model: PrinterModel;
|
||||||
|
id: [number, ...number[]];
|
||||||
|
dpi: number;
|
||||||
|
printDirection: PrintDirection;
|
||||||
|
printheadPixels: number;
|
||||||
|
paperTypes: number[];
|
||||||
|
densityMin: number;
|
||||||
|
densityMax: number;
|
||||||
|
densityDefault: number;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("export const modelsLibrary: PrinterModelMeta[] = [");
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.codes.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = item.name.toUpperCase().replaceAll("-", "_");
|
||||||
|
const dir = dir_d[item.printDirection];
|
||||||
|
const ppmm = ppmm_d[item.paccuracyName];
|
||||||
|
const paperTypes = item.paperType.split(',').map(e => labeltypes_d[e]).join(", ");
|
||||||
|
|
||||||
|
console.log(" {");
|
||||||
|
console.log(` model: PrinterModel.${name},`);
|
||||||
|
console.log(` id: [${item.codes.join(', ')}],`);
|
||||||
|
console.log(` dpi: ${item.paccuracyName},`);
|
||||||
|
console.log(` printDirection: "${dir}",`);
|
||||||
|
console.log(` printheadPixels: ${Math.ceil(item.widthSetEnd * ppmm)},`);
|
||||||
|
console.log(` paperTypes: [${paperTypes}],`);
|
||||||
|
console.log(` densityMin: ${item.solubilitySetStart},`);
|
||||||
|
console.log(` densityMax: ${item.solubilitySetEnd},`);
|
||||||
|
console.log(` densityDefault: ${item.solubilitySetDefault},`);
|
||||||
|
console.log(" },");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("];");
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
export const getPrinterMetaById = (id: number): PrinterModelMeta | undefined => {
|
||||||
|
return modelsLibrary.find((o) => o.id.includes(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPrinterMetaByModel = (model: PrinterModel): PrinterModelMeta | undefined => {
|
||||||
|
return modelsLibrary.find((o) => o.model === model);
|
||||||
|
};`);
|
||||||
|
|
||||||
|
});
|
102
utils/parse-dump.mjs
Normal file
102
utils/parse-dump.mjs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Utils, NiimbotPacket, RequestCommandId, ResponseCommandId } from "../dist/index.js";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
|
const TSHARK_PATH = "C:\\Program Files\\Wireshark\\tshark.exe";
|
||||||
|
|
||||||
|
if (process.argv.length < 4 || !["usb", "ble"].includes(process.argv[2])) {
|
||||||
|
console.error("usage: yarn parse-dump <ble|usb> <filename> [min|min-out]");
|
||||||
|
}
|
||||||
|
|
||||||
|
const capType = process.argv[2];
|
||||||
|
const capPath = process.argv[3];
|
||||||
|
const args = ["-2", "-r", capPath, "-P", "-T", "fields", "-e", /*"frame.time_relative"*/ "frame.time_delta"];
|
||||||
|
const display = process.argv.length > 4 ? process.argv[4] : "";
|
||||||
|
|
||||||
|
if (capType === "ble") {
|
||||||
|
args.push("-R", "btspp.data", "-e", "hci_h4.direction", "-e", "btspp.data");
|
||||||
|
} else {
|
||||||
|
args.push("-R", "usb.capdata", "-e", "usb.dst", "-e", "usb.capdata");
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawned = spawn(TSHARK_PATH, args);
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
spawned.stdout.on("data", (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
spawned.stderr.on("data", (data) => {
|
||||||
|
console.error(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
spawned.on("close", (code) => {
|
||||||
|
console.log(`child process exited with code ${code}`);
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error(output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = output.split(/\r?\n/);
|
||||||
|
let printStarted = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const splitted = line.split("\t");
|
||||||
|
|
||||||
|
if (splitted.length !== 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [time, direction, hexData] = splitted;
|
||||||
|
direction = ["host", "0x01"].includes(direction) ? "<<" : ">>";
|
||||||
|
|
||||||
|
let comment = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = Utils.hexToBuf(hexData);
|
||||||
|
const packets = NiimbotPacket.fromBytesMultiPacket(data);
|
||||||
|
|
||||||
|
if (packets.length === 0) {
|
||||||
|
comment = "Parse error (no packets found)";
|
||||||
|
} else if (packets.length > 1) {
|
||||||
|
comment = `Multi-packet (x${packets.length}); `;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const packet of packets) {
|
||||||
|
if (direction === ">>") {
|
||||||
|
comment += RequestCommandId[packet.command] ?? "???";
|
||||||
|
if (packet.command === RequestCommandId.PrintStart) {
|
||||||
|
printStarted = true;
|
||||||
|
const versions = { 1: "v1", 2: "v3", 7: "v4", 8: "v5" };
|
||||||
|
comment += "_" + versions[packet.dataLength];
|
||||||
|
} else if (packet.command === RequestCommandId.SetPageSize) {
|
||||||
|
const versions = { 2: "v1", 4: "v2", 6: "v3", 8: "v5" };
|
||||||
|
comment += "_" + versions[packet.dataLength];
|
||||||
|
}else if (packet.command === RequestCommandId.PrintEnd) {
|
||||||
|
printStarted = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
comment += ResponseCommandId[packet.command] ?? "???";
|
||||||
|
}
|
||||||
|
comment += `(${packet.dataLength}); `;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
comment = "Invalid packet";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display === "min") {
|
||||||
|
console.log(`${direction} ${comment}`);
|
||||||
|
} else if (display === "min-out") {
|
||||||
|
if (direction === ">>") {
|
||||||
|
console.log(`${direction} ${comment}`);
|
||||||
|
}
|
||||||
|
} else if (display === "print-task") {
|
||||||
|
if (direction === ">>" && printStarted) {
|
||||||
|
console.log(`${direction} ${comment}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[${time}] ${direction} ${hexData}\t// ${comment}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
23
utils/translate-devices-json.mjs
Normal file
23
utils/translate-devices-json.mjs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const locale = (
|
||||||
|
await (await fetch("https://oss-print.niimbot.com/public_resources/static_resources/languagePack/en.json")).json()
|
||||||
|
)["lang"];
|
||||||
|
|
||||||
|
const translateDeep = (obj) => {
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (typeof obj[key] === "object") {
|
||||||
|
translateDeep(obj[key]);
|
||||||
|
} else if (typeof obj[key] === "string" && key === "multilingualCode") {
|
||||||
|
const translated = locale[obj[key]];
|
||||||
|
obj["translated"] = translated ? translated["value"] : "TRANSLATION_NOT_FOUND";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const devices = await (await fetch("https://oss-print.niimbot.com/public_resources/static_resources/devices.json")).json();
|
||||||
|
|
||||||
|
const translated = devices.map(translateDeep);
|
||||||
|
|
||||||
|
console.log(JSON.stringify(translated, null, 2));
|
47
yarn.lock
Normal file
47
yarn.lock
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/node@^20.14.2":
|
||||||
|
version "20.14.2"
|
||||||
|
resolved "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz"
|
||||||
|
integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/w3c-web-serial@^1.0.6":
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.npmjs.org/@types/w3c-web-serial/-/w3c-web-serial-1.0.6.tgz"
|
||||||
|
integrity sha512-5IlDdQ2C56sCVwc7CUlqT9Axxw+0V/FbWRbErklYIzZ5mKL9s4l7epXHygn+4X7L2nmAPnVvRl55XUVo0760Rg==
|
||||||
|
|
||||||
|
"@types/web-bluetooth@^0.0.20":
|
||||||
|
version "0.0.20"
|
||||||
|
resolved "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz"
|
||||||
|
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
|
||||||
|
|
||||||
|
async-mutex@^0.5.0:
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz"
|
||||||
|
integrity sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
|
tslib@^2.4.0:
|
||||||
|
version "2.6.3"
|
||||||
|
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz"
|
||||||
|
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
|
||||||
|
|
||||||
|
typescript-event-target@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.1.tgz"
|
||||||
|
integrity sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==
|
||||||
|
|
||||||
|
typescript@^5.4.5:
|
||||||
|
version "5.4.5"
|
||||||
|
resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz"
|
||||||
|
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
Loading…
x
Reference in New Issue
Block a user