Webserial Chrome

OP #7451156
Lesenswert?

Mittlerweile sollte es theoretisch einfach sein, über eine Webbrowser direkt auf die serielle Schnittstelle zuzugreifen und z.B. Daten zum Mikrocontroller zu übertragen:

https://tigoe.github.io/html-for-conndev/webSerial/

Nicht jeder Browser ist kompatibel:

https://entwickler.de/api/webserial-api-webapplikation-hardware-interaktion

Bei mir funktioniert das leider nicht. Webserial scheint von Chrome unterstützt, aber er zeigt keine Schnittstellen an (Bild). Woran könnte es liegen?

System: Ubuntu 20.04 Chromiung: Version 114.0.5735.198 (Offizieller Build) snap (64-Bit)

christoph@..:~$ groups
christoph .. dialout .. plugdev .. usbusers

Angehängte Dateien:
#7451190
Lesenswert?

Christoph M. schrieb:

Chromiung: Version 114.0.5735.198 (Offizieller Build) snap (64-Bit)

Würde vermuten dass "snap" daran schuld ist. Da wird der Chromium nochmal in sein eigenes "Gefängnis" gesperrt, und wenn die Snap-Macher nicht daran gedacht haben, auch serielle Ports dort rein zu lassen, wird das nix.

Screenshot: Version 114.0.5735.106 (Offizieller Build) built on Debian 12.0, running on Debian 12.0 (64-Bit)

-> Installier dir einen Chrome, der kommt ohne Snap. Oder such eine Anleitung, wie du die Debian-Chromium-Paketquellen in's Ubuntu gefrickelt kriegst.

Edit: Evtl reicht auch ein

sudo snap connect chromium:raw-usb

um das Chromium-Snap an USB-Devices ranzulassen.

Angehängte Dateien:
OP #7451208
Lesenswert?

Danke für die Hilfe :-)

Evtl reicht auch ein

sudo snap connect chromium:raw-usb

um das Chromium-Snap an USB-Devices ranzulassen.

Das habe ich jetzt probiert, aber es scheint nicht zu funktionieren.

Ich habe das Snap-Paket mal deinstalliert und dann mit

https://askubuntu.com/questions/510056/how-to-install-google-chrome

.. sudo apt-get update sudo apt-get install google-chrome-stable

Chrome direkt installiert. Zeigt aber weiterhin keine Schnittstellen.

Funktioniert Webserial bei Dir? Wenn ja, auf welchem System?

#7451226
Lesenswert?

Christoph M. schrieb:

Funktioniert Webserial bei Dir? Wenn ja, auf welchem System?

Ja. Debian 12, sowohl mit Chromium als auch mit Chrome.

Deine Installations-Anleitung ist von 2014, auch wenn die mal aktualisiert wurde.

Keine Ahnung warum das über ein PPA läuft. Du kannst Chrome auch direkt von hier herunterladen: https://www.google.com/chrome/

Und dann einfach das heruntergeladene .deb installieren. Das legt dann auch einen sources.list Eintrag für automatische Updates an.

Kann aber sein, dass das PPA exakt dasselbe tut.

Chromium kann problemlos parallel installiert bleiben.

Beitrag #7451446 wurde von einem Moderator gelöscht.
OP #7451463
Lesenswert?

Keine Ahnung warum das über ein PPA läuft. Du kannst Chrome auch direkt von hier herunterladen: https://www.google.com/chrome/

Ok, vielen Dank für die Hilfe. Jetzt läuft es.

Mich würde interessieren, wie es bei den Windows-Usern so ist: Läuft es dort ebenfalls? Ziel: eine platforunabhängige GUI für Mikrocontroller. Deshalb wäre es gut, wenn Webserial auf allen Platformen einigermaßen problemlos läuft.

Was mir auffällt: Das Beispiel vom Eingangspost läuft nicht so gut. Die empfangenen Zeichen sind nicht sonderlich gut synchronisiert.

https://tigoe.github.io/html-for-conndev/webSerial/

Das Terminal

https://googlechromelabs.github.io/serial-terminal/

läuft, aber wenn ich senden will scheint es Probleme mit CR/LF zu geben. Da wäre die Frage, ob das bei Windows per default anders ist.

