Serial
Pick a serial port, open it with the line settings the device expects, then read and write bytes - the browser's answer to a terminal program.
@inject Bit.Butil.Serial serialMDN reference
RequestPort must run inside a user gesture.
True when the runtime exposes navigator.serial. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Serial serial
var supported = await serial.IsSupported();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.
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();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.
{
// 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();
}
}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.
IAsyncDisposable
{
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();
}
}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.
{
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);
}
}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.
await using var watch = await serial.SubscribeConnection(
onConnected: port => Console.WriteLine($"+ {port.Id}"),
onDisconnected: port => Console.WriteLine($"- {port.Id}"));Drops this origin's permission for the port, so it stops appearing in GetPorts until the user picks it again.
{
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;
}
}API reference
ValueTask<bool> IsSupported()ValueTask<SerialPort?> RequestPort(params SerialPortFilter[] filters)ValueTask<SerialPort[]> GetPorts()ValueTask<ButilSubscription> SubscribeConnection(Action<SerialPort>? onConnected = null, Action<SerialPort>? onDisconnected = null)ValueTask DisposeAsync()void InvokeSerialData(Guid id, byte[] data)void InvokeSerialError(Guid id, string message)void InvokeSerialConnected(Guid id, SerialPortInfo info)void InvokeSerialDisconnected(Guid id, SerialPortInfo info)SerialPortInfo Info { get; }ValueTask<bool> Open(SerialOptions? options = null) / ValueTask Close() / ValueTask<bool> IsOpen()ValueTask<SerialPortInfo?> GetInfo()ValueTask<bool> Write(byte[] data) / ValueTask<bool> WriteText(string text)ValueTask<ButilSubscription> SubscribeData(Action<byte[]> onData, Action<string>? onError = null)ValueTask<SerialSignals?> GetSignals() / ValueTask<bool> SetSignals(bool? dataTerminalReady = null, bool? requestToSend = null, bool? signalBreak = null)ValueTask<bool> Forget()ValueTask DisposeAsync()class SerialOptions { int BaudRate; byte DataBits; byte StopBits; SerialParity Parity; int BufferSize; SerialFlowControl FlowControl; }class SerialPortFilter { ushort? UsbVendorId; ushort? UsbProductId; }class SerialPortInfo { string Id; ushort? UsbVendorId; ushort? UsbProductId; bool Open; }class SerialSignals { bool ClearToSend; bool DataCarrierDetect; bool DataSetReady; bool RingIndicator; }