@ -1,16 +1,15 @@
import ' dart:async ' ;
import ' dart:async ' ;
import ' dart:math ' as math ;
import ' package:flutter/material.dart ' ;
import ' package:flutter/material.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:provider/provider.dart ' ;
import ' package:provider/provider.dart ' ;
import ' ../connector/meshcore_connector.dart ' ;
import ' ../connector/meshcore_connector.dart ' ;
import ' ../connector/meshcore_connector_usb.dart ' ;
import ' ../l10n/l10n.dart ' ;
import ' ../l10n/l10n.dart ' ;
import ' ../utils/app_logger.dart ' ;
import ' ../utils/app_logger.dart ' ;
import ' ../utils/platform_info.dart ' ;
import ' ../utils/platform_info.dart ' ;
import ' ../utils/usb_port_labels.dart ' ;
import ' ../utils/usb_port_labels.dart ' ;
import ' ../widgets/adaptive_app_bar_title.dart ' ;
import ' contacts_screen.dart ' ;
import ' contacts_screen.dart ' ;
import ' scanner_screen.dart ' ;
import ' scanner_screen.dart ' ;
@ -24,20 +23,12 @@ class UsbScreen extends StatefulWidget {
class _UsbScreenState extends State < UsbScreen > {
class _UsbScreenState extends State < UsbScreen > {
final List < String > _ports = < String > [ ] ;
final List < String > _ports = < String > [ ] ;
bool _isLoadingPorts = true ;
bool _isLoadingPorts = true ;
bool _isConnecting = false ;
bool _navigatedToContacts = false ;
bool _navigatedToContacts = false ;
bool _didScheduleInitialLoad = false ;
bool _didScheduleInitialLoad = false ;
String ? _selectedPort ;
String ? _connectedPortDisplayLabel ;
String ? _errorText ;
Timer ? _hotPlugTimer ;
Timer ? _hotPlugTimer ;
late final MeshCoreConnector _connector ;
late final MeshCoreConnector _connector ;
late final MeshCoreConnectorUsb _usbConnector ;
late final VoidCallback _connectionListener ;
late final VoidCallback _connectionListener ;
/ / / Whether the current platform supports dynamic hot - plug polling .
/ / / On desktop ( macOS , Windows , Linux ) we poll continuously so the user
/ / / never needs to hit Refresh manually .
bool get _supportsHotPlug = >
bool get _supportsHotPlug = >
PlatformInfo . isWindows | | PlatformInfo . isLinux | | PlatformInfo . isMacOS ;
PlatformInfo . isWindows | | PlatformInfo . isLinux | | PlatformInfo . isMacOS ;
@ -45,25 +36,13 @@ class _UsbScreenState extends State<UsbScreen> {
void initState ( ) {
void initState ( ) {
super . initState ( ) ;
super . initState ( ) ;
_connector = context . read < MeshCoreConnector > ( ) ;
_connector = context . read < MeshCoreConnector > ( ) ;
_usbConnector = MeshCoreConnectorUsb ( _connector ) ;
_connectionListener = ( ) {
_connectionListener = ( ) {
if ( ! mounted ) return ;
if ( ! mounted ) return ;
final activeUsbPortDisplayLabel = _usbConnector . activeUsbPortDisplayLabel ;
if ( _connector . state = = MeshCoreConnectionState . disconnected ) {
final shouldUpdateDisplayLabel =
activeUsbPortDisplayLabel ! = _connectedPortDisplayLabel ;
if ( _usbConnector . state = = MeshCoreConnectionState . disconnected ) {
_navigatedToContacts = false ;
_navigatedToContacts = false ;
setState ( ( ) {
_isConnecting = false ;
_connectedPortDisplayLabel = activeUsbPortDisplayLabel ;
} ) ;
} else if ( shouldUpdateDisplayLabel ) {
setState ( ( ) {
_connectedPortDisplayLabel = activeUsbPortDisplayLabel ;
} ) ;
}
}
if ( _ usbC onnector. state = = MeshCoreConnectionState . connected & &
if ( _connector . state = = MeshCoreConnectionState . connected & &
_ usbC onnector. isUsbTransportConnected & &
_connector . isUsbTransportConnected & &
! _navigatedToContacts ) {
! _navigatedToContacts ) {
_navigatedToContacts = true ;
_navigatedToContacts = true ;
Navigator . of ( context ) . pushReplacement (
Navigator . of ( context ) . pushReplacement (
@ -71,14 +50,15 @@ class _UsbScreenState extends State<UsbScreen> {
) ;
) ;
}
}
} ;
} ;
_ usbC onnector. addListener ( _connectionListener ) ;
_ c onnector. addListener ( _connectionListener ) ;
_startHotPlugTimer ( ) ;
_startHotPlugTimer ( ) ;
}
}
@ override
@ override
void didChangeDependencies ( ) {
void didChangeDependencies ( ) {
super . didChangeDependencies ( ) ;
super . didChangeDependencies ( ) ;
_usbConnector . setRequestPortLabel ( context . l10n . usbScreenStatus ) ;
_connector . setUsbRequestPortLabel ( context . l10n . usbScreenStatus ) ;
_connector . setUsbFallbackDeviceName ( context . l10n . usbFallbackDeviceName ) ;
if ( ! _didScheduleInitialLoad ) {
if ( ! _didScheduleInitialLoad ) {
_didScheduleInitialLoad = true ;
_didScheduleInitialLoad = true ;
unawaited ( _loadPorts ( ) ) ;
unawaited ( _loadPorts ( ) ) ;
@ -89,12 +69,12 @@ class _UsbScreenState extends State<UsbScreen> {
void dispose ( ) {
void dispose ( ) {
_hotPlugTimer ? . cancel ( ) ;
_hotPlugTimer ? . cancel ( ) ;
_hotPlugTimer = null ;
_hotPlugTimer = null ;
_ usbC onnector. removeListener ( _connectionListener ) ;
_ c onnector. removeListener ( _connectionListener ) ;
if ( ! _navigatedToContacts & &
if ( ! _navigatedToContacts & &
_ usbC onnector. activeTransport = = MeshCoreTransportType . usb & &
_ c onnector. activeTransport = = MeshCoreTransportType . usb & &
_ usbC onnector. state ! = MeshCoreConnectionState . disconnected ) {
_ c onnector. state ! = MeshCoreConnectionState . disconnected ) {
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
unawaited ( _ usbC onnector. disconnect ( manual: true ) ) ;
unawaited ( _ c onnector. disconnect ( manual: true ) ) ;
} ) ;
} ) ;
}
}
super . dispose ( ) ;
super . dispose ( ) ;
@ -102,284 +82,138 @@ class _UsbScreenState extends State<UsbScreen> {
@ override
@ override
Widget build ( BuildContext context ) {
Widget build ( BuildContext context ) {
final theme = Theme . of ( context ) ;
final l10n = context . l10n ;
return Scaffold (
return Scaffold (
appBar: AppBar (
appBar: AppBar (
leading: IconButton (
leading: IconButton (
icon: const Icon ( Icons . arrow_back ) ,
icon: const Icon ( Icons . arrow_back ) ,
onPressed: ( ) {
onPressed: ( ) = > Navigator . of ( context ) . maybePop ( ) ,
appLogger . info ( ' Back button pressed ' , tag: ' UsbScreen ' ) ;
Navigator . of ( context ) . maybePop ( ) ;
} ,
) ,
title: Text (
l10n . connectionChoiceUsbLabel ,
style: theme . textTheme . titleLarge ,
) ,
) ,
title: AdaptiveAppBarTitle ( context . l10n . usbScreenTitle ) ,
centerTitle: true ,
centerTitle: true ,
actions: [
if ( PlatformInfo . isWeb | |
PlatformInfo . isAndroid | |
PlatformInfo . isIOS )
TextButton . icon (
onPressed: ( ) {
appLogger . info (
' Bluetooth selected, opening ScannerScreen ' ,
tag: ' UsbScreen ' ,
) ;
Navigator . of ( context ) . pushReplacement (
MaterialPageRoute ( builder: ( _ ) = > const ScannerScreen ( ) ) ,
) ;
} ,
icon: const Icon ( Icons . bluetooth ) ,
label: Text ( l10n . connectionChoiceBluetoothLabel ) ,
) ,
] ,
) ,
) ,
body: SafeArea (
body: SafeArea (
child: LayoutBuilder (
top: false ,
builder: ( context , constraints ) {
child: Consumer < MeshCoreConnector > (
final availableHeight = constraints . maxHeight . isFinite
builder: ( context , connector , child ) {
? constraints . maxHeight
return Column (
: 600.0 ;
children: [
final availableWidth = constraints . maxWidth . isFinite
_buildStatusBar ( context , connector ) ,
? constraints . maxWidth
Expanded ( child: _buildPortList ( context , connector ) ) ,
: 800.0 ;
] ,
final gap = math . max ( 8.0 , math . min ( 16.0 , availableHeight * 0.025 ) ) ;
final iconSize = math . max (
28.0 ,
math . min ( 72.0 , availableHeight * 0.12 ) ,
) ;
) ;
final isNarrow = availableWidth < 460.0 ;
} ,
) ,
return Padding (
) ,
padding: const EdgeInsets . symmetric ( horizontal: 24 , vertical: 20 ) ,
bottomNavigationBar: Consumer < MeshCoreConnector > (
child: Column (
builder: ( context , connector , child ) {
crossAxisAlignment: CrossAxisAlignment . stretch ,
final isLoading = _isLoadingPorts ;
children: [
final showBle = PlatformInfo . isWeb | |
/ / ─ ─ Compact header ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
PlatformInfo . isAndroid | |
Row (
PlatformInfo . isIOS ;
children: [
Icon (
return SafeArea (
Icons . usb ,
top: false ,
size: iconSize . clamp ( 24.0 , 40.0 ) ,
minimum: const EdgeInsets . fromLTRB ( 16 , 8 , 16 , 16 ) ,
color: theme . colorScheme . primary ,
child: Row (
) ,
mainAxisAlignment: MainAxisAlignment . end ,
SizedBox ( width: gap ) ,
children: [
Expanded (
if ( showBle )
child: Column (
FloatingActionButton . extended (
crossAxisAlignment: CrossAxisAlignment . start ,
onPressed: ( ) {
mainAxisSize: MainAxisSize . min ,
Navigator . of ( context ) . pushReplacement (
children: [
MaterialPageRoute (
Text (
builder: ( _ ) = > const ScannerScreen ( ) ,
l10n . usbScreenTitle ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . titleLarge ? . copyWith (
fontWeight: FontWeight . w600 ,
) ,
) ,
Text (
l10n . usbScreenSubtitle ,
maxLines: 2 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodySmall ? . copyWith (
color: theme . colorScheme . onSurfaceVariant ,
) ,
) ,
] ,
) ,
) ,
) ,
) ;
] ,
} ,
heroTag: ' usb_ble_action ' ,
icon: const Icon ( Icons . bluetooth ) ,
label: Text ( context . l10n . connectionChoiceBluetoothLabel ) ,
) ,
) ,
SizedBox ( height: gap ) ,
if ( showBle ) const SizedBox ( width: 12 ) ,
/ / ─ ─ Port list takes all remaining space ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
if ( ! _supportsHotPlug )
Expanded ( child: _buildPortList ( context ) ) ,
FloatingActionButton . extended (
if ( _errorText ! = null ) . . . [
onPressed: isLoading ? null : _loadPorts ,
SizedBox ( height: gap * 0.5 ) ,
heroTag: ' usb_refresh_action ' ,
Text (
icon: isLoading
_errorText ! ,
? const SizedBox (
textAlign: TextAlign . center ,
width: 20 ,
maxLines: 3 ,
height: 20 ,
overflow: TextOverflow . ellipsis ,
child: CircularProgressIndicator ( strokeWidth: 2 ) ,
style: theme . textTheme . bodySmall ? . copyWith (
)
color: theme . colorScheme . error ,
: const Icon ( Icons . refresh ) ,
) ,
label: Text ( context . l10n . repeater_refresh ) ,
) ,
] ,
SizedBox ( height: gap ) ,
/ / ─ ─ Action buttons ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
if ( isNarrow )
Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
if ( ! _supportsHotPlug ) . . . [
OutlinedButton . icon (
onPressed: _isLoadingPorts | | _isConnecting
? null
: ( ) {
appLogger . info (
' Refresh ports pressed ' ,
tag: ' UsbScreen ' ,
) ;
_loadPorts ( ) ;
} ,
icon: const Icon ( Icons . refresh ) ,
label: Text ( l10n . repeater_refresh ) ,
) ,
SizedBox ( height: gap ) ,
] ,
FilledButton . icon (
onPressed: _canConnect
? ( ) {
final rawPortName = normalizeUsbPortName (
_selectedPort ! ,
) ;
appLogger . info (
' Connect pressed for $ _selectedPort (raw: $ rawPortName ) ' ,
tag: ' UsbScreen ' ,
) ;
_connectSelectedPort ( ) ;
}
: null ,
icon: _isConnecting
? const SizedBox (
width: 18 ,
height: 18 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
) ,
)
: const Icon ( Icons . usb ) ,
label: Text ( l10n . common_connect ) ,
) ,
] ,
)
else
Row (
children: [
if ( ! _supportsHotPlug ) . . . [
Expanded (
child: OutlinedButton . icon (
onPressed: _isLoadingPorts | | _isConnecting
? null
: ( ) {
appLogger . info (
' Refresh ports pressed ' ,
tag: ' UsbScreen ' ,
) ;
_loadPorts ( ) ;
} ,
icon: const Icon ( Icons . refresh ) ,
label: Text ( l10n . repeater_refresh ) ,
) ,
) ,
SizedBox ( width: gap ) ,
] ,
Expanded (
child: FilledButton . icon (
onPressed: _canConnect
? ( ) {
final rawPortName = normalizeUsbPortName (
_selectedPort ! ,
) ;
appLogger . info (
' Connect pressed for $ _selectedPort (raw: $ rawPortName ) ' ,
tag: ' UsbScreen ' ,
) ;
_connectSelectedPort ( ) ;
}
: null ,
icon: _isConnecting
? const SizedBox (
width: 18 ,
height: 18 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
) ,
)
: const Icon ( Icons . usb ) ,
label: Text ( l10n . common_connect ) ,
) ,
) ,
] ,
) ,
SizedBox ( height: math . max ( 4.0 , gap * 0.5 ) ) ,
Text (
l10n . usbScreenNote ,
textAlign: TextAlign . center ,
maxLines: 2 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodySmall ? . copyWith (
color: theme . colorScheme . onSurfaceVariant ,
) ,
) ,
) ,
] ,
] ,
) ,
) ,
) ;
) ;
} ,
} ,
) ,
) ,
) ,
) ;
) ;
}
}
bool get _canConnect = >
Widget _buildStatusBar ( BuildContext context , MeshCoreConnector connector ) {
! _isLoadingPorts & &
final l10n = context . l10n ;
! _isConnecting & &
String statusText ;
_selectedPort ! = null & &
Color statusColor ;
_selectedPort ! . isNotEmpty ;
void _startHotPlugTimer ( ) {
if ( ! _supportsHotPlug ) return ;
_hotPlugTimer ? . cancel ( ) ;
_hotPlugTimer = Timer . periodic ( const Duration ( seconds: 2 ) , ( _ ) {
_pollHotPlug ( ) ;
} ) ;
}
Future < void > _pollHotPlug ( ) async {
if ( _isLoadingPorts ) {
/ / Don ' t interfere with an active connection attempt or initial load.
statusText = l10n . usbStatus_searching ;
if ( _isConnecting | | _isLoadingPorts ) return ;
statusColor = Colors . blue ;
if ( ! mounted ) return ;
} else if ( connector . isUsbTransportConnected ) {
try {
switch ( connector . state ) {
final ports = await _connector . listUsbPorts ( ) ;
case MeshCoreConnectionState . connected:
if ( ! mounted ) return ;
statusText = l10n . scanner_connectedTo (
final added = ports . where ( ( p ) = > ! _ports . contains ( p ) ) . toList ( ) ;
connector . activeUsbPortDisplayLabel ? ? ' USB ' ,
final removed = _ports . where ( ( p ) = > ! ports . contains ( p ) ) . toList ( ) ;
) ;
if ( added . isEmpty & & removed . isEmpty ) return ;
statusColor = Colors . green ;
setState ( ( ) {
case MeshCoreConnectionState . disconnecting:
_ports
statusText = l10n . scanner_disconnecting ;
. . clear ( )
statusColor = Colors . orange ;
. . addAll ( ports ) ;
default :
if ( _ports . isEmpty ) {
statusText = l10n . usbStatus_notConnected ;
_selectedPort = null ;
statusColor = Colors . grey ;
} else if ( added . isNotEmpty ) {
}
/ / Auto - select the newly - connected device .
} else if ( connector . state = = MeshCoreConnectionState . connecting & &
_selectedPort = added . first ;
connector . activeTransport = = MeshCoreTransportType . usb ) {
} else if ( _selectedPort ! = null & & ! _ports . contains ( _selectedPort ) ) {
statusText = l10n . usbStatus_connecting ;
/ / Previously - selected device was unplugged .
statusColor = Colors . orange ;
_selectedPort = _ports . isNotEmpty ? _ports . first : null ;
} else {
}
statusText = l10n . usbStatus_notConnected ;
} ) ;
statusColor = Colors . grey ;
} catch ( _ ) {
/ / Silent — hot - plug failures are non - critical .
}
}
return Container (
width: double . infinity ,
padding: const EdgeInsets . symmetric ( vertical: 8 , horizontal: 16 ) ,
color: statusColor . withValues ( alpha: 0.1 ) ,
child: Row (
children: [
Icon ( Icons . circle , size: 12 , color: statusColor ) ,
const SizedBox ( width: 8 ) ,
Text (
statusText ,
style: TextStyle ( color: statusColor , fontWeight: FontWeight . w500 ) ,
) ,
] ,
) ,
) ;
}
}
Widget _buildPortList ( BuildContext context ) {
Widget _buildPortList ( BuildContext context , MeshCoreConnector connector ) {
final theme = Theme . of ( context ) ;
final l10n = context . l10n ;
final l10n = context . l10n ;
if ( _isLoadingPorts ) {
if ( _isLoadingPorts ) {
return Center (
return Center (
child: Column (
child: Column (
mainAxisSize: MainAxisSize . min ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
children: [
const CircularProgressIndicator ( ) ,
Icon ( Icons . usb , size: 64 , color: Colors . grey [ 400 ] ) ,
const SizedBox ( height: 12 ) ,
const SizedBox ( height: 16 ) ,
Text ( l10n . common_loading ) ,
Text (
l10n . usbStatus_searching ,
style: TextStyle ( fontSize: 16 , color: Colors . grey [ 600 ] ) ,
) ,
] ,
] ,
) ,
) ,
) ;
) ;
@ -387,152 +221,143 @@ class _UsbScreenState extends State<UsbScreen> {
if ( _ports . isEmpty ) {
if ( _ports . isEmpty ) {
return Center (
return Center (
child: Text (
child: Column (
l10n . usbScreenEmptyState ,
mainAxisAlignment: MainAxisAlignment . center ,
textAlign: TextAlign . center ,
children: [
style: theme . textTheme . bodyMedium ? . copyWith (
Icon ( Icons . usb , size: 64 , color: Colors . grey [ 400 ] ) ,
color: theme . colorScheme . onSurfaceVariant ,
const SizedBox ( height: 16 ) ,
) ,
Text (
l10n . usbScreenEmptyState ,
textAlign: TextAlign . center ,
style: TextStyle ( fontSize: 16 , color: Colors . grey [ 600 ] ) ,
) ,
] ,
) ,
) ,
) ;
) ;
}
}
final isConnecting =
connector . state = = MeshCoreConnectionState . connecting & &
connector . activeTransport = = MeshCoreTransportType . usb ;
return ListView . separated (
return ListView . separated (
padding: const EdgeInsets . all ( 8 ) ,
itemCount: _ports . length ,
itemCount: _ports . length ,
separatorBuilder: ( context , index ) = > const Divider ( ) ,
itemBuilder: ( context , index ) {
itemBuilder: ( context , index ) {
final port = _ports [ index ] ;
final port = _ports [ index ] ;
final isSelected = port = = _selectedPort ;
final displayName = friendlyUsbPortName ( port ) ;
final displayName = _friendlyPortName ( port ) ;
final rawName = normalizeUsbPortName ( port ) ;
final rawName = normalizeUsbPortName ( port ) ;
final showRawName =
final showRawName =
rawName ! = displayName & & ! rawName . startsWith ( ' web: ' ) ;
rawName ! = displayName & & ! rawName . startsWith ( ' web: ' ) ;
return Material (
color: isSelected
return ListTile (
? theme . colorScheme . primaryContainer
leading: const Icon ( Icons . usb ) ,
: theme . colorScheme . surfaceContainerLow ,
title: Text (
borderRadius: BorderRadius . circular ( 16 ) ,
displayName ,
child: ListTile (
style: const TextStyle ( fontWeight: FontWeight . w500 ) ,
onTap: _isConnecting
) ,
? null
subtitle: showRawName ? Text ( rawName ) : null ,
: ( ) {
trailing: ElevatedButton (
setState ( ( ) {
onPressed:
_selectedPort = port ;
isConnecting ? null : ( ) = > _connectPort ( port ) ,
_errorText = null ;
child: Text ( l10n . common_connect ) ,
} ) ;
appLogger . info ( ' Selected port $ port ' , tag: ' UsbScreen ' ) ;
} ,
leading: Icon (
Icons . usb ,
color: isSelected
? theme . colorScheme . onPrimaryContainer
: theme . colorScheme . onSurfaceVariant ,
) ,
title: Text (
displayName ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . titleMedium ? . copyWith (
color: isSelected ? theme . colorScheme . onPrimaryContainer : null ,
) ,
) ,
subtitle: showRawName
? Text (
rawName ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
style: theme . textTheme . bodySmall ? . copyWith (
color: isSelected
? theme . colorScheme . onPrimaryContainer
: theme . colorScheme . onSurfaceVariant ,
) ,
)
: null ,
trailing: isSelected
? Icon (
Icons . check_circle ,
color: theme . colorScheme . onPrimaryContainer ,
)
: null ,
) ,
) ,
onTap: isConnecting ? null : ( ) = > _connectPort ( port ) ,
) ;
) ;
} ,
} ,
separatorBuilder: ( context , index ) = > const SizedBox ( height: 10 ) ,
) ;
) ;
}
}
void _startHotPlugTimer ( ) {
if ( ! _supportsHotPlug ) return ;
_hotPlugTimer ? . cancel ( ) ;
_hotPlugTimer = Timer . periodic ( const Duration ( seconds: 2 ) , ( _ ) {
_pollHotPlug ( ) ;
} ) ;
}
Future < void > _pollHotPlug ( ) async {
if ( _isLoadingPorts ) return ;
if ( ! mounted ) return ;
/ / Don ' t poll while connecting or connected.
if ( _connector . state ! = MeshCoreConnectionState . disconnected ) return ;
try {
final ports = await _connector . listUsbPorts ( ) ;
if ( ! mounted ) return ;
final added = ports . where ( ( p ) = > ! _ports . contains ( p ) ) . toList ( ) ;
final removed = _ports . where ( ( p ) = > ! ports . contains ( p ) ) . toList ( ) ;
if ( added . isEmpty & & removed . isEmpty ) return ;
setState ( ( ) {
_ports
. . clear ( )
. . addAll ( ports ) ;
} ) ;
} catch ( _ ) {
/ / Silent — hot - plug failures are non - critical .
}
}
Future < void > _loadPorts ( ) async {
Future < void > _loadPorts ( ) async {
if ( ! mounted ) return ;
if ( ! mounted ) return ;
_usbConnector . setRequestPortLabel ( context . l10n . usbScreenStatus ) ;
_ connector. setUsb RequestPortLabel( context . l10n . usbScreenStatus ) ;
setState ( ( ) {
setState ( ( ) {
_isLoadingPorts = true ;
_isLoadingPorts = true ;
_errorText = null ;
} ) ;
} ) ;
try {
try {
final ports = await _usbConnector . listPorts ( ) ;
final ports = await _ connector. listUsb Ports( ) ;
if ( ! mounted ) return ;
if ( ! mounted ) return ;
setState ( ( ) {
setState ( ( ) {
_ports
_ports
. . clear ( )
. . clear ( )
. . addAll ( ports ) ;
. . addAll ( ports ) ;
if ( _ports . isEmpty ) {
_selectedPort = null ;
} else if ( ! _ports . contains ( _selectedPort ) ) {
_selectedPort = _ports . first ;
}
_isLoadingPorts = false ;
_isLoadingPorts = false ;
} ) ;
} ) ;
} catch ( error ) {
} catch ( error ) {
if ( ! mounted ) return ;
if ( ! mounted ) return ;
setState ( ( ) {
setState ( ( ) {
_ports . clear ( ) ;
_ports . clear ( ) ;
_selectedPort = null ;
_errorText = _friendlyErrorMessage ( error ) ;
_isLoadingPorts = false ;
_isLoadingPorts = false ;
} ) ;
} ) ;
_showError ( error ) ;
}
}
}
}
Future < void > _connectSelectedPort ( ) async {
Future < void > _connectPort ( String port ) async {
final selectedPort = _selectedPort ;
if ( _connector . state ! = MeshCoreConnectionState . disconnected ) return ;
if ( selectedPort = = null | | selectedPort . isEmpty ) {
return ;
}
_usbConnector . setRequestPortLabel ( context . l10n . usbScreenStatus ) ;
if ( _usbConnector . state ! = MeshCoreConnectionState . disconnected ) {
setState ( ( ) {
_isConnecting = false ;
_errorText = null ;
} ) ;
return ;
}
final rawPortName = normalizeUsbPortName ( selectedPort ) ;
setState ( ( ) {
final rawPortName = normalizeUsbPortName ( port ) ;
_isConnecting = true ;
appLogger . info ( ' Connect tapped for $ port (raw: $ rawPortName ) ' ,
_errorText = null ;
tag: ' UsbScreen ' ) ;
} ) ;
try {
try {
await _ usbConnector. connect ( portName: rawPortName ) ;
await _connector . connectUsb ( portName: rawPortName ) ;
} catch ( error , stackTrace ) {
} catch ( error , stackTrace ) {
appLogger . error (
appLogger . error (
' Connect failed for $ rawPortName : $ error \n $ stackTrace ' ,
' Connect failed for $ rawPortName : $ error \n $ stackTrace ' ,
tag: ' UsbScreen ' ,
tag: ' UsbScreen ' ,
) ;
) ;
if ( ! mounted ) return ;
if ( ! mounted ) return ;
setState ( ( ) {
_showError ( error ) ;
_isConnecting = false ;
_errorText = _friendlyErrorMessage ( error ) ;
} ) ;
/ / Re - scan so stale or renamed port entries are cleared from the list .
unawaited ( _loadPorts ( ) ) ;
unawaited ( _loadPorts ( ) ) ;
}
}
}
}
void _showError ( Object error ) {
if ( ! mounted ) return ;
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text ( _friendlyErrorMessage ( error ) ) ,
backgroundColor: Colors . red ,
) ,
) ;
}
String _friendlyErrorMessage ( Object error ) {
String _friendlyErrorMessage ( Object error ) {
final l10n = context . l10n ;
final l10n = context . l10n ;
if ( error is PlatformException ) {
if ( error is PlatformException ) {
switch ( error . code ) {
switch ( error . code ) {
case ' usb_permission_denied ' :
case ' usb_permission_denied ' :
@ -546,43 +371,35 @@ class _UsbScreenState extends State<UsbScreen> {
return l10n . usbErrorBusy ;
return l10n . usbErrorBusy ;
case ' usb_not_connected ' :
case ' usb_not_connected ' :
return l10n . usbErrorNotConnected ;
return l10n . usbErrorNotConnected ;
case ' usb_driver_missing ' :
case ' usb_open_failed ' :
case ' usb_open_failed ' :
case ' usb_driver_missing ' :
return l10n . usbErrorOpenFailed ;
return l10n . usbErrorOpenFailed ;
case ' usb_connect_failed ' :
case ' usb_connect_failed ' :
case ' usb_write_failed ' :
case ' usb_io_error ' :
return l10n . usbErrorConnectFailed ;
return l10n . usbErrorConnectFailed ;
}
}
}
}
var msg = error . toString ( ) ;
if ( error is UnsupportedError ) {
if ( msg . startsWith ( ' Bad state: ' ) ) {
return l10n . usbErrorUnsupported ;
msg = msg . substring ( ' Bad state: ' . length ) ;
} else if ( msg . startsWith ( ' Exception: ' ) ) {
msg = msg . substring ( ' Exception: ' . length ) ;
}
}
switch ( msg ) {
if ( error is StateError ) {
case ' USB serial transport is already active ' :
final msg = error . message ;
return l10n . usbErrorAlreadyActive ;
if ( msg . contains ( ' already active ' ) ) return l10n . usbErrorAlreadyActive ;
case ' No USB serial device selected ' :
if ( msg . contains ( ' No USB serial device selected ' ) ) {
return l10n . usbErrorNoDeviceSelected ;
return l10n . usbErrorNoDeviceSelected ;
case ' USB serial port is not open ' :
}
if ( msg . contains ( ' not open ' ) | | msg . contains ( ' closed ' ) ) {
return l10n . usbErrorPortClosed ;
return l10n . usbErrorPortClosed ;
case ' USB serial is not supported on this platform. ' :
}
case ' Web Serial is not supported by this browser. ' :
if ( msg . contains ( ' Timed out ' ) ) return l10n . usbErrorConnectTimedOut ;
return l10n . usbErrorUnsupported ;
if ( msg . contains ( ' Failed to open ' ) ) return l10n . usbErrorOpenFailed ;
case ' Timed out waiting for SELF_INFO during connect ' :
return l10n . usbErrorConnectTimedOut ;
}
}
if ( msg. startsWith ( ' Failed to open USB port ' ) ) {
if ( error is TimeoutException ) {
return l10n . usbError OpenFailed ;
return l10n . usbError ConnectTimedOut ;
}
}
return msg ;
return error. toString ( ) ;
}
}
String _friendlyPortName ( String portLabel ) = > friendlyUsbPortName ( portLabel ) ;
}
}