#7451509
Lesenswert?

Christoph M. schrieb:

läuft, aber wenn ich senden will scheint es Probleme mit CR/LF zu geben.

Ändert der "Convert EOL"-Knopf das?

Aber eigentlich egal, wenn du deine eigene Oberfläche für deine eigenen µC-Projekte baust, kannst du das Zeilenende-Zeichen auf beiden Seiten passend festlegen. Auch das Buffering für empfangene Zeichen kannst du da so handhaben, wie es für deine Anwendung passt.

OP #7472959
Lesenswert?

Im Moment versuche ich, mit Webserial Daten zu senden und zu empfangen. Dazu ein kurzes Beispiel: Drückt man den "send" Knopf, wird das Wort "hello" gesendet. Alle Zeichen von der seriellen Schnittstelle sollen im Browserfenster angezeigt werden.

Aus mir unerfindlichen Gründen wird nur das "hello" gesendet. Man sieht es auf dem Oszilloskop. Das Empfangen will aber einfach nicht funktionieren.

Hat jemand eine Idee?

Webserial Code:

1
<!DOCTYPE html>
2
<html>
3
<head>
4
    <title>Web Serial Example</title>
5
</head>
6
<body>
7
    <button id="connectButton">Connect to Serial Device</button>
8
    <button id="sendButton" disabled>Send "Hello"</button>
9
    <div id="receivedData" style="border: 1px solid #ccc; padding: 10px; margin-top: 10px;"></div>
10

11
    <script>
12
        const connectButton = document.getElementById('connectButton');
13
        const sendButton = document.getElementById('sendButton');
14
        const receivedDataDiv = document.getElementById('receivedData');
15
        let port;
16

17
        connectButton.addEventListener('click', async () => {
18
            try {
19
                port = await navigator.serial.requestPort();
20
                await port.open({ baudRate: 115200 }); // Set baud rate to 115200
21
                sendButton.disabled = false;
22
                connectButton.disabled = true;
23

24
                port.addEventListener('readable', onSerialDataReceived);
25
            } catch (error) {
26
                console.error('Error opening serial port:', error);
27
            }
28
        });
29

30
        sendButton.addEventListener('click', async () => {
31
            if (!port) return;
32
            try {
33
                const writer = port.writable.getWriter();
34
                const message = 'Hello\n';
35
                writer.write(new TextEncoder().encode(message));
36
                writer.releaseLock();
37
            } catch (error) {
38
                console.error('Error sending data:', error);
39
            }
40
        });
41

42
        async function onSerialDataReceived() {
43
            const reader = port.readable.getReader();
44
            try {
45
                while (true) {
46
                    const { value, done } = await reader.read();
47
                    if (done) {
48
                        break;
49
                    }
50
                    const receivedText = new TextDecoder().decode(value);
51
                    receivedDataDiv.innerHTML += receivedText + '<br>';
52
                }
53
            } catch (error) {
54
                console.error('Error reading data:', error);
55
            } finally {
56
                reader.releaseLock();
57
            }
58
        }
59
    </script>
60
</body>
61
</html>

Das Echo-Programm auf dem Mikrocontroller sieht so aus:

1
int incomingByte = 0;    // for incoming serial data
2
void setup()
3
{
4
  Serial.begin(115200);    // opens serial port, sets data rate to 9600 bps
5
}
6

7
void loop()
8
{
9
  if (Serial.available() > 0)
10
  {
11
    incomingByte = Serial.read();
12
    Serial.println((char)incomingByte);
13
  }
14
}
Angehängte Dateien:
#7473297
Lesenswert?

Christoph M. schrieb:

port.addEventListener('readable', onSerialDataReceived);

gibt es nicht. SerialPort hat events für connect/disconnect, aber nicht für "readable".

Brauchst du auch nicht, du kannst einfach "async" vom reader lesen.

d.H.: Ändere Zeile

port.readable.addEventListener('readable', onSerialDataReceived); in onSerialDataReceived(); (ohne "await"!)

Und schon funktioniert dein Beispielcode. (Vorschlag auf kleinste Änderung hin ausgewählt, nicht auf schönsten Code)

Nachtrag: Falls du dich wunderst, warum ich "Hello" im receivedData stehen habe und kein

