Veröffentlicht am
Ich wollte Etiketten mit QR-Code direkt aus der Weboberfläche drucken, ohne Druckdialog,
ohne CUPS und ohne Druckserver. Der Browser schickt die Druckbefehle dazu über WebUSB
roh an den Drucker. Vor ein paar Jahren hätte das noch einen Umweg über lpr gebraucht
und einen Dienst, den jemand am Laufen hält.
Der Drucker
Es ist ein TOSHIBA B-FV4T-TS14-QM-R, ein Desktop-Etikettendrucker aus der B-FV4-Serie.
| Merkmal | Was dahintersteckt |
|---|---|
| Druckverfahren | Thermotransfer (mit Farbband) und Thermodirekt (ohne). Das T in B-FV4T steht für Thermotransfer, die B-FV4D kann nur Thermodirekt. |
| Auflösung | TS14 = 300 dpi, also rund 12 Punkte je Millimeter. Die GS14-Variante hat 203 dpi. |
| Druckbreite | rund 105,7 mm. „4 Zoll" ist nur der Name der Baugröße, 4 × 25,4 wären 101,6 mm. |
| Schnittstellen | USB 2.0, Ethernet, seriell |
| Befehlssprache | TPCL, dazu Emulationen für ZPL2, EPL, DPL und IPL |
Bevor überhaupt etwas ankommt, müssen im B-FV Setting Tool zwei Einstellungen stimmen:
Befehlssprache TPCL und Auflösung 300 DPI. Steht das Gerät auf einer Emulation, nimmt
es die Befehle ohne Meldung an und druckt nichts Brauchbares.

Im selben Tool steht auch der USB-Gerätepfad mit vid_08a6&pid_b000. Das sind Vendor- und
Produkt-ID, nach denen du später filtern kannst.
Die Druckdaten
Der Drucker bekommt weder Bilder noch PDF. Er bekommt Textbefehle in
TPCL, eingepackt in STX (0x02) und ETX
(0x03). Diese Funktion baut ein Etikett mit QR-Code:
function qrLabel(url, anzahl = 1) {
const STX = '\x02';
const ETX = '\x03';
return (
STX +
'{D0295,0295,0295|}\n' +
'{C|}\n' +
`{XB01;0051,0051,T,M,06,A,0,M2=${url}|}\n` +
`{XS;I,${String(anzahl).padStart(4, '0')},0002D4000|}\n` +
ETX +
'\n'
);
}

