webserial 1.2.0
webserial: ^1.2.0 copied to clipboard
WebSerial support for Dart built on the web package.
import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
import 'package:webserial/webserial.dart';
// Extension type to safely access the `port` property on connect/disconnect events.
extension type SerialPortEvent._(JSObject _) implements web.Event, JSObject {
// port is actually pretty useless as according to this its always undefined in "modern browers"
// https://github.com/WICG/serial/issues/156#issuecomment-1007087173
// external JSSerialPort? get port;
external JSSerialPort? get target;
}
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
JSSerialPort? _port;
web.ReadableStreamDefaultReader? _reader;
bool _isReading = false;
String _status = 'Click to connect to a serial device.';
void setStatus(String s) {
_status = s;
}
@override
void initState() {
super.initState();
// Global listener is essential for automatic reconnection.
_setupGlobalEventListeners();
}
/// Sets up a global listener to detect when any permitted device is reconnected.
void _setupGlobalEventListeners() {
serial.onconnect = ((web.Event event) {
final portEvent = event as SerialPortEvent;
debugPrint('Global event: A serial port was connected.');
final connectedPort = portEvent.target;
if (_port == null && mounted) {
setState(() {
setStatus('Known device reconnected. Attempting to auto-connect...');
});
_connectToPort(connectedPort);
}
} as void Function(web.Event))
.toJS;
}
/// This function is for the *initial* user-driven connection.
void chooseSerialDevice() async {
if (_port != null) {
debugPrint("A port is already open.");
return;
}
try {
final filters = [
JSFilterObject(usbVendorId: 0x1D50, usbProductId: 0x6192)
];
final selectedPort = await requestWebSerialPort(filters.toJS);
if (selectedPort != null) {
await _connectToPort(selectedPort);
} else {
debugPrint("No port selected.");
}
} catch (e) {
debugPrint("Error requesting serial port: $e");
setState(() {
_status = "Error: ${e.toString()}";
});
}
}
/// Common logic to connect to a given port.
Future<void> _connectToPort(JSSerialPort? port) async {
if (_port != null || port == null) return;
try {
_port = port;
await _port!
.open(
JSSerialOptions(
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
bufferSize: 64,
flowControl: "none",
),
)
.toDart;
debugPrint("Port opened successfully.");
final info = _port!.getInfo();
setState(() {
_status =
"Connected to VendorID: ${info.usbVendorId}, ProductID: ${info.usbProductId}";
});
// Attach an instance-specific listener for disconnection.
_port!.ondisconnect = ((web.Event event) {
debugPrint('Instance event: The active port disconnected.');
_closePort();
} as void Function(web.Event))
.toJS;
_isReading = true;
_readLoop();
_sendData();
} catch (e) {
debugPrint("Error during connection process: $e");
await _closePort();
}
}
void _sendData() {
if (_port?.writable == null) return;
final writer = _port!.writable!.getWriter();
final request = Uint8List.fromList([0x02]);
final JSUint8Array jsReq = request.toJS;
writer.write(jsReq);
writer.releaseLock();
debugPrint("Sent data: $request");
}
Future<void> _readLoop() async {
if (_port?.readable == null) return;
_reader = _port!.readable!.getReader() as web.ReadableStreamDefaultReader?;
debugPrint("Starting read loop...");
while (_isReading) {
try {
final result = await _reader!.read().toDart;
if (result.done) {
debugPrint("Reader reported stream is done.");
break;
}
debugPrint("DATA: ${result.value}");
} catch (e) {
debugPrint("Read loop failed, initiating cleanup. Error: $e");
break;
}
}
debugPrint("Exited read loop.");
await _closePort();
}
Future<void> _closePort() async {
if (_port == null) return;
_isReading = false;
final portToClose = _port;
final readerToCancel = _reader;
// Update state and UI immediately to prevent race conditions.
_port = null;
_reader = null;
if (mounted) {
setState(() {
_status = 'Port disconnected. Click to reconnect or plug in device.';
});
}
// Perform async cleanup in the background.
try {
await readerToCancel?.cancel().toDart;
} catch (e) {
debugPrint("Error cancelling reader (expected): $e");
}
try {
await portToClose?.close().toDart;
} catch (e) {
debugPrint("Error closing port (expected): $e");
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Web Serial Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_status),
const SizedBox(height: 20),
MaterialButton(
color: Theme.of(context).primaryColor,
textColor: Colors.white,
onPressed: _port == null ? chooseSerialDevice : null,
disabledColor: Colors.grey,
child: const Text('Connect Serial Device'),
),
],
),
),
),
);
}
}