H e l l o

Ich hab keinen Arduino mit deinem Code angeklemmt, sondern einfach einen USB->Serial Stöpsel mit Jumper über Rx/Tx.

Angehängte Dateien:
OP #7473592
Lesenswert?

Vielen Dank, funktioniert super.

Ich hab keinen Arduino mit deinem Code angeklemmt, sondern einfach einen USB->Serial Stöpsel mit Jumper über Rx/Tx.

Gute Idee. Das Arduino-Script habe ich für's debugging gemacht, weil ich vermutete, dass Webserial vielleicht für den Empfang CR und NL braucht. War wohl nicht so ;-)

Hier mal eine Version, die einen Zähler sendet und die empfangenen Werte in einem Scrollfenster darstellt. Wichtig ist, dass erst beim Empfang des NL upgedatet wird, sonst hat man immer Unterbrechungen in der Ausgabe.

1
<!DOCTYPE html>
2
<html>
3
<head>
4
    <title>Web Serial Example</title>
5
</head>
6
<body>
7
    <button id="connectButton">Connect to Serial Device</button>
8
    <button id="sendButton" disabled>Send "Counter"</button>
9
    <div id="receivedData" style="border: 1px solid #ccc; padding: 10px; margin-top: 10px; height: 200px; overflow: auto;"></div>
10

11
    <script>
12
        const connectButton = document.getElementById('connectButton');
13
        const sendButton = document.getElementById('sendButton');
14
        const receivedDataDiv = document.getElementById('receivedData');
15
        let port;
16
        let reader;
17
        let receivedBuffer = '';
18
        let counter = 0;
19

20
        connectButton.addEventListener('click', async () => {
21
            try {
22
                port = await navigator.serial.requestPort();
23
                await port.open({ baudRate: 115200 }); // Set baud rate to 115200
24
                sendButton.disabled = false;
25
                connectButton.disabled = true;
26

27
                reader = port.readable.getReader();
28
                startReadingSerialData();
29
            } catch (error) {
30
                console.error('Error opening serial port:', error);
31
            }
32
        });
33

34
        sendButton.addEventListener('click', async () => {
35
            if (!port) return;
36
            try {
37
                const writer = port.writable.getWriter();
38
                const message = `Counter: ${counter}\n`; // Append '\n' for newline
39
                writer.write(new TextEncoder().encode(message));
40
                writer.releaseLock();
41
                counter++;
42
            } catch (error) {
43
                console.error('Error sending data:', error);
44
            }
45
        });
46

47
        async function readSerialData() {
48
            try {
49
                while (true) {
50
                    const { value, done } = await reader.read();
51
                    if (done) {
52
                        break;
53
                    }
54
                    const receivedText = new TextDecoder().decode(value);
55
                    receivedBuffer += receivedText;
56
                    while (receivedBuffer.includes('\n')) {
57
                        const newlineIndex = receivedBuffer.indexOf('\n');
58
                        const line = receivedBuffer.substring(0, newlineIndex + 1);
59
                        receivedBuffer = receivedBuffer.substring(newlineIndex + 1);
60
                        appendToReceivedData(line);
61
                    }
62
                }
63
            } catch (error) {
64
                console.error('Error reading data:', error);
65
            }
66
        }
67

68
        function appendToReceivedData(text) {
69
            receivedDataDiv.innerHTML += text + '<br>';
70
            receivedDataDiv.scrollTop = receivedDataDiv.scrollHeight;
71
        }
72

73
        async function startReadingSerialData() {
74
            readSerialData();
75
        }
76

77
        async function stopReadingSerialData() {
78
            if (reader) {
79
                await reader.cancel();
80
                reader = null;
81
            }
82
        }
83

84
        connectButton.addEventListener('click', startReadingSerialData);
85
        window.addEventListener('beforeunload', stopReadingSerialData);
86
    </script>
87
</body>
88
</html>

Was mir jetzt noch fehlt: Es sollen die empfangenen Werte in verschiedene Textausgaben umgeleitet werden.

Empfang Textausgabe
======== ============
zaehlerA -> textfeld1
zaehlerA -> textfeld2
zaehlerA -> textfeld3

#7473733
Lesenswert?

Christoph M. schrieb:

