loading
Warning:
Chromium only, over HTTPS Web Serial ships in Chromium-based browsers on desktop and on Android. Firefox and Safari do not implement it. RequestPort must run inside a user gesture.
Note:
Reading is a subscription, not a call Bytes arrive whenever the device sends them, in whatever chunks the driver hands over, so a message split across two chunks arrives as two callbacks. Framing a protocol on top - lines, length prefixes - is the caller's job.

Support check

IsSupported

True when the runtime exposes navigator.serial. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.

C#
@inject Bit.Butil.Serial serial

var supported = await serial.IsSupported();
Live sample
support output
Results will appear here when you interact with the samples.

Pick a port

RequestPort / GetPorts

RequestPort opens the browser's port chooser. GetPorts returns the ports this origin was already granted, without a prompt - which is how an app reconnects to the same board on a later visit.

C#
private SerialPort? _port;

_port = await serial.RequestPort();
// or narrowed to one adapter:
_port = await serial.RequestPort(new SerialPortFilter { UsbVendorId = 0x2341 });

var granted = await serial.GetPorts();
Live sample
port output
Results will appear here when you interact with the samples.

Open and close

Open / Close / IsOpen / GetInfo

The line settings have to match what the device expects - a mismatched baud rate still produces bytes, just not the right ones. Opening an already-open port with different settings cycles it, which ends any read subscription.

Razor
@code {
    // The port from RequestPort above - a grant is per port, and this is the way back to one the
    // user has already chosen.
    private SerialPort? _port;

    private async Task OpenAndClose()
    {
        await _port!.Open(new SerialOptions
        {
            BaudRate = 115200,
            DataBits = 8,
            StopBits = 1,
            Parity = SerialParity.None,
            FlowControl = SerialFlowControl.None
        });

        await _port.Close();
    }
}
Live sample
Baud rate
Data bits
Stop bits
Parity
Flow control
no port picked yet
open output
Results will appear here when you interact with the samples.

Read and write

SubscribeData / Write / WriteText

SubscribeData starts the read loop and hands each chunk to the callback; disposing the subscription stops it and releases the stream lock, which is what a later Close needs.

Razor
@implements IAsyncDisposable

@code {
    private SerialPort? _port;              // from RequestPort, and opened
    private ButilSubscription? _reading;

    private async Task Talk()
    {
        _reading = await _port!.SubscribeData(
            onData: bytes => Console.WriteLine(Encoding.UTF8.GetString(bytes)),
            onError: message => Console.WriteLine(message));

        await _port.WriteText("AT\r\n");
        await _port.Write([0x01, 0x02]);
    }

    // The subscription holds the port's readable stream locked; nothing else can read it until
    // this goes away.
    public async ValueTask DisposeAsync()
    {
        if (_reading is not null) await _reading.DisposeAsync();
    }
}
Live sample
no port picked yet
Text to send
Bytes to send (hex, space separated)
data output
Results will appear here when you interact with the samples.

Control lines

GetSignals / SetSignals

GetSignals reads the input lines the device drives; SetSignals drives the output lines. A null argument leaves that line as it is - which matters, because toggling DTR resets many microcontroller boards.

Razor
@code {
    private SerialPort? _port;   // from RequestPort, and opened

    private async Task Signals()
    {
        var signals = await _port!.GetSignals();
        // signals.ClearToSend, DataSetReady, DataCarrierDetect, RingIndicator

        await _port.SetSignals(dataTerminalReady: true);
    }
}
Live sample
signal output
Results will appear here when you interact with the samples.

Plug and unplug

SubscribeConnection

Watches ports appearing and disappearing - a USB serial adapter being plugged in or pulled out. Only ports this origin already has permission for raise these.

C#
await using var watch = await serial.SubscribeConnection(
    onConnected: port => Console.WriteLine($"+ {port.Id}"),
    onDisconnected: port => Console.WriteLine($"- {port.Id}"));
Live sample
connection output
Results will appear here when you interact with the samples.

