@mmote/niimblue-node 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ ## niimblue-node [![NPM](https://img.shields.io/npm/v/@mmote/niimblue-node)](https://npmjs.com/package/@mmote/niimblue-node)
2
+
3
+ [niimbluelib](https://github.com/MultiMote/niimbluelib) BLE and serial client implementations for non-browser use cases.
4
+
5
+ Command line interface, simple REST server are also included.
6
+
7
+ Tested with:
8
+
9
+ * Windows 10
10
+ * Bluetooth adapter (TP-LINK UB500)
11
+ * USB serial connection
12
+ * Printers: B1, D110
13
+
14
+ Usage example:
15
+
16
+ * [src/service.ts](src/service.ts)
17
+
18
+ ### Install
19
+
20
+ Global (for cli usage):
21
+
22
+ ```bash
23
+ npm i -g @mmote/niimblue-node
24
+ ```
25
+
26
+ [node-gyp](https://www.npmjs.com/package/node-gyp) is required to install [noble](https://www.npmjs.com/package/@abandonware/noble) dependency.
27
+ It requires working compiler installed on your system.
28
+
29
+ Windows requirements:
30
+
31
+ * [MS Build tools 2019+](https://visualstudio.microsoft.com/downloads/?q=build+tools)
32
+ - C++ build tools with `Windows SDK >=22000` must be installed
33
+ * Python 3
34
+
35
+ See [node-gyp](https://github.com/nodejs/node-gyp) and [noble](https://github.com/abandonware/noble) installation.
36
+
37
+ ### Command-line usage
38
+
39
+ While development:
40
+
41
+ ```bash
42
+ npm run cli --- <options>
43
+ ```
44
+
45
+ If installed as package globally:
46
+
47
+ ```bash
48
+ niimblue-cli <options>
49
+ ```
50
+
51
+ Available options:
52
+
53
+ ```bash
54
+ niimblue-cli help print
55
+ niimblue-cli help info
56
+ niimblue-cli help scan
57
+ niimblue-cli help server
58
+ niimblue-cli help flash
59
+ ```
60
+
61
+ #### Examples
62
+
63
+ B1 BLE:
64
+
65
+ ```bash
66
+ niimblue-cli print -d -t ble -a 27:03:07:17:6e:82 -p B1 -o top label_15x30.png
67
+ ```
68
+
69
+ D110 BLE:
70
+
71
+ ```bash
72
+ niimblue-cli print -d -t ble -a 26:03:03:c3:f9:11 -p D110 -o left label_15x30.png
73
+ ```
74
+
75
+ B1 serial, long parameter names (will resize image to fit 50x30 label keeping aspect ration):
76
+
77
+ ```bash
78
+ niimblue-cli print --debug --transport serial --address COM8 --print-task B1 --print-direction top --label-width 384 --label-height 240 label_15x30.png
79
+ ```
80
+
81
+ B1 firmware upgrade via serial:
82
+
83
+ ```bash
84
+ niimblue-cli flash -t serial -a COM8 -n 5.14 -f path/to/B1_5.14.bin
85
+ ```
86
+
87
+ ### Server mode
88
+
89
+ You can start a simple server with:
90
+
91
+ ```bash
92
+ niimblue-cli server
93
+ ```
94
+
95
+ Enable debug logging, set host and port, enable CORS:
96
+
97
+ ```bash
98
+ niimblue-cli server -d -h 0.0.0.0 -p 5000 --cors
99
+ ```
100
+
101
+ [Server API docs](https://multimote.github.io/niimblue-node/server/)
package/cli.mjs ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "./dist/cli/index.js"
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const extra_typings_1 = require("@commander-js/extra-typings");
4
+ const niimbluelib_1 = require("@mmote/niimbluelib");
5
+ const server_1 = require("../server");
6
+ const worker_1 = require("./worker");
7
+ const intOption = (value) => {
8
+ const parsed = parseInt(value, 10);
9
+ if (isNaN(parsed) || parsed < 0) {
10
+ throw new extra_typings_1.InvalidArgumentError("Integer required");
11
+ }
12
+ return parsed;
13
+ };
14
+ extra_typings_1.program.name("niimblue-cli");
15
+ extra_typings_1.program
16
+ .command("info")
17
+ .description("Printer information")
18
+ .requiredOption("-d, --debug", "Debug information", false)
19
+ .addOption(new extra_typings_1.Option("-t, --transport <type>", "Transport").makeOptionMandatory().choices(["ble", "serial"]))
20
+ .requiredOption("-a, --address <string>", "Device bluetooth address or serial port name/path")
21
+ .action(worker_1.cliPrinterInfo);
22
+ extra_typings_1.program
23
+ .command("scan")
24
+ .description("Get available device list")
25
+ .requiredOption("-n, --timeout <number>", "Timeout", intOption, 5000)
26
+ .addOption(new extra_typings_1.Option("-t, --transport <type>", "Transport").makeOptionMandatory().choices(["ble", "serial"]))
27
+ .action(worker_1.cliScan);
28
+ extra_typings_1.program
29
+ .command("print")
30
+ .description("Prints image")
31
+ .argument("<path>", "PNG image path")
32
+ .requiredOption("-d, --debug", "Debug information", false)
33
+ .addOption(new extra_typings_1.Option("-t, --transport <type>", "Transport").makeOptionMandatory().choices(["ble", "serial"]))
34
+ .requiredOption("-a, --address <string>", "Device bluetooth address or serial port name/path")
35
+ .addOption(new extra_typings_1.Option("-o, --print-direction <dir>", "Print direction").choices(["left", "top"]))
36
+ .addOption(new extra_typings_1.Option("-p, --print-task <type>", "Print task").choices(niimbluelib_1.printTaskNames))
37
+ .requiredOption("-l, --label-type <type number>", "Label type", intOption, 1)
38
+ .requiredOption("-q, --density <number>", "Density", intOption, 3)
39
+ .requiredOption("-n, --quantity <number>", "Quantity", intOption, 1)
40
+ .requiredOption("-x, --threshold <number>", "Threshold", intOption, 128)
41
+ .option("-w, --label-width <number>", "Label width", intOption)
42
+ .option("-h, --label-height <number>", "Label height", intOption)
43
+ .addOption(new extra_typings_1.Option("-f, --image-fit <dir>", "Image fit while resizing (label-width and label-height must be set)").choices([
44
+ "contain",
45
+ "cover",
46
+ "fill",
47
+ "inside",
48
+ "outside",
49
+ ]).default("contain"))
50
+ .addOption(new extra_typings_1.Option("-m, --image-position <dir>", "Image position while resizing (label-width and label-height must be set)").choices([
51
+ "left",
52
+ "top",
53
+ "centre",
54
+ "right top",
55
+ "right",
56
+ "right bottom",
57
+ "bottom",
58
+ "left bottom",
59
+ "left top",
60
+ ]).default("centre"))
61
+ .action(worker_1.cliConnectAndPrintImageFile);
62
+ extra_typings_1.program
63
+ .command("server")
64
+ .description("Start in server mode")
65
+ .requiredOption("-d, --debug", "Debug information", false)
66
+ .requiredOption("-c, --cors", "Enable CORS", false)
67
+ .requiredOption("-p, --port <number>", "Listen port", intOption, 5000)
68
+ .requiredOption("-h, --host <host>", "Listen hostname", "localhost")
69
+ .action(server_1.cliStartServer);
70
+ extra_typings_1.program
71
+ .command("flash")
72
+ .description("Flash firmware")
73
+ .requiredOption("-d, --debug", "Debug information", false)
74
+ .addOption(new extra_typings_1.Option("-t, --transport <type>", "Transport").makeOptionMandatory().choices(["ble", "serial"]))
75
+ .requiredOption("-a, --address <string>", "Device bluetooth address or serial port name/path")
76
+ .requiredOption("-f, --file <path>", "Firmware path")
77
+ .requiredOption("-n, --new-version <version>", "New firmware version")
78
+ .action(worker_1.cliFlashFirmware);
79
+ extra_typings_1.program.parse();
@@ -0,0 +1,41 @@
1
+ import { LabelType, PrintDirection, PrintTaskName } from "@mmote/niimbluelib";
2
+ import { TransportType } from "../utils";
3
+ export type SharpImageFit = "contain" | "cover" | "fill" | "inside" | "outside";
4
+ export type SharpImagePosition = "left" | "top" | "centre" | "right top" | "right" | "right bottom" | "bottom" | "left bottom" | "left top";
5
+ export interface TransportOptions {
6
+ transport: TransportType;
7
+ address: string;
8
+ }
9
+ export interface ScanOptions {
10
+ transport: TransportType;
11
+ timeout: number;
12
+ }
13
+ export interface InfoOptions {
14
+ transport: TransportType;
15
+ address: string;
16
+ debug: boolean;
17
+ }
18
+ export interface FirmwareOptions {
19
+ transport: TransportType;
20
+ address: string;
21
+ file: string;
22
+ newVersion: string;
23
+ debug: boolean;
24
+ }
25
+ export interface PrintOptions {
26
+ printTask?: PrintTaskName;
27
+ printDirection?: PrintDirection;
28
+ quantity: number;
29
+ labelType: LabelType;
30
+ density: number;
31
+ threshold: number;
32
+ labelWidth?: number;
33
+ labelHeight?: number;
34
+ imageFit: SharpImageFit;
35
+ imagePosition: SharpImagePosition;
36
+ debug: boolean;
37
+ }
38
+ export declare const cliConnectAndPrintImageFile: (path: string, options: PrintOptions & TransportOptions) => Promise<never>;
39
+ export declare const cliScan: (options: ScanOptions) => Promise<never>;
40
+ export declare const cliPrinterInfo: (options: InfoOptions) => Promise<never>;
41
+ export declare const cliFlashFirmware: (options: FirmwareOptions) => Promise<never>;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cliFlashFirmware = exports.cliPrinterInfo = exports.cliScan = exports.cliConnectAndPrintImageFile = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const sharp_1 = __importDefault(require("sharp"));
9
+ const __1 = require("..");
10
+ const utils_1 = require("../utils");
11
+ const extra_typings_1 = require("@commander-js/extra-typings");
12
+ const cliConnectAndPrintImageFile = async (path, options) => {
13
+ const client = (0, utils_1.initClient)(options.transport, options.address, options.debug);
14
+ if (options.debug) {
15
+ console.log("Connecting to", options.transport, options.address);
16
+ }
17
+ await client.connect();
18
+ let image = await (0, utils_1.loadImageFromFile)(path);
19
+ image = image.flatten({ background: "#fff" }).threshold(options.threshold);
20
+ if (options.labelWidth !== undefined && options.labelHeight !== undefined) {
21
+ image = image.resize(options.labelWidth, options.labelHeight, {
22
+ kernel: sharp_1.default.kernel.nearest,
23
+ fit: options.imageFit,
24
+ position: options.imagePosition,
25
+ background: "#fff",
26
+ });
27
+ }
28
+ else if (options.imageFit !== undefined || options.imagePosition !== undefined) {
29
+ throw new extra_typings_1.InvalidArgumentError("label-width and label-height must be set");
30
+ }
31
+ const printDirection = options.printDirection ?? client.getModelMetadata()?.printDirection;
32
+ const printTask = options.printTask ?? client.getPrintTaskType();
33
+ const encoded = await __1.ImageEncoder.encodeImage(image, printDirection);
34
+ if (printTask === undefined) {
35
+ throw new Error("Unable to detect print task, please set it manually");
36
+ }
37
+ if (options.debug) {
38
+ console.log("Print task:", printTask);
39
+ }
40
+ try {
41
+ await (0, utils_1.printImage)(client, printTask, encoded, {
42
+ quantity: options.quantity,
43
+ labelType: options.labelType,
44
+ density: options.density,
45
+ });
46
+ }
47
+ finally {
48
+ await client.disconnect();
49
+ }
50
+ process.exit(0);
51
+ };
52
+ exports.cliConnectAndPrintImageFile = cliConnectAndPrintImageFile;
53
+ const cliScan = async (options) => {
54
+ if (options.transport === "ble") {
55
+ const devices = await __1.NiimbotHeadlessBleClient.scan(options.timeout);
56
+ devices.forEach((dev) => console.log(`${dev.address}: ${dev.name}`));
57
+ }
58
+ else if (options.transport === "serial") {
59
+ const devices = await __1.NiimbotHeadlessSerialClient.scan();
60
+ devices.forEach((dev) => console.log(`${dev.address}: ${dev.name}`));
61
+ }
62
+ process.exit(0);
63
+ };
64
+ exports.cliScan = cliScan;
65
+ const cliPrinterInfo = async (options) => {
66
+ const client = (0, utils_1.initClient)(options.transport, options.address, options.debug);
67
+ await client.connect();
68
+ console.log("Printer info:", client.getPrinterInfo());
69
+ console.log("Model metadata:", client.getModelMetadata());
70
+ console.log("Detected print task:", client.getPrintTaskType());
71
+ await client.disconnect();
72
+ process.exit(0);
73
+ };
74
+ exports.cliPrinterInfo = cliPrinterInfo;
75
+ const cliFlashFirmware = async (options) => {
76
+ const data = fs_1.default.readFileSync(options.file);
77
+ const client = (0, utils_1.initClient)(options.transport, options.address, options.debug);
78
+ await client.connect();
79
+ client.stopHeartbeat();
80
+ const listener = (e) => {
81
+ console.log(`Sending ${e.currentChunk}/${e.totalChunks}`);
82
+ };
83
+ client.on("firmwareprogress", listener);
84
+ try {
85
+ console.log("Uploading firmware...");
86
+ await client.abstraction.firmwareUpgrade(data, options.newVersion);
87
+ console.log("Done, printer will shut down");
88
+ }
89
+ finally {
90
+ client.off("firmwareprogress", listener);
91
+ await client.disconnect();
92
+ }
93
+ process.exit(0);
94
+ };
95
+ exports.cliFlashFirmware = cliFlashFirmware;
@@ -0,0 +1,21 @@
1
+ import { ConnectionInfo, NiimbotAbstractClient } from "@mmote/niimbluelib";
2
+ export interface ScanItem {
3
+ address: string;
4
+ name: string;
5
+ }
6
+ export declare class NiimbotHeadlessBleClient extends NiimbotAbstractClient {
7
+ private addr;
8
+ private device;
9
+ private channel;
10
+ constructor();
11
+ /** Set device mac address for connect */
12
+ setAddress(address: string): void;
13
+ static waitAdapterReady(): Promise<void>;
14
+ static scan(timeoutMs?: number): Promise<ScanItem[]>;
15
+ private getDevice;
16
+ private connectToDevice;
17
+ connect(): Promise<ConnectionInfo>;
18
+ isConnected(): boolean;
19
+ disconnect(): Promise<void>;
20
+ sendRaw(data: Uint8Array, force?: boolean): Promise<void>;
21
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NiimbotHeadlessBleClient = void 0;
7
+ const noble_1 = __importDefault(require("@abandonware/noble"));
8
+ const niimbluelib_1 = require("@mmote/niimbluelib");
9
+ class NiimbotHeadlessBleClient extends niimbluelib_1.NiimbotAbstractClient {
10
+ constructor() {
11
+ super();
12
+ this.addr = "";
13
+ }
14
+ /** Set device mac address for connect */
15
+ setAddress(address) {
16
+ this.addr = address.toLowerCase();
17
+ }
18
+ static async waitAdapterReady() {
19
+ if (noble_1.default._state === "poweredOn") {
20
+ return;
21
+ }
22
+ return new Promise((resolve, reject) => {
23
+ let timer;
24
+ noble_1.default.on("stateChange", async (state) => {
25
+ clearTimeout(timer);
26
+ if (state === "poweredOn") {
27
+ resolve();
28
+ }
29
+ else {
30
+ reject(new Error(`BLE state is ${state}`));
31
+ }
32
+ });
33
+ timer = setTimeout(() => {
34
+ reject(new Error("Can't init BLE"));
35
+ }, 5000);
36
+ });
37
+ }
38
+ static async scan(timeoutMs = 5000) {
39
+ await NiimbotHeadlessBleClient.waitAdapterReady();
40
+ return new Promise((resolve, reject) => {
41
+ const peripherals = [];
42
+ let timer;
43
+ noble_1.default.on("discover", async (peripheral) => {
44
+ peripherals.push({
45
+ address: peripheral.address,
46
+ name: peripheral.advertisement.localName || "unknown",
47
+ });
48
+ });
49
+ noble_1.default.startScanning([], false, (error) => {
50
+ if (error) {
51
+ clearTimeout(timer);
52
+ reject(error);
53
+ }
54
+ });
55
+ timer = setTimeout(() => {
56
+ noble_1.default.stopScanning();
57
+ resolve(peripherals);
58
+ }, timeoutMs ?? 5000);
59
+ });
60
+ }
61
+ async getDevice(address, timeoutMs = 5000) {
62
+ await NiimbotHeadlessBleClient.waitAdapterReady();
63
+ return new Promise((resolve, reject) => {
64
+ let timer;
65
+ noble_1.default.on("discover", async (peripheral) => {
66
+ if (peripheral.address === address) {
67
+ clearTimeout(timer);
68
+ resolve(peripheral);
69
+ }
70
+ });
71
+ noble_1.default.startScanning([], false, (error) => {
72
+ if (error)
73
+ reject(error);
74
+ });
75
+ timer = setTimeout(() => {
76
+ noble_1.default.stopScanning();
77
+ reject(new Error("Device not found"));
78
+ }, timeoutMs ?? 5000);
79
+ });
80
+ }
81
+ async connectToDevice(address, timeoutMs = 5000) {
82
+ const periph = await this.getDevice(address, timeoutMs);
83
+ await periph.connectAsync();
84
+ const services = await periph.discoverServicesAsync();
85
+ let channelCharacteristic;
86
+ for (const service of services) {
87
+ if (service.uuid.length < 5) {
88
+ continue;
89
+ }
90
+ const characteristics = await service.discoverCharacteristicsAsync();
91
+ const suitableCharacteristic = characteristics.find((ch) => ch.properties.includes("notify") && ch.properties.includes("writeWithoutResponse"));
92
+ if (suitableCharacteristic) {
93
+ channelCharacteristic = suitableCharacteristic;
94
+ break;
95
+ }
96
+ }
97
+ if (channelCharacteristic === undefined) {
98
+ await periph.disconnectAsync();
99
+ throw new Error("Unable to find suitable channel characteristic");
100
+ }
101
+ periph.on("disconnect", () => {
102
+ this.stopHeartbeat();
103
+ this.emit("disconnect", new niimbluelib_1.DisconnectEvent());
104
+ this.device = undefined;
105
+ this.channel = undefined;
106
+ });
107
+ channelCharacteristic.on("read", (data, isNotification) => {
108
+ if (isNotification)
109
+ this.processRawPacket(new Uint8Array(data));
110
+ });
111
+ channelCharacteristic.subscribeAsync();
112
+ this.channel = channelCharacteristic;
113
+ this.device = periph;
114
+ }
115
+ async connect() {
116
+ await this.disconnect();
117
+ if (!this.addr) {
118
+ throw new Error("Device address not set");
119
+ }
120
+ await this.connectToDevice(this.addr);
121
+ try {
122
+ await this.initialNegotiate();
123
+ await this.fetchPrinterInfo();
124
+ }
125
+ catch (e) {
126
+ console.error("Unable to fetch printer info.");
127
+ console.error(e);
128
+ }
129
+ const result = {
130
+ deviceName: this.device.advertisement.localName ?? this.addr,
131
+ result: this.info.connectResult ?? niimbluelib_1.ConnectResult.FirmwareErrors,
132
+ };
133
+ this.emit("connect", new niimbluelib_1.ConnectEvent(result));
134
+ return result;
135
+ }
136
+ isConnected() {
137
+ return this.device !== undefined && this.channel !== undefined;
138
+ }
139
+ async disconnect() {
140
+ this.stopHeartbeat();
141
+ if (this.device !== undefined) {
142
+ await this.device.disconnectAsync();
143
+ this.emit("disconnect", new niimbluelib_1.DisconnectEvent());
144
+ }
145
+ this.device = undefined;
146
+ this.channel = undefined;
147
+ }
148
+ async sendRaw(data, force) {
149
+ const send = async () => {
150
+ if (!this.isConnected()) {
151
+ this.disconnect();
152
+ throw new Error("Disconnected");
153
+ }
154
+ await niimbluelib_1.Utils.sleep(this.packetIntervalMs);
155
+ await this.channel.writeAsync(Buffer.from(data), true);
156
+ this.emit("rawpacketsent", new niimbluelib_1.RawPacketSentEvent(data));
157
+ };
158
+ if (force) {
159
+ await send();
160
+ }
161
+ else {
162
+ await this.mutex.runExclusive(send);
163
+ }
164
+ }
165
+ }
166
+ exports.NiimbotHeadlessBleClient = NiimbotHeadlessBleClient;
@@ -0,0 +1,20 @@
1
+ import { ConnectionInfo, NiimbotAbstractClient } from "@mmote/niimbluelib";
2
+ export interface ScanItem {
3
+ address: string;
4
+ name: string;
5
+ }
6
+ /** WIP. Uses serial communication (serialport lib) */
7
+ export declare class NiimbotHeadlessSerialClient extends NiimbotAbstractClient {
8
+ private device?;
9
+ private portName?;
10
+ private isOpen;
11
+ constructor();
12
+ /** Set port for connect */
13
+ setPort(portName: string): void;
14
+ connect(): Promise<ConnectionInfo>;
15
+ private dataReady;
16
+ disconnect(): Promise<void>;
17
+ isConnected(): boolean;
18
+ sendRaw(data: Uint8Array, force?: boolean): Promise<void>;
19
+ static scan(): Promise<ScanItem[]>;
20
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NiimbotHeadlessSerialClient = void 0;
4
+ const serialport_1 = require("serialport");
5
+ const niimbluelib_1 = require("@mmote/niimbluelib");
6
+ // Open SerialPort asynchronously instead of callback
7
+ const serialOpenAsync = (path) => {
8
+ return new Promise((resolve, reject) => {
9
+ const p = new serialport_1.SerialPort({ path, baudRate: 115200, endOnClose: true, autoOpen: false });
10
+ p.open((err) => {
11
+ if (err) {
12
+ reject(err);
13
+ }
14
+ else {
15
+ resolve(p);
16
+ }
17
+ });
18
+ });
19
+ };
20
+ /** WIP. Uses serial communication (serialport lib) */
21
+ class NiimbotHeadlessSerialClient extends niimbluelib_1.NiimbotAbstractClient {
22
+ constructor() {
23
+ super();
24
+ this.isOpen = false;
25
+ }
26
+ /** Set port for connect */
27
+ setPort(portName) {
28
+ this.portName = portName;
29
+ }
30
+ async connect() {
31
+ await this.disconnect();
32
+ if (!this.portName) {
33
+ throw new Error("Port not set");
34
+ }
35
+ const _port = await serialOpenAsync(this.portName);
36
+ this.isOpen = true;
37
+ _port.on("close", () => {
38
+ this.isOpen = false;
39
+ this.emit("disconnect", new niimbluelib_1.DisconnectEvent());
40
+ });
41
+ _port.on("readable", () => {
42
+ this.dataReady();
43
+ });
44
+ this.device = _port;
45
+ try {
46
+ await this.initialNegotiate();
47
+ await this.fetchPrinterInfo();
48
+ }
49
+ catch (e) {
50
+ console.error("Unable to fetch printer info (is it turned on?).");
51
+ console.error(e);
52
+ }
53
+ const result = {
54
+ deviceName: `Serial (${this.portName})`,
55
+ result: this.info.connectResult ?? niimbluelib_1.ConnectResult.FirmwareErrors,
56
+ };
57
+ this.emit("connect", new niimbluelib_1.ConnectEvent(result));
58
+ return result;
59
+ }
60
+ dataReady() {
61
+ while (true) {
62
+ try {
63
+ const result = this.device.read();
64
+ if (result !== null) {
65
+ if (this.debug) {
66
+ console.info(`<< serial chunk ${niimbluelib_1.Utils.bufToHex(result)}`);
67
+ }
68
+ this.processRawPacket(result);
69
+ }
70
+ else {
71
+ break;
72
+ }
73
+ }
74
+ catch (_e) {
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ async disconnect() {
80
+ this.stopHeartbeat();
81
+ this.device?.close();
82
+ }
83
+ isConnected() {
84
+ return this.isOpen;
85
+ }
86
+ async sendRaw(data, force) {
87
+ const send = async () => {
88
+ if (!this.isConnected()) {
89
+ throw new Error("Not connected");
90
+ }
91
+ await niimbluelib_1.Utils.sleep(this.packetIntervalMs);
92
+ this.device.write(Buffer.from(data));
93
+ this.emit("rawpacketsent", new niimbluelib_1.RawPacketSentEvent(data));
94
+ };
95
+ if (force) {
96
+ await send();
97
+ }
98
+ else {
99
+ await this.mutex.runExclusive(send);
100
+ }
101
+ }
102
+ static async scan() {
103
+ const ports = await serialport_1.SerialPort.list();
104
+ return ports.map((p) => {
105
+ let name = "unknown";
106
+ let pRaw = p;
107
+ if (pRaw["friendlyName"] !== undefined) {
108
+ name = pRaw["friendlyName"];
109
+ }
110
+ else if (p.pnpId !== undefined) {
111
+ name = p.pnpId;
112
+ }
113
+ return {
114
+ name,
115
+ address: p.path,
116
+ };
117
+ });
118
+ }
119
+ }
120
+ exports.NiimbotHeadlessSerialClient = NiimbotHeadlessSerialClient;
@@ -0,0 +1,6 @@
1
+ import { EncodedImage, PrintDirection } from "@mmote/niimbluelib";
2
+ import sharp from "sharp";
3
+ export declare class ImageEncoder {
4
+ static encodeImage(src: sharp.Sharp, printDirection?: PrintDirection): Promise<EncodedImage>;
5
+ static isPixelNonWhite(buf: Buffer<ArrayBufferLike>, imgWidth: number, imgHeight: number, x: number, y: number, printDirection?: PrintDirection): boolean;
6
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ImageEncoder = void 0;
4
+ const niimbluelib_1 = require("@mmote/niimbluelib");
5
+ class ImageEncoder {
6
+ static async encodeImage(src, printDirection = "left") {
7
+ const rowsData = [];
8
+ const { data, info } = await src
9
+ .flatten({ background: "#fff" })
10
+ .toColorspace("b-w")
11
+ .raw()
12
+ .toBuffer({ resolveWithObject: true });
13
+ let cols = info.width;
14
+ let rows = info.height;
15
+ if (printDirection === "left") {
16
+ cols = info.height;
17
+ rows = info.width;
18
+ }
19
+ if (cols % 8 !== 0) {
20
+ throw new Error("Column count must be multiple of 8");
21
+ }
22
+ for (let row = 0; row < rows; row++) {
23
+ let isVoid = true;
24
+ let blackPixelsCount = 0;
25
+ const rowData = new Uint8Array(cols / 8);
26
+ for (let colOct = 0; colOct < cols / 8; colOct++) {
27
+ let pixelsOctet = 0;
28
+ for (let colBit = 0; colBit < 8; colBit++) {
29
+ if (ImageEncoder.isPixelNonWhite(data, info.width, info.height, colOct * 8 + colBit, row, printDirection)) {
30
+ pixelsOctet |= 1 << (7 - colBit);
31
+ isVoid = false;
32
+ blackPixelsCount++;
33
+ }
34
+ }
35
+ rowData[colOct] = pixelsOctet;
36
+ }
37
+ const newPart = {
38
+ dataType: isVoid ? "void" : "pixels",
39
+ rowNumber: row,
40
+ repeat: 1,
41
+ rowData: isVoid ? undefined : rowData,
42
+ blackPixelsCount,
43
+ };
44
+ // Check previous row and increment repeats instead of adding new row if data is same
45
+ if (rowsData.length === 0) {
46
+ rowsData.push(newPart);
47
+ }
48
+ else {
49
+ const lastPacket = rowsData[rowsData.length - 1];
50
+ let same = newPart.dataType === lastPacket.dataType;
51
+ if (same && newPart.dataType === "pixels") {
52
+ same = niimbluelib_1.Utils.u8ArraysEqual(newPart.rowData, lastPacket.rowData);
53
+ }
54
+ if (same) {
55
+ lastPacket.repeat++;
56
+ }
57
+ else {
58
+ rowsData.push(newPart);
59
+ }
60
+ }
61
+ }
62
+ return { cols, rows, rowsData };
63
+ }
64
+ static isPixelNonWhite(buf, imgWidth, imgHeight, x, y, printDirection = "left") {
65
+ let idx = y * imgWidth + x;
66
+ if (printDirection === "left") {
67
+ idx = (imgHeight - 1 - x) * imgWidth + y;
68
+ }
69
+ return buf.at(idx) !== 0xff;
70
+ }
71
+ }
72
+ exports.ImageEncoder = ImageEncoder;
@@ -0,0 +1,4 @@
1
+ export { NiimbotHeadlessSerialClient } from "./client/headless_serial_impl";
2
+ export { NiimbotHeadlessBleClient } from "./client/headless_ble_impl";
3
+ export { ImageEncoder } from "./image_encoder";
4
+ export * from "./utils";
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ImageEncoder = exports.NiimbotHeadlessBleClient = exports.NiimbotHeadlessSerialClient = void 0;
18
+ var headless_serial_impl_1 = require("./client/headless_serial_impl");
19
+ Object.defineProperty(exports, "NiimbotHeadlessSerialClient", { enumerable: true, get: function () { return headless_serial_impl_1.NiimbotHeadlessSerialClient; } });
20
+ var headless_ble_impl_1 = require("./client/headless_ble_impl");
21
+ Object.defineProperty(exports, "NiimbotHeadlessBleClient", { enumerable: true, get: function () { return headless_ble_impl_1.NiimbotHeadlessBleClient; } });
22
+ var image_encoder_1 = require("./image_encoder");
23
+ Object.defineProperty(exports, "ImageEncoder", { enumerable: true, get: function () { return image_encoder_1.ImageEncoder; } });
24
+ __exportStar(require("./utils"), exports);
@@ -0,0 +1,7 @@
1
+ export type ServerOptions = {
2
+ debug: boolean;
3
+ port: number;
4
+ host: string;
5
+ cors: boolean;
6
+ };
7
+ export declare const cliStartServer: (options: ServerOptions) => void;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.cliStartServer = void 0;
37
+ const simple_server_1 = require("./simple_server");
38
+ const w = __importStar(require("./worker"));
39
+ const cliStartServer = (options) => {
40
+ w.setDebug(options.debug);
41
+ const s = new simple_server_1.SimpleServer();
42
+ if (options.cors) {
43
+ s.enableCors();
44
+ }
45
+ s.anything("/", w.index);
46
+ s.post("/connect", w.connect);
47
+ s.post("/disconnect", w.disconnect);
48
+ s.get("/connected", w.connected);
49
+ s.get("/info", w.info);
50
+ s.post("/print", w.print);
51
+ s.post("/scan", w.scan);
52
+ s.start(options.host, options.port, () => {
53
+ console.log(`Server is listening ${options.host}:${options.port}`);
54
+ if (options.cors) {
55
+ console.log("CORS enabled");
56
+ }
57
+ });
58
+ };
59
+ exports.cliStartServer = cliStartServer;
@@ -0,0 +1,20 @@
1
+ import http from "http";
2
+ import { z } from "zod";
3
+ type RouteHandler = (request: http.IncomingMessage) => unknown;
4
+ export declare class RestError extends Error {
5
+ readonly status: number;
6
+ constructor(message: string, status?: number);
7
+ }
8
+ export declare const writeObj: (response: http.ServerResponse, o: unknown, status?: number) => void;
9
+ export declare const readBodyJson: <T>(request: http.IncomingMessage, schema: z.ZodType<T>) => Promise<T>;
10
+ export declare class SimpleServer {
11
+ private readonly routes;
12
+ private corsEnabled;
13
+ enableCors(): void;
14
+ get(path: string, handler: RouteHandler): void;
15
+ post(path: string, handler: RouteHandler): void;
16
+ anything(path: string, handler: RouteHandler): void;
17
+ private onRequest;
18
+ start(host: string, port: number, listeningListener?: () => void): void;
19
+ }
20
+ export {};
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SimpleServer = exports.readBodyJson = exports.writeObj = exports.RestError = void 0;
7
+ const http_1 = __importDefault(require("http"));
8
+ const zod_1 = require("zod");
9
+ class RestError extends Error {
10
+ constructor(message, status = 500) {
11
+ super(message);
12
+ this.status = status;
13
+ }
14
+ }
15
+ exports.RestError = RestError;
16
+ const writeObj = (response, o, status = 200) => {
17
+ response.setHeader("Content-Type", "application/json");
18
+ response.writeHead(status);
19
+ response.end(JSON.stringify(o));
20
+ };
21
+ exports.writeObj = writeObj;
22
+ const readBodyJson = async (request, schema) => {
23
+ return new Promise((resolve, reject) => {
24
+ const bodyParts = [];
25
+ request
26
+ .on("data", (chunk) => {
27
+ bodyParts.push(chunk);
28
+ })
29
+ .on("end", () => {
30
+ let body = Buffer.concat(bodyParts).toString();
31
+ let data = null;
32
+ try {
33
+ data = JSON.parse(body);
34
+ }
35
+ catch (e) {
36
+ reject(e);
37
+ }
38
+ if (data === null) {
39
+ reject(new Error("No data"));
40
+ }
41
+ const result = schema.safeParse(data);
42
+ if (result.success) {
43
+ resolve(result.data);
44
+ }
45
+ else {
46
+ reject(result.error);
47
+ }
48
+ });
49
+ });
50
+ };
51
+ exports.readBodyJson = readBodyJson;
52
+ class SimpleServer {
53
+ constructor() {
54
+ this.routes = [];
55
+ this.corsEnabled = false;
56
+ }
57
+ enableCors() {
58
+ this.corsEnabled = true;
59
+ }
60
+ get(path, handler) {
61
+ this.routes.push({ path, handler, method: "GET" });
62
+ }
63
+ post(path, handler) {
64
+ this.routes.push({ path, handler, method: "POST" });
65
+ }
66
+ anything(path, handler) {
67
+ this.routes.push({ path, handler });
68
+ }
69
+ async onRequest(request, response) {
70
+ if (request.url === undefined || request.method === undefined) {
71
+ return;
72
+ }
73
+ console.log(`${request.socket.remoteAddress} ${request.method} ${request.url}`);
74
+ if (this.corsEnabled) {
75
+ response.setHeader("Access-Control-Allow-Origin", "*");
76
+ response.setHeader("Access-Control-Allow-Headers", "*");
77
+ response.setHeader("Access-Control-Allow-Methods", "OPTIONS, POST, GET");
78
+ response.setHeader("Access-Control-Max-Age", 2592000); // 30 days
79
+ if (request.method === "OPTIONS") {
80
+ response.writeHead(204);
81
+ response.end();
82
+ return;
83
+ }
84
+ }
85
+ try {
86
+ const route = this.routes.find((r) => r.path === request.url && (r.method === undefined || r.method === request.method));
87
+ if (route === undefined) {
88
+ (0, exports.writeObj)(response, { error: "Not found" }, 404);
89
+ return;
90
+ }
91
+ if (request.method === "POST" && request.headers["content-type"] !== "application/json") {
92
+ (0, exports.writeObj)(response, { error: "Only JSON accepted" }, 400);
93
+ return;
94
+ }
95
+ const result = await route.handler(request);
96
+ (0, exports.writeObj)(response, result, 200);
97
+ }
98
+ catch (e) {
99
+ if (e instanceof zod_1.z.ZodError) {
100
+ const error = e.issues.map((i) => `${i.path.join("→")}: ${i.message}`).join("\n");
101
+ (0, exports.writeObj)(response, { error }, 400);
102
+ }
103
+ else if (e instanceof RestError) {
104
+ (0, exports.writeObj)(response, { error: e.message }, e.status);
105
+ }
106
+ else {
107
+ (0, exports.writeObj)(response, { error: `${e}` }, 500);
108
+ }
109
+ }
110
+ }
111
+ start(host, port, listeningListener) {
112
+ const server = http_1.default.createServer();
113
+ server.on("request", (req, res) => this.onRequest(req, res));
114
+ server.listen({ port, host }, listeningListener);
115
+ }
116
+ }
117
+ exports.SimpleServer = SimpleServer;
@@ -0,0 +1,25 @@
1
+ import { IncomingMessage } from "http";
2
+ export declare const setDebug: (v: boolean) => void;
3
+ export declare const index: () => {
4
+ message: string;
5
+ };
6
+ export declare const connect: (r: IncomingMessage) => Promise<{
7
+ message: string;
8
+ }>;
9
+ export declare const disconnect: () => Promise<{
10
+ message: string;
11
+ }>;
12
+ export declare const connected: () => Promise<{
13
+ connected: boolean;
14
+ }>;
15
+ export declare const info: () => Promise<{
16
+ printerInfo: import("@mmote/niimbluelib").PrinterInfo;
17
+ modelMetadata: import("@mmote/niimbluelib").PrinterModelMeta | undefined;
18
+ detectedPrintTask: "D11_V1" | "D110" | "B1" | "B21_V1" | "V5" | undefined;
19
+ }>;
20
+ export declare const print: (r: IncomingMessage) => Promise<{
21
+ message: string;
22
+ }>;
23
+ export declare const scan: (r: IncomingMessage) => Promise<{
24
+ devices: import("../client/headless_ble_impl").ScanItem[];
25
+ }>;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.scan = exports.print = exports.info = exports.connected = exports.disconnect = exports.connect = exports.index = exports.setDebug = void 0;
7
+ const niimbluelib_1 = require("@mmote/niimbluelib");
8
+ const sharp_1 = __importDefault(require("sharp"));
9
+ const zod_1 = require("zod");
10
+ const headless_ble_impl_1 = require("../client/headless_ble_impl");
11
+ const image_encoder_1 = require("../image_encoder");
12
+ const utils_1 = require("../utils");
13
+ const simple_server_1 = require("./simple_server");
14
+ const headless_serial_impl_1 = require("../client/headless_serial_impl");
15
+ let client = null;
16
+ let debug = false;
17
+ const ConnectSchema = zod_1.z.object({
18
+ transport: zod_1.z.enum(["serial", "ble"]),
19
+ address: zod_1.z.string(),
20
+ });
21
+ const ScanSchema = zod_1.z.object({
22
+ transport: zod_1.z.enum(["serial", "ble"]),
23
+ timeout: zod_1.z.number().default(5000),
24
+ });
25
+ const [firstTask, ...otherTasks] = niimbluelib_1.printTaskNames;
26
+ const PrintSchema = zod_1.z
27
+ .object({
28
+ printDirection: zod_1.z.enum(["left", "top"]).optional(),
29
+ printTask: zod_1.z.enum([firstTask, ...otherTasks]).optional(),
30
+ quantity: zod_1.z.number().min(1).default(1),
31
+ labelType: zod_1.z.number().min(1).default(niimbluelib_1.LabelType.WithGaps),
32
+ density: zod_1.z.number().min(1).default(3),
33
+ imageBase64: zod_1.z.string().optional(),
34
+ imageUrl: zod_1.z.string().optional(),
35
+ labelWidth: zod_1.z.number().positive().optional(),
36
+ labelHeight: zod_1.z.number().positive().optional(),
37
+ threshold: zod_1.z.number().min(1).max(255).default(128),
38
+ imagePosition: zod_1.z
39
+ .enum(["centre", "top", "right top", "right", "right bottom", "bottom", "left bottom", "left", "left top"])
40
+ .default("centre"),
41
+ imageFit: zod_1.z.enum(["contain", "cover", "fill", "inside", "outside"]).default("contain"),
42
+ })
43
+ .refine(({ imageUrl, imageBase64 }) => {
44
+ return !!imageUrl !== !!imageBase64;
45
+ }, { message: "imageUrl or imageBase64 must be defined", path: ["image"] });
46
+ const setDebug = (v) => {
47
+ debug = v;
48
+ };
49
+ exports.setDebug = setDebug;
50
+ const assertConnected = () => {
51
+ if (!client?.isConnected()) {
52
+ throw new simple_server_1.RestError("Not connected", 400);
53
+ }
54
+ };
55
+ const index = () => ({ message: "Server is working" });
56
+ exports.index = index;
57
+ const connect = async (r) => {
58
+ const data = await (0, simple_server_1.readBodyJson)(r, ConnectSchema);
59
+ if (client?.isConnected()) {
60
+ throw new simple_server_1.RestError("Already connected", 400);
61
+ }
62
+ client = (0, utils_1.initClient)(data.transport, data.address, debug);
63
+ await client.connect();
64
+ return { message: "Connected" };
65
+ };
66
+ exports.connect = connect;
67
+ const disconnect = async () => {
68
+ assertConnected();
69
+ await client.disconnect();
70
+ client = null;
71
+ return { message: "Disconnected" };
72
+ };
73
+ exports.disconnect = disconnect;
74
+ const connected = async () => {
75
+ return { connected: !!client?.isConnected() };
76
+ };
77
+ exports.connected = connected;
78
+ const info = async () => {
79
+ assertConnected();
80
+ return {
81
+ printerInfo: client.getPrinterInfo(),
82
+ modelMetadata: client.getModelMetadata(),
83
+ detectedPrintTask: client.getPrintTaskType(),
84
+ };
85
+ };
86
+ exports.info = info;
87
+ const print = async (r) => {
88
+ assertConnected();
89
+ const options = await (0, simple_server_1.readBodyJson)(r, PrintSchema);
90
+ let image;
91
+ if (options.imageBase64 !== undefined) {
92
+ image = await (0, utils_1.loadImageFromBase64)(options.imageBase64);
93
+ }
94
+ else if (options.imageUrl !== undefined) {
95
+ image = await (0, utils_1.loadImageFromUrl)(options.imageUrl);
96
+ }
97
+ else {
98
+ throw new simple_server_1.RestError("Image is not defined", 400);
99
+ }
100
+ image = image.flatten({ background: "#fff" });
101
+ if (options.labelWidth !== undefined && options.labelHeight !== undefined) {
102
+ image = image.resize(options.labelWidth, options.labelHeight, {
103
+ kernel: sharp_1.default.kernel.nearest,
104
+ fit: options.imageFit,
105
+ position: options.imagePosition,
106
+ background: "#fff",
107
+ });
108
+ }
109
+ image = image.threshold(options.threshold);
110
+ // await image.toFile("tmp.png");
111
+ const printDirection = options.printDirection ?? client.getModelMetadata()?.printDirection;
112
+ const printTask = options.printTask ?? client.getPrintTaskType();
113
+ const encoded = await image_encoder_1.ImageEncoder.encodeImage(image, printDirection);
114
+ if (printTask === undefined) {
115
+ throw new simple_server_1.RestError("Unable to detect print task, please set it manually", 400);
116
+ }
117
+ if (debug) {
118
+ console.log("Print task:", printTask);
119
+ }
120
+ await (0, utils_1.printImage)(client, printTask, encoded, {
121
+ quantity: options.quantity,
122
+ labelType: options.labelType,
123
+ density: options.density,
124
+ });
125
+ return { message: "Printed" };
126
+ };
127
+ exports.print = print;
128
+ const scan = async (r) => {
129
+ const options = await (0, simple_server_1.readBodyJson)(r, ScanSchema);
130
+ if (options.transport === "ble") {
131
+ return { devices: await headless_ble_impl_1.NiimbotHeadlessBleClient.scan(options.timeout) };
132
+ }
133
+ else if (options.transport === "serial") {
134
+ return { devices: await headless_serial_impl_1.NiimbotHeadlessSerialClient.scan() };
135
+ }
136
+ throw new simple_server_1.RestError("Invalid transport", 400);
137
+ };
138
+ exports.scan = scan;
@@ -0,0 +1 @@
1
+ {"root":["../src/image_encoder.ts","../src/index.ts","../src/utils.ts","../src/cli/index.ts","../src/cli/worker.ts","../src/client/headless_ble_impl.ts","../src/client/headless_serial_impl.ts","../src/server/index.ts","../src/server/simple_server.ts","../src/server/worker.ts"],"version":"5.7.3"}
@@ -0,0 +1,13 @@
1
+ import { EncodedImage, LabelType, NiimbotAbstractClient, PrintTaskName } from "@mmote/niimbluelib";
2
+ import sharp from "sharp";
3
+ export type TransportType = "serial" | "ble";
4
+ export interface PrintOptions {
5
+ quantity?: number;
6
+ labelType?: LabelType;
7
+ density?: number;
8
+ }
9
+ export declare const initClient: (transport: TransportType, address: string, debug: boolean) => NiimbotAbstractClient;
10
+ export declare const printImage: (client: NiimbotAbstractClient, printTaskName: PrintTaskName, encoded: EncodedImage, options: PrintOptions) => Promise<void>;
11
+ export declare const loadImageFromBase64: (b64: string) => Promise<sharp.Sharp>;
12
+ export declare const loadImageFromUrl: (url: string) => Promise<sharp.Sharp>;
13
+ export declare const loadImageFromFile: (path: string) => Promise<sharp.Sharp>;
package/dist/utils.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadImageFromFile = exports.loadImageFromUrl = exports.loadImageFromBase64 = exports.printImage = exports.initClient = void 0;
7
+ const niimbluelib_1 = require("@mmote/niimbluelib");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const sharp_1 = __importDefault(require("sharp"));
10
+ const stream_1 = require("stream");
11
+ const _1 = require(".");
12
+ const initClient = (transport, address, debug) => {
13
+ let client = null;
14
+ if (transport === "serial") {
15
+ client = new _1.NiimbotHeadlessSerialClient();
16
+ client.setPort(address);
17
+ }
18
+ else if (transport === "ble") {
19
+ client = new _1.NiimbotHeadlessBleClient();
20
+ client.setAddress(address);
21
+ }
22
+ else {
23
+ throw new Error("Invalid transport");
24
+ }
25
+ client.on("printprogress", (e) => {
26
+ console.log(`Page ${e.page}/${e.pagesTotal}, Page print ${e.pagePrintProgress}%, Page feed ${e.pageFeedProgress}%`);
27
+ });
28
+ client.on("heartbeatfailed", (e) => {
29
+ const maxFails = 5;
30
+ console.warn(`Heartbeat failed ${e.failedAttempts}/${maxFails}`);
31
+ if (e.failedAttempts >= maxFails) {
32
+ console.warn("Disconnecting");
33
+ client.disconnect();
34
+ }
35
+ });
36
+ if (debug) {
37
+ client.on("packetsent", (e) => {
38
+ console.log(`>> ${niimbluelib_1.Utils.bufToHex(e.packet.toBytes())} (${niimbluelib_1.RequestCommandId[e.packet.command]})`);
39
+ });
40
+ client.on("packetreceived", (e) => {
41
+ console.log(`<< ${niimbluelib_1.Utils.bufToHex(e.packet.toBytes())} (${niimbluelib_1.ResponseCommandId[e.packet.command]})`);
42
+ });
43
+ client.on("connect", () => {
44
+ console.log("Connected");
45
+ });
46
+ client.on("disconnect", () => {
47
+ console.log("Disconnected");
48
+ });
49
+ }
50
+ return client;
51
+ };
52
+ exports.initClient = initClient;
53
+ const printImage = async (client, printTaskName, encoded, options) => {
54
+ const printTask = client.abstraction.newPrintTask(printTaskName, {
55
+ density: options.density ?? 3,
56
+ labelType: options.labelType ?? niimbluelib_1.LabelType.WithGaps,
57
+ totalPages: options.quantity ?? 1,
58
+ statusPollIntervalMs: 500,
59
+ statusTimeoutMs: 8_000,
60
+ });
61
+ try {
62
+ await printTask.printInit();
63
+ await printTask.printPage(encoded, options.quantity ?? 1);
64
+ await printTask.waitForFinished();
65
+ }
66
+ catch (e) {
67
+ console.error(e);
68
+ }
69
+ await client.abstraction.printEnd();
70
+ };
71
+ exports.printImage = printImage;
72
+ const loadImageFromBase64 = async (b64) => {
73
+ const buf = Buffer.from(b64, "base64");
74
+ const stream = stream_1.Readable.from(buf);
75
+ return stream.pipe((0, sharp_1.default)());
76
+ };
77
+ exports.loadImageFromBase64 = loadImageFromBase64;
78
+ const loadImageFromUrl = async (url) => {
79
+ const { body, ok, status } = await fetch(url);
80
+ if (!ok) {
81
+ throw new Error(`Can't fetch image, error ${status}`);
82
+ }
83
+ if (body === null) {
84
+ throw new Error("Body is null");
85
+ }
86
+ return stream_1.Readable.fromWeb(body).pipe((0, sharp_1.default)());
87
+ };
88
+ exports.loadImageFromUrl = loadImageFromUrl;
89
+ const loadImageFromFile = async (path) => {
90
+ const stream = fs_1.default.createReadStream(path);
91
+ return stream.pipe((0, sharp_1.default)());
92
+ };
93
+ exports.loadImageFromFile = loadImageFromFile;
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@mmote/niimblue-node",
3
+ "version": "0.0.7",
4
+ "description": "Headless clients for niimbluelib. Command line interface, simple REST server are also included.",
5
+ "keywords": [
6
+ "command-line",
7
+ "thermal-printer",
8
+ "label-printer",
9
+ "niimbot",
10
+ "niimbot-d110",
11
+ "niimbot-b1",
12
+ "bluetooth",
13
+ "serial"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/MultiMote/niimblue-node.git"
18
+ },
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "files": [
22
+ "/dist"
23
+ ],
24
+ "bin": {
25
+ "niimblue-cli": "./cli.mjs"
26
+ },
27
+ "author": "MultiMote",
28
+ "license": "MIT",
29
+ "private": false,
30
+ "scripts": {
31
+ "clean-build": "yarn clean && yarn build",
32
+ "build": "tsc --build",
33
+ "cli": "tsc --build && node cli.mjs"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.14.2",
37
+ "typescript": "^5.4.5"
38
+ },
39
+ "dependencies": {
40
+ "@abandonware/noble": "^1.9.2-26",
41
+ "@commander-js/extra-typings": "^12.1.0",
42
+ "@mmote/niimbluelib": "0.0.1-alpha.24",
43
+ "async-mutex": "^0.5.0",
44
+ "commander": "^12.1.0",
45
+ "serialport": "^12.0.0",
46
+ "sharp": "^0.33.5",
47
+ "zod": "^3.23.8"
48
+ }
49
+ }