const receivedText = new TextDecoder().decode(value); receivedBuffer += receivedText;

Kleine Anmerkung, das kann später schwer zu findende "Heisenbugs" geben: der Standard-Textdecoder ist für UTF-8, und da kann ein Zeichen mehrere Bytes lang sein. Der gelesene Chunk von der Seriellen kann mitten in einem UTF-8-Zeichen enden, das Zeichen ginge dann verloren. Der TextDecoder kann das handhaben, aber natürlich nur, wenn du nicht ständig neue Decoder erstellst, sondern denselben TextDecoder für den gesamten Datenstrom beibehältst. (Dann ".decode(value, { stream: !done });" mit dem "done"-Boolean aus dem reader.read() )

Oder: TextDecoder mit Ascii initialisieren, da ist "ein Byte"=="ein Buchstabe".

Oder: Gleich einen "TextDecoderStream" nehmen, mit ".pipeThrough()" direkt an den Serial-reader-Stream koppeln.

Übrigens, wenn du sowieso schon am "Streamen" der Daten bist:

1
// untested
2
const lineTransformer = new TransformStream({
3
  start(controller) {
4
    this.partialLine = '';
5
  },
6
  transform(chunk, controller) {
7
    const str = this.partialLine + chunk.toString();
8
    const lines = str.split('\n');
9
    for (let i = 0; i < lines.length - 1; i++) {
10
      controller.enqueue(lines[i]);
11
    }
12
    this.partialLine = lines[lines.length - 1];
13
  },
14
  flush(controller) {
15
    if (this.partialLine !== '') {
16
      controller.enqueue(this.partialLine);
17
    }
18
  }
19
});
20

21
const readLineStream = port.readable.pipeThrough(new TextDecoderStream(new TextDecoder())).pipeThrough(lineTransformer);

Und schon kannst du aus dem readLineStream.getReader() immer schön ganze Zeilen lesen...

OP #7473970
Lesenswert?

Kleine Anmerkung, das kann später schwer zu findende "Heisenbugs" geben: der Standard-Textdecoder ist für UTF-8, und da kann ein Zeichen mehrere Bytes lang sein.

Danke für den Hinweis. Sollte der Mikrocontroller nicht immer ganze Bytes senden?

1
const readLineStream = port.readable.pipeThrough(new TextDecoderStream(new TextDecoder())).pipeThrough(lineTransformer)

Das verstehe ich nicht ganz. Wie kann ich den LineTransformer dann lesen?

P.s: kleiner Seitenbranch ..

Ich habe gerade mal die Pipico "Scopy" App auf dem Handy installiert

Beitrag "PiPico Oszilloskop"

den Pipico mit der Scopy Firmware geflasht. Es funktioniert und die App kann mit dem Pipico kommunizieren. Dann habe ich

Serial-Terminal über Chromium zu einem ESP8266-Patinchen.
https://googlechromelabs.github.io/serial-terminal/

ausprobiert, aber behauptet Chrome, das nicht zu unterstützen. Ich habe ein älteres S8, vielleicht liegt es daran, oder geht es grundsätzlich nicht?

#7473998
Lesenswert?

Christoph M. schrieb:

Danke für den Hinweis. Sollte der Mikrocontroller nicht immer ganze Bytes senden?

Ganze Bytes schon, aber keine ganzen Buchstaben (Du verwendest UTF-8)

ein 'Ä' ist da z.B. 0xC3 0x84

Wenn du dem TextDecoder (wegen blöd gelegenem Chunk) zuerst ein 0xC3 gibst, und danach einem neuem TextDecoder das 0x84, kann keiner von Beiden was damit anfangen.

Deshalb: TextDecoder nicht immer wieder frisch mit "new" anlegen, solange leben lassen wie der ReadStream lebt. Und Daten mit "stream:true" einfüllen, wenn die Daten gestreamt werden, wie das am Seriellen Port nunmal so ist. Wenn das zu kompliziert ist: der TextDecoderStream kapselt das für dich weg.

Oder zu verzichtest auf UTF-8, und nimmst z.B. Ascii oder ISO-8859-15, dann musst du das aber dem TextDecoder im Konstruktor mitteilen.

Christoph M. schrieb:

