Veröffentlicht am
Ich wollte das Gewicht direkt im Browser haben, ohne Zusatzprogramm und ohne lokalen Dienst dazwischen. Die Waage ist eine GRAM Xtrem F1-30. Sie hängt per USB am Rechner, und der Browser spricht über die Web Serial API mit ihr.
Bis das lief, bin ich an vier Stellen hängen geblieben, die ich in keiner Anleitung gefunden habe. Um die geht es unten.
Die Hardware
Das Kabel
Die Xtrem hat eine normale RS-232-Schnittstelle. Heutige PCs haben keinen COM-Port mehr, also brauchst du einen USB-Seriell-Adapter. Beim Kauf solltest du auf Folgendes achten:
| Eigenschaft | Warum |
|---|---|
| Echter RS-232-Pegel (±12 V), nicht TTL mit 3,3 V oder 5 V | Die Waage spricht klassisches RS-232. Die billigen „USB-TTL"-Platinen mit Stiftleiste passen nicht. |
| DB9-Stecker (male) am Adapter | Passt auf die DB9-Buchse am Waagenkabel. |
FTDI-Chip (Vendor-ID 0403) |
Läuft stabil, Treiber gibt es für alles. Prolific-Nachbauten machen gern Ärger. |
| USB-A oder USB-C | Egal, Hauptsache FTDI und RS-232. |
Im Browser meldet sich der Adapter mit der Vendor-ID 0403, ein FT232R zusätzlich mit der
Produkt-ID 6001. Daran erkennt die Seite ihn später wieder, ohne dass jemand etwas
auswählen muss.
Treiber
Unter Windows 11 reicht Einstecken, im Geräte-Manager steht dann gleich ein „USB Serial Port (COM3)". Unter Windows 10 hat mein Rechner den Adapter erst erkannt, nachdem ich den FTDI-VCP-Treiber (Virtual COM Port) installiert hatte.
Der Stromanschluss
An der Waage selbst gibt es keine Buchse für das Netzteil. Das war die erste Stelle, an der ich hängen geblieben bin. Die Xtrem hat ein Kombikabel, über das Daten und Strom laufen. Die Strombuchse sitzt am DB9-Stecker am anderen Ende, also da, wo auch der USB-Seriell-Adapter steckt. Das Netzteil (7,5 V, 1 A) gehört deshalb an den Kabelstecker und nicht an die Waage.
Wenn die Waage läuft, leuchtet die grüne LED. Sie blinkt, solange Daten über die Schnittstelle gehen. So siehst du am schnellsten, ob überhaupt etwas passiert.
Die Waage einrichten
Mit WLAN-Modul macht die Xtrem ab Werk einen eigenen Hotspot auf. Die Werkseinstellungen stehen im Kommunikationsprotokoll des Herstellers:
| Einstellung | Standardwert |
|---|---|
| SSID | GRAM_01 |
| Passwort | 12345678 |
| IP der Waage | 192.168.4.1 |
| TCP-Server-Port | 6666 |
| UDP senden / empfangen | 4444 / 5555 |
Eingestellt wird die Waage mit dem Windows-Programm GRAM-PC-XTREM. Du verbindest dich mit dem WLAN der Waage und startest das Programm.

Die Einstellungen sind mit einer PIN gesperrt. Ab Werk ist das 1009, genauso ein
Standardwert wie das WLAN-Passwort oben.
Wichtig: Das PC-Programm belegt den COM-Port. Ein COM-Port kann nur von einem Programm gleichzeitig benutzt werden. Solange GRAM-PC-XTREM über USB verbunden ist, kommt der Browser also nicht dran. Schließ es vorher.
Die serielle Schnittstelle steht ab Werk auf 9600 Baud, 8 Datenbits, keine Parität, ein Stoppbit und kein Handshake. Das habe ich so gelassen.
Geht es auch ohne Kabel? Die Xtrem gibt das Gewicht auch über TCP heraus. Der
„TCP server port (XTREM)" steht ab Werk auf 6666, und genau diesen Port sperren Chrome
und Edge immer (ERR_UNSAFE_PORT, er steht als alter IRC-Port auf ihrer Blockliste).
Stellst du ihn auf 80 um, erreichst du die Waage unter http://<IP-der-Waage>, ganz ohne
Web Serial. Für einen festen Packplatz finde ich das Kabel trotzdem stabiler. Deshalb geht
es ab hier um Web Serial.

