libniimai 0.3.0 → 0.5.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 +5 -2
- package/dist/client.d.ts +43 -3
- package/dist/index.d.ts +4 -4
- package/dist/libniimai.js +130 -9
- package/dist/libniimai.js.map +1 -1
- package/dist/service.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,7 @@ Returns a `PrinterService`, which owns one printer connection.
|
|
|
107
107
|
| `labelType` | Paper type, from `PAPER_TYPES`: 1 labels with gaps, 2 black mark, 3 continuous. |
|
|
108
108
|
| `offset` | Nudges the print, in mm: `{ x, y }`. |
|
|
109
109
|
| `gapMm` | Stickers with gaps on printers that cannot sense them (`status.canFeed`): each label is followed by exactly this much feed, in place of the tear-off feed. |
|
|
110
|
+
| `rotate` | Turns each page clockwise by 0, 90, 180 or 270 degrees before sending, for landscape labels. `offset` still moves the print relative to the paper. |
|
|
110
111
|
|
|
111
112
|
**`PrinterStatus`**
|
|
112
113
|
|
|
@@ -159,11 +160,13 @@ Every piece is exported, for building your own flow:
|
|
|
159
160
|
- `TsplPrinterClient`, `EscPosPrinterClient` and `CatPrinterClient` take any `ThermalTransport`, so they also work over other links.
|
|
160
161
|
- `tsplJob`, `escposJob` and `catJob` build the bytes for one page; `escposFeed` and `catFeed` the bytes that move the paper.
|
|
161
162
|
- `identifyPrinter(name)` matches a Bluetooth name to a known model across every family, from `PRINTER_CATALOG`; `tsplModelFor`, `escposModelFor` and `catModelFor` look in one family.
|
|
162
|
-
- `
|
|
163
|
+
- `supportIssue(kind)` says why a connection type cannot be used in the current browser, as `{ reason, topic }`, or null when it can. `topic` names what fixes it: `linux` (a Chrome flag), `brave` (Brave flags), `ios`, `https` or `browsers`. `unsupportedReason(kind)` returns the reason alone.
|
|
164
|
+
- `detectBrowser()` returns the system, the browser and whether it is built on Chromium.
|
|
165
|
+
- `connectErrorTopic(err)` maps a connect error from a browser that has Bluetooth but cannot use it (adapter off, API turned off) to the same topics.
|
|
163
166
|
|
|
164
167
|
## Browsers
|
|
165
168
|
|
|
166
|
-
Needs Web Bluetooth or Web Serial: Chrome or Edge on desktop, or Chrome on Android. On iPhone and iPad, the Bluefy browser adds Web Bluetooth.
|
|
169
|
+
Needs Web Bluetooth or Web Serial: Chrome or Edge on desktop, or Chrome on Android. On iPhone and iPad, the Bluefy browser adds Web Bluetooth. On Linux, Chrome needs `chrome://flags/#enable-web-bluetooth` turned on; Brave keeps both APIs behind `brave://flags`.
|
|
167
170
|
|
|
168
171
|
## Feedback
|
|
169
172
|
|
package/dist/client.d.ts
CHANGED
|
@@ -72,9 +72,49 @@ export declare const supportsBluetooth: () => boolean;
|
|
|
72
72
|
export declare const supportsSerial: () => boolean;
|
|
73
73
|
export type Platform = 'ios' | 'android' | 'desktop';
|
|
74
74
|
export declare function detectPlatform(ua?: string, touchPoints?: number): Platform;
|
|
75
|
+
export type OperatingSystem = 'ios' | 'android' | 'chromeos' | 'linux' | 'mac' | 'windows' | 'other';
|
|
76
|
+
export type BrowserName = 'chrome' | 'edge' | 'opera' | 'brave' | 'samsung' | 'bluefy' | 'firefox' | 'safari' | 'other';
|
|
77
|
+
export interface BrowserInfo {
|
|
78
|
+
os: OperatingSystem;
|
|
79
|
+
browser: BrowserName;
|
|
80
|
+
/** Built on Chromium, so Web Bluetooth and Web Serial can be there. */
|
|
81
|
+
chromium: boolean;
|
|
82
|
+
/** Served over https or from localhost; browsers hide both APIs elsewhere. */
|
|
83
|
+
secure: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** What the browser tells about itself. Defaults to the running browser. */
|
|
86
|
+
export interface BrowserEnv {
|
|
87
|
+
ua: string;
|
|
88
|
+
touchPoints: number;
|
|
89
|
+
/** Brave hides itself in the user agent; it has `navigator.brave` instead. */
|
|
90
|
+
brave: boolean;
|
|
91
|
+
secure: boolean;
|
|
92
|
+
}
|
|
93
|
+
export declare function detectBrowser(env?: Partial<BrowserEnv>): BrowserInfo;
|
|
75
94
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
95
|
+
* A Help topic that shows how to get a connection type working:
|
|
96
|
+
* - `linux`: Chrome and other Chromium browsers on Linux need a flag for Bluetooth.
|
|
97
|
+
* - `brave`: Brave keeps Bluetooth and USB behind flags.
|
|
98
|
+
* - `ios`: iPhone and iPad browsers, and Bluefy.
|
|
99
|
+
* - `https`: the page has to be served over https.
|
|
100
|
+
* - `browsers`: which browsers work.
|
|
79
101
|
*/
|
|
102
|
+
export type SetupTopic = 'linux' | 'brave' | 'ios' | 'https' | 'browsers';
|
|
103
|
+
export interface SupportIssue {
|
|
104
|
+
/** In a few words, what is missing or what to do. */
|
|
105
|
+
reason: string;
|
|
106
|
+
topic: SetupTopic;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Why a connection type cannot be used in this browser, and where to read how
|
|
110
|
+
* to fix it, or null when it can be used.
|
|
111
|
+
*/
|
|
112
|
+
export declare function supportIssue(kind: ConnectionKind, env?: Partial<BrowserEnv>): SupportIssue | null;
|
|
113
|
+
/** supportIssue's reason alone. */
|
|
80
114
|
export declare function unsupportedReason(kind: ConnectionKind, platform?: Platform): string | null;
|
|
115
|
+
/**
|
|
116
|
+
* Errors browsers raise when Bluetooth is there but cannot be used: the
|
|
117
|
+
* adapter is off or missing, or the browser turned the API off. The Help topic
|
|
118
|
+
* to point to, or null.
|
|
119
|
+
*/
|
|
120
|
+
export declare function connectErrorTopic(err: unknown): SetupTopic | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,10 +17,10 @@ export interface CreatePrinterOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/** A ready printer service: connect, watch its status, print canvases. */
|
|
19
19
|
export declare function createPrinter(options?: CreatePrinterOptions): PrinterService;
|
|
20
|
-
export { PrinterService, DEMO_NAME, friendlyError, fitToPrinthead, shiftCanvas } from './service.js';
|
|
21
|
-
export type { ClientFactory, PageSource, PrinterState, PrinterStatus, PrintOffset, PrintOptions } from './service.js';
|
|
22
|
-
export { detectPlatform, supportsBluetooth, supportsSerial, unsupportedReason } from './client.js';
|
|
23
|
-
export type { ClientLike, ConnectionKind, Platform, PrintTaskLike } from './client.js';
|
|
20
|
+
export { PrinterService, DEMO_NAME, friendlyError, fitToPrinthead, rotateCanvas, shiftCanvas } from './service.js';
|
|
21
|
+
export type { ClientFactory, PageSource, PrinterState, PrinterStatus, PrintOffset, PrintOptions, Rotation } from './service.js';
|
|
22
|
+
export { connectErrorTopic, detectBrowser, detectPlatform, supportIssue, supportsBluetooth, supportsSerial, unsupportedReason } from './client.js';
|
|
23
|
+
export type { BrowserEnv, BrowserInfo, BrowserName, ClientLike, ConnectionKind, OperatingSystem, Platform, PrintTaskLike, SetupTopic, SupportIssue } from './client.js';
|
|
24
24
|
export { correctTask, findManualProfile, inkOutsideHead, MANUAL_PROFILES, printheadCrop, printheadMm, resolveProfile } from './profiles.js';
|
|
25
25
|
export type { LabelSize, PrintDirection, PrinterProfile } from './profiles.js';
|
|
26
26
|
export { effectivePaper, labelTypeOf, PAPER_TYPES, paperFromLabelType, paperName } from './paper.js';
|
package/dist/libniimai.js
CHANGED
|
@@ -1574,6 +1574,22 @@ function shiftCanvas(canvas, dx, dy) {
|
|
|
1574
1574
|
ctx.drawImage(canvas, dx, dy);
|
|
1575
1575
|
return out;
|
|
1576
1576
|
}
|
|
1577
|
+
/** The page turned clockwise by `degrees`, a multiple of 90. Returns the canvas itself for no turn. */
|
|
1578
|
+
function rotateCanvas(canvas, degrees) {
|
|
1579
|
+
const turn = (Math.round(degrees / 90) % 4 + 4) % 4;
|
|
1580
|
+
if (!turn) return canvas;
|
|
1581
|
+
const out = document.createElement("canvas");
|
|
1582
|
+
out.width = turn % 2 ? canvas.height : canvas.width;
|
|
1583
|
+
out.height = turn % 2 ? canvas.width : canvas.height;
|
|
1584
|
+
const ctx = out.getContext("2d");
|
|
1585
|
+
if (!ctx) return canvas;
|
|
1586
|
+
ctx.fillStyle = "#fff";
|
|
1587
|
+
ctx.fillRect(0, 0, out.width, out.height);
|
|
1588
|
+
ctx.translate(out.width / 2, out.height / 2);
|
|
1589
|
+
ctx.rotate(turn * Math.PI / 2);
|
|
1590
|
+
ctx.drawImage(canvas, -canvas.width / 2, -canvas.height / 2);
|
|
1591
|
+
return out;
|
|
1592
|
+
}
|
|
1577
1593
|
/** Crops the page to what the printhead covers. Returns the canvas itself when it already fits. */
|
|
1578
1594
|
function fitToPrinthead(canvas, profile) {
|
|
1579
1595
|
const crop = printheadCrop(canvas.width, canvas.height, profile);
|
|
@@ -1825,7 +1841,7 @@ var PrinterService = class {
|
|
|
1825
1841
|
y: 0
|
|
1826
1842
|
};
|
|
1827
1843
|
for (let i = 0; i < pages.count; i++) {
|
|
1828
|
-
const canvas = await pages.page(i);
|
|
1844
|
+
const canvas = rotateCanvas(await pages.page(i), opts.rotate ?? 0);
|
|
1829
1845
|
const page = fitToPrinthead(shiftCanvas(canvas, mmToPx(offset.x, profile.dpi), mmToPx(offset.y, profile.dpi)), profile);
|
|
1830
1846
|
const image = client.encodePage ? await client.encodePage(page) : await this.encode(page, profile.printDirection);
|
|
1831
1847
|
if (i === 0) {
|
|
@@ -1835,6 +1851,7 @@ var PrinterService = class {
|
|
|
1835
1851
|
direction: profile.printDirection,
|
|
1836
1852
|
printhead: profile.printheadPixels,
|
|
1837
1853
|
page: `${canvas.width}x${canvas.height}`,
|
|
1854
|
+
rotate: opts.rotate ?? 0,
|
|
1838
1855
|
sent: `${page.width}x${page.height}`,
|
|
1839
1856
|
density,
|
|
1840
1857
|
copies,
|
|
@@ -1871,17 +1888,121 @@ function detectPlatform(ua = typeof navigator === "undefined" ? "" : navigator.u
|
|
|
1871
1888
|
if (/Android/.test(ua)) return "android";
|
|
1872
1889
|
return "desktop";
|
|
1873
1890
|
}
|
|
1891
|
+
function currentEnv() {
|
|
1892
|
+
if (typeof navigator === "undefined") return {
|
|
1893
|
+
ua: "",
|
|
1894
|
+
touchPoints: 0,
|
|
1895
|
+
brave: false,
|
|
1896
|
+
secure: true
|
|
1897
|
+
};
|
|
1898
|
+
return {
|
|
1899
|
+
ua: navigator.userAgent,
|
|
1900
|
+
touchPoints: navigator.maxTouchPoints,
|
|
1901
|
+
brave: "brave" in navigator,
|
|
1902
|
+
secure: typeof window === "undefined" || window.isSecureContext !== false
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
function detectBrowser(env = {}) {
|
|
1906
|
+
const { ua, touchPoints, brave, secure } = {
|
|
1907
|
+
...currentEnv(),
|
|
1908
|
+
...env
|
|
1909
|
+
};
|
|
1910
|
+
const platform = detectPlatform(ua, touchPoints);
|
|
1911
|
+
const os = platform !== "desktop" ? platform : /CrOS/.test(ua) ? "chromeos" : /Windows/.test(ua) ? "windows" : /Macintosh|Mac OS X/.test(ua) ? "mac" : /Linux|X11|BSD/.test(ua) ? "linux" : "other";
|
|
1912
|
+
return {
|
|
1913
|
+
os,
|
|
1914
|
+
browser: brave ? "brave" : /Bluefy/i.test(ua) ? "bluefy" : /Firefox|FxiOS/.test(ua) ? "firefox" : /Edg(e|A|iOS)?\//.test(ua) ? "edge" : /OPR\/|Opera/.test(ua) ? "opera" : /SamsungBrowser/.test(ua) ? "samsung" : /Chrome\/|CriOS|Chromium/.test(ua) ? "chrome" : /Safari\//.test(ua) ? "safari" : "other",
|
|
1915
|
+
chromium: os !== "ios" && /Chrome\/|Chromium/.test(ua),
|
|
1916
|
+
secure
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
var BROWSER_NAMES = {
|
|
1920
|
+
firefox: "Firefox",
|
|
1921
|
+
safari: "Safari",
|
|
1922
|
+
samsung: "Samsung Internet",
|
|
1923
|
+
opera: "Opera"
|
|
1924
|
+
};
|
|
1874
1925
|
/**
|
|
1875
|
-
* Why a connection type cannot be used in this browser,
|
|
1876
|
-
*
|
|
1877
|
-
* Bluetooth nor Web Serial; Bluefy adds Web Bluetooth there.
|
|
1926
|
+
* Why a connection type cannot be used in this browser, and where to read how
|
|
1927
|
+
* to fix it, or null when it can be used.
|
|
1878
1928
|
*/
|
|
1879
|
-
function
|
|
1929
|
+
function supportIssue(kind, env = {}) {
|
|
1880
1930
|
if (kind === "demo") return null;
|
|
1881
1931
|
if (kind === "bluetooth" ? supportsBluetooth() : supportsSerial()) return null;
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1932
|
+
const { os, browser, chromium, secure } = detectBrowser(env);
|
|
1933
|
+
const bluetooth = kind === "bluetooth";
|
|
1934
|
+
if (os === "ios") return {
|
|
1935
|
+
reason: bluetooth ? "On iPhone and iPad, use the Bluefy browser" : "Not available on iPhone and iPad",
|
|
1936
|
+
topic: "ios"
|
|
1937
|
+
};
|
|
1938
|
+
if (!secure && chromium) return {
|
|
1939
|
+
reason: "Needs an https page",
|
|
1940
|
+
topic: "https"
|
|
1941
|
+
};
|
|
1942
|
+
if (browser === "brave") return {
|
|
1943
|
+
reason: "Turn it on in Brave flags",
|
|
1944
|
+
topic: "brave"
|
|
1945
|
+
};
|
|
1946
|
+
if (os === "android") {
|
|
1947
|
+
if (!bluetooth) return {
|
|
1948
|
+
reason: "Needs Chrome or Edge on a computer",
|
|
1949
|
+
topic: "browsers"
|
|
1950
|
+
};
|
|
1951
|
+
return {
|
|
1952
|
+
reason: browser === "chrome" ? "Update Chrome" : "Open niimai in Chrome",
|
|
1953
|
+
topic: "browsers"
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
if (bluetooth && os === "linux" && chromium) return {
|
|
1957
|
+
reason: "Needs a Chrome flag on Linux",
|
|
1958
|
+
topic: "linux"
|
|
1959
|
+
};
|
|
1960
|
+
const name = BROWSER_NAMES[browser];
|
|
1961
|
+
if (name) return {
|
|
1962
|
+
reason: `${name} has no ${bluetooth ? "Bluetooth" : "USB"} access; use Chrome or Edge`,
|
|
1963
|
+
topic: "browsers"
|
|
1964
|
+
};
|
|
1965
|
+
if (chromium) return {
|
|
1966
|
+
reason: "Turned off in this browser",
|
|
1967
|
+
topic: "browsers"
|
|
1968
|
+
};
|
|
1969
|
+
return {
|
|
1970
|
+
reason: "Needs Chrome or Edge",
|
|
1971
|
+
topic: "browsers"
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
/** Stand-ins for the platform-only signature below. */
|
|
1975
|
+
var PLATFORM_ENVS = {
|
|
1976
|
+
ios: {
|
|
1977
|
+
ua: "iPhone",
|
|
1978
|
+
touchPoints: 5,
|
|
1979
|
+
brave: false
|
|
1980
|
+
},
|
|
1981
|
+
android: {
|
|
1982
|
+
ua: "Android Firefox",
|
|
1983
|
+
touchPoints: 5,
|
|
1984
|
+
brave: false
|
|
1985
|
+
},
|
|
1986
|
+
desktop: {
|
|
1987
|
+
ua: "",
|
|
1988
|
+
touchPoints: 0,
|
|
1989
|
+
brave: false
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
/** supportIssue's reason alone. */
|
|
1993
|
+
function unsupportedReason(kind, platform) {
|
|
1994
|
+
return supportIssue(kind, platform ? PLATFORM_ENVS[platform] : {})?.reason ?? null;
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Errors browsers raise when Bluetooth is there but cannot be used: the
|
|
1998
|
+
* adapter is off or missing, or the browser turned the API off. The Help topic
|
|
1999
|
+
* to point to, or null.
|
|
2000
|
+
*/
|
|
2001
|
+
function connectErrorTopic(err) {
|
|
2002
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2003
|
+
if (/globally disabled/i.test(message)) return "brave";
|
|
2004
|
+
if (/adapter not available|Bluetooth adapter|Bluetooth is (turned off|disabled|not available)/i.test(message)) return detectBrowser().os === "linux" ? "linux" : "browsers";
|
|
2005
|
+
return null;
|
|
1885
2006
|
}
|
|
1886
2007
|
// src/index.ts
|
|
1887
2008
|
/**
|
|
@@ -1903,6 +2024,6 @@ async function createClient(kind) {
|
|
|
1903
2024
|
function createPrinter(options = {}) {
|
|
1904
2025
|
return new PrinterService(options.createClient ?? createClient, async (canvas, direction) => (await niimbot()).ImageEncoder.encodeCanvas(canvas, 0, direction));
|
|
1905
2026
|
}
|
|
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 };
|
|
2027
|
+
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, connectErrorTopic, correctTask, createClient, createPrinter, describeBleError, detectBrowser, detectPlatform, effectivePaper, escposFeed, escposJob, escposModelFor, findManualProfile, findPrinterType, fitToPrinthead, friendlyError, identifyPrinter, inkOutsideHead, labelTypeOf, modelFor, paperFromLabelType, paperName, printheadCrop, printheadMm, resolveProfile, rotateCanvas, shiftCanvas, supportIssue, supportsBluetooth, supportsSerial, tsplJob, tsplModelFor, unsupportedReason, webTransport };
|
|
1907
2028
|
|
|
1908
2029
|
//# sourceMappingURL=libniimai.js.map
|
package/dist/libniimai.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"libniimai.js","names":["sleep","encodeRows","encodeRows"],"sources":["../src/ble.ts","../src/thermal.ts","../src/cat.ts","../src/escpos.ts","../src/tspl.ts","../src/catalog.ts","../src/bluetooth.ts","../src/mock.ts","../src/paper.ts","../src/profiles.ts","../src/service.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["import type { ConnectionInfo, NiimbotAbstractClient } from '@mmote/niimbluelib';\n\ntype Lib = typeof import('@mmote/niimbluelib');\n\n/** The standard NIIMBOT service, followed by serial-over-BLE services common on budget printers. */\nexport const CANDIDATE_SERVICES = [\n 'e7810a71-73ae-499d-8c15-faa9aef0c3f2', // NIIMBOT\n '0000e0ff-3c17-d293-8e48-14fe2e4da212', // NIIMBOT B21 C2B (model id 771), channel ffe1\n '0000ae30-0000-1000-8000-00805f9b34fb', // AE30 printer UART\n '0000ff00-0000-1000-8000-00805f9b34fb',\n '0000ffe0-0000-1000-8000-00805f9b34fb', // HM-10 style\n '0000fff0-0000-1000-8000-00805f9b34fb',\n '000018f0-0000-1000-8000-00805f9b34fb', // generic printer\n '49535343-fe7d-4ae5-8fa9-9fafd205e455', // Microchip transparent UART\n '6e400001-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART\n];\n\n/** Name prefixes of NIIMBOT printers; the chooser lists only these and known cat printers. */\nexport const NAME_PREFIXES = ['A', 'B', 'C', 'D', 'E', 'F', 'H', 'J', 'K', 'M', 'N', 'P', 'S', 'T', 'Z'];\n\n/** Opens the browser's printer chooser. `all` lists every device, for printers whose names niimai does not know. */\nexport function requestPrinter(extraPrefixes: string[] = [], all = false): Promise<BluetoothDevice> {\n if (all) return navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: CANDIDATE_SERVICES });\n return navigator.bluetooth.requestDevice({\n filters: [...[...NAME_PREFIXES, ...extraPrefixes].map((namePrefix) => ({ namePrefix })), { services: [CANDIDATE_SERVICES[0]] }],\n optionalServices: CANDIDATE_SERVICES,\n });\n}\n\ninterface CharLike {\n uuid: string;\n properties: { notify: boolean; indicate?: boolean; write: boolean; writeWithoutResponse: boolean };\n}\n\nexport interface ServiceLike<C extends CharLike = CharLike> {\n uuid: string;\n getCharacteristics(): Promise<C[]>;\n}\n\nexport interface Channel<C extends CharLike = CharLike> {\n write: C;\n notify: C;\n}\n\n/**\n * Picks the characteristics to talk through. A single characteristic that both\n * notifies and accepts writes wins (the NIIMBOT layout); otherwise a write and\n * a notify characteristic from the same service are paired.\n */\nexport async function pickChannel<C extends CharLike>(services: ServiceLike<C>[]): Promise<Channel<C> | null> {\n const all: { service: string; chars: C[] }[] = [];\n for (const service of services) all.push({ service: service.uuid, chars: await service.getCharacteristics() });\n\n const canWrite = (c: C) => c.properties.writeWithoutResponse || c.properties.write;\n const canNotify = (c: C) => c.properties.notify || !!c.properties.indicate;\n\n for (const { chars } of all) {\n const both = chars.find((c) => c.properties.writeWithoutResponse && c.properties.notify);\n if (both) return { write: both, notify: both };\n }\n for (const { chars } of all) {\n const write = chars.find(canWrite);\n const notify = chars.find(canNotify);\n if (write && notify) return { write, notify };\n }\n return null;\n}\n\n/** Turns Chrome's terse GATT errors into something a person can act on. */\nexport function describeBleError(err: unknown, found: string[] = []): Error {\n const msg = err instanceof Error ? err.message : String(err);\n if (/no services found/i.test(msg) || (found.length === 0 && /characteristic/i.test(msg))) {\n return new Error('This printer uses an unknown Bluetooth service. See Help → Printer not recognised.');\n }\n if (/characteristic/i.test(msg)) return new Error(`No usable channel. Services: ${found.join(', ')}`);\n return err instanceof Error ? err : new Error(msg);\n}\n\n/**\n * A Web Bluetooth client that accepts more service layouts than niimbluelib's\n * own, which only knows the standard NIIMBOT service. The protocol handling\n * (packets, negotiation, heartbeat) is inherited unchanged.\n */\nexport function createBleClient(lib: Lib, chosen?: BluetoothDevice): NiimbotAbstractClient {\n class FlexibleBleClient extends lib.NiimbotAbstractClient {\n private server?: BluetoothRemoteGATTServer;\n private channel?: Channel<BluetoothRemoteGATTCharacteristic>;\n\n async connect(): Promise<ConnectionInfo> {\n await this.disconnect();\n const device = chosen ?? (await requestPrinter());\n if (!device.gatt) throw new Error('Device has no GATT server');\n\n const onDisconnect = () => {\n this.server = undefined;\n this.channel = undefined;\n this.emit('disconnect', new lib.DisconnectEvent());\n device.removeEventListener('gattserverdisconnected', onDisconnect);\n };\n device.addEventListener('gattserverdisconnected', onDisconnect);\n\n const server = await device.gatt.connect();\n let found: string[] = [];\n try {\n const services = await server.getPrimaryServices();\n found = services.map((s) => s.uuid);\n console.info('[niimai] services', found);\n const channel = await pickChannel(services);\n if (!channel) throw new Error('Suitable characteristic not found');\n console.info('[niimai] channel', channel.write.uuid, channel.notify.uuid);\n channel.notify.addEventListener('characteristicvaluechanged', (e) => {\n const value = (e.target as BluetoothRemoteGATTCharacteristic).value;\n if (value) this.processRawPacket(value);\n });\n await channel.notify.startNotifications();\n this.server = server;\n this.channel = channel;\n } catch (err) {\n server.disconnect();\n throw describeBleError(err, found);\n }\n\n await this.negotiateAndGetPrinterInfo(() => this.disconnect());\n const result: ConnectionInfo = {\n deviceName: device.name,\n result: this.printerInfo.connectResult ?? lib.ConnectResult.FirmwareErrors,\n };\n this.emit('connect', new lib.ConnectEvent(result));\n return result;\n }\n\n isConnected(): boolean {\n return !!this.server && !!this.channel;\n }\n\n async disconnect(): Promise<void> {\n this.stopHeartbeat();\n this.server?.disconnect();\n this.server = undefined;\n this.channel = undefined;\n }\n\n async sendRaw(data: Uint8Array, force?: boolean): Promise<void> {\n const send = async () => {\n const c = this.channel?.write;\n if (!c) throw new Error('Channel is closed');\n await lib.Utils.sleep(this.packetIntervalMs);\n const buf = data.slice().buffer;\n if (c.properties.writeWithoutResponse) await c.writeValueWithoutResponse(buf);\n else await c.writeValueWithResponse(buf);\n this.emit('rawpacketsent', new lib.RawPacketSentEvent(data));\n };\n if (force) await send();\n else await this.mutex.runExclusive(send);\n }\n\n getType() {\n return 'bluetooth' as const;\n }\n }\n return new FlexibleBleClient();\n}\n","/**\n * Shared ground for the extra Bluetooth thermal printers: cat printers and\n * ESC/POS mini printers (58 mm continuous paper, 384-dot head) and TSPL\n * label printers (labels with gaps, up to 80 mm). niimai's focus is NIIMBOT;\n * these clients are kept apart so NIIMBOT printing never goes through them.\n */\nimport type { PrinterModelMeta, PrintTaskName } from '@mmote/niimbluelib';\nimport type { LabelSize } from './profiles';\nimport type { ClientLike, PrintTaskLike } from './client';\n\n/** Dots across the head: 48 mm at 203 dpi. */\nexport const THERMAL_HEAD_DOTS = 384;\n/** The print width in whole millimetres, the widest a design on continuous paper can be. */\nexport const THERMAL_HEAD_MM = 48;\n\nexport type Confidence = 'tested' | 'high' | 'medium';\n\nexport interface ThermalModel {\n /** Bluetooth name prefix. */\n prefix: string;\n confidence: Confidence;\n note?: string;\n /** The prefix names a brand or a generic Bluetooth module, not a model; the printer's own name is shown. */\n generic?: boolean;\n /** Maker and model, where the Bluetooth name does not say. */\n brand?: string;\n /** Dots across the head, where it differs from the family's usual one: 576 on 80 mm receipt printers. */\n headDots?: number;\n}\n\n/** The name a printer shows: its model, or its own Bluetooth name when the model entry is only a brand or a stock name. */\nexport const shownName = (model: ThermalModel | null, transport: { name?: string }, fallback: string): string =>\n (model && !model.generic ? model.prefix : transport.name) || model?.prefix || fallback;\n\n/** The known model a Bluetooth name belongs to, longest prefix first. */\nexport function modelFor<M extends ThermalModel>(models: M[], name: string | undefined): M | null {\n if (!name) return null;\n return [...models].sort((a, b) => b.prefix.length - a.prefix.length).find((m) => name.startsWith(m.prefix)) ?? null;\n}\n\n/** Sizes that suit 58 mm continuous paper: the full 48 mm print width at common lengths. */\nexport const THERMAL_SIZES: LabelSize[] = [15, 20, 30, 48, 80].map((heightMm) => ({ widthMm: 48, heightMm }));\nexport const THERMAL_DEFAULT_SIZE: LabelSize = { widthMm: 48, heightMm: 30 };\n\n/**\n * Packs a 1-bit page (RGBA, dark = black) into rows as wide as the head, centred across\n * the head. Cat printers want the leftmost dot in the lowest bit, ESC/POS in\n * the highest.\n */\nexport function packRows(data: ArrayLike<number>, width: number, height: number, order: 'lsb' | 'msb', head = THERMAL_HEAD_DOTS): Uint8Array[] {\n const offset = Math.max(0, Math.floor((head - width) / 2));\n const rows: Uint8Array[] = [];\n for (let y = 0; y < height; y++) {\n const row = new Uint8Array(head / 8);\n for (let x = 0; x < width && x + offset < head; x++) {\n if (data[(y * width + x) * 4] < 128) {\n const dot = x + offset;\n row[dot >> 3] |= order === 'lsb' ? 1 << (dot & 7) : 0x80 >> (dot & 7);\n }\n }\n rows.push(row);\n }\n return rows;\n}\n\n/** The Bluetooth link, kept narrow so tests can stand in for it. */\nexport interface ThermalTransport {\n name?: string;\n write(chunk: Uint8Array): Promise<void>;\n onNotify(fn: (data: Uint8Array) => void): void;\n onDisconnect(fn: () => void): void;\n disconnect(): Promise<void>;\n}\n\nexport interface Pace {\n /** Pause after each write. */\n chunkMs: number;\n /** How often to look again while the printer asks to wait. */\n pauseMs: number;\n}\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\n/**\n * A thermal printer behind the same interface as a NIIMBOT client, so the\n * printer service drives both. It reports itself through model metadata with\n * kind \"thermal\"; it has no roll tags, battery or heartbeat to offer.\n * Subclasses supply the row encoding and the bytes for a job.\n */\nexport abstract class ThermalPrinterClient implements ClientLike {\n private listeners = new Map<string, Set<(e: unknown) => void>>();\n private connected = false;\n /** Set while the printer asks to wait (cat printers' flow control). */\n protected paused = false;\n /** Bytes per write. Starts large and halves if the link refuses it. */\n private chunk: number;\n\n protected abstract readonly densityLevels: number;\n /** The model name shown for this printer. */\n protected abstract readonly modelName: string;\n /** Dots across the head. */\n protected readonly headDots: number = THERMAL_HEAD_DOTS;\n /** \"left\": the label runs sideways through the head, as on NIIMBOT D-series printers. */\n protected readonly printDirection: 'top' | 'left' = 'top';\n /** Continuous-paper printers report \"thermal\"; label printers report nothing and get the roll UI. */\n protected readonly kind: 'thermal' | undefined = 'thermal';\n /** Label sizes common for this printer, offered in place of NIIMBOT roll sizes. */\n protected readonly labelSizes: LabelSize[] | undefined = undefined;\n protected abstract encodeRows(data: ArrayLike<number>, width: number, height: number): Uint8Array[];\n /** `gapDots`: labels with gaps on a printer that cannot sense them; each label is followed by exactly this much feed. */\n protected abstract job(rows: Uint8Array[], density: number, copies: number, labelType: number, gapDots?: number): Uint8Array;\n /** Called after each page is sent; printers that confirm a job wait here. */\n protected async afterPage(): Promise<void> {}\n\n constructor(\n protected transport: ThermalTransport,\n protected pace: Pace,\n firstChunk: number,\n ) {\n this.chunk = firstChunk;\n transport.onDisconnect(() => {\n this.connected = false;\n this.emit('disconnect', {});\n });\n }\n\n async connect() {\n this.connected = true;\n return { deviceName: this.transport.name };\n }\n\n async disconnect() {\n this.connected = false;\n await this.transport.disconnect();\n }\n\n isConnected() {\n return this.connected;\n }\n\n on(event: string, fn: (e: unknown) => void) {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\n this.listeners.get(event)!.add(fn);\n }\n\n off(event: string, fn: (e: unknown) => void) {\n this.listeners.get(event)?.delete(fn);\n }\n\n private emit(event: string, e: unknown) {\n this.listeners.get(event)?.forEach((fn) => fn(e));\n }\n\n getPrinterInfo() {\n return {};\n }\n\n getModelMetadata(): PrinterModelMeta {\n return {\n model: this.modelName,\n ...(this.kind ? { kind: this.kind } : {}),\n ...(this.labelSizes ? { labelSizes: this.labelSizes } : {}),\n dpi: 203,\n printheadPixels: this.headDots,\n printDirection: this.printDirection,\n densityMin: 1,\n densityMax: this.densityLevels,\n densityDefault: Math.ceil(this.densityLevels / 2),\n } as unknown as PrinterModelMeta;\n }\n\n getPrintTaskType(): PrintTaskName {\n // The service needs a task name; these clients print the same way whatever it is.\n return 'B1' as PrintTaskName;\n }\n\n getRfidInfo() {\n return {};\n }\n\n async fetchRfidInfo() {}\n startHeartbeat() {}\n stopHeartbeat() {}\n\n /** Pages arrive as 1-bit canvases; the service hands them here instead of to niimbluelib's encoder. */\n encodePage(canvas: HTMLCanvasElement): Uint8Array[] {\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('Canvas is unavailable');\n return this.encodeRows(ctx.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height);\n }\n\n /** Sends bytes in chunks, waiting while the printer asks for a pause. `onSent` hears the share sent, 0–1. */\n async send(bytes: Uint8Array, onSent?: (share: number) => void): Promise<void> {\n for (let i = 0; i < bytes.length; ) {\n while (this.paused) await sleep(this.pace.pauseMs);\n const part = bytes.subarray(i, i + this.chunk);\n try {\n await this.transport.write(part);\n } catch (err) {\n if (this.chunk <= 20) throw err;\n this.chunk = Math.max(20, this.chunk >> 1);\n continue;\n }\n i += part.length;\n onSent?.(i / bytes.length);\n await sleep(this.pace.chunkMs);\n }\n }\n\n protocol = {\n printEnd: async () => true,\n newPrintTask: (_name: PrintTaskName, options: Record<string, unknown>): PrintTaskLike => {\n const density = Number(options.density ?? 1);\n const labelType = Number(options.labelType ?? 1);\n const gapDots = options.gapDots === undefined ? undefined : Math.max(0, Math.round(Number(options.gapDots)));\n // A batch sends several pages to one task; progress counts across them.\n let printed = 0;\n return {\n printInit: async () => {},\n printPage: async (image, quantity = 1) => {\n const total = () => Math.max(printed, Number(options.totalPages ?? printed));\n // Sending is the slow part over Bluetooth, so report it as it goes, in whole percents.\n let shown = -1;\n await this.send(this.job(image as Uint8Array[], density, Math.max(1, quantity), labelType, gapDots), (share) => {\n const sending = Math.floor(share * 100);\n if (sending === shown) return;\n shown = sending;\n this.emit('printprogress', { page: printed, pagesTotal: total(), pagePrintProgress: 0, pageFeedProgress: 0, sending });\n });\n await this.afterPage();\n printed += Math.max(1, quantity);\n this.emit('printprogress', { page: printed, pagesTotal: total(), pagePrintProgress: 100, pageFeedProgress: 100 });\n },\n waitForFinished: async () => {},\n printEnd: async () => true,\n };\n },\n };\n}\n\n/** A Web Bluetooth link through one service: a write characteristic and, if present, a notify one. */\nexport async function webTransport(\n device: BluetoothDevice,\n server: BluetoothRemoteGATTServer,\n ids: { service: string; write: string; notify?: string },\n): Promise<ThermalTransport> {\n const service = await server.getPrimaryService(ids.service);\n const tx = await service.getCharacteristic(ids.write);\n const notify: ((d: Uint8Array) => void)[] = [];\n if (ids.notify) {\n const rx = await service.getCharacteristic(ids.notify);\n rx.addEventListener('characteristicvaluechanged', (e) => {\n const v = (e.target as BluetoothRemoteGATTCharacteristic).value;\n if (v) notify.forEach((fn) => fn(new Uint8Array(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength))));\n });\n await rx.startNotifications();\n }\n return {\n name: device.name,\n write: async (chunk) => {\n const buf = chunk.slice().buffer;\n if (tx.properties.writeWithoutResponse) await tx.writeValueWithoutResponse(buf);\n else await tx.writeValueWithResponse(buf);\n },\n onNotify: (fn) => notify.push(fn),\n onDisconnect: (fn) => device.addEventListener('gattserverdisconnected', fn),\n disconnect: async () => server.disconnect(),\n };\n}\n","/**\n * \"Cat printers\": the small Bluetooth thermal printers sold as GB01, MX06 and\n * the like. See thermal.ts for what they share with other extras.\n *\n * Protocol, from rbaron/catprinter and NaitLee/Cat-Printer: packets are\n * 51 78 <cmd> 00 <len> 00 <payload> <crc8(payload)> FF over service AE30,\n * written to AE01. The printer sends flow control on AE02.\n */\nimport { modelFor, packRows, shownName, THERMAL_HEAD_DOTS, ThermalPrinterClient, type Confidence, type Pace, type ThermalModel, type ThermalTransport } from './thermal';\n\nexport const CAT_SERVICE = '0000ae30-0000-1000-8000-00805f9b34fb';\nexport const CAT_TX = '0000ae01-0000-1000-8000-00805f9b34fb';\nexport const CAT_RX = '0000ae02-0000-1000-8000-00805f9b34fb';\n\nconst ROW_BYTES = THERMAL_HEAD_DOTS / 8;\n\nexport type { Confidence };\n\nexport interface CatModel extends ThermalModel {\n /** Takes the newer \"start printing\" command. */\n newKind?: boolean;\n /** Mishandles the feed command, so the paper is fed with blank rows. */\n feedWithRows?: boolean;\n}\n\n/**\n * Models known to speak this protocol. \"high\": supported by both reference\n * projects and widely used. \"medium\": listed by Cat-Printer, fewer reports.\n * \"tested\": printed with niimai on real hardware.\n */\nexport const CAT_MODELS: CatModel[] = [\n { prefix: 'GB01', confidence: 'high' },\n { prefix: 'GB02', confidence: 'high' },\n { prefix: 'GB03', confidence: 'high', newKind: true },\n { prefix: 'MX05', confidence: 'high', feedWithRows: true },\n { prefix: 'MX06', confidence: 'high', feedWithRows: true },\n { prefix: 'MX10', confidence: 'high', feedWithRows: true },\n { prefix: 'MX08', confidence: 'medium', feedWithRows: true },\n { prefix: 'MX09', confidence: 'medium', feedWithRows: true },\n { prefix: 'MX11', confidence: 'medium' },\n { prefix: 'GT01', confidence: 'medium' },\n { prefix: 'YT01', confidence: 'medium' },\n { prefix: 'PD01', confidence: 'medium' },\n { prefix: 'MXTP', confidence: 'medium' },\n { prefix: 'SC03h', confidence: 'medium' },\n];\n\n/** Look-alikes that use other protocols. Listed in Help and docs/thermal-printers.md. */\nexport const CAT_UNSUPPORTED: { name: string; note: string }[] = [\n { name: 'MXW01', note: 'Newer protocol' },\n { name: 'PeriPage', note: 'Different protocol' },\n { name: 'Phomemo M02 / T02', note: 'Different protocol' },\n];\n\nexport const catModelFor = (name: string | undefined): CatModel | null => modelFor(CAT_MODELS, name);\n\n// CRC-8, polynomial 0x07, as the printers expect.\nconst CRC_TABLE = Array.from({ length: 256 }, (_, i) => {\n let c = i;\n for (let b = 0; b < 8; b++) c = c & 0x80 ? ((c << 1) ^ 0x07) & 0xff : (c << 1) & 0xff;\n return c;\n});\nexport const crc8 = (data: ArrayLike<number>): number => {\n let c = 0;\n for (let i = 0; i < data.length; i++) c = CRC_TABLE[(c ^ data[i]) & 0xff];\n return c;\n};\n\nexport function packet(cmd: number, payload: ArrayLike<number>): number[] {\n if (payload.length > 0xff) throw new Error('Payload too long');\n return [0x51, 0x78, cmd, 0x00, payload.length, 0x00, ...Array.from(payload), crc8(payload), 0xff];\n}\n\nconst u16 = (v: number) => [v & 0xff, (v >> 8) & 0xff];\n\nconst CMD = {\n retract: 0xa0,\n feed: 0xa1,\n row: 0xa2,\n state: 0xa3,\n quality: 0xa4,\n lattice: 0xa6,\n update: 0xa9,\n energy: 0xaf,\n speed: 0xbd,\n applyEnergy: 0xbe,\n} as const;\n\nconst LATTICE_START = [0xaa, 0x55, 0x17, 0x38, 0x44, 0x5f, 0x5f, 0x5f, 0x44, 0x38, 0x2c];\nconst LATTICE_END = [0xaa, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17];\n\n/** Flow control the printer sends on AE02 when its buffer fills and drains. */\nexport const FLOW_PAUSE = [0x51, 0x78, 0xae, 0x01, 0x01, 0x00, 0x10, 0x70, 0xff];\nexport const FLOW_RESUME = [0x51, 0x78, 0xae, 0x01, 0x01, 0x00, 0x00, 0x00, 0xff];\n\n/** Heat energy for each darkness step; Cat-Printer prints images at 0x4000. */\nconst ENERGY = [0x2000, 0x3000, 0x4000, 0x6000, 0x9000];\n/** Rows fed after the last copy, so the print clears the tear bar. */\nconst FEED_AFTER = 128;\n/** Blank rows between copies. */\nconst GAP_BETWEEN = 24;\n\n/** Rows for a cat printer: leftmost dot in the lowest bit. */\nexport const encodeRows = (data: ArrayLike<number>, width: number, height: number): Uint8Array[] => packRows(data, width, height, 'lsb');\n\n/**\n * Everything sent for one job: setup, the rows for each copy, then feed and\n * finish. With `gapDots`, for labels with gaps, every label is followed by\n * exactly that feed, so the next print starts on the next label.\n */\nexport function printJob(rows: Uint8Array[], { density, copies, model, gapDots }: { density: number; copies: number; model: CatModel | null; gapDots?: number }): Uint8Array {\n const out: number[] = [];\n const push = (cmd: number, payload: ArrayLike<number>) => out.push(...packet(cmd, payload));\n push(CMD.state, [0]);\n if (model?.newKind) out.push(0x12);\n push(CMD.state, [0]);\n push(CMD.quality, [50]);\n push(CMD.speed, [32]);\n push(CMD.energy, u16(ENERGY[Math.min(ENERGY.length, Math.max(1, Math.round(density))) - 1]));\n push(CMD.applyEnergy, [1]);\n push(CMD.update, [0]);\n push(CMD.lattice, LATTICE_START);\n const blank = new Uint8Array(ROW_BYTES);\n for (let c = 0; c < copies; c++) {\n if (c > 0) for (let i = 0; i < (gapDots ?? GAP_BETWEEN); i++) push(CMD.row, blank);\n for (const row of rows) push(CMD.row, row);\n }\n push(CMD.lattice, LATTICE_END);\n push(CMD.speed, [8]);\n if (gapDots !== undefined) out.push(...catFeed(gapDots, model));\n else if (model?.feedWithRows) for (let i = 0; i < FEED_AFTER; i++) push(CMD.row, blank);\n else push(CMD.feed, u16(FEED_AFTER));\n push(CMD.state, [0]);\n return Uint8Array.from(out);\n}\n\n/** Moves the paper by `dots`, back when negative. Models that mishandle the feed command get blank rows. */\nexport function catFeed(dots: number, model: CatModel | null): Uint8Array {\n const n = Math.abs(Math.round(dots));\n if (!n) return new Uint8Array();\n if (dots < 0) return Uint8Array.from(packet(CMD.retract, u16(n)));\n if (!model?.feedWithRows) return Uint8Array.from(packet(CMD.feed, u16(n)));\n const blank = new Uint8Array(ROW_BYTES);\n return Uint8Array.from(Array.from({ length: n }, () => packet(CMD.row, blank)).flat());\n}\n\n/** A cat printer: the shared thermal client with cat packets and flow control. */\nexport class CatPrinterClient extends ThermalPrinterClient {\n protected readonly densityLevels = ENERGY.length;\n protected readonly modelName: string;\n\n constructor(\n transport: ThermalTransport,\n readonly model: CatModel | null,\n pace: Pace = { chunkMs: 20, pauseMs: 100 },\n ) {\n super(transport, pace, 180);\n this.modelName = shownName(model, transport, 'Cat printer');\n transport.onNotify((data) => {\n if (same(data, FLOW_PAUSE)) this.paused = true;\n else if (same(data, FLOW_RESUME)) this.paused = false;\n });\n }\n\n protected encodeRows(data: ArrayLike<number>, width: number, height: number) {\n return encodeRows(data, width, height);\n }\n\n protected job(rows: Uint8Array[], density: number, copies: number, _labelType: number, gapDots?: number) {\n return printJob(rows, { density, copies, model: this.model, gapDots });\n }\n\n readonly feedsBack = true;\n\n /** These printers cannot sense the paper, so the user lines it up by hand. */\n async feedPaper(dots: number): Promise<void> {\n await this.send(catFeed(dots, this.model));\n }\n}\n\nconst same = (a: ArrayLike<number>, b: number[]) => a.length === b.length && b.every((v, i) => a[i] === v);\n","/**\n * ESC/POS mini printers: 58 mm Bluetooth receipt printers such as the MP583\n * (YAEN MPT-II). They take standard raster images (GS v 0) over the 18F0\n * printer service. See thermal.ts for what they share with other extras.\n */\nimport { modelFor, packRows, shownName, THERMAL_HEAD_DOTS, ThermalPrinterClient, type Pace, type ThermalModel, type ThermalTransport } from './thermal';\n\nexport const ESCPOS_SERVICE = '000018f0-0000-1000-8000-00805f9b34fb';\nexport const ESCPOS_WRITE = '00002af1-0000-1000-8000-00805f9b34fb';\n/** The serial service many budget printers offer, Xprinter among them; NIIMBOT printers use it too. */\nexport const ESCPOS_ALT_SERVICE = 'e7810a71-73ae-499d-8c15-faa9aef0c3f2';\nexport const ESCPOS_ALT_WRITE = 'bef8d6c9-9c21-4c9e-b632-bd58c1009f9f';\n\n/**\n * Models known to take ESC/POS raster images, by Bluetooth name. \"tested\":\n * printed with niimai on real hardware. \"high\": supported by several\n * open-source printer projects. \"medium\": the name is reported by one or two\n * of them, untested here. Names from receipt-printer projects such as\n * WebBluetoothReceiptPrinter and bitbank2/Thermal_Printer; see\n * docs/thermal-printers.md for sources. Most of these printers share one\n * Bluetooth module, so its services tell nothing apart: the name decides.\n */\n// A model's prefix is the name it shows and the key its saved settings live under, so existing entries never change it.\nexport const ESCPOS_MODELS: ThermalModel[] = [\n { prefix: 'MP583', confidence: 'tested', note: 'Sold as YAEN MPT-II' },\n { prefix: 'MPT-II', confidence: 'high', note: 'Stock module name on many 58 mm printers' },\n { prefix: 'MP58', confidence: 'medium' },\n { prefix: 'MP210', confidence: 'medium' },\n { prefix: 'MTP-II', confidence: 'high', note: 'Stock module name on many 58 mm printers' },\n { prefix: 'MTP-2', confidence: 'medium' },\n { prefix: 'MPT-3', confidence: 'medium', note: '80 mm', headDots: 576 },\n { prefix: 'MTP-3', confidence: 'medium', note: '80 mm', headDots: 576 },\n { prefix: 'PT-210', confidence: 'high', brand: 'Goojprt PT-210' },\n { prefix: 'PT210', confidence: 'medium', brand: 'Goojprt PT-210' },\n { prefix: 'GOJRPT', confidence: 'medium', brand: 'Goojprt' },\n { prefix: 'Gojprt', confidence: 'medium', brand: 'Goojprt' },\n { prefix: 'PT-200', confidence: 'medium' },\n { prefix: 'RPP02', confidence: 'high', brand: 'Rongta RPP02N', note: 'Also sold as EPPOS EP58 Plus' },\n { prefix: 'RPP300', confidence: 'medium', brand: 'Rongta RPP300', note: '80 mm', headDots: 576 },\n { prefix: 'ZJ-58', confidence: 'medium', brand: 'Zjiang' },\n { prefix: 'TM-P', confidence: 'medium', brand: 'Epson TM-P series' },\n { prefix: 'XP-P323B', confidence: 'medium', brand: 'Xprinter XP-P323B', note: '80 mm', headDots: 576 },\n { prefix: 'XP-', confidence: 'medium', brand: 'other Xprinters', generic: true },\n // Many budget printers keep their Bluetooth module's stock name: the Xprinter XP-58IIL shows as \"Printer001\".\n { prefix: 'Printer', confidence: 'tested', brand: 'Xprinter XP-58IIL, HOIN, Gprinter', note: 'Stock module name', generic: true },\n { prefix: 'printer', confidence: 'medium', note: 'Stock module name', generic: true },\n { prefix: 'BlueTooth Printer', confidence: 'medium', note: 'Stock module name: Zjiang POS-5805 and others', generic: true },\n { prefix: '58Printer', confidence: 'medium', note: 'Stock module name', generic: true },\n];\n\nexport const escposModelFor = (name: string | undefined): ThermalModel | null => modelFor(ESCPOS_MODELS, name);\n\n/** Rows per raster command; small bands keep the printer's buffer from overflowing. */\nconst BAND = 24;\n/** Dots fed between copies. */\nconst GAP_DOTS = 24;\n/** Lines fed after the last copy, so the print clears the tear bar. */\nconst FEED_LINES = 4;\n\n/** Rows for an ESC/POS printer: leftmost dot in the highest bit, centred on a head `head` dots wide. */\nexport const encodeRows = (data: ArrayLike<number>, width: number, height: number, head = THERMAL_HEAD_DOTS): Uint8Array[] =>\n packRows(data, width, height, 'msb', head);\n\n/**\n * ESC @, then each copy as GS v 0 bands with a gap between, then a feed past\n * the tear bar. With `gapDots`, for labels with gaps, every label is followed\n * by exactly that feed, so the next print starts on the next label.\n */\nexport function escposJob(rows: Uint8Array[], copies: number, gapDots?: number): Uint8Array {\n const out: number[] = [0x1b, 0x40];\n const rowBytes = rows[0]?.length ?? THERMAL_HEAD_DOTS / 8;\n for (let c = 0; c < copies; c++) {\n if (c > 0) out.push(...escposFeed(gapDots ?? GAP_DOTS));\n for (let i = 0; i < rows.length; i += BAND) {\n const band = rows.slice(i, i + BAND);\n out.push(0x1d, 0x76, 0x30, 0x00, rowBytes & 0xff, rowBytes >> 8, band.length & 0xff, band.length >> 8);\n for (const row of band) out.push(...row);\n }\n }\n if (gapDots === undefined) out.push(0x1b, 0x64, FEED_LINES);\n else out.push(...escposFeed(gapDots));\n return Uint8Array.from(out);\n}\n\n/**\n * Moves the paper forward by `dots`: line feeds with the line spacing set to\n * the distance (ESC 3 n, LF), then the default spacing back (ESC 2). The\n * MP583 ignores ESC J, the usual dot feed, but takes line feeds. These\n * printers have no reverse feed.\n */\nexport function escposFeed(dots: number): Uint8Array {\n const out: number[] = [];\n for (let left = Math.round(dots); left > 0; left -= 255) out.push(0x1b, 0x33, Math.min(255, left), 0x0a);\n if (out.length) out.push(0x1b, 0x32);\n return Uint8Array.from(out);\n}\n\n/**\n * An ESC/POS printer: the shared thermal client with raster commands. These\n * printers send no flow control, so writes are paced. Heat settings vary by\n * vendor, so darkness is left to the printer.\n */\nexport class EscPosPrinterClient extends ThermalPrinterClient {\n protected readonly densityLevels = 1;\n protected readonly modelName: string;\n protected readonly headDots: number;\n\n constructor(\n transport: ThermalTransport,\n readonly model: ThermalModel | null,\n pace: Pace = { chunkMs: 30, pauseMs: 100 },\n ) {\n super(transport, pace, 100);\n this.modelName = shownName(model, transport, 'Thermal printer');\n this.headDots = model?.headDots ?? THERMAL_HEAD_DOTS;\n }\n\n protected encodeRows(data: ArrayLike<number>, width: number, height: number) {\n return encodeRows(data, width, height, this.headDots);\n }\n\n protected job(rows: Uint8Array[], _density: number, copies: number, _labelType: number, gapDots?: number) {\n return escposJob(rows, copies, gapDots);\n }\n\n /** These printers cannot sense the paper, so the user lines it up by hand. */\n async feedPaper(dots: number): Promise<void> {\n if (dots < 0) throw new Error('This printer cannot move the paper back');\n await this.send(escposFeed(dots));\n }\n}\n","/**\n * TSPL label printers: 2- and 3-inch desktop label printers that take TSC's\n * label language over the 18F0 printer service. A job is a few text commands\n * (SIZE, GAP, DENSITY, CLS) around one BITMAP of the page, then PRINT. The\n * printer finds each label's edge itself. It answers \"OK\" on 2AF0 when it has\n * taken a job, and a status byte to ESC ! ?.\n */\nimport type { LabelSize } from './profiles';\nimport { modelFor, shownName, ThermalPrinterClient, type Pace, type ThermalModel, type ThermalTransport } from './thermal';\n\nexport const TSPL_SERVICE = '000018f0-0000-1000-8000-00805f9b34fb';\nexport const TSPL_WRITE = '00002af1-0000-1000-8000-00805f9b34fb';\nexport const TSPL_NOTIFY = '00002af0-0000-1000-8000-00805f9b34fb';\n\n/** Dots across the head: 80 mm at 203 dpi. */\nexport const TSPL_HEAD_DOTS = 640;\nconst DOTS_PER_MM = 8;\n\n/**\n * Models known to take TSPL over 18F0. \"tested\": printed with niimai on real\n * hardware. \"medium\": reported by open-source printer projects, untested here.\n */\nexport const TSPL_MODELS: ThermalModel[] = [\n { prefix: 'QR380A', confidence: 'tested', note: 'Sold as CC390' },\n { prefix: 'GP-M322', confidence: 'medium', brand: 'Gprinter GP-M322', headDots: 576 },\n];\n\nexport const tsplModelFor = (name: string | undefined): ThermalModel | null => modelFor(TSPL_MODELS, name);\n\n/** Common 2- to 3-inch label sizes, width × height in mm, smallest first. */\nexport const TSPL_SIZES: LabelSize[] = [\n [40, 30],\n [50, 25],\n [50, 30],\n [50, 40],\n [57, 32],\n [60, 40],\n [50, 50],\n [70, 50],\n [75, 50],\n [80, 50],\n [60, 60],\n [80, 60],\n [80, 80],\n [75, 100],\n].map(([widthMm, heightMm]) => ({ widthMm, heightMm }));\n\n/** TSPL's darkness steps, 0 to 15; niimai offers 1 to 15. */\nconst DENSITY_LEVELS = 15;\n/** The gap between labels, for printers that look for it. */\nconst GAP_MM = 2;\n\n/** NIIMBOT label types, as the print dialog passes them. */\nconst LABEL_TYPE = { blackmark: 2, continuous: 3 } as const;\n\n/** The status query, and the bits of its answer that stop a print. */\nexport const STATUS_QUERY = [0x1b, 0x21, 0x3f];\nconst STATUS_FAULTS: [number, string][] = [\n [0x01, 'Printer cover is open'],\n [0x02, 'Paper jam'],\n [0x04, 'Out of paper'],\n [0x08, 'Out of ribbon'],\n];\n\n/** Replies that mean a job was taken: \"OK\". */\nconst OK = [0x4f, 0x4b];\n/** How long to wait for \"OK\" before asking the printer what is wrong. */\nconst ACK_TIMEOUT_MS = 20_000;\n/** How long to wait for the answer to a status query. */\nconst STATUS_TIMEOUT_MS = 2_000;\n\n/**\n * Rows for a TSPL BITMAP: as wide as the page, leftmost dot in the highest\n * bit, and 0 for a dark dot.\n */\nexport function encodeRows(data: ArrayLike<number>, width: number, height: number): Uint8Array[] {\n const bytes = Math.ceil(width / 8);\n const rows: Uint8Array[] = [];\n for (let y = 0; y < height; y++) {\n const row = new Uint8Array(bytes).fill(0xff);\n for (let x = 0; x < width; x++) if (data[(y * width + x) * 4] < 128) row[x >> 3] &= ~(0x80 >> (x & 7));\n rows.push(row);\n }\n return rows;\n}\n\nconst ascii = (s: string) => Array.from(s, (c) => c.charCodeAt(0));\nconst mm = (dots: number) => `${Math.round((dots / DOTS_PER_MM) * 10) / 10} mm`;\n\n/** How the printer finds the start of each label. */\nfunction paperCommand(labelType: number): string {\n if (labelType === LABEL_TYPE.continuous) return 'GAP 0 mm,0 mm';\n if (labelType === LABEL_TYPE.blackmark) return `BLINE ${GAP_MM} mm,0 mm`;\n return `GAP ${GAP_MM} mm,0 mm`;\n}\n\n/** One label: setup, the page as one bitmap at the top left, then print it `copies` times. */\nexport function tsplJob(rows: Uint8Array[], { density, copies, labelType }: { density: number; copies: number; labelType: number }): Uint8Array {\n const bytes = rows[0]?.length ?? 0;\n const height = rows.length;\n const head = [\n `SIZE ${mm(bytes * 8)},${mm(height)}`,\n paperCommand(labelType),\n 'DIRECTION 0',\n `DENSITY ${Math.min(DENSITY_LEVELS, Math.max(0, Math.round(density)))}`,\n 'CLS',\n ];\n const out = ascii(head.map((l) => `${l}\\r\\n`).join('') + `BITMAP 0,0,${bytes},${height},0,`);\n for (const row of rows) for (const b of row) out.push(b);\n out.push(...ascii(`\\r\\nPRINT ${Math.max(1, copies)},1\\r\\n`));\n return Uint8Array.from(out);\n}\n\n/**\n * A TSPL label printer: the shared thermal client with TSPL jobs. It shows\n * as a label printer (roll sizes, paper types). Writes are paced at the size\n * these printers' BLE modules take without dropping data.\n */\nexport class TsplPrinterClient extends ThermalPrinterClient {\n protected readonly densityLevels = DENSITY_LEVELS;\n protected readonly modelName: string;\n protected readonly headDots: number;\n protected readonly kind = undefined;\n protected readonly labelSizes = TSPL_SIZES;\n\n private oks = 0;\n private status: number | null = null;\n private wake: (() => void) | null = null;\n\n constructor(\n transport: ThermalTransport,\n readonly model: ThermalModel | null,\n pace: Pace = { chunkMs: 30, pauseMs: 100 },\n private ackTimeoutMs = ACK_TIMEOUT_MS,\n ) {\n super(transport, pace, 100);\n this.modelName = shownName(model, transport, 'Label printer');\n this.headDots = model?.headDots ?? TSPL_HEAD_DOTS;\n transport.onNotify((data) => {\n if (data.length >= 2 && data[0] === OK[0] && data[1] === OK[1]) this.oks++;\n else if (data.length === 1) this.status = data[0];\n else return;\n this.wake?.();\n });\n }\n\n protected encodeRows(data: ArrayLike<number>, width: number, height: number) {\n return encodeRows(data, width, height);\n }\n\n /** These printers cannot sense the roll; its size is remembered per printer. */\n rollKey() {\n return `printer:${this.transport.name || this.modelName}`;\n }\n\n protected job(rows: Uint8Array[], density: number, copies: number, labelType: number) {\n this.oks = 0;\n return tsplJob(rows, { density, copies, labelType });\n }\n\n /** Resolves when `done` holds or the time runs out. */\n private async until(done: () => boolean, ms: number) {\n const deadline = Date.now() + ms;\n while (!done() && Date.now() < deadline) {\n await new Promise<void>((resolve) => {\n const t = setTimeout(resolve, deadline - Date.now());\n this.wake = () => (clearTimeout(t), resolve());\n });\n }\n this.wake = null;\n }\n\n /**\n * Waits for the printer's \"OK\", so the next label never lands on a busy\n * printer. Without one, asks for its status: a fault fails the print, and a\n * printer that is merely quiet is left to carry on.\n */\n protected async afterPage() {\n await this.until(() => this.oks > 0, this.ackTimeoutMs);\n if (this.oks > 0) return;\n this.status = null;\n await this.send(Uint8Array.from(STATUS_QUERY));\n await this.until(() => this.status !== null, STATUS_TIMEOUT_MS);\n const fault = STATUS_FAULTS.find(([bit]) => (this.status ?? 0) & bit);\n if (fault) throw new Error(fault[1]);\n console.warn('[niimai] Label printer did not confirm the job');\n }\n}\n","/**\n * Every printer outside NIIMBOT that libniimai knows, by Bluetooth name, in\n * one lookup across families. A name matches one family at most, and a name\n * that matches none is known to be unknown before anything is sent to it.\n * The models themselves live with their family: ESCPOS_MODELS, TSPL_MODELS\n * and CAT_MODELS.\n */\nimport { CAT_MODELS } from './cat';\nimport { ESCPOS_MODELS } from './escpos';\nimport { THERMAL_HEAD_DOTS, type ThermalModel } from './thermal';\nimport { TSPL_HEAD_DOTS, TSPL_MODELS } from './tspl';\n\nexport type PrinterFamily = 'escpos' | 'tspl' | 'cat';\n\nexport interface CatalogEntry {\n family: PrinterFamily;\n model: ThermalModel;\n}\n\nexport const PRINTER_CATALOG: CatalogEntry[] = [\n ...ESCPOS_MODELS.map((model) => ({ family: 'escpos' as const, model })),\n ...TSPL_MODELS.map((model) => ({ family: 'tspl' as const, model })),\n ...CAT_MODELS.map((model) => ({ family: 'cat' as const, model })),\n];\n\n/** The known printer a Bluetooth name belongs to, longest prefix first across every family. */\nexport function identifyPrinter(name: string | undefined): CatalogEntry | null {\n if (!name) return null;\n let best: CatalogEntry | null = null;\n for (const entry of PRINTER_CATALOG) {\n if (name.startsWith(entry.model.prefix) && entry.model.prefix.length > (best?.model.prefix.length ?? 0)) best = entry;\n }\n return best;\n}\n\n/** A kind of printer to pick for one niimai does not recognise. */\nexport interface PrinterType extends CatalogEntry {\n id: string;\n name: string;\n}\n\n/** The kinds of printer an unrecognised one can be set to, alongside NIIMBOT's MANUAL_PROFILES. */\nexport const PRINTER_TYPES: PrinterType[] = [\n { id: 'escpos-58', name: 'Receipt printer, 58 mm', family: 'escpos', model: { prefix: '', confidence: 'medium', generic: true, headDots: THERMAL_HEAD_DOTS } },\n { id: 'escpos-80', name: 'Receipt printer, 80 mm', family: 'escpos', model: { prefix: '', confidence: 'medium', generic: true, headDots: 576 } },\n { id: 'tspl', name: 'Label printer (TSPL)', family: 'tspl', model: { prefix: '', confidence: 'medium', generic: true, headDots: TSPL_HEAD_DOTS } },\n { id: 'cat', name: 'Cat printer', family: 'cat', model: { prefix: '', confidence: 'medium', generic: true } },\n];\n\nexport const findPrinterType = (id: string | null | undefined): PrinterType | null => PRINTER_TYPES.find((t) => t.id === id) ?? null;\n","import { CANDIDATE_SERVICES, createBleClient, requestPrinter } from './ble';\nimport { findPrinterType, identifyPrinter, PRINTER_CATALOG, type CatalogEntry, type PrinterFamily, type PrinterType } from './catalog';\nimport { CAT_RX, CAT_SERVICE, CAT_TX, CatPrinterClient } from './cat';\nimport { ESCPOS_ALT_SERVICE, ESCPOS_ALT_WRITE, ESCPOS_SERVICE, ESCPOS_WRITE, EscPosPrinterClient } from './escpos';\nimport { TSPL_NOTIFY, TSPL_SERVICE, TSPL_WRITE, TsplPrinterClient } from './tspl';\nimport { webTransport } from './thermal';\nimport type { ClientLike } from './client';\n\ntype Lib = typeof import('@mmote/niimbluelib');\n\n/**\n * A writable characteristic on one of the serial services budget printers\n * offer, for ESC/POS printers on none of the known layouts.\n */\nasync function writeChannel(server: BluetoothRemoteGATTServer, services: string[]): Promise<{ service: string; write: string } | null> {\n for (const service of CANDIDATE_SERVICES) {\n if (!services.includes(service) || service === CAT_SERVICE) continue;\n const chars = await server\n .getPrimaryService(service)\n .then((s) => s.getCharacteristics())\n .catch(() => []);\n const write = chars.find((c) => c.properties.writeWithoutResponse || c.properties.write);\n if (write) return { service, write: write.uuid };\n }\n return null;\n}\n\nconst PRINTER_TYPE_NAMES: Record<PrinterFamily, string> = { escpos: 'receipt printer', tspl: 'label printer', cat: 'cat printer' };\n\n/** The client for a known or hand-picked thermal printer, over the service its family uses; null when the printer lacks it. */\nasync function thermalClient(device: BluetoothDevice, server: BluetoothRemoteGATTServer, services: string[], { family, model }: CatalogEntry): Promise<ClientLike | null> {\n if (family === 'cat') {\n return services.includes(CAT_SERVICE) ? new CatPrinterClient(await webTransport(device, server, { service: CAT_SERVICE, write: CAT_TX, notify: CAT_RX }), model) : null;\n }\n if (family === 'tspl') {\n return services.includes(TSPL_SERVICE) ? new TsplPrinterClient(await webTransport(device, server, { service: TSPL_SERVICE, write: TSPL_WRITE, notify: TSPL_NOTIFY }), model) : null;\n }\n const channel = services.includes(ESCPOS_SERVICE)\n ? { service: ESCPOS_SERVICE, write: ESCPOS_WRITE }\n : services.includes(ESCPOS_ALT_SERVICE)\n ? { service: ESCPOS_ALT_SERVICE, write: ESCPOS_ALT_WRITE }\n : await writeChannel(server, services);\n return channel ? new EscPosPrinterClient(await webTransport(device, server, channel), model) : null;\n}\n\n/** The name starts like one of niimbluelib's NIIMBOT models (\"D110-…\", \"B21_…\"). */\nfunction looksNiimbot(lib: Lib, name: string | undefined): boolean {\n const token = name?.split(/[-_ ]/)[0].toUpperCase();\n return !!token && lib.modelsLibrary.some((m) => m.model.split('_')[0] === token);\n}\n\n/**\n * The Bluetooth printer behind the Connect button. One chooser lists NIIMBOT\n * printers and the known extra thermal printers. A known cat printer offering\n * AE30 gets the cat client; a known ESC/POS printer the ESC/POS client, over\n * 18F0, the shared E781 service (as Xprinters use), or any serial service\n * with a writable characteristic; a known TSPL label printer offering 18F0\n * the TSPL client; and everything else the NIIMBOT client exactly as before.\n * Known names never overlap, so each printer matches one client at most.\n */\nexport class BluetoothPrinter implements ClientLike {\n private inner: ClientLike | null = null;\n\n constructor(private lib: () => Promise<Lib>) {}\n\n private get client(): ClientLike {\n if (!this.inner) throw new Error('Not connected');\n return this.inner;\n }\n\n /** The kind of printer to treat an unrecognised one as; null leaves it to NIIMBOT. */\n private fallback: PrinterType | null = null;\n\n unrecognisedAs(typeId: string | null) {\n this.fallback = findPrinterType(typeId);\n }\n\n async connect() {\n // With a type picked for unrecognised printers, the chooser lists every device, whatever its name.\n const device = await requestPrinter(\n PRINTER_CATALOG.map((e) => e.model.prefix),\n !!this.fallback,\n );\n const known = identifyPrinter(device.name);\n // A hand-picked type is only for printers that are neither known nor named like a NIIMBOT: a NIIMBOT always keeps its own client.\n const fallback = !known && this.fallback && !looksNiimbot(await this.lib(), device.name) ? this.fallback : null;\n const entry = known ?? fallback;\n if (entry && device.gatt) {\n const server = await device.gatt.connect();\n const services = (await server.getPrimaryServices().catch(() => [])).map((s) => s.uuid);\n this.inner = await thermalClient(device, server, services, entry);\n if (this.inner) return this.inner.connect();\n // A printer set to a type by hand that lacks its service should say so, not fall to NIIMBOT.\n if (!known) throw new Error(`${device.name ?? 'This printer'} does not offer a ${PRINTER_TYPE_NAMES[entry.family]} service`);\n }\n const lib = await this.lib();\n this.inner = createBleClient(lib, device) as unknown as ClientLike;\n try {\n return await this.inner.connect();\n } catch (err) {\n if (looksNiimbot(lib, device.name)) throw err;\n 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.`);\n }\n }\n\n async disconnect() {\n await this.inner?.disconnect();\n }\n\n isConnected() {\n return !!this.inner?.isConnected();\n }\n\n on(event: string, fn: (e: any) => void) {\n return this.client.on(event, fn);\n }\n\n off(event: string, fn: (e: any) => void) {\n return this.inner?.off(event, fn);\n }\n\n getPrinterInfo() {\n return this.inner?.getPrinterInfo() ?? {};\n }\n\n getModelMetadata() {\n return this.inner?.getModelMetadata();\n }\n\n getPrintTaskType() {\n return this.inner?.getPrintTaskType();\n }\n\n getRfidInfo() {\n return this.inner?.getRfidInfo() ?? {};\n }\n\n fetchRfidInfo() {\n return this.client.fetchRfidInfo();\n }\n\n startHeartbeat() {\n this.inner?.startHeartbeat();\n }\n\n stopHeartbeat() {\n this.inner?.stopHeartbeat();\n }\n\n isHeartbeatStarted() {\n return !!this.inner?.isHeartbeatStarted?.();\n }\n\n get protocol() {\n return this.client.protocol;\n }\n\n get encodePage() {\n return this.inner?.encodePage?.bind(this.inner);\n }\n\n get rollKey() {\n return this.inner?.rollKey?.bind(this.inner);\n }\n\n get feedPaper() {\n return this.inner?.feedPaper?.bind(this.inner);\n }\n\n get feedsBack() {\n return this.inner?.feedsBack;\n }\n}\n","import type { PrinterModelMeta } from '@mmote/niimbluelib';\nimport type { ClientLike, PrintTaskLike } from './client';\n\ntype Listener = (e: any) => void;\n\n/** A printer that only exists in memory. Used by the demo mode and the tests. */\nexport class MockClient implements ClientLike {\n connected = false;\n printed: { image: unknown; quantity: number; heartbeat: boolean }[] = [];\n failNextPrint: string | null = null;\n private listeners = new Map<string, Set<Listener>>();\n\n /** Pass null to act as a model the library does not recognise. */\n constructor(private meta: PrinterModelMeta | null = DEMO_META) {}\n\n async connect() {\n this.connected = true;\n this.emit('connect', { info: { deviceName: 'Demo printer' } });\n return { deviceName: 'Demo printer' };\n }\n async disconnect() {\n this.connected = false;\n this.emit('disconnect', {});\n }\n isConnected() {\n return this.connected;\n }\n on(event: string, fn: Listener) {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\n this.listeners.get(event)!.add(fn);\n return this;\n }\n off(event: string, fn: Listener) {\n this.listeners.get(event)?.delete(fn);\n return this;\n }\n emit(event: string, e: unknown) {\n this.listeners.get(event)?.forEach((fn) => fn(e));\n }\n protocolVersion = 1;\n getPrinterInfo() {\n return { batteryPercents: 80, modelId: 512, printheadWidth: 96, protocolVersion: this.protocolVersion };\n }\n getModelMetadata() {\n return this.meta ?? undefined;\n }\n getPrintTaskType() {\n return this.meta ? ('D11_V1' as const) : undefined;\n }\n /** Set false to act like printers that report only the roll's barcode. */\n reportsPaperSize = true;\n\n getRfidInfo() {\n return {\n labelRfidInfo: { tagPresent: true, barCode: '6971501227941', allPaper: 210, usedPaper: 12, consumablesType: 1 },\n paperInfo: this.reportsPaperSize ? { valid: true, paperWidth: 40, paperHeight: 12 } : undefined,\n };\n }\n async fetchRfidInfo() {\n return this.getRfidInfo();\n }\n startHeartbeat() {\n this.heartbeat = true;\n this.emit('heartbeat', { data: { batteryPercents: 80, lidClosed: true, paperInserted: true } });\n }\n heartbeat = false;\n stopHeartbeat() {\n this.heartbeat = false;\n }\n /** How many \"print end\" polls answer false before the printer reports done. */\n printEndPending = 0;\n protocol = {\n printEnd: async () => {\n if (this.printEndPending > 0) {\n this.printEndPending--;\n return false;\n }\n return true;\n },\n newPrintTask: (_name: string, options: Record<string, unknown>): PrintTaskLike => {\n const total = Number(options.totalPages ?? 1);\n return {\n printInit: async () => {\n if (this.failNextPrint) {\n const msg = this.failNextPrint;\n this.failNextPrint = null;\n throw new Error(msg);\n }\n },\n printPage: async (image, quantity = 1) => {\n this.printed.push({ image, quantity, heartbeat: this.heartbeat });\n },\n waitForFinished: async () => {\n for (let page = 1; page <= total; page++) {\n this.emit('printprogress', { page, pagesTotal: total, pagePrintProgress: 100, pageFeedProgress: 100 });\n }\n },\n printEnd: async () => true,\n };\n },\n };\n}\n\nexport const DEMO_META = {\n model: 'D11',\n id: [512],\n dpi: 203,\n printDirection: 'left',\n printheadPixels: 96,\n paperTypes: [1],\n densityMin: 1,\n densityMax: 3,\n densityDefault: 2,\n} as unknown as PrinterModelMeta;\n","/**\n * Paper types, as niimbluelib's LabelType numbers. The printer uses this to\n * find the start of each label, so the wrong one mis-feeds the roll.\n */\nexport type PaperType = 'gaps' | 'blackmark' | 'continuous' | 'perforated' | 'transparent';\n\nexport const PAPER_TYPES: { value: PaperType; label: string; labelType: number }[] = [\n { value: 'gaps', label: 'Labels with gaps', labelType: 1 },\n { value: 'blackmark', label: 'Black mark', labelType: 2 },\n { value: 'continuous', label: 'Continuous', labelType: 3 },\n { value: 'perforated', label: 'Perforated', labelType: 4 },\n { value: 'transparent', label: 'Transparent', labelType: 5 },\n];\n\nexport const paperFromLabelType = (n: number | undefined): PaperType | undefined => PAPER_TYPES.find((p) => p.labelType === n)?.value;\n\nexport const labelTypeOf = (p: PaperType): number => PAPER_TYPES.find((x) => x.value === p)!.labelType;\n\nexport const paperName = (p: PaperType): string => PAPER_TYPES.find((x) => x.value === p)!.label;\n\n/** The paper to print on: the label's own choice, then what the roll reports, then labels with gaps. */\nexport const effectivePaper = (chosen: PaperType | undefined, fromRoll: PaperType | undefined): PaperType => chosen ?? fromRoll ?? 'gaps';\n","import type { PrinterModelMeta, PrintTaskName } from '@mmote/niimbluelib';\n\n/** A label's size in millimetres. */\nexport interface LabelSize {\n widthMm: number;\n heightMm: number;\n}\n\nexport type PrintDirection = 'left' | 'top';\n\n/** Everything the app needs to know to render and send a page to a printer. */\nexport interface PrinterProfile {\n id: string;\n name: string;\n dpi: number;\n printheadPixels: number;\n printDirection: PrintDirection;\n printTask: PrintTaskName;\n densityMin: number;\n densityMax: number;\n densityDefault: number;\n /** Extra thermal printers (cat, ESC/POS): continuous paper, no roll tags. Unset for NIIMBOT. */\n kind?: 'thermal';\n /** Label sizes common for this printer (TSPL label printers). Unset for NIIMBOT, whose sizes come from its rolls. */\n sizes?: LabelSize[];\n}\n\n/** Manual profiles for printers niimbluelib does not recognise yet. Each mirrors a known model family. */\nexport const MANUAL_PROFILES: PrinterProfile[] = [\n { id: 'd11', name: 'D11 family', dpi: 203, printheadPixels: 96, printDirection: 'left', printTask: 'D11_V1', densityMin: 1, densityMax: 3, densityDefault: 2 },\n { id: 'd110', name: 'D110 family', dpi: 203, printheadPixels: 96, printDirection: 'left', printTask: 'D110', densityMin: 1, densityMax: 3, densityDefault: 2 },\n { id: 'b1', name: 'B1 family', dpi: 203, printheadPixels: 384, printDirection: 'top', printTask: 'B1', densityMin: 1, densityMax: 5, densityDefault: 3 },\n { id: 'b21', name: 'B21 family', dpi: 203, printheadPixels: 384, printDirection: 'top', printTask: 'B21_V1', densityMin: 1, densityMax: 5, densityDefault: 3 },\n];\n\nexport const findManualProfile = (id: string | null | undefined): PrinterProfile | undefined =>\n MANUAL_PROFILES.find((p) => p.id === id);\n\n/**\n * Print tasks verified on hardware where niimbluelib's choice prints blank.\n * A B21 C2B on protocol 0 needs the original B21 packet formats; the library\n * gives every B21 C2B the B1 task, which that firmware accepts but never prints.\n */\nconst TASK_QUIRKS: { model: string; maxProtocol: number; task: PrintTaskName }[] = [{ model: 'B21_C2B', maxProtocol: 0, task: 'B21_V1' }];\n\nexport function correctTask(model: string, protocolVersion: number | undefined, task: PrintTaskName | undefined): PrintTaskName | undefined {\n const quirk = TASK_QUIRKS.find((q) => q.model === model && (protocolVersion ?? 0) <= q.maxProtocol);\n return quirk?.task ?? task;\n}\n\n/**\n * The profile to print with: the library's model data when it recognises the\n * printer, otherwise the manual choice. A detected model always wins, so a\n * choice left over from another printer cannot send the wrong settings.\n */\nexport function resolveProfile(\n meta: PrinterModelMeta | undefined,\n libraryTask: PrintTaskName | undefined,\n manual?: PrinterProfile,\n protocolVersion?: number,\n): PrinterProfile | null {\n const task = meta ? correctTask(meta.model, protocolVersion, libraryTask) : undefined;\n if (!meta || !task) return manual ?? null;\n return {\n id: `auto:${meta.model}`,\n name: meta.model.replace(/_/g, ' '),\n dpi: meta.dpi,\n printheadPixels: meta.printheadPixels,\n printDirection: meta.printDirection,\n printTask: task,\n densityMin: meta.densityMin,\n densityMax: meta.densityMax,\n densityDefault: meta.densityDefault,\n ...((meta as { kind?: string }).kind === 'thermal' ? { kind: 'thermal' as const } : {}),\n ...((meta as { labelSizes?: LabelSize[] }).labelSizes ? { sizes: (meta as { labelSizes?: LabelSize[] }).labelSizes } : {}),\n };\n}\n\n/** Widest label edge the printhead covers, in millimetres. */\nexport const printheadMm = (p: PrinterProfile): number => (p.printheadPixels / p.dpi) * 25.4;\n\n/**\n * The part of a page the printhead can print, centred across the head. Pages\n * wider than the head are cropped: printers discard rows longer than the head.\n */\nexport function printheadCrop(width: number, height: number, p: Pick<PrinterProfile, 'printheadPixels' | 'printDirection'>) {\n const acrossIsWidth = p.printDirection === 'top';\n const across = acrossIsWidth ? width : height;\n const keep = Math.min(across, p.printheadPixels);\n const offset = Math.floor((across - keep) / 2);\n return acrossIsWidth\n ? { x: offset, y: 0, width: keep, height, cropped: keep < across }\n : { x: 0, y: offset, width, height: keep, cropped: keep < across };\n}\n\n/**\n * True when any black pixel of a page (RGBA, as from getImageData) falls\n * outside what the printhead covers, so part of the design would not print.\n */\nexport function inkOutsideHead(data: ArrayLike<number>, width: number, height: number, p: Pick<PrinterProfile, 'printheadPixels' | 'printDirection'>): boolean {\n const crop = printheadCrop(width, height, p);\n if (!crop.cropped) return false;\n for (let y = 0; y < height; y++) {\n const insideRow = y >= crop.y && y < crop.y + crop.height;\n for (let x = 0; x < width; x++) {\n if (insideRow && x >= crop.x && x < crop.x + crop.width) {\n x = crop.x + crop.width - 1;\n continue;\n }\n if (data[(y * width + x) * 4] < 128) return true;\n }\n }\n return false;\n}\n","import type { ClientLike, ConnectionKind, PrintTaskLike } from './client';\nimport { paperFromLabelType, type PaperType } from './paper';\nimport { findManualProfile, printheadCrop, resolveProfile, type PrinterProfile } from './profiles';\n\nexport type PrinterState = 'disconnected' | 'connecting' | 'ready' | 'printing';\n\nexport interface PrinterStatus {\n state: PrinterState;\n deviceName?: string;\n battery?: number;\n /** Null when the printer is unknown and no manual profile is chosen. */\n profile: PrinterProfile | null;\n detectedModel?: string;\n /**\n * The loaded roll. Size comes from the printer's paper info where it reports\n * one; the barcode identifies the roll when it does not (the B21 C2B, for one).\n */\n label?: { widthMm?: number; heightMm?: number; remaining?: number; capacity?: number; barcode?: string; paper?: PaperType; manual?: boolean };\n lidClosed?: boolean;\n paperInserted?: boolean;\n /** Pages done of the total; `sending` is how much of the current page has gone over the link, 0–100, for printers that report it. */\n progress?: { page: number; total: number; sending?: number };\n /** The paper can be moved by hand with `feed()`, on printers that cannot sense it. */\n canFeed?: boolean;\n /** `feed()` can also move the paper back. */\n canRetract?: boolean;\n}\n\n/** Nudges the printed image, in mm: positive x moves right, positive y moves down. */\nexport interface PrintOffset {\n x: number;\n y: number;\n}\n\nexport interface PrintOptions {\n density: number;\n /** Copies of each page. */\n copies: number;\n labelType?: number;\n offset?: PrintOffset;\n /**\n * Labels with gaps on a printer that cannot sense them (`status.canFeed`):\n * the gap between labels, in mm. Each label is followed by exactly this\n * much feed in place of the tear-off feed, so prints stay on the labels.\n */\n gapMm?: number;\n}\n\n/** Different labels in one job, drawn one at a time so a long batch never holds every page at once. */\nexport interface PageSource {\n count: number;\n page: (index: number) => Promise<HTMLCanvasElement>;\n}\n\ntype Encoder = (canvas: HTMLCanvasElement, direction: PrinterProfile['printDirection']) => unknown | Promise<unknown>;\nexport type ClientFactory = (kind: ConnectionKind) => Promise<ClientLike>;\n\nconst DISCONNECTED: PrinterStatus = { state: 'disconnected', profile: null };\n\n/** What the demo printer is called wherever a printer's name shows. */\nexport const DEMO_NAME = 'Demo printer';\n\nconst PRINT_TIMEOUT_MS = 20_000;\nconst PAGE_TIMEOUT_MS = 10_000;\nconst PRINT_END_POLL_MS = 300;\nconst mmToPx = (mm: number, dpi: number): number => (mm / 25.4) * dpi;\n\n/** The page moved by whole pixels on a white canvas of the same size. */\nexport function shiftCanvas(canvas: HTMLCanvasElement, dx: number, dy: number): HTMLCanvasElement {\n dx = Math.round(dx);\n dy = Math.round(dy);\n if (!dx && !dy) return canvas;\n const out = document.createElement('canvas');\n out.width = canvas.width;\n out.height = canvas.height;\n const ctx = out.getContext('2d');\n if (!ctx) return canvas;\n ctx.fillStyle = '#fff';\n ctx.fillRect(0, 0, out.width, out.height);\n ctx.drawImage(canvas, dx, dy);\n return out;\n}\n\n/** Crops the page to what the printhead covers. Returns the canvas itself when it already fits. */\nexport function fitToPrinthead(canvas: HTMLCanvasElement, profile: PrinterProfile): HTMLCanvasElement {\n const crop = printheadCrop(canvas.width, canvas.height, profile);\n if (!crop.cropped) return canvas;\n const out = document.createElement('canvas');\n out.width = crop.width;\n out.height = crop.height;\n const ctx = out.getContext('2d');\n if (!ctx) return canvas;\n ctx.drawImage(canvas, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);\n return out;\n}\n\n/** Black pixels on the page, for the print log. A zero here means the page itself was blank. */\nfunction inkPixels(canvas: HTMLCanvasElement): number | null {\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);\n let n = 0;\n for (let i = 0; i < data.length; i += 4) if (data[i] < 128) n++;\n return n;\n}\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => reject(new Error('Timeout waiting for the printer to finish')), ms);\n p.then(\n (v) => (clearTimeout(t), resolve(v)),\n (e) => (clearTimeout(t), reject(e)),\n );\n });\n}\n\n/** Translates the library's error codes and messages into short, readable ones. */\nexport function friendlyError(err: unknown): string {\n const msg = err instanceof Error ? err.message : String(err);\n if (/user cancelled|no device selected|chooser/i.test(msg)) return 'Connection cancelled';\n if (/timeout waiting for the printer to finish/i.test(msg)) return 'The printer did not confirm the print';\n if (/timeout/i.test(msg)) return 'Printer stopped responding';\n if (/lid|cover/i.test(msg)) return 'Close the printer lid';\n if (/paper|label/i.test(msg) && /no|out|missing/i.test(msg)) return 'Load a label roll';\n if (/unknown Bluetooth service|No usable channel/i.test(msg)) return msg;\n if (/disconnected|gatt/i.test(msg)) return 'Printer disconnected';\n return msg || 'Something went wrong';\n}\n\n/**\n * Owns the printer connection. The UI subscribes to status updates and calls\n * connect/print; the raw protocol stays inside niimbluelib.\n */\nexport class PrinterService {\n private client: ClientLike | null = null;\n private status: PrinterStatus = DISCONNECTED;\n private listeners = new Set<(s: PrinterStatus) => void>();\n private manualProfileId: string | null = null;\n private detach: (() => void) | null = null;\n /** Paper moves run one after another, and a print waits for them. */\n private feeding: Promise<void> = Promise.resolve();\n\n /** How the current printer is connected. */\n private kind: ConnectionKind | null = null;\n\n constructor(\n private createClient: ClientFactory,\n private encode: Encoder,\n ) {}\n\n getStatus(): PrinterStatus {\n return this.status;\n }\n\n subscribe(fn: (s: PrinterStatus) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private set(patch: Partial<PrinterStatus>): void {\n this.replace({ ...this.status, ...patch });\n }\n\n /** Swaps the whole status, so fields from an earlier connection never linger. */\n private replace(status: PrinterStatus): void {\n this.status = status;\n this.listeners.forEach((fn) => fn(this.status));\n }\n\n setManualProfile(id: string | null): void {\n this.manualProfileId = id;\n if (this.client) this.set({ profile: this.currentProfile() });\n }\n\n private currentProfile(): PrinterProfile | null {\n const c = this.client;\n const profile = resolveProfile(c?.getModelMetadata(), c?.getPrintTaskType(), findManualProfile(this.manualProfileId), c?.getPrinterInfo().protocolVersion);\n // Whatever printer stands in for the demo, it never passes for a real model.\n return profile && this.kind === 'demo' ? { ...profile, name: DEMO_NAME } : profile;\n }\n\n private readRfid(): PrinterStatus['label'] {\n // Printers that cannot sense the roll keep the size the user sets, under a key of their own.\n const manual = this.client?.rollKey?.();\n if (manual) return { barcode: manual, manual: true };\n const info = this.client?.getRfidInfo();\n if (!info) return undefined;\n const paper = info.paperInfo?.valid ? info.paperInfo : undefined;\n const tag = info.labelRfidInfo?.tagPresent ? info.labelRfidInfo : undefined;\n if (!paper && !tag) return undefined;\n return {\n widthMm: paper?.paperWidth,\n heightMm: paper?.paperHeight,\n remaining: tag && tag.allPaper !== undefined && tag.allPaper >= 0 ? Math.max(0, tag.allPaper - (tag.usedPaper ?? 0)) : undefined,\n capacity: tag && tag.allPaper !== undefined && tag.allPaper > 0 ? tag.allPaper : undefined,\n barcode: tag?.barCode || undefined,\n paper: paperFromLabelType(tag?.consumablesType ?? paper?.paperType),\n };\n }\n\n async connect(kind: ConnectionKind): Promise<void> {\n if (this.client) await this.disconnect();\n this.kind = kind;\n this.replace({ ...DISCONNECTED, state: 'connecting' });\n let client: ClientLike;\n try {\n client = await this.createClient(kind);\n client.unrecognisedAs?.(this.manualProfileId);\n const info = await client.connect();\n this.client = client;\n this.set({ deviceName: info.deviceName });\n } catch (err) {\n this.client = null;\n this.replace(DISCONNECTED);\n throw new Error(friendlyError(err));\n }\n\n const onDisconnect = () => this.handleDisconnect();\n const onHeartbeat = (e: { data: { batteryPercents?: number; lidClosed?: boolean; paperInserted?: boolean } }) =>\n this.set({ battery: e.data.batteryPercents ?? this.status.battery, lidClosed: e.data.lidClosed, paperInserted: e.data.paperInserted });\n const onRfid = () => this.set({ label: this.readRfid() });\n const onProgress = (e: { page: number; pagesTotal: number; sending?: number }) =>\n this.set({ progress: { page: e.page, total: e.pagesTotal, ...(e.sending !== undefined ? { sending: e.sending } : {}) } });\n client.on('disconnect', onDisconnect);\n client.on('heartbeat', onHeartbeat);\n client.on('rfidinfofetched', onRfid);\n client.on('printprogress', onProgress);\n this.detach = () => {\n client.off('disconnect', onDisconnect);\n client.off('heartbeat', onHeartbeat);\n client.off('rfidinfofetched', onRfid);\n client.off('printprogress', onProgress);\n };\n\n const meta = client.getModelMetadata();\n this.set({\n state: 'ready',\n battery: client.getPrinterInfo().batteryPercents,\n detectedModel: meta?.model,\n profile: this.currentProfile(),\n label: this.readRfid(),\n ...(client.feedPaper ? { canFeed: true } : {}),\n ...(client.feedPaper && client.feedsBack ? { canRetract: true } : {}),\n });\n client.startHeartbeat();\n }\n\n private handleDisconnect(): void {\n this.detach?.();\n this.detach = null;\n this.client = null;\n this.replace(DISCONNECTED);\n }\n\n async disconnect(): Promise<void> {\n const c = this.client;\n if (!c) return;\n c.stopHeartbeat();\n try {\n await c.disconnect();\n } finally {\n this.handleDisconnect();\n }\n }\n\n /**\n * B21_V1 finishes by polling \"print end\", but niimbluelib's poll cancels its\n * own timer on the first reply, so it never resolves unless the printer is\n * already done. That task polls here instead.\n */\n private async waitForFinished(client: ClientLike, task: PrintTaskLike, name: string, copies: number): Promise<void> {\n if (name !== 'B21_V1') return task.waitForFinished();\n this.set({ progress: { page: 0, total: copies } });\n while (!(await client.protocol.printEnd())) await sleep(PRINT_END_POLL_MS);\n this.set({ progress: { page: copies, total: copies } });\n }\n\n /**\n * Moves the paper by `mm`, out of the printer when positive and back in when\n * negative, to line it up on printers that cannot sense it.\n */\n feed(mm: number): Promise<void> {\n const client = this.client;\n const profile = this.status.profile;\n if (!client || !profile) return Promise.reject(new Error('Connect a printer first'));\n if (!client.feedPaper) return Promise.reject(new Error('This printer finds the paper itself'));\n if (mm < 0 && !client.feedsBack) return Promise.reject(new Error('This printer cannot move the paper back'));\n if (this.status.state === 'printing') return Promise.reject(new Error('Wait for the print to finish'));\n const dots = Math.round(mmToPx(mm, profile.dpi));\n const run = this.feeding.then(() => (dots ? client.feedPaper!(dots) : undefined));\n this.feeding = run.catch(() => {});\n return run.catch((err) => {\n throw new Error(friendlyError(err));\n });\n }\n\n async print(source: HTMLCanvasElement | PageSource, opts: PrintOptions): Promise<void> {\n const client = this.client;\n const profile = this.status.profile;\n if (!client) throw new Error('Connect a printer first');\n if (!profile) throw new Error('Choose your printer type');\n if (this.status.state === 'printing') throw new Error('Already printing');\n await this.feeding;\n\n const pages: PageSource = 'page' in source ? source : { count: 1, page: async () => source };\n const copies = Math.max(1, Math.floor(opts.copies));\n const total = copies * Math.max(1, pages.count);\n const density = Math.min(profile.densityMax, Math.max(profile.densityMin, Math.round(opts.density)));\n const task = client.protocol.newPrintTask(profile.printTask, {\n totalPages: total,\n density,\n labelType: opts.labelType ?? 1,\n ...(opts.gapMm !== undefined && client.feedPaper ? { gapDots: Math.round(mmToPx(Math.max(0, opts.gapMm), profile.dpi)) } : {}),\n pageColor: 0,\n statusPollIntervalMs: 100,\n statusTimeoutMs: 8000,\n });\n\n // Heartbeat queries interleaved with page data spoil prints on some models.\n client.stopHeartbeat();\n this.set({ state: 'printing', progress: { page: 0, total } });\n try {\n const offset = opts.offset ?? { x: 0, y: 0 };\n for (let i = 0; i < pages.count; i++) {\n const canvas = await pages.page(i);\n const shifted = shiftCanvas(canvas, mmToPx(offset.x, profile.dpi), mmToPx(offset.y, profile.dpi));\n const page = fitToPrinthead(shifted, profile);\n const image = client.encodePage ? await client.encodePage(page) : await this.encode(page, profile.printDirection);\n if (i === 0) {\n console.info('[niimai] print', {\n profile: profile.id,\n task: profile.printTask,\n direction: profile.printDirection,\n printhead: profile.printheadPixels,\n page: `${canvas.width}x${canvas.height}`,\n sent: `${page.width}x${page.height}`,\n density,\n copies,\n pages: pages.count,\n ink: inkPixels(page),\n });\n await task.printInit();\n }\n await task.printPage(image, copies);\n }\n await withTimeout(this.waitForFinished(client, task, profile.printTask, total), PRINT_TIMEOUT_MS + total * PAGE_TIMEOUT_MS);\n } catch (err) {\n throw new Error(friendlyError(err));\n } finally {\n try {\n await task.printEnd();\n } catch {\n // The page already failed; the original error is the useful one.\n }\n if (this.client) {\n client.startHeartbeat();\n this.set({ state: 'ready', progress: undefined });\n void client.fetchRfidInfo().then(() => this.set({ label: this.readRfid() }), () => {});\n }\n }\n }\n}\n","import type { PrinterModelMeta, PrintTaskName } from '@mmote/niimbluelib';\n\n/**\n * The slice of niimbluelib's client the app uses. Keeping it narrow lets the\n * mock printer and the tests stand in for real hardware.\n */\nexport interface PrintTaskLike {\n printInit(): Promise<void>;\n printPage(image: unknown, quantity?: number): Promise<void>;\n waitForFinished(): Promise<void>;\n printEnd(): Promise<boolean>;\n}\n\nexport interface ClientLike {\n connect(): Promise<{ deviceName?: string }>;\n disconnect(): Promise<void>;\n isConnected(): boolean;\n on(event: string, fn: (e: any) => void): unknown;\n off(event: string, fn: (e: any) => void): unknown;\n getPrinterInfo(): { batteryPercents?: number; modelId?: number; printheadWidth?: number; protocolVersion?: number };\n getModelMetadata(): PrinterModelMeta | undefined;\n getPrintTaskType(): PrintTaskName | undefined;\n getRfidInfo(): {\n labelRfidInfo?: { tagPresent?: boolean; barCode?: string; allPaper?: number; usedPaper?: number; consumablesType?: number };\n paperInfo?: { valid?: boolean; paperWidth?: number; paperHeight?: number; paperType?: number };\n };\n fetchRfidInfo(): Promise<unknown>;\n startHeartbeat(): void;\n stopHeartbeat(): void;\n isHeartbeatStarted?(): boolean;\n protocol: {\n newPrintTask(name: PrintTaskName, options: Record<string, unknown>): PrintTaskLike;\n /** True once the printer has finished every page. */\n printEnd(): Promise<boolean>;\n };\n /** Clients that encode pages themselves (cat printers); NIIMBOT pages go through niimbluelib's encoder. */\n encodePage?(canvas: HTMLCanvasElement): unknown;\n /**\n * Label printers that cannot sense the roll: a key to remember the roll\n * size the user sets, per printer. NIIMBOT printers read their rolls instead.\n */\n rollKey?(): string;\n /** Printers that cannot sense the paper: moves it by `dots`, back into the printer when negative. */\n feedPaper?(dots: number): Promise<void>;\n /** feedPaper can also move the paper back. */\n feedsBack?: boolean;\n /**\n * Bluetooth: the kind of printer to treat one with an unknown name as, set\n * before connecting: an id from PRINTER_TYPES, or a NIIMBOT profile id the\n * client ignores.\n */\n unrecognisedAs?(typeId: string | null): void;\n}\n\nexport type ConnectionKind = 'bluetooth' | 'serial' | 'demo';\n\nexport const supportsBluetooth = (): boolean => typeof navigator !== 'undefined' && 'bluetooth' in navigator;\nexport const supportsSerial = (): boolean => typeof navigator !== 'undefined' && 'serial' in navigator;\n\nexport type Platform = 'ios' | 'android' | 'desktop';\n\nexport function detectPlatform(ua = typeof navigator === 'undefined' ? '' : navigator.userAgent, touchPoints = typeof navigator === 'undefined' ? 0 : navigator.maxTouchPoints): Platform {\n // iPadOS reports itself as a Mac; touch support gives it away.\n if (/iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && touchPoints > 1)) return 'ios';\n if (/Android/.test(ua)) return 'android';\n return 'desktop';\n}\n\n/**\n * Why a connection type cannot be used in this browser, in a few words, or\n * null when it can. iOS browsers all run on WebKit, which has neither Web\n * Bluetooth nor Web Serial; Bluefy adds Web Bluetooth there.\n */\nexport function unsupportedReason(kind: ConnectionKind, platform: Platform = detectPlatform()): string | null {\n if (kind === 'demo') return null;\n if (kind === 'bluetooth' ? supportsBluetooth() : supportsSerial()) return null;\n if (platform === 'ios') return kind === 'bluetooth' ? 'On iPhone and iPad, use the Bluefy browser' : 'Not available on iPhone and iPad';\n if (platform === 'android') return 'Open niimai in Chrome';\n return 'Needs Chrome or Edge';\n}\n","/**\n * libniimai: connect to NIIMBOT, TSPL, ESC/POS and cat printers from the\n * browser over Web Bluetooth or Web Serial, and print a canvas on them.\n */\n/// <reference types=\"web-bluetooth\" preserve=\"true\" />\n/// <reference types=\"w3c-web-serial\" preserve=\"true\" />\nimport { BluetoothPrinter } from './bluetooth';\nimport type { ClientLike, ConnectionKind } from './client';\nimport { MockClient } from './mock';\nimport { PrinterService, type ClientFactory } from './service';\n\n// niimbluelib loads only when it is needed, so pages that never meet a NIIMBOT stay light.\nconst niimbot = () => import('@mmote/niimbluelib');\n\n/**\n * A client for one kind of connection. Bluetooth finds the printer family\n * from the device the user picks; serial is NIIMBOT only; demo is a printer\n * in memory.\n */\nexport async function createClient(kind: ConnectionKind): Promise<ClientLike> {\n if (kind === 'demo') return new MockClient();\n if (kind === 'bluetooth') return new BluetoothPrinter(niimbot);\n const lib = await niimbot();\n return new lib.NiimbotSerialClient() as unknown as ClientLike;\n}\n\nexport interface CreatePrinterOptions {\n /**\n * Makes the client for each connection. Defaults to createClient; wrap it\n * to change one kind of connection and keep the others.\n */\n createClient?: ClientFactory;\n}\n\n/** A ready printer service: connect, watch its status, print canvases. */\nexport function createPrinter(options: CreatePrinterOptions = {}): PrinterService {\n return new PrinterService(options.createClient ?? createClient, async (canvas, direction) =>\n (await niimbot()).ImageEncoder.encodeCanvas(canvas, 0, direction),\n );\n}\n\nexport { PrinterService, DEMO_NAME, friendlyError, fitToPrinthead, shiftCanvas } from './service';\nexport type { ClientFactory, PageSource, PrinterState, PrinterStatus, PrintOffset, PrintOptions } from './service';\n\nexport { detectPlatform, supportsBluetooth, supportsSerial, unsupportedReason } from './client';\nexport type { ClientLike, ConnectionKind, Platform, PrintTaskLike } from './client';\n\nexport { correctTask, findManualProfile, inkOutsideHead, MANUAL_PROFILES, printheadCrop, printheadMm, resolveProfile } from './profiles';\nexport type { LabelSize, PrintDirection, PrinterProfile } from './profiles';\n\nexport { effectivePaper, labelTypeOf, PAPER_TYPES, paperFromLabelType, paperName } from './paper';\nexport type { PaperType } from './paper';\n\nexport { findPrinterType, identifyPrinter, PRINTER_CATALOG, PRINTER_TYPES } from './catalog';\nexport type { CatalogEntry, PrinterFamily, PrinterType } from './catalog';\n\nexport { BluetoothPrinter } from './bluetooth';\nexport { describeBleError } from './ble';\nexport { DEMO_META, MockClient } from './mock';\n\nexport { modelFor, THERMAL_DEFAULT_SIZE, THERMAL_HEAD_DOTS, THERMAL_HEAD_MM, THERMAL_SIZES, ThermalPrinterClient, webTransport } from './thermal';\nexport type { Confidence, Pace, ThermalModel, ThermalTransport } from './thermal';\n\nexport { CAT_MODELS, CAT_UNSUPPORTED, CatPrinterClient, catFeed, catModelFor, printJob as catJob } from './cat';\nexport type { CatModel } from './cat';\nexport { ESCPOS_MODELS, EscPosPrinterClient, escposFeed, escposJob, escposModelFor } from './escpos';\nexport { TSPL_MODELS, TSPL_SIZES, TsplPrinterClient, tsplJob, tsplModelFor } from './tspl';\n"],"mappings":";;AAKA,IAAa,qBAAqB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,IAAa,gBAAgB;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;AAAG;;AAGvG,SAAgB,eAAe,gBAA0B,CAAC,GAAG,MAAM,OAAiC;CAClG,IAAI,KAAK,OAAO,UAAU,UAAU,cAAc;EAAE,kBAAkB;EAAM,kBAAkB;CAAmB,CAAC;CAClH,OAAO,UAAU,UAAU,cAAc;EACvC,SAAS,CAAC,GAAG,CAAC,GAAG,eAAe,GAAG,aAAa,CAAC,CAAC,KAAK,gBAAgB,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,CAAC,mBAAmB,EAAE,EAAE,CAAC;EAC9H,kBAAkB;CACpB,CAAC;AACH;;;;;;AAsBA,eAAsB,YAAgC,UAAwD;CAC5G,MAAM,MAAyC,CAAC;CAChD,KAAK,MAAM,WAAW,UAAU,IAAI,KAAK;EAAE,SAAS,QAAQ;EAAM,OAAO,MAAM,QAAQ,mBAAmB;CAAE,CAAC;CAE7G,MAAM,YAAY,MAAS,EAAE,WAAW,wBAAwB,EAAE,WAAW;CAC7E,MAAM,aAAa,MAAS,EAAE,WAAW,UAAU,CAAC,CAAC,EAAE,WAAW;CAElE,KAAK,MAAM,EAAE,WAAW,KAAK;EAC3B,MAAM,OAAO,MAAM,MAAM,MAAM,EAAE,WAAW,wBAAwB,EAAE,WAAW,MAAM;EACvF,IAAI,MAAM,OAAO;GAAE,OAAO;GAAM,QAAQ;EAAK;CAC/C;CACA,KAAK,MAAM,EAAE,WAAW,KAAK;EAC3B,MAAM,QAAQ,MAAM,KAAK,QAAQ;EACjC,MAAM,SAAS,MAAM,KAAK,SAAS;EACnC,IAAI,SAAS,QAAQ,OAAO;GAAE;GAAO;EAAO;CAC9C;CACA,OAAO;AACT;;AAGA,SAAgB,iBAAiB,KAAc,QAAkB,CAAC,GAAU;CAC1E,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC3D,IAAI,qBAAqB,KAAK,GAAG,KAAM,MAAM,WAAW,KAAK,kBAAkB,KAAK,GAAG,GACrF,uBAAO,IAAI,MAAM,oFAAoF;CAEvG,IAAI,kBAAkB,KAAK,GAAG,GAAG,uBAAO,IAAI,MAAM,gCAAgC,MAAM,KAAK,IAAI,GAAG;CACpG,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,GAAG;AACnD;;;;;;AAOA,SAAgB,gBAAgB,KAAU,QAAiD;CACzF,MAAM,0BAA0B,IAAI,sBAAsB;EACxD;EACA;EAEA,MAAM,UAAmC;GACvC,MAAM,KAAK,WAAW;GACtB,MAAM,SAAS,UAAW,MAAM,eAAe;GAC/C,IAAI,CAAC,OAAO,MAAM,MAAM,IAAI,MAAM,2BAA2B;GAE7D,MAAM,qBAAqB;IACzB,KAAK,SAAS,KAAA;IACd,KAAK,UAAU,KAAA;IACf,KAAK,KAAK,cAAc,IAAI,IAAI,gBAAgB,CAAC;IACjD,OAAO,oBAAoB,0BAA0B,YAAY;GACnE;GACA,OAAO,iBAAiB,0BAA0B,YAAY;GAE9D,MAAM,SAAS,MAAM,OAAO,KAAK,QAAQ;GACzC,IAAI,QAAkB,CAAC;GACvB,IAAI;IACF,MAAM,WAAW,MAAM,OAAO,mBAAmB;IACjD,QAAQ,SAAS,KAAK,MAAM,EAAE,IAAI;IAClC,QAAQ,KAAK,qBAAqB,KAAK;IACvC,MAAM,UAAU,MAAM,YAAY,QAAQ;IAC1C,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,mCAAmC;IACjE,QAAQ,KAAK,oBAAoB,QAAQ,MAAM,MAAM,QAAQ,OAAO,IAAI;IACxE,QAAQ,OAAO,iBAAiB,+BAA+B,MAAM;KACnE,MAAM,QAAS,EAAE,OAA6C;KAC9D,IAAI,OAAO,KAAK,iBAAiB,KAAK;IACxC,CAAC;IACD,MAAM,QAAQ,OAAO,mBAAmB;IACxC,KAAK,SAAS;IACd,KAAK,UAAU;GACjB,SAAS,KAAK;IACZ,OAAO,WAAW;IAClB,MAAM,iBAAiB,KAAK,KAAK;GACnC;GAEA,MAAM,KAAK,iCAAiC,KAAK,WAAW,CAAC;GAC7D,MAAM,SAAyB;IAC7B,YAAY,OAAO;IACnB,QAAQ,KAAK,YAAY,iBAAiB,IAAI,cAAc;GAC9D;GACA,KAAK,KAAK,WAAW,IAAI,IAAI,aAAa,MAAM,CAAC;GACjD,OAAO;EACT;EAEA,cAAuB;GACrB,OAAO,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,KAAK;EACjC;EAEA,MAAM,aAA4B;GAChC,KAAK,cAAc;GACnB,KAAK,QAAQ,WAAW;GACxB,KAAK,SAAS,KAAA;GACd,KAAK,UAAU,KAAA;EACjB;EAEA,MAAM,QAAQ,MAAkB,OAAgC;GAC9D,MAAM,OAAO,YAAY;IACvB,MAAM,IAAI,KAAK,SAAS;IACxB,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,mBAAmB;IAC3C,MAAM,IAAI,MAAM,MAAM,KAAK,gBAAgB;IAC3C,MAAM,MAAM,KAAK,MAAM,CAAC,CAAC;IACzB,IAAI,EAAE,WAAW,sBAAsB,MAAM,EAAE,0BAA0B,GAAG;SACvE,MAAM,EAAE,uBAAuB,GAAG;IACvC,KAAK,KAAK,iBAAiB,IAAI,IAAI,mBAAmB,IAAI,CAAC;GAC7D;GACA,IAAI,OAAO,MAAM,KAAK;QACjB,MAAM,KAAK,MAAM,aAAa,IAAI;EACzC;EAEA,UAAU;GACR,OAAO;EACT;CACF;CACA,OAAO,IAAI,kBAAkB;AAC/B;;;ACtJA,IAAa,oBAAoB;;AAEjC,IAAa,kBAAkB;;AAkB/B,IAAa,aAAa,OAA4B,WAA8B,cACjF,SAAS,CAAC,MAAM,UAAU,MAAM,SAAS,UAAU,SAAS,OAAO,UAAU;;AAGhF,SAAgB,SAAiC,QAAa,MAAoC;CAChG,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,SAAS,EAAE,OAAO,MAAM,CAAC,CAAC,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,CAAC,KAAK;AACjH;;AAGA,IAAa,gBAA6B;CAAC;CAAI;CAAI;CAAI;CAAI;AAAE,CAAC,CAAC,KAAK,cAAc;CAAE,SAAS;CAAI;AAAS,EAAE;AAC5G,IAAa,uBAAkC;CAAE,SAAS;CAAI,UAAU;AAAG;;;;;;AAO3E,SAAgB,SAAS,MAAyB,OAAe,QAAgB,OAAsB,OAAA,KAAwC;CAC7I,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,OAAO,OAAO,SAAS,CAAC,CAAC;CACzD,MAAM,OAAqB,CAAC;CAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAC/B,MAAM,MAAM,IAAI,WAAW,OAAO,CAAC;EACnC,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,IAAI,SAAS,MAAM,KAC9C,IAAI,MAAM,IAAI,QAAQ,KAAK,KAAK,KAAK;GACnC,MAAM,MAAM,IAAI;GAChB,IAAI,OAAO,MAAM,UAAU,QAAQ,MAAM,MAAM,KAAK,QAAS,MAAM;EACrE;EAEF,KAAK,KAAK,GAAG;CACf;CACA,OAAO;AACT;AAkBA,IAAMA,WAAS,OAAe,IAAI,SAAS,MAAM,WAAW,GAAG,EAAE,CAAC;;;;;;;AAQlE,IAAsB,uBAAtB,MAAiE;CA0BnD;CACA;CA1BZ,4BAAoB,IAAI,IAAuC;CAC/D,YAAoB;;CAEpB,SAAmB;;CAEnB;;CAMA,WAAA;;CAEA,iBAAoD;;CAEpD,OAAiD;;CAEjD,aAAyD,KAAA;;CAKzD,MAAgB,YAA2B,CAAC;CAE5C,YACE,WACA,MACA,YACA;EAHU,KAAA,YAAA;EACA,KAAA,OAAA;EAGV,KAAK,QAAQ;EACb,UAAU,mBAAmB;GAC3B,KAAK,YAAY;GACjB,KAAK,KAAK,cAAc,CAAC,CAAC;EAC5B,CAAC;CACH;CAEA,MAAM,UAAU;EACd,KAAK,YAAY;EACjB,OAAO,EAAE,YAAY,KAAK,UAAU,KAAK;CAC3C;CAEA,MAAM,aAAa;EACjB,KAAK,YAAY;EACjB,MAAM,KAAK,UAAU,WAAW;CAClC;CAEA,cAAc;EACZ,OAAO,KAAK;CACd;CAEA,GAAG,OAAe,IAA0B;EAC1C,IAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG,KAAK,UAAU,IAAI,uBAAO,IAAI,IAAI,CAAC;EACnE,KAAK,UAAU,IAAI,KAAK,CAAC,CAAE,IAAI,EAAE;CACnC;CAEA,IAAI,OAAe,IAA0B;EAC3C,KAAK,UAAU,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE;CACtC;CAEA,KAAa,OAAe,GAAY;EACtC,KAAK,UAAU,IAAI,KAAK,CAAC,EAAE,SAAS,OAAO,GAAG,CAAC,CAAC;CAClD;CAEA,iBAAiB;EACf,OAAO,CAAC;CACV;CAEA,mBAAqC;EACnC,OAAO;GACL,OAAO,KAAK;GACZ,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;GACvC,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;GACzD,KAAK;GACL,iBAAiB,KAAK;GACtB,gBAAgB,KAAK;GACrB,YAAY;GACZ,YAAY,KAAK;GACjB,gBAAgB,KAAK,KAAK,KAAK,gBAAgB,CAAC;EAClD;CACF;CAEA,mBAAkC;EAEhC,OAAO;CACT;CAEA,cAAc;EACZ,OAAO,CAAC;CACV;CAEA,MAAM,gBAAgB,CAAC;CACvB,iBAAiB,CAAC;CAClB,gBAAgB,CAAC;;CAGjB,WAAW,QAAyC;EAClD,MAAM,MAAM,OAAO,WAAW,IAAI;EAClC,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,uBAAuB;EACjD,OAAO,KAAK,WAAW,IAAI,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,OAAO,OAAO,MAAM;CAC9G;;CAGA,MAAM,KAAK,OAAmB,QAAiD;EAC7E,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,SAAU;GAClC,OAAO,KAAK,QAAQ,MAAMA,QAAM,KAAK,KAAK,OAAO;GACjD,MAAM,OAAO,MAAM,SAAS,GAAG,IAAI,KAAK,KAAK;GAC7C,IAAI;IACF,MAAM,KAAK,UAAU,MAAM,IAAI;GACjC,SAAS,KAAK;IACZ,IAAI,KAAK,SAAS,IAAI,MAAM;IAC5B,KAAK,QAAQ,KAAK,IAAI,IAAI,KAAK,SAAS,CAAC;IACzC;GACF;GACA,KAAK,KAAK;GACV,SAAS,IAAI,MAAM,MAAM;GACzB,MAAMA,QAAM,KAAK,KAAK,OAAO;EAC/B;CACF;CAEA,WAAW;EACT,UAAU,YAAY;EACtB,eAAe,OAAsB,YAAoD;GACvF,MAAM,UAAU,OAAO,QAAQ,WAAW,CAAC;GAC3C,MAAM,YAAY,OAAO,QAAQ,aAAa,CAAC;GAC/C,MAAM,UAAU,QAAQ,YAAY,KAAA,IAAY,KAAA,IAAY,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,OAAO,CAAC,CAAC;GAE3G,IAAI,UAAU;GACd,OAAO;IACL,WAAW,YAAY,CAAC;IACxB,WAAW,OAAO,OAAO,WAAW,MAAM;KACxC,MAAM,cAAc,KAAK,IAAI,SAAS,OAAO,QAAQ,cAAc,OAAO,CAAC;KAE3E,IAAI,QAAQ;KACZ,MAAM,KAAK,KAAK,KAAK,IAAI,OAAuB,SAAS,KAAK,IAAI,GAAG,QAAQ,GAAG,WAAW,OAAO,IAAI,UAAU;MAC9G,MAAM,UAAU,KAAK,MAAM,QAAQ,GAAG;MACtC,IAAI,YAAY,OAAO;MACvB,QAAQ;MACR,KAAK,KAAK,iBAAiB;OAAE,MAAM;OAAS,YAAY,MAAM;OAAG,mBAAmB;OAAG,kBAAkB;OAAG;MAAQ,CAAC;KACvH,CAAC;KACD,MAAM,KAAK,UAAU;KACrB,WAAW,KAAK,IAAI,GAAG,QAAQ;KAC/B,KAAK,KAAK,iBAAiB;MAAE,MAAM;MAAS,YAAY,MAAM;MAAG,mBAAmB;MAAK,kBAAkB;KAAI,CAAC;IAClH;IACA,iBAAiB,YAAY,CAAC;IAC9B,UAAU,YAAY;GACxB;EACF;CACF;AACF;;AAGA,eAAsB,aACpB,QACA,QACA,KAC2B;CAC3B,MAAM,UAAU,MAAM,OAAO,kBAAkB,IAAI,OAAO;CAC1D,MAAM,KAAK,MAAM,QAAQ,kBAAkB,IAAI,KAAK;CACpD,MAAM,SAAsC,CAAC;CAC7C,IAAI,IAAI,QAAQ;EACd,MAAM,KAAK,MAAM,QAAQ,kBAAkB,IAAI,MAAM;EACrD,GAAG,iBAAiB,+BAA+B,MAAM;GACvD,MAAM,IAAK,EAAE,OAA6C;GAC1D,IAAI,GAAG,OAAO,SAAS,OAAO,GAAG,IAAI,WAAW,EAAE,OAAO,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;EAC7G,CAAC;EACD,MAAM,GAAG,mBAAmB;CAC9B;CACA,OAAO;EACL,MAAM,OAAO;EACb,OAAO,OAAO,UAAU;GACtB,MAAM,MAAM,MAAM,MAAM,CAAC,CAAC;GAC1B,IAAI,GAAG,WAAW,sBAAsB,MAAM,GAAG,0BAA0B,GAAG;QACzE,MAAM,GAAG,uBAAuB,GAAG;EAC1C;EACA,WAAW,OAAO,OAAO,KAAK,EAAE;EAChC,eAAe,OAAO,OAAO,iBAAiB,0BAA0B,EAAE;EAC1E,YAAY,YAAY,OAAO,WAAW;CAC5C;AACF;;;;;;;;;;AClQA,IAAa,cAAc;AAC3B,IAAa,SAAS;AACtB,IAAa,SAAS;AAEtB,IAAM,YAAA;;;;;;AAgBN,IAAa,aAAyB;CACpC;EAAE,QAAQ;EAAQ,YAAY;CAAO;CACrC;EAAE,QAAQ;EAAQ,YAAY;CAAO;CACrC;EAAE,QAAQ;EAAQ,YAAY;EAAQ,SAAS;CAAK;CACpD;EAAE,QAAQ;EAAQ,YAAY;EAAQ,cAAc;CAAK;CACzD;EAAE,QAAQ;EAAQ,YAAY;EAAQ,cAAc;CAAK;CACzD;EAAE,QAAQ;EAAQ,YAAY;EAAQ,cAAc;CAAK;CACzD;EAAE,QAAQ;EAAQ,YAAY;EAAU,cAAc;CAAK;CAC3D;EAAE,QAAQ;EAAQ,YAAY;EAAU,cAAc;CAAK;CAC3D;EAAE,QAAQ;EAAQ,YAAY;CAAS;CACvC;EAAE,QAAQ;EAAQ,YAAY;CAAS;CACvC;EAAE,QAAQ;EAAQ,YAAY;CAAS;CACvC;EAAE,QAAQ;EAAQ,YAAY;CAAS;CACvC;EAAE,QAAQ;EAAQ,YAAY;CAAS;CACvC;EAAE,QAAQ;EAAS,YAAY;CAAS;AAC1C;;AAGA,IAAa,kBAAoD;CAC/D;EAAE,MAAM;EAAS,MAAM;CAAiB;CACxC;EAAE,MAAM;EAAY,MAAM;CAAqB;CAC/C;EAAE,MAAM;EAAqB,MAAM;CAAqB;AAC1D;AAEA,IAAa,eAAe,SAA8C,SAAS,YAAY,IAAI;AAGnG,IAAM,YAAY,MAAM,KAAK,EAAE,QAAQ,IAAI,IAAI,GAAG,MAAM;CACtD,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,IAAI,OAAS,KAAK,IAAK,KAAQ,MAAQ,KAAK,IAAK;CACjF,OAAO;AACT,CAAC;AACD,IAAa,QAAQ,SAAoC;CACvD,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,IAAI,WAAW,IAAI,KAAK,MAAM;CACpE,OAAO;AACT;AAEA,SAAgB,OAAO,KAAa,SAAsC;CACxE,IAAI,QAAQ,SAAS,KAAM,MAAM,IAAI,MAAM,kBAAkB;CAC7D,OAAO;EAAC;EAAM;EAAM;EAAK;EAAM,QAAQ;EAAQ;EAAM,GAAG,MAAM,KAAK,OAAO;EAAG,KAAK,OAAO;EAAG;CAAI;AAClG;AAEA,IAAM,OAAO,MAAc,CAAC,IAAI,KAAO,KAAK,IAAK,GAAI;AAErD,IAAM,MAAM;CACV,SAAS;CACT,MAAM;CACN,KAAK;CACL,OAAO;CACP,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,aAAa;AACf;AAEA,IAAM,gBAAgB;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI;AACvF,IAAM,cAAc;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI;;AAGrF,IAAa,aAAa;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI;AAC/E,IAAa,cAAc;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI;;AAGhF,IAAM,SAAS;CAAC;CAAQ;CAAQ;CAAQ;CAAQ;AAAM;;AAEtD,IAAM,aAAa;;AAEnB,IAAM,cAAc;;AAGpB,IAAaC,gBAAc,MAAyB,OAAe,WAAiC,SAAS,MAAM,OAAO,QAAQ,KAAK;;;;;;AAOvI,SAAgB,SAAS,MAAoB,EAAE,SAAS,QAAQ,OAAO,WAAsG;CAC3K,MAAM,MAAgB,CAAC;CACvB,MAAM,QAAQ,KAAa,YAA+B,IAAI,KAAK,GAAG,OAAO,KAAK,OAAO,CAAC;CAC1F,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC;CACnB,IAAI,OAAO,SAAS,IAAI,KAAK,EAAI;CACjC,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC;CACnB,KAAK,IAAI,SAAS,CAAC,EAAE,CAAC;CACtB,KAAK,IAAI,OAAO,CAAC,EAAE,CAAC;CACpB,KAAK,IAAI,QAAQ,IAAI,OAAO,KAAK,IAAI,OAAO,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;CAC3F,KAAK,IAAI,aAAa,CAAC,CAAC,CAAC;CACzB,KAAK,IAAI,QAAQ,CAAC,CAAC,CAAC;CACpB,KAAK,IAAI,SAAS,aAAa;CAC/B,MAAM,QAAQ,IAAI,WAAW,SAAS;CACtC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAC/B,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,WAAW,cAAc,KAAK,KAAK,IAAI,KAAK,KAAK;EACjF,KAAK,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,GAAG;CAC3C;CACA,KAAK,IAAI,SAAS,WAAW;CAC7B,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC;CACnB,IAAI,YAAY,KAAA,GAAW,IAAI,KAAK,GAAG,QAAQ,SAAS,KAAK,CAAC;MACzD,IAAI,OAAO,cAAc,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK,KAAK,IAAI,KAAK,KAAK;MACjF,KAAK,IAAI,MAAM,IAAI,UAAU,CAAC;CACnC,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC;CACnB,OAAO,WAAW,KAAK,GAAG;AAC5B;;AAGA,SAAgB,QAAQ,MAAc,OAAoC;CACxE,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CACnC,IAAI,CAAC,GAAG,uBAAO,IAAI,WAAW;CAC9B,IAAI,OAAO,GAAG,OAAO,WAAW,KAAK,OAAO,IAAI,SAAS,IAAI,CAAC,CAAC,CAAC;CAChE,IAAI,CAAC,OAAO,cAAc,OAAO,WAAW,KAAK,OAAO,IAAI,MAAM,IAAI,CAAC,CAAC,CAAC;CACzE,MAAM,QAAQ,IAAI,WAAW,SAAS;CACtC,OAAO,WAAW,KAAK,MAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACvF;;AAGA,IAAa,mBAAb,cAAsC,qBAAqB;CAM9C;CALX,gBAAmC,OAAO;CAC1C;CAEA,YACE,WACA,OACA,OAAa;EAAE,SAAS;EAAI,SAAS;CAAI,GACzC;EACA,MAAM,WAAW,MAAM,GAAG;EAHjB,KAAA,QAAA;EAIT,KAAK,YAAY,UAAU,OAAO,WAAW,aAAa;EAC1D,UAAU,UAAU,SAAS;GAC3B,IAAI,KAAK,MAAM,UAAU,GAAG,KAAK,SAAS;QACrC,IAAI,KAAK,MAAM,WAAW,GAAG,KAAK,SAAS;EAClD,CAAC;CACH;CAEA,WAAqB,MAAyB,OAAe,QAAgB;EAC3E,OAAOA,aAAW,MAAM,OAAO,MAAM;CACvC;CAEA,IAAc,MAAoB,SAAiB,QAAgB,YAAoB,SAAkB;EACvG,OAAO,SAAS,MAAM;GAAE;GAAS;GAAQ,OAAO,KAAK;GAAO;EAAQ,CAAC;CACvE;CAEA,YAAqB;;CAGrB,MAAM,UAAU,MAA6B;EAC3C,MAAM,KAAK,KAAK,QAAQ,MAAM,KAAK,KAAK,CAAC;CAC3C;AACF;AAEA,IAAM,QAAQ,GAAsB,MAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,CAAC;;;;;;;AC7KzG,IAAa,iBAAiB;AAC9B,IAAa,eAAe;;AAE5B,IAAa,qBAAqB;AAClC,IAAa,mBAAmB;;;;;;;;;;AAYhC,IAAa,gBAAgC;CAC3C;EAAE,QAAQ;EAAS,YAAY;EAAU,MAAM;CAAsB;CACrE;EAAE,QAAQ;EAAU,YAAY;EAAQ,MAAM;CAA2C;CACzF;EAAE,QAAQ;EAAQ,YAAY;CAAS;CACvC;EAAE,QAAQ;EAAS,YAAY;CAAS;CACxC;EAAE,QAAQ;EAAU,YAAY;EAAQ,MAAM;CAA2C;CACzF;EAAE,QAAQ;EAAS,YAAY;CAAS;CACxC;EAAE,QAAQ;EAAS,YAAY;EAAU,MAAM;EAAS,UAAU;CAAI;CACtE;EAAE,QAAQ;EAAS,YAAY;EAAU,MAAM;EAAS,UAAU;CAAI;CACtE;EAAE,QAAQ;EAAU,YAAY;EAAQ,OAAO;CAAiB;CAChE;EAAE,QAAQ;EAAS,YAAY;EAAU,OAAO;CAAiB;CACjE;EAAE,QAAQ;EAAU,YAAY;EAAU,OAAO;CAAU;CAC3D;EAAE,QAAQ;EAAU,YAAY;EAAU,OAAO;CAAU;CAC3D;EAAE,QAAQ;EAAU,YAAY;CAAS;CACzC;EAAE,QAAQ;EAAS,YAAY;EAAQ,OAAO;EAAiB,MAAM;CAA+B;CACpG;EAAE,QAAQ;EAAU,YAAY;EAAU,OAAO;EAAiB,MAAM;EAAS,UAAU;CAAI;CAC/F;EAAE,QAAQ;EAAS,YAAY;EAAU,OAAO;CAAS;CACzD;EAAE,QAAQ;EAAQ,YAAY;EAAU,OAAO;CAAoB;CACnE;EAAE,QAAQ;EAAY,YAAY;EAAU,OAAO;EAAqB,MAAM;EAAS,UAAU;CAAI;CACrG;EAAE,QAAQ;EAAO,YAAY;EAAU,OAAO;EAAmB,SAAS;CAAK;CAE/E;EAAE,QAAQ;EAAW,YAAY;EAAU,OAAO;EAAqC,MAAM;EAAqB,SAAS;CAAK;CAChI;EAAE,QAAQ;EAAW,YAAY;EAAU,MAAM;EAAqB,SAAS;CAAK;CACpF;EAAE,QAAQ;EAAqB,YAAY;EAAU,MAAM;EAAiD,SAAS;CAAK;CAC1H;EAAE,QAAQ;EAAa,YAAY;EAAU,MAAM;EAAqB,SAAS;CAAK;AACxF;AAEA,IAAa,kBAAkB,SAAkD,SAAS,eAAe,IAAI;;AAG7G,IAAM,OAAO;;AAEb,IAAM,WAAW;;AAEjB,IAAM,aAAa;;AAGnB,IAAaC,gBAAc,MAAyB,OAAe,QAAgB,OAAA,QACjF,SAAS,MAAM,OAAO,QAAQ,OAAO,IAAI;;;;;;AAO3C,SAAgB,UAAU,MAAoB,QAAgB,SAA8B;CAC1F,MAAM,MAAgB,CAAC,IAAM,EAAI;CACjC,MAAM,WAAW,KAAK,EAAE,EAAE,UAAA;CAC1B,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAC/B,IAAI,IAAI,GAAG,IAAI,KAAK,GAAG,WAAW,WAAW,QAAQ,CAAC;EACtD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,MAAM;GAC1C,MAAM,OAAO,KAAK,MAAM,GAAG,IAAI,IAAI;GACnC,IAAI,KAAK,IAAM,KAAM,IAAM,GAAM,WAAW,KAAM,YAAY,GAAG,KAAK,SAAS,KAAM,KAAK,UAAU,CAAC;GACrG,KAAK,MAAM,OAAO,MAAM,IAAI,KAAK,GAAG,GAAG;EACzC;CACF;CACA,IAAI,YAAY,KAAA,GAAW,IAAI,KAAK,IAAM,KAAM,UAAU;MACrD,IAAI,KAAK,GAAG,WAAW,OAAO,CAAC;CACpC,OAAO,WAAW,KAAK,GAAG;AAC5B;;;;;;;AAQA,SAAgB,WAAW,MAA0B;CACnD,MAAM,MAAgB,CAAC;CACvB,KAAK,IAAI,OAAO,KAAK,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,KAAK,IAAI,KAAK,IAAM,IAAM,KAAK,IAAI,KAAK,IAAI,GAAG,EAAI;CACvG,IAAI,IAAI,QAAQ,IAAI,KAAK,IAAM,EAAI;CACnC,OAAO,WAAW,KAAK,GAAG;AAC5B;;;;;;AAOA,IAAa,sBAAb,cAAyC,qBAAqB;CAOjD;CANX,gBAAmC;CACnC;CACA;CAEA,YACE,WACA,OACA,OAAa;EAAE,SAAS;EAAI,SAAS;CAAI,GACzC;EACA,MAAM,WAAW,MAAM,GAAG;EAHjB,KAAA,QAAA;EAIT,KAAK,YAAY,UAAU,OAAO,WAAW,iBAAiB;EAC9D,KAAK,WAAW,OAAO,YAAA;CACzB;CAEA,WAAqB,MAAyB,OAAe,QAAgB;EAC3E,OAAOA,aAAW,MAAM,OAAO,QAAQ,KAAK,QAAQ;CACtD;CAEA,IAAc,MAAoB,UAAkB,QAAgB,YAAoB,SAAkB;EACxG,OAAO,UAAU,MAAM,QAAQ,OAAO;CACxC;;CAGA,MAAM,UAAU,MAA6B;EAC3C,IAAI,OAAO,GAAG,MAAM,IAAI,MAAM,yCAAyC;EACvE,MAAM,KAAK,KAAK,WAAW,IAAI,CAAC;CAClC;AACF;;ACxHA,IAAa,eAAe;AAC5B,IAAa,aAAa;AAC1B,IAAa,cAAc;AAI3B,IAAM,cAAc;;;;;AAMpB,IAAa,cAA8B,CACzC;CAAE,QAAQ;CAAU,YAAY;CAAU,MAAM;AAAgB,GAChE;CAAE,QAAQ;CAAW,YAAY;CAAU,OAAO;CAAoB,UAAU;AAAI,CACtF;AAEA,IAAa,gBAAgB,SAAkD,SAAS,aAAa,IAAI;;AAGzG,IAAa,aAA0B;CACrC,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,EAAE;CACP,CAAC,IAAI,GAAG;AACV,CAAC,CAAC,KAAK,CAAC,SAAS,eAAe;CAAE;CAAS;AAAS,EAAE;;AAGtD,IAAM,iBAAiB;;AAEvB,IAAM,SAAS;;AAGf,IAAM,aAAa;CAAE,WAAW;CAAG,YAAY;AAAE;;AAGjD,IAAa,eAAe;CAAC;CAAM;CAAM;AAAI;AAC7C,IAAM,gBAAoC;CACxC,CAAC,GAAM,uBAAuB;CAC9B,CAAC,GAAM,WAAW;CAClB,CAAC,GAAM,cAAc;CACrB,CAAC,GAAM,eAAe;AACxB;;AAGA,IAAM,KAAK,CAAC,IAAM,EAAI;;AAEtB,IAAM,iBAAiB;;AAEvB,IAAM,oBAAoB;;;;;AAM1B,SAAgB,WAAW,MAAyB,OAAe,QAA8B;CAC/F,MAAM,QAAQ,KAAK,KAAK,QAAQ,CAAC;CACjC,MAAM,OAAqB,CAAC;CAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAC/B,MAAM,MAAM,IAAI,WAAW,KAAK,CAAC,CAAC,KAAK,GAAI;EAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,IAAI,MAAM,IAAI,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,MAAM,EAAE,QAAS,IAAI;EACnG,KAAK,KAAK,GAAG;CACf;CACA,OAAO;AACT;AAEA,IAAM,SAAS,MAAc,MAAM,KAAK,IAAI,MAAM,EAAE,WAAW,CAAC,CAAC;AACjE,IAAM,MAAM,SAAiB,GAAG,KAAK,MAAO,OAAO,cAAe,EAAE,IAAI,GAAG;;AAG3E,SAAS,aAAa,WAA2B;CAC/C,IAAI,cAAc,WAAW,YAAY,OAAO;CAChD,IAAI,cAAc,WAAW,WAAW,OAAO,SAAS,OAAO;CAC/D,OAAO,OAAO,OAAO;AACvB;;AAGA,SAAgB,QAAQ,MAAoB,EAAE,SAAS,QAAQ,aAAiF;CAC9I,MAAM,QAAQ,KAAK,EAAE,EAAE,UAAU;CACjC,MAAM,SAAS,KAAK;CAQpB,MAAM,MAAM,MAAM;EANhB,QAAQ,GAAG,QAAQ,CAAC,EAAE,GAAG,GAAG,MAAM;EAClC,aAAa,SAAS;EACtB;EACA,WAAW,KAAK,IAAI,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,CAAC;EACpE;CAEgB,CAAA,CAAK,KAAK,MAAM,GAAG,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,IAAI,cAAc,MAAM,GAAG,OAAO,IAAI;CAC3F,KAAK,MAAM,OAAO,MAAM,KAAK,MAAM,KAAK,KAAK,IAAI,KAAK,CAAC;CACvD,IAAI,KAAK,GAAG,MAAM,aAAa,KAAK,IAAI,GAAG,MAAM,EAAE,OAAO,CAAC;CAC3D,OAAO,WAAW,KAAK,GAAG;AAC5B;;;;;;AAOA,IAAa,oBAAb,cAAuC,qBAAqB;CAa/C;CAED;CAdV,gBAAmC;CACnC;CACA;CACA,OAA0B,KAAA;CAC1B,aAAgC;CAEhC,MAAc;CACd,SAAgC;CAChC,OAAoC;CAEpC,YACE,WACA,OACA,OAAa;EAAE,SAAS;EAAI,SAAS;CAAI,GACzC,eAAuB,gBACvB;EACA,MAAM,WAAW,MAAM,GAAG;EAJjB,KAAA,QAAA;EAED,KAAA,eAAA;EAGR,KAAK,YAAY,UAAU,OAAO,WAAW,eAAe;EAC5D,KAAK,WAAW,OAAO,YAAA;EACvB,UAAU,UAAU,SAAS;GAC3B,IAAI,KAAK,UAAU,KAAK,KAAK,OAAO,GAAG,MAAM,KAAK,OAAO,GAAG,IAAI,KAAK;QAChE,IAAI,KAAK,WAAW,GAAG,KAAK,SAAS,KAAK;QAC1C;GACL,KAAK,OAAO;EACd,CAAC;CACH;CAEA,WAAqB,MAAyB,OAAe,QAAgB;EAC3E,OAAO,WAAW,MAAM,OAAO,MAAM;CACvC;;CAGA,UAAU;EACR,OAAO,WAAW,KAAK,UAAU,QAAQ,KAAK;CAChD;CAEA,IAAc,MAAoB,SAAiB,QAAgB,WAAmB;EACpF,KAAK,MAAM;EACX,OAAO,QAAQ,MAAM;GAAE;GAAS;GAAQ;EAAU,CAAC;CACrD;;CAGA,MAAc,MAAM,MAAqB,IAAY;EACnD,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,OAAO,CAAC,KAAK,KAAK,KAAK,IAAI,IAAI,UAC7B,MAAM,IAAI,SAAe,YAAY;GACnC,MAAM,IAAI,WAAW,SAAS,WAAW,KAAK,IAAI,CAAC;GACnD,KAAK,cAAc,aAAa,CAAC,GAAG,QAAQ;EAC9C,CAAC;EAEH,KAAK,OAAO;CACd;;;;;;CAOA,MAAgB,YAAY;EAC1B,MAAM,KAAK,YAAY,KAAK,MAAM,GAAG,KAAK,YAAY;EACtD,IAAI,KAAK,MAAM,GAAG;EAClB,KAAK,SAAS;EACd,MAAM,KAAK,KAAK,WAAW,KAAK,YAAY,CAAC;EAC7C,MAAM,KAAK,YAAY,KAAK,WAAW,MAAM,iBAAiB;EAC9D,MAAM,QAAQ,cAAc,MAAM,CAAC,UAAU,KAAK,UAAU,KAAK,GAAG;EACpE,IAAI,OAAO,MAAM,IAAI,MAAM,MAAM,EAAE;EACnC,QAAQ,KAAK,gDAAgD;CAC/D;AACF;;;;;;;;;ACxKA,IAAa,kBAAkC;CAC7C,GAAG,cAAc,KAAK,WAAW;EAAE,QAAQ;EAAmB;CAAM,EAAE;CACtE,GAAG,YAAY,KAAK,WAAW;EAAE,QAAQ;EAAiB;CAAM,EAAE;CAClE,GAAG,WAAW,KAAK,WAAW;EAAE,QAAQ;EAAgB;CAAM,EAAE;AAClE;;AAGA,SAAgB,gBAAgB,MAA+C;CAC7E,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,OAA4B;CAChC,KAAK,MAAM,SAAS,iBAClB,IAAI,KAAK,WAAW,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,OAAO,UAAU,MAAM,MAAM,OAAO,UAAU,IAAI,OAAO;CAElH,OAAO;AACT;;AASA,IAAa,gBAA+B;CAC1C;EAAE,IAAI;EAAa,MAAM;EAA0B,QAAQ;EAAU,OAAO;GAAE,QAAQ;GAAI,YAAY;GAAU,SAAS;GAAM,UAAA;EAA4B;CAAE;CAC7J;EAAE,IAAI;EAAa,MAAM;EAA0B,QAAQ;EAAU,OAAO;GAAE,QAAQ;GAAI,YAAY;GAAU,SAAS;GAAM,UAAU;EAAI;CAAE;CAC/I;EAAE,IAAI;EAAQ,MAAM;EAAwB,QAAQ;EAAQ,OAAO;GAAE,QAAQ;GAAI,YAAY;GAAU,SAAS;GAAM,UAAA;EAAyB;CAAE;CACjJ;EAAE,IAAI;EAAO,MAAM;EAAe,QAAQ;EAAO,OAAO;GAAE,QAAQ;GAAI,YAAY;GAAU,SAAS;EAAK;CAAE;AAC9G;AAEA,IAAa,mBAAmB,OAAsD,cAAc,MAAM,MAAM,EAAE,OAAO,EAAE,KAAK;;;;;;ACnChI,eAAe,aAAa,QAAmC,UAAwE;CACrI,KAAK,MAAM,WAAW,oBAAoB;EACxC,IAAI,CAAC,SAAS,SAAS,OAAO,KAAK,YAAA,wCAAyB;EAK5D,MAAM,SAAQ,MAJM,OACjB,kBAAkB,OAAO,CAAC,CAC1B,MAAM,MAAM,EAAE,mBAAmB,CAAC,CAAC,CACnC,YAAY,CAAC,CAAC,EAAA,CACG,MAAM,MAAM,EAAE,WAAW,wBAAwB,EAAE,WAAW,KAAK;EACvF,IAAI,OAAO,OAAO;GAAE;GAAS,OAAO,MAAM;EAAK;CACjD;CACA,OAAO;AACT;AAEA,IAAM,qBAAoD;CAAE,QAAQ;CAAmB,MAAM;CAAiB,KAAK;AAAc;;AAGjI,eAAe,cAAc,QAAyB,QAAmC,UAAoB,EAAE,QAAQ,SAAmD;CACxK,IAAI,WAAW,OACb,OAAO,SAAS,SAAA,sCAAoB,IAAI,IAAI,iBAAiB,MAAM,aAAa,QAAQ,QAAQ;EAAE,SAAS;EAAa,OAAO;EAAQ,QAAQ;CAAO,CAAC,GAAG,KAAK,IAAI;CAErK,IAAI,WAAW,QACb,OAAO,SAAS,SAAA,sCAAqB,IAAI,IAAI,kBAAkB,MAAM,aAAa,QAAQ,QAAQ;EAAE,SAAS;EAAc,OAAO;EAAY,QAAQ;CAAY,CAAC,GAAG,KAAK,IAAI;CAEjL,MAAM,UAAU,SAAS,SAAA,sCAAuB,IAC5C;EAAE,SAAS;EAAgB,OAAO;CAAa,IAC/C,SAAS,SAAA,sCAA2B,IAClC;EAAE,SAAS;EAAoB,OAAO;CAAiB,IACvD,MAAM,aAAa,QAAQ,QAAQ;CACzC,OAAO,UAAU,IAAI,oBAAoB,MAAM,aAAa,QAAQ,QAAQ,OAAO,GAAG,KAAK,IAAI;AACjG;;AAGA,SAAS,aAAa,KAAU,MAAmC;CACjE,MAAM,QAAQ,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,YAAY;CAClD,OAAO,CAAC,CAAC,SAAS,IAAI,cAAc,MAAM,MAAM,EAAE,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,KAAK;AACjF;;;;;;;;;;AAWA,IAAa,mBAAb,MAAoD;CAG9B;CAFpB,QAAmC;CAEnC,YAAY,KAAiC;EAAzB,KAAA,MAAA;CAA0B;CAE9C,IAAY,SAAqB;EAC/B,IAAI,CAAC,KAAK,OAAO,MAAM,IAAI,MAAM,eAAe;EAChD,OAAO,KAAK;CACd;;CAGA,WAAuC;CAEvC,eAAe,QAAuB;EACpC,KAAK,WAAW,gBAAgB,MAAM;CACxC;CAEA,MAAM,UAAU;EAEd,MAAM,SAAS,MAAM,eACnB,gBAAgB,KAAK,MAAM,EAAE,MAAM,MAAM,GACzC,CAAC,CAAC,KAAK,QACT;EACA,MAAM,QAAQ,gBAAgB,OAAO,IAAI;EAEzC,MAAM,WAAW,CAAC,SAAS,KAAK,YAAY,CAAC,aAAa,MAAM,KAAK,IAAI,GAAG,OAAO,IAAI,IAAI,KAAK,WAAW;EAC3G,MAAM,QAAQ,SAAS;EACvB,IAAI,SAAS,OAAO,MAAM;GACxB,MAAM,SAAS,MAAM,OAAO,KAAK,QAAQ;GACzC,MAAM,YAAY,MAAM,OAAO,mBAAmB,CAAC,CAAC,YAAY,CAAC,CAAC,EAAA,CAAG,KAAK,MAAM,EAAE,IAAI;GACtF,KAAK,QAAQ,MAAM,cAAc,QAAQ,QAAQ,UAAU,KAAK;GAChE,IAAI,KAAK,OAAO,OAAO,KAAK,MAAM,QAAQ;GAE1C,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,GAAG,OAAO,QAAQ,eAAe,oBAAoB,mBAAmB,MAAM,QAAQ,SAAS;EAC7H;EACA,MAAM,MAAM,MAAM,KAAK,IAAI;EAC3B,KAAK,QAAQ,gBAAgB,KAAK,MAAM;EACxC,IAAI;GACF,OAAO,MAAM,KAAK,MAAM,QAAQ;EAClC,SAAS,KAAK;GACZ,IAAI,aAAa,KAAK,OAAO,IAAI,GAAG,MAAM;GAC1C,MAAM,IAAI,MAAM,GAAG,OAAO,QAAQ,eAAe,qGAAqG;EACxJ;CACF;CAEA,MAAM,aAAa;EACjB,MAAM,KAAK,OAAO,WAAW;CAC/B;CAEA,cAAc;EACZ,OAAO,CAAC,CAAC,KAAK,OAAO,YAAY;CACnC;CAEA,GAAG,OAAe,IAAsB;EACtC,OAAO,KAAK,OAAO,GAAG,OAAO,EAAE;CACjC;CAEA,IAAI,OAAe,IAAsB;EACvC,OAAO,KAAK,OAAO,IAAI,OAAO,EAAE;CAClC;CAEA,iBAAiB;EACf,OAAO,KAAK,OAAO,eAAe,KAAK,CAAC;CAC1C;CAEA,mBAAmB;EACjB,OAAO,KAAK,OAAO,iBAAiB;CACtC;CAEA,mBAAmB;EACjB,OAAO,KAAK,OAAO,iBAAiB;CACtC;CAEA,cAAc;EACZ,OAAO,KAAK,OAAO,YAAY,KAAK,CAAC;CACvC;CAEA,gBAAgB;EACd,OAAO,KAAK,OAAO,cAAc;CACnC;CAEA,iBAAiB;EACf,KAAK,OAAO,eAAe;CAC7B;CAEA,gBAAgB;EACd,KAAK,OAAO,cAAc;CAC5B;CAEA,qBAAqB;EACnB,OAAO,CAAC,CAAC,KAAK,OAAO,qBAAqB;CAC5C;CAEA,IAAI,WAAW;EACb,OAAO,KAAK,OAAO;CACrB;CAEA,IAAI,aAAa;EACf,OAAO,KAAK,OAAO,YAAY,KAAK,KAAK,KAAK;CAChD;CAEA,IAAI,UAAU;EACZ,OAAO,KAAK,OAAO,SAAS,KAAK,KAAK,KAAK;CAC7C;CAEA,IAAI,YAAY;EACd,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,KAAK;CAC/C;CAEA,IAAI,YAAY;EACd,OAAO,KAAK,OAAO;CACrB;AACF;;;ACtKA,IAAa,aAAb,MAA8C;CAOxB;CANpB,YAAY;CACZ,UAAsE,CAAC;CACvE,gBAA+B;CAC/B,4BAAoB,IAAI,IAA2B;;CAGnD,YAAY,OAAwC,WAAW;EAA3C,KAAA,OAAA;CAA4C;CAEhE,MAAM,UAAU;EACd,KAAK,YAAY;EACjB,KAAK,KAAK,WAAW,EAAE,MAAM,EAAE,YAAY,eAAe,EAAE,CAAC;EAC7D,OAAO,EAAE,YAAY,eAAe;CACtC;CACA,MAAM,aAAa;EACjB,KAAK,YAAY;EACjB,KAAK,KAAK,cAAc,CAAC,CAAC;CAC5B;CACA,cAAc;EACZ,OAAO,KAAK;CACd;CACA,GAAG,OAAe,IAAc;EAC9B,IAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG,KAAK,UAAU,IAAI,uBAAO,IAAI,IAAI,CAAC;EACnE,KAAK,UAAU,IAAI,KAAK,CAAC,CAAE,IAAI,EAAE;EACjC,OAAO;CACT;CACA,IAAI,OAAe,IAAc;EAC/B,KAAK,UAAU,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE;EACpC,OAAO;CACT;CACA,KAAK,OAAe,GAAY;EAC9B,KAAK,UAAU,IAAI,KAAK,CAAC,EAAE,SAAS,OAAO,GAAG,CAAC,CAAC;CAClD;CACA,kBAAkB;CAClB,iBAAiB;EACf,OAAO;GAAE,iBAAiB;GAAI,SAAS;GAAK,gBAAgB;GAAI,iBAAiB,KAAK;EAAgB;CACxG;CACA,mBAAmB;EACjB,OAAO,KAAK,QAAQ,KAAA;CACtB;CACA,mBAAmB;EACjB,OAAO,KAAK,OAAQ,WAAqB,KAAA;CAC3C;;CAEA,mBAAmB;CAEnB,cAAc;EACZ,OAAO;GACL,eAAe;IAAE,YAAY;IAAM,SAAS;IAAiB,UAAU;IAAK,WAAW;IAAI,iBAAiB;GAAE;GAC9G,WAAW,KAAK,mBAAmB;IAAE,OAAO;IAAM,YAAY;IAAI,aAAa;GAAG,IAAI,KAAA;EACxF;CACF;CACA,MAAM,gBAAgB;EACpB,OAAO,KAAK,YAAY;CAC1B;CACA,iBAAiB;EACf,KAAK,YAAY;EACjB,KAAK,KAAK,aAAa,EAAE,MAAM;GAAE,iBAAiB;GAAI,WAAW;GAAM,eAAe;EAAK,EAAE,CAAC;CAChG;CACA,YAAY;CACZ,gBAAgB;EACd,KAAK,YAAY;CACnB;;CAEA,kBAAkB;CAClB,WAAW;EACT,UAAU,YAAY;GACpB,IAAI,KAAK,kBAAkB,GAAG;IAC5B,KAAK;IACL,OAAO;GACT;GACA,OAAO;EACT;EACA,eAAe,OAAe,YAAoD;GAChF,MAAM,QAAQ,OAAO,QAAQ,cAAc,CAAC;GAC5C,OAAO;IACL,WAAW,YAAY;KACrB,IAAI,KAAK,eAAe;MACtB,MAAM,MAAM,KAAK;MACjB,KAAK,gBAAgB;MACrB,MAAM,IAAI,MAAM,GAAG;KACrB;IACF;IACA,WAAW,OAAO,OAAO,WAAW,MAAM;KACxC,KAAK,QAAQ,KAAK;MAAE;MAAO;MAAU,WAAW,KAAK;KAAU,CAAC;IAClE;IACA,iBAAiB,YAAY;KAC3B,KAAK,IAAI,OAAO,GAAG,QAAQ,OAAO,QAChC,KAAK,KAAK,iBAAiB;MAAE;MAAM,YAAY;MAAO,mBAAmB;MAAK,kBAAkB;KAAI,CAAC;IAEzG;IACA,UAAU,YAAY;GACxB;EACF;CACF;AACF;AAEA,IAAa,YAAY;CACvB,OAAO;CACP,IAAI,CAAC,GAAG;CACR,KAAK;CACL,gBAAgB;CAChB,iBAAiB;CACjB,YAAY,CAAC,CAAC;CACd,YAAY;CACZ,YAAY;CACZ,gBAAgB;AAClB;;AC3GA,IAAa,cAAwE;CACnF;EAAE,OAAO;EAAQ,OAAO;EAAoB,WAAW;CAAE;CACzD;EAAE,OAAO;EAAa,OAAO;EAAc,WAAW;CAAE;CACxD;EAAE,OAAO;EAAc,OAAO;EAAc,WAAW;CAAE;CACzD;EAAE,OAAO;EAAc,OAAO;EAAc,WAAW;CAAE;CACzD;EAAE,OAAO;EAAe,OAAO;EAAe,WAAW;CAAE;AAC7D;AAEA,IAAa,sBAAsB,MAAiD,YAAY,MAAM,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE;AAEhI,IAAa,eAAe,MAAyB,YAAY,MAAM,MAAM,EAAE,UAAU,CAAC,CAAC,CAAE;AAE7F,IAAa,aAAa,MAAyB,YAAY,MAAM,MAAM,EAAE,UAAU,CAAC,CAAC,CAAE;;AAG3F,IAAa,kBAAkB,QAA+B,aAA+C,UAAU,YAAY;;;ACOnI,IAAa,kBAAoC;CAC/C;EAAE,IAAI;EAAO,MAAM;EAAc,KAAK;EAAK,iBAAiB;EAAI,gBAAgB;EAAQ,WAAW;EAAU,YAAY;EAAG,YAAY;EAAG,gBAAgB;CAAE;CAC7J;EAAE,IAAI;EAAQ,MAAM;EAAe,KAAK;EAAK,iBAAiB;EAAI,gBAAgB;EAAQ,WAAW;EAAQ,YAAY;EAAG,YAAY;EAAG,gBAAgB;CAAE;CAC7J;EAAE,IAAI;EAAM,MAAM;EAAa,KAAK;EAAK,iBAAiB;EAAK,gBAAgB;EAAO,WAAW;EAAM,YAAY;EAAG,YAAY;EAAG,gBAAgB;CAAE;CACvJ;EAAE,IAAI;EAAO,MAAM;EAAc,KAAK;EAAK,iBAAiB;EAAK,gBAAgB;EAAO,WAAW;EAAU,YAAY;EAAG,YAAY;EAAG,gBAAgB;CAAE;AAC/J;AAEA,IAAa,qBAAqB,OAChC,gBAAgB,MAAM,MAAM,EAAE,OAAO,EAAE;;;;;;AAOzC,IAAM,cAA6E,CAAC;CAAE,OAAO;CAAW,aAAa;CAAG,MAAM;AAAS,CAAC;AAExI,SAAgB,YAAY,OAAe,iBAAqC,MAA4D;CAE1I,OADc,YAAY,MAAM,MAAM,EAAE,UAAU,UAAU,mBAAmB,MAAM,EAAE,WAChF,CAAA,EAAO,QAAQ;AACxB;;;;;;AAOA,SAAgB,eACd,MACA,aACA,QACA,iBACuB;CACvB,MAAM,OAAO,OAAO,YAAY,KAAK,OAAO,iBAAiB,WAAW,IAAI,KAAA;CAC5E,IAAI,CAAC,QAAQ,CAAC,MAAM,OAAO,UAAU;CACrC,OAAO;EACL,IAAI,QAAQ,KAAK;EACjB,MAAM,KAAK,MAAM,QAAQ,MAAM,GAAG;EAClC,KAAK,KAAK;EACV,iBAAiB,KAAK;EACtB,gBAAgB,KAAK;EACrB,WAAW;EACX,YAAY,KAAK;EACjB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACrB,GAAK,KAA2B,SAAS,YAAY,EAAE,MAAM,UAAmB,IAAI,CAAC;EACrF,GAAK,KAAsC,aAAa,EAAE,OAAQ,KAAsC,WAAW,IAAI,CAAC;CAC1H;AACF;;AAGA,IAAa,eAAe,MAA+B,EAAE,kBAAkB,EAAE,MAAO;;;;;AAMxF,SAAgB,cAAc,OAAe,QAAgB,GAA+D;CAC1H,MAAM,gBAAgB,EAAE,mBAAmB;CAC3C,MAAM,SAAS,gBAAgB,QAAQ;CACvC,MAAM,OAAO,KAAK,IAAI,QAAQ,EAAE,eAAe;CAC/C,MAAM,SAAS,KAAK,OAAO,SAAS,QAAQ,CAAC;CAC7C,OAAO,gBACH;EAAE,GAAG;EAAQ,GAAG;EAAG,OAAO;EAAM;EAAQ,SAAS,OAAO;CAAO,IAC/D;EAAE,GAAG;EAAG,GAAG;EAAQ;EAAO,QAAQ;EAAM,SAAS,OAAO;CAAO;AACrE;;;;;AAMA,SAAgB,eAAe,MAAyB,OAAe,QAAgB,GAAwE;CAC7J,MAAM,OAAO,cAAc,OAAO,QAAQ,CAAC;CAC3C,IAAI,CAAC,KAAK,SAAS,OAAO;CAC1B,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;EAC/B,MAAM,YAAY,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK;EACnD,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,IAAI,aAAa,KAAK,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK,OAAO;IACvD,IAAI,KAAK,IAAI,KAAK,QAAQ;IAC1B;GACF;GACA,IAAI,MAAM,IAAI,QAAQ,KAAK,KAAK,KAAK,OAAO;EAC9C;CACF;CACA,OAAO;AACT;;ACxDA,IAAM,eAA8B;CAAE,OAAO;CAAgB,SAAS;AAAK;;AAG3E,IAAa,YAAY;AAEzB,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,UAAU,IAAY,QAAyB,KAAK,OAAQ;;AAGlE,SAAgB,YAAY,QAA2B,IAAY,IAA+B;CAChG,KAAK,KAAK,MAAM,EAAE;CAClB,KAAK,KAAK,MAAM,EAAE;CAClB,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO;CACvB,MAAM,MAAM,SAAS,cAAc,QAAQ;CAC3C,IAAI,QAAQ,OAAO;CACnB,IAAI,SAAS,OAAO;CACpB,MAAM,MAAM,IAAI,WAAW,IAAI;CAC/B,IAAI,CAAC,KAAK,OAAO;CACjB,IAAI,YAAY;CAChB,IAAI,SAAS,GAAG,GAAG,IAAI,OAAO,IAAI,MAAM;CACxC,IAAI,UAAU,QAAQ,IAAI,EAAE;CAC5B,OAAO;AACT;;AAGA,SAAgB,eAAe,QAA2B,SAA4C;CACpG,MAAM,OAAO,cAAc,OAAO,OAAO,OAAO,QAAQ,OAAO;CAC/D,IAAI,CAAC,KAAK,SAAS,OAAO;CAC1B,MAAM,MAAM,SAAS,cAAc,QAAQ;CAC3C,IAAI,QAAQ,KAAK;CACjB,IAAI,SAAS,KAAK;CAClB,MAAM,MAAM,IAAI,WAAW,IAAI;CAC/B,IAAI,CAAC,KAAK,OAAO;CACjB,IAAI,UAAU,QAAQ,KAAK,GAAG,KAAK,GAAG,KAAK,OAAO,KAAK,QAAQ,GAAG,GAAG,KAAK,OAAO,KAAK,MAAM;CAC5F,OAAO;AACT;;AAGA,SAAS,UAAU,QAA0C;CAC3D,MAAM,MAAM,OAAO,WAAW,IAAI;CAClC,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,EAAE,SAAS,IAAI,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;CACnE,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG,IAAI,KAAK,KAAK,KAAK;CAC5D,OAAO;AACT;AAEA,IAAM,SAAS,OAAe,IAAI,SAAS,MAAM,WAAW,GAAG,EAAE,CAAC;AAElE,SAAS,YAAe,GAAe,IAAwB;CAC7D,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,IAAI,iBAAiB,uBAAO,IAAI,MAAM,2CAA2C,CAAC,GAAG,EAAE;EAC7F,EAAE,MACC,OAAO,aAAa,CAAC,GAAG,QAAQ,CAAC,KACjC,OAAO,aAAa,CAAC,GAAG,OAAO,CAAC,EACnC;CACF,CAAC;AACH;;AAGA,SAAgB,cAAc,KAAsB;CAClD,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC3D,IAAI,6CAA6C,KAAK,GAAG,GAAG,OAAO;CACnE,IAAI,6CAA6C,KAAK,GAAG,GAAG,OAAO;CACnE,IAAI,WAAW,KAAK,GAAG,GAAG,OAAO;CACjC,IAAI,aAAa,KAAK,GAAG,GAAG,OAAO;CACnC,IAAI,eAAe,KAAK,GAAG,KAAK,kBAAkB,KAAK,GAAG,GAAG,OAAO;CACpE,IAAI,+CAA+C,KAAK,GAAG,GAAG,OAAO;CACrE,IAAI,qBAAqB,KAAK,GAAG,GAAG,OAAO;CAC3C,OAAO,OAAO;AAChB;;;;;AAMA,IAAa,iBAAb,MAA4B;CAahB;CACA;CAbV,SAAoC;CACpC,SAAgC;CAChC,4BAAoB,IAAI,IAAgC;CACxD,kBAAyC;CACzC,SAAsC;;CAEtC,UAAiC,QAAQ,QAAQ;;CAGjD,OAAsC;CAEtC,YACE,cACA,QACA;EAFQ,KAAA,eAAA;EACA,KAAA,SAAA;CACP;CAEH,YAA2B;EACzB,OAAO,KAAK;CACd;CAEA,UAAU,IAA4C;EACpD,KAAK,UAAU,IAAI,EAAE;EACrB,aAAa,KAAK,UAAU,OAAO,EAAE;CACvC;CAEA,IAAY,OAAqC;EAC/C,KAAK,QAAQ;GAAE,GAAG,KAAK;GAAQ,GAAG;EAAM,CAAC;CAC3C;;CAGA,QAAgB,QAA6B;EAC3C,KAAK,SAAS;EACd,KAAK,UAAU,SAAS,OAAO,GAAG,KAAK,MAAM,CAAC;CAChD;CAEA,iBAAiB,IAAyB;EACxC,KAAK,kBAAkB;EACvB,IAAI,KAAK,QAAQ,KAAK,IAAI,EAAE,SAAS,KAAK,eAAe,EAAE,CAAC;CAC9D;CAEA,iBAAgD;EAC9C,MAAM,IAAI,KAAK;EACf,MAAM,UAAU,eAAe,GAAG,iBAAiB,GAAG,GAAG,iBAAiB,GAAG,kBAAkB,KAAK,eAAe,GAAG,GAAG,eAAe,CAAC,CAAC,eAAe;EAEzJ,OAAO,WAAW,KAAK,SAAS,SAAS;GAAE,GAAG;GAAS,MAAM;EAAU,IAAI;CAC7E;CAEA,WAA2C;EAEzC,MAAM,SAAS,KAAK,QAAQ,UAAU;EACtC,IAAI,QAAQ,OAAO;GAAE,SAAS;GAAQ,QAAQ;EAAK;EACnD,MAAM,OAAO,KAAK,QAAQ,YAAY;EACtC,IAAI,CAAC,MAAM,OAAO,KAAA;EAClB,MAAM,QAAQ,KAAK,WAAW,QAAQ,KAAK,YAAY,KAAA;EACvD,MAAM,MAAM,KAAK,eAAe,aAAa,KAAK,gBAAgB,KAAA;EAClE,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,KAAA;EAC3B,OAAO;GACL,SAAS,OAAO;GAChB,UAAU,OAAO;GACjB,WAAW,OAAO,IAAI,aAAa,KAAA,KAAa,IAAI,YAAY,IAAI,KAAK,IAAI,GAAG,IAAI,YAAY,IAAI,aAAa,EAAE,IAAI,KAAA;GACvH,UAAU,OAAO,IAAI,aAAa,KAAA,KAAa,IAAI,WAAW,IAAI,IAAI,WAAW,KAAA;GACjF,SAAS,KAAK,WAAW,KAAA;GACzB,OAAO,mBAAmB,KAAK,mBAAmB,OAAO,SAAS;EACpE;CACF;CAEA,MAAM,QAAQ,MAAqC;EACjD,IAAI,KAAK,QAAQ,MAAM,KAAK,WAAW;EACvC,KAAK,OAAO;EACZ,KAAK,QAAQ;GAAE,GAAG;GAAc,OAAO;EAAa,CAAC;EACrD,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,KAAK,aAAa,IAAI;GACrC,OAAO,iBAAiB,KAAK,eAAe;GAC5C,MAAM,OAAO,MAAM,OAAO,QAAQ;GAClC,KAAK,SAAS;GACd,KAAK,IAAI,EAAE,YAAY,KAAK,WAAW,CAAC;EAC1C,SAAS,KAAK;GACZ,KAAK,SAAS;GACd,KAAK,QAAQ,YAAY;GACzB,MAAM,IAAI,MAAM,cAAc,GAAG,CAAC;EACpC;EAEA,MAAM,qBAAqB,KAAK,iBAAiB;EACjD,MAAM,eAAe,MACnB,KAAK,IAAI;GAAE,SAAS,EAAE,KAAK,mBAAmB,KAAK,OAAO;GAAS,WAAW,EAAE,KAAK;GAAW,eAAe,EAAE,KAAK;EAAc,CAAC;EACvI,MAAM,eAAe,KAAK,IAAI,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC;EACxD,MAAM,cAAc,MAClB,KAAK,IAAI,EAAE,UAAU;GAAE,MAAM,EAAE;GAAM,OAAO,EAAE;GAAY,GAAI,EAAE,YAAY,KAAA,IAAY,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;EAAG,EAAE,CAAC;EAC1H,OAAO,GAAG,cAAc,YAAY;EACpC,OAAO,GAAG,aAAa,WAAW;EAClC,OAAO,GAAG,mBAAmB,MAAM;EACnC,OAAO,GAAG,iBAAiB,UAAU;EACrC,KAAK,eAAe;GAClB,OAAO,IAAI,cAAc,YAAY;GACrC,OAAO,IAAI,aAAa,WAAW;GACnC,OAAO,IAAI,mBAAmB,MAAM;GACpC,OAAO,IAAI,iBAAiB,UAAU;EACxC;EAEA,MAAM,OAAO,OAAO,iBAAiB;EACrC,KAAK,IAAI;GACP,OAAO;GACP,SAAS,OAAO,eAAe,CAAC,CAAC;GACjC,eAAe,MAAM;GACrB,SAAS,KAAK,eAAe;GAC7B,OAAO,KAAK,SAAS;GACrB,GAAI,OAAO,YAAY,EAAE,SAAS,KAAK,IAAI,CAAC;GAC5C,GAAI,OAAO,aAAa,OAAO,YAAY,EAAE,YAAY,KAAK,IAAI,CAAC;EACrE,CAAC;EACD,OAAO,eAAe;CACxB;CAEA,mBAAiC;EAC/B,KAAK,SAAS;EACd,KAAK,SAAS;EACd,KAAK,SAAS;EACd,KAAK,QAAQ,YAAY;CAC3B;CAEA,MAAM,aAA4B;EAChC,MAAM,IAAI,KAAK;EACf,IAAI,CAAC,GAAG;EACR,EAAE,cAAc;EAChB,IAAI;GACF,MAAM,EAAE,WAAW;EACrB,UAAU;GACR,KAAK,iBAAiB;EACxB;CACF;;;;;;CAOA,MAAc,gBAAgB,QAAoB,MAAqB,MAAc,QAA+B;EAClH,IAAI,SAAS,UAAU,OAAO,KAAK,gBAAgB;EACnD,KAAK,IAAI,EAAE,UAAU;GAAE,MAAM;GAAG,OAAO;EAAO,EAAE,CAAC;EACjD,OAAO,CAAE,MAAM,OAAO,SAAS,SAAS,GAAI,MAAM,MAAM,iBAAiB;EACzE,KAAK,IAAI,EAAE,UAAU;GAAE,MAAM;GAAQ,OAAO;EAAO,EAAE,CAAC;CACxD;;;;;CAMA,KAAK,IAA2B;EAC9B,MAAM,SAAS,KAAK;EACpB,MAAM,UAAU,KAAK,OAAO;EAC5B,IAAI,CAAC,UAAU,CAAC,SAAS,OAAO,QAAQ,uBAAO,IAAI,MAAM,yBAAyB,CAAC;EACnF,IAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,uBAAO,IAAI,MAAM,qCAAqC,CAAC;EAC7F,IAAI,KAAK,KAAK,CAAC,OAAO,WAAW,OAAO,QAAQ,uBAAO,IAAI,MAAM,yCAAyC,CAAC;EAC3G,IAAI,KAAK,OAAO,UAAU,YAAY,OAAO,QAAQ,uBAAO,IAAI,MAAM,8BAA8B,CAAC;EACrG,MAAM,OAAO,KAAK,MAAM,OAAO,IAAI,QAAQ,GAAG,CAAC;EAC/C,MAAM,MAAM,KAAK,QAAQ,WAAY,OAAO,OAAO,UAAW,IAAI,IAAI,KAAA,CAAU;EAChF,KAAK,UAAU,IAAI,YAAY,CAAC,CAAC;EACjC,OAAO,IAAI,OAAO,QAAQ;GACxB,MAAM,IAAI,MAAM,cAAc,GAAG,CAAC;EACpC,CAAC;CACH;CAEA,MAAM,MAAM,QAAwC,MAAmC;EACrF,MAAM,SAAS,KAAK;EACpB,MAAM,UAAU,KAAK,OAAO;EAC5B,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,yBAAyB;EACtD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,0BAA0B;EACxD,IAAI,KAAK,OAAO,UAAU,YAAY,MAAM,IAAI,MAAM,kBAAkB;EACxE,MAAM,KAAK;EAEX,MAAM,QAAoB,UAAU,SAAS,SAAS;GAAE,OAAO;GAAG,MAAM,YAAY;EAAO;EAC3F,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,MAAM,CAAC;EAClD,MAAM,QAAQ,SAAS,KAAK,IAAI,GAAG,MAAM,KAAK;EAC9C,MAAM,UAAU,KAAK,IAAI,QAAQ,YAAY,KAAK,IAAI,QAAQ,YAAY,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC;EACnG,MAAM,OAAO,OAAO,SAAS,aAAa,QAAQ,WAAW;GAC3D,YAAY;GACZ;GACA,WAAW,KAAK,aAAa;GAC7B,GAAI,KAAK,UAAU,KAAA,KAAa,OAAO,YAAY,EAAE,SAAS,KAAK,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,GAAG,QAAQ,GAAG,CAAC,EAAE,IAAI,CAAC;GAC5H,WAAW;GACX,sBAAsB;GACtB,iBAAiB;EACnB,CAAC;EAGD,OAAO,cAAc;EACrB,KAAK,IAAI;GAAE,OAAO;GAAY,UAAU;IAAE,MAAM;IAAG;GAAM;EAAE,CAAC;EAC5D,IAAI;GACF,MAAM,SAAS,KAAK,UAAU;IAAE,GAAG;IAAG,GAAG;GAAE;GAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,OAAO,KAAK;IACpC,MAAM,SAAS,MAAM,MAAM,KAAK,CAAC;IAEjC,MAAM,OAAO,eADG,YAAY,QAAQ,OAAO,OAAO,GAAG,QAAQ,GAAG,GAAG,OAAO,OAAO,GAAG,QAAQ,GAAG,CACnE,GAAS,OAAO;IAC5C,MAAM,QAAQ,OAAO,aAAa,MAAM,OAAO,WAAW,IAAI,IAAI,MAAM,KAAK,OAAO,MAAM,QAAQ,cAAc;IAChH,IAAI,MAAM,GAAG;KACX,QAAQ,KAAK,kBAAkB;MAC7B,SAAS,QAAQ;MACjB,MAAM,QAAQ;MACd,WAAW,QAAQ;MACnB,WAAW,QAAQ;MACnB,MAAM,GAAG,OAAO,MAAM,GAAG,OAAO;MAChC,MAAM,GAAG,KAAK,MAAM,GAAG,KAAK;MAC5B;MACA;MACA,OAAO,MAAM;MACb,KAAK,UAAU,IAAI;KACrB,CAAC;KACD,MAAM,KAAK,UAAU;IACvB;IACA,MAAM,KAAK,UAAU,OAAO,MAAM;GACpC;GACA,MAAM,YAAY,KAAK,gBAAgB,QAAQ,MAAM,QAAQ,WAAW,KAAK,GAAG,mBAAmB,QAAQ,eAAe;EAC5H,SAAS,KAAK;GACZ,MAAM,IAAI,MAAM,cAAc,GAAG,CAAC;EACpC,UAAU;GACR,IAAI;IACF,MAAM,KAAK,SAAS;GACtB,QAAQ,CAER;GACA,IAAI,KAAK,QAAQ;IACf,OAAO,eAAe;IACtB,KAAK,IAAI;KAAE,OAAO;KAAS,UAAU,KAAA;IAAU,CAAC;IAChD,OAAY,cAAc,CAAC,CAAC,WAAW,KAAK,IAAI,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC;GACvF;EACF;CACF;AACF;;ACnTA,IAAa,0BAAmC,OAAO,cAAc,eAAe,eAAe;AACnG,IAAa,uBAAgC,OAAO,cAAc,eAAe,YAAY;AAI7F,SAAgB,eAAe,KAAK,OAAO,cAAc,cAAc,KAAK,UAAU,WAAW,cAAc,OAAO,cAAc,cAAc,IAAI,UAAU,gBAA0B;CAExL,IAAI,mBAAmB,KAAK,EAAE,KAAM,YAAY,KAAK,EAAE,KAAK,cAAc,GAAI,OAAO;CACrF,IAAI,UAAU,KAAK,EAAE,GAAG,OAAO;CAC/B,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAkB,MAAsB,WAAqB,eAAe,GAAkB;CAC5G,IAAI,SAAS,QAAQ,OAAO;CAC5B,IAAI,SAAS,cAAc,kBAAkB,IAAI,eAAe,GAAG,OAAO;CAC1E,IAAI,aAAa,OAAO,OAAO,SAAS,cAAc,+CAA+C;CACrG,IAAI,aAAa,WAAW,OAAO;CACnC,OAAO;AACT;;;;;;ACnEA,IAAM,gBAAgB,OAAO;;;;;;AAO7B,eAAsB,aAAa,MAA2C;CAC5E,IAAI,SAAS,QAAQ,OAAO,IAAI,WAAW;CAC3C,IAAI,SAAS,aAAa,OAAO,IAAI,iBAAiB,OAAO;CAE7D,OAAO,KAAI,OADO,QAAQ,GAAA,CACX,oBAAoB;AACrC;;AAWA,SAAgB,cAAc,UAAgC,CAAC,GAAmB;CAChF,OAAO,IAAI,eAAe,QAAQ,gBAAgB,cAAc,OAAO,QAAQ,eAC5E,MAAM,QAAQ,EAAA,CAAG,aAAa,aAAa,QAAQ,GAAG,SAAS,CAClE;AACF"}
|
|
1
|
+
{"version":3,"file":"libniimai.js","names":["sleep","encodeRows","encodeRows"],"sources":["../src/ble.ts","../src/thermal.ts","../src/cat.ts","../src/escpos.ts","../src/tspl.ts","../src/catalog.ts","../src/bluetooth.ts","../src/mock.ts","../src/paper.ts","../src/profiles.ts","../src/service.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["import type { ConnectionInfo, NiimbotAbstractClient } from '@mmote/niimbluelib';\n\ntype Lib = typeof import('@mmote/niimbluelib');\n\n/** The standard NIIMBOT service, followed by serial-over-BLE services common on budget printers. */\nexport const CANDIDATE_SERVICES = [\n 'e7810a71-73ae-499d-8c15-faa9aef0c3f2', // NIIMBOT\n '0000e0ff-3c17-d293-8e48-14fe2e4da212', // NIIMBOT B21 C2B (model id 771), channel ffe1\n '0000ae30-0000-1000-8000-00805f9b34fb', // AE30 printer UART\n '0000ff00-0000-1000-8000-00805f9b34fb',\n '0000ffe0-0000-1000-8000-00805f9b34fb', // HM-10 style\n '0000fff0-0000-1000-8000-00805f9b34fb',\n '000018f0-0000-1000-8000-00805f9b34fb', // generic printer\n '49535343-fe7d-4ae5-8fa9-9fafd205e455', // Microchip transparent UART\n '6e400001-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART\n];\n\n/** Name prefixes of NIIMBOT printers; the chooser lists only these and known cat printers. */\nexport const NAME_PREFIXES = ['A', 'B', 'C', 'D', 'E', 'F', 'H', 'J', 'K', 'M', 'N', 'P', 'S', 'T', 'Z'];\n\n/** Opens the browser's printer chooser. `all` lists every device, for printers whose names niimai does not know. */\nexport function requestPrinter(extraPrefixes: string[] = [], all = false): Promise<BluetoothDevice> {\n if (all) return navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: CANDIDATE_SERVICES });\n return navigator.bluetooth.requestDevice({\n filters: [...[...NAME_PREFIXES, ...extraPrefixes].map((namePrefix) => ({ namePrefix })), { services: [CANDIDATE_SERVICES[0]] }],\n optionalServices: CANDIDATE_SERVICES,\n });\n}\n\ninterface CharLike {\n uuid: string;\n properties: { notify: boolean; indicate?: boolean; write: boolean; writeWithoutResponse: boolean };\n}\n\nexport interface ServiceLike<C extends CharLike = CharLike> {\n uuid: string;\n getCharacteristics(): Promise<C[]>;\n}\n\nexport interface Channel<C extends CharLike = CharLike> {\n write: C;\n notify: C;\n}\n\n/**\n * Picks the characteristics to talk through. A single characteristic that both\n * notifies and accepts writes wins (the NIIMBOT layout); otherwise a write and\n * a notify characteristic from the same service are paired.\n */\nexport async function pickChannel<C extends CharLike>(services: ServiceLike<C>[]): Promise<Channel<C> | null> {\n const all: { service: string; chars: C[] }[] = [];\n for (const service of services) all.push({ service: service.uuid, chars: await service.getCharacteristics() });\n\n const canWrite = (c: C) => c.properties.writeWithoutResponse || c.properties.write;\n const canNotify = (c: C) => c.properties.notify || !!c.properties.indicate;\n\n for (const { chars } of all) {\n const both = chars.find((c) => c.properties.writeWithoutResponse && c.properties.notify);\n if (both) return { write: both, notify: both };\n }\n for (const { chars } of all) {\n const write = chars.find(canWrite);\n const notify = chars.find(canNotify);\n if (write && notify) return { write, notify };\n }\n return null;\n}\n\n/** Turns Chrome's terse GATT errors into something a person can act on. */\nexport function describeBleError(err: unknown, found: string[] = []): Error {\n const msg = err instanceof Error ? err.message : String(err);\n if (/no services found/i.test(msg) || (found.length === 0 && /characteristic/i.test(msg))) {\n return new Error('This printer uses an unknown Bluetooth service. See Help → Printer not recognised.');\n }\n if (/characteristic/i.test(msg)) return new Error(`No usable channel. Services: ${found.join(', ')}`);\n return err instanceof Error ? err : new Error(msg);\n}\n\n/**\n * A Web Bluetooth client that accepts more service layouts than niimbluelib's\n * own, which only knows the standard NIIMBOT service. The protocol handling\n * (packets, negotiation, heartbeat) is inherited unchanged.\n */\nexport function createBleClient(lib: Lib, chosen?: BluetoothDevice): NiimbotAbstractClient {\n class FlexibleBleClient extends lib.NiimbotAbstractClient {\n private server?: BluetoothRemoteGATTServer;\n private channel?: Channel<BluetoothRemoteGATTCharacteristic>;\n\n async connect(): Promise<ConnectionInfo> {\n await this.disconnect();\n const device = chosen ?? (await requestPrinter());\n if (!device.gatt) throw new Error('Device has no GATT server');\n\n const onDisconnect = () => {\n this.server = undefined;\n this.channel = undefined;\n this.emit('disconnect', new lib.DisconnectEvent());\n device.removeEventListener('gattserverdisconnected', onDisconnect);\n };\n device.addEventListener('gattserverdisconnected', onDisconnect);\n\n const server = await device.gatt.connect();\n let found: string[] = [];\n try {\n const services = await server.getPrimaryServices();\n found = services.map((s) => s.uuid);\n console.info('[niimai] services', found);\n const channel = await pickChannel(services);\n if (!channel) throw new Error('Suitable characteristic not found');\n console.info('[niimai] channel', channel.write.uuid, channel.notify.uuid);\n channel.notify.addEventListener('characteristicvaluechanged', (e) => {\n const value = (e.target as BluetoothRemoteGATTCharacteristic).value;\n if (value) this.processRawPacket(value);\n });\n await channel.notify.startNotifications();\n this.server = server;\n this.channel = channel;\n } catch (err) {\n server.disconnect();\n throw describeBleError(err, found);\n }\n\n await this.negotiateAndGetPrinterInfo(() => this.disconnect());\n const result: ConnectionInfo = {\n deviceName: device.name,\n result: this.printerInfo.connectResult ?? lib.ConnectResult.FirmwareErrors,\n };\n this.emit('connect', new lib.ConnectEvent(result));\n return result;\n }\n\n isConnected(): boolean {\n return !!this.server && !!this.channel;\n }\n\n async disconnect(): Promise<void> {\n this.stopHeartbeat();\n this.server?.disconnect();\n this.server = undefined;\n this.channel = undefined;\n }\n\n async sendRaw(data: Uint8Array, force?: boolean): Promise<void> {\n const send = async () => {\n const c = this.channel?.write;\n if (!c) throw new Error('Channel is closed');\n await lib.Utils.sleep(this.packetIntervalMs);\n const buf = data.slice().buffer;\n if (c.properties.writeWithoutResponse) await c.writeValueWithoutResponse(buf);\n else await c.writeValueWithResponse(buf);\n this.emit('rawpacketsent', new lib.RawPacketSentEvent(data));\n };\n if (force) await send();\n else await this.mutex.runExclusive(send);\n }\n\n getType() {\n return 'bluetooth' as const;\n }\n }\n return new FlexibleBleClient();\n}\n","/**\n * Shared ground for the extra Bluetooth thermal printers: cat printers and\n * ESC/POS mini printers (58 mm continuous paper, 384-dot head) and TSPL\n * label printers (labels with gaps, up to 80 mm). niimai's focus is NIIMBOT;\n * these clients are kept apart so NIIMBOT printing never goes through them.\n */\nimport type { PrinterModelMeta, PrintTaskName } from '@mmote/niimbluelib';\nimport type { LabelSize } from './profiles';\nimport type { ClientLike, PrintTaskLike } from './client';\n\n/** Dots across the head: 48 mm at 203 dpi. */\nexport const THERMAL_HEAD_DOTS = 384;\n/** The print width in whole millimetres, the widest a design on continuous paper can be. */\nexport const THERMAL_HEAD_MM = 48;\n\nexport type Confidence = 'tested' | 'high' | 'medium';\n\nexport interface ThermalModel {\n /** Bluetooth name prefix. */\n prefix: string;\n confidence: Confidence;\n note?: string;\n /** The prefix names a brand or a generic Bluetooth module, not a model; the printer's own name is shown. */\n generic?: boolean;\n /** Maker and model, where the Bluetooth name does not say. */\n brand?: string;\n /** Dots across the head, where it differs from the family's usual one: 576 on 80 mm receipt printers. */\n headDots?: number;\n}\n\n/** The name a printer shows: its model, or its own Bluetooth name when the model entry is only a brand or a stock name. */\nexport const shownName = (model: ThermalModel | null, transport: { name?: string }, fallback: string): string =>\n (model && !model.generic ? model.prefix : transport.name) || model?.prefix || fallback;\n\n/** The known model a Bluetooth name belongs to, longest prefix first. */\nexport function modelFor<M extends ThermalModel>(models: M[], name: string | undefined): M | null {\n if (!name) return null;\n return [...models].sort((a, b) => b.prefix.length - a.prefix.length).find((m) => name.startsWith(m.prefix)) ?? null;\n}\n\n/** Sizes that suit 58 mm continuous paper: the full 48 mm print width at common lengths. */\nexport const THERMAL_SIZES: LabelSize[] = [15, 20, 30, 48, 80].map((heightMm) => ({ widthMm: 48, heightMm }));\nexport const THERMAL_DEFAULT_SIZE: LabelSize = { widthMm: 48, heightMm: 30 };\n\n/**\n * Packs a 1-bit page (RGBA, dark = black) into rows as wide as the head, centred across\n * the head. Cat printers want the leftmost dot in the lowest bit, ESC/POS in\n * the highest.\n */\nexport function packRows(data: ArrayLike<number>, width: number, height: number, order: 'lsb' | 'msb', head = THERMAL_HEAD_DOTS): Uint8Array[] {\n const offset = Math.max(0, Math.floor((head - width) / 2));\n const rows: Uint8Array[] = [];\n for (let y = 0; y < height; y++) {\n const row = new Uint8Array(head / 8);\n for (let x = 0; x < width && x + offset < head; x++) {\n if (data[(y * width + x) * 4] < 128) {\n const dot = x + offset;\n row[dot >> 3] |= order === 'lsb' ? 1 << (dot & 7) : 0x80 >> (dot & 7);\n }\n }\n rows.push(row);\n }\n return rows;\n}\n\n/** The Bluetooth link, kept narrow so tests can stand in for it. */\nexport interface ThermalTransport {\n name?: string;\n write(chunk: Uint8Array): Promise<void>;\n onNotify(fn: (data: Uint8Array) => void): void;\n onDisconnect(fn: () => void): void;\n disconnect(): Promise<void>;\n}\n\nexport interface Pace {\n /** Pause after each write. */\n chunkMs: number;\n /** How often to look again while the printer asks to wait. */\n pauseMs: number;\n}\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\n/**\n * A thermal printer behind the same interface as a NIIMBOT client, so the\n * printer service drives both. It reports itself through model metadata with\n * kind \"thermal\"; it has no roll tags, battery or heartbeat to offer.\n * Subclasses supply the row encoding and the bytes for a job.\n */\nexport abstract class ThermalPrinterClient implements ClientLike {\n private listeners = new Map<string, Set<(e: unknown) => void>>();\n private connected = false;\n /** Set while the printer asks to wait (cat printers' flow control). */\n protected paused = false;\n /** Bytes per write. Starts large and halves if the link refuses it. */\n private chunk: number;\n\n protected abstract readonly densityLevels: number;\n /** The model name shown for this printer. */\n protected abstract readonly modelName: string;\n /** Dots across the head. */\n protected readonly headDots: number = THERMAL_HEAD_DOTS;\n /** \"left\": the label runs sideways through the head, as on NIIMBOT D-series printers. */\n protected readonly printDirection: 'top' | 'left' = 'top';\n /** Continuous-paper printers report \"thermal\"; label printers report nothing and get the roll UI. */\n protected readonly kind: 'thermal' | undefined = 'thermal';\n /** Label sizes common for this printer, offered in place of NIIMBOT roll sizes. */\n protected readonly labelSizes: LabelSize[] | undefined = undefined;\n protected abstract encodeRows(data: ArrayLike<number>, width: number, height: number): Uint8Array[];\n /** `gapDots`: labels with gaps on a printer that cannot sense them; each label is followed by exactly this much feed. */\n protected abstract job(rows: Uint8Array[], density: number, copies: number, labelType: number, gapDots?: number): Uint8Array;\n /** Called after each page is sent; printers that confirm a job wait here. */\n protected async afterPage(): Promise<void> {}\n\n constructor(\n protected transport: ThermalTransport,\n protected pace: Pace,\n firstChunk: number,\n ) {\n this.chunk = firstChunk;\n transport.onDisconnect(() => {\n this.connected = false;\n this.emit('disconnect', {});\n });\n }\n\n async connect() {\n this.connected = true;\n return { deviceName: this.transport.name };\n }\n\n async disconnect() {\n this.connected = false;\n await this.transport.disconnect();\n }\n\n isConnected() {\n return this.connected;\n }\n\n on(event: string, fn: (e: unknown) => void) {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\n this.listeners.get(event)!.add(fn);\n }\n\n off(event: string, fn: (e: unknown) => void) {\n this.listeners.get(event)?.delete(fn);\n }\n\n private emit(event: string, e: unknown) {\n this.listeners.get(event)?.forEach((fn) => fn(e));\n }\n\n getPrinterInfo() {\n return {};\n }\n\n getModelMetadata(): PrinterModelMeta {\n return {\n model: this.modelName,\n ...(this.kind ? { kind: this.kind } : {}),\n ...(this.labelSizes ? { labelSizes: this.labelSizes } : {}),\n dpi: 203,\n printheadPixels: this.headDots,\n printDirection: this.printDirection,\n densityMin: 1,\n densityMax: this.densityLevels,\n densityDefault: Math.ceil(this.densityLevels / 2),\n } as unknown as PrinterModelMeta;\n }\n\n getPrintTaskType(): PrintTaskName {\n // The service needs a task name; these clients print the same way whatever it is.\n return 'B1' as PrintTaskName;\n }\n\n getRfidInfo() {\n return {};\n }\n\n async fetchRfidInfo() {}\n startHeartbeat() {}\n stopHeartbeat() {}\n\n /** Pages arrive as 1-bit canvases; the service hands them here instead of to niimbluelib's encoder. */\n encodePage(canvas: HTMLCanvasElement): Uint8Array[] {\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('Canvas is unavailable');\n return this.encodeRows(ctx.getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height);\n }\n\n /** Sends bytes in chunks, waiting while the printer asks for a pause. `onSent` hears the share sent, 0–1. */\n async send(bytes: Uint8Array, onSent?: (share: number) => void): Promise<void> {\n for (let i = 0; i < bytes.length; ) {\n while (this.paused) await sleep(this.pace.pauseMs);\n const part = bytes.subarray(i, i + this.chunk);\n try {\n await this.transport.write(part);\n } catch (err) {\n if (this.chunk <= 20) throw err;\n this.chunk = Math.max(20, this.chunk >> 1);\n continue;\n }\n i += part.length;\n onSent?.(i / bytes.length);\n await sleep(this.pace.chunkMs);\n }\n }\n\n protocol = {\n printEnd: async () => true,\n newPrintTask: (_name: PrintTaskName, options: Record<string, unknown>): PrintTaskLike => {\n const density = Number(options.density ?? 1);\n const labelType = Number(options.labelType ?? 1);\n const gapDots = options.gapDots === undefined ? undefined : Math.max(0, Math.round(Number(options.gapDots)));\n // A batch sends several pages to one task; progress counts across them.\n let printed = 0;\n return {\n printInit: async () => {},\n printPage: async (image, quantity = 1) => {\n const total = () => Math.max(printed, Number(options.totalPages ?? printed));\n // Sending is the slow part over Bluetooth, so report it as it goes, in whole percents.\n let shown = -1;\n await this.send(this.job(image as Uint8Array[], density, Math.max(1, quantity), labelType, gapDots), (share) => {\n const sending = Math.floor(share * 100);\n if (sending === shown) return;\n shown = sending;\n this.emit('printprogress', { page: printed, pagesTotal: total(), pagePrintProgress: 0, pageFeedProgress: 0, sending });\n });\n await this.afterPage();\n printed += Math.max(1, quantity);\n this.emit('printprogress', { page: printed, pagesTotal: total(), pagePrintProgress: 100, pageFeedProgress: 100 });\n },\n waitForFinished: async () => {},\n printEnd: async () => true,\n };\n },\n };\n}\n\n/** A Web Bluetooth link through one service: a write characteristic and, if present, a notify one. */\nexport async function webTransport(\n device: BluetoothDevice,\n server: BluetoothRemoteGATTServer,\n ids: { service: string; write: string; notify?: string },\n): Promise<ThermalTransport> {\n const service = await server.getPrimaryService(ids.service);\n const tx = await service.getCharacteristic(ids.write);\n const notify: ((d: Uint8Array) => void)[] = [];\n if (ids.notify) {\n const rx = await service.getCharacteristic(ids.notify);\n rx.addEventListener('characteristicvaluechanged', (e) => {\n const v = (e.target as BluetoothRemoteGATTCharacteristic).value;\n if (v) notify.forEach((fn) => fn(new Uint8Array(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength))));\n });\n await rx.startNotifications();\n }\n return {\n name: device.name,\n write: async (chunk) => {\n const buf = chunk.slice().buffer;\n if (tx.properties.writeWithoutResponse) await tx.writeValueWithoutResponse(buf);\n else await tx.writeValueWithResponse(buf);\n },\n onNotify: (fn) => notify.push(fn),\n onDisconnect: (fn) => device.addEventListener('gattserverdisconnected', fn),\n disconnect: async () => server.disconnect(),\n };\n}\n","/**\n * \"Cat printers\": the small Bluetooth thermal printers sold as GB01, MX06 and\n * the like. See thermal.ts for what they share with other extras.\n *\n * Protocol, from rbaron/catprinter and NaitLee/Cat-Printer: packets are\n * 51 78 <cmd> 00 <len> 00 <payload> <crc8(payload)> FF over service AE30,\n * written to AE01. The printer sends flow control on AE02.\n */\nimport { modelFor, packRows, shownName, THERMAL_HEAD_DOTS, ThermalPrinterClient, type Confidence, type Pace, type ThermalModel, type ThermalTransport } from './thermal';\n\nexport const CAT_SERVICE = '0000ae30-0000-1000-8000-00805f9b34fb';\nexport const CAT_TX = '0000ae01-0000-1000-8000-00805f9b34fb';\nexport const CAT_RX = '0000ae02-0000-1000-8000-00805f9b34fb';\n\nconst ROW_BYTES = THERMAL_HEAD_DOTS / 8;\n\nexport type { Confidence };\n\nexport interface CatModel extends ThermalModel {\n /** Takes the newer \"start printing\" command. */\n newKind?: boolean;\n /** Mishandles the feed command, so the paper is fed with blank rows. */\n feedWithRows?: boolean;\n}\n\n/**\n * Models known to speak this protocol. \"high\": supported by both reference\n * projects and widely used. \"medium\": listed by Cat-Printer, fewer reports.\n * \"tested\": printed with niimai on real hardware.\n */\nexport const CAT_MODELS: CatModel[] = [\n { prefix: 'GB01', confidence: 'high' },\n { prefix: 'GB02', confidence: 'high' },\n { prefix: 'GB03', confidence: 'high', newKind: true },\n { prefix: 'MX05', confidence: 'high', feedWithRows: true },\n { prefix: 'MX06', confidence: 'high', feedWithRows: true },\n { prefix: 'MX10', confidence: 'high', feedWithRows: true },\n { prefix: 'MX08', confidence: 'medium', feedWithRows: true },\n { prefix: 'MX09', confidence: 'medium', feedWithRows: true },\n { prefix: 'MX11', confidence: 'medium' },\n { prefix: 'GT01', confidence: 'medium' },\n { prefix: 'YT01', confidence: 'medium' },\n { prefix: 'PD01', confidence: 'medium' },\n { prefix: 'MXTP', confidence: 'medium' },\n { prefix: 'SC03h', confidence: 'medium' },\n];\n\n/** Look-alikes that use other protocols. Listed in Help and docs/thermal-printers.md. */\nexport const CAT_UNSUPPORTED: { name: string; note: string }[] = [\n { name: 'MXW01', note: 'Newer protocol' },\n { name: 'PeriPage', note: 'Different protocol' },\n { name: 'Phomemo M02 / T02', note: 'Different protocol' },\n];\n\nexport const catModelFor = (name: string | undefined): CatModel | null => modelFor(CAT_MODELS, name);\n\n// CRC-8, polynomial 0x07, as the printers expect.\nconst CRC_TABLE = Array.from({ length: 256 }, (_, i) => {\n let c = i;\n for (let b = 0; b < 8; b++) c = c & 0x80 ? ((c << 1) ^ 0x07) & 0xff : (c << 1) & 0xff;\n return c;\n});\nexport const crc8 = (data: ArrayLike<number>): number => {\n let c = 0;\n for (let i = 0; i < data.length; i++) c = CRC_TABLE[(c ^ data[i]) & 0xff];\n return c;\n};\n\nexport function packet(cmd: number, payload: ArrayLike<number>): number[] {\n if (payload.length > 0xff) throw new Error('Payload too long');\n return [0x51, 0x78, cmd, 0x00, payload.length, 0x00, ...Array.from(payload), crc8(payload), 0xff];\n}\n\nconst u16 = (v: number) => [v & 0xff, (v >> 8) & 0xff];\n\nconst CMD = {\n retract: 0xa0,\n feed: 0xa1,\n row: 0xa2,\n state: 0xa3,\n quality: 0xa4,\n lattice: 0xa6,\n update: 0xa9,\n energy: 0xaf,\n speed: 0xbd,\n applyEnergy: 0xbe,\n} as const;\n\nconst LATTICE_START = [0xaa, 0x55, 0x17, 0x38, 0x44, 0x5f, 0x5f, 0x5f, 0x44, 0x38, 0x2c];\nconst LATTICE_END = [0xaa, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17];\n\n/** Flow control the printer sends on AE02 when its buffer fills and drains. */\nexport const FLOW_PAUSE = [0x51, 0x78, 0xae, 0x01, 0x01, 0x00, 0x10, 0x70, 0xff];\nexport const FLOW_RESUME = [0x51, 0x78, 0xae, 0x01, 0x01, 0x00, 0x00, 0x00, 0xff];\n\n/** Heat energy for each darkness step; Cat-Printer prints images at 0x4000. */\nconst ENERGY = [0x2000, 0x3000, 0x4000, 0x6000, 0x9000];\n/** Rows fed after the last copy, so the print clears the tear bar. */\nconst FEED_AFTER = 128;\n/** Blank rows between copies. */\nconst GAP_BETWEEN = 24;\n\n/** Rows for a cat printer: leftmost dot in the lowest bit. */\nexport const encodeRows = (data: ArrayLike<number>, width: number, height: number): Uint8Array[] => packRows(data, width, height, 'lsb');\n\n/**\n * Everything sent for one job: setup, the rows for each copy, then feed and\n * finish. With `gapDots`, for labels with gaps, every label is followed by\n * exactly that feed, so the next print starts on the next label.\n */\nexport function printJob(rows: Uint8Array[], { density, copies, model, gapDots }: { density: number; copies: number; model: CatModel | null; gapDots?: number }): Uint8Array {\n const out: number[] = [];\n const push = (cmd: number, payload: ArrayLike<number>) => out.push(...packet(cmd, payload));\n push(CMD.state, [0]);\n if (model?.newKind) out.push(0x12);\n push(CMD.state, [0]);\n push(CMD.quality, [50]);\n push(CMD.speed, [32]);\n push(CMD.energy, u16(ENERGY[Math.min(ENERGY.length, Math.max(1, Math.round(density))) - 1]));\n push(CMD.applyEnergy, [1]);\n push(CMD.update, [0]);\n push(CMD.lattice, LATTICE_START);\n const blank = new Uint8Array(ROW_BYTES);\n for (let c = 0; c < copies; c++) {\n if (c > 0) for (let i = 0; i < (gapDots ?? GAP_BETWEEN); i++) push(CMD.row, blank);\n for (const row of rows) push(CMD.row, row);\n }\n push(CMD.lattice, LATTICE_END);\n push(CMD.speed, [8]);\n if (gapDots !== undefined) out.push(...catFeed(gapDots, model));\n else if (model?.feedWithRows) for (let i = 0; i < FEED_AFTER; i++) push(CMD.row, blank);\n else push(CMD.feed, u16(FEED_AFTER));\n push(CMD.state, [0]);\n return Uint8Array.from(out);\n}\n\n/** Moves the paper by `dots`, back when negative. Models that mishandle the feed command get blank rows. */\nexport function catFeed(dots: number, model: CatModel | null): Uint8Array {\n const n = Math.abs(Math.round(dots));\n if (!n) return new Uint8Array();\n if (dots < 0) return Uint8Array.from(packet(CMD.retract, u16(n)));\n if (!model?.feedWithRows) return Uint8Array.from(packet(CMD.feed, u16(n)));\n const blank = new Uint8Array(ROW_BYTES);\n return Uint8Array.from(Array.from({ length: n }, () => packet(CMD.row, blank)).flat());\n}\n\n/** A cat printer: the shared thermal client with cat packets and flow control. */\nexport class CatPrinterClient extends ThermalPrinterClient {\n protected readonly densityLevels = ENERGY.length;\n protected readonly modelName: string;\n\n constructor(\n transport: ThermalTransport,\n readonly model: CatModel | null,\n pace: Pace = { chunkMs: 20, pauseMs: 100 },\n ) {\n super(transport, pace, 180);\n this.modelName = shownName(model, transport, 'Cat printer');\n transport.onNotify((data) => {\n if (same(data, FLOW_PAUSE)) this.paused = true;\n else if (same(data, FLOW_RESUME)) this.paused = false;\n });\n }\n\n protected encodeRows(data: ArrayLike<number>, width: number, height: number) {\n return encodeRows(data, width, height);\n }\n\n protected job(rows: Uint8Array[], density: number, copies: number, _labelType: number, gapDots?: number) {\n return printJob(rows, { density, copies, model: this.model, gapDots });\n }\n\n readonly feedsBack = true;\n\n /** These printers cannot sense the paper, so the user lines it up by hand. */\n async feedPaper(dots: number): Promise<void> {\n await this.send(catFeed(dots, this.model));\n }\n}\n\nconst same = (a: ArrayLike<number>, b: number[]) => a.length === b.length && b.every((v, i) => a[i] === v);\n","/**\n * ESC/POS mini printers: 58 mm Bluetooth receipt printers such as the MP583\n * (YAEN MPT-II). They take standard raster images (GS v 0) over the 18F0\n * printer service. See thermal.ts for what they share with other extras.\n */\nimport { modelFor, packRows, shownName, THERMAL_HEAD_DOTS, ThermalPrinterClient, type Pace, type ThermalModel, type ThermalTransport } from './thermal';\n\nexport const ESCPOS_SERVICE = '000018f0-0000-1000-8000-00805f9b34fb';\nexport const ESCPOS_WRITE = '00002af1-0000-1000-8000-00805f9b34fb';\n/** The serial service many budget printers offer, Xprinter among them; NIIMBOT printers use it too. */\nexport const ESCPOS_ALT_SERVICE = 'e7810a71-73ae-499d-8c15-faa9aef0c3f2';\nexport const ESCPOS_ALT_WRITE = 'bef8d6c9-9c21-4c9e-b632-bd58c1009f9f';\n\n/**\n * Models known to take ESC/POS raster images, by Bluetooth name. \"tested\":\n * printed with niimai on real hardware. \"high\": supported by several\n * open-source printer projects. \"medium\": the name is reported by one or two\n * of them, untested here. Names from receipt-printer projects such as\n * WebBluetoothReceiptPrinter and bitbank2/Thermal_Printer; see\n * docs/thermal-printers.md for sources. Most of these printers share one\n * Bluetooth module, so its services tell nothing apart: the name decides.\n */\n// A model's prefix is the name it shows and the key its saved settings live under, so existing entries never change it.\nexport const ESCPOS_MODELS: ThermalModel[] = [\n { prefix: 'MP583', confidence: 'tested', note: 'Sold as YAEN MPT-II' },\n { prefix: 'MPT-II', confidence: 'high', note: 'Stock module name on many 58 mm printers' },\n { prefix: 'MP58', confidence: 'medium' },\n { prefix: 'MP210', confidence: 'medium' },\n { prefix: 'MTP-II', confidence: 'high', note: 'Stock module name on many 58 mm printers' },\n { prefix: 'MTP-2', confidence: 'medium' },\n { prefix: 'MPT-3', confidence: 'medium', note: '80 mm', headDots: 576 },\n { prefix: 'MTP-3', confidence: 'medium', note: '80 mm', headDots: 576 },\n { prefix: 'PT-210', confidence: 'high', brand: 'Goojprt PT-210' },\n { prefix: 'PT210', confidence: 'medium', brand: 'Goojprt PT-210' },\n { prefix: 'GOJRPT', confidence: 'medium', brand: 'Goojprt' },\n { prefix: 'Gojprt', confidence: 'medium', brand: 'Goojprt' },\n { prefix: 'PT-200', confidence: 'medium' },\n { prefix: 'RPP02', confidence: 'high', brand: 'Rongta RPP02N', note: 'Also sold as EPPOS EP58 Plus' },\n { prefix: 'RPP300', confidence: 'medium', brand: 'Rongta RPP300', note: '80 mm', headDots: 576 },\n { prefix: 'ZJ-58', confidence: 'medium', brand: 'Zjiang' },\n { prefix: 'TM-P', confidence: 'medium', brand: 'Epson TM-P series' },\n { prefix: 'XP-P323B', confidence: 'medium', brand: 'Xprinter XP-P323B', note: '80 mm', headDots: 576 },\n { prefix: 'XP-', confidence: 'medium', brand: 'other Xprinters', generic: true },\n // Many budget printers keep their Bluetooth module's stock name: the Xprinter XP-58IIL shows as \"Printer001\".\n { prefix: 'Printer', confidence: 'tested', brand: 'Xprinter XP-58IIL, HOIN, Gprinter', note: 'Stock module name', generic: true },\n { prefix: 'printer', confidence: 'medium', note: 'Stock module name', generic: true },\n { prefix: 'BlueTooth Printer', confidence: 'medium', note: 'Stock module name: Zjiang POS-5805 and others', generic: true },\n { prefix: '58Printer', confidence: 'medium', note: 'Stock module name', generic: true },\n];\n\nexport const escposModelFor = (name: string | undefined): ThermalModel | null => modelFor(ESCPOS_MODELS, name);\n\n/** Rows per raster command; small bands keep the printer's buffer from overflowing. */\nconst BAND = 24;\n/** Dots fed between copies. */\nconst GAP_DOTS = 24;\n/** Lines fed after the last copy, so the print clears the tear bar. */\nconst FEED_LINES = 4;\n\n/** Rows for an ESC/POS printer: leftmost dot in the highest bit, centred on a head `head` dots wide. */\nexport const encodeRows = (data: ArrayLike<number>, width: number, height: number, head = THERMAL_HEAD_DOTS): Uint8Array[] =>\n packRows(data, width, height, 'msb', head);\n\n/**\n * ESC @, then each copy as GS v 0 bands with a gap between, then a feed past\n * the tear bar. With `gapDots`, for labels with gaps, every label is followed\n * by exactly that feed, so the next print starts on the next label.\n */\nexport function escposJob(rows: Uint8Array[], copies: number, gapDots?: number): Uint8Array {\n const out: number[] = [0x1b, 0x40];\n const rowBytes = rows[0]?.length ?? THERMAL_HEAD_DOTS / 8;\n for (let c = 0; c < copies; c++) {\n if (c > 0) out.push(...escposFeed(gapDots ?? GAP_DOTS));\n for (let i = 0; i < rows.length; i += BAND) {\n const band = rows.slice(i, i + BAND);\n out.push(0x1d, 0x76, 0x30, 0x00, rowBytes & 0xff, rowBytes >> 8, band.length & 0xff, band.length >> 8);\n for (const row of band) out.push(...row);\n }\n }\n if (gapDots === undefined) out.push(0x1b, 0x64, FEED_LINES);\n else out.push(...escposFeed(gapDots));\n return Uint8Array.from(out);\n}\n\n/**\n * Moves the paper forward by `dots`: line feeds with the line spacing set to\n * the distance (ESC 3 n, LF), then the default spacing back (ESC 2). The\n * MP583 ignores ESC J, the usual dot feed, but takes line feeds. These\n * printers have no reverse feed.\n */\nexport function escposFeed(dots: number): Uint8Array {\n const out: number[] = [];\n for (let left = Math.round(dots); left > 0; left -= 255) out.push(0x1b, 0x33, Math.min(255, left), 0x0a);\n if (out.length) out.push(0x1b, 0x32);\n return Uint8Array.from(out);\n}\n\n/**\n * An ESC/POS printer: the shared thermal client with raster commands. These\n * printers send no flow control, so writes are paced. Heat settings vary by\n * vendor, so darkness is left to the printer.\n */\nexport class EscPosPrinterClient extends ThermalPrinterClient {\n protected readonly densityLevels = 1;\n protected readonly modelName: string;\n protected readonly headDots: number;\n\n constructor(\n transport: ThermalTransport,\n readonly model: ThermalModel | null,\n pace: Pace = { chunkMs: 30, pauseMs: 100 },\n ) {\n super(transport, pace, 100);\n this.modelName = shownName(model, transport, 'Thermal printer');\n this.headDots = model?.headDots ?? THERMAL_HEAD_DOTS;\n }\n\n protected encodeRows(data: ArrayLike<number>, width: number, height: number) {\n return encodeRows(data, width, height, this.headDots);\n }\n\n protected job(rows: Uint8Array[], _density: number, copies: number, _labelType: number, gapDots?: number) {\n return escposJob(rows, copies, gapDots);\n }\n\n /** These printers cannot sense the paper, so the user lines it up by hand. */\n async feedPaper(dots: number): Promise<void> {\n if (dots < 0) throw new Error('This printer cannot move the paper back');\n await this.send(escposFeed(dots));\n }\n}\n","/**\n * TSPL label printers: 2- and 3-inch desktop label printers that take TSC's\n * label language over the 18F0 printer service. A job is a few text commands\n * (SIZE, GAP, DENSITY, CLS) around one BITMAP of the page, then PRINT. The\n * printer finds each label's edge itself. It answers \"OK\" on 2AF0 when it has\n * taken a job, and a status byte to ESC ! ?.\n */\nimport type { LabelSize } from './profiles';\nimport { modelFor, shownName, ThermalPrinterClient, type Pace, type ThermalModel, type ThermalTransport } from './thermal';\n\nexport const TSPL_SERVICE = '000018f0-0000-1000-8000-00805f9b34fb';\nexport const TSPL_WRITE = '00002af1-0000-1000-8000-00805f9b34fb';\nexport const TSPL_NOTIFY = '00002af0-0000-1000-8000-00805f9b34fb';\n\n/** Dots across the head: 80 mm at 203 dpi. */\nexport const TSPL_HEAD_DOTS = 640;\nconst DOTS_PER_MM = 8;\n\n/**\n * Models known to take TSPL over 18F0. \"tested\": printed with niimai on real\n * hardware. \"medium\": reported by open-source printer projects, untested here.\n */\nexport const TSPL_MODELS: ThermalModel[] = [\n { prefix: 'QR380A', confidence: 'tested', note: 'Sold as CC390' },\n { prefix: 'GP-M322', confidence: 'medium', brand: 'Gprinter GP-M322', headDots: 576 },\n];\n\nexport const tsplModelFor = (name: string | undefined): ThermalModel | null => modelFor(TSPL_MODELS, name);\n\n/** Common 2- to 3-inch label sizes, width × height in mm, smallest first. */\nexport const TSPL_SIZES: LabelSize[] = [\n [40, 30],\n [50, 25],\n [50, 30],\n [50, 40],\n [57, 32],\n [60, 40],\n [50, 50],\n [70, 50],\n [75, 50],\n [80, 50],\n [60, 60],\n [80, 60],\n [80, 80],\n [75, 100],\n].map(([widthMm, heightMm]) => ({ widthMm, heightMm }));\n\n/** TSPL's darkness steps, 0 to 15; niimai offers 1 to 15. */\nconst DENSITY_LEVELS = 15;\n/** The gap between labels, for printers that look for it. */\nconst GAP_MM = 2;\n\n/** NIIMBOT label types, as the print dialog passes them. */\nconst LABEL_TYPE = { blackmark: 2, continuous: 3 } as const;\n\n/** The status query, and the bits of its answer that stop a print. */\nexport const STATUS_QUERY = [0x1b, 0x21, 0x3f];\nconst STATUS_FAULTS: [number, string][] = [\n [0x01, 'Printer cover is open'],\n [0x02, 'Paper jam'],\n [0x04, 'Out of paper'],\n [0x08, 'Out of ribbon'],\n];\n\n/** Replies that mean a job was taken: \"OK\". */\nconst OK = [0x4f, 0x4b];\n/** How long to wait for \"OK\" before asking the printer what is wrong. */\nconst ACK_TIMEOUT_MS = 20_000;\n/** How long to wait for the answer to a status query. */\nconst STATUS_TIMEOUT_MS = 2_000;\n\n/**\n * Rows for a TSPL BITMAP: as wide as the page, leftmost dot in the highest\n * bit, and 0 for a dark dot.\n */\nexport function encodeRows(data: ArrayLike<number>, width: number, height: number): Uint8Array[] {\n const bytes = Math.ceil(width / 8);\n const rows: Uint8Array[] = [];\n for (let y = 0; y < height; y++) {\n const row = new Uint8Array(bytes).fill(0xff);\n for (let x = 0; x < width; x++) if (data[(y * width + x) * 4] < 128) row[x >> 3] &= ~(0x80 >> (x & 7));\n rows.push(row);\n }\n return rows;\n}\n\nconst ascii = (s: string) => Array.from(s, (c) => c.charCodeAt(0));\nconst mm = (dots: number) => `${Math.round((dots / DOTS_PER_MM) * 10) / 10} mm`;\n\n/** How the printer finds the start of each label. */\nfunction paperCommand(labelType: number): string {\n if (labelType === LABEL_TYPE.continuous) return 'GAP 0 mm,0 mm';\n if (labelType === LABEL_TYPE.blackmark) return `BLINE ${GAP_MM} mm,0 mm`;\n return `GAP ${GAP_MM} mm,0 mm`;\n}\n\n/** One label: setup, the page as one bitmap at the top left, then print it `copies` times. */\nexport function tsplJob(rows: Uint8Array[], { density, copies, labelType }: { density: number; copies: number; labelType: number }): Uint8Array {\n const bytes = rows[0]?.length ?? 0;\n const height = rows.length;\n const head = [\n `SIZE ${mm(bytes * 8)},${mm(height)}`,\n paperCommand(labelType),\n 'DIRECTION 0',\n `DENSITY ${Math.min(DENSITY_LEVELS, Math.max(0, Math.round(density)))}`,\n 'CLS',\n ];\n const out = ascii(head.map((l) => `${l}\\r\\n`).join('') + `BITMAP 0,0,${bytes},${height},0,`);\n for (const row of rows) for (const b of row) out.push(b);\n out.push(...ascii(`\\r\\nPRINT ${Math.max(1, copies)},1\\r\\n`));\n return Uint8Array.from(out);\n}\n\n/**\n * A TSPL label printer: the shared thermal client with TSPL jobs. It shows\n * as a label printer (roll sizes, paper types). Writes are paced at the size\n * these printers' BLE modules take without dropping data.\n */\nexport class TsplPrinterClient extends ThermalPrinterClient {\n protected readonly densityLevels = DENSITY_LEVELS;\n protected readonly modelName: string;\n protected readonly headDots: number;\n protected readonly kind = undefined;\n protected readonly labelSizes = TSPL_SIZES;\n\n private oks = 0;\n private status: number | null = null;\n private wake: (() => void) | null = null;\n\n constructor(\n transport: ThermalTransport,\n readonly model: ThermalModel | null,\n pace: Pace = { chunkMs: 30, pauseMs: 100 },\n private ackTimeoutMs = ACK_TIMEOUT_MS,\n ) {\n super(transport, pace, 100);\n this.modelName = shownName(model, transport, 'Label printer');\n this.headDots = model?.headDots ?? TSPL_HEAD_DOTS;\n transport.onNotify((data) => {\n if (data.length >= 2 && data[0] === OK[0] && data[1] === OK[1]) this.oks++;\n else if (data.length === 1) this.status = data[0];\n else return;\n this.wake?.();\n });\n }\n\n protected encodeRows(data: ArrayLike<number>, width: number, height: number) {\n return encodeRows(data, width, height);\n }\n\n /** These printers cannot sense the roll; its size is remembered per printer. */\n rollKey() {\n return `printer:${this.transport.name || this.modelName}`;\n }\n\n protected job(rows: Uint8Array[], density: number, copies: number, labelType: number) {\n this.oks = 0;\n return tsplJob(rows, { density, copies, labelType });\n }\n\n /** Resolves when `done` holds or the time runs out. */\n private async until(done: () => boolean, ms: number) {\n const deadline = Date.now() + ms;\n while (!done() && Date.now() < deadline) {\n await new Promise<void>((resolve) => {\n const t = setTimeout(resolve, deadline - Date.now());\n this.wake = () => (clearTimeout(t), resolve());\n });\n }\n this.wake = null;\n }\n\n /**\n * Waits for the printer's \"OK\", so the next label never lands on a busy\n * printer. Without one, asks for its status: a fault fails the print, and a\n * printer that is merely quiet is left to carry on.\n */\n protected async afterPage() {\n await this.until(() => this.oks > 0, this.ackTimeoutMs);\n if (this.oks > 0) return;\n this.status = null;\n await this.send(Uint8Array.from(STATUS_QUERY));\n await this.until(() => this.status !== null, STATUS_TIMEOUT_MS);\n const fault = STATUS_FAULTS.find(([bit]) => (this.status ?? 0) & bit);\n if (fault) throw new Error(fault[1]);\n console.warn('[niimai] Label printer did not confirm the job');\n }\n}\n","/**\n * Every printer outside NIIMBOT that libniimai knows, by Bluetooth name, in\n * one lookup across families. A name matches one family at most, and a name\n * that matches none is known to be unknown before anything is sent to it.\n * The models themselves live with their family: ESCPOS_MODELS, TSPL_MODELS\n * and CAT_MODELS.\n */\nimport { CAT_MODELS } from './cat';\nimport { ESCPOS_MODELS } from './escpos';\nimport { THERMAL_HEAD_DOTS, type ThermalModel } from './thermal';\nimport { TSPL_HEAD_DOTS, TSPL_MODELS } from './tspl';\n\nexport type PrinterFamily = 'escpos' | 'tspl' | 'cat';\n\nexport interface CatalogEntry {\n family: PrinterFamily;\n model: ThermalModel;\n}\n\nexport const PRINTER_CATALOG: CatalogEntry[] = [\n ...ESCPOS_MODELS.map((model) => ({ family: 'escpos' as const, model })),\n ...TSPL_MODELS.map((model) => ({ family: 'tspl' as const, model })),\n ...CAT_MODELS.map((model) => ({ family: 'cat' as const, model })),\n];\n\n/** The known printer a Bluetooth name belongs to, longest prefix first across every family. */\nexport function identifyPrinter(name: string | undefined): CatalogEntry | null {\n if (!name) return null;\n let best: CatalogEntry | null = null;\n for (const entry of PRINTER_CATALOG) {\n if (name.startsWith(entry.model.prefix) && entry.model.prefix.length > (best?.model.prefix.length ?? 0)) best = entry;\n }\n return best;\n}\n\n/** A kind of printer to pick for one niimai does not recognise. */\nexport interface PrinterType extends CatalogEntry {\n id: string;\n name: string;\n}\n\n/** The kinds of printer an unrecognised one can be set to, alongside NIIMBOT's MANUAL_PROFILES. */\nexport const PRINTER_TYPES: PrinterType[] = [\n { id: 'escpos-58', name: 'Receipt printer, 58 mm', family: 'escpos', model: { prefix: '', confidence: 'medium', generic: true, headDots: THERMAL_HEAD_DOTS } },\n { id: 'escpos-80', name: 'Receipt printer, 80 mm', family: 'escpos', model: { prefix: '', confidence: 'medium', generic: true, headDots: 576 } },\n { id: 'tspl', name: 'Label printer (TSPL)', family: 'tspl', model: { prefix: '', confidence: 'medium', generic: true, headDots: TSPL_HEAD_DOTS } },\n { id: 'cat', name: 'Cat printer', family: 'cat', model: { prefix: '', confidence: 'medium', generic: true } },\n];\n\nexport const findPrinterType = (id: string | null | undefined): PrinterType | null => PRINTER_TYPES.find((t) => t.id === id) ?? null;\n","import { CANDIDATE_SERVICES, createBleClient, requestPrinter } from './ble';\nimport { findPrinterType, identifyPrinter, PRINTER_CATALOG, type CatalogEntry, type PrinterFamily, type PrinterType } from './catalog';\nimport { CAT_RX, CAT_SERVICE, CAT_TX, CatPrinterClient } from './cat';\nimport { ESCPOS_ALT_SERVICE, ESCPOS_ALT_WRITE, ESCPOS_SERVICE, ESCPOS_WRITE, EscPosPrinterClient } from './escpos';\nimport { TSPL_NOTIFY, TSPL_SERVICE, TSPL_WRITE, TsplPrinterClient } from './tspl';\nimport { webTransport } from './thermal';\nimport type { ClientLike } from './client';\n\ntype Lib = typeof import('@mmote/niimbluelib');\n\n/**\n * A writable characteristic on one of the serial services budget printers\n * offer, for ESC/POS printers on none of the known layouts.\n */\nasync function writeChannel(server: BluetoothRemoteGATTServer, services: string[]): Promise<{ service: string; write: string } | null> {\n for (const service of CANDIDATE_SERVICES) {\n if (!services.includes(service) || service === CAT_SERVICE) continue;\n const chars = await server\n .getPrimaryService(service)\n .then((s) => s.getCharacteristics())\n .catch(() => []);\n const write = chars.find((c) => c.properties.writeWithoutResponse || c.properties.write);\n if (write) return { service, write: write.uuid };\n }\n return null;\n}\n\nconst PRINTER_TYPE_NAMES: Record<PrinterFamily, string> = { escpos: 'receipt printer', tspl: 'label printer', cat: 'cat printer' };\n\n/** The client for a known or hand-picked thermal printer, over the service its family uses; null when the printer lacks it. */\nasync function thermalClient(device: BluetoothDevice, server: BluetoothRemoteGATTServer, services: string[], { family, model }: CatalogEntry): Promise<ClientLike | null> {\n if (family === 'cat') {\n return services.includes(CAT_SERVICE) ? new CatPrinterClient(await webTransport(device, server, { service: CAT_SERVICE, write: CAT_TX, notify: CAT_RX }), model) : null;\n }\n if (family === 'tspl') {\n return services.includes(TSPL_SERVICE) ? new TsplPrinterClient(await webTransport(device, server, { service: TSPL_SERVICE, write: TSPL_WRITE, notify: TSPL_NOTIFY }), model) : null;\n }\n const channel = services.includes(ESCPOS_SERVICE)\n ? { service: ESCPOS_SERVICE, write: ESCPOS_WRITE }\n : services.includes(ESCPOS_ALT_SERVICE)\n ? { service: ESCPOS_ALT_SERVICE, write: ESCPOS_ALT_WRITE }\n : await writeChannel(server, services);\n return channel ? new EscPosPrinterClient(await webTransport(device, server, channel), model) : null;\n}\n\n/** The name starts like one of niimbluelib's NIIMBOT models (\"D110-…\", \"B21_…\"). */\nfunction looksNiimbot(lib: Lib, name: string | undefined): boolean {\n const token = name?.split(/[-_ ]/)[0].toUpperCase();\n return !!token && lib.modelsLibrary.some((m) => m.model.split('_')[0] === token);\n}\n\n/**\n * The Bluetooth printer behind the Connect button. One chooser lists NIIMBOT\n * printers and the known extra thermal printers. A known cat printer offering\n * AE30 gets the cat client; a known ESC/POS printer the ESC/POS client, over\n * 18F0, the shared E781 service (as Xprinters use), or any serial service\n * with a writable characteristic; a known TSPL label printer offering 18F0\n * the TSPL client; and everything else the NIIMBOT client exactly as before.\n * Known names never overlap, so each printer matches one client at most.\n */\nexport class BluetoothPrinter implements ClientLike {\n private inner: ClientLike | null = null;\n\n constructor(private lib: () => Promise<Lib>) {}\n\n private get client(): ClientLike {\n if (!this.inner) throw new Error('Not connected');\n return this.inner;\n }\n\n /** The kind of printer to treat an unrecognised one as; null leaves it to NIIMBOT. */\n private fallback: PrinterType | null = null;\n\n unrecognisedAs(typeId: string | null) {\n this.fallback = findPrinterType(typeId);\n }\n\n async connect() {\n // With a type picked for unrecognised printers, the chooser lists every device, whatever its name.\n const device = await requestPrinter(\n PRINTER_CATALOG.map((e) => e.model.prefix),\n !!this.fallback,\n );\n const known = identifyPrinter(device.name);\n // A hand-picked type is only for printers that are neither known nor named like a NIIMBOT: a NIIMBOT always keeps its own client.\n const fallback = !known && this.fallback && !looksNiimbot(await this.lib(), device.name) ? this.fallback : null;\n const entry = known ?? fallback;\n if (entry && device.gatt) {\n const server = await device.gatt.connect();\n const services = (await server.getPrimaryServices().catch(() => [])).map((s) => s.uuid);\n this.inner = await thermalClient(device, server, services, entry);\n if (this.inner) return this.inner.connect();\n // A printer set to a type by hand that lacks its service should say so, not fall to NIIMBOT.\n if (!known) throw new Error(`${device.name ?? 'This printer'} does not offer a ${PRINTER_TYPE_NAMES[entry.family]} service`);\n }\n const lib = await this.lib();\n this.inner = createBleClient(lib, device) as unknown as ClientLike;\n try {\n return await this.inner.connect();\n } catch (err) {\n if (looksNiimbot(lib, device.name)) throw err;\n 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.`);\n }\n }\n\n async disconnect() {\n await this.inner?.disconnect();\n }\n\n isConnected() {\n return !!this.inner?.isConnected();\n }\n\n on(event: string, fn: (e: any) => void) {\n return this.client.on(event, fn);\n }\n\n off(event: string, fn: (e: any) => void) {\n return this.inner?.off(event, fn);\n }\n\n getPrinterInfo() {\n return this.inner?.getPrinterInfo() ?? {};\n }\n\n getModelMetadata() {\n return this.inner?.getModelMetadata();\n }\n\n getPrintTaskType() {\n return this.inner?.getPrintTaskType();\n }\n\n getRfidInfo() {\n return this.inner?.getRfidInfo() ?? {};\n }\n\n fetchRfidInfo() {\n return this.client.fetchRfidInfo();\n }\n\n startHeartbeat() {\n this.inner?.startHeartbeat();\n }\n\n stopHeartbeat() {\n this.inner?.stopHeartbeat();\n }\n\n isHeartbeatStarted() {\n return !!this.inner?.isHeartbeatStarted?.();\n }\n\n get protocol() {\n return this.client.protocol;\n }\n\n get encodePage() {\n return this.inner?.encodePage?.bind(this.inner);\n }\n\n get rollKey() {\n return this.inner?.rollKey?.bind(this.inner);\n }\n\n get feedPaper() {\n return this.inner?.feedPaper?.bind(this.inner);\n }\n\n get feedsBack() {\n return this.inner?.feedsBack;\n }\n}\n","import type { PrinterModelMeta } from '@mmote/niimbluelib';\nimport type { ClientLike, PrintTaskLike } from './client';\n\ntype Listener = (e: any) => void;\n\n/** A printer that only exists in memory. Used by the demo mode and the tests. */\nexport class MockClient implements ClientLike {\n connected = false;\n printed: { image: unknown; quantity: number; heartbeat: boolean }[] = [];\n failNextPrint: string | null = null;\n private listeners = new Map<string, Set<Listener>>();\n\n /** Pass null to act as a model the library does not recognise. */\n constructor(private meta: PrinterModelMeta | null = DEMO_META) {}\n\n async connect() {\n this.connected = true;\n this.emit('connect', { info: { deviceName: 'Demo printer' } });\n return { deviceName: 'Demo printer' };\n }\n async disconnect() {\n this.connected = false;\n this.emit('disconnect', {});\n }\n isConnected() {\n return this.connected;\n }\n on(event: string, fn: Listener) {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\n this.listeners.get(event)!.add(fn);\n return this;\n }\n off(event: string, fn: Listener) {\n this.listeners.get(event)?.delete(fn);\n return this;\n }\n emit(event: string, e: unknown) {\n this.listeners.get(event)?.forEach((fn) => fn(e));\n }\n protocolVersion = 1;\n getPrinterInfo() {\n return { batteryPercents: 80, modelId: 512, printheadWidth: 96, protocolVersion: this.protocolVersion };\n }\n getModelMetadata() {\n return this.meta ?? undefined;\n }\n getPrintTaskType() {\n return this.meta ? ('D11_V1' as const) : undefined;\n }\n /** Set false to act like printers that report only the roll's barcode. */\n reportsPaperSize = true;\n\n getRfidInfo() {\n return {\n labelRfidInfo: { tagPresent: true, barCode: '6971501227941', allPaper: 210, usedPaper: 12, consumablesType: 1 },\n paperInfo: this.reportsPaperSize ? { valid: true, paperWidth: 40, paperHeight: 12 } : undefined,\n };\n }\n async fetchRfidInfo() {\n return this.getRfidInfo();\n }\n startHeartbeat() {\n this.heartbeat = true;\n this.emit('heartbeat', { data: { batteryPercents: 80, lidClosed: true, paperInserted: true } });\n }\n heartbeat = false;\n stopHeartbeat() {\n this.heartbeat = false;\n }\n /** How many \"print end\" polls answer false before the printer reports done. */\n printEndPending = 0;\n protocol = {\n printEnd: async () => {\n if (this.printEndPending > 0) {\n this.printEndPending--;\n return false;\n }\n return true;\n },\n newPrintTask: (_name: string, options: Record<string, unknown>): PrintTaskLike => {\n const total = Number(options.totalPages ?? 1);\n return {\n printInit: async () => {\n if (this.failNextPrint) {\n const msg = this.failNextPrint;\n this.failNextPrint = null;\n throw new Error(msg);\n }\n },\n printPage: async (image, quantity = 1) => {\n this.printed.push({ image, quantity, heartbeat: this.heartbeat });\n },\n waitForFinished: async () => {\n for (let page = 1; page <= total; page++) {\n this.emit('printprogress', { page, pagesTotal: total, pagePrintProgress: 100, pageFeedProgress: 100 });\n }\n },\n printEnd: async () => true,\n };\n },\n };\n}\n\nexport const DEMO_META = {\n model: 'D11',\n id: [512],\n dpi: 203,\n printDirection: 'left',\n printheadPixels: 96,\n paperTypes: [1],\n densityMin: 1,\n densityMax: 3,\n densityDefault: 2,\n} as unknown as PrinterModelMeta;\n","/**\n * Paper types, as niimbluelib's LabelType numbers. The printer uses this to\n * find the start of each label, so the wrong one mis-feeds the roll.\n */\nexport type PaperType = 'gaps' | 'blackmark' | 'continuous' | 'perforated' | 'transparent';\n\nexport const PAPER_TYPES: { value: PaperType; label: string; labelType: number }[] = [\n { value: 'gaps', label: 'Labels with gaps', labelType: 1 },\n { value: 'blackmark', label: 'Black mark', labelType: 2 },\n { value: 'continuous', label: 'Continuous', labelType: 3 },\n { value: 'perforated', label: 'Perforated', labelType: 4 },\n { value: 'transparent', label: 'Transparent', labelType: 5 },\n];\n\nexport const paperFromLabelType = (n: number | undefined): PaperType | undefined => PAPER_TYPES.find((p) => p.labelType === n)?.value;\n\nexport const labelTypeOf = (p: PaperType): number => PAPER_TYPES.find((x) => x.value === p)!.labelType;\n\nexport const paperName = (p: PaperType): string => PAPER_TYPES.find((x) => x.value === p)!.label;\n\n/** The paper to print on: the label's own choice, then what the roll reports, then labels with gaps. */\nexport const effectivePaper = (chosen: PaperType | undefined, fromRoll: PaperType | undefined): PaperType => chosen ?? fromRoll ?? 'gaps';\n","import type { PrinterModelMeta, PrintTaskName } from '@mmote/niimbluelib';\n\n/** A label's size in millimetres. */\nexport interface LabelSize {\n widthMm: number;\n heightMm: number;\n}\n\nexport type PrintDirection = 'left' | 'top';\n\n/** Everything the app needs to know to render and send a page to a printer. */\nexport interface PrinterProfile {\n id: string;\n name: string;\n dpi: number;\n printheadPixels: number;\n printDirection: PrintDirection;\n printTask: PrintTaskName;\n densityMin: number;\n densityMax: number;\n densityDefault: number;\n /** Extra thermal printers (cat, ESC/POS): continuous paper, no roll tags. Unset for NIIMBOT. */\n kind?: 'thermal';\n /** Label sizes common for this printer (TSPL label printers). Unset for NIIMBOT, whose sizes come from its rolls. */\n sizes?: LabelSize[];\n}\n\n/** Manual profiles for printers niimbluelib does not recognise yet. Each mirrors a known model family. */\nexport const MANUAL_PROFILES: PrinterProfile[] = [\n { id: 'd11', name: 'D11 family', dpi: 203, printheadPixels: 96, printDirection: 'left', printTask: 'D11_V1', densityMin: 1, densityMax: 3, densityDefault: 2 },\n { id: 'd110', name: 'D110 family', dpi: 203, printheadPixels: 96, printDirection: 'left', printTask: 'D110', densityMin: 1, densityMax: 3, densityDefault: 2 },\n { id: 'b1', name: 'B1 family', dpi: 203, printheadPixels: 384, printDirection: 'top', printTask: 'B1', densityMin: 1, densityMax: 5, densityDefault: 3 },\n { id: 'b21', name: 'B21 family', dpi: 203, printheadPixels: 384, printDirection: 'top', printTask: 'B21_V1', densityMin: 1, densityMax: 5, densityDefault: 3 },\n];\n\nexport const findManualProfile = (id: string | null | undefined): PrinterProfile | undefined =>\n MANUAL_PROFILES.find((p) => p.id === id);\n\n/**\n * Print tasks verified on hardware where niimbluelib's choice prints blank.\n * A B21 C2B on protocol 0 needs the original B21 packet formats; the library\n * gives every B21 C2B the B1 task, which that firmware accepts but never prints.\n */\nconst TASK_QUIRKS: { model: string; maxProtocol: number; task: PrintTaskName }[] = [{ model: 'B21_C2B', maxProtocol: 0, task: 'B21_V1' }];\n\nexport function correctTask(model: string, protocolVersion: number | undefined, task: PrintTaskName | undefined): PrintTaskName | undefined {\n const quirk = TASK_QUIRKS.find((q) => q.model === model && (protocolVersion ?? 0) <= q.maxProtocol);\n return quirk?.task ?? task;\n}\n\n/**\n * The profile to print with: the library's model data when it recognises the\n * printer, otherwise the manual choice. A detected model always wins, so a\n * choice left over from another printer cannot send the wrong settings.\n */\nexport function resolveProfile(\n meta: PrinterModelMeta | undefined,\n libraryTask: PrintTaskName | undefined,\n manual?: PrinterProfile,\n protocolVersion?: number,\n): PrinterProfile | null {\n const task = meta ? correctTask(meta.model, protocolVersion, libraryTask) : undefined;\n if (!meta || !task) return manual ?? null;\n return {\n id: `auto:${meta.model}`,\n name: meta.model.replace(/_/g, ' '),\n dpi: meta.dpi,\n printheadPixels: meta.printheadPixels,\n printDirection: meta.printDirection,\n printTask: task,\n densityMin: meta.densityMin,\n densityMax: meta.densityMax,\n densityDefault: meta.densityDefault,\n ...((meta as { kind?: string }).kind === 'thermal' ? { kind: 'thermal' as const } : {}),\n ...((meta as { labelSizes?: LabelSize[] }).labelSizes ? { sizes: (meta as { labelSizes?: LabelSize[] }).labelSizes } : {}),\n };\n}\n\n/** Widest label edge the printhead covers, in millimetres. */\nexport const printheadMm = (p: PrinterProfile): number => (p.printheadPixels / p.dpi) * 25.4;\n\n/**\n * The part of a page the printhead can print, centred across the head. Pages\n * wider than the head are cropped: printers discard rows longer than the head.\n */\nexport function printheadCrop(width: number, height: number, p: Pick<PrinterProfile, 'printheadPixels' | 'printDirection'>) {\n const acrossIsWidth = p.printDirection === 'top';\n const across = acrossIsWidth ? width : height;\n const keep = Math.min(across, p.printheadPixels);\n const offset = Math.floor((across - keep) / 2);\n return acrossIsWidth\n ? { x: offset, y: 0, width: keep, height, cropped: keep < across }\n : { x: 0, y: offset, width, height: keep, cropped: keep < across };\n}\n\n/**\n * True when any black pixel of a page (RGBA, as from getImageData) falls\n * outside what the printhead covers, so part of the design would not print.\n */\nexport function inkOutsideHead(data: ArrayLike<number>, width: number, height: number, p: Pick<PrinterProfile, 'printheadPixels' | 'printDirection'>): boolean {\n const crop = printheadCrop(width, height, p);\n if (!crop.cropped) return false;\n for (let y = 0; y < height; y++) {\n const insideRow = y >= crop.y && y < crop.y + crop.height;\n for (let x = 0; x < width; x++) {\n if (insideRow && x >= crop.x && x < crop.x + crop.width) {\n x = crop.x + crop.width - 1;\n continue;\n }\n if (data[(y * width + x) * 4] < 128) return true;\n }\n }\n return false;\n}\n","import type { ClientLike, ConnectionKind, PrintTaskLike } from './client';\nimport { paperFromLabelType, type PaperType } from './paper';\nimport { findManualProfile, printheadCrop, resolveProfile, type PrinterProfile } from './profiles';\n\nexport type PrinterState = 'disconnected' | 'connecting' | 'ready' | 'printing';\n\nexport interface PrinterStatus {\n state: PrinterState;\n deviceName?: string;\n battery?: number;\n /** Null when the printer is unknown and no manual profile is chosen. */\n profile: PrinterProfile | null;\n detectedModel?: string;\n /**\n * The loaded roll. Size comes from the printer's paper info where it reports\n * one; the barcode identifies the roll when it does not (the B21 C2B, for one).\n */\n label?: { widthMm?: number; heightMm?: number; remaining?: number; capacity?: number; barcode?: string; paper?: PaperType; manual?: boolean };\n lidClosed?: boolean;\n paperInserted?: boolean;\n /** Pages done of the total; `sending` is how much of the current page has gone over the link, 0–100, for printers that report it. */\n progress?: { page: number; total: number; sending?: number };\n /** The paper can be moved by hand with `feed()`, on printers that cannot sense it. */\n canFeed?: boolean;\n /** `feed()` can also move the paper back. */\n canRetract?: boolean;\n}\n\n/** Nudges the printed image, in mm: positive x moves right, positive y moves down. */\nexport interface PrintOffset {\n x: number;\n y: number;\n}\n\nexport interface PrintOptions {\n density: number;\n /** Copies of each page. */\n copies: number;\n labelType?: number;\n offset?: PrintOffset;\n /**\n * Labels with gaps on a printer that cannot sense them (`status.canFeed`):\n * the gap between labels, in mm. Each label is followed by exactly this\n * much feed in place of the tear-off feed, so prints stay on the labels.\n */\n gapMm?: number;\n /**\n * Turns each page clockwise by this many degrees before it is sent, for\n * labels designed landscape: the page is drawn the way it reads and prints\n * turned onto the paper. Applied before `offset`, which stays relative to the paper.\n */\n rotate?: Rotation;\n}\n\n/** Quarter turns, in degrees clockwise. */\nexport type Rotation = 0 | 90 | 180 | 270;\n\n/** Different labels in one job, drawn one at a time so a long batch never holds every page at once. */\nexport interface PageSource {\n count: number;\n page: (index: number) => Promise<HTMLCanvasElement>;\n}\n\ntype Encoder = (canvas: HTMLCanvasElement, direction: PrinterProfile['printDirection']) => unknown | Promise<unknown>;\nexport type ClientFactory = (kind: ConnectionKind) => Promise<ClientLike>;\n\nconst DISCONNECTED: PrinterStatus = { state: 'disconnected', profile: null };\n\n/** What the demo printer is called wherever a printer's name shows. */\nexport const DEMO_NAME = 'Demo printer';\n\nconst PRINT_TIMEOUT_MS = 20_000;\nconst PAGE_TIMEOUT_MS = 10_000;\nconst PRINT_END_POLL_MS = 300;\nconst mmToPx = (mm: number, dpi: number): number => (mm / 25.4) * dpi;\n\n/** The page moved by whole pixels on a white canvas of the same size. */\nexport function shiftCanvas(canvas: HTMLCanvasElement, dx: number, dy: number): HTMLCanvasElement {\n dx = Math.round(dx);\n dy = Math.round(dy);\n if (!dx && !dy) return canvas;\n const out = document.createElement('canvas');\n out.width = canvas.width;\n out.height = canvas.height;\n const ctx = out.getContext('2d');\n if (!ctx) return canvas;\n ctx.fillStyle = '#fff';\n ctx.fillRect(0, 0, out.width, out.height);\n ctx.drawImage(canvas, dx, dy);\n return out;\n}\n\n/** The page turned clockwise by `degrees`, a multiple of 90. Returns the canvas itself for no turn. */\nexport function rotateCanvas(canvas: HTMLCanvasElement, degrees: number): HTMLCanvasElement {\n const turn = (((Math.round(degrees / 90) % 4) + 4) % 4) as 0 | 1 | 2 | 3;\n if (!turn) return canvas;\n const out = document.createElement('canvas');\n out.width = turn % 2 ? canvas.height : canvas.width;\n out.height = turn % 2 ? canvas.width : canvas.height;\n const ctx = out.getContext('2d');\n if (!ctx) return canvas;\n ctx.fillStyle = '#fff';\n ctx.fillRect(0, 0, out.width, out.height);\n ctx.translate(out.width / 2, out.height / 2);\n ctx.rotate((turn * Math.PI) / 2);\n ctx.drawImage(canvas, -canvas.width / 2, -canvas.height / 2);\n return out;\n}\n\n/** Crops the page to what the printhead covers. Returns the canvas itself when it already fits. */\nexport function fitToPrinthead(canvas: HTMLCanvasElement, profile: PrinterProfile): HTMLCanvasElement {\n const crop = printheadCrop(canvas.width, canvas.height, profile);\n if (!crop.cropped) return canvas;\n const out = document.createElement('canvas');\n out.width = crop.width;\n out.height = crop.height;\n const ctx = out.getContext('2d');\n if (!ctx) return canvas;\n ctx.drawImage(canvas, crop.x, crop.y, crop.width, crop.height, 0, 0, crop.width, crop.height);\n return out;\n}\n\n/** Black pixels on the page, for the print log. A zero here means the page itself was blank. */\nfunction inkPixels(canvas: HTMLCanvasElement): number | null {\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);\n let n = 0;\n for (let i = 0; i < data.length; i += 4) if (data[i] < 128) n++;\n return n;\n}\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => reject(new Error('Timeout waiting for the printer to finish')), ms);\n p.then(\n (v) => (clearTimeout(t), resolve(v)),\n (e) => (clearTimeout(t), reject(e)),\n );\n });\n}\n\n/** Translates the library's error codes and messages into short, readable ones. */\nexport function friendlyError(err: unknown): string {\n const msg = err instanceof Error ? err.message : String(err);\n if (/user cancelled|no device selected|chooser/i.test(msg)) return 'Connection cancelled';\n if (/timeout waiting for the printer to finish/i.test(msg)) return 'The printer did not confirm the print';\n if (/timeout/i.test(msg)) return 'Printer stopped responding';\n if (/lid|cover/i.test(msg)) return 'Close the printer lid';\n if (/paper|label/i.test(msg) && /no|out|missing/i.test(msg)) return 'Load a label roll';\n if (/unknown Bluetooth service|No usable channel/i.test(msg)) return msg;\n if (/disconnected|gatt/i.test(msg)) return 'Printer disconnected';\n return msg || 'Something went wrong';\n}\n\n/**\n * Owns the printer connection. The UI subscribes to status updates and calls\n * connect/print; the raw protocol stays inside niimbluelib.\n */\nexport class PrinterService {\n private client: ClientLike | null = null;\n private status: PrinterStatus = DISCONNECTED;\n private listeners = new Set<(s: PrinterStatus) => void>();\n private manualProfileId: string | null = null;\n private detach: (() => void) | null = null;\n /** Paper moves run one after another, and a print waits for them. */\n private feeding: Promise<void> = Promise.resolve();\n\n /** How the current printer is connected. */\n private kind: ConnectionKind | null = null;\n\n constructor(\n private createClient: ClientFactory,\n private encode: Encoder,\n ) {}\n\n getStatus(): PrinterStatus {\n return this.status;\n }\n\n subscribe(fn: (s: PrinterStatus) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private set(patch: Partial<PrinterStatus>): void {\n this.replace({ ...this.status, ...patch });\n }\n\n /** Swaps the whole status, so fields from an earlier connection never linger. */\n private replace(status: PrinterStatus): void {\n this.status = status;\n this.listeners.forEach((fn) => fn(this.status));\n }\n\n setManualProfile(id: string | null): void {\n this.manualProfileId = id;\n if (this.client) this.set({ profile: this.currentProfile() });\n }\n\n private currentProfile(): PrinterProfile | null {\n const c = this.client;\n const profile = resolveProfile(c?.getModelMetadata(), c?.getPrintTaskType(), findManualProfile(this.manualProfileId), c?.getPrinterInfo().protocolVersion);\n // Whatever printer stands in for the demo, it never passes for a real model.\n return profile && this.kind === 'demo' ? { ...profile, name: DEMO_NAME } : profile;\n }\n\n private readRfid(): PrinterStatus['label'] {\n // Printers that cannot sense the roll keep the size the user sets, under a key of their own.\n const manual = this.client?.rollKey?.();\n if (manual) return { barcode: manual, manual: true };\n const info = this.client?.getRfidInfo();\n if (!info) return undefined;\n const paper = info.paperInfo?.valid ? info.paperInfo : undefined;\n const tag = info.labelRfidInfo?.tagPresent ? info.labelRfidInfo : undefined;\n if (!paper && !tag) return undefined;\n return {\n widthMm: paper?.paperWidth,\n heightMm: paper?.paperHeight,\n remaining: tag && tag.allPaper !== undefined && tag.allPaper >= 0 ? Math.max(0, tag.allPaper - (tag.usedPaper ?? 0)) : undefined,\n capacity: tag && tag.allPaper !== undefined && tag.allPaper > 0 ? tag.allPaper : undefined,\n barcode: tag?.barCode || undefined,\n paper: paperFromLabelType(tag?.consumablesType ?? paper?.paperType),\n };\n }\n\n async connect(kind: ConnectionKind): Promise<void> {\n if (this.client) await this.disconnect();\n this.kind = kind;\n this.replace({ ...DISCONNECTED, state: 'connecting' });\n let client: ClientLike;\n try {\n client = await this.createClient(kind);\n client.unrecognisedAs?.(this.manualProfileId);\n const info = await client.connect();\n this.client = client;\n this.set({ deviceName: info.deviceName });\n } catch (err) {\n this.client = null;\n this.replace(DISCONNECTED);\n throw new Error(friendlyError(err));\n }\n\n const onDisconnect = () => this.handleDisconnect();\n const onHeartbeat = (e: { data: { batteryPercents?: number; lidClosed?: boolean; paperInserted?: boolean } }) =>\n this.set({ battery: e.data.batteryPercents ?? this.status.battery, lidClosed: e.data.lidClosed, paperInserted: e.data.paperInserted });\n const onRfid = () => this.set({ label: this.readRfid() });\n const onProgress = (e: { page: number; pagesTotal: number; sending?: number }) =>\n this.set({ progress: { page: e.page, total: e.pagesTotal, ...(e.sending !== undefined ? { sending: e.sending } : {}) } });\n client.on('disconnect', onDisconnect);\n client.on('heartbeat', onHeartbeat);\n client.on('rfidinfofetched', onRfid);\n client.on('printprogress', onProgress);\n this.detach = () => {\n client.off('disconnect', onDisconnect);\n client.off('heartbeat', onHeartbeat);\n client.off('rfidinfofetched', onRfid);\n client.off('printprogress', onProgress);\n };\n\n const meta = client.getModelMetadata();\n this.set({\n state: 'ready',\n battery: client.getPrinterInfo().batteryPercents,\n detectedModel: meta?.model,\n profile: this.currentProfile(),\n label: this.readRfid(),\n ...(client.feedPaper ? { canFeed: true } : {}),\n ...(client.feedPaper && client.feedsBack ? { canRetract: true } : {}),\n });\n client.startHeartbeat();\n }\n\n private handleDisconnect(): void {\n this.detach?.();\n this.detach = null;\n this.client = null;\n this.replace(DISCONNECTED);\n }\n\n async disconnect(): Promise<void> {\n const c = this.client;\n if (!c) return;\n c.stopHeartbeat();\n try {\n await c.disconnect();\n } finally {\n this.handleDisconnect();\n }\n }\n\n /**\n * B21_V1 finishes by polling \"print end\", but niimbluelib's poll cancels its\n * own timer on the first reply, so it never resolves unless the printer is\n * already done. That task polls here instead.\n */\n private async waitForFinished(client: ClientLike, task: PrintTaskLike, name: string, copies: number): Promise<void> {\n if (name !== 'B21_V1') return task.waitForFinished();\n this.set({ progress: { page: 0, total: copies } });\n while (!(await client.protocol.printEnd())) await sleep(PRINT_END_POLL_MS);\n this.set({ progress: { page: copies, total: copies } });\n }\n\n /**\n * Moves the paper by `mm`, out of the printer when positive and back in when\n * negative, to line it up on printers that cannot sense it.\n */\n feed(mm: number): Promise<void> {\n const client = this.client;\n const profile = this.status.profile;\n if (!client || !profile) return Promise.reject(new Error('Connect a printer first'));\n if (!client.feedPaper) return Promise.reject(new Error('This printer finds the paper itself'));\n if (mm < 0 && !client.feedsBack) return Promise.reject(new Error('This printer cannot move the paper back'));\n if (this.status.state === 'printing') return Promise.reject(new Error('Wait for the print to finish'));\n const dots = Math.round(mmToPx(mm, profile.dpi));\n const run = this.feeding.then(() => (dots ? client.feedPaper!(dots) : undefined));\n this.feeding = run.catch(() => {});\n return run.catch((err) => {\n throw new Error(friendlyError(err));\n });\n }\n\n async print(source: HTMLCanvasElement | PageSource, opts: PrintOptions): Promise<void> {\n const client = this.client;\n const profile = this.status.profile;\n if (!client) throw new Error('Connect a printer first');\n if (!profile) throw new Error('Choose your printer type');\n if (this.status.state === 'printing') throw new Error('Already printing');\n await this.feeding;\n\n const pages: PageSource = 'page' in source ? source : { count: 1, page: async () => source };\n const copies = Math.max(1, Math.floor(opts.copies));\n const total = copies * Math.max(1, pages.count);\n const density = Math.min(profile.densityMax, Math.max(profile.densityMin, Math.round(opts.density)));\n const task = client.protocol.newPrintTask(profile.printTask, {\n totalPages: total,\n density,\n labelType: opts.labelType ?? 1,\n ...(opts.gapMm !== undefined && client.feedPaper ? { gapDots: Math.round(mmToPx(Math.max(0, opts.gapMm), profile.dpi)) } : {}),\n pageColor: 0,\n statusPollIntervalMs: 100,\n statusTimeoutMs: 8000,\n });\n\n // Heartbeat queries interleaved with page data spoil prints on some models.\n client.stopHeartbeat();\n this.set({ state: 'printing', progress: { page: 0, total } });\n try {\n const offset = opts.offset ?? { x: 0, y: 0 };\n for (let i = 0; i < pages.count; i++) {\n const canvas = rotateCanvas(await pages.page(i), opts.rotate ?? 0);\n const shifted = shiftCanvas(canvas, mmToPx(offset.x, profile.dpi), mmToPx(offset.y, profile.dpi));\n const page = fitToPrinthead(shifted, profile);\n const image = client.encodePage ? await client.encodePage(page) : await this.encode(page, profile.printDirection);\n if (i === 0) {\n console.info('[niimai] print', {\n profile: profile.id,\n task: profile.printTask,\n direction: profile.printDirection,\n printhead: profile.printheadPixels,\n page: `${canvas.width}x${canvas.height}`,\n rotate: opts.rotate ?? 0,\n sent: `${page.width}x${page.height}`,\n density,\n copies,\n pages: pages.count,\n ink: inkPixels(page),\n });\n await task.printInit();\n }\n await task.printPage(image, copies);\n }\n await withTimeout(this.waitForFinished(client, task, profile.printTask, total), PRINT_TIMEOUT_MS + total * PAGE_TIMEOUT_MS);\n } catch (err) {\n throw new Error(friendlyError(err));\n } finally {\n try {\n await task.printEnd();\n } catch {\n // The page already failed; the original error is the useful one.\n }\n if (this.client) {\n client.startHeartbeat();\n this.set({ state: 'ready', progress: undefined });\n void client.fetchRfidInfo().then(() => this.set({ label: this.readRfid() }), () => {});\n }\n }\n }\n}\n","import type { PrinterModelMeta, PrintTaskName } from '@mmote/niimbluelib';\n\n/**\n * The slice of niimbluelib's client the app uses. Keeping it narrow lets the\n * mock printer and the tests stand in for real hardware.\n */\nexport interface PrintTaskLike {\n printInit(): Promise<void>;\n printPage(image: unknown, quantity?: number): Promise<void>;\n waitForFinished(): Promise<void>;\n printEnd(): Promise<boolean>;\n}\n\nexport interface ClientLike {\n connect(): Promise<{ deviceName?: string }>;\n disconnect(): Promise<void>;\n isConnected(): boolean;\n on(event: string, fn: (e: any) => void): unknown;\n off(event: string, fn: (e: any) => void): unknown;\n getPrinterInfo(): { batteryPercents?: number; modelId?: number; printheadWidth?: number; protocolVersion?: number };\n getModelMetadata(): PrinterModelMeta | undefined;\n getPrintTaskType(): PrintTaskName | undefined;\n getRfidInfo(): {\n labelRfidInfo?: { tagPresent?: boolean; barCode?: string; allPaper?: number; usedPaper?: number; consumablesType?: number };\n paperInfo?: { valid?: boolean; paperWidth?: number; paperHeight?: number; paperType?: number };\n };\n fetchRfidInfo(): Promise<unknown>;\n startHeartbeat(): void;\n stopHeartbeat(): void;\n isHeartbeatStarted?(): boolean;\n protocol: {\n newPrintTask(name: PrintTaskName, options: Record<string, unknown>): PrintTaskLike;\n /** True once the printer has finished every page. */\n printEnd(): Promise<boolean>;\n };\n /** Clients that encode pages themselves (cat printers); NIIMBOT pages go through niimbluelib's encoder. */\n encodePage?(canvas: HTMLCanvasElement): unknown;\n /**\n * Label printers that cannot sense the roll: a key to remember the roll\n * size the user sets, per printer. NIIMBOT printers read their rolls instead.\n */\n rollKey?(): string;\n /** Printers that cannot sense the paper: moves it by `dots`, back into the printer when negative. */\n feedPaper?(dots: number): Promise<void>;\n /** feedPaper can also move the paper back. */\n feedsBack?: boolean;\n /**\n * Bluetooth: the kind of printer to treat one with an unknown name as, set\n * before connecting: an id from PRINTER_TYPES, or a NIIMBOT profile id the\n * client ignores.\n */\n unrecognisedAs?(typeId: string | null): void;\n}\n\nexport type ConnectionKind = 'bluetooth' | 'serial' | 'demo';\n\nexport const supportsBluetooth = (): boolean => typeof navigator !== 'undefined' && 'bluetooth' in navigator;\nexport const supportsSerial = (): boolean => typeof navigator !== 'undefined' && 'serial' in navigator;\n\nexport type Platform = 'ios' | 'android' | 'desktop';\n\nexport function detectPlatform(ua = typeof navigator === 'undefined' ? '' : navigator.userAgent, touchPoints = typeof navigator === 'undefined' ? 0 : navigator.maxTouchPoints): Platform {\n // iPadOS reports itself as a Mac; touch support gives it away.\n if (/iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && touchPoints > 1)) return 'ios';\n if (/Android/.test(ua)) return 'android';\n return 'desktop';\n}\n\nexport type OperatingSystem = 'ios' | 'android' | 'chromeos' | 'linux' | 'mac' | 'windows' | 'other';\nexport type BrowserName = 'chrome' | 'edge' | 'opera' | 'brave' | 'samsung' | 'bluefy' | 'firefox' | 'safari' | 'other';\n\nexport interface BrowserInfo {\n os: OperatingSystem;\n browser: BrowserName;\n /** Built on Chromium, so Web Bluetooth and Web Serial can be there. */\n chromium: boolean;\n /** Served over https or from localhost; browsers hide both APIs elsewhere. */\n secure: boolean;\n}\n\n/** What the browser tells about itself. Defaults to the running browser. */\nexport interface BrowserEnv {\n ua: string;\n touchPoints: number;\n /** Brave hides itself in the user agent; it has `navigator.brave` instead. */\n brave: boolean;\n secure: boolean;\n}\n\nfunction currentEnv(): BrowserEnv {\n if (typeof navigator === 'undefined') return { ua: '', touchPoints: 0, brave: false, secure: true };\n return {\n ua: navigator.userAgent,\n touchPoints: navigator.maxTouchPoints,\n brave: 'brave' in navigator,\n secure: typeof window === 'undefined' || window.isSecureContext !== false,\n };\n}\n\nexport function detectBrowser(env: Partial<BrowserEnv> = {}): BrowserInfo {\n const { ua, touchPoints, brave, secure } = { ...currentEnv(), ...env };\n const platform = detectPlatform(ua, touchPoints);\n const os: OperatingSystem =\n platform !== 'desktop' ? platform : /CrOS/.test(ua) ? 'chromeos' : /Windows/.test(ua) ? 'windows' : /Macintosh|Mac OS X/.test(ua) ? 'mac' : /Linux|X11|BSD/.test(ua) ? 'linux' : 'other';\n const browser: BrowserName = brave\n ? 'brave'\n : /Bluefy/i.test(ua)\n ? 'bluefy'\n : /Firefox|FxiOS/.test(ua)\n ? 'firefox'\n : /Edg(e|A|iOS)?\\//.test(ua)\n ? 'edge'\n : /OPR\\/|Opera/.test(ua)\n ? 'opera'\n : /SamsungBrowser/.test(ua)\n ? 'samsung'\n : /Chrome\\/|CriOS|Chromium/.test(ua)\n ? 'chrome'\n : /Safari\\//.test(ua)\n ? 'safari'\n : 'other';\n // Every iOS browser runs on WebKit, whatever its name.\n const chromium = os !== 'ios' && /Chrome\\/|Chromium/.test(ua);\n return { os, browser, chromium, secure };\n}\n\n/**\n * A Help topic that shows how to get a connection type working:\n * - `linux`: Chrome and other Chromium browsers on Linux need a flag for Bluetooth.\n * - `brave`: Brave keeps Bluetooth and USB behind flags.\n * - `ios`: iPhone and iPad browsers, and Bluefy.\n * - `https`: the page has to be served over https.\n * - `browsers`: which browsers work.\n */\nexport type SetupTopic = 'linux' | 'brave' | 'ios' | 'https' | 'browsers';\n\nexport interface SupportIssue {\n /** In a few words, what is missing or what to do. */\n reason: string;\n topic: SetupTopic;\n}\n\nconst BROWSER_NAMES: Partial<Record<BrowserName, string>> = { firefox: 'Firefox', safari: 'Safari', samsung: 'Samsung Internet', opera: 'Opera' };\n\n/**\n * Why a connection type cannot be used in this browser, and where to read how\n * to fix it, or null when it can be used.\n */\nexport function supportIssue(kind: ConnectionKind, env: Partial<BrowserEnv> = {}): SupportIssue | null {\n if (kind === 'demo') return null;\n if (kind === 'bluetooth' ? supportsBluetooth() : supportsSerial()) return null;\n const { os, browser, chromium, secure } = detectBrowser(env);\n const bluetooth = kind === 'bluetooth';\n if (os === 'ios') return { reason: bluetooth ? 'On iPhone and iPad, use the Bluefy browser' : 'Not available on iPhone and iPad', topic: 'ios' };\n if (!secure && chromium) return { reason: 'Needs an https page', topic: 'https' };\n if (browser === 'brave') return { reason: 'Turn it on in Brave flags', topic: 'brave' };\n if (os === 'android') {\n if (!bluetooth) return { reason: 'Needs Chrome or Edge on a computer', topic: 'browsers' };\n return { reason: browser === 'chrome' ? 'Update Chrome' : 'Open niimai in Chrome', topic: 'browsers' };\n }\n if (bluetooth && os === 'linux' && chromium) return { reason: 'Needs a Chrome flag on Linux', topic: 'linux' };\n const name = BROWSER_NAMES[browser];\n if (name) return { reason: `${name} has no ${bluetooth ? 'Bluetooth' : 'USB'} access; use Chrome or Edge`, topic: 'browsers' };\n if (chromium) return { reason: 'Turned off in this browser', topic: 'browsers' };\n return { reason: 'Needs Chrome or Edge', topic: 'browsers' };\n}\n\n/** Stand-ins for the platform-only signature below. */\nconst PLATFORM_ENVS: Record<Platform, Partial<BrowserEnv>> = {\n ios: { ua: 'iPhone', touchPoints: 5, brave: false },\n android: { ua: 'Android Firefox', touchPoints: 5, brave: false },\n desktop: { ua: '', touchPoints: 0, brave: false },\n};\n\n/** supportIssue's reason alone. */\nexport function unsupportedReason(kind: ConnectionKind, platform?: Platform): string | null {\n return supportIssue(kind, platform ? PLATFORM_ENVS[platform] : {})?.reason ?? null;\n}\n\n/**\n * Errors browsers raise when Bluetooth is there but cannot be used: the\n * adapter is off or missing, or the browser turned the API off. The Help topic\n * to point to, or null.\n */\nexport function connectErrorTopic(err: unknown): SetupTopic | null {\n const message = err instanceof Error ? err.message : String(err);\n if (/globally disabled/i.test(message)) return 'brave';\n if (/adapter not available|Bluetooth adapter|Bluetooth is (turned off|disabled|not available)/i.test(message)) return detectBrowser().os === 'linux' ? 'linux' : 'browsers';\n return null;\n}\n","/**\n * libniimai: connect to NIIMBOT, TSPL, ESC/POS and cat printers from the\n * browser over Web Bluetooth or Web Serial, and print a canvas on them.\n */\n/// <reference types=\"web-bluetooth\" preserve=\"true\" />\n/// <reference types=\"w3c-web-serial\" preserve=\"true\" />\nimport { BluetoothPrinter } from './bluetooth';\nimport type { ClientLike, ConnectionKind } from './client';\nimport { MockClient } from './mock';\nimport { PrinterService, type ClientFactory } from './service';\n\n// niimbluelib loads only when it is needed, so pages that never meet a NIIMBOT stay light.\nconst niimbot = () => import('@mmote/niimbluelib');\n\n/**\n * A client for one kind of connection. Bluetooth finds the printer family\n * from the device the user picks; serial is NIIMBOT only; demo is a printer\n * in memory.\n */\nexport async function createClient(kind: ConnectionKind): Promise<ClientLike> {\n if (kind === 'demo') return new MockClient();\n if (kind === 'bluetooth') return new BluetoothPrinter(niimbot);\n const lib = await niimbot();\n return new lib.NiimbotSerialClient() as unknown as ClientLike;\n}\n\nexport interface CreatePrinterOptions {\n /**\n * Makes the client for each connection. Defaults to createClient; wrap it\n * to change one kind of connection and keep the others.\n */\n createClient?: ClientFactory;\n}\n\n/** A ready printer service: connect, watch its status, print canvases. */\nexport function createPrinter(options: CreatePrinterOptions = {}): PrinterService {\n return new PrinterService(options.createClient ?? createClient, async (canvas, direction) =>\n (await niimbot()).ImageEncoder.encodeCanvas(canvas, 0, direction),\n );\n}\n\nexport { PrinterService, DEMO_NAME, friendlyError, fitToPrinthead, rotateCanvas, shiftCanvas } from './service';\nexport type { ClientFactory, PageSource, PrinterState, PrinterStatus, PrintOffset, PrintOptions, Rotation } from './service';\n\nexport { connectErrorTopic, detectBrowser, detectPlatform, supportIssue, supportsBluetooth, supportsSerial, unsupportedReason } from './client';\nexport type { BrowserEnv, BrowserInfo, BrowserName, ClientLike, ConnectionKind, OperatingSystem, Platform, PrintTaskLike, SetupTopic, SupportIssue } from './client';\n\nexport { correctTask, findManualProfile, inkOutsideHead, MANUAL_PROFILES, printheadCrop, printheadMm, resolveProfile } from './profiles';\nexport type { LabelSize, PrintDirection, PrinterProfile } from './profiles';\n\nexport { effectivePaper, labelTypeOf, PAPER_TYPES, paperFromLabelType, paperName } from './paper';\nexport type { PaperType } from './paper';\n\nexport { findPrinterType, identifyPrinter, PRINTER_CATALOG, PRINTER_TYPES } from './catalog';\nexport type { CatalogEntry, PrinterFamily, PrinterType } from './catalog';\n\nexport { BluetoothPrinter } from './bluetooth';\nexport { describeBleError } from './ble';\nexport { DEMO_META, MockClient } from './mock';\n\nexport { modelFor, THERMAL_DEFAULT_SIZE, THERMAL_HEAD_DOTS, THERMAL_HEAD_MM, THERMAL_SIZES, ThermalPrinterClient, webTransport } from './thermal';\nexport type { Confidence, Pace, ThermalModel, ThermalTransport } from './thermal';\n\nexport { CAT_MODELS, CAT_UNSUPPORTED, CatPrinterClient, catFeed, catModelFor, printJob as catJob } from './cat';\nexport type { CatModel } from './cat';\nexport { ESCPOS_MODELS, EscPosPrinterClient, escposFeed, escposJob, escposModelFor } from './escpos';\nexport { TSPL_MODELS, TSPL_SIZES, TsplPrinterClient, tsplJob, tsplModelFor } from './tspl';\n"],"mappings":"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"}
|
package/dist/service.d.ts
CHANGED
|
@@ -52,7 +52,15 @@ export interface PrintOptions {
|
|
|
52
52
|
* much feed in place of the tear-off feed, so prints stay on the labels.
|
|
53
53
|
*/
|
|
54
54
|
gapMm?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Turns each page clockwise by this many degrees before it is sent, for
|
|
57
|
+
* labels designed landscape: the page is drawn the way it reads and prints
|
|
58
|
+
* turned onto the paper. Applied before `offset`, which stays relative to the paper.
|
|
59
|
+
*/
|
|
60
|
+
rotate?: Rotation;
|
|
55
61
|
}
|
|
62
|
+
/** Quarter turns, in degrees clockwise. */
|
|
63
|
+
export type Rotation = 0 | 90 | 180 | 270;
|
|
56
64
|
/** Different labels in one job, drawn one at a time so a long batch never holds every page at once. */
|
|
57
65
|
export interface PageSource {
|
|
58
66
|
count: number;
|
|
@@ -64,6 +72,8 @@ export type ClientFactory = (kind: ConnectionKind) => Promise<ClientLike>;
|
|
|
64
72
|
export declare const DEMO_NAME = "Demo printer";
|
|
65
73
|
/** The page moved by whole pixels on a white canvas of the same size. */
|
|
66
74
|
export declare function shiftCanvas(canvas: HTMLCanvasElement, dx: number, dy: number): HTMLCanvasElement;
|
|
75
|
+
/** The page turned clockwise by `degrees`, a multiple of 90. Returns the canvas itself for no turn. */
|
|
76
|
+
export declare function rotateCanvas(canvas: HTMLCanvasElement, degrees: number): HTMLCanvasElement;
|
|
67
77
|
/** Crops the page to what the printhead covers. Returns the canvas itself when it already fits. */
|
|
68
78
|
export declare function fitToPrinthead(canvas: HTMLCanvasElement, profile: PrinterProfile): HTMLCanvasElement;
|
|
69
79
|
/** Translates the library's error codes and messages into short, readable ones. */
|