Das Ergebnis ist eine gewöhnliche Zeichenkette. Die Seite baut sie selbst zusammen, ein Server ist am Druck nicht beteiligt.
Der Knopf über dem Block lädt das, was qrLabel('https://www.prefabcortex.com')
zurückgibt, also 111 Byte. STX und ETX sind darin echt enthalten. Aus einem Codeblock mit
fertigen Befehlen kopierst du die beiden unsichtbaren Zeichen nämlich nicht mit, und dann
druckt der Drucker einfach nichts. Es ist dieselbe Datei wie bei Beispiel 2 im
TPCL-Artikel.
Braucht der Drucker einen Treiber?
Nein. WebUSB spricht das Gerät direkt an, ohne Druckertreiber, Druckdialog, CUPS oder Druckserver. Die Seite erzeugt den Befehlstext, der Browser schiebt ihn über USB hinaus, der Drucker führt ihn aus. Unter Windows muss der Druckertreiber sogar weg, dazu unten mehr.
Läuft das in Firefox oder Safari?
Nein. WebUSB gibt es nur in Chromium-Browsern (Chrome, Edge, Opera, Brave) und nur in einem
sicheren Kontext, also über https:// oder localhost. Mozilla und Apple haben beide
öffentlich erklärt, dass sie WebUSB nicht umsetzen wollen. Ihnen ist der direkte Zugriff
einer Webseite auf USB-Geräte zu weitgehend. Darauf zu warten, dass sich das ändert, lohnt
also nicht.
Auch Chrome für Android kann WebUSB.
Der Code
Den Drucker einmal auswählen
Der Geräte-Dialog geht nur nach einem Klick auf:
async function selectPrinter() {
try {
// filters: [] zeigt alle USB-Geräte, filters: [{ vendorId: 0x08a6 }] nur den Toshiba
const device = await navigator.usb.requestDevice({ filters: [] });
localStorage.setItem('usbVendorId', device.vendorId);
localStorage.setItem('usbProductId', device.productId);
} catch (err) {
// NotFoundError = Dialog ohne Auswahl geschlossen
if (err.name !== 'NotFoundError') {
alert('Fehler beim Wählen des Druckers: ' + err.message);
}
}
}
Anders als Web Serial gibt WebUSB auch den Produktnamen heraus (device.productName). Das
Gerät lässt sich damit auch ohne gespeicherte IDs wiederfinden.
async function printViaUsb(content) {
const devices = await navigator.usb.getDevices(); // nur freigegebene Geräte
const device = devices.find(
(d) =>
d.vendorId === parseInt(localStorage.getItem('usbVendorId')) &&
d.productId === parseInt(localStorage.getItem('usbProductId')),
);
if (!device) {
throw new Error('Kein Drucker eingerichtet');
}
await device.open();
try {
await device.selectConfiguration(1);
await device.claimInterface(0);
// TextEncoder macht aus dem Text mit STX und ETX die Bytes für den Drucker
await device.transferOut(1, new TextEncoder().encode(content));
} finally {
await device.close(); // sonst kommt der nächste Druck nicht heran
}
}
Endpoint 1 und Interface 0 stehen hier fest drin. Bei einem anderen Drucker kann das
anders sein. Sauberer ist es, den OUT-Endpoint nachzuschlagen:
const alt = device.configuration.interfaces[0].alternate;
const out = alt.endpoints.find(
(e) => e.direction === 'out' && e.type === 'bulk',
);
await device.transferOut(out.endpointNumber, data);
Beides zusammen
Der Knopf in der Oberfläche baut das Etikett und schickt es an den Drucker:
await printViaUsb(qrLabel('https://www.prefabcortex.com'));
„Access denied" bei claimInterface
Das ist der häufigste Abbruch, und am Code liegt er nicht. Unter Windows hält der
installierte Druckertreiber das USB-Interface exklusiv fest. device.claimInterface(0)
scheitert dann mit NetworkError: Access denied oder The device is protected. Unter Linux
passiert dasselbe, wenn der Kernel schon ein Treibermodul an das Gerät gehängt hat.
Helfen tut nur, das Gerät an einen generischen USB-Treiber zu binden statt an den Druckertreiber. Unter Windows ist das WinUSB, meistens mit Zadig installiert. Danach ist der Drucker für Windows kein Drucker mehr. Über den Druckdialog geht dann nichts mehr, über WebUSB alles. Das solltest du also bewusst für dieses Gerät entscheiden.
Wer den Drucker weiter aus Word heraus benutzen will, kann WebUSB deshalb nicht nehmen. Dann bleibt nur der Weg über den Treiber und einen Druckserver.
Stolperfallen
- Falsches Gerät ausgewählt: Mit
filters: []stehen auch Maus, Tastatur und Scanner in der Liste. - Drucker aus oder abgezogen:
open()schlägt fehl, die Freigabe bleibt aber bestehen. Nach dem Wiedereinstecken geht es ohne neuen Dialog weiter. - Der Aufkleber kommt nicht heraus: Der Auftrag ist angekommen, das Gerät blinkt nur, und
erst die Feed-Taste schiebt das Etikett heraus.
Forward Feed WaitaufOFFhat bei mir nicht gereicht. Als Nächstes würde ich den Spender-Modus probieren, bei dem das Etikett gleich vom Trägerpapier abgezogen wird. - Anzahl begrenzen: Ein Tippfehler im Mengenfeld soll keine ganze Rolle leer drucken.
