libniimai 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/dist/bluetooth.d.ts +28 -43
- package/dist/catalog.d.ts +21 -1
- package/dist/client.d.ts +17 -6
- package/dist/escpos.d.ts +5 -1
- package/dist/index.d.ts +8 -5
- package/dist/libniimai.js +386 -85
- package/dist/libniimai.js.map +1 -1
- package/dist/routed.d.ts +54 -0
- package/dist/service.d.ts +17 -0
- package/dist/thermal.d.ts +5 -1
- package/dist/usb.d.ts +39 -0
- package/package.json +4 -2
package/dist/libniimai.js
CHANGED
|
@@ -198,7 +198,7 @@ function packRows(data, width, height, order, head = 384) {
|
|
|
198
198
|
}
|
|
199
199
|
return rows;
|
|
200
200
|
}
|
|
201
|
-
var sleep$
|
|
201
|
+
var sleep$2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
202
202
|
/**
|
|
203
203
|
* A thermal printer behind the same interface as a NIIMBOT client, so the
|
|
204
204
|
* printer service drives both. It reports itself through model metadata with
|
|
@@ -227,7 +227,7 @@ var ThermalPrinterClient = class {
|
|
|
227
227
|
constructor(transport, pace, firstChunk) {
|
|
228
228
|
this.transport = transport;
|
|
229
229
|
this.pace = pace;
|
|
230
|
-
this.chunk = firstChunk;
|
|
230
|
+
this.chunk = transport.chunkBytes ?? firstChunk;
|
|
231
231
|
transport.onDisconnect(() => {
|
|
232
232
|
this.connected = false;
|
|
233
233
|
this.emit("disconnect", {});
|
|
@@ -288,7 +288,7 @@ var ThermalPrinterClient = class {
|
|
|
288
288
|
/** Sends bytes in chunks, waiting while the printer asks for a pause. `onSent` hears the share sent, 0–1. */
|
|
289
289
|
async send(bytes, onSent) {
|
|
290
290
|
for (let i = 0; i < bytes.length;) {
|
|
291
|
-
while (this.paused) await sleep$
|
|
291
|
+
while (this.paused) await sleep$2(this.pace.pauseMs);
|
|
292
292
|
const part = bytes.subarray(i, i + this.chunk);
|
|
293
293
|
try {
|
|
294
294
|
await this.transport.write(part);
|
|
@@ -299,7 +299,7 @@ var ThermalPrinterClient = class {
|
|
|
299
299
|
}
|
|
300
300
|
i += part.length;
|
|
301
301
|
onSent?.(i / bytes.length);
|
|
302
|
-
await sleep$
|
|
302
|
+
await sleep$2(this.pace.chunkMs);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
protocol = {
|
|
@@ -835,6 +835,8 @@ var EscPosPrinterClient = class extends ThermalPrinterClient {
|
|
|
835
835
|
densityLevels = 1;
|
|
836
836
|
modelName;
|
|
837
837
|
headDots;
|
|
838
|
+
/** A stock module name or a printer matched by its services: the head's width is a guess until set. */
|
|
839
|
+
headUnknown;
|
|
838
840
|
constructor(transport, model, pace = {
|
|
839
841
|
chunkMs: 30,
|
|
840
842
|
pauseMs: 100
|
|
@@ -843,6 +845,11 @@ var EscPosPrinterClient = class extends ThermalPrinterClient {
|
|
|
843
845
|
this.model = model;
|
|
844
846
|
this.modelName = shownName(model, transport, "Thermal printer");
|
|
845
847
|
this.headDots = model?.headDots ?? 384;
|
|
848
|
+
this.headUnknown = !!model?.generic && model.headDots === void 0;
|
|
849
|
+
}
|
|
850
|
+
/** Sets the head's width, in whole bytes of dots, on a printer whose width is not known; a known head keeps its own. */
|
|
851
|
+
setHeadDots(dots) {
|
|
852
|
+
if (this.headUnknown) this.headDots = Math.max(8, Math.round(dots / 8) * 8);
|
|
846
853
|
}
|
|
847
854
|
encodeRows(data, width, height) {
|
|
848
855
|
return encodeRows$1(data, width, height, this.headDots);
|
|
@@ -868,7 +875,8 @@ var DOTS_PER_MM = 8;
|
|
|
868
875
|
var TSPL_MODELS = [{
|
|
869
876
|
prefix: "QR380A",
|
|
870
877
|
confidence: "tested",
|
|
871
|
-
note: "Sold as CC390"
|
|
878
|
+
note: "Sold as CC390",
|
|
879
|
+
usbIds: [[10473, 649]]
|
|
872
880
|
}, {
|
|
873
881
|
prefix: "GP-M322",
|
|
874
882
|
confidence: "medium",
|
|
@@ -1033,13 +1041,6 @@ var TsplPrinterClient = class extends ThermalPrinterClient {
|
|
|
1033
1041
|
}
|
|
1034
1042
|
};
|
|
1035
1043
|
// src/catalog.ts
|
|
1036
|
-
/**
|
|
1037
|
-
* Every printer outside NIIMBOT that libniimai knows, by Bluetooth name, in
|
|
1038
|
-
* one lookup across families. A name matches one family at most, and a name
|
|
1039
|
-
* that matches none is known to be unknown before anything is sent to it.
|
|
1040
|
-
* The models themselves live with their family: ESCPOS_MODELS, TSPL_MODELS
|
|
1041
|
-
* and CAT_MODELS.
|
|
1042
|
-
*/
|
|
1043
1044
|
var PRINTER_CATALOG = [
|
|
1044
1045
|
...ESCPOS_MODELS.map((model) => ({
|
|
1045
1046
|
family: "escpos",
|
|
@@ -1061,6 +1062,20 @@ function identifyPrinter(name) {
|
|
|
1061
1062
|
for (const entry of PRINTER_CATALOG) if (name.startsWith(entry.model.prefix) && entry.model.prefix.length > (best?.model.prefix.length ?? 0)) best = entry;
|
|
1062
1063
|
return best;
|
|
1063
1064
|
}
|
|
1065
|
+
/** The known printer behind a USB device: by its vendor and product IDs, then by its product name. */
|
|
1066
|
+
function identifyUsbPrinter({ vendorId, productId, productName }) {
|
|
1067
|
+
return PRINTER_CATALOG.find((e) => e.model.usbIds?.some(([v, p]) => v === vendorId && p === productId)) ?? identifyPrinter(productName);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* The client for a thermal printer over a cable link. Cat printers are
|
|
1071
|
+
* Bluetooth only, so they get null. `ackTimeoutMs`: how long a label printer
|
|
1072
|
+
* waits for its "OK" before asking for its status.
|
|
1073
|
+
*/
|
|
1074
|
+
function cableClient({ family, model }, transport, pace, ackTimeoutMs) {
|
|
1075
|
+
if (family === "tspl") return new TsplPrinterClient(transport, model, pace, ackTimeoutMs);
|
|
1076
|
+
if (family === "escpos") return new EscPosPrinterClient(transport, model, pace);
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1064
1079
|
/** The kinds of printer an unrecognised one can be set to, alongside NIIMBOT's MANUAL_PROFILES. */
|
|
1065
1080
|
var PRINTER_TYPES = [
|
|
1066
1081
|
{
|
|
@@ -1108,6 +1123,76 @@ var PRINTER_TYPES = [
|
|
|
1108
1123
|
}
|
|
1109
1124
|
];
|
|
1110
1125
|
var findPrinterType = (id) => PRINTER_TYPES.find((t) => t.id === id) ?? null;
|
|
1126
|
+
// src/routed.ts
|
|
1127
|
+
/**
|
|
1128
|
+
* A client that picks the real client when it connects, then passes
|
|
1129
|
+
* everything through to it: the Bluetooth and USB printers behind the
|
|
1130
|
+
* Connect buttons.
|
|
1131
|
+
*/
|
|
1132
|
+
var RoutedClient = class {
|
|
1133
|
+
inner = null;
|
|
1134
|
+
get client() {
|
|
1135
|
+
if (!this.inner) throw new Error("Not connected");
|
|
1136
|
+
return this.inner;
|
|
1137
|
+
}
|
|
1138
|
+
async disconnect() {
|
|
1139
|
+
await this.inner?.disconnect();
|
|
1140
|
+
}
|
|
1141
|
+
isConnected() {
|
|
1142
|
+
return !!this.inner?.isConnected();
|
|
1143
|
+
}
|
|
1144
|
+
on(event, fn) {
|
|
1145
|
+
return this.client.on(event, fn);
|
|
1146
|
+
}
|
|
1147
|
+
off(event, fn) {
|
|
1148
|
+
return this.inner?.off(event, fn);
|
|
1149
|
+
}
|
|
1150
|
+
getPrinterInfo() {
|
|
1151
|
+
return this.inner?.getPrinterInfo() ?? {};
|
|
1152
|
+
}
|
|
1153
|
+
getModelMetadata() {
|
|
1154
|
+
return this.inner?.getModelMetadata();
|
|
1155
|
+
}
|
|
1156
|
+
getPrintTaskType() {
|
|
1157
|
+
return this.inner?.getPrintTaskType();
|
|
1158
|
+
}
|
|
1159
|
+
getRfidInfo() {
|
|
1160
|
+
return this.inner?.getRfidInfo() ?? {};
|
|
1161
|
+
}
|
|
1162
|
+
fetchRfidInfo() {
|
|
1163
|
+
return this.client.fetchRfidInfo();
|
|
1164
|
+
}
|
|
1165
|
+
startHeartbeat() {
|
|
1166
|
+
this.inner?.startHeartbeat();
|
|
1167
|
+
}
|
|
1168
|
+
stopHeartbeat() {
|
|
1169
|
+
this.inner?.stopHeartbeat();
|
|
1170
|
+
}
|
|
1171
|
+
isHeartbeatStarted() {
|
|
1172
|
+
return !!this.inner?.isHeartbeatStarted?.();
|
|
1173
|
+
}
|
|
1174
|
+
get protocol() {
|
|
1175
|
+
return this.client.protocol;
|
|
1176
|
+
}
|
|
1177
|
+
get encodePage() {
|
|
1178
|
+
return this.inner?.encodePage?.bind(this.inner);
|
|
1179
|
+
}
|
|
1180
|
+
get rollKey() {
|
|
1181
|
+
return this.inner?.rollKey?.bind(this.inner);
|
|
1182
|
+
}
|
|
1183
|
+
get feedPaper() {
|
|
1184
|
+
return this.inner?.feedPaper?.bind(this.inner);
|
|
1185
|
+
}
|
|
1186
|
+
get headUnknown() {
|
|
1187
|
+
return this.inner?.headUnknown;
|
|
1188
|
+
}
|
|
1189
|
+
get setHeadDots() {
|
|
1190
|
+
return this.inner?.setHeadDots?.bind(this.inner);
|
|
1191
|
+
}
|
|
1192
|
+
get feedsBack() {
|
|
1193
|
+
return this.inner?.feedsBack;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1111
1196
|
// src/bluetooth.ts
|
|
1112
1197
|
/**
|
|
1113
1198
|
* A writable characteristic on one of the serial services budget printers
|
|
@@ -1124,7 +1209,7 @@ async function writeChannel(server, services) {
|
|
|
1124
1209
|
}
|
|
1125
1210
|
return null;
|
|
1126
1211
|
}
|
|
1127
|
-
var PRINTER_TYPE_NAMES = {
|
|
1212
|
+
var PRINTER_TYPE_NAMES$1 = {
|
|
1128
1213
|
escpos: "receipt printer",
|
|
1129
1214
|
tspl: "label printer",
|
|
1130
1215
|
cat: "cat printer"
|
|
@@ -1141,19 +1226,68 @@ async function thermalClient(device, server, services, { family, model }) {
|
|
|
1141
1226
|
write: TSPL_WRITE,
|
|
1142
1227
|
notify: TSPL_NOTIFY
|
|
1143
1228
|
}), model) : null;
|
|
1144
|
-
const
|
|
1229
|
+
const fixed = services.includes("000018f0-0000-1000-8000-00805f9b34fb") ? {
|
|
1145
1230
|
service: ESCPOS_SERVICE,
|
|
1146
1231
|
write: ESCPOS_WRITE
|
|
1147
1232
|
} : services.includes("e7810a71-73ae-499d-8c15-faa9aef0c3f2") ? {
|
|
1148
1233
|
service: ESCPOS_ALT_SERVICE,
|
|
1149
1234
|
write: ESCPOS_ALT_WRITE
|
|
1150
|
-
} :
|
|
1235
|
+
} : null;
|
|
1236
|
+
const transport = fixed && await webTransport(device, server, fixed).catch(() => null);
|
|
1237
|
+
if (transport) return new EscPosPrinterClient(transport, model);
|
|
1238
|
+
const channel = await writeChannel(server, services);
|
|
1151
1239
|
return channel ? new EscPosPrinterClient(await webTransport(device, server, channel), model) : null;
|
|
1152
1240
|
}
|
|
1241
|
+
/** NIIMBOT printers offer one of these; a printer with neither cannot be one. */
|
|
1242
|
+
var NIIMBOT_SERVICES = CANDIDATE_SERVICES.slice(0, 2);
|
|
1243
|
+
/** A receipt printer matched by its services: its head's width is unknown until the paper says. */
|
|
1244
|
+
var RECEIPT = {
|
|
1245
|
+
family: "escpos",
|
|
1246
|
+
model: {
|
|
1247
|
+
prefix: "",
|
|
1248
|
+
confidence: "medium",
|
|
1249
|
+
generic: true
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
var CAT = findPrinterType("cat");
|
|
1253
|
+
/**
|
|
1254
|
+
* What an unknown printer that did not answer as a NIIMBOT is, by its
|
|
1255
|
+
* services: the cat service makes it a cat printer, and any other serial
|
|
1256
|
+
* service outside NIIMBOT's a receipt printer. Null when it offers only
|
|
1257
|
+
* NIIMBOT services, or nothing to print through: those stay NIIMBOT's.
|
|
1258
|
+
*/
|
|
1259
|
+
function byServices(services) {
|
|
1260
|
+
if (services.includes("0000ae30-0000-1000-8000-00805f9b34fb")) return CAT;
|
|
1261
|
+
return services.some((s) => CANDIDATE_SERVICES.includes(s) && !NIIMBOT_SERVICES.includes(s)) ? RECEIPT : null;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Clears out the NIIMBOT tries a receipt printer took as text: FS . leaves
|
|
1265
|
+
* Chinese character mode, so no half character swallows what follows; two
|
|
1266
|
+
* line feeds print what is left in the buffer, even after a dangling byte;
|
|
1267
|
+
* ESC @ resets the printer.
|
|
1268
|
+
*/
|
|
1269
|
+
var FLUSH = Uint8Array.from([
|
|
1270
|
+
28,
|
|
1271
|
+
46,
|
|
1272
|
+
10,
|
|
1273
|
+
10,
|
|
1274
|
+
27,
|
|
1275
|
+
64
|
|
1276
|
+
]);
|
|
1277
|
+
var sleep$1 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1278
|
+
/** The printer's GATT server again after a failed attempt dropped the link, giving it a moment between tries. */
|
|
1279
|
+
async function reconnect(device) {
|
|
1280
|
+
for (let i = 1; i <= 3; i++) {
|
|
1281
|
+
await sleep$1(250 * i);
|
|
1282
|
+
const server = await device.gatt?.connect().catch(() => null);
|
|
1283
|
+
if (server) return server;
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1153
1287
|
/** The name starts like one of niimbluelib's NIIMBOT models ("D110-…", "B21_…"). */
|
|
1154
1288
|
function looksNiimbot(lib, name) {
|
|
1155
1289
|
const token = name?.split(/[-_ ]/)[0].toUpperCase();
|
|
1156
|
-
return !!token && lib.modelsLibrary
|
|
1290
|
+
return !!token && !!lib.modelsLibrary?.some((m) => m.model.split("_")[0] === token);
|
|
1157
1291
|
}
|
|
1158
1292
|
/**
|
|
1159
1293
|
* The Bluetooth printer behind the Connect button. One chooser lists NIIMBOT
|
|
@@ -1164,22 +1298,26 @@ function looksNiimbot(lib, name) {
|
|
|
1164
1298
|
* the TSPL client; and everything else the NIIMBOT client exactly as before.
|
|
1165
1299
|
* Known names never overlap, so each printer matches one client at most.
|
|
1166
1300
|
*/
|
|
1167
|
-
var BluetoothPrinter = class {
|
|
1301
|
+
var BluetoothPrinter = class extends RoutedClient {
|
|
1168
1302
|
lib;
|
|
1169
|
-
inner = null;
|
|
1170
1303
|
constructor(lib) {
|
|
1304
|
+
super();
|
|
1171
1305
|
this.lib = lib;
|
|
1172
1306
|
}
|
|
1173
|
-
get client() {
|
|
1174
|
-
if (!this.inner) throw new Error("Not connected");
|
|
1175
|
-
return this.inner;
|
|
1176
|
-
}
|
|
1177
1307
|
/** The kind of printer to treat an unrecognised one as; null leaves it to NIIMBOT. */
|
|
1178
1308
|
fallback = null;
|
|
1179
1309
|
unrecognisedAs(typeId) {
|
|
1180
1310
|
this.fallback = findPrinterType(typeId);
|
|
1181
1311
|
}
|
|
1312
|
+
/** Printers matched by their services before, by Bluetooth name: they skip the NIIMBOT handshake. */
|
|
1313
|
+
remembered = {};
|
|
1314
|
+
/** What the printer connected now was matched as by its services, if it was. */
|
|
1315
|
+
matchedAs;
|
|
1316
|
+
rememberMatches(matches) {
|
|
1317
|
+
this.remembered = matches;
|
|
1318
|
+
}
|
|
1182
1319
|
async connect() {
|
|
1320
|
+
this.matchedAs = void 0;
|
|
1183
1321
|
const device = await requestPrinter(PRINTER_CATALOG.map((e) => e.model.prefix), !!this.fallback);
|
|
1184
1322
|
const known = identifyPrinter(device.name);
|
|
1185
1323
|
const fallback = !known && this.fallback && !looksNiimbot(await this.lib(), device.name) ? this.fallback : null;
|
|
@@ -1189,67 +1327,53 @@ var BluetoothPrinter = class {
|
|
|
1189
1327
|
const services = (await server.getPrimaryServices().catch(() => [])).map((s) => s.uuid);
|
|
1190
1328
|
this.inner = await thermalClient(device, server, services, entry);
|
|
1191
1329
|
if (this.inner) return this.inner.connect();
|
|
1192
|
-
if (!known) throw new Error(`${device.name ?? "This printer"} does not offer a ${PRINTER_TYPE_NAMES[entry.family]} service`);
|
|
1330
|
+
if (!known) throw new Error(`${device.name ?? "This printer"} does not offer a ${PRINTER_TYPE_NAMES$1[entry.family]} service`);
|
|
1193
1331
|
}
|
|
1194
1332
|
const lib = await this.lib();
|
|
1333
|
+
const before = !entry && device.name && !looksNiimbot(lib, device.name) ? this.remembered[device.name] : void 0;
|
|
1334
|
+
if (before && device.gatt) {
|
|
1335
|
+
const server = await device.gatt.connect();
|
|
1336
|
+
const client = await thermalClient(device, server, (await server.getPrimaryServices().catch(() => [])).map((s) => s.uuid), before === "cat" ? CAT : RECEIPT).catch(() => null);
|
|
1337
|
+
if (client) {
|
|
1338
|
+
this.inner = client;
|
|
1339
|
+
this.matchedAs = before;
|
|
1340
|
+
return client.connect();
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1195
1343
|
this.inner = createBleClient(lib, device);
|
|
1196
1344
|
try {
|
|
1197
1345
|
return await this.inner.connect();
|
|
1198
1346
|
} catch (err) {
|
|
1199
|
-
if (looksNiimbot(lib, device.name)) throw err;
|
|
1200
|
-
|
|
1347
|
+
if (looksNiimbot(lib, device.name) || entry) throw err;
|
|
1348
|
+
return this.unknown(device, err);
|
|
1201
1349
|
}
|
|
1202
1350
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
startHeartbeat() {
|
|
1231
|
-
this.inner?.startHeartbeat();
|
|
1232
|
-
}
|
|
1233
|
-
stopHeartbeat() {
|
|
1234
|
-
this.inner?.stopHeartbeat();
|
|
1235
|
-
}
|
|
1236
|
-
isHeartbeatStarted() {
|
|
1237
|
-
return !!this.inner?.isHeartbeatStarted?.();
|
|
1238
|
-
}
|
|
1239
|
-
get protocol() {
|
|
1240
|
-
return this.client.protocol;
|
|
1241
|
-
}
|
|
1242
|
-
get encodePage() {
|
|
1243
|
-
return this.inner?.encodePage?.bind(this.inner);
|
|
1244
|
-
}
|
|
1245
|
-
get rollKey() {
|
|
1246
|
-
return this.inner?.rollKey?.bind(this.inner);
|
|
1247
|
-
}
|
|
1248
|
-
get feedPaper() {
|
|
1249
|
-
return this.inner?.feedPaper?.bind(this.inner);
|
|
1250
|
-
}
|
|
1251
|
-
get feedsBack() {
|
|
1252
|
-
return this.inner?.feedsBack;
|
|
1351
|
+
/**
|
|
1352
|
+
* An unknown name that failed its first NIIMBOT try. NIIMBOT goes first
|
|
1353
|
+
* again, so an unknown NIIMBOT that missed one handshake still gets its
|
|
1354
|
+
* own client; only after a second miss do its services decide, and a
|
|
1355
|
+
* printer offering only NIIMBOT services is never handed to another client.
|
|
1356
|
+
*/
|
|
1357
|
+
async unknown(device, first) {
|
|
1358
|
+
await sleep$1(250);
|
|
1359
|
+
try {
|
|
1360
|
+
return await this.inner.connect();
|
|
1361
|
+
} catch {}
|
|
1362
|
+
const server = await reconnect(device);
|
|
1363
|
+
const services = server ? (await server.getPrimaryServices().catch(() => [])).map((s) => s.uuid) : [];
|
|
1364
|
+
console.info("[niimai] services", device.name, services);
|
|
1365
|
+
const guess = byServices(services);
|
|
1366
|
+
const client = server && guess ? await thermalClient(device, server, services, guess).catch(() => null) : null;
|
|
1367
|
+
if (client) {
|
|
1368
|
+
this.inner = client;
|
|
1369
|
+
const info = await client.connect();
|
|
1370
|
+
this.matchedAs = guess.family === "cat" ? "cat" : "escpos";
|
|
1371
|
+
if (this.matchedAs === "escpos") await client.send(FLUSH).catch(() => {});
|
|
1372
|
+
return info;
|
|
1373
|
+
}
|
|
1374
|
+
console.warn("[niimai] connect failed", device.name, first);
|
|
1375
|
+
if (!services.length) throw new Error(`${device.name ?? "This printer"} offers no print service niimai knows. See Help → Printer not recognised.`);
|
|
1376
|
+
throw new Error(`Could not connect to ${device.name ?? "this printer"}. Pick its type under Advanced and connect again.`);
|
|
1253
1377
|
}
|
|
1254
1378
|
};
|
|
1255
1379
|
// src/mock.ts
|
|
@@ -1621,7 +1745,7 @@ function withTimeout(p, ms) {
|
|
|
1621
1745
|
/** Translates the library's error codes and messages into short, readable ones. */
|
|
1622
1746
|
function friendlyError(err) {
|
|
1623
1747
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1624
|
-
if (/user cancelled|no device selected|chooser/i.test(msg)) return "Connection cancelled";
|
|
1748
|
+
if (/user cancelled|no (device|port) selected|chooser/i.test(msg)) return "Connection cancelled";
|
|
1625
1749
|
if (/timeout waiting for the printer to finish/i.test(msg)) return "The printer did not confirm the print";
|
|
1626
1750
|
if (/timeout/i.test(msg)) return "Printer stopped responding";
|
|
1627
1751
|
if (/lid|cover/i.test(msg)) return "Close the printer lid";
|
|
@@ -1668,6 +1792,16 @@ var PrinterService = class {
|
|
|
1668
1792
|
this.status = status;
|
|
1669
1793
|
this.listeners.forEach((fn) => fn(this.status));
|
|
1670
1794
|
}
|
|
1795
|
+
matches = {};
|
|
1796
|
+
/**
|
|
1797
|
+
* Printers matched by their services on earlier connections, by Bluetooth
|
|
1798
|
+
* name (from `status.matchedAs` and `status.deviceName`). They connect
|
|
1799
|
+
* straight to that client, skipping the NIIMBOT handshake, whose bytes a
|
|
1800
|
+
* receipt printer would print as text.
|
|
1801
|
+
*/
|
|
1802
|
+
rememberMatches(matches) {
|
|
1803
|
+
this.matches = matches;
|
|
1804
|
+
}
|
|
1671
1805
|
setManualProfile(id) {
|
|
1672
1806
|
this.manualProfileId = id;
|
|
1673
1807
|
if (this.client) this.set({ profile: this.currentProfile() });
|
|
@@ -1711,6 +1845,7 @@ var PrinterService = class {
|
|
|
1711
1845
|
try {
|
|
1712
1846
|
client = await this.createClient(kind);
|
|
1713
1847
|
client.unrecognisedAs?.(this.manualProfileId);
|
|
1848
|
+
client.rememberMatches?.(this.matches);
|
|
1714
1849
|
const info = await client.connect();
|
|
1715
1850
|
this.client = client;
|
|
1716
1851
|
this.set({ deviceName: info.deviceName });
|
|
@@ -1749,7 +1884,9 @@ var PrinterService = class {
|
|
|
1749
1884
|
profile: this.currentProfile(),
|
|
1750
1885
|
label: this.readRfid(),
|
|
1751
1886
|
...client.feedPaper ? { canFeed: true } : {},
|
|
1752
|
-
...client.feedPaper && client.feedsBack ? { canRetract: true } : {}
|
|
1887
|
+
...client.feedPaper && client.feedsBack ? { canRetract: true } : {},
|
|
1888
|
+
...client.headUnknown && client.setHeadDots ? { headUnknown: true } : {},
|
|
1889
|
+
...client.matchedAs ? { matchedAs: client.matchedAs } : {}
|
|
1753
1890
|
});
|
|
1754
1891
|
client.startHeartbeat();
|
|
1755
1892
|
}
|
|
@@ -1787,6 +1924,18 @@ var PrinterService = class {
|
|
|
1787
1924
|
} });
|
|
1788
1925
|
}
|
|
1789
1926
|
/**
|
|
1927
|
+
* Sets the print width, in mm, on a printer whose head width is not known
|
|
1928
|
+
* (`status.headUnknown`); the profile follows. Other printers keep theirs.
|
|
1929
|
+
*/
|
|
1930
|
+
setPrintWidth(mm) {
|
|
1931
|
+
const client = this.client;
|
|
1932
|
+
const profile = this.status.profile;
|
|
1933
|
+
if (!client?.setHeadDots || !this.status.headUnknown || !profile) return;
|
|
1934
|
+
client.setHeadDots(mmToPx(mm, profile.dpi));
|
|
1935
|
+
const next = this.currentProfile();
|
|
1936
|
+
if (next?.printheadPixels !== profile.printheadPixels) this.set({ profile: next });
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1790
1939
|
* Moves the paper by `mm`, out of the printer when positive and back in when
|
|
1791
1940
|
* negative, to line it up on printers that cannot sense it.
|
|
1792
1941
|
*/
|
|
@@ -1883,6 +2032,12 @@ var PrinterService = class {
|
|
|
1883
2032
|
// src/client.ts
|
|
1884
2033
|
var supportsBluetooth = () => typeof navigator !== "undefined" && "bluetooth" in navigator;
|
|
1885
2034
|
var supportsSerial = () => typeof navigator !== "undefined" && "serial" in navigator;
|
|
2035
|
+
var supportsUsb = () => typeof navigator !== "undefined" && "usb" in navigator;
|
|
2036
|
+
var SUPPORTS = {
|
|
2037
|
+
bluetooth: supportsBluetooth,
|
|
2038
|
+
serial: supportsSerial,
|
|
2039
|
+
usb: supportsUsb
|
|
2040
|
+
};
|
|
1886
2041
|
function detectPlatform(ua = typeof navigator === "undefined" ? "" : navigator.userAgent, touchPoints = typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints) {
|
|
1887
2042
|
if (/iPhone|iPad|iPod/.test(ua) || /Macintosh/.test(ua) && touchPoints > 1) return "ios";
|
|
1888
2043
|
if (/Android/.test(ua)) return "android";
|
|
@@ -1928,7 +2083,7 @@ var BROWSER_NAMES = {
|
|
|
1928
2083
|
*/
|
|
1929
2084
|
function supportIssue(kind, env = {}) {
|
|
1930
2085
|
if (kind === "demo") return null;
|
|
1931
|
-
if (kind
|
|
2086
|
+
if (SUPPORTS[kind]()) return null;
|
|
1932
2087
|
const { os, browser, chromium, secure } = detectBrowser(env);
|
|
1933
2088
|
const bluetooth = kind === "bluetooth";
|
|
1934
2089
|
if (os === "ios") return {
|
|
@@ -1994,36 +2149,182 @@ function unsupportedReason(kind, platform) {
|
|
|
1994
2149
|
return supportIssue(kind, platform ? PLATFORM_ENVS[platform] : {})?.reason ?? null;
|
|
1995
2150
|
}
|
|
1996
2151
|
/**
|
|
1997
|
-
* Errors browsers raise when Bluetooth is there but cannot be used:
|
|
1998
|
-
* adapter is off or missing,
|
|
1999
|
-
* to point to, or null.
|
|
2152
|
+
* Errors browsers raise when Bluetooth or USB is there but cannot be used:
|
|
2153
|
+
* the adapter is off or missing, the browser turned the API off, or the
|
|
2154
|
+
* system keeps a USB printer for itself. The Help topic to point to, or null.
|
|
2000
2155
|
*/
|
|
2001
2156
|
function connectErrorTopic(err) {
|
|
2002
2157
|
const message = err instanceof Error ? err.message : String(err);
|
|
2003
2158
|
if (/globally disabled/i.test(message)) return "brave";
|
|
2159
|
+
if (/claim interface|Access denied|Unable to open device/i.test(message)) return "usb";
|
|
2004
2160
|
if (/adapter not available|Bluetooth adapter|Bluetooth is (turned off|disabled|not available)/i.test(message)) return detectBrowser().os === "linux" ? "linux" : "browsers";
|
|
2005
2161
|
return null;
|
|
2006
2162
|
}
|
|
2163
|
+
// src/usb.ts
|
|
2164
|
+
/**
|
|
2165
|
+
* Label and receipt printers on a USB cable that show up as standard USB
|
|
2166
|
+
* printers (interface class 7), which Web Serial cannot see. They take the
|
|
2167
|
+
* same TSPL or ESC/POS bytes as over Bluetooth, through WebUSB. NIIMBOT
|
|
2168
|
+
* printers show up as serial ports and keep using Web Serial.
|
|
2169
|
+
*/
|
|
2170
|
+
var USB_PRINTER_CLASS = 7;
|
|
2171
|
+
/** USB moves data far faster than Bluetooth and paces itself, so writes are large and back to back. */
|
|
2172
|
+
var USB_PACE = {
|
|
2173
|
+
chunkMs: 0,
|
|
2174
|
+
pauseMs: 100
|
|
2175
|
+
};
|
|
2176
|
+
var USB_CHUNK = 4096;
|
|
2177
|
+
/** Label printers may not send "OK" over USB; a status query after this long tells a fault from a quiet printer. */
|
|
2178
|
+
var USB_ACK_TIMEOUT_MS = 5e3;
|
|
2179
|
+
/**
|
|
2180
|
+
* The fields of an IEEE 1284 device ID, keys in capitals:
|
|
2181
|
+
* "MFG:ACME;COMMAND SET:TSC/ESC/POS;MODEL:CC390;" → { MFG, 'COMMAND SET', MODEL }.
|
|
2182
|
+
*/
|
|
2183
|
+
function parseDeviceId(text) {
|
|
2184
|
+
const fields = {};
|
|
2185
|
+
for (const part of text.split(";")) {
|
|
2186
|
+
const at = part.indexOf(":");
|
|
2187
|
+
if (at > 0) fields[part.slice(0, at).trim().toUpperCase()] = part.slice(at + 1).trim();
|
|
2188
|
+
}
|
|
2189
|
+
return fields;
|
|
2190
|
+
}
|
|
2191
|
+
/** The printer languages a device ID names, TSPL first: label printers often take ESC/POS as well. */
|
|
2192
|
+
function familiesFromDeviceId(text) {
|
|
2193
|
+
const id = parseDeviceId(text);
|
|
2194
|
+
const commands = id.CMD ?? id["COMMAND SET"] ?? "";
|
|
2195
|
+
const families = [];
|
|
2196
|
+
if (/\b(TSPL2?|TSC)\b/i.test(commands)) families.push("tspl");
|
|
2197
|
+
if (/ESC\/?POS|\bEPSON\b/i.test(commands)) families.push("escpos");
|
|
2198
|
+
return families;
|
|
2199
|
+
}
|
|
2200
|
+
var isPrinter = (device) => device.configuration?.interfaces.some((i) => i.alternates.some((a) => a.interfaceClass === 7)) ?? true;
|
|
2201
|
+
/** USB printers this site may already use that are plugged in. */
|
|
2202
|
+
async function rememberedUsbPrinters() {
|
|
2203
|
+
if (!supportsUsb()) return [];
|
|
2204
|
+
return (await navigator.usb.getDevices().catch(() => [])).filter(isPrinter);
|
|
2205
|
+
}
|
|
2206
|
+
/** Opens a USB printer and claims its printer interface. Also reads the printer's device ID. */
|
|
2207
|
+
async function usbTransport(device) {
|
|
2208
|
+
await device.open();
|
|
2209
|
+
if (!device.configuration) await device.selectConfiguration(1);
|
|
2210
|
+
const iface = device.configuration.interfaces.find((i) => i.alternates.some((a) => a.interfaceClass === 7));
|
|
2211
|
+
if (!iface) throw new Error(`${device.productName ?? "This device"} is not a USB printer`);
|
|
2212
|
+
const alt = iface.alternates.find((a) => a.interfaceClass === 7);
|
|
2213
|
+
await device.claimInterface(iface.interfaceNumber);
|
|
2214
|
+
if (alt.alternateSetting !== 0) await device.selectAlternateInterface(iface.interfaceNumber, alt.alternateSetting);
|
|
2215
|
+
const out = alt.endpoints.find((e) => e.direction === "out" && e.type === "bulk");
|
|
2216
|
+
const back = alt.endpoints.find((e) => e.direction === "in" && e.type === "bulk");
|
|
2217
|
+
if (!out) throw new Error(`${device.productName ?? "This printer"} has no way to send it data`);
|
|
2218
|
+
const deviceId = await device.controlTransferIn({
|
|
2219
|
+
requestType: "class",
|
|
2220
|
+
recipient: "interface",
|
|
2221
|
+
request: 0,
|
|
2222
|
+
value: 0,
|
|
2223
|
+
index: iface.interfaceNumber << 8 | alt.alternateSetting
|
|
2224
|
+
}, 1024).then((r) => r.data ? new TextDecoder().decode(new Uint8Array(r.data.buffer, r.data.byteOffset + 2, Math.max(0, r.data.byteLength - 2))) : "").catch(() => "");
|
|
2225
|
+
const notify = [];
|
|
2226
|
+
const gone = [];
|
|
2227
|
+
let open = true;
|
|
2228
|
+
if (back) (async () => {
|
|
2229
|
+
while (open) {
|
|
2230
|
+
const r = await device.transferIn(back.endpointNumber, back.packetSize).catch(() => null);
|
|
2231
|
+
if (!r) break;
|
|
2232
|
+
if (r.data?.byteLength) notify.forEach((fn) => fn(new Uint8Array(r.data.buffer.slice(r.data.byteOffset, r.data.byteOffset + r.data.byteLength))));
|
|
2233
|
+
}
|
|
2234
|
+
})();
|
|
2235
|
+
const onUnplug = (e) => {
|
|
2236
|
+
if (e.device !== device) return;
|
|
2237
|
+
open = false;
|
|
2238
|
+
navigator.usb.removeEventListener("disconnect", onUnplug);
|
|
2239
|
+
gone.forEach((fn) => fn());
|
|
2240
|
+
};
|
|
2241
|
+
navigator.usb.addEventListener("disconnect", onUnplug);
|
|
2242
|
+
return {
|
|
2243
|
+
deviceId,
|
|
2244
|
+
transport: {
|
|
2245
|
+
name: device.productName || void 0,
|
|
2246
|
+
chunkBytes: USB_CHUNK,
|
|
2247
|
+
write: async (chunk) => {
|
|
2248
|
+
if ((await device.transferOut(out.endpointNumber, chunk.slice())).status !== "ok") throw new Error("The printer did not take the data");
|
|
2249
|
+
},
|
|
2250
|
+
onNotify: (fn) => notify.push(fn),
|
|
2251
|
+
onDisconnect: (fn) => gone.push(fn),
|
|
2252
|
+
disconnect: async () => {
|
|
2253
|
+
open = false;
|
|
2254
|
+
navigator.usb.removeEventListener("disconnect", onUnplug);
|
|
2255
|
+
await device.releaseInterface(iface.interfaceNumber).catch(() => {});
|
|
2256
|
+
await device.close().catch(() => {});
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
var PRINTER_TYPE_NAMES = {
|
|
2262
|
+
escpos: "receipt printer",
|
|
2263
|
+
tspl: "label printer",
|
|
2264
|
+
cat: "cat printer"
|
|
2265
|
+
};
|
|
2266
|
+
/**
|
|
2267
|
+
* The USB printer behind the USB button for label and receipt printers. A
|
|
2268
|
+
* printer this site already uses connects straight away when it is the only
|
|
2269
|
+
* one plugged in; otherwise the browser lists USB printers. The printer's
|
|
2270
|
+
* language comes from the catalog, then from the type picked under Advanced
|
|
2271
|
+
* when the printer speaks it (or does not say), then from the device ID the
|
|
2272
|
+
* printer reports.
|
|
2273
|
+
*/
|
|
2274
|
+
var UsbPrinter = class extends RoutedClient {
|
|
2275
|
+
fallback = null;
|
|
2276
|
+
unrecognisedAs(typeId) {
|
|
2277
|
+
this.fallback = findPrinterType(typeId);
|
|
2278
|
+
}
|
|
2279
|
+
async connect() {
|
|
2280
|
+
const remembered = await rememberedUsbPrinters();
|
|
2281
|
+
const device = remembered.length === 1 ? remembered[0] : await navigator.usb.requestDevice({ filters: [{ classCode: 7 }] });
|
|
2282
|
+
const { transport, deviceId } = await usbTransport(device);
|
|
2283
|
+
const name = device.productName || "This printer";
|
|
2284
|
+
const families = familiesFromDeviceId(deviceId);
|
|
2285
|
+
const picked = this.fallback && (!families.length || families.includes(this.fallback.family)) ? this.fallback : null;
|
|
2286
|
+
const entry = identifyUsbPrinter(device) ?? picked ?? (families.length ? {
|
|
2287
|
+
family: families[0],
|
|
2288
|
+
model: {
|
|
2289
|
+
prefix: "",
|
|
2290
|
+
confidence: "medium",
|
|
2291
|
+
generic: true
|
|
2292
|
+
}
|
|
2293
|
+
} : null);
|
|
2294
|
+
if (!entry) {
|
|
2295
|
+
await transport.disconnect();
|
|
2296
|
+
throw new Error(`${name} did not say which printer language it speaks. Pick its type under Advanced and connect again.`);
|
|
2297
|
+
}
|
|
2298
|
+
this.inner = cableClient(entry, transport, USB_PACE, USB_ACK_TIMEOUT_MS);
|
|
2299
|
+
if (!this.inner) {
|
|
2300
|
+
await transport.disconnect();
|
|
2301
|
+
throw new Error(`A ${PRINTER_TYPE_NAMES[entry.family]} cannot print over USB`);
|
|
2302
|
+
}
|
|
2303
|
+
return this.inner.connect();
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2007
2306
|
// src/index.ts
|
|
2008
2307
|
/**
|
|
2009
2308
|
* libniimai: connect to NIIMBOT, TSPL, ESC/POS and cat printers from the
|
|
2010
|
-
* browser over Web Bluetooth
|
|
2309
|
+
* browser over Web Bluetooth, Web Serial or WebUSB, and print a canvas on them.
|
|
2011
2310
|
*/
|
|
2012
2311
|
var niimbot = () => import("@mmote/niimbluelib");
|
|
2013
2312
|
/**
|
|
2014
2313
|
* A client for one kind of connection. Bluetooth finds the printer family
|
|
2015
|
-
* from the device the user picks; serial is NIIMBOT only;
|
|
2016
|
-
*
|
|
2314
|
+
* from the device the user picks; serial is NIIMBOT only; usb is for label
|
|
2315
|
+
* and receipt printers that show up as standard USB printers; demo is a
|
|
2316
|
+
* printer in memory.
|
|
2017
2317
|
*/
|
|
2018
2318
|
async function createClient(kind) {
|
|
2019
2319
|
if (kind === "demo") return new MockClient();
|
|
2020
2320
|
if (kind === "bluetooth") return new BluetoothPrinter(niimbot);
|
|
2321
|
+
if (kind === "usb") return new UsbPrinter();
|
|
2021
2322
|
return new (await (niimbot())).NiimbotSerialClient();
|
|
2022
2323
|
}
|
|
2023
2324
|
/** A ready printer service: connect, watch its status, print canvases. */
|
|
2024
2325
|
function createPrinter(options = {}) {
|
|
2025
2326
|
return new PrinterService(options.createClient ?? createClient, async (canvas, direction) => (await niimbot()).ImageEncoder.encodeCanvas(canvas, 0, direction));
|
|
2026
2327
|
}
|
|
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 };
|
|
2328
|
+
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, USB_PRINTER_CLASS, UsbPrinter, byServices, catFeed, printJob as catJob, catModelFor, connectErrorTopic, correctTask, createClient, createPrinter, describeBleError, detectBrowser, detectPlatform, effectivePaper, escposFeed, escposJob, escposModelFor, familiesFromDeviceId, findManualProfile, findPrinterType, fitToPrinthead, friendlyError, identifyPrinter, identifyUsbPrinter, inkOutsideHead, labelTypeOf, modelFor, paperFromLabelType, paperName, parseDeviceId, printheadCrop, printheadMm, rememberedUsbPrinters, resolveProfile, rotateCanvas, shiftCanvas, supportIssue, supportsBluetooth, supportsSerial, supportsUsb, tsplJob, tsplModelFor, unsupportedReason, usbTransport, webTransport };
|
|
2028
2329
|
|
|
2029
2330
|
//# sourceMappingURL=libniimai.js.map
|