mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Refactor USB screen, add debug logging, fix UI issues
- Rewrite UsbScreen to mirror ScannerScreen patterns (status bar, tap-to-connect port list, bottom FABs, SnackBar errors) - Extract MeshCoreUsbManager from MeshCoreConnector for cleaner USB transport ownership - Add debug logging throughout USB connection flow (connector, manager, web/native services) - Print debug logs to console in debug mode even when app debug log setting is disabled - Localize remaining hardcoded strings (Web Serial Device fallback label, USB status bar keys, companion firmware timeout hint) - Fix Swedish misspelling in translations (stöderliga → stödda) - Guard Linux notification init against missing D-Bus session bus - Fix SNRIndicator hit-test error by adding minimum size constraints - Update USB flow tests for new UI patterns
This commit is contained in:
parent
8238b6197f
commit
fef73b7b62
42 changed files with 981 additions and 553 deletions
|
|
@ -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<String>("portName")
|
||||
val baudRate = call.argument<Int>("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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Uint8List>? _usbFrameSubscription;
|
||||
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
|
||||
String? _activeUsbPortKey;
|
||||
String? _activeUsbPortLabel;
|
||||
|
||||
final List<ScanResult> _scanResults = [];
|
||||
final List<Contact> _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<List<String>> listUsbPorts() => _usbSerialService.listPorts();
|
||||
Future<List<String>> listUsbPorts() => _usbManager.listPorts();
|
||||
|
||||
void setUsbRequestPortLabel(String label) {
|
||||
_usbSerialService.setRequestPortLabel(label);
|
||||
_usbManager.setRequestPortLabel(label);
|
||||
}
|
||||
|
||||
void setUsbFallbackDeviceName(String label) {
|
||||
_usbManager.setFallbackDeviceName(label);
|
||||
}
|
||||
|
||||
Future<void> 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<void>.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);
|
||||
|
|
|
|||
|
|
@ -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<Uint8List> get frameStream => _service.frameStream;
|
||||
|
||||
void addListener(VoidCallback listener) => connector.addListener(listener);
|
||||
void removeListener(VoidCallback listener) =>
|
||||
connector.removeListener(listener);
|
||||
// --- Configuration ---
|
||||
Future<List<String>> listPorts() => _service.listPorts();
|
||||
|
||||
Future<List<String>> 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<void> connect({required String portName, int baudRate = 115200}) {
|
||||
return connector.connectUsb(portName: portName, baudRate: baudRate);
|
||||
// --- Connection lifecycle ---
|
||||
Future<void> 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<void> disconnect({bool manual = true}) {
|
||||
return connector.disconnect(manual: manual);
|
||||
Future<void> disconnect() async {
|
||||
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
|
||||
await _service.disconnect();
|
||||
_activePortKey = null;
|
||||
_activePortLabel = null;
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) => _service.write(data);
|
||||
|
||||
// --- Label management ---
|
||||
void updateConnectedLabel(String selfName) {
|
||||
_service.updateConnectedLabel(selfName);
|
||||
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_service.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 връзка."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 => 'Сканиране за устройства...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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ń...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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 => 'Поиск устройств...';
|
||||
|
|
|
|||
|
|
@ -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í...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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 => 'Пошук пристроїв...';
|
||||
|
|
|
|||
|
|
@ -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 => '正在搜索设备...';
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\"."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 伴侣固件。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UsbScreen> {
|
||||
final List<String> _ports = <String>[];
|
||||
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<UsbScreen> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
_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<UsbScreen> {
|
|||
);
|
||||
}
|
||||
};
|
||||
_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<UsbScreen> {
|
|||
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<UsbScreen> {
|
|||
|
||||
@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<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildStatusBar(context, connector),
|
||||
Expanded(child: _buildPortList(context, connector)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
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<UsbScreen> {
|
|||
}
|
||||
|
||||
Future<void> _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<UsbScreen> {
|
|||
_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<void> _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<void> _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<void> _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<UsbScreen> {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<bool> _ensureInitialized() async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ String describeWebUsbPort({
|
|||
required int? vendorId,
|
||||
required int? productId,
|
||||
String requestPortLabel = 'Choose USB Device',
|
||||
String fallbackDeviceName = 'Web Serial Device',
|
||||
Map<String, String> knownUsbNames = const <String, String>{},
|
||||
}) {
|
||||
if (vendorId == null && productId == null) {
|
||||
|
|
@ -43,7 +44,7 @@ String describeWebUsbPort({
|
|||
? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}']
|
||||
: null;
|
||||
|
||||
final parts = <String>[knownName ?? 'Web Serial Device'];
|
||||
final parts = <String>[knownName ?? fallbackDeviceName];
|
||||
if (vendorHex != null) {
|
||||
parts.add('VID:$vendorHex');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,40 +78,36 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||
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<SNRIndicator> {
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
|
|||
final List<String> _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<ElevatedButton>(),
|
||||
));
|
||||
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: <String>['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<ElevatedButton>(),
|
||||
));
|
||||
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: <String>['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<ElevatedButton>(),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(connectAttempted, isTrue);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(
|
||||
find.text('Another USB connection request is already in progress.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue