mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-03 15:30:57 +10: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:
@@ -40,15 +40,15 @@ class MeshcoreUsbFunctions(
|
|||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
private var eventSink: EventChannel.EventSink? = null
|
@Volatile private var eventSink: EventChannel.EventSink? = null
|
||||||
private var usbConnection: UsbDeviceConnection? = null
|
@Volatile private var usbConnection: UsbDeviceConnection? = null
|
||||||
private var usbInEndpoint: UsbEndpoint? = null
|
@Volatile private var usbInEndpoint: UsbEndpoint? = null
|
||||||
private var usbOutEndpoint: UsbEndpoint? = null
|
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
|
||||||
private var controlInterface: UsbInterface? = null
|
@Volatile private var controlInterface: UsbInterface? = null
|
||||||
private var dataInterface: UsbInterface? = null
|
@Volatile private var dataInterface: UsbInterface? = null
|
||||||
private var readThread: Thread? = null
|
private var readThread: Thread? = null
|
||||||
@Volatile private var isReading = false
|
@Volatile private var isReading = false
|
||||||
private var connectedDeviceName: String? = null
|
@Volatile private var connectedDeviceName: String? = null
|
||||||
|
|
||||||
private var pendingConnectResult: MethodChannel.Result? = null
|
private var pendingConnectResult: MethodChannel.Result? = null
|
||||||
private var pendingConnectPortName: String? = null
|
private var pendingConnectPortName: String? = null
|
||||||
@@ -86,7 +86,7 @@ class MeshcoreUsbFunctions(
|
|||||||
if (device == null) {
|
if (device == null) {
|
||||||
result.error(
|
result.error(
|
||||||
"usb_device_missing",
|
"usb_device_missing",
|
||||||
"USB device no longer available for $portName",
|
null,
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -95,7 +95,7 @@ class MeshcoreUsbFunctions(
|
|||||||
val granted =
|
val granted =
|
||||||
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||||
if (!granted || !usbManager.hasPermission(device)) {
|
if (!granted || !usbManager.hasPermission(device)) {
|
||||||
result.error("usb_permission_denied", "USB permission denied", null)
|
result.error("usb_permission_denied", null, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,13 +176,13 @@ class MeshcoreUsbFunctions(
|
|||||||
val portName = call.argument<String>("portName")
|
val portName = call.argument<String>("portName")
|
||||||
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
||||||
if (portName.isNullOrBlank()) {
|
if (portName.isNullOrBlank()) {
|
||||||
result.error("usb_invalid_port", "Port name is required", null)
|
result.error("usb_invalid_port", null, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val device = findUsbDevice(portName)
|
val device = findUsbDevice(portName)
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
result.error("usb_device_missing", "USB device not found for $portName", null)
|
result.error("usb_device_missing", null, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ class MeshcoreUsbFunctions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pendingConnectResult != null) {
|
if (pendingConnectResult != null) {
|
||||||
result.error("usb_busy", "Another USB permission request is already pending", null)
|
result.error("usb_busy", null, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,11 +214,11 @@ class MeshcoreUsbFunctions(
|
|||||||
val connection = usbConnection
|
val connection = usbConnection
|
||||||
val endpoint = usbOutEndpoint
|
val endpoint = usbOutEndpoint
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
result.error("usb_invalid_data", "Data is required", null)
|
result.error("usb_invalid_data", null, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (connection == null || endpoint == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ class MeshcoreUsbFunctions(
|
|||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
result.error(
|
result.error(
|
||||||
"usb_driver_missing",
|
"usb_driver_missing",
|
||||||
"No compatible USB serial interface for ${device.deviceName}",
|
null,
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@ class MeshcoreUsbFunctions(
|
|||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
result.error(
|
result.error(
|
||||||
"usb_open_failed",
|
"usb_open_failed",
|
||||||
"UsbManager could not open ${device.deviceName}",
|
null,
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ class MeshcoreUsbFunctions(
|
|||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
result.error(
|
result.error(
|
||||||
"usb_open_failed",
|
"usb_open_failed",
|
||||||
"Could not claim USB data interface for ${device.deviceName}",
|
null,
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -299,20 +299,21 @@ class MeshcoreUsbFunctions(
|
|||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
result.error(
|
result.error(
|
||||||
"usb_open_failed",
|
"usb_open_failed",
|
||||||
"Could not claim USB control interface for ${device.deviceName}",
|
null,
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@execute
|
return@execute
|
||||||
}
|
}
|
||||||
|
|
||||||
configureDevice(connection, config, baudRate)
|
|
||||||
|
|
||||||
usbConnection = connection
|
usbConnection = connection
|
||||||
usbInEndpoint = config.inEndpoint
|
usbInEndpoint = config.inEndpoint
|
||||||
usbOutEndpoint = config.outEndpoint
|
usbOutEndpoint = config.outEndpoint
|
||||||
controlInterface = config.controlInterface
|
controlInterface = config.controlInterface
|
||||||
dataInterface = config.dataInterface
|
dataInterface = config.dataInterface
|
||||||
|
|
||||||
|
configureDevice(connection, config, baudRate)
|
||||||
|
|
||||||
connectedDeviceName = device.deviceName
|
connectedDeviceName = device.deviceName
|
||||||
startReadLoop()
|
startReadLoop()
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import '../services/path_history_service.dart';
|
|||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/background_service.dart';
|
import '../services/background_service.dart';
|
||||||
import '../services/notification_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_message_store.dart';
|
||||||
import '../storage/channel_order_store.dart';
|
import '../storage/channel_order_store.dart';
|
||||||
import '../storage/channel_settings_store.dart';
|
import '../storage/channel_settings_store.dart';
|
||||||
@@ -114,11 +114,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String? _lastDeviceId;
|
String? _lastDeviceId;
|
||||||
String? _lastDeviceDisplayName;
|
String? _lastDeviceDisplayName;
|
||||||
bool _manualDisconnect = false;
|
bool _manualDisconnect = false;
|
||||||
final UsbSerialService _usbSerialService = UsbSerialService();
|
final MeshCoreUsbManager _usbManager = MeshCoreUsbManager();
|
||||||
StreamSubscription<Uint8List>? _usbFrameSubscription;
|
StreamSubscription<Uint8List>? _usbFrameSubscription;
|
||||||
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
|
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
|
||||||
String? _activeUsbPortKey;
|
|
||||||
String? _activeUsbPortLabel;
|
|
||||||
|
|
||||||
final List<ScanResult> _scanResults = [];
|
final List<ScanResult> _scanResults = [];
|
||||||
final List<Contact> _contacts = [];
|
final List<Contact> _contacts = [];
|
||||||
@@ -252,9 +250,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String get deviceIdLabel => _deviceId ?? 'Unknown';
|
String get deviceIdLabel => _deviceId ?? 'Unknown';
|
||||||
|
|
||||||
MeshCoreTransportType get activeTransport => _activeTransport;
|
MeshCoreTransportType get activeTransport => _activeTransport;
|
||||||
String? get activeUsbPort => _activeUsbPortKey;
|
String? get activeUsbPort => _usbManager.activePortKey;
|
||||||
String? get activeUsbPortDisplayLabel =>
|
String? get activeUsbPortDisplayLabel => _usbManager.activePortDisplayLabel;
|
||||||
_activeUsbPortLabel ?? _activeUsbPortKey;
|
|
||||||
bool get isUsbTransportConnected =>
|
bool get isUsbTransportConnected =>
|
||||||
_state == MeshCoreConnectionState.connected &&
|
_state == MeshCoreConnectionState.connected &&
|
||||||
_activeTransport == MeshCoreTransportType.usb;
|
_activeTransport == MeshCoreTransportType.usb;
|
||||||
@@ -661,7 +658,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_bleDebugLogService = bleDebugLogService;
|
_bleDebugLogService = bleDebugLogService;
|
||||||
_appDebugLogService = appDebugLogService;
|
_appDebugLogService = appDebugLogService;
|
||||||
_backgroundService = backgroundService;
|
_backgroundService = backgroundService;
|
||||||
_usbSerialService.setDebugLogService(_appDebugLogService);
|
_usbManager.setDebugLogService(_appDebugLogService);
|
||||||
|
|
||||||
// Initialize notification service
|
// Initialize notification service
|
||||||
_notificationService.initialize();
|
_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) {
|
void setUsbRequestPortLabel(String label) {
|
||||||
_usbSerialService.setRequestPortLabel(label);
|
_usbManager.setRequestPortLabel(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUsbFallbackDeviceName(String label) {
|
||||||
|
_usbManager.setFallbackDeviceName(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> connectUsb({
|
Future<void> connectUsb({
|
||||||
@@ -883,53 +884,70 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}) async {
|
}) async {
|
||||||
if (_state == MeshCoreConnectionState.connecting ||
|
if (_state == MeshCoreConnectionState.connecting ||
|
||||||
_state == MeshCoreConnectionState.connected) {
|
_state == MeshCoreConnectionState.connected) {
|
||||||
|
_appDebugLogService?.warn(
|
||||||
|
'connectUsb ignored: already $_state',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
_appDebugLogService?.info(
|
||||||
_activeUsbPortKey = null;
|
'connectUsb: port=$portName baud=$baudRate',
|
||||||
_activeUsbPortLabel = null;
|
tag: 'USB',
|
||||||
|
);
|
||||||
|
|
||||||
await stopScan();
|
await stopScan();
|
||||||
_cancelReconnectTimer();
|
_cancelReconnectTimer();
|
||||||
_manualDisconnect = false;
|
_manualDisconnect = false;
|
||||||
_resetConnectionHandshakeState();
|
_resetConnectionHandshakeState();
|
||||||
_activeTransport = MeshCoreTransportType.usb;
|
_activeTransport = MeshCoreTransportType.usb;
|
||||||
_activeUsbPortKey = portName;
|
|
||||||
_activeUsbPortLabel = portName;
|
|
||||||
_setState(MeshCoreConnectionState.connecting);
|
_setState(MeshCoreConnectionState.connecting);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _usbFrameSubscription?.cancel();
|
await _usbFrameSubscription?.cancel();
|
||||||
_usbFrameSubscription = null;
|
_usbFrameSubscription = null;
|
||||||
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
|
_appDebugLogService?.info(
|
||||||
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
|
'connectUsb: opening serial port…',
|
||||||
_activeUsbPortLabel =
|
tag: 'USB',
|
||||||
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
|
);
|
||||||
|
await _usbManager.connect(portName: portName, baudRate: baudRate);
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (PlatformInfo.isWeb) {
|
if (PlatformInfo.isWeb) {
|
||||||
await stopScan();
|
await stopScan();
|
||||||
}
|
}
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||||
_usbFrameSubscription = _usbSerialService.frameStream.listen(
|
_usbFrameSubscription = _usbManager.frameStream.listen(
|
||||||
_handleFrame,
|
_handleFrame,
|
||||||
onError: (error, stackTrace) {
|
onError: (error, stackTrace) {
|
||||||
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
|
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
|
||||||
unawaited(disconnect(manual: false));
|
unawaited(disconnect(manual: false));
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
|
_appDebugLogService?.warn('USB frame stream ended', tag: 'USB');
|
||||||
unawaited(disconnect(manual: false));
|
unawaited(disconnect(manual: false));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.connected);
|
_setState(MeshCoreConnectionState.connected);
|
||||||
_pendingInitialChannelSync = true;
|
_pendingInitialChannelSync = true;
|
||||||
|
_appDebugLogService?.info(
|
||||||
|
'connectUsb: requesting device info…',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
await _requestDeviceInfo();
|
await _requestDeviceInfo();
|
||||||
_startBatteryPolling();
|
_startBatteryPolling();
|
||||||
var gotSelfInfo = await _waitForSelfInfo(
|
var gotSelfInfo = await _waitForSelfInfo(
|
||||||
timeout: const Duration(seconds: 3),
|
timeout: const Duration(seconds: 3),
|
||||||
);
|
);
|
||||||
if (!gotSelfInfo) {
|
if (!gotSelfInfo) {
|
||||||
|
_appDebugLogService?.warn(
|
||||||
|
'connectUsb: SELF_INFO timeout, retrying…',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
await refreshDeviceInfo();
|
await refreshDeviceInfo();
|
||||||
gotSelfInfo = await _waitForSelfInfo(
|
gotSelfInfo = await _waitForSelfInfo(
|
||||||
timeout: const Duration(seconds: 3),
|
timeout: const Duration(seconds: 3),
|
||||||
@@ -939,7 +957,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
throw StateError('Timed out waiting for SELF_INFO during connect');
|
throw StateError('Timed out waiting for SELF_INFO during connect');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_appDebugLogService?.info('connectUsb: syncing time…', tag: 'USB');
|
||||||
await syncTime();
|
await syncTime();
|
||||||
|
_appDebugLogService?.info('connectUsb: complete', tag: 'USB');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
|
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
|
||||||
await disconnect(manual: false);
|
await disconnect(manual: false);
|
||||||
@@ -954,8 +974,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||||
_activeUsbPortKey = null;
|
|
||||||
_activeUsbPortLabel = null;
|
|
||||||
|
|
||||||
await stopScan();
|
await stopScan();
|
||||||
_setState(MeshCoreConnectionState.connecting);
|
_setState(MeshCoreConnectionState.connecting);
|
||||||
@@ -1282,7 +1300,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
await _usbFrameSubscription?.cancel();
|
await _usbFrameSubscription?.cancel();
|
||||||
_usbFrameSubscription = null;
|
_usbFrameSubscription = null;
|
||||||
await _usbSerialService.disconnect();
|
await _usbManager.disconnect();
|
||||||
|
|
||||||
await _notifySubscription?.cancel();
|
await _notifySubscription?.cancel();
|
||||||
_notifySubscription = null;
|
_notifySubscription = null;
|
||||||
@@ -1341,8 +1359,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_reactionSendQueueSequence = 0;
|
_reactionSendQueueSequence = 0;
|
||||||
|
|
||||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||||
_activeUsbPortKey = null;
|
|
||||||
_activeUsbPortLabel = null;
|
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.disconnected);
|
_setState(MeshCoreConnectionState.disconnected);
|
||||||
_appDebugLogService?.info(
|
_appDebugLogService?.info(
|
||||||
@@ -1365,7 +1381,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_bleDebugLogService?.logFrame(data, outgoing: true);
|
_bleDebugLogService?.logFrame(data, outgoing: true);
|
||||||
|
|
||||||
if (_activeTransport == MeshCoreTransportType.usb) {
|
if (_activeTransport == MeshCoreTransportType.usb) {
|
||||||
await _usbSerialService.write(data);
|
await _usbManager.write(data);
|
||||||
} else {
|
} else {
|
||||||
if (_rxCharacteristic == null) {
|
if (_rxCharacteristic == null) {
|
||||||
throw Exception("MeshCore RX characteristic not available");
|
throw Exception("MeshCore RX characteristic not available");
|
||||||
@@ -2464,9 +2480,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (_activeTransport == MeshCoreTransportType.usb &&
|
if (_activeTransport == MeshCoreTransportType.usb &&
|
||||||
selfName != null &&
|
selfName != null &&
|
||||||
selfName.isNotEmpty) {
|
selfName.isNotEmpty) {
|
||||||
_usbSerialService.updateConnectedLabel(selfName);
|
_usbManager.updateConnectedLabel(selfName);
|
||||||
_activeUsbPortLabel =
|
|
||||||
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
|
|
||||||
}
|
}
|
||||||
_awaitingSelfInfo = false;
|
_awaitingSelfInfo = false;
|
||||||
_selfInfoRetryTimer?.cancel();
|
_selfInfoRetryTimer?.cancel();
|
||||||
@@ -4246,7 +4260,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_reconnectTimer?.cancel();
|
_reconnectTimer?.cancel();
|
||||||
_batteryPollTimer?.cancel();
|
_batteryPollTimer?.cancel();
|
||||||
_receivedFramesController.close();
|
_receivedFramesController.close();
|
||||||
_usbSerialService.dispose();
|
_usbManager.dispose();
|
||||||
|
|
||||||
// Flush pending unread writes before disposal
|
// Flush pending unread writes before disposal
|
||||||
_unreadStore.flush();
|
_unreadStore.flush();
|
||||||
@@ -4269,6 +4283,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final header = packet.readByte();
|
final header = packet.readByte();
|
||||||
routeType = header & 0x03;
|
routeType = header & 0x03;
|
||||||
payloadType = (header >> 2) & 0x0F;
|
payloadType = (header >> 2) & 0x0F;
|
||||||
|
if (routeType == _routeTransportFlood ||
|
||||||
|
routeType == _routeTransportDirect) {
|
||||||
|
packet.skipBytes(4); // Skip transport-specific bytes
|
||||||
|
}
|
||||||
//final payloadVer = (header >> 6) & 0x03;
|
//final payloadVer = (header >> 6) & 0x03;
|
||||||
final pathLen = packet.readByte();
|
final pathLen = packet.readByte();
|
||||||
pathBytes = packet.readBytes(pathLen);
|
pathBytes = packet.readBytes(pathLen);
|
||||||
@@ -4301,7 +4319,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
packet.skipBytes(1); // Skip SNR byte
|
packet.skipBytes(1); // Skip SNR byte
|
||||||
packet.skipBytes(1); // Skip RSSI byte
|
packet.skipBytes(1); // Skip RSSI byte
|
||||||
final header = packet.readByte();
|
final header = packet.readByte();
|
||||||
|
final routeType = header & 0x03;
|
||||||
payloadType = (header >> 2) & 0x0F;
|
payloadType = (header >> 2) & 0x0F;
|
||||||
|
if (routeType == _routeTransportFlood ||
|
||||||
|
routeType == _routeTransportDirect) {
|
||||||
|
packet.skipBytes(4); // Skip transport-specific bytes
|
||||||
|
}
|
||||||
//final payloadVer = (header >> 6) & 0x03;
|
//final payloadVer = (header >> 6) & 0x03;
|
||||||
final pathLen = packet.readByte();
|
final pathLen = packet.readByte();
|
||||||
pathBytes = packet.readBytes(pathLen);
|
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 {
|
/// Manages USB serial transport for MeshCore devices.
|
||||||
const MeshCoreConnectorUsb(this.connector);
|
///
|
||||||
|
/// 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;
|
// --- Getters ---
|
||||||
MeshCoreTransportType get activeTransport => connector.activeTransport;
|
String? get activePortKey => _activePortKey;
|
||||||
String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel;
|
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||||
bool get isUsbTransportConnected => connector.isUsbTransportConnected;
|
bool get isConnected => _service.isConnected;
|
||||||
|
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||||
|
|
||||||
void addListener(VoidCallback listener) => connector.addListener(listener);
|
// --- Configuration ---
|
||||||
void removeListener(VoidCallback listener) =>
|
Future<List<String>> listPorts() => _service.listPorts();
|
||||||
connector.removeListener(listener);
|
|
||||||
|
|
||||||
Future<List<String>> listPorts() => connector.listUsbPorts();
|
void setRequestPortLabel(String label) =>
|
||||||
|
_service.setRequestPortLabel(label);
|
||||||
|
|
||||||
void setRequestPortLabel(String label) {
|
void setFallbackDeviceName(String label) =>
|
||||||
connector.setUsbRequestPortLabel(label);
|
_service.setFallbackDeviceName(label);
|
||||||
|
|
||||||
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
|
_debugLog = service;
|
||||||
|
_service.setDebugLogService(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> connect({required String portName, int baudRate = 115200}) {
|
// --- Connection lifecycle ---
|
||||||
return connector.connectUsb(portName: portName, baudRate: baudRate);
|
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}) {
|
Future<void> disconnect() async {
|
||||||
return connector.disconnect(manual: manual);
|
_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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
|
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
|
||||||
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
|
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
|
||||||
"usbErrorPortClosed": "USB връзката не е активна.",
|
"usbErrorPortClosed": "USB връзката не е активна.",
|
||||||
"usbErrorConnectTimedOut": "Изчаква се, но устройството не отговаря в рамките на зададения време."
|
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Свързване към USB устройство...",
|
||||||
|
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Изберете USB устройство",
|
||||||
|
"usbStatus_searching": "Търсене на USB устройства...",
|
||||||
|
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1875,5 +1875,17 @@
|
|||||||
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
|
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
|
||||||
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
|
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
|
||||||
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -65,7 +65,19 @@
|
|||||||
"usbErrorAlreadyActive": "A USB connection is already active.",
|
"usbErrorAlreadyActive": "A USB connection is already active.",
|
||||||
"usbErrorNoDeviceSelected": "No USB device was selected.",
|
"usbErrorNoDeviceSelected": "No USB device was selected.",
|
||||||
"usbErrorPortClosed": "The USB connection is not open.",
|
"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_scanning": "Scanning for devices...",
|
||||||
"scanner_connecting": "Connecting...",
|
"scanner_connecting": "Connecting...",
|
||||||
"scanner_disconnecting": "Disconnecting...",
|
"scanner_disconnecting": "Disconnecting...",
|
||||||
|
|||||||
+13
-1
@@ -1875,5 +1875,17 @@
|
|||||||
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
|
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
|
||||||
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
|
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
|
||||||
"usbErrorPortClosed": "La conexión USB no está activa.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
|
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
|
||||||
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
|
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
|
||||||
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
|
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
|
||||||
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
|
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
|
||||||
"usbErrorPortClosed": "La connessione USB non è attiva.",
|
"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.
|
/// No description provided for @usbErrorConnectTimedOut.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// 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;
|
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.
|
/// No description provided for @scanner_scanning.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -175,7 +175,25 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Сканиране за устройства...';
|
String get scanner_scanning => 'Сканиране за устройства...';
|
||||||
|
|||||||
@@ -177,7 +177,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scannen nach Geräten...';
|
String get scanner_scanning => 'Scannen nach Geräten...';
|
||||||
|
|||||||
@@ -174,7 +174,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scanning for devices...';
|
String get scanner_scanning => 'Scanning for devices...';
|
||||||
|
|||||||
@@ -177,7 +177,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||||
|
|||||||
@@ -176,7 +176,25 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Recherche de périphériques...';
|
String get scanner_scanning => 'Recherche de périphériques...';
|
||||||
|
|||||||
@@ -177,7 +177,25 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||||
|
|||||||
@@ -175,7 +175,24 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||||
|
|||||||
@@ -177,7 +177,25 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Skanowanie urządzeń...';
|
String get scanner_scanning => 'Skanowanie urządzeń...';
|
||||||
|
|||||||
@@ -177,7 +177,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||||
|
|||||||
@@ -177,7 +177,25 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Поиск устройств...';
|
String get scanner_scanning => 'Поиск устройств...';
|
||||||
|
|||||||
@@ -177,7 +177,24 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Skrívania zariadení...';
|
String get scanner_scanning => 'Skrívania zariadení...';
|
||||||
|
|||||||
@@ -174,7 +174,25 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Skeniram za naprave...';
|
String get scanner_scanning => 'Skeniram za naprave...';
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbScreenNote =>
|
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
|
@override
|
||||||
String get usbScreenEmptyState =>
|
String get usbScreenEmptyState =>
|
||||||
@@ -175,7 +175,24 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Söker efter enheter...';
|
String get scanner_scanning => 'Söker efter enheter...';
|
||||||
|
|||||||
@@ -175,7 +175,25 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get usbErrorConnectTimedOut =>
|
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
|
@override
|
||||||
String get scanner_scanning => 'Пошук пристроїв...';
|
String get scanner_scanning => 'Пошук пристроїв...';
|
||||||
|
|||||||
@@ -166,7 +166,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get usbErrorPortClosed => 'USB 连接未建立。';
|
String get usbErrorPortClosed => 'USB 连接未建立。';
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String get scanner_scanning => '正在搜索设备...';
|
String get scanner_scanning => '正在搜索设备...';
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
|
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
|
||||||
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
|
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
|
||||||
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "Połączenie USB jest już aktywne.",
|
"usbErrorAlreadyActive": "Połączenie USB jest już aktywne.",
|
||||||
"usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.",
|
"usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.",
|
||||||
"usbErrorPortClosed": "Połączenie USB nie jest aktywne.",
|
"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\"."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
|
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
|
||||||
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
|
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
|
||||||
"usbErrorPortClosed": "A conexão USB não está ativa.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1087,5 +1087,17 @@
|
|||||||
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
|
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
|
||||||
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
|
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
|
||||||
"usbErrorPortClosed": "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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
|
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
|
||||||
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
|
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
|
||||||
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
|
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
|
||||||
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
|
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
|
||||||
"usbErrorPortClosed": "USB povezava ni aktivirana.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -1834,7 +1834,7 @@
|
|||||||
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
|
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
|
||||||
"usbScreenTitle": "Anslut via USB",
|
"usbScreenTitle": "Anslut via USB",
|
||||||
"usbScreenStatus": "Välj en USB-enhet",
|
"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.",
|
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
|
||||||
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
|
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
|
||||||
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
|
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
|
||||||
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
|
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
|
||||||
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
|
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
|
||||||
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1847,5 +1847,17 @@
|
|||||||
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||||
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||||
"usbErrorPortClosed": "З'єднання 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."
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1852,5 +1852,17 @@
|
|||||||
"usbErrorAlreadyActive": "USB 连接已建立。",
|
"usbErrorAlreadyActive": "USB 连接已建立。",
|
||||||
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
|
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
|
||||||
"usbErrorPortClosed": "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 伴侣固件。"
|
||||||
}
|
}
|
||||||
|
|||||||
+221
-404
@@ -1,16 +1,15 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_connector_usb.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../utils/platform_info.dart';
|
import '../utils/platform_info.dart';
|
||||||
import '../utils/usb_port_labels.dart';
|
import '../utils/usb_port_labels.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
import 'scanner_screen.dart';
|
import 'scanner_screen.dart';
|
||||||
|
|
||||||
@@ -24,20 +23,12 @@ class UsbScreen extends StatefulWidget {
|
|||||||
class _UsbScreenState extends State<UsbScreen> {
|
class _UsbScreenState extends State<UsbScreen> {
|
||||||
final List<String> _ports = <String>[];
|
final List<String> _ports = <String>[];
|
||||||
bool _isLoadingPorts = true;
|
bool _isLoadingPorts = true;
|
||||||
bool _isConnecting = false;
|
|
||||||
bool _navigatedToContacts = false;
|
bool _navigatedToContacts = false;
|
||||||
bool _didScheduleInitialLoad = false;
|
bool _didScheduleInitialLoad = false;
|
||||||
String? _selectedPort;
|
|
||||||
String? _connectedPortDisplayLabel;
|
|
||||||
String? _errorText;
|
|
||||||
Timer? _hotPlugTimer;
|
Timer? _hotPlugTimer;
|
||||||
late final MeshCoreConnector _connector;
|
late final MeshCoreConnector _connector;
|
||||||
late final MeshCoreConnectorUsb _usbConnector;
|
|
||||||
late final VoidCallback _connectionListener;
|
late final VoidCallback _connectionListener;
|
||||||
|
|
||||||
/// Whether the current platform supports dynamic hot-plug polling.
|
|
||||||
/// On desktop (macOS, Windows, Linux) we poll continuously so the user
|
|
||||||
/// never needs to hit Refresh manually.
|
|
||||||
bool get _supportsHotPlug =>
|
bool get _supportsHotPlug =>
|
||||||
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
|
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
|
||||||
|
|
||||||
@@ -45,25 +36,13 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_connector = context.read<MeshCoreConnector>();
|
_connector = context.read<MeshCoreConnector>();
|
||||||
_usbConnector = MeshCoreConnectorUsb(_connector);
|
|
||||||
_connectionListener = () {
|
_connectionListener = () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel;
|
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||||
final shouldUpdateDisplayLabel =
|
|
||||||
activeUsbPortDisplayLabel != _connectedPortDisplayLabel;
|
|
||||||
if (_usbConnector.state == MeshCoreConnectionState.disconnected) {
|
|
||||||
_navigatedToContacts = false;
|
_navigatedToContacts = false;
|
||||||
setState(() {
|
|
||||||
_isConnecting = false;
|
|
||||||
_connectedPortDisplayLabel = activeUsbPortDisplayLabel;
|
|
||||||
});
|
|
||||||
} else if (shouldUpdateDisplayLabel) {
|
|
||||||
setState(() {
|
|
||||||
_connectedPortDisplayLabel = activeUsbPortDisplayLabel;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (_usbConnector.state == MeshCoreConnectionState.connected &&
|
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
_usbConnector.isUsbTransportConnected &&
|
_connector.isUsbTransportConnected &&
|
||||||
!_navigatedToContacts) {
|
!_navigatedToContacts) {
|
||||||
_navigatedToContacts = true;
|
_navigatedToContacts = true;
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
@@ -71,14 +50,15 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_usbConnector.addListener(_connectionListener);
|
_connector.addListener(_connectionListener);
|
||||||
_startHotPlugTimer();
|
_startHotPlugTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
_connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName);
|
||||||
if (!_didScheduleInitialLoad) {
|
if (!_didScheduleInitialLoad) {
|
||||||
_didScheduleInitialLoad = true;
|
_didScheduleInitialLoad = true;
|
||||||
unawaited(_loadPorts());
|
unawaited(_loadPorts());
|
||||||
@@ -89,12 +69,12 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_hotPlugTimer?.cancel();
|
_hotPlugTimer?.cancel();
|
||||||
_hotPlugTimer = null;
|
_hotPlugTimer = null;
|
||||||
_usbConnector.removeListener(_connectionListener);
|
_connector.removeListener(_connectionListener);
|
||||||
if (!_navigatedToContacts &&
|
if (!_navigatedToContacts &&
|
||||||
_usbConnector.activeTransport == MeshCoreTransportType.usb &&
|
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||||
_usbConnector.state != MeshCoreConnectionState.disconnected) {
|
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
unawaited(_usbConnector.disconnect(manual: true));
|
unawaited(_connector.disconnect(manual: true));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -102,234 +82,192 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final l10n = context.l10n;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
appLogger.info('Back button pressed', tag: 'UsbScreen');
|
|
||||||
Navigator.of(context).maybePop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
l10n.connectionChoiceUsbLabel,
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
),
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
actions: [
|
|
||||||
if (PlatformInfo.isWeb ||
|
|
||||||
PlatformInfo.isAndroid ||
|
|
||||||
PlatformInfo.isIOS)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
appLogger.info(
|
|
||||||
'Bluetooth selected, opening ScannerScreen',
|
|
||||||
tag: 'UsbScreen',
|
|
||||||
);
|
|
||||||
Navigator.of(context).pushReplacement(
|
|
||||||
MaterialPageRoute(builder: (_) => const ScannerScreen()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.bluetooth),
|
|
||||||
label: Text(l10n.connectionChoiceBluetoothLabel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: LayoutBuilder(
|
top: false,
|
||||||
builder: (context, constraints) {
|
child: Consumer<MeshCoreConnector>(
|
||||||
final availableHeight = constraints.maxHeight.isFinite
|
builder: (context, connector, child) {
|
||||||
? constraints.maxHeight
|
return Column(
|
||||||
: 600.0;
|
children: [
|
||||||
final availableWidth = constraints.maxWidth.isFinite
|
_buildStatusBar(context, connector),
|
||||||
? constraints.maxWidth
|
Expanded(child: _buildPortList(context, connector)),
|
||||||
: 800.0;
|
],
|
||||||
final gap = math.max(8.0, math.min(16.0, availableHeight * 0.025));
|
|
||||||
final iconSize = math.max(
|
|
||||||
28.0,
|
|
||||||
math.min(72.0, availableHeight * 0.12),
|
|
||||||
);
|
|
||||||
final isNarrow = availableWidth < 460.0;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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 =>
|
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||||
!_isLoadingPorts &&
|
final l10n = context.l10n;
|
||||||
!_isConnecting &&
|
String statusText;
|
||||||
_selectedPort != null &&
|
Color statusColor;
|
||||||
_selectedPort!.isNotEmpty;
|
|
||||||
|
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() {
|
void _startHotPlugTimer() {
|
||||||
if (!_supportsHotPlug) return;
|
if (!_supportsHotPlug) return;
|
||||||
@@ -340,9 +278,10 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pollHotPlug() async {
|
Future<void> _pollHotPlug() async {
|
||||||
// Don't interfere with an active connection attempt or initial load.
|
if (_isLoadingPorts) return;
|
||||||
if (_isConnecting || _isLoadingPorts) return;
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
// Don't poll while connecting or connected.
|
||||||
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
try {
|
try {
|
||||||
final ports = await _connector.listUsbPorts();
|
final ports = await _connector.listUsbPorts();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -353,186 +292,72 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
_ports
|
_ports
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(ports);
|
..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 (_) {
|
} catch (_) {
|
||||||
// Silent — hot-plug failures are non-critical.
|
// 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 {
|
Future<void> _loadPorts() async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingPorts = true;
|
_isLoadingPorts = true;
|
||||||
_errorText = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final ports = await _usbConnector.listPorts();
|
final ports = await _connector.listUsbPorts();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_ports
|
_ports
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(ports);
|
..addAll(ports);
|
||||||
if (_ports.isEmpty) {
|
|
||||||
_selectedPort = null;
|
|
||||||
} else if (!_ports.contains(_selectedPort)) {
|
|
||||||
_selectedPort = _ports.first;
|
|
||||||
}
|
|
||||||
_isLoadingPorts = false;
|
_isLoadingPorts = false;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_ports.clear();
|
_ports.clear();
|
||||||
_selectedPort = null;
|
|
||||||
_errorText = _friendlyErrorMessage(error);
|
|
||||||
_isLoadingPorts = false;
|
_isLoadingPorts = false;
|
||||||
});
|
});
|
||||||
|
_showError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _connectSelectedPort() async {
|
Future<void> _connectPort(String port) async {
|
||||||
final selectedPort = _selectedPort;
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
if (selectedPort == null || selectedPort.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
|
|
||||||
if (_usbConnector.state != MeshCoreConnectionState.disconnected) {
|
|
||||||
setState(() {
|
|
||||||
_isConnecting = false;
|
|
||||||
_errorText = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final rawPortName = normalizeUsbPortName(selectedPort);
|
|
||||||
|
|
||||||
setState(() {
|
final rawPortName = normalizeUsbPortName(port);
|
||||||
_isConnecting = true;
|
appLogger.info('Connect tapped for $port (raw: $rawPortName)',
|
||||||
_errorText = null;
|
tag: 'UsbScreen');
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _usbConnector.connect(portName: rawPortName);
|
await _connector.connectUsb(portName: rawPortName);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
appLogger.error(
|
appLogger.error(
|
||||||
'Connect failed for $rawPortName: $error\n$stackTrace',
|
'Connect failed for $rawPortName: $error\n$stackTrace',
|
||||||
tag: 'UsbScreen',
|
tag: 'UsbScreen',
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
_showError(error);
|
||||||
_isConnecting = false;
|
|
||||||
_errorText = _friendlyErrorMessage(error);
|
|
||||||
});
|
|
||||||
// Re-scan so stale or renamed port entries are cleared from the list.
|
|
||||||
unawaited(_loadPorts());
|
unawaited(_loadPorts());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showError(Object error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_friendlyErrorMessage(error)),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _friendlyErrorMessage(Object error) {
|
String _friendlyErrorMessage(Object error) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
|
||||||
if (error is PlatformException) {
|
if (error is PlatformException) {
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case 'usb_permission_denied':
|
case 'usb_permission_denied':
|
||||||
@@ -546,43 +371,35 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
return l10n.usbErrorBusy;
|
return l10n.usbErrorBusy;
|
||||||
case 'usb_not_connected':
|
case 'usb_not_connected':
|
||||||
return l10n.usbErrorNotConnected;
|
return l10n.usbErrorNotConnected;
|
||||||
case 'usb_driver_missing':
|
|
||||||
case 'usb_open_failed':
|
case 'usb_open_failed':
|
||||||
|
case 'usb_driver_missing':
|
||||||
return l10n.usbErrorOpenFailed;
|
return l10n.usbErrorOpenFailed;
|
||||||
case 'usb_connect_failed':
|
case 'usb_connect_failed':
|
||||||
case 'usb_write_failed':
|
|
||||||
case 'usb_io_error':
|
|
||||||
return l10n.usbErrorConnectFailed;
|
return l10n.usbErrorConnectFailed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg = error.toString();
|
if (error is UnsupportedError) {
|
||||||
if (msg.startsWith('Bad state: ')) {
|
return l10n.usbErrorUnsupported;
|
||||||
msg = msg.substring('Bad state: '.length);
|
|
||||||
} else if (msg.startsWith('Exception: ')) {
|
|
||||||
msg = msg.substring('Exception: '.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (msg) {
|
if (error is StateError) {
|
||||||
case 'USB serial transport is already active':
|
final msg = error.message;
|
||||||
return l10n.usbErrorAlreadyActive;
|
if (msg.contains('already active')) return l10n.usbErrorAlreadyActive;
|
||||||
case 'No USB serial device selected':
|
if (msg.contains('No USB serial device selected')) {
|
||||||
return l10n.usbErrorNoDeviceSelected;
|
return l10n.usbErrorNoDeviceSelected;
|
||||||
case 'USB serial port is not open':
|
}
|
||||||
|
if (msg.contains('not open') || msg.contains('closed')) {
|
||||||
return l10n.usbErrorPortClosed;
|
return l10n.usbErrorPortClosed;
|
||||||
case 'USB serial is not supported on this platform.':
|
}
|
||||||
case 'Web Serial is not supported by this browser.':
|
if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut;
|
||||||
return l10n.usbErrorUnsupported;
|
if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed;
|
||||||
case 'Timed out waiting for SELF_INFO during connect':
|
|
||||||
return l10n.usbErrorConnectTimedOut;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.startsWith('Failed to open USB port ')) {
|
if (error is TimeoutException) {
|
||||||
return l10n.usbErrorOpenFailed;
|
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',
|
String tag = 'App',
|
||||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
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(
|
_entries.add(
|
||||||
AppDebugLogEntry(
|
AppDebugLogEntry(
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'dart:io' show Platform, File;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
static final NotificationService _instance = NotificationService._internal();
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
@@ -75,6 +77,15 @@ class NotificationService {
|
|||||||
linux: linuxSettings,
|
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 {
|
try {
|
||||||
await _notifications.initialize(
|
await _notifications.initialize(
|
||||||
settings: initSettings,
|
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 {
|
Future<bool> _ensureInitialized() async {
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
await initialize();
|
await initialize();
|
||||||
|
|||||||
@@ -325,6 +325,10 @@ class UsbSerialService {
|
|||||||
// Native implementations do not use a synthetic chooser row.
|
// Native implementations do not use a synthetic chooser row.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setFallbackDeviceName(String label) {
|
||||||
|
// Native implementations use OS-provided device names.
|
||||||
|
}
|
||||||
|
|
||||||
void updateConnectedLabel(String label) {
|
void updateConnectedLabel(String label) {
|
||||||
final trimmed = label.trim();
|
final trimmed = label.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class UsbSerialService {
|
|||||||
String? _connectedPortName;
|
String? _connectedPortName;
|
||||||
String? _connectedPortKey;
|
String? _connectedPortKey;
|
||||||
String _requestPortLabel = 'Choose USB Device';
|
String _requestPortLabel = 'Choose USB Device';
|
||||||
|
String _fallbackDeviceName = 'Web Serial Device';
|
||||||
AppDebugLogService? _debugLogService;
|
AppDebugLogService? _debugLogService;
|
||||||
|
|
||||||
UsbSerialStatus get status => _status;
|
UsbSerialStatus get status => _status;
|
||||||
@@ -77,11 +78,19 @@ class UsbSerialService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final requestedPortName = normalizeUsbPortName(portName);
|
final requestedPortName = normalizeUsbPortName(portName);
|
||||||
|
_debugLogService?.info(
|
||||||
|
'Web connect: requested=$requestedPortName baud=$baudRate',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
final selectedPortKey = requestedPortName.startsWith('web:port:')
|
final selectedPortKey = requestedPortName.startsWith('web:port:')
|
||||||
? requestedPortName
|
? requestedPortName
|
||||||
: null;
|
: null;
|
||||||
_port = _authorizedPortsByKey[requestedPortName];
|
_port = _authorizedPortsByKey[requestedPortName];
|
||||||
final authorizedPorts = await _getAuthorizedPorts();
|
final authorizedPorts = await _getAuthorizedPorts();
|
||||||
|
_debugLogService?.info(
|
||||||
|
'Web connect: ${authorizedPorts.length} authorized port(s), cached=${_port != null}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
_port ??= _selectPort(authorizedPorts, requestedPortName);
|
_port ??= _selectPort(authorizedPorts, requestedPortName);
|
||||||
|
|
||||||
_port ??= await _requestPort();
|
_port ??= await _requestPort();
|
||||||
@@ -89,6 +98,10 @@ class UsbSerialService {
|
|||||||
throw StateError('No USB serial device selected');
|
throw StateError('No USB serial device selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_debugLogService?.info(
|
||||||
|
'Web connect: opening port at $baudRate baud…',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
await _openPort(_port!, baudRate);
|
await _openPort(_port!, baudRate);
|
||||||
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
|
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
|
||||||
_connectedPortName = _displayLabelForPort(
|
_connectedPortName = _displayLabelForPort(
|
||||||
@@ -105,6 +118,10 @@ class UsbSerialService {
|
|||||||
tag: 'USB Serial',
|
tag: 'USB Serial',
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
_debugLogService?.error(
|
||||||
|
'Web connect failed: $error',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
await _cleanupFailedConnect();
|
await _cleanupFailedConnect();
|
||||||
_status = UsbSerialStatus.disconnected;
|
_status = UsbSerialStatus.disconnected;
|
||||||
_connectedPortName = null;
|
_connectedPortName = null;
|
||||||
@@ -194,6 +211,14 @@ class UsbSerialService {
|
|||||||
_requestPortLabel = trimmed;
|
_requestPortLabel = trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setFallbackDeviceName(String label) {
|
||||||
|
final trimmed = label.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_fallbackDeviceName = trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
void setDebugLogService(AppDebugLogService? service) {
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
_debugLogService = service;
|
_debugLogService = service;
|
||||||
}
|
}
|
||||||
@@ -403,6 +428,7 @@ class UsbSerialService {
|
|||||||
vendorId: hasVendor ? vendorId : null,
|
vendorId: hasVendor ? vendorId : null,
|
||||||
productId: hasProduct ? productId : null,
|
productId: hasProduct ? productId : null,
|
||||||
requestPortLabel: _requestPortLabel,
|
requestPortLabel: _requestPortLabel,
|
||||||
|
fallbackDeviceName: _fallbackDeviceName,
|
||||||
knownUsbNames: _knownUsbNames,
|
knownUsbNames: _knownUsbNames,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ String describeWebUsbPort({
|
|||||||
required int? vendorId,
|
required int? vendorId,
|
||||||
required int? productId,
|
required int? productId,
|
||||||
String requestPortLabel = 'Choose USB Device',
|
String requestPortLabel = 'Choose USB Device',
|
||||||
|
String fallbackDeviceName = 'Web Serial Device',
|
||||||
Map<String, String> knownUsbNames = const <String, String>{},
|
Map<String, String> knownUsbNames = const <String, String>{},
|
||||||
}) {
|
}) {
|
||||||
if (vendorId == null && productId == null) {
|
if (vendorId == null && productId == null) {
|
||||||
@@ -43,7 +44,7 @@ String describeWebUsbPort({
|
|||||||
? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}']
|
? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}']
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final parts = <String>[knownName ?? 'Web Serial Device'];
|
final parts = <String>[knownName ?? fallbackDeviceName];
|
||||||
if (vendorHex != null) {
|
if (vendorHex != null) {
|
||||||
parts.add('VID:$vendorHex');
|
parts.add('VID:$vendorHex');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,40 +78,36 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||||||
widget.connector.currentSf,
|
widget.connector.currentSf,
|
||||||
);
|
);
|
||||||
|
|
||||||
return InkWell(
|
return ConstrainedBox(
|
||||||
onTap: () {
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||||
if (directRepeater != null) {
|
child: InkWell(
|
||||||
_showFullPathDialog(context, directBestRepeaters);
|
onTap: directRepeater != null
|
||||||
}
|
? () => _showFullPathDialog(context, directBestRepeaters)
|
||||||
},
|
: null,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Icon(snrUi.icon, size: 18, color: snrUi.color),
|
||||||
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)
|
|
||||||
Text(
|
Text(
|
||||||
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
|
snrUi.text,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, color: snrUi.color),
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
],
|
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(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(l10n.snrIndicator_nearByRepeaters),
|
title: Text(l10n.snrIndicator_nearByRepeaters),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
itemCount: directBestRepeaters.length,
|
itemCount: directBestRepeaters.length,
|
||||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
|
|||||||
final List<String> _ports;
|
final List<String> _ports;
|
||||||
|
|
||||||
String? requestPortLabel;
|
String? requestPortLabel;
|
||||||
|
String? fallbackDeviceName;
|
||||||
int connectUsbCalls = 0;
|
int connectUsbCalls = 0;
|
||||||
String? lastConnectPortName;
|
String? lastConnectPortName;
|
||||||
String? fakeActiveUsbPort;
|
String? fakeActiveUsbPort;
|
||||||
@@ -30,6 +31,9 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
|
|||||||
@override
|
@override
|
||||||
MeshCoreConnectionState get state => initialState;
|
MeshCoreConnectionState get state => initialState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MeshCoreTransportType get activeTransport => MeshCoreTransportType.usb;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get activeUsbPort => fakeActiveUsbPort;
|
String? get activeUsbPort => fakeActiveUsbPort;
|
||||||
|
|
||||||
@@ -64,6 +68,11 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
|
|||||||
void setUsbRequestPortLabel(String label) {
|
void setUsbRequestPortLabel(String label) {
|
||||||
requestPortLabel = label;
|
requestPortLabel = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setUsbFallbackDeviceName(String label) {
|
||||||
|
fallbackDeviceName = label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTestApp({
|
Widget _buildTestApp({
|
||||||
@@ -107,16 +116,23 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
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();
|
await tester.pump();
|
||||||
|
|
||||||
expect(connector.connectUsbCalls, 0);
|
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(
|
testWidgets(
|
||||||
'UsbScreen keeps raw selection when connector USB display label changes',
|
'UsbScreen sends raw port name when tapping Connect',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
final connector = _FakeMeshCoreConnector(
|
final connector = _FakeMeshCoreConnector(
|
||||||
ports: <String>['COM6 - USB Serial Device (COM6)'],
|
ports: <String>['COM6 - USB Serial Device (COM6)'],
|
||||||
@@ -127,12 +143,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
connector.fakeActiveUsbPortDisplayLabel =
|
await tester.tap(find.ancestor(
|
||||||
'COM6 - KD3CGK mesh-utility.org';
|
of: find.text('Connect'),
|
||||||
connector.notifyListeners();
|
matching: find.bySubtype<ElevatedButton>(),
|
||||||
await tester.pump(const Duration(milliseconds: 60));
|
));
|
||||||
|
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Connect'));
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(connector.connectUsbCalls, 1);
|
expect(connector.connectUsbCalls, 1);
|
||||||
@@ -163,7 +177,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('Error Handling', () {
|
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();
|
final connector = _FakeMeshCoreConnector();
|
||||||
connector.listUsbPortsImpl = () async {
|
connector.listUsbPortsImpl = () async {
|
||||||
throw PlatformException(
|
throw PlatformException(
|
||||||
@@ -180,7 +195,7 @@ void main() {
|
|||||||
expect(find.text('USB permission was denied.'), findsOneWidget);
|
expect(find.text('USB permission was denied.'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('connection failure completes without leaving loading state', (
|
testWidgets('connection failure shows SnackBar error', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
|
final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
|
||||||
@@ -195,11 +210,17 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
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();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(connectAttempted, isTrue);
|
expect(connectAttempted, isTrue);
|
||||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
expect(
|
||||||
|
find.text('Another USB connection request is already in progress.'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user