Revoke the grant

Forget

Drops this origin's permission for the port, so it stops appearing in GetPorts until the user picks it again.

Razor
@code {
    private SerialPort? _port;   // from RequestPort

    // Drops the grant, so GetPorts stops returning it and the picker has to be shown again.
    private async Task Revoke()
    {
        var revoked = await _port!.Forget();
        _port = null;
    }
}
Live sample
forget output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes navigator.serial. Returns default (false) during prerender/SSR instead of throwing.
RequestPort
ValueTask<SerialPort?> RequestPort(params SerialPortFilter[] filters)
Opens the port chooser and returns the picked port, or null when dismissed. Needs a user gesture.
GetPorts
ValueTask<SerialPort[]> GetPorts()
The ports this origin has already been granted, without a prompt.
SubscribeConnection
ValueTask<ButilSubscription> SubscribeConnection(Action<SerialPort>? onConnected = null, Action<SerialPort>? onDisconnected = null)
Watches permitted ports appearing and disappearing.
DisposeAsync
ValueTask DisposeAsync()
Closes every port this service handed out, detaches listeners and releases the JS callback reference.
InvokeSerialData
void InvokeSerialData(Guid id, byte[] data)
JSInvokable interop plumbing for read chunks - not intended for app code.
InvokeSerialError
void InvokeSerialError(Guid id, string message)
JSInvokable interop plumbing for read-loop failures - not intended for app code.
InvokeSerialConnected
void InvokeSerialConnected(Guid id, SerialPortInfo info)
JSInvokable interop plumbing for the connect event - not intended for app code.
InvokeSerialDisconnected
void InvokeSerialDisconnected(Guid id, SerialPortInfo info)
JSInvokable interop plumbing for the disconnect event - not intended for app code.
SerialPort.Info
SerialPortInfo Info { get; }
The port as it was when the handle was created: Id, UsbVendorId, UsbProductId, Open.
SerialPort.Open / Close / IsOpen
ValueTask<bool> Open(SerialOptions? options = null) / ValueTask Close() / ValueTask<bool> IsOpen()
Opens with the given line settings, closes, and reports the open state.
SerialPort.GetInfo
ValueTask<SerialPortInfo?> GetInfo()
Re-reads the port's state; Info is only the snapshot from when the handle was created.
SerialPort.Write / WriteText
ValueTask<bool> Write(byte[] data) / ValueTask<bool> WriteText(string text)
Writes raw bytes, or UTF-8 text.
SerialPort.SubscribeData
ValueTask<ButilSubscription> SubscribeData(Action<byte[]> onData, Action<string>? onError = null)
Starts the read loop. Dispose the subscription to stop reading and release the stream lock.
SerialPort.GetSignals / SetSignals
ValueTask<SerialSignals?> GetSignals() / ValueTask<bool> SetSignals(bool? dataTerminalReady = null, bool? requestToSend = null, bool? signalBreak = null)
Reads the input control lines and drives the output ones. Nulls leave a line unchanged.
SerialPort.Forget
ValueTask<bool> Forget()
Revokes this origin's permission for the port.
SerialPort.DisposeAsync
ValueTask DisposeAsync()
Stops reading, closes the port and releases the browser-side reference.
SerialOptions
class SerialOptions { int BaudRate; byte DataBits; byte StopBits; SerialParity Parity; int BufferSize; SerialFlowControl FlowControl; }
The line settings a port is opened with.
SerialPortFilter
class SerialPortFilter { ushort? UsbVendorId; ushort? UsbProductId; }
One chooser filter. Only USB-attached adapters can be filtered.
SerialPortInfo
class SerialPortInfo { string Id; ushort? UsbVendorId; ushort? UsbProductId; bool Open; }
A granted port.
SerialSignals
class SerialSignals { bool ClearToSend; bool DataCarrierDetect; bool DataSetReady; bool RingIndicator; }
The input control lines the device is driving.
An unhandled error has occurred. Reload 🗙