Das Protokoll in Kurzform
Jede Nachricht ist ein Rahmen:
STX Absender(2) Ziel(2) Befehl(1) Adresse(4) Länge(2, hex) Daten LRC(2) ETX [CR LF]
0x02 0x03
- Absender und Ziel sind Geräteadressen,
FFheißt „an alle". Ich schicke alles von00anFF. - Befehl:
Rlesen,Wschreiben,Eausführen. Antworten kommen mit Kleinbuchstaben zurück (r,w,e). - Die Adresse ist das Register.
- LRC ist eine Prüfsumme. Das Beispiel des Herstellers schickt dort einfach
00, weil die Prüfung ab Werk ausgeschaltet ist (Register0011h). Bei mir hat das so funktioniert.
Diese vier Befehle brauchst du:
| Aktion | Register | Rahmen |
|---|---|---|
| Stream starten | 1011 |
\x02 00FFE10110000 \x03\r\n |
| Stream stoppen | 1010 |
\x02 00FFE10100000 \x03\r\n |
| Tara | 0102 |
\x02 00FFE01020000 \x03\r\n |
| Nullstellen | 0105 |
\x02 00FFE01050000 \x03\r\n |
Am Beispiel Tara: 00 Absender, FF Ziel, E ausführen, 0102 Register, 00
Datenlänge, 00 LRC.
Die zweite Stelle, die mich Zeit gekostet hat: Die Waage sendet nichts von selbst. Erst
nach dem Startbefehl 1011 schickt sie laufend die Gewichtsmeldung, ab Werk alle 50 ms.
Tara oder Null?
- Null setzt den Nullpunkt neu. Das brauchst du, wenn die leere Waage ein paar Gramm anzeigt.
- Tara übernimmt das Gewicht, das gerade draufliegt, zum Beispiel einen leeren Karton. Danach zeigt die Waage netto an. Das Brutto bleibt erhalten, und der Tara-Wert geht vom Wiegebereich ab.
Für „Tara löschen" gibt es noch 1103.
Die Gewichtsmeldung
Die Nutzdaten sind immer 26 Zeichen (1A hex) lang. Im Rahmen ohne STX und ETX sieht
das so aus:
Pos. 0-1 2-3 4 5-8 9-10 11 12-19 20-21 22 23-30 31-32 33 34-36 37-38
Abs. Ziel r 0107 1A W Brutto Einheit T Tara Einheit S Status LRC
- Brutto und Tara haben je acht Zeichen, rechtsbündig mit Leerzeichen aufgefüllt und mit
Dezimalpunkt, zum Beispiel
" 230.3". - Die Einheit ist
g,kg,ozoderlb. - Der Status besteht aus zwölf Bit, geschrieben als drei Hex-Zeichen.
| Bit | Wert | Bedeutung |
|---|---|---|
| 0 | 0x001 |
Null, Gewicht ungefähr 0 |
| 1 | 0x002 |
Tara aktiv |
| 2 | 0x004 |
stabil, das Gewicht steht still |
| 3 | 0x008 |
Netto-Anzeige |
| 5 | 0x020 |
High-Resolution-Modus |
| 7 | 0x080 |
Überlast |
| 8 | 0x100 |
negatives Gewicht |
Die dritte Stelle war das Vorzeichen. Es steht vor den acht Stellen des Werts, an
Position 11 bzw. 22, wo sonst W und T stehen. Wer nur die acht Stellen liest, wie das
Beispiel des Herstellers mit Substring(12, 8), verliert bei negativem Gewicht das Minus.
Läuft das in Firefox oder Safari?
Nein. Die Web Serial API gibt es nur in Chromium-Browsern wie Chrome, Edge, Opera und
Brave. Außerdem nur in einem sicheren Kontext. Über reines http:// ist
navigator.serial einfach undefined. Mozilla und Apple haben beide gesagt, dass sie die
API nicht einbauen wollen, weil ihnen der direkte Zugriff einer Webseite auf serielle
Geräte zu weit geht. Darauf zu warten, dass sich das ändert, lohnt sich also nicht.
Wer die Waage im Firefox braucht, kommt um ein lokales Programm nicht herum. Und genau das wollte ich hier vermeiden.
Der Code
Konstanten
const SERIAL_OPTIONS = {
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: 'none',
flowControl: 'none',
};
const XTREM_START = '\x0200FFE10110000\x03\r\n';
const XTREM_STOP = '\x0200FFE10100000\x03\r\n';
const XTREM_ZERO = '\x0200FFE01050000\x03\r\n';
const XTREM_TARE = '\x0200FFE01020000\x03\r\n';
Die Waage auswählen
Der Geräte-Dialog darf nur nach einem Klick aufgehen:
document.getElementById('chooseScale').addEventListener('click', async () => {
try {
const port = await navigator.serial.requestPort();
await connect(port);
} catch (err) {
// NotFoundError = Dialog ohne Auswahl geschlossen, das ist kein Fehler
if (err.name !== 'NotFoundError') console.error(err);
}
});
Einmal freigegeben, merkt der Browser sich den Port, und beim nächsten Laden geht es ohne Dialog:
const ports = await navigator.serial.getPorts();
const scalePort = ports.find((p) => p.getInfo().usbVendorId === 0x0403);
if (scalePort) await connect(scalePort);
Mehr als Vendor- und Produkt-ID gibt Web Serial nicht heraus. Zwei gleiche Adapter am selben Rechner kannst du damit nicht auseinanderhalten.
Verbinden und Stream starten
let port = null;
let writing = Promise.resolve();
async function connect(selectedPort) {
port = selectedPort;
await port.open(SERIAL_OPTIONS);
await send(XTREM_START);
readLoop();
}
Senden, immer schön nacheinander
function send(command) {
writing = writing.then(async () => {
if (!port || !port.writable) return;
const writer = port.writable.getWriter();
try {
await writer.write(new TextEncoder().encode(command));
} catch (err) {
// Gerät weg. Der Fehler darf die Kette nicht abreißen lassen
} finally {
writer.releaseLock();
}
});
return writing;
}
Wozu die Kette über writing? Ein Stream hat nur einen Writer. Wenn zwei Schreibvorgänge
gleichzeitig laufen, wirft getWriter() einen Fehler. Noch unangenehmer: Hängt ein
Schreibvorgang noch, während port.close() läuft, schlägt das Schließen fehl. Der Port
bleibt dann offen, und jeder weitere Verbindungsversuch scheitert, bis die Seite neu geladen
wird. Das war die vierte Stelle, und die hat mich einen halben Tag gekostet.
Das catch gehört dazu. Scheitert ein Schreibvorgang ohne es, bleibt writing abgelehnt,
und kein späterer Befehl wird mehr gesendet, auch der Stoppbefehl beim Trennen nicht.
Lesen: Rahmen aus dem Datenstrom schneiden
Ein read() liefert nicht zuverlässig genau einen Rahmen, mal ist es ein halber, mal sind
es drei. Deshalb wird gepuffert und an STX und ETX zerschnitten:
let reader = null;
async function readLoop() {
reader = port.readable.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
while (true) {
const start = buffer.indexOf('\x02');
if (start === -1) {
buffer = '';
break;
}
if (start > 0) buffer = buffer.slice(start);
const end = buffer.indexOf('\x03');
if (end === -1) break; // Rahmen noch unvollständig
const frame = buffer.slice(1, end);
buffer = buffer.slice(end + 1); // CR/LF fliegt beim nächsten STX-Suchen raus
const reading = parseFrame(frame);
if (reading) showWeight(reading);
}
// Kein ETX in Sicht, etwa bei falscher Baudrate: nicht endlos puffern
if (buffer.length > 1024) buffer = '';
}
} catch (err) {
// Kabel gezogen oder Lesefehler: Hier endet nur die Schleife
console.error(err);
} finally {
reader.releaseLock();
reader = null;
}
}
Die Meldung auswerten
function parseFrame(frame) {
const target = frame.slice(2, 4);
if (target !== '00' && target !== 'FF') return null;
if (frame.slice(5, 9) !== '0107') return null;
const length = parseInt(frame.slice(9, 11), 16);
// Kopf(11) + Daten + LRC(2). Unter 26 Zeichen läse man die Prüfsumme als Status.
if (
!Number.isInteger(length) ||
length < 26 ||
frame.length !== length + 13
)
return null;
const gross = parseValue(frame, 11);
const tare = parseValue(frame, 22);
const status = parseInt(frame.slice(34, 37), 16);
if (gross === null || tare === null || !Number.isInteger(status))
return null;
const isNet = (status & 0x008) !== 0;
return {
weight: isNet ? gross - tare : gross,
unit: frame.slice(20, 22).trim(),
stable: (status & 0x004) !== 0,
tared: (status & 0x002) !== 0,
};
}
// Vorzeichen steht VOR den acht Stellen, nicht darin
function parseValue(frame, at) {
const digits = frame.slice(at + 1, at + 9).trim();
if (!/^\d+(?:\.\d+)?$/.test(digits)) return null;
return frame[at] === '-' ? -Number(digits) : Number(digits);
}
Wenn du das Gewicht automatisch übernimmst, warte auf stable. Sonst erwischst du den
Wert, während der Karton noch wackelt.
Tara und Null
document
.getElementById('zero')
.addEventListener('click', () => send(XTREM_ZERO));
document
.getElementById('tare')
.addEventListener('click', () => send(XTREM_TARE));
Sauber trennen
Schalte den Stream vor dem Schließen ab. Sonst sendet die Waage weiter ins Leere, und der Port ist beim nächsten Öffnen womöglich noch belegt:
async function disconnect() {
if (reader) await reader.cancel().catch(() => {}); // beendet readLoop()
// Nicht ewig warten, falls das Gerät schon weg ist
await Promise.race([
send(XTREM_STOP),
new Promise((r) => setTimeout(r, 1000)),
]);
try {
await port.close();
} catch (err) {
/* Gerät schon abgezogen */
}
port = null;
}
window.addEventListener('pagehide', disconnect);
Was sonst noch schiefgeht
- Der Schalter „Sealed": Solange er aktiv ist, antwortet die Xtrem über RS-232 auf gar
nichts. Ich hatte erwartet, dass dann nur die internen Werte gesperrt sind. Bei meiner
(ungeeichten) Waage stand er auf
Sealed, und erst nach dem Ausschalten kamen Werte. Warum das so ist, weiß ich nicht. Ich vermute, dass die Firmware bei aktivem Siegel absichtlich nichts herausgibt, damit niemand ungeeichte Werte für geeichte hält. - „Failed to execute 'open' on 'SerialPort'": Der Port ist belegt. Entweder läuft noch GRAM-PC-XTREM, oder ein anderer Tab hat die Waage offen, oder die Seite wurde gerade neu geladen und das alte Dokument hält den Port noch ein paar Millisekunden. Im letzten Fall hilft ein zweiter Versuch nach 500 ms. Weitere Versuche dann mit größerem Abstand.
- Es kommen keine Werte: Die Waage war beim Start noch aus, oder das Kabel wurde neu
gesteckt. Dann hat sie den Startbefehl nie bekommen. Schick
XTREM_STARTjede Sekunde neu, solange nichts ankommt. Das Beispiel des Herstellers macht es genauso. - Kabel gezogen:
reader.read()wirft einen Fehler oder liefertdone. Mitnavigator.serial.addEventListener("connect", …)merkst du, wenn die Waage wieder da ist. - Tab im Hintergrund: Trenn bei
visibilitychangedie Verbindung, damit ein anderer Tab die Waage bekommen kann. - Nur Müll im Puffer: Meistens stimmt die Baudrate nicht. Ab Werk ist sie 9600.
- Nichts passiert, die grüne LED ist aus: Das Netzteil steckt nicht. Die Buchse sitzt am Stecker des seriellen Kabels, nicht an der Waage.
Fazit
Die Industriewaage läuft jetzt im Browser, ohne zusätzliche Software, mit ein paar hundert Zeilen JavaScript. Das Protokoll selbst war schnell verstanden. Aufgehalten haben mich vier Kleinigkeiten:
- Die Strombuchse sitzt am Kabelstecker.
- Die Waage sendet erst nach einem Startbefehl.
- Das Vorzeichen steht vor dem Messwert.
- Schreibvorgänge müssen nacheinander laufen, sonst bleibt der Port hängen.

