Web Serial API i odczyt temperatury
- Opublikowano • 3 min
Jesteś w serii Lab. Prezentuję w niej moje eksperymenty, niekoniecznie w skończonej albo uporządkowanej formie.
Zabawy z Web Serial API
Ostatnio bawię się z Web Serial API, sprzężając JavaScript z mikrokontrolerami. Na tapetę wziąłem odczyt temperatury przy pomocy ESP8266 i prostego czujnika LM35. Po podłączeniu urządzenia do portu USB i zezwoleniu na dostęp do portu szeregowego, następuje wymiana danych pomiędzy aplikacją webową a ESP8266.
Narzędziownik
- Vue.js
- Warstwa UI oraz komunikacja z Web Serial API poprzez composable
- JavaScript
- Natywny Web Serial API
- C++
- Odczyt temperatury oraz komunikacja po porcie szeregowym
- ESP8266
- Mikrokontroler
Poniższe demo jest interaktywne. Niżej zamieszczam nagranie.
Can’t connect to serial port :-(.
Web Serial API
Read temperature using native Web Serial API, ESP8266, LM35 thermometer and Vue3
Działa to tak, że firmware oczekuje polecenia READ\n a po odczytaniu danych z czujnika wysyła OK:28.00\n gdzie 28.00 to odczytana temperatura. Oczywiście nazwy poleceń/format jest dowolny; u mnie tak pasowało.
Aplikacja została zrobiona w Vue.js i o ile masz przy sobie jakieś urządzenie, które wyśle dane w formacie OK:XX.YY\n wirtualny termometr pokaże ci temperaturę. ;)
Tak wygląda strona firmware:
const int PIN_TEMP = A0;
void setup() {
Serial.begin(9600);
Serial.println("OK:CONNECTED");
}
void loop() {
if (Serial.available()) {
// Expects "READ\n"
String line = Serial.readStringUntil('\n');
line.trim();
if (line == "READ") {
float temp = read_temperature();
Serial.println("OK:" + String(temp));
} else {
Serial.println("Invalid input");
}
}
}
float read_temperature() {
float reading = analogRead(PIN_TEMP);
float voltage = (reading / 1024.0) * 3300;
float temp = voltage / 10;
return temp;
}
Po stronie frontend, najbardziej istotna część to połączenie po porcie szeregowym. Logikę umieściłem w composable.
import { computed, ref, watch } from "vue";
export type PortState = "closed" | "closing" | "open" | "opening";
function useSerialPort({ onData }: { onData: (line: string) => void }) {
const canUseSerial = ref<boolean>("serial" in navigator);
const portState = ref<PortState>("closed");
const portRef = ref<SerialPort | null>(null);
const readerRef = ref<ReadableStreamDefaultReader<Uint8Array> | null>(null);
const writerRef = ref<WritableStreamDefaultWriter<Uint8Array> | null>(null);
const isOpen = computed(() => portState.value === "open");
const isClosed = computed(() => portState.value === "closed");
const isOpening = computed(() => portState.value === "opening");
async function connect() {
try {
await requestPort();
} catch (e) {
console.log("Could not connect to port", e);
}
}
async function openPort(port: SerialPort) {
try {
await port.open({ baudRate: 9600 });
portRef.value = port;
writerRef.value = port.writable?.getWriter() ?? null;
readerRef.value = port.readable?.getReader() ?? null;
portState.value = "open";
console.info("Connected.");
} catch (e) {
portState.value = "closed";
console.log("Could not open port", e);
}
}
async function requestPort() {
try {
const port = await navigator.serial.requestPort();
portState.value = "opening";
await openPort(port);
} catch (e) {
throw new Error("Could not request port", { cause: e });
}
}
async function sendCommand(cmd: string) {
if (!writerRef.value) return;
await writerRef.value.write(new TextEncoder().encode(cmd + "\n"));
console.debug("→", cmd);
}
function parseStream(value: string): string[] {
let newlineIndex = -1;
let buffer = value;
const lines: string[] = [];
const commandSeparator = "\n";
while ((newlineIndex = buffer.indexOf(commandSeparator)) >= 0) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (line) lines.push(line);
}
return lines;
}
const stopReaderWatch = watch(readerRef, (reader, _old, onCleanup) => {
if (!isOpen.value || !reader) return;
let cancelled = false;
onCleanup(() => {
cancelled = true;
reader.cancel().catch(() => {});
portState.value = "closed";
readerRef.value = null;
});
(async () => {
const decoder = new TextDecoder();
try {
while (!cancelled) {
const { value, done } = await reader.read();
if (done) break;
// `buffer` is decoded value from device, e.g `OK:29.00\n`
// In this case device only sends one line (marked by '\n')
const buffer: string = decoder.decode(value, { stream: true });
const lines = parseStream(buffer);
const lastLine = lines.at(-1);
if (lastLine) onData(lastLine);
}
} catch (err) {
portState.value = "closed";
readerRef.value = null;
console.log("Read loop error:", err);
} finally {
reader.releaseLock();
}
})();
});
function cleanup() {
stopReaderWatch();
}
return {
canUseSerial,
portState,
isOpen,
isClosed,
isOpening,
portRef,
readerRef,
writerRef,
connect,
requestPort,
openPort,
sendCommand,
cleanup,
};
}
export default useSerialPort;
Nie wszystkie stany (szczególnie wyjątki) są obsłużone, ale nie ma większego znaczenia tutaj. Komponent następnie korzysta z composable:
const serialPort = useSerialPort({ onData: onData });
function onData(line: string) {
if (!line.startsWith("OK:")) return;
const value = parseFloat(line.slice(3)); // float after 'OK:' (3 characters)
if (!Number.isNaN(value)) {
rawTemp.value = value;
}
}
Jeżeli port jest otwarty, możemy odczytać temperaturę:
serialPort.sendCommand("READ");
Należy pamiętać, by najpiew dać uprawnienia:
<template>
<button @click="serialPort.connect">Connect to serial port</button>
</template>
Cześć! Jestem otwarty na nowe wyzwania zawodowe.
Brzmi interesująco? Skontaktuj się ze mną poprzez poprzez LinkedIn
Demo
Po pierwszym pomiarze przytykam palucha na termometrze i temperatura wzrasta. A ESP8266 jest podłączone tak:
Dane lecą po porcie szeregowym (tutaj USB micro).