multi_serial_port 0.1.1
multi_serial_port: ^0.1.1 copied to clipboard
Android multi-port serial plugin based on Android-SerialPort-API. Bind devices by USB physical path; API aligned with serialport_plus.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:multi_serial_port/multi_serial_port.dart';
import 'connection_panel.dart';
import 'console_panel.dart';
import 'serial_log.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'multi_serial_port',
theme: ThemeData(
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const TerminalPage(),
);
}
}
class TerminalPage extends StatefulWidget {
const TerminalPage({super.key});
@override
State<TerminalPage> createState() => _TerminalPageState();
}
class _TerminalPageState extends State<TerminalPage> {
static const _maxEntries = 500;
final _port = FlutterSerialPort('example');
final _input = TextEditingController();
final _scroll = ScrollController();
final _entries = <LogEntry>[];
StreamSubscription<Uint8List>? _subscription;
List<PortOption> _options = const [];
String? _selectedPath;
int _baudRate = 115200;
int _dataBits = 8;
int _parity = 0;
int _stopBits = 1;
bool _showHex = false;
bool _sendHex = false;
String _lineEnding = '\n';
int _rxBytes = 0;
int _txBytes = 0;
@override
void initState() {
super.initState();
_refresh();
}
@override
void dispose() {
_subscription?.cancel();
_port.close();
_input.dispose();
_scroll.dispose();
super.dispose();
}
Future<void> _refresh() async {
final usbDevices = await _port.getAllPhysicalDevices();
final devicePaths = await _port.getAllDevicesPath();
if (!mounted) return;
final options = PortOption.from(usbDevices, devicePaths);
setState(() {
_options = options;
if (!options.any((o) => o.path == _selectedPath)) {
_selectedPath = options.isEmpty ? null : options.first.path;
}
});
_log(LogEntry.info('检测到 ${options.length} 个端口'));
}
Future<void> _toggle() async {
if (_port.isOpen) {
await _subscription?.cancel();
_subscription = null;
await _port.close();
_log(LogEntry.info('串口已关闭'));
setState(() {});
return;
}
final path = _selectedPath;
if (path == null) return;
// 先订阅再打开,避免刚打开就到来的数据被漏掉。
_subscription = _port.read().listen((data) => _log(LogEntry.rx(data)));
final ok = await _port.open(
path,
_baudRate,
_dataBits,
_parity,
_stopBits,
);
if (!ok) {
await _subscription?.cancel();
_subscription = null;
_log(LogEntry.info('打开失败:$path'));
} else {
_log(LogEntry.info('已打开 ${_port.devicePath} @$_baudRate'));
}
setState(() {});
}
Future<void> _send() async {
final text = _input.text;
if (text.isEmpty) return;
final Uint8List data;
if (_sendHex) {
final parsed = parseHex(text);
if (parsed == null) {
_log(LogEntry.info('十六进制格式有误,需成对的 0-9A-F'));
return;
}
data = parsed;
} else {
data = Uint8List.fromList(utf8.encode(text + _lineEnding));
}
if (await _port.write(data)) {
_log(LogEntry.tx(data));
} else {
_log(LogEntry.info('发送失败'));
}
}
void _log(LogEntry entry) {
setState(() {
final length = entry.bytes?.length ?? 0;
if (entry.kind == LogKind.rx) _rxBytes += length;
if (entry.kind == LogKind.tx) _txBytes += length;
_entries.add(entry);
if (_entries.length > _maxEntries) {
_entries.removeRange(0, _entries.length - _maxEntries);
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scroll.hasClients) {
_scroll.jumpTo(_scroll.position.maxScrollExtent);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('串口调试'),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Center(
child: Text(_port.isOpen ? '已连接' : '未连接'),
),
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
ConnectionPanel(
options: _options,
selectedPath: _selectedPath,
baudRate: _baudRate,
dataBits: _dataBits,
parity: _parity,
stopBits: _stopBits,
isOpen: _port.isOpen,
onPathChanged: (value) => setState(() => _selectedPath = value),
onBaudRateChanged: (value) => setState(() => _baudRate = value),
onDataBitsChanged: (value) => setState(() => _dataBits = value),
onParityChanged: (value) => setState(() => _parity = value),
onStopBitsChanged: (value) => setState(() => _stopBits = value),
onRefresh: _refresh,
onToggle: _toggle,
),
const SizedBox(height: 12),
Expanded(
child: ConsolePanel(
entries: _entries,
controller: _scroll,
showHex: _showHex,
rxBytes: _rxBytes,
txBytes: _txBytes,
onShowHexChanged: (value) => setState(() => _showHex = value),
onClear: () => setState(() {
_entries.clear();
_rxBytes = 0;
_txBytes = 0;
}),
),
),
const SizedBox(height: 12),
SendPanel(
controller: _input,
sendHex: _sendHex,
lineEnding: _lineEnding,
enabled: _port.isOpen,
onSendHexChanged: (value) => setState(() => _sendHex = value),
onLineEndingChanged: (value) =>
setState(() => _lineEnding = value),
onSend: _send,
),
],
),
),
),
);
}
}