Das verstehe ich nicht ganz. Wie kann ich den LineTransformer dann lesen?

wie bisher auch, nur dass eben immer ein einzeiliger String rausfällt.

1
  const reader=readLineStream.getReader();
2
  while (true) {
3
     const { value, done } = await reader.read();
4
     // value ist jetzt ein string, und immer eine Zeile.
5
...

Oder du packst dir einen WritableStream hinter den lineTransformer, dann kannst du auch ohne "while(true)-Schleife immer dann einen Callback bekommen, wenn wieder eine Zeile komplett gelesen wurde.

...

Android: Nein. https://caniuse.com/web-serial bzw. Jain: Über das Polyfill, was per Web-USB direkt auf USB->Seriell Adapter zugreift, und deren "Treiber" in Javascript nachgebaut hat, geht es evtl. https://caniuse.com/webusb

#7474014
Lesenswert?

Ich hab das mal in dein Beispielprogramm zusammengepackt.

im Endeffekt:

1
                const lineReceiver = new WritableStream({
2
                  write: lineReceived
3
                });
4
                port.readable
5
                  .pipeThrough(new TextDecoderStream())
6
                  .pipeThrough(lineTransformer)
7
                  .pipeTo(lineReceiver);

Bewirkt dass "lineReceived" als Callback automatisch ausgeführt wird, sobald eine Zeile komplett ist.

Beim Senden (am Hallo-Knopf) habe ich eingebaut, dass er einen Teil schön langsam Byteweise schickt, und einen Block mit mehreren Zeilen in einem Rutsch, um zu Zeigen dass so der Text-Decoder trotz Sonderzeichen nicht aus dem Tritt kommt, und der lineTransformer auch bei mehreren Zeilen in einem Chunk alle Zeilen brav an den callback übergibt.

Zum Testen hatte ich wieder Rx/Tx gebrückt.

Angehängte Dateien:
#7474373
Lesenswert?

Christoph M. schrieb:

Zuerst war ich durch die Delays irritiert. Aber wenn man sie raus macht, funktioniert es gut.

Wie geschrieben, die waren zum Testen drinnen, um eine maximal ungünstige Unterteilung des Datenstroms in gelesene Chunks zu erreichen. Wenn der Test mit so einem Worst-Case funktioniert, wird's auch in der Realität bei wesentlich "angenehmeren" Datenblöcken funktionieren.

#7474504
Lesenswert?

Εrnst B. schrieb:

TextDecoder mit Ascii initialisieren

Statt ASCII sollte man ISO8859-1 bzw. CP1252 (auch "Windows ANSI" genannt) verwenden, dann lassen sich auch die üblichen Sonderzeichen, die im mitteleuropäischen Raum üblich sind, verwenden. Jedes darstellbare Zeichen ist dann genau ein Byte groß.

Allerdings sollte man auch den Quelltext für den µC in dieser Codierung erstellen.

So schick UTF-8 an mancher Stelle auch sein mag, so extrem ungeeignet ist es bei µC-Anwendungen.

Terminalprogramme wie z.B. Teraterm verwenden ebenfalls diese Zeichencodierung.

#7474525
Lesenswert?

Harald K. schrieb:

Statt ASCII sollte man ISO8859-1 bzw. CP1252 (auch "Windows ANSI" genannt) verwenden, dann lassen sich auch die üblichen Sonderzeichen, die im mitteleuropäischen Raum üblich sind, verwenden. Jedes darstellbare Zeichen ist dann genau ein Byte groß.

ISO8859-15 hatte ich vorgeschlagen, €. Aber es tut ja nicht weh den TextDecoder einfach richtig zu verwenden, dann funktioniert es mit jeder Kodierung, egal ob 7-Bit-ASCII oder UTF-32. Und man muss sich (außer dem Einstellen der verwendeten Kodierung im Konstruktor) nie wieder Gedanken darum machen.

#7475002
Lesenswert?

Harald K. schrieb:

Dann muss aber die Gegenstelle (oft ein µC) auch davon wissen.

Na und? Wenn der µC es nicht kann oder nicht können soll, nimmt man eben einen 8-Bit-Zeichensatz.

Ist trotzdem kein Grund, auf PC-Seite das Zeichensatz-Dekodieren absichtlich falsch zu implementieren, wenn der korrekte Code sogar noch kürzer, einfacher und resourcenschonender ist.

#7488073
Lesenswert?

Christian M. schrieb:

Mich würde brennend interessieren ob das jemand bestätigen kann! Oder halt auf den meisten Plattformen halt...

sh. oben.

Εrnst B. schrieb:

Android: Nein. https://caniuse.com/web-serial bzw. Jain: Über das Polyfill, was per Web-USB direkt auf USB->Seriell Adapter zugreift, und deren "Treiber" in Javascript nachgebaut hat, geht es evtl. https://caniuse.com/webusb

Also: Android bei manchen USB->Seriell Wandlern per Polyfill, iPhone schaut in die Röhre, Windows und Linux: Problemlos, zumindest solange man nicht auf Windows95 o.Ä. festhängt und deshalb keine Browser-Updates mehr kriegt. Desktop-Mac: Hab ich nicht getestet, vermutlich nur mit Chrome, Safari bleibt außen vor.

Mit den Daten von caniuse: 30% aller User deckst du damit ab, am Desktop ca. 75%, wenn die Leute bereit sind einen vmtl. schon installierten, aber selten verwendeten Browser zu Starten: Schätze 80-90%.

Inzwischen stammt eben ein Großteil des Web-Traffics von Telefonen/Tablets, deshalb der große Unterschied zwischen "alle User" und "Desktop only"

Angehängte Dateien:
#7488094
Lesenswert?

Εrnst B. schrieb:

wie bisher auch, nur dass eben immer ein einzeiliger String rausfällt. const reader=readLineStream.getReader(); while (true) { const { value, done } = await reader.read(); // value ist jetzt ein string, und immer eine Zeile.

WTF, die ReadableStream API ist praktisch ein AsyncIterator, aber mit minimalen inkompatiblen unterschieden!?!

Der reader von einem ReadableStream hat die Methoden read(), cancel(), releaseLock(). Ein AsyncGenerator hat next(), return(), throw(). read() und next() geben beide eine Promise zurück, beide resultieren in einem Objekt mit {value, done}.

Und sie implementieren noch nicht einmal Symbol.asyncIterator! Würden sie das machen, gienge ein simples "for await"

Naja, muss man halt manuell konvertieren:

1
async function* readerToAsyncIterator(r){
2
  r ??= this;
3
  while(true){
4
    const res = await r.read();
5
    if(res.done) return;
6
    try {
7
      yield res.value;
8
    } catch(e) {
9
      r.cancel(e);
10
      throw e;
11
    }
12
  }
13
}

Dann sollte das hier gehen (ungetestet):

1
const reader = readLineStream.getReader();
2
for await(const value of readerToAsyncIterator(reader)){
3
  
4
}

Mit etwas monkypatching geht es noch einfacher, ist aber ein wenig ein hack:

1
ReadableStreamDefaultReader.prototype[Symbol.asyncIterator] = readerToAsyncIterator;
2
const reader = readLineStream.getReader();
3
for await(const value of reader){
4
  
5
}
OP #7489824
Lesenswert?

Daniel A. (daniel-a)

WTF, die ReadableStream API ist praktisch ein AsyncIterator, aber mit minimalen inkompatiblen unterschieden!?!

Schön wäre es, wenn du aus deinem Beitrag ein ausführbares Beispiel machst, damit die Leute schnell ausprobieren können, ob es auch wirklich funktioniert und besser ist, als die von Ernst vorgeschlagene Methode.

Hier ein Beispiel, wie das aussehen kann:

Beitrag "Re: Webserial Chrome"

#7489952
Lesenswert?

Christoph M. schrieb:

ob es auch wirklich funktioniert und besser ist, als die von Ernst vorgeschlagene Methode.

Er hat ja nicht wirklich was daran geändert, nur den Stream-Reader von Chrome so umgepatcht (auf den Stand vom Firefox gebracht...), dass man statt dem umständlichen

1
  while (true) {
2
     const { value, done } = await reader.read();

das schönere

1
for await (const value of reader){

schreiben kann.

Wenn man die Variante mit dem "lineReceived"-Callback verwenden mag ändert sich nix, da entfällt die Schleife komplett.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren