diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt index 52e7650..279ba8a 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt @@ -40,15 +40,15 @@ class MeshcoreUsbFunctions( private val mainHandler = Handler(Looper.getMainLooper()) private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor() - private var eventSink: EventChannel.EventSink? = null - private var usbConnection: UsbDeviceConnection? = null - private var usbInEndpoint: UsbEndpoint? = null - private var usbOutEndpoint: UsbEndpoint? = null - private var controlInterface: UsbInterface? = null - private var dataInterface: UsbInterface? = null + @Volatile private var eventSink: EventChannel.EventSink? = null + @Volatile private var usbConnection: UsbDeviceConnection? = null + @Volatile private var usbInEndpoint: UsbEndpoint? = null + @Volatile private var usbOutEndpoint: UsbEndpoint? = null + @Volatile private var controlInterface: UsbInterface? = null + @Volatile private var dataInterface: UsbInterface? = null private var readThread: Thread? = null @Volatile private var isReading = false - private var connectedDeviceName: String? = null + @Volatile private var connectedDeviceName: String? = null private var pendingConnectResult: MethodChannel.Result? = null private var pendingConnectPortName: String? = null @@ -86,7 +86,7 @@ class MeshcoreUsbFunctions( if (device == null) { result.error( "usb_device_missing", - "USB device no longer available for $portName", + null, null, ) return @@ -95,7 +95,7 @@ class MeshcoreUsbFunctions( val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) if (!granted || !usbManager.hasPermission(device)) { - result.error("usb_permission_denied", "USB permission denied", null) + result.error("usb_permission_denied", null, null) return } @@ -176,13 +176,13 @@ class MeshcoreUsbFunctions( val portName = call.argument("portName") val baudRate = call.argument("baudRate") ?: 115200 if (portName.isNullOrBlank()) { - result.error("usb_invalid_port", "Port name is required", null) + result.error("usb_invalid_port", null, null) return } val device = findUsbDevice(portName) if (device == null) { - result.error("usb_device_missing", "USB device not found for $portName", null) + result.error("usb_device_missing", null, null) return } @@ -192,7 +192,7 @@ class MeshcoreUsbFunctions( } if (pendingConnectResult != null) { - result.error("usb_busy", "Another USB permission request is already pending", null) + result.error("usb_busy", null, null) return } @@ -214,11 +214,11 @@ class MeshcoreUsbFunctions( val connection = usbConnection val endpoint = usbOutEndpoint if (data == null) { - result.error("usb_invalid_data", "Data is required", null) + result.error("usb_invalid_data", null, null) return } if (connection == null || endpoint == null) { - result.error("usb_not_connected", "USB serial port is not connected", null) + result.error("usb_not_connected", null, null) return } @@ -259,7 +259,7 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_driver_missing", - "No compatible USB serial interface for ${device.deviceName}", + null, null, ) } @@ -271,7 +271,7 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_open_failed", - "UsbManager could not open ${device.deviceName}", + null, null, ) } @@ -283,7 +283,7 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_open_failed", - "Could not claim USB data interface for ${device.deviceName}", + null, null, ) } @@ -299,20 +299,21 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_open_failed", - "Could not claim USB control interface for ${device.deviceName}", + null, null, ) } return@execute } - configureDevice(connection, config, baudRate) - usbConnection = connection usbInEndpoint = config.inEndpoint usbOutEndpoint = config.outEndpoint controlInterface = config.controlInterface dataInterface = config.dataInterface + + configureDevice(connection, config, baudRate) + connectedDeviceName = device.deviceName startReadLoop() diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 4299c43..4fa8412 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -21,7 +21,7 @@ import '../services/path_history_service.dart'; import '../services/app_settings_service.dart'; import '../services/background_service.dart'; import '../services/notification_service.dart'; -import '../services/usb_serial_service.dart'; +import 'meshcore_connector_usb.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; @@ -114,11 +114,9 @@ class MeshCoreConnector extends ChangeNotifier { String? _lastDeviceId; String? _lastDeviceDisplayName; bool _manualDisconnect = false; - final UsbSerialService _usbSerialService = UsbSerialService(); + final MeshCoreUsbManager _usbManager = MeshCoreUsbManager(); StreamSubscription? _usbFrameSubscription; MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; - String? _activeUsbPortKey; - String? _activeUsbPortLabel; final List _scanResults = []; final List _contacts = []; @@ -252,9 +250,8 @@ class MeshCoreConnector extends ChangeNotifier { String get deviceIdLabel => _deviceId ?? 'Unknown'; MeshCoreTransportType get activeTransport => _activeTransport; - String? get activeUsbPort => _activeUsbPortKey; - String? get activeUsbPortDisplayLabel => - _activeUsbPortLabel ?? _activeUsbPortKey; + String? get activeUsbPort => _usbManager.activePortKey; + String? get activeUsbPortDisplayLabel => _usbManager.activePortDisplayLabel; bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; @@ -661,7 +658,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService = bleDebugLogService; _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; - _usbSerialService.setDebugLogService(_appDebugLogService); + _usbManager.setDebugLogService(_appDebugLogService); // Initialize notification service _notificationService.initialize(); @@ -871,10 +868,14 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future> listUsbPorts() => _usbSerialService.listPorts(); + Future> listUsbPorts() => _usbManager.listPorts(); void setUsbRequestPortLabel(String label) { - _usbSerialService.setRequestPortLabel(label); + _usbManager.setRequestPortLabel(label); + } + + void setUsbFallbackDeviceName(String label) { + _usbManager.setFallbackDeviceName(label); } Future connectUsb({ @@ -883,53 +884,70 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { + _appDebugLogService?.warn( + 'connectUsb ignored: already $_state', + tag: 'USB', + ); return; } - _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; + _appDebugLogService?.info( + 'connectUsb: port=$portName baud=$baudRate', + tag: 'USB', + ); await stopScan(); _cancelReconnectTimer(); _manualDisconnect = false; _resetConnectionHandshakeState(); _activeTransport = MeshCoreTransportType.usb; - _activeUsbPortKey = portName; - _activeUsbPortLabel = portName; _setState(MeshCoreConnectionState.connecting); try { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; - await _usbSerialService.connect(portName: portName, baudRate: baudRate); - _activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey; - _activeUsbPortLabel = - _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; + _appDebugLogService?.info( + 'connectUsb: opening serial port…', + tag: 'USB', + ); + await _usbManager.connect(portName: portName, baudRate: baudRate); + _appDebugLogService?.info( + 'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}', + tag: 'USB', + ); notifyListeners(); if (PlatformInfo.isWeb) { await stopScan(); } await Future.delayed(const Duration(milliseconds: 200)); - _usbFrameSubscription = _usbSerialService.frameStream.listen( + _usbFrameSubscription = _usbManager.frameStream.listen( _handleFrame, onError: (error, stackTrace) { _appDebugLogService?.error('USB transport error: $error', tag: 'USB'); unawaited(disconnect(manual: false)); }, onDone: () { + _appDebugLogService?.warn('USB frame stream ended', tag: 'USB'); unawaited(disconnect(manual: false)); }, ); _setState(MeshCoreConnectionState.connected); _pendingInitialChannelSync = true; + _appDebugLogService?.info( + 'connectUsb: requesting device info…', + tag: 'USB', + ); await _requestDeviceInfo(); _startBatteryPolling(); var gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), ); if (!gotSelfInfo) { + _appDebugLogService?.warn( + 'connectUsb: SELF_INFO timeout, retrying…', + tag: 'USB', + ); await refreshDeviceInfo(); gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -939,7 +957,9 @@ class MeshCoreConnector extends ChangeNotifier { throw StateError('Timed out waiting for SELF_INFO during connect'); } + _appDebugLogService?.info('connectUsb: syncing time…', tag: 'USB'); await syncTime(); + _appDebugLogService?.info('connectUsb: complete', tag: 'USB'); } catch (error) { _appDebugLogService?.error('USB connection error: $error', tag: 'USB'); await disconnect(manual: false); @@ -954,8 +974,6 @@ class MeshCoreConnector extends ChangeNotifier { } _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; await stopScan(); _setState(MeshCoreConnectionState.connecting); @@ -1282,7 +1300,7 @@ class MeshCoreConnector extends ChangeNotifier { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; - await _usbSerialService.disconnect(); + await _usbManager.disconnect(); await _notifySubscription?.cancel(); _notifySubscription = null; @@ -1341,8 +1359,6 @@ class MeshCoreConnector extends ChangeNotifier { _reactionSendQueueSequence = 0; _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; _setState(MeshCoreConnectionState.disconnected); _appDebugLogService?.info( @@ -1365,7 +1381,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService?.logFrame(data, outgoing: true); if (_activeTransport == MeshCoreTransportType.usb) { - await _usbSerialService.write(data); + await _usbManager.write(data); } else { if (_rxCharacteristic == null) { throw Exception("MeshCore RX characteristic not available"); @@ -2464,9 +2480,7 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb && selfName != null && selfName.isNotEmpty) { - _usbSerialService.updateConnectedLabel(selfName); - _activeUsbPortLabel = - _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; + _usbManager.updateConnectedLabel(selfName); } _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); @@ -4246,7 +4260,7 @@ class MeshCoreConnector extends ChangeNotifier { _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); - _usbSerialService.dispose(); + _usbManager.dispose(); // Flush pending unread writes before disposal _unreadStore.flush(); @@ -4269,6 +4283,10 @@ class MeshCoreConnector extends ChangeNotifier { final header = packet.readByte(); routeType = header & 0x03; payloadType = (header >> 2) & 0x0F; + if (routeType == _routeTransportFlood || + routeType == _routeTransportDirect) { + packet.skipBytes(4); // Skip transport-specific bytes + } //final payloadVer = (header >> 6) & 0x03; final pathLen = packet.readByte(); pathBytes = packet.readBytes(pathLen); @@ -4301,7 +4319,12 @@ class MeshCoreConnector extends ChangeNotifier { packet.skipBytes(1); // Skip SNR byte packet.skipBytes(1); // Skip RSSI byte final header = packet.readByte(); + final routeType = header & 0x03; payloadType = (header >> 2) & 0x0F; + if (routeType == _routeTransportFlood || + routeType == _routeTransportDirect) { + packet.skipBytes(4); // Skip transport-specific bytes + } //final payloadVer = (header >> 6) & 0x03; final pathLen = packet.readByte(); pathBytes = packet.readBytes(pathLen); diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index b1fbe15..a0f70d5 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -1,32 +1,71 @@ -import 'package:flutter/foundation.dart'; +import 'dart:typed_data'; -import 'meshcore_connector.dart'; +import '../services/app_debug_log_service.dart'; +import '../services/usb_serial_service.dart'; -class MeshCoreConnectorUsb { - const MeshCoreConnectorUsb(this.connector); +/// Manages USB serial transport for MeshCore devices. +/// +/// Owns the [UsbSerialService] and USB-specific connection state. +/// The main [MeshCoreConnector] delegates all USB operations here. +class MeshCoreUsbManager { + MeshCoreUsbManager(); - final MeshCoreConnector connector; + final UsbSerialService _service = UsbSerialService(); + AppDebugLogService? _debugLog; + String? _activePortKey; + String? _activePortLabel; - MeshCoreConnectionState get state => connector.state; - MeshCoreTransportType get activeTransport => connector.activeTransport; - String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel; - bool get isUsbTransportConnected => connector.isUsbTransportConnected; + // --- Getters --- + String? get activePortKey => _activePortKey; + String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey; + bool get isConnected => _service.isConnected; + Stream get frameStream => _service.frameStream; - void addListener(VoidCallback listener) => connector.addListener(listener); - void removeListener(VoidCallback listener) => - connector.removeListener(listener); + // --- Configuration --- + Future> listPorts() => _service.listPorts(); - Future> listPorts() => connector.listUsbPorts(); + void setRequestPortLabel(String label) => + _service.setRequestPortLabel(label); - void setRequestPortLabel(String label) { - connector.setUsbRequestPortLabel(label); + void setFallbackDeviceName(String label) => + _service.setFallbackDeviceName(label); + + void setDebugLogService(AppDebugLogService? service) { + _debugLog = service; + _service.setDebugLogService(service); } - Future connect({required String portName, int baudRate = 115200}) { - return connector.connectUsb(portName: portName, baudRate: baudRate); + // --- Connection lifecycle --- + Future connect({required String portName, int baudRate = 115200}) async { + _debugLog?.info( + 'UsbManager.connect: portName=$portName baud=$baudRate', + tag: 'USB', + ); + await _service.connect(portName: portName, baudRate: baudRate); + _activePortKey = _service.activePortKey ?? portName; + _activePortLabel = _service.activePortDisplayLabel ?? portName; + _debugLog?.info( + 'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel', + tag: 'USB', + ); } - Future disconnect({bool manual = true}) { - return connector.disconnect(manual: manual); + Future disconnect() async { + _debugLog?.info('UsbManager.disconnect', tag: 'USB'); + await _service.disconnect(); + _activePortKey = null; + _activePortLabel = null; + } + + Future write(Uint8List data) => _service.write(data); + + // --- Label management --- + void updateConnectedLabel(String selfName) { + _service.updateConnectedLabel(selfName); + _activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel; + } + + void dispose() { + _service.dispose(); } } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c2879dd..94d8997 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "USB връзката вече е активирана.", "usbErrorNoDeviceSelected": "Няма избран USB устройство.", "usbErrorPortClosed": "USB връзката не е активна.", - "usbErrorConnectTimedOut": "Изчаква се, но устройството не отговаря в рамките на зададения време." + "usbFallbackDeviceName": "Устройство за четене на уеб серийни данни", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_connecting": "Свързване към USB устройство...", + "usbConnectionFailed": "Неуспешно свързване през USB: {error}", + "usbStatus_notConnected": "Изберете USB устройство", + "usbStatus_searching": "Търсене на USB устройства...", + "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ad6b0bf..9ba0f51 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1875,5 +1875,17 @@ "usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.", "usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.", "usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.", - "usbErrorConnectTimedOut": "Wartezeit abgelaufen, da keine Antwort vom Gerät empfangen wurde." + "usbFallbackDeviceName": "Web-Serielle Geräte", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Suche nach USB-Geräten...", + "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", + "usbStatus_connecting": "Verbindung zum USB-Gerät...", + "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", + "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4ffa573..2605628 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -65,7 +65,19 @@ "usbErrorAlreadyActive": "A USB connection is already active.", "usbErrorNoDeviceSelected": "No USB device was selected.", "usbErrorPortClosed": "The USB connection is not open.", - "usbErrorConnectTimedOut": "Timed out waiting for the device to respond.", + "usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.", + "usbFallbackDeviceName": "Web Serial Device", + "usbStatus_notConnected": "Select a USB device", + "usbStatus_connecting": "Connecting to USB device...", + "usbStatus_searching": "Searching for USB devices...", + "usbConnectionFailed": "USB connection failed: {error}", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "scanner_scanning": "Scanning for devices...", "scanner_connecting": "Connecting...", "scanner_disconnecting": "Disconnecting...", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 09545c7..9b791d3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1875,5 +1875,17 @@ "usbErrorAlreadyActive": "La conexión USB ya está activa.", "usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.", "usbErrorPortClosed": "La conexión USB no está activa.", - "usbErrorConnectTimedOut": "Se ha agotado el tiempo de espera mientras se esperaba la respuesta del dispositivo." + "usbFallbackDeviceName": "Dispositivo de serie web", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_connecting": "Conectándose al dispositivo USB...", + "usbStatus_searching": "Buscando dispositivos USB...", + "usbStatus_notConnected": "Seleccione un dispositivo USB", + "usbConnectionFailed": "Error al conectar mediante USB: {error}", + "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f6750f8..a7bedc9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Une connexion USB est déjà établie.", "usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.", "usbErrorPortClosed": "La connexion USB n'est pas établie.", - "usbErrorConnectTimedOut": "Attente avec délai, en attendant une réponse de l'appareil." + "usbFallbackDeviceName": "Dispositif de communication série sur le Web", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_notConnected": "Sélectionnez un périphérique USB", + "usbConnectionFailed": "Échec de la connexion USB : {error}", + "usbStatus_connecting": "Connexion au périphérique USB...", + "usbStatus_searching": "Recherche de périphériques USB...", + "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b4e5c14..423ff40 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "La connessione USB è già attiva.", "usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.", "usbErrorPortClosed": "La connessione USB non è attiva.", - "usbErrorConnectTimedOut": "Attesa superata, in attesa di una risposta dal dispositivo." + "usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Ricerca di dispositivi USB...", + "usbConnectionFailed": "Errore nella connessione USB: {error}", + "usbStatus_notConnected": "Seleziona un dispositivo USB", + "usbStatus_connecting": "Connessione al dispositivo USB...", + "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d3e5db9..8d3f86b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -433,9 +433,39 @@ abstract class AppLocalizations { /// No description provided for @usbErrorConnectTimedOut. /// /// In en, this message translates to: - /// **'Timed out waiting for the device to respond.'** + /// **'Connection timed out. Make sure the device has USB Companion firmware.'** String get usbErrorConnectTimedOut; + /// No description provided for @usbFallbackDeviceName. + /// + /// In en, this message translates to: + /// **'Web Serial Device'** + String get usbFallbackDeviceName; + + /// No description provided for @usbStatus_notConnected. + /// + /// In en, this message translates to: + /// **'Select a USB device'** + String get usbStatus_notConnected; + + /// No description provided for @usbStatus_connecting. + /// + /// In en, this message translates to: + /// **'Connecting to USB device...'** + String get usbStatus_connecting; + + /// No description provided for @usbStatus_searching. + /// + /// In en, this message translates to: + /// **'Searching for USB devices...'** + String get usbStatus_searching; + + /// No description provided for @usbConnectionFailed. + /// + /// In en, this message translates to: + /// **'USB connection failed: {error}'** + String usbConnectionFailed(String error); + /// No description provided for @scanner_scanning. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cb4739a..356106e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -175,7 +175,25 @@ class AppLocalizationsBg extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Изчаква се, но устройството не отговаря в рамките на зададения време.'; + 'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.'; + + @override + String get usbFallbackDeviceName => + 'Устройство за четене на уеб серийни данни'; + + @override + String get usbStatus_notConnected => 'Изберете USB устройство'; + + @override + String get usbStatus_connecting => 'Свързване към USB устройство...'; + + @override + String get usbStatus_searching => 'Търсене на USB устройства...'; + + @override + String usbConnectionFailed(String error) { + return 'Неуспешно свързване през USB: $error'; + } @override String get scanner_scanning => 'Сканиране за устройства...'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ad7dd84..6353f35 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -177,7 +177,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Wartezeit abgelaufen, da keine Antwort vom Gerät empfangen wurde.'; + 'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.'; + + @override + String get usbFallbackDeviceName => 'Web-Serielle Geräte'; + + @override + String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus'; + + @override + String get usbStatus_connecting => 'Verbindung zum USB-Gerät...'; + + @override + String get usbStatus_searching => 'Suche nach USB-Geräten...'; + + @override + String usbConnectionFailed(String error) { + return 'Fehler beim USB-Verbindungsaufbau: $error'; + } @override String get scanner_scanning => 'Scannen nach Geräten...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5a9fc53..9c20df7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -174,7 +174,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Timed out waiting for the device to respond.'; + 'Connection timed out. Make sure the device has USB Companion firmware.'; + + @override + String get usbFallbackDeviceName => 'Web Serial Device'; + + @override + String get usbStatus_notConnected => 'Select a USB device'; + + @override + String get usbStatus_connecting => 'Connecting to USB device...'; + + @override + String get usbStatus_searching => 'Searching for USB devices...'; + + @override + String usbConnectionFailed(String error) { + return 'USB connection failed: $error'; + } @override String get scanner_scanning => 'Scanning for devices...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 60b0936..eecbd48 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -177,7 +177,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Se ha agotado el tiempo de espera mientras se esperaba la respuesta del dispositivo.'; + 'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => 'Dispositivo de serie web'; + + @override + String get usbStatus_notConnected => 'Seleccione un dispositivo USB'; + + @override + String get usbStatus_connecting => 'Conectándose al dispositivo USB...'; + + @override + String get usbStatus_searching => 'Buscando dispositivos USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Error al conectar mediante USB: $error'; + } @override String get scanner_scanning => 'Escaneando dispositivos...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index fdeee92..5cabc86 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -176,7 +176,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Attente avec délai, en attendant une réponse de l\'appareil.'; + 'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Dispositif de communication série sur le Web'; + + @override + String get usbStatus_notConnected => 'Sélectionnez un périphérique USB'; + + @override + String get usbStatus_connecting => 'Connexion au périphérique USB...'; + + @override + String get usbStatus_searching => 'Recherche de périphériques USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Échec de la connexion USB : $error'; + } @override String get scanner_scanning => 'Recherche de périphériques...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a5ae362..d170540 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -177,7 +177,25 @@ class AppLocalizationsIt extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Attesa superata, in attesa di una risposta dal dispositivo.'; + 'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Dispositivo per comunicazione seriale su rete'; + + @override + String get usbStatus_notConnected => 'Seleziona un dispositivo USB'; + + @override + String get usbStatus_connecting => 'Connessione al dispositivo USB...'; + + @override + String get usbStatus_searching => 'Ricerca di dispositivi USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Errore nella connessione USB: $error'; + } @override String get scanner_scanning => 'Scansione in corso per i dispositivi...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a437106..323ba34 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -175,7 +175,24 @@ class AppLocalizationsNl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Wachtperiode is verlopen, aangezien het apparaat niet reageerde.'; + 'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.'; + + @override + String get usbFallbackDeviceName => 'Web-serieapparaat'; + + @override + String get usbStatus_notConnected => 'Selecteer een USB-apparaat'; + + @override + String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...'; + + @override + String get usbStatus_searching => 'Zoeken naar USB-apparaten...'; + + @override + String usbConnectionFailed(String error) { + return 'Fout bij de USB-verbinding: $error'; + } @override String get scanner_scanning => 'Scannen naar apparaten...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2f034b9..9175c3e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -177,7 +177,25 @@ class AppLocalizationsPl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji.'; + 'Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".'; + + @override + String get usbFallbackDeviceName => + 'Urządzenie do komunikacji przez sieć (seria)'; + + @override + String get usbStatus_notConnected => 'Wybierz urządzenie USB'; + + @override + String get usbStatus_connecting => 'Połączenie z urządzeniem USB...'; + + @override + String get usbStatus_searching => 'Wyszukiwanie urządzeń USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Błąd połączenia USB: $error'; + } @override String get scanner_scanning => 'Skanowanie urządzeń...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 7795e53..ff09213 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -177,7 +177,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Tempo limite aguardando a resposta do dispositivo.'; + 'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web'; + + @override + String get usbStatus_notConnected => 'Selecione um dispositivo USB'; + + @override + String get usbStatus_connecting => 'Conectando ao dispositivo USB...'; + + @override + String get usbStatus_searching => 'Procurando por dispositivos USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Falha na conexão USB: $error'; + } @override String get scanner_scanning => 'Procurando por dispositivos...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b58ae8d..69a5891 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -177,7 +177,25 @@ class AppLocalizationsRu extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Ожидание ответа от устройства превысило установленное время.'; + 'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Устройство для последовательного подключения к сети'; + + @override + String get usbStatus_notConnected => 'Выберите USB-устройство'; + + @override + String get usbStatus_connecting => 'Подключение к USB-устройству...'; + + @override + String get usbStatus_searching => 'Поиск USB-устройств...'; + + @override + String usbConnectionFailed(String error) { + return 'Не удалось установить соединение через USB: $error'; + } @override String get scanner_scanning => 'Поиск устройств...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 403a612..d0e75b0 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -177,7 +177,24 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Čakal som, kým sa zariadenie neozvými, ale časový limit sa dobehol.'; + 'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => 'Webový sériový zariadenie'; + + @override + String get usbStatus_notConnected => 'Vyberte USB zariadenie'; + + @override + String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...'; + + @override + String get usbStatus_searching => 'Hľadanie USB zariadení...'; + + @override + String usbConnectionFailed(String error) { + return 'Neúspešné pripojenie cez USB: $error'; + } @override String get scanner_scanning => 'Skrívania zariadení...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index db9368f..21e3d9d 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -174,7 +174,25 @@ class AppLocalizationsSl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval.'; + 'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Naprave za serijsko komunikacijo preko spleta'; + + @override + String get usbStatus_notConnected => 'Izberite USB naprave.'; + + @override + String get usbStatus_connecting => 'Povezava z USB napravo...'; + + @override + String get usbStatus_searching => 'Iskanje USB naprav...'; + + @override + String usbConnectionFailed(String error) { + return 'Napaka pri povezavi preko USB: $error'; + } @override String get scanner_scanning => 'Skeniram za naprave...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index d223cd6..5951fae 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -129,7 +129,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv på stöderliga Android-enheter och på skrivbordsplattformar.'; + 'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.'; @override String get usbScreenEmptyState => @@ -175,7 +175,24 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Tiden har löpt ut medan vi väntade på att enheten skulle svara.'; + 'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.'; + + @override + String get usbFallbackDeviceName => 'Web-serieenhet'; + + @override + String get usbStatus_notConnected => 'Välj en USB-enhet'; + + @override + String get usbStatus_connecting => 'Anslutning till USB-enhet...'; + + @override + String get usbStatus_searching => 'Söker efter USB-enheter...'; + + @override + String usbConnectionFailed(String error) { + return 'Fel vid USB-anslutning: $error'; + } @override String get scanner_scanning => 'Söker efter enheter...'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 676e941..b8fd60a 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -175,7 +175,25 @@ class AppLocalizationsUk extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Час очікування закінчився, оскільки пристрій не відповів.'; + 'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Пристрій для передачі даних по веб-серіалах'; + + @override + String get usbStatus_notConnected => 'Виберіть пристрій USB'; + + @override + String get usbStatus_connecting => 'Підключення до USB-пристрою...'; + + @override + String get usbStatus_searching => 'Пошук пристроїв USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Не вдалося встановити з\'єднання через USB: $error'; + } @override String get scanner_scanning => 'Пошук пристроїв...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ff42929..27e6c21 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -166,7 +166,24 @@ class AppLocalizationsZh extends AppLocalizations { String get usbErrorPortClosed => 'USB 连接未建立。'; @override - String get usbErrorConnectTimedOut => '等待设备响应超时。'; + String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。'; + + @override + String get usbFallbackDeviceName => 'Web 串流设备'; + + @override + String get usbStatus_notConnected => '选择一个 USB 设备'; + + @override + String get usbStatus_connecting => '连接USB设备...'; + + @override + String get usbStatus_searching => '正在搜索 USB 设备...'; + + @override + String usbConnectionFailed(String error) { + return 'USB 连接失败:$error'; + } @override String get scanner_scanning => '正在搜索设备...'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7d56216..94df130 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Een USB-verbinding is al actief.", "usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.", "usbErrorPortClosed": "De USB-verbinding is niet actief.", - "usbErrorConnectTimedOut": "Wachtperiode is verlopen, aangezien het apparaat niet reageerde." + "usbFallbackDeviceName": "Web-serieapparaat", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbConnectionFailed": "Fout bij de USB-verbinding: {error}", + "usbStatus_notConnected": "Selecteer een USB-apparaat", + "usbStatus_connecting": "Verbinding maken met USB-apparaat...", + "usbStatus_searching": "Zoeken naar USB-apparaten...", + "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index f6cd0be..d020e0e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Połączenie USB jest już aktywne.", "usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.", "usbErrorPortClosed": "Połączenie USB nie jest aktywne.", - "usbErrorConnectTimedOut": "Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji." + "usbFallbackDeviceName": "Urządzenie do komunikacji przez sieć (seria)", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Wyszukiwanie urządzeń USB...", + "usbStatus_connecting": "Połączenie z urządzeniem USB...", + "usbStatus_notConnected": "Wybierz urządzenie USB", + "usbConnectionFailed": "Błąd połączenia USB: {error}", + "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 77bc5c7..d52cb41 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "A conexão USB já está ativa.", "usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.", "usbErrorPortClosed": "A conexão USB não está ativa.", - "usbErrorConnectTimedOut": "Tempo limite aguardando a resposta do dispositivo." + "usbFallbackDeviceName": "Dispositivo de Serial para a Web", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Procurando por dispositivos USB...", + "usbStatus_notConnected": "Selecione um dispositivo USB", + "usbConnectionFailed": "Falha na conexão USB: {error}", + "usbStatus_connecting": "Conectando ao dispositivo USB...", + "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 319496a..92fd55e 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1087,5 +1087,17 @@ "usbErrorAlreadyActive": "USB-соединение уже установлено.", "usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.", "usbErrorPortClosed": "USB-соединение не установлено.", - "usbErrorConnectTimedOut": "Ожидание ответа от устройства превысило установленное время." + "usbFallbackDeviceName": "Устройство для последовательного подключения к сети", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Поиск USB-устройств...", + "usbStatus_connecting": "Подключение к USB-устройству...", + "usbConnectionFailed": "Не удалось установить соединение через USB: {error}", + "usbStatus_notConnected": "Выберите USB-устройство", + "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 571d2f2..141147c 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.", "usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.", "usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.", - "usbErrorConnectTimedOut": "Čakal som, kým sa zariadenie neozvými, ale časový limit sa dobehol." + "usbFallbackDeviceName": "Webový sériový zariadenie", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Hľadanie USB zariadení...", + "usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}", + "usbStatus_notConnected": "Vyberte USB zariadenie", + "usbStatus_connecting": "Pripojenie k USB zariadeniu...", + "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 631e75e..12529d6 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "USB povezava je že aktivirana.", "usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.", "usbErrorPortClosed": "USB povezava ni aktivirana.", - "usbErrorConnectTimedOut": "Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval." + "usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_notConnected": "Izberite USB naprave.", + "usbStatus_connecting": "Povezava z USB napravo...", + "usbStatus_searching": "Iskanje USB naprav...", + "usbConnectionFailed": "Napaka pri povezavi preko USB: {error}", + "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index be1ada8..f7615df 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1834,7 +1834,7 @@ "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", "usbScreenTitle": "Anslut via USB", "usbScreenStatus": "Välj en USB-enhet", - "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderliga Android-enheter och på skrivbordsplattformar.", + "usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.", "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.", "usbErrorPermissionDenied": "Tillgången via USB nekas.", "usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.", @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.", "usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.", "usbErrorPortClosed": "USB-anslutningen är inte aktiv.", - "usbErrorConnectTimedOut": "Tiden har löpt ut medan vi väntade på att enheten skulle svara." + "usbFallbackDeviceName": "Web-serieenhet", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_connecting": "Anslutning till USB-enhet...", + "usbStatus_notConnected": "Välj en USB-enhet", + "usbConnectionFailed": "Fel vid USB-anslutning: {error}", + "usbStatus_searching": "Söker efter USB-enheter...", + "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 76ac380..7794098 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", "usbErrorPortClosed": "З'єднання USB не встановлено.", - "usbErrorConnectTimedOut": "Час очікування закінчився, оскільки пристрій не відповів." + "usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Пошук пристроїв USB...", + "usbStatus_notConnected": "Виберіть пристрій USB", + "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", + "usbStatus_connecting": "Підключення до USB-пристрою...", + "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 76893c9..dfc8e64 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1852,5 +1852,17 @@ "usbErrorAlreadyActive": "USB 连接已建立。", "usbErrorNoDeviceSelected": "未选择任何 USB 设备。", "usbErrorPortClosed": "USB 连接未建立。", - "usbErrorConnectTimedOut": "等待设备响应超时。" + "usbFallbackDeviceName": "Web 串流设备", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "正在搜索 USB 设备...", + "usbStatus_connecting": "连接USB设备...", + "usbStatus_notConnected": "选择一个 USB 设备", + "usbConnectionFailed": "USB 连接失败:{error}", + "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。" } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 5c4dfb1..c78e0c1 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -1,16 +1,15 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; -import '../connector/meshcore_connector_usb.dart'; import '../l10n/l10n.dart'; import '../utils/app_logger.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import 'contacts_screen.dart'; import 'scanner_screen.dart'; @@ -24,20 +23,12 @@ class UsbScreen extends StatefulWidget { class _UsbScreenState extends State { final List _ports = []; bool _isLoadingPorts = true; - bool _isConnecting = false; bool _navigatedToContacts = false; bool _didScheduleInitialLoad = false; - String? _selectedPort; - String? _connectedPortDisplayLabel; - String? _errorText; Timer? _hotPlugTimer; late final MeshCoreConnector _connector; - late final MeshCoreConnectorUsb _usbConnector; 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 => PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS; @@ -45,25 +36,13 @@ class _UsbScreenState extends State { void initState() { super.initState(); _connector = context.read(); - _usbConnector = MeshCoreConnectorUsb(_connector); _connectionListener = () { if (!mounted) return; - final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel; - final shouldUpdateDisplayLabel = - activeUsbPortDisplayLabel != _connectedPortDisplayLabel; - if (_usbConnector.state == MeshCoreConnectionState.disconnected) { + if (_connector.state == MeshCoreConnectionState.disconnected) { _navigatedToContacts = false; - setState(() { - _isConnecting = false; - _connectedPortDisplayLabel = activeUsbPortDisplayLabel; - }); - } else if (shouldUpdateDisplayLabel) { - setState(() { - _connectedPortDisplayLabel = activeUsbPortDisplayLabel; - }); } - if (_usbConnector.state == MeshCoreConnectionState.connected && - _usbConnector.isUsbTransportConnected && + if (_connector.state == MeshCoreConnectionState.connected && + _connector.isUsbTransportConnected && !_navigatedToContacts) { _navigatedToContacts = true; Navigator.of(context).pushReplacement( @@ -71,14 +50,15 @@ class _UsbScreenState extends State { ); } }; - _usbConnector.addListener(_connectionListener); + _connector.addListener(_connectionListener); _startHotPlugTimer(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + _connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName); if (!_didScheduleInitialLoad) { _didScheduleInitialLoad = true; unawaited(_loadPorts()); @@ -89,12 +69,12 @@ class _UsbScreenState extends State { void dispose() { _hotPlugTimer?.cancel(); _hotPlugTimer = null; - _usbConnector.removeListener(_connectionListener); + _connector.removeListener(_connectionListener); if (!_navigatedToContacts && - _usbConnector.activeTransport == MeshCoreTransportType.usb && - _usbConnector.state != MeshCoreConnectionState.disconnected) { + _connector.activeTransport == MeshCoreTransportType.usb && + _connector.state != MeshCoreConnectionState.disconnected) { WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(_usbConnector.disconnect(manual: true)); + unawaited(_connector.disconnect(manual: true)); }); } super.dispose(); @@ -102,234 +82,192 @@ class _UsbScreenState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final l10n = context.l10n; - return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () { - appLogger.info('Back button pressed', tag: 'UsbScreen'); - Navigator.of(context).maybePop(); - }, - ), - title: Text( - l10n.connectionChoiceUsbLabel, - style: theme.textTheme.titleLarge, + onPressed: () => Navigator.of(context).maybePop(), ), + title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle), 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( - child: LayoutBuilder( - builder: (context, constraints) { - final availableHeight = constraints.maxHeight.isFinite - ? constraints.maxHeight - : 600.0; - final availableWidth = constraints.maxWidth.isFinite - ? constraints.maxWidth - : 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), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // ── Compact header ────────────────────────────────────── - Row( - children: [ - Icon( - Icons.usb, - size: iconSize.clamp(24.0, 40.0), - color: theme.colorScheme.primary, - ), - SizedBox(width: gap), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 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, - ), - ), - ], - ), - ), - ], - ), - SizedBox(height: gap), - // ── Port list takes all remaining space ───────────────── - Expanded(child: _buildPortList(context)), - if (_errorText != null) ...[ - SizedBox(height: gap * 0.5), - Text( - _errorText!, - textAlign: TextAlign.center, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - ], - 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, - ), - ), - ], - ), + top: false, + child: Consumer( + builder: (context, connector, child) { + return Column( + children: [ + _buildStatusBar(context, connector), + Expanded(child: _buildPortList(context, connector)), + ], ); }, ), ), + bottomNavigationBar: Consumer( + builder: (context, connector, child) { + final isLoading = _isLoadingPorts; + final showBle = PlatformInfo.isWeb || + PlatformInfo.isAndroid || + PlatformInfo.isIOS; + + return SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (showBle) + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => const ScannerScreen(), + ), + ); + }, + heroTag: 'usb_ble_action', + icon: const Icon(Icons.bluetooth), + label: Text(context.l10n.connectionChoiceBluetoothLabel), + ), + if (showBle) const SizedBox(width: 12), + if (!_supportsHotPlug) + FloatingActionButton.extended( + onPressed: isLoading ? null : _loadPorts, + heroTag: 'usb_refresh_action', + icon: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + label: Text(context.l10n.repeater_refresh), + ), + ], + ), + ); + }, + ), ); } - bool get _canConnect => - !_isLoadingPorts && - !_isConnecting && - _selectedPort != null && - _selectedPort!.isNotEmpty; + Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + String statusText; + Color statusColor; + + if (_isLoadingPorts) { + statusText = l10n.usbStatus_searching; + statusColor = Colors.blue; + } else if (connector.isUsbTransportConnected) { + switch (connector.state) { + case MeshCoreConnectionState.connected: + statusText = l10n.scanner_connectedTo( + connector.activeUsbPortDisplayLabel ?? 'USB', + ); + statusColor = Colors.green; + case MeshCoreConnectionState.disconnecting: + statusText = l10n.scanner_disconnecting; + statusColor = Colors.orange; + default: + statusText = l10n.usbStatus_notConnected; + statusColor = Colors.grey; + } + } else if (connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.usb) { + statusText = l10n.usbStatus_connecting; + statusColor = Colors.orange; + } else { + statusText = l10n.usbStatus_notConnected; + statusColor = Colors.grey; + } + + 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, MeshCoreConnector connector) { + final l10n = context.l10n; + + if (_isLoadingPorts) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.usb, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + l10n.usbStatus_searching, + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ); + } + + if (_ports.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.usb, size: 64, color: Colors.grey[400]), + 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( + padding: const EdgeInsets.all(8), + itemCount: _ports.length, + separatorBuilder: (context, index) => const Divider(), + itemBuilder: (context, index) { + final port = _ports[index]; + final displayName = friendlyUsbPortName(port); + final rawName = normalizeUsbPortName(port); + final showRawName = + rawName != displayName && !rawName.startsWith('web:'); + + return ListTile( + leading: const Icon(Icons.usb), + title: Text( + displayName, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: showRawName ? Text(rawName) : null, + trailing: ElevatedButton( + onPressed: + isConnecting ? null : () => _connectPort(port), + child: Text(l10n.common_connect), + ), + onTap: isConnecting ? null : () => _connectPort(port), + ); + }, + ); + } void _startHotPlugTimer() { if (!_supportsHotPlug) return; @@ -340,9 +278,10 @@ class _UsbScreenState extends State { } Future _pollHotPlug() async { - // Don't interfere with an active connection attempt or initial load. - if (_isConnecting || _isLoadingPorts) return; + 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; @@ -353,186 +292,72 @@ class _UsbScreenState extends State { _ports ..clear() ..addAll(ports); - if (_ports.isEmpty) { - _selectedPort = null; - } else if (added.isNotEmpty) { - // Auto-select the newly-connected device. - _selectedPort = added.first; - } else if (_selectedPort != null && !_ports.contains(_selectedPort)) { - // Previously-selected device was unplugged. - _selectedPort = _ports.isNotEmpty ? _ports.first : null; - } }); } catch (_) { // Silent — hot-plug failures are non-critical. } } - Widget _buildPortList(BuildContext context) { - final theme = Theme.of(context); - final l10n = context.l10n; - - if (_isLoadingPorts) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 12), - Text(l10n.common_loading), - ], - ), - ); - } - - if (_ports.isEmpty) { - return Center( - child: Text( - l10n.usbScreenEmptyState, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ); - } - - return ListView.separated( - itemCount: _ports.length, - itemBuilder: (context, index) { - final port = _ports[index]; - final isSelected = port == _selectedPort; - final displayName = _friendlyPortName(port); - final rawName = normalizeUsbPortName(port); - final showRawName = - rawName != displayName && !rawName.startsWith('web:'); - return Material( - color: isSelected - ? theme.colorScheme.primaryContainer - : theme.colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(16), - child: ListTile( - onTap: _isConnecting - ? null - : () { - setState(() { - _selectedPort = port; - _errorText = null; - }); - 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, - ), - ); - }, - separatorBuilder: (context, index) => const SizedBox(height: 10), - ); - } - Future _loadPorts() async { if (!mounted) return; - _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); setState(() { _isLoadingPorts = true; - _errorText = null; }); try { - final ports = await _usbConnector.listPorts(); + final ports = await _connector.listUsbPorts(); if (!mounted) return; setState(() { _ports ..clear() ..addAll(ports); - if (_ports.isEmpty) { - _selectedPort = null; - } else if (!_ports.contains(_selectedPort)) { - _selectedPort = _ports.first; - } _isLoadingPorts = false; }); } catch (error) { if (!mounted) return; setState(() { _ports.clear(); - _selectedPort = null; - _errorText = _friendlyErrorMessage(error); _isLoadingPorts = false; }); + _showError(error); } } - Future _connectSelectedPort() async { - final selectedPort = _selectedPort; - 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); + Future _connectPort(String port) async { + if (_connector.state != MeshCoreConnectionState.disconnected) return; - setState(() { - _isConnecting = true; - _errorText = null; - }); + final rawPortName = normalizeUsbPortName(port); + appLogger.info('Connect tapped for $port (raw: $rawPortName)', + tag: 'UsbScreen'); try { - await _usbConnector.connect(portName: rawPortName); + await _connector.connectUsb(portName: rawPortName); } catch (error, stackTrace) { appLogger.error( 'Connect failed for $rawPortName: $error\n$stackTrace', tag: 'UsbScreen', ); if (!mounted) return; - setState(() { - _isConnecting = false; - _errorText = _friendlyErrorMessage(error); - }); - // Re-scan so stale or renamed port entries are cleared from the list. + _showError(error); 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) { final l10n = context.l10n; + if (error is PlatformException) { switch (error.code) { case 'usb_permission_denied': @@ -546,43 +371,35 @@ class _UsbScreenState extends State { return l10n.usbErrorBusy; case 'usb_not_connected': return l10n.usbErrorNotConnected; - case 'usb_driver_missing': case 'usb_open_failed': + case 'usb_driver_missing': return l10n.usbErrorOpenFailed; case 'usb_connect_failed': - case 'usb_write_failed': - case 'usb_io_error': return l10n.usbErrorConnectFailed; } } - var msg = error.toString(); - if (msg.startsWith('Bad state: ')) { - msg = msg.substring('Bad state: '.length); - } else if (msg.startsWith('Exception: ')) { - msg = msg.substring('Exception: '.length); + if (error is UnsupportedError) { + return l10n.usbErrorUnsupported; } - switch (msg) { - case 'USB serial transport is already active': - return l10n.usbErrorAlreadyActive; - case 'No USB serial device selected': + if (error is StateError) { + final msg = error.message; + if (msg.contains('already active')) return l10n.usbErrorAlreadyActive; + if (msg.contains('No USB serial device selected')) { return l10n.usbErrorNoDeviceSelected; - case 'USB serial port is not open': + } + if (msg.contains('not open') || msg.contains('closed')) { return l10n.usbErrorPortClosed; - case 'USB serial is not supported on this platform.': - case 'Web Serial is not supported by this browser.': - return l10n.usbErrorUnsupported; - case 'Timed out waiting for SELF_INFO during connect': - return l10n.usbErrorConnectTimedOut; + } + if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut; + if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed; } - if (msg.startsWith('Failed to open USB port ')) { - return l10n.usbErrorOpenFailed; + if (error is TimeoutException) { + return l10n.usbErrorConnectTimedOut; } - return msg; + return error.toString(); } - - String _friendlyPortName(String portLabel) => friendlyUsbPortName(portLabel); } diff --git a/lib/services/app_debug_log_service.dart b/lib/services/app_debug_log_service.dart index 6a35b17..c63e625 100644 --- a/lib/services/app_debug_log_service.dart +++ b/lib/services/app_debug_log_service.dart @@ -52,7 +52,12 @@ class AppDebugLogService extends ChangeNotifier { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, }) { - if (!_enabled) return; + if (!_enabled && !kDebugMode) return; + if (!_enabled) { + // In debug mode, still print to console but don't store entries. + debugPrint('[$tag] $message'); + return; + } _entries.add( AppDebugLogEntry( diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index b3df59f..7295376 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,9 +1,11 @@ +import 'dart:io' show Platform, File; import 'dart:ui'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; import '../l10n/app_localizations.dart'; +import '../utils/platform_info.dart'; class NotificationService { static final NotificationService _instance = NotificationService._internal(); @@ -75,6 +77,15 @@ class NotificationService { linux: linuxSettings, ); + // On Linux, the notifications plugin opens a D-Bus session bus + // connection whose async subscription can throw an unhandled + // SocketException when the bus socket is missing (e.g. running as + // root or inside a container without a session bus). + if (PlatformInfo.isLinux && !_isDbusSessionAvailable()) { + debugPrint('Skipping notification init: D-Bus session bus unavailable'); + return; + } + try { await _notifications.initialize( settings: initSettings, @@ -86,6 +97,16 @@ class NotificationService { } } + static bool _isDbusSessionAvailable() { + final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS']; + if (addr != null && addr.isNotEmpty) return true; + // Fallback: check the default socket for the current user. + final uid = Platform.environment['UID'] ?? + Platform.environment['EUID']; + final path = '/run/user/${uid ?? '1000'}/bus'; + return File(path).existsSync(); + } + Future _ensureInitialized() async { if (!_isInitialized) { await initialize(); diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 69ed5b8..fca3d19 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -325,6 +325,10 @@ class UsbSerialService { // Native implementations do not use a synthetic chooser row. } + void setFallbackDeviceName(String label) { + // Native implementations use OS-provided device names. + } + void updateConnectedLabel(String label) { final trimmed = label.trim(); if (trimmed.isEmpty) { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 4953cf5..88aa81d 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -32,6 +32,7 @@ class UsbSerialService { String? _connectedPortName; String? _connectedPortKey; String _requestPortLabel = 'Choose USB Device'; + String _fallbackDeviceName = 'Web Serial Device'; AppDebugLogService? _debugLogService; UsbSerialStatus get status => _status; @@ -77,11 +78,19 @@ class UsbSerialService { try { final requestedPortName = normalizeUsbPortName(portName); + _debugLogService?.info( + 'Web connect: requested=$requestedPortName baud=$baudRate', + tag: 'USB Serial', + ); final selectedPortKey = requestedPortName.startsWith('web:port:') ? requestedPortName : null; _port = _authorizedPortsByKey[requestedPortName]; final authorizedPorts = await _getAuthorizedPorts(); + _debugLogService?.info( + 'Web connect: ${authorizedPorts.length} authorized port(s), cached=${_port != null}', + tag: 'USB Serial', + ); _port ??= _selectPort(authorizedPorts, requestedPortName); _port ??= await _requestPort(); @@ -89,6 +98,10 @@ class UsbSerialService { throw StateError('No USB serial device selected'); } + _debugLogService?.info( + 'Web connect: opening port at $baudRate baud…', + tag: 'USB Serial', + ); await _openPort(_port!, baudRate); _connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey); _connectedPortName = _displayLabelForPort( @@ -105,6 +118,10 @@ class UsbSerialService { tag: 'USB Serial', ); } catch (error) { + _debugLogService?.error( + 'Web connect failed: $error', + tag: 'USB Serial', + ); await _cleanupFailedConnect(); _status = UsbSerialStatus.disconnected; _connectedPortName = null; @@ -194,6 +211,14 @@ class UsbSerialService { _requestPortLabel = trimmed; } + void setFallbackDeviceName(String label) { + final trimmed = label.trim(); + if (trimmed.isEmpty) { + return; + } + _fallbackDeviceName = trimmed; + } + void setDebugLogService(AppDebugLogService? service) { _debugLogService = service; } @@ -403,6 +428,7 @@ class UsbSerialService { vendorId: hasVendor ? vendorId : null, productId: hasProduct ? productId : null, requestPortLabel: _requestPortLabel, + fallbackDeviceName: _fallbackDeviceName, knownUsbNames: _knownUsbNames, ); } diff --git a/lib/utils/usb_port_labels.dart b/lib/utils/usb_port_labels.dart index 1eb8796..05dfc85 100644 --- a/lib/utils/usb_port_labels.dart +++ b/lib/utils/usb_port_labels.dart @@ -31,6 +31,7 @@ String describeWebUsbPort({ required int? vendorId, required int? productId, String requestPortLabel = 'Choose USB Device', + String fallbackDeviceName = 'Web Serial Device', Map knownUsbNames = const {}, }) { if (vendorId == null && productId == null) { @@ -43,7 +44,7 @@ String describeWebUsbPort({ ? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}'] : null; - final parts = [knownName ?? 'Web Serial Device']; + final parts = [knownName ?? fallbackDeviceName]; if (vendorHex != null) { parts.add('VID:$vendorHex'); } diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index f3becea..1f592eb 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -78,40 +78,36 @@ class _SNRIndicatorState extends State { widget.connector.currentSf, ); - return InkWell( - onTap: () { - if (directRepeater != null) { - _showFullPathDialog(context, directBestRepeaters); - } - }, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(snrUi.icon, size: 18, color: snrUi.color), - Text( - snrUi.text, - style: TextStyle(fontSize: 12, color: snrUi.color), - ), - ], - ), - if (directRepeater != null) + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), + child: InkWell( + onTap: directRepeater != null + ? () => _showFullPathDialog(context, directBestRepeaters) + : null, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(snrUi.icon, size: 18, color: snrUi.color), Text( - '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.grey, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + snrUi.text, + style: TextStyle(fontSize: 12, color: snrUi.color), ), - ], + if (directRepeater != null) + Text( + '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ), ); @@ -148,8 +144,10 @@ class _SNRIndicatorState extends State { builder: (context) => AlertDialog( title: Text(l10n.snrIndicator_nearByRepeaters), content: SizedBox( + width: double.maxFinite, child: Scrollbar( child: ListView.separated( + shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 4), itemCount: directBestRepeaters.length, separatorBuilder: (_, _) => const Divider(height: 1), diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 12ecdbe..1d8b504 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -19,6 +19,7 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { final List _ports; String? requestPortLabel; + String? fallbackDeviceName; int connectUsbCalls = 0; String? lastConnectPortName; String? fakeActiveUsbPort; @@ -30,6 +31,9 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { @override MeshCoreConnectionState get state => initialState; + @override + MeshCoreTransportType get activeTransport => MeshCoreTransportType.usb; + @override String? get activeUsbPort => fakeActiveUsbPort; @@ -64,6 +68,11 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { void setUsbRequestPortLabel(String label) { requestPortLabel = label; } + + @override + void setUsbFallbackDeviceName(String label) { + fallbackDeviceName = label; + } } Widget _buildTestApp({ @@ -107,16 +116,23 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + )); await tester.pump(); expect(connector.connectUsbCalls, 0); - expect(find.byType(CircularProgressIndicator), findsNothing); + + // UsbScreen.dispose() schedules disconnect work that debounces notify. + // Drain that debounce timer before test teardown. + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); }, ); testWidgets( - 'UsbScreen keeps raw selection when connector USB display label changes', + 'UsbScreen sends raw port name when tapping Connect', (tester) async { final connector = _FakeMeshCoreConnector( ports: ['COM6 - USB Serial Device (COM6)'], @@ -127,12 +143,10 @@ void main() { ); await tester.pumpAndSettle(); - connector.fakeActiveUsbPortDisplayLabel = - 'COM6 - KD3CGK mesh-utility.org'; - connector.notifyListeners(); - await tester.pump(const Duration(milliseconds: 60)); - - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + )); await tester.pump(); expect(connector.connectUsbCalls, 1); @@ -163,7 +177,8 @@ void main() { }); group('Error Handling', () { - testWidgets('shows error message when listing ports fails', (tester) async { + testWidgets('shows error SnackBar when listing ports fails', + (tester) async { final connector = _FakeMeshCoreConnector(); connector.listUsbPortsImpl = () async { throw PlatformException( @@ -180,7 +195,7 @@ void main() { expect(find.text('USB permission was denied.'), findsOneWidget); }); - testWidgets('connection failure completes without leaving loading state', ( + testWidgets('connection failure shows SnackBar error', ( tester, ) async { final connector = _FakeMeshCoreConnector(ports: ['COM1']); @@ -195,11 +210,17 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + )); await tester.pumpAndSettle(); expect(connectAttempted, isTrue); - expect(find.byType(CircularProgressIndicator), findsNothing); + expect( + find.text('Another USB connection request is already in progress.'), + findsOneWidget, + ); }); }); }