@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 +101 -0
- package/cli.mjs +3 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +79 -0
- package/dist/cli/worker.d.ts +41 -0
- package/dist/cli/worker.js +95 -0
- package/dist/client/headless_ble_impl.d.ts +21 -0
- package/dist/client/headless_ble_impl.js +166 -0
- package/dist/client/headless_serial_impl.d.ts +20 -0
- package/dist/client/headless_serial_impl.js +120 -0
- package/dist/image_encoder.d.ts +6 -0
- package/dist/image_encoder.js +72 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +24 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +59 -0
- package/dist/server/simple_server.d.ts +20 -0
- package/dist/server/simple_server.js +117 -0
- package/dist/server/worker.d.ts +25 -0
- package/dist/server/worker.js +138 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +93 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
## 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 @@
|
|
|
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;
|
package/dist/index.d.ts
ADDED
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,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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|