libniimai 0.2.0 → 0.3.0
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 +14 -11
- package/dist/ble.d.ts +2 -2
- package/dist/bluetooth.d.ts +10 -4
- package/dist/cat.d.ts +13 -3
- package/dist/catalog.d.ts +17 -0
- package/dist/client.d.ts +10 -0
- package/dist/escpos.d.ts +29 -8
- package/dist/index.d.ts +4 -2
- package/dist/libniimai.js +370 -65
- package/dist/libniimai.js.map +1 -1
- package/dist/service.d.ts +17 -0
- package/dist/thermal.d.ts +12 -1
- package/dist/tspl.d.ts +2 -2
- package/package.json +1 -1
package/dist/libniimai.js
CHANGED
|
@@ -29,8 +29,12 @@ var NAME_PREFIXES = [
|
|
|
29
29
|
"T",
|
|
30
30
|
"Z"
|
|
31
31
|
];
|
|
32
|
-
/** Opens the browser's printer chooser. */
|
|
33
|
-
function requestPrinter(extraPrefixes = []) {
|
|
32
|
+
/** Opens the browser's printer chooser. `all` lists every device, for printers whose names niimai does not know. */
|
|
33
|
+
function requestPrinter(extraPrefixes = [], all = false) {
|
|
34
|
+
if (all) return navigator.bluetooth.requestDevice({
|
|
35
|
+
acceptAllDevices: true,
|
|
36
|
+
optionalServices: CANDIDATE_SERVICES
|
|
37
|
+
});
|
|
34
38
|
return navigator.bluetooth.requestDevice({
|
|
35
39
|
filters: [...[...NAME_PREFIXES, ...extraPrefixes].map((namePrefix) => ({ namePrefix })), { services: [CANDIDATE_SERVICES[0]] }],
|
|
36
40
|
optionalServices: CANDIDATE_SERVICES
|
|
@@ -154,6 +158,8 @@ function createBleClient(lib, chosen) {
|
|
|
154
158
|
var THERMAL_HEAD_DOTS = 384;
|
|
155
159
|
/** The print width in whole millimetres, the widest a design on continuous paper can be. */
|
|
156
160
|
var THERMAL_HEAD_MM = 48;
|
|
161
|
+
/** The name a printer shows: its model, or its own Bluetooth name when the model entry is only a brand or a stock name. */
|
|
162
|
+
var shownName = (model, transport, fallback) => (model && !model.generic ? model.prefix : transport.name) || model?.prefix || fallback;
|
|
157
163
|
/** The known model a Bluetooth name belongs to, longest prefix first. */
|
|
158
164
|
function modelFor(models, name) {
|
|
159
165
|
if (!name) return null;
|
|
@@ -301,13 +307,14 @@ var ThermalPrinterClient = class {
|
|
|
301
307
|
newPrintTask: (_name, options) => {
|
|
302
308
|
const density = Number(options.density ?? 1);
|
|
303
309
|
const labelType = Number(options.labelType ?? 1);
|
|
310
|
+
const gapDots = options.gapDots === void 0 ? void 0 : Math.max(0, Math.round(Number(options.gapDots)));
|
|
304
311
|
let printed = 0;
|
|
305
312
|
return {
|
|
306
313
|
printInit: async () => {},
|
|
307
314
|
printPage: async (image, quantity = 1) => {
|
|
308
315
|
const total = () => Math.max(printed, Number(options.totalPages ?? printed));
|
|
309
316
|
let shown = -1;
|
|
310
|
-
await this.send(this.job(image, density, Math.max(1, quantity), labelType), (share) => {
|
|
317
|
+
await this.send(this.job(image, density, Math.max(1, quantity), labelType, gapDots), (share) => {
|
|
311
318
|
const sending = Math.floor(share * 100);
|
|
312
319
|
if (sending === shown) return;
|
|
313
320
|
shown = sending;
|
|
@@ -371,7 +378,7 @@ async function webTransport(device, server, ids) {
|
|
|
371
378
|
var CAT_SERVICE = "0000ae30-0000-1000-8000-00805f9b34fb";
|
|
372
379
|
var CAT_TX = "0000ae01-0000-1000-8000-00805f9b34fb";
|
|
373
380
|
var CAT_RX = "0000ae02-0000-1000-8000-00805f9b34fb";
|
|
374
|
-
var ROW_BYTES
|
|
381
|
+
var ROW_BYTES = 48;
|
|
375
382
|
/**
|
|
376
383
|
* Models known to speak this protocol. "high": supported by both reference
|
|
377
384
|
* projects and widely used. "medium": listed by Cat-Printer, fewer reports.
|
|
@@ -483,6 +490,7 @@ function packet(cmd, payload) {
|
|
|
483
490
|
}
|
|
484
491
|
var u16 = (v) => [v & 255, v >> 8 & 255];
|
|
485
492
|
var CMD = {
|
|
493
|
+
retract: 160,
|
|
486
494
|
feed: 161,
|
|
487
495
|
row: 162,
|
|
488
496
|
state: 163,
|
|
@@ -556,8 +564,12 @@ var FEED_AFTER = 128;
|
|
|
556
564
|
var GAP_BETWEEN = 24;
|
|
557
565
|
/** Rows for a cat printer: leftmost dot in the lowest bit. */
|
|
558
566
|
var encodeRows$2 = (data, width, height) => packRows(data, width, height, "lsb");
|
|
559
|
-
/**
|
|
560
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Everything sent for one job: setup, the rows for each copy, then feed and
|
|
569
|
+
* finish. With `gapDots`, for labels with gaps, every label is followed by
|
|
570
|
+
* exactly that feed, so the next print starts on the next label.
|
|
571
|
+
*/
|
|
572
|
+
function printJob(rows, { density, copies, model, gapDots }) {
|
|
561
573
|
const out = [];
|
|
562
574
|
const push = (cmd, payload) => out.push(...packet(cmd, payload));
|
|
563
575
|
push(CMD.state, [0]);
|
|
@@ -569,18 +581,28 @@ function printJob(rows, { density, copies, model }) {
|
|
|
569
581
|
push(CMD.applyEnergy, [1]);
|
|
570
582
|
push(CMD.update, [0]);
|
|
571
583
|
push(CMD.lattice, LATTICE_START);
|
|
572
|
-
const blank = new Uint8Array(ROW_BYTES
|
|
584
|
+
const blank = new Uint8Array(ROW_BYTES);
|
|
573
585
|
for (let c = 0; c < copies; c++) {
|
|
574
|
-
if (c > 0) for (let i = 0; i < GAP_BETWEEN; i++) push(CMD.row, blank);
|
|
586
|
+
if (c > 0) for (let i = 0; i < (gapDots ?? GAP_BETWEEN); i++) push(CMD.row, blank);
|
|
575
587
|
for (const row of rows) push(CMD.row, row);
|
|
576
588
|
}
|
|
577
589
|
push(CMD.lattice, LATTICE_END);
|
|
578
590
|
push(CMD.speed, [8]);
|
|
579
|
-
if (
|
|
591
|
+
if (gapDots !== void 0) out.push(...catFeed(gapDots, model));
|
|
592
|
+
else if (model?.feedWithRows) for (let i = 0; i < FEED_AFTER; i++) push(CMD.row, blank);
|
|
580
593
|
else push(CMD.feed, u16(FEED_AFTER));
|
|
581
594
|
push(CMD.state, [0]);
|
|
582
595
|
return Uint8Array.from(out);
|
|
583
596
|
}
|
|
597
|
+
/** Moves the paper by `dots`, back when negative. Models that mishandle the feed command get blank rows. */
|
|
598
|
+
function catFeed(dots, model) {
|
|
599
|
+
const n = Math.abs(Math.round(dots));
|
|
600
|
+
if (!n) return /* @__PURE__ */ new Uint8Array();
|
|
601
|
+
if (dots < 0) return Uint8Array.from(packet(CMD.retract, u16(n)));
|
|
602
|
+
if (!model?.feedWithRows) return Uint8Array.from(packet(CMD.feed, u16(n)));
|
|
603
|
+
const blank = new Uint8Array(ROW_BYTES);
|
|
604
|
+
return Uint8Array.from(Array.from({ length: n }, () => packet(CMD.row, blank)).flat());
|
|
605
|
+
}
|
|
584
606
|
/** A cat printer: the shared thermal client with cat packets and flow control. */
|
|
585
607
|
var CatPrinterClient = class extends ThermalPrinterClient {
|
|
586
608
|
model;
|
|
@@ -592,7 +614,7 @@ var CatPrinterClient = class extends ThermalPrinterClient {
|
|
|
592
614
|
}) {
|
|
593
615
|
super(transport, pace, 180);
|
|
594
616
|
this.model = model;
|
|
595
|
-
this.modelName = model
|
|
617
|
+
this.modelName = shownName(model, transport, "Cat printer");
|
|
596
618
|
transport.onNotify((data) => {
|
|
597
619
|
if (same(data, FLOW_PAUSE)) this.paused = true;
|
|
598
620
|
else if (same(data, FLOW_RESUME)) this.paused = false;
|
|
@@ -601,13 +623,19 @@ var CatPrinterClient = class extends ThermalPrinterClient {
|
|
|
601
623
|
encodeRows(data, width, height) {
|
|
602
624
|
return encodeRows$2(data, width, height);
|
|
603
625
|
}
|
|
604
|
-
job(rows, density, copies) {
|
|
626
|
+
job(rows, density, copies, _labelType, gapDots) {
|
|
605
627
|
return printJob(rows, {
|
|
606
628
|
density,
|
|
607
629
|
copies,
|
|
608
|
-
model: this.model
|
|
630
|
+
model: this.model,
|
|
631
|
+
gapDots
|
|
609
632
|
});
|
|
610
633
|
}
|
|
634
|
+
feedsBack = true;
|
|
635
|
+
/** These printers cannot sense the paper, so the user lines it up by hand. */
|
|
636
|
+
async feedPaper(dots) {
|
|
637
|
+
await this.send(catFeed(dots, this.model));
|
|
638
|
+
}
|
|
611
639
|
};
|
|
612
640
|
var same = (a, b) => a.length === b.length && b.every((v, i) => a[i] === v);
|
|
613
641
|
// src/escpos.ts
|
|
@@ -618,10 +646,17 @@ var same = (a, b) => a.length === b.length && b.every((v, i) => a[i] === v);
|
|
|
618
646
|
*/
|
|
619
647
|
var ESCPOS_SERVICE = "000018f0-0000-1000-8000-00805f9b34fb";
|
|
620
648
|
var ESCPOS_WRITE = "00002af1-0000-1000-8000-00805f9b34fb";
|
|
649
|
+
/** The serial service many budget printers offer, Xprinter among them; NIIMBOT printers use it too. */
|
|
650
|
+
var ESCPOS_ALT_SERVICE = "e7810a71-73ae-499d-8c15-faa9aef0c3f2";
|
|
651
|
+
var ESCPOS_ALT_WRITE = "bef8d6c9-9c21-4c9e-b632-bd58c1009f9f";
|
|
621
652
|
/**
|
|
622
|
-
* Models known to take ESC/POS raster images
|
|
623
|
-
* with niimai on real hardware. "
|
|
624
|
-
*
|
|
653
|
+
* Models known to take ESC/POS raster images, by Bluetooth name. "tested":
|
|
654
|
+
* printed with niimai on real hardware. "high": supported by several
|
|
655
|
+
* open-source printer projects. "medium": the name is reported by one or two
|
|
656
|
+
* of them, untested here. Names from receipt-printer projects such as
|
|
657
|
+
* WebBluetoothReceiptPrinter and bitbank2/Thermal_Printer; see
|
|
658
|
+
* docs/thermal-printers.md for sources. Most of these printers share one
|
|
659
|
+
* Bluetooth module, so its services tell nothing apart: the name decides.
|
|
625
660
|
*/
|
|
626
661
|
var ESCPOS_MODELS = [
|
|
627
662
|
{
|
|
@@ -631,19 +666,57 @@ var ESCPOS_MODELS = [
|
|
|
631
666
|
},
|
|
632
667
|
{
|
|
633
668
|
prefix: "MPT-II",
|
|
634
|
-
confidence: "high"
|
|
669
|
+
confidence: "high",
|
|
670
|
+
note: "Stock module name on many 58 mm printers"
|
|
635
671
|
},
|
|
636
672
|
{
|
|
637
673
|
prefix: "MP58",
|
|
638
674
|
confidence: "medium"
|
|
639
675
|
},
|
|
676
|
+
{
|
|
677
|
+
prefix: "MP210",
|
|
678
|
+
confidence: "medium"
|
|
679
|
+
},
|
|
640
680
|
{
|
|
641
681
|
prefix: "MTP-II",
|
|
682
|
+
confidence: "high",
|
|
683
|
+
note: "Stock module name on many 58 mm printers"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
prefix: "MTP-2",
|
|
642
687
|
confidence: "medium"
|
|
643
688
|
},
|
|
689
|
+
{
|
|
690
|
+
prefix: "MPT-3",
|
|
691
|
+
confidence: "medium",
|
|
692
|
+
note: "80 mm",
|
|
693
|
+
headDots: 576
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
prefix: "MTP-3",
|
|
697
|
+
confidence: "medium",
|
|
698
|
+
note: "80 mm",
|
|
699
|
+
headDots: 576
|
|
700
|
+
},
|
|
644
701
|
{
|
|
645
702
|
prefix: "PT-210",
|
|
646
|
-
confidence: "
|
|
703
|
+
confidence: "high",
|
|
704
|
+
brand: "Goojprt PT-210"
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
prefix: "PT210",
|
|
708
|
+
confidence: "medium",
|
|
709
|
+
brand: "Goojprt PT-210"
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
prefix: "GOJRPT",
|
|
713
|
+
confidence: "medium",
|
|
714
|
+
brand: "Goojprt"
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
prefix: "Gojprt",
|
|
718
|
+
confidence: "medium",
|
|
719
|
+
brand: "Goojprt"
|
|
647
720
|
},
|
|
648
721
|
{
|
|
649
722
|
prefix: "PT-200",
|
|
@@ -651,35 +724,105 @@ var ESCPOS_MODELS = [
|
|
|
651
724
|
},
|
|
652
725
|
{
|
|
653
726
|
prefix: "RPP02",
|
|
654
|
-
confidence: "
|
|
727
|
+
confidence: "high",
|
|
728
|
+
brand: "Rongta RPP02N",
|
|
729
|
+
note: "Also sold as EPPOS EP58 Plus"
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
prefix: "RPP300",
|
|
733
|
+
confidence: "medium",
|
|
734
|
+
brand: "Rongta RPP300",
|
|
735
|
+
note: "80 mm",
|
|
736
|
+
headDots: 576
|
|
655
737
|
},
|
|
656
738
|
{
|
|
657
739
|
prefix: "ZJ-58",
|
|
658
|
-
confidence: "medium"
|
|
740
|
+
confidence: "medium",
|
|
741
|
+
brand: "Zjiang"
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
prefix: "TM-P",
|
|
745
|
+
confidence: "medium",
|
|
746
|
+
brand: "Epson TM-P series"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
prefix: "XP-P323B",
|
|
750
|
+
confidence: "medium",
|
|
751
|
+
brand: "Xprinter XP-P323B",
|
|
752
|
+
note: "80 mm",
|
|
753
|
+
headDots: 576
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
prefix: "XP-",
|
|
757
|
+
confidence: "medium",
|
|
758
|
+
brand: "other Xprinters",
|
|
759
|
+
generic: true
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
prefix: "Printer",
|
|
763
|
+
confidence: "tested",
|
|
764
|
+
brand: "Xprinter XP-58IIL, HOIN, Gprinter",
|
|
765
|
+
note: "Stock module name",
|
|
766
|
+
generic: true
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
prefix: "printer",
|
|
770
|
+
confidence: "medium",
|
|
771
|
+
note: "Stock module name",
|
|
772
|
+
generic: true
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
prefix: "BlueTooth Printer",
|
|
776
|
+
confidence: "medium",
|
|
777
|
+
note: "Stock module name: Zjiang POS-5805 and others",
|
|
778
|
+
generic: true
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
prefix: "58Printer",
|
|
782
|
+
confidence: "medium",
|
|
783
|
+
note: "Stock module name",
|
|
784
|
+
generic: true
|
|
659
785
|
}
|
|
660
786
|
];
|
|
661
787
|
var escposModelFor = (name) => modelFor(ESCPOS_MODELS, name);
|
|
662
|
-
var ROW_BYTES = 48;
|
|
663
788
|
/** Rows per raster command; small bands keep the printer's buffer from overflowing. */
|
|
664
789
|
var BAND = 24;
|
|
665
790
|
/** Dots fed between copies. */
|
|
666
791
|
var GAP_DOTS = 24;
|
|
667
792
|
/** Lines fed after the last copy, so the print clears the tear bar. */
|
|
668
793
|
var FEED_LINES = 4;
|
|
669
|
-
/** Rows for an ESC/POS printer: leftmost dot in the highest bit. */
|
|
670
|
-
var encodeRows$1 = (data, width, height) => packRows(data, width, height, "msb");
|
|
671
|
-
/**
|
|
672
|
-
|
|
794
|
+
/** Rows for an ESC/POS printer: leftmost dot in the highest bit, centred on a head `head` dots wide. */
|
|
795
|
+
var encodeRows$1 = (data, width, height, head = 384) => packRows(data, width, height, "msb", head);
|
|
796
|
+
/**
|
|
797
|
+
* ESC @, then each copy as GS v 0 bands with a gap between, then a feed past
|
|
798
|
+
* the tear bar. With `gapDots`, for labels with gaps, every label is followed
|
|
799
|
+
* by exactly that feed, so the next print starts on the next label.
|
|
800
|
+
*/
|
|
801
|
+
function escposJob(rows, copies, gapDots) {
|
|
673
802
|
const out = [27, 64];
|
|
803
|
+
const rowBytes = rows[0]?.length ?? 48;
|
|
674
804
|
for (let c = 0; c < copies; c++) {
|
|
675
|
-
if (c > 0) out.push(
|
|
805
|
+
if (c > 0) out.push(...escposFeed(gapDots ?? GAP_DOTS));
|
|
676
806
|
for (let i = 0; i < rows.length; i += BAND) {
|
|
677
807
|
const band = rows.slice(i, i + BAND);
|
|
678
|
-
out.push(29, 118, 48, 0,
|
|
808
|
+
out.push(29, 118, 48, 0, rowBytes & 255, rowBytes >> 8, band.length & 255, band.length >> 8);
|
|
679
809
|
for (const row of band) out.push(...row);
|
|
680
810
|
}
|
|
681
811
|
}
|
|
682
|
-
out.push(27, 100, FEED_LINES);
|
|
812
|
+
if (gapDots === void 0) out.push(27, 100, FEED_LINES);
|
|
813
|
+
else out.push(...escposFeed(gapDots));
|
|
814
|
+
return Uint8Array.from(out);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Moves the paper forward by `dots`: line feeds with the line spacing set to
|
|
818
|
+
* the distance (ESC 3 n, LF), then the default spacing back (ESC 2). The
|
|
819
|
+
* MP583 ignores ESC J, the usual dot feed, but takes line feeds. These
|
|
820
|
+
* printers have no reverse feed.
|
|
821
|
+
*/
|
|
822
|
+
function escposFeed(dots) {
|
|
823
|
+
const out = [];
|
|
824
|
+
for (let left = Math.round(dots); left > 0; left -= 255) out.push(27, 51, Math.min(255, left), 10);
|
|
825
|
+
if (out.length) out.push(27, 50);
|
|
683
826
|
return Uint8Array.from(out);
|
|
684
827
|
}
|
|
685
828
|
/**
|
|
@@ -691,19 +834,26 @@ var EscPosPrinterClient = class extends ThermalPrinterClient {
|
|
|
691
834
|
model;
|
|
692
835
|
densityLevels = 1;
|
|
693
836
|
modelName;
|
|
837
|
+
headDots;
|
|
694
838
|
constructor(transport, model, pace = {
|
|
695
839
|
chunkMs: 30,
|
|
696
840
|
pauseMs: 100
|
|
697
841
|
}) {
|
|
698
842
|
super(transport, pace, 100);
|
|
699
843
|
this.model = model;
|
|
700
|
-
this.modelName = model
|
|
844
|
+
this.modelName = shownName(model, transport, "Thermal printer");
|
|
845
|
+
this.headDots = model?.headDots ?? 384;
|
|
701
846
|
}
|
|
702
847
|
encodeRows(data, width, height) {
|
|
703
|
-
return encodeRows$1(data, width, height);
|
|
848
|
+
return encodeRows$1(data, width, height, this.headDots);
|
|
849
|
+
}
|
|
850
|
+
job(rows, _density, copies, _labelType, gapDots) {
|
|
851
|
+
return escposJob(rows, copies, gapDots);
|
|
704
852
|
}
|
|
705
|
-
|
|
706
|
-
|
|
853
|
+
/** These printers cannot sense the paper, so the user lines it up by hand. */
|
|
854
|
+
async feedPaper(dots) {
|
|
855
|
+
if (dots < 0) throw new Error("This printer cannot move the paper back");
|
|
856
|
+
await this.send(escposFeed(dots));
|
|
707
857
|
}
|
|
708
858
|
};
|
|
709
859
|
// src/tspl.ts
|
|
@@ -713,12 +863,17 @@ var TSPL_NOTIFY = "00002af0-0000-1000-8000-00805f9b34fb";
|
|
|
713
863
|
var DOTS_PER_MM = 8;
|
|
714
864
|
/**
|
|
715
865
|
* Models known to take TSPL over 18F0. "tested": printed with niimai on real
|
|
716
|
-
* hardware.
|
|
866
|
+
* hardware. "medium": reported by open-source printer projects, untested here.
|
|
717
867
|
*/
|
|
718
868
|
var TSPL_MODELS = [{
|
|
719
869
|
prefix: "QR380A",
|
|
720
870
|
confidence: "tested",
|
|
721
871
|
note: "Sold as CC390"
|
|
872
|
+
}, {
|
|
873
|
+
prefix: "GP-M322",
|
|
874
|
+
confidence: "medium",
|
|
875
|
+
brand: "Gprinter GP-M322",
|
|
876
|
+
headDots: 576
|
|
722
877
|
}];
|
|
723
878
|
var tsplModelFor = (name) => modelFor(TSPL_MODELS, name);
|
|
724
879
|
/** Common 2- to 3-inch label sizes, width × height in mm, smallest first. */
|
|
@@ -815,7 +970,7 @@ var TsplPrinterClient = class extends ThermalPrinterClient {
|
|
|
815
970
|
ackTimeoutMs;
|
|
816
971
|
densityLevels = DENSITY_LEVELS;
|
|
817
972
|
modelName;
|
|
818
|
-
headDots
|
|
973
|
+
headDots;
|
|
819
974
|
kind = void 0;
|
|
820
975
|
labelSizes = TSPL_SIZES;
|
|
821
976
|
oks = 0;
|
|
@@ -828,7 +983,8 @@ var TsplPrinterClient = class extends ThermalPrinterClient {
|
|
|
828
983
|
super(transport, pace, 100);
|
|
829
984
|
this.model = model;
|
|
830
985
|
this.ackTimeoutMs = ackTimeoutMs;
|
|
831
|
-
this.modelName = model
|
|
986
|
+
this.modelName = shownName(model, transport, "Label printer");
|
|
987
|
+
this.headDots = model?.headDots ?? 640;
|
|
832
988
|
transport.onNotify((data) => {
|
|
833
989
|
if (data.length >= 2 && data[0] === OK[0] && data[1] === OK[1]) this.oks++;
|
|
834
990
|
else if (data.length === 1) this.status = data[0];
|
|
@@ -876,14 +1032,137 @@ var TsplPrinterClient = class extends ThermalPrinterClient {
|
|
|
876
1032
|
console.warn("[niimai] Label printer did not confirm the job");
|
|
877
1033
|
}
|
|
878
1034
|
};
|
|
1035
|
+
// src/catalog.ts
|
|
1036
|
+
/**
|
|
1037
|
+
* Every printer outside NIIMBOT that libniimai knows, by Bluetooth name, in
|
|
1038
|
+
* one lookup across families. A name matches one family at most, and a name
|
|
1039
|
+
* that matches none is known to be unknown before anything is sent to it.
|
|
1040
|
+
* The models themselves live with their family: ESCPOS_MODELS, TSPL_MODELS
|
|
1041
|
+
* and CAT_MODELS.
|
|
1042
|
+
*/
|
|
1043
|
+
var PRINTER_CATALOG = [
|
|
1044
|
+
...ESCPOS_MODELS.map((model) => ({
|
|
1045
|
+
family: "escpos",
|
|
1046
|
+
model
|
|
1047
|
+
})),
|
|
1048
|
+
...TSPL_MODELS.map((model) => ({
|
|
1049
|
+
family: "tspl",
|
|
1050
|
+
model
|
|
1051
|
+
})),
|
|
1052
|
+
...CAT_MODELS.map((model) => ({
|
|
1053
|
+
family: "cat",
|
|
1054
|
+
model
|
|
1055
|
+
}))
|
|
1056
|
+
];
|
|
1057
|
+
/** The known printer a Bluetooth name belongs to, longest prefix first across every family. */
|
|
1058
|
+
function identifyPrinter(name) {
|
|
1059
|
+
if (!name) return null;
|
|
1060
|
+
let best = null;
|
|
1061
|
+
for (const entry of PRINTER_CATALOG) if (name.startsWith(entry.model.prefix) && entry.model.prefix.length > (best?.model.prefix.length ?? 0)) best = entry;
|
|
1062
|
+
return best;
|
|
1063
|
+
}
|
|
1064
|
+
/** The kinds of printer an unrecognised one can be set to, alongside NIIMBOT's MANUAL_PROFILES. */
|
|
1065
|
+
var PRINTER_TYPES = [
|
|
1066
|
+
{
|
|
1067
|
+
id: "escpos-58",
|
|
1068
|
+
name: "Receipt printer, 58 mm",
|
|
1069
|
+
family: "escpos",
|
|
1070
|
+
model: {
|
|
1071
|
+
prefix: "",
|
|
1072
|
+
confidence: "medium",
|
|
1073
|
+
generic: true,
|
|
1074
|
+
headDots: 384
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
id: "escpos-80",
|
|
1079
|
+
name: "Receipt printer, 80 mm",
|
|
1080
|
+
family: "escpos",
|
|
1081
|
+
model: {
|
|
1082
|
+
prefix: "",
|
|
1083
|
+
confidence: "medium",
|
|
1084
|
+
generic: true,
|
|
1085
|
+
headDots: 576
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
id: "tspl",
|
|
1090
|
+
name: "Label printer (TSPL)",
|
|
1091
|
+
family: "tspl",
|
|
1092
|
+
model: {
|
|
1093
|
+
prefix: "",
|
|
1094
|
+
confidence: "medium",
|
|
1095
|
+
generic: true,
|
|
1096
|
+
headDots: 640
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
id: "cat",
|
|
1101
|
+
name: "Cat printer",
|
|
1102
|
+
family: "cat",
|
|
1103
|
+
model: {
|
|
1104
|
+
prefix: "",
|
|
1105
|
+
confidence: "medium",
|
|
1106
|
+
generic: true
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
];
|
|
1110
|
+
var findPrinterType = (id) => PRINTER_TYPES.find((t) => t.id === id) ?? null;
|
|
879
1111
|
// src/bluetooth.ts
|
|
880
1112
|
/**
|
|
1113
|
+
* A writable characteristic on one of the serial services budget printers
|
|
1114
|
+
* offer, for ESC/POS printers on none of the known layouts.
|
|
1115
|
+
*/
|
|
1116
|
+
async function writeChannel(server, services) {
|
|
1117
|
+
for (const service of CANDIDATE_SERVICES) {
|
|
1118
|
+
if (!services.includes(service) || service === "0000ae30-0000-1000-8000-00805f9b34fb") continue;
|
|
1119
|
+
const write = (await server.getPrimaryService(service).then((s) => s.getCharacteristics()).catch(() => [])).find((c) => c.properties.writeWithoutResponse || c.properties.write);
|
|
1120
|
+
if (write) return {
|
|
1121
|
+
service,
|
|
1122
|
+
write: write.uuid
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
var PRINTER_TYPE_NAMES = {
|
|
1128
|
+
escpos: "receipt printer",
|
|
1129
|
+
tspl: "label printer",
|
|
1130
|
+
cat: "cat printer"
|
|
1131
|
+
};
|
|
1132
|
+
/** The client for a known or hand-picked thermal printer, over the service its family uses; null when the printer lacks it. */
|
|
1133
|
+
async function thermalClient(device, server, services, { family, model }) {
|
|
1134
|
+
if (family === "cat") return services.includes("0000ae30-0000-1000-8000-00805f9b34fb") ? new CatPrinterClient(await webTransport(device, server, {
|
|
1135
|
+
service: CAT_SERVICE,
|
|
1136
|
+
write: CAT_TX,
|
|
1137
|
+
notify: CAT_RX
|
|
1138
|
+
}), model) : null;
|
|
1139
|
+
if (family === "tspl") return services.includes("000018f0-0000-1000-8000-00805f9b34fb") ? new TsplPrinterClient(await webTransport(device, server, {
|
|
1140
|
+
service: TSPL_SERVICE,
|
|
1141
|
+
write: TSPL_WRITE,
|
|
1142
|
+
notify: TSPL_NOTIFY
|
|
1143
|
+
}), model) : null;
|
|
1144
|
+
const channel = services.includes("000018f0-0000-1000-8000-00805f9b34fb") ? {
|
|
1145
|
+
service: ESCPOS_SERVICE,
|
|
1146
|
+
write: ESCPOS_WRITE
|
|
1147
|
+
} : services.includes("e7810a71-73ae-499d-8c15-faa9aef0c3f2") ? {
|
|
1148
|
+
service: ESCPOS_ALT_SERVICE,
|
|
1149
|
+
write: ESCPOS_ALT_WRITE
|
|
1150
|
+
} : await writeChannel(server, services);
|
|
1151
|
+
return channel ? new EscPosPrinterClient(await webTransport(device, server, channel), model) : null;
|
|
1152
|
+
}
|
|
1153
|
+
/** The name starts like one of niimbluelib's NIIMBOT models ("D110-…", "B21_…"). */
|
|
1154
|
+
function looksNiimbot(lib, name) {
|
|
1155
|
+
const token = name?.split(/[-_ ]/)[0].toUpperCase();
|
|
1156
|
+
return !!token && lib.modelsLibrary.some((m) => m.model.split("_")[0] === token);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
881
1159
|
* The Bluetooth printer behind the Connect button. One chooser lists NIIMBOT
|
|
882
1160
|
* printers and the known extra thermal printers. A known cat printer offering
|
|
883
|
-
* AE30 gets the cat client
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
1161
|
+
* AE30 gets the cat client; a known ESC/POS printer the ESC/POS client, over
|
|
1162
|
+
* 18F0, the shared E781 service (as Xprinters use), or any serial service
|
|
1163
|
+
* with a writable characteristic; a known TSPL label printer offering 18F0
|
|
1164
|
+
* the TSPL client; and everything else the NIIMBOT client exactly as before.
|
|
1165
|
+
* Known names never overlap, so each printer matches one client at most.
|
|
887
1166
|
*/
|
|
888
1167
|
var BluetoothPrinter = class {
|
|
889
1168
|
lib;
|
|
@@ -895,36 +1174,31 @@ var BluetoothPrinter = class {
|
|
|
895
1174
|
if (!this.inner) throw new Error("Not connected");
|
|
896
1175
|
return this.inner;
|
|
897
1176
|
}
|
|
1177
|
+
/** The kind of printer to treat an unrecognised one as; null leaves it to NIIMBOT. */
|
|
1178
|
+
fallback = null;
|
|
1179
|
+
unrecognisedAs(typeId) {
|
|
1180
|
+
this.fallback = findPrinterType(typeId);
|
|
1181
|
+
}
|
|
898
1182
|
async connect() {
|
|
899
|
-
const device = await requestPrinter(
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const cat = catModelFor(device.name);
|
|
905
|
-
const escpos = escposModelFor(device.name);
|
|
906
|
-
const tspl = tsplModelFor(device.name);
|
|
907
|
-
if ((cat || escpos || tspl) && device.gatt) {
|
|
1183
|
+
const device = await requestPrinter(PRINTER_CATALOG.map((e) => e.model.prefix), !!this.fallback);
|
|
1184
|
+
const known = identifyPrinter(device.name);
|
|
1185
|
+
const fallback = !known && this.fallback && !looksNiimbot(await this.lib(), device.name) ? this.fallback : null;
|
|
1186
|
+
const entry = known ?? fallback;
|
|
1187
|
+
if (entry && device.gatt) {
|
|
908
1188
|
const server = await device.gatt.connect();
|
|
909
1189
|
const services = (await server.getPrimaryServices().catch(() => [])).map((s) => s.uuid);
|
|
910
|
-
|
|
911
|
-
service: CAT_SERVICE,
|
|
912
|
-
write: CAT_TX,
|
|
913
|
-
notify: CAT_RX
|
|
914
|
-
}), cat);
|
|
915
|
-
else if (escpos && services.includes("000018f0-0000-1000-8000-00805f9b34fb")) this.inner = new EscPosPrinterClient(await webTransport(device, server, {
|
|
916
|
-
service: ESCPOS_SERVICE,
|
|
917
|
-
write: ESCPOS_WRITE
|
|
918
|
-
}), escpos);
|
|
919
|
-
else if (tspl && services.includes("000018f0-0000-1000-8000-00805f9b34fb")) this.inner = new TsplPrinterClient(await webTransport(device, server, {
|
|
920
|
-
service: TSPL_SERVICE,
|
|
921
|
-
write: TSPL_WRITE,
|
|
922
|
-
notify: TSPL_NOTIFY
|
|
923
|
-
}), tspl);
|
|
1190
|
+
this.inner = await thermalClient(device, server, services, entry);
|
|
924
1191
|
if (this.inner) return this.inner.connect();
|
|
1192
|
+
if (!known) throw new Error(`${device.name ?? "This printer"} does not offer a ${PRINTER_TYPE_NAMES[entry.family]} service`);
|
|
1193
|
+
}
|
|
1194
|
+
const lib = await this.lib();
|
|
1195
|
+
this.inner = createBleClient(lib, device);
|
|
1196
|
+
try {
|
|
1197
|
+
return await this.inner.connect();
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
if (looksNiimbot(lib, device.name)) throw err;
|
|
1200
|
+
throw new Error(`${device.name ?? "This printer"} did not answer as a NIIMBOT. If it is another kind, pick its type under Advanced and connect again.`);
|
|
925
1201
|
}
|
|
926
|
-
this.inner = createBleClient(await this.lib(), device);
|
|
927
|
-
return this.inner.connect();
|
|
928
1202
|
}
|
|
929
1203
|
async disconnect() {
|
|
930
1204
|
await this.inner?.disconnect();
|
|
@@ -971,6 +1245,12 @@ var BluetoothPrinter = class {
|
|
|
971
1245
|
get rollKey() {
|
|
972
1246
|
return this.inner?.rollKey?.bind(this.inner);
|
|
973
1247
|
}
|
|
1248
|
+
get feedPaper() {
|
|
1249
|
+
return this.inner?.feedPaper?.bind(this.inner);
|
|
1250
|
+
}
|
|
1251
|
+
get feedsBack() {
|
|
1252
|
+
return this.inner?.feedsBack;
|
|
1253
|
+
}
|
|
974
1254
|
};
|
|
975
1255
|
// src/mock.ts
|
|
976
1256
|
/** A printer that only exists in memory. Used by the demo mode and the tests. */
|
|
@@ -1346,6 +1626,8 @@ var PrinterService = class {
|
|
|
1346
1626
|
listeners = /* @__PURE__ */ new Set();
|
|
1347
1627
|
manualProfileId = null;
|
|
1348
1628
|
detach = null;
|
|
1629
|
+
/** Paper moves run one after another, and a print waits for them. */
|
|
1630
|
+
feeding = Promise.resolve();
|
|
1349
1631
|
/** How the current printer is connected. */
|
|
1350
1632
|
kind = null;
|
|
1351
1633
|
constructor(createClient, encode) {
|
|
@@ -1412,6 +1694,7 @@ var PrinterService = class {
|
|
|
1412
1694
|
let client;
|
|
1413
1695
|
try {
|
|
1414
1696
|
client = await this.createClient(kind);
|
|
1697
|
+
client.unrecognisedAs?.(this.manualProfileId);
|
|
1415
1698
|
const info = await client.connect();
|
|
1416
1699
|
this.client = client;
|
|
1417
1700
|
this.set({ deviceName: info.deviceName });
|
|
@@ -1448,7 +1731,9 @@ var PrinterService = class {
|
|
|
1448
1731
|
battery: client.getPrinterInfo().batteryPercents,
|
|
1449
1732
|
detectedModel: meta?.model,
|
|
1450
1733
|
profile: this.currentProfile(),
|
|
1451
|
-
label: this.readRfid()
|
|
1734
|
+
label: this.readRfid(),
|
|
1735
|
+
...client.feedPaper ? { canFeed: true } : {},
|
|
1736
|
+
...client.feedPaper && client.feedsBack ? { canRetract: true } : {}
|
|
1452
1737
|
});
|
|
1453
1738
|
client.startHeartbeat();
|
|
1454
1739
|
}
|
|
@@ -1485,12 +1770,31 @@ var PrinterService = class {
|
|
|
1485
1770
|
total: copies
|
|
1486
1771
|
} });
|
|
1487
1772
|
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Moves the paper by `mm`, out of the printer when positive and back in when
|
|
1775
|
+
* negative, to line it up on printers that cannot sense it.
|
|
1776
|
+
*/
|
|
1777
|
+
feed(mm) {
|
|
1778
|
+
const client = this.client;
|
|
1779
|
+
const profile = this.status.profile;
|
|
1780
|
+
if (!client || !profile) return Promise.reject(/* @__PURE__ */ new Error("Connect a printer first"));
|
|
1781
|
+
if (!client.feedPaper) return Promise.reject(/* @__PURE__ */ new Error("This printer finds the paper itself"));
|
|
1782
|
+
if (mm < 0 && !client.feedsBack) return Promise.reject(/* @__PURE__ */ new Error("This printer cannot move the paper back"));
|
|
1783
|
+
if (this.status.state === "printing") return Promise.reject(/* @__PURE__ */ new Error("Wait for the print to finish"));
|
|
1784
|
+
const dots = Math.round(mmToPx(mm, profile.dpi));
|
|
1785
|
+
const run = this.feeding.then(() => dots ? client.feedPaper(dots) : void 0);
|
|
1786
|
+
this.feeding = run.catch(() => {});
|
|
1787
|
+
return run.catch((err) => {
|
|
1788
|
+
throw new Error(friendlyError(err));
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1488
1791
|
async print(source, opts) {
|
|
1489
1792
|
const client = this.client;
|
|
1490
1793
|
const profile = this.status.profile;
|
|
1491
1794
|
if (!client) throw new Error("Connect a printer first");
|
|
1492
1795
|
if (!profile) throw new Error("Choose your printer type");
|
|
1493
1796
|
if (this.status.state === "printing") throw new Error("Already printing");
|
|
1797
|
+
await this.feeding;
|
|
1494
1798
|
const pages = "page" in source ? source : {
|
|
1495
1799
|
count: 1,
|
|
1496
1800
|
page: async () => source
|
|
@@ -1502,6 +1806,7 @@ var PrinterService = class {
|
|
|
1502
1806
|
totalPages: total,
|
|
1503
1807
|
density,
|
|
1504
1808
|
labelType: opts.labelType ?? 1,
|
|
1809
|
+
...opts.gapMm !== void 0 && client.feedPaper ? { gapDots: Math.round(mmToPx(Math.max(0, opts.gapMm), profile.dpi)) } : {},
|
|
1505
1810
|
pageColor: 0,
|
|
1506
1811
|
statusPollIntervalMs: 100,
|
|
1507
1812
|
statusTimeoutMs: 8e3
|
|
@@ -1598,6 +1903,6 @@ async function createClient(kind) {
|
|
|
1598
1903
|
function createPrinter(options = {}) {
|
|
1599
1904
|
return new PrinterService(options.createClient ?? createClient, async (canvas, direction) => (await niimbot()).ImageEncoder.encodeCanvas(canvas, 0, direction));
|
|
1600
1905
|
}
|
|
1601
|
-
export { BluetoothPrinter, CAT_MODELS, CAT_UNSUPPORTED, CatPrinterClient, DEMO_META, DEMO_NAME, ESCPOS_MODELS, EscPosPrinterClient, MANUAL_PROFILES, MockClient, PAPER_TYPES, PrinterService, THERMAL_DEFAULT_SIZE, THERMAL_HEAD_DOTS, THERMAL_HEAD_MM, THERMAL_SIZES, TSPL_MODELS, TSPL_SIZES, ThermalPrinterClient, TsplPrinterClient, printJob as catJob, catModelFor, correctTask, createClient, createPrinter, describeBleError, detectPlatform, effectivePaper, escposJob, escposModelFor, findManualProfile, fitToPrinthead, friendlyError, inkOutsideHead, labelTypeOf, modelFor, paperFromLabelType, paperName, printheadCrop, printheadMm, resolveProfile, shiftCanvas, supportsBluetooth, supportsSerial, tsplJob, tsplModelFor, unsupportedReason, webTransport };
|
|
1906
|
+
export { BluetoothPrinter, CAT_MODELS, CAT_UNSUPPORTED, CatPrinterClient, DEMO_META, DEMO_NAME, ESCPOS_MODELS, EscPosPrinterClient, MANUAL_PROFILES, MockClient, PAPER_TYPES, PRINTER_CATALOG, PRINTER_TYPES, PrinterService, THERMAL_DEFAULT_SIZE, THERMAL_HEAD_DOTS, THERMAL_HEAD_MM, THERMAL_SIZES, TSPL_MODELS, TSPL_SIZES, ThermalPrinterClient, TsplPrinterClient, catFeed, printJob as catJob, catModelFor, correctTask, createClient, createPrinter, describeBleError, detectPlatform, effectivePaper, escposFeed, escposJob, escposModelFor, findManualProfile, findPrinterType, fitToPrinthead, friendlyError, identifyPrinter, inkOutsideHead, labelTypeOf, modelFor, paperFromLabelType, paperName, printheadCrop, printheadMm, resolveProfile, shiftCanvas, supportsBluetooth, supportsSerial, tsplJob, tsplModelFor, unsupportedReason, webTransport };
|
|
1602
1907
|
|
|
1603
1908
|
//# sourceMappingURL=libniimai.js.map
|