mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Refine USB transport flow
- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
This commit is contained in:
committed by
just-stuff-tm
parent
74da9e82b5
commit
44c0670dae
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
android {
|
||||
namespace = "com.meshcore.meshcore_open"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "29.0.14206865"
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -84,5 +84,4 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
|
||||
}
|
||||
|
||||
@@ -1,408 +1,18 @@
|
||||
package com.meshcore.meshcore_open
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
|
||||
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
|
||||
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
|
||||
|
||||
private lateinit var usbManager: UsbManager
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var usbConnection: UsbDeviceConnection? = null
|
||||
private var usbPort: UsbSerialPort? = null
|
||||
private var ioManager: SerialInputOutputManager? = null
|
||||
private var connectedDeviceName: String? = null
|
||||
|
||||
private var pendingConnectResult: MethodChannel.Result? = null
|
||||
private var pendingConnectPortName: String? = null
|
||||
private var pendingConnectBaudRate: Int = 115200
|
||||
|
||||
private val permissionReceiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||
handleUsbDetached(intent)
|
||||
return
|
||||
}
|
||||
usbPermissionAction -> {
|
||||
}
|
||||
else -> {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (intent.action != usbPermissionAction) {
|
||||
return
|
||||
}
|
||||
|
||||
val result = pendingConnectResult
|
||||
val portName = pendingConnectPortName
|
||||
pendingConnectResult = null
|
||||
pendingConnectPortName = null
|
||||
|
||||
if (result == null || portName == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val granted =
|
||||
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||
if (!granted) {
|
||||
result.error("usb_permission_denied", "USB permission denied", null)
|
||||
return
|
||||
}
|
||||
|
||||
val device = findUsbDevice(portName)
|
||||
if (device == null) {
|
||||
result.error(
|
||||
"usb_device_missing",
|
||||
"USB device no longer available for $portName",
|
||||
null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
openUsbDevice(device, pendingConnectBaudRate, result)
|
||||
}
|
||||
}
|
||||
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
registerUsbPermissionReceiver()
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"listPorts" -> result.success(listUsbPorts())
|
||||
"connect" -> handleUsbConnect(call, result)
|
||||
"write" -> handleUsbWrite(call, result)
|
||||
"disconnect" -> {
|
||||
scheduleCloseUsbConnection {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
|
||||
.setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
}
|
||||
},
|
||||
)
|
||||
usbFunctions.configureFlutterEngine(flutterEngine)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
closeUsbConnection()
|
||||
usbIoExecutor.shutdownNow()
|
||||
try {
|
||||
unregisterReceiver(permissionReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
usbFunctions.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun registerUsbPermissionReceiver() {
|
||||
val filter =
|
||||
IntentFilter().apply {
|
||||
addAction(usbPermissionAction)
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
registerReceiver(permissionReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listUsbPorts(): List<String> {
|
||||
val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
|
||||
return drivers.map { driver ->
|
||||
val device = driver.device
|
||||
val productName = device.productName ?: "USB Serial Device"
|
||||
val vendorProduct =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"VID:%04X PID:%04X",
|
||||
device.vendorId,
|
||||
device.productId,
|
||||
)
|
||||
"${device.deviceName} - $productName - $vendorProduct"
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
|
||||
val portName = call.argument<String>("portName")
|
||||
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
||||
if (portName.isNullOrBlank()) {
|
||||
result.error("usb_invalid_port", "Port name is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
val device = findUsbDevice(portName)
|
||||
if (device == null) {
|
||||
result.error("usb_device_missing", "USB device not found for $portName", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (usbManager.hasPermission(device)) {
|
||||
openUsbDevice(device, baudRate, result)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingConnectResult != null) {
|
||||
result.error("usb_busy", "Another USB permission request is already pending", null)
|
||||
return
|
||||
}
|
||||
|
||||
pendingConnectResult = result
|
||||
pendingConnectPortName = portName
|
||||
pendingConnectBaudRate = baudRate
|
||||
|
||||
val permissionIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(usbPermissionAction).setPackage(packageName),
|
||||
pendingIntentFlags(),
|
||||
)
|
||||
usbManager.requestPermission(device, permissionIntent)
|
||||
}
|
||||
|
||||
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
|
||||
val data = call.argument<ByteArray>("data")
|
||||
val port = usbPort
|
||||
if (data == null) {
|
||||
result.error("usb_invalid_data", "Data is required", null)
|
||||
return
|
||||
}
|
||||
if (port == null) {
|
||||
result.error("usb_not_connected", "USB serial port is not connected", null)
|
||||
return
|
||||
}
|
||||
|
||||
usbIoExecutor.execute {
|
||||
try {
|
||||
port.write(data, 1000)
|
||||
mainHandler.post {
|
||||
result.success(null)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
mainHandler.post {
|
||||
result.error("usb_write_failed", error.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findUsbDevice(portName: String): UsbDevice? {
|
||||
return usbManager.deviceList.values.firstOrNull { it.deviceName == portName }
|
||||
}
|
||||
|
||||
private fun openUsbDevice(
|
||||
device: UsbDevice,
|
||||
baudRate: Int,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
usbIoExecutor.execute {
|
||||
try {
|
||||
closeUsbConnection()
|
||||
|
||||
val driver = UsbSerialProber.getDefaultProber().probeDevice(device)
|
||||
if (driver == null) {
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_driver_missing",
|
||||
"No USB serial driver for ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
val connection = usbManager.openDevice(device)
|
||||
if (connection == null) {
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_open_failed",
|
||||
"UsbManager could not open ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
val port = firstPort(driver)
|
||||
if (port == null) {
|
||||
connection.close()
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_port_missing",
|
||||
"No USB serial port exposed by ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
port.open(connection)
|
||||
port.setParameters(
|
||||
baudRate,
|
||||
8,
|
||||
UsbSerialPort.STOPBITS_1,
|
||||
UsbSerialPort.PARITY_NONE,
|
||||
)
|
||||
port.rts = false
|
||||
port.dtr = true
|
||||
|
||||
usbConnection = connection
|
||||
usbPort = port
|
||||
connectedDeviceName = device.deviceName
|
||||
|
||||
ioManager =
|
||||
SerialInputOutputManager(
|
||||
port,
|
||||
object : SerialInputOutputManager.Listener {
|
||||
override fun onNewData(data: ByteArray) {
|
||||
mainHandler.post {
|
||||
eventSink?.success(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRunError(e: Exception) {
|
||||
mainHandler.post {
|
||||
eventSink?.error(
|
||||
"usb_io_error",
|
||||
e.message ?: "USB serial I/O error",
|
||||
null,
|
||||
)
|
||||
}
|
||||
scheduleCloseUsbConnection()
|
||||
}
|
||||
},
|
||||
).also { manager ->
|
||||
manager.start()
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
result.success(null)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
closeUsbConnection()
|
||||
mainHandler.post {
|
||||
result.error("usb_connect_failed", error.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstPort(driver: UsbSerialDriver): UsbSerialPort? {
|
||||
return driver.ports.firstOrNull()
|
||||
}
|
||||
|
||||
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
|
||||
usbIoExecutor.execute {
|
||||
closeUsbConnection()
|
||||
if (onComplete != null) {
|
||||
mainHandler.post(onComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun closeUsbConnection() {
|
||||
try {
|
||||
ioManager?.stop()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
ioManager = null
|
||||
|
||||
try {
|
||||
usbPort?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
usbPort = null
|
||||
|
||||
try {
|
||||
usbConnection?.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
usbConnection = null
|
||||
connectedDeviceName = null
|
||||
}
|
||||
|
||||
private fun handleUsbDetached(intent: Intent) {
|
||||
val detachedDevice =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
||||
}
|
||||
|
||||
val detachedName = detachedDevice?.deviceName ?: return
|
||||
|
||||
if (pendingConnectPortName == detachedName) {
|
||||
pendingConnectResult?.error(
|
||||
"usb_device_detached",
|
||||
"USB device was removed before the connection completed",
|
||||
null,
|
||||
)
|
||||
pendingConnectResult = null
|
||||
pendingConnectPortName = null
|
||||
}
|
||||
|
||||
if (connectedDeviceName == detachedName) {
|
||||
scheduleCloseUsbConnection {
|
||||
eventSink?.error(
|
||||
"usb_device_detached",
|
||||
"USB device was disconnected",
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pendingIntentFlags(): Int {
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
return flags
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
package com.meshcore.meshcore_open
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbConstants
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbInterface
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class MeshcoreUsbFunctions(
|
||||
private val activity: FlutterActivity,
|
||||
) {
|
||||
private companion object {
|
||||
const val usbRecipientInterface = 0x01
|
||||
}
|
||||
|
||||
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
|
||||
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
|
||||
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
|
||||
|
||||
private val usbManager by lazy {
|
||||
activity.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
}
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var usbConnection: UsbDeviceConnection? = null
|
||||
private var usbInEndpoint: UsbEndpoint? = null
|
||||
private var usbOutEndpoint: UsbEndpoint? = null
|
||||
private var controlInterface: UsbInterface? = null
|
||||
private var dataInterface: UsbInterface? = null
|
||||
private var readThread: Thread? = null
|
||||
@Volatile private var isReading = false
|
||||
private var connectedDeviceName: String? = null
|
||||
|
||||
private var pendingConnectResult: MethodChannel.Result? = null
|
||||
private var pendingConnectPortName: String? = null
|
||||
private var pendingConnectBaudRate: Int = 115200
|
||||
|
||||
private data class PortConfig(
|
||||
val controlInterface: UsbInterface?,
|
||||
val dataInterface: UsbInterface,
|
||||
val inEndpoint: UsbEndpoint,
|
||||
val outEndpoint: UsbEndpoint,
|
||||
)
|
||||
|
||||
private val permissionReceiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||
handleUsbDetached(intent)
|
||||
return
|
||||
}
|
||||
usbPermissionAction -> Unit
|
||||
else -> return
|
||||
}
|
||||
|
||||
val result = pendingConnectResult
|
||||
val portName = pendingConnectPortName
|
||||
pendingConnectResult = null
|
||||
pendingConnectPortName = null
|
||||
|
||||
if (result == null || portName == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val granted =
|
||||
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||
if (!granted) {
|
||||
result.error("usb_permission_denied", "USB permission denied", null)
|
||||
return
|
||||
}
|
||||
|
||||
val device = findUsbDevice(portName)
|
||||
if (device == null) {
|
||||
result.error(
|
||||
"usb_device_missing",
|
||||
"USB device no longer available for $portName",
|
||||
null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
openUsbDevice(device, pendingConnectBaudRate, result)
|
||||
}
|
||||
}
|
||||
|
||||
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
registerUsbPermissionReceiver()
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"listPorts" -> result.success(listUsbPorts())
|
||||
"connect" -> handleUsbConnect(call, result)
|
||||
"write" -> handleUsbWrite(call, result)
|
||||
"disconnect" -> {
|
||||
scheduleCloseUsbConnection {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
|
||||
.setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
closeUsbConnection()
|
||||
usbIoExecutor.shutdownNow()
|
||||
try {
|
||||
activity.unregisterReceiver(permissionReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerUsbPermissionReceiver() {
|
||||
val filter =
|
||||
IntentFilter().apply {
|
||||
addAction(usbPermissionAction)
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity.registerReceiver(permissionReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listUsbPorts(): List<String> {
|
||||
return usbManager.deviceList.values.map { device ->
|
||||
val productName = device.productName ?: "USB Serial Device"
|
||||
val vendorProduct =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"VID:%04X PID:%04X",
|
||||
device.vendorId,
|
||||
device.productId,
|
||||
)
|
||||
"${device.deviceName} - $productName - $vendorProduct"
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
|
||||
val portName = call.argument<String>("portName")
|
||||
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
||||
if (portName.isNullOrBlank()) {
|
||||
result.error("usb_invalid_port", "Port name is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
val device = findUsbDevice(portName)
|
||||
if (device == null) {
|
||||
result.error("usb_device_missing", "USB device not found for $portName", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (usbManager.hasPermission(device)) {
|
||||
openUsbDevice(device, baudRate, result)
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingConnectResult != null) {
|
||||
result.error("usb_busy", "Another USB permission request is already pending", null)
|
||||
return
|
||||
}
|
||||
|
||||
pendingConnectResult = result
|
||||
pendingConnectPortName = portName
|
||||
pendingConnectBaudRate = baudRate
|
||||
|
||||
val permissionIntent = PendingIntent.getBroadcast(
|
||||
activity,
|
||||
0,
|
||||
Intent(usbPermissionAction).setPackage(activity.packageName),
|
||||
pendingIntentFlags(),
|
||||
)
|
||||
usbManager.requestPermission(device, permissionIntent)
|
||||
}
|
||||
|
||||
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
|
||||
val data = call.argument<ByteArray>("data")
|
||||
val connection = usbConnection
|
||||
val endpoint = usbOutEndpoint
|
||||
if (data == null) {
|
||||
result.error("usb_invalid_data", "Data is required", null)
|
||||
return
|
||||
}
|
||||
if (connection == null || endpoint == null) {
|
||||
result.error("usb_not_connected", "USB serial port is not connected", null)
|
||||
return
|
||||
}
|
||||
|
||||
usbIoExecutor.execute {
|
||||
try {
|
||||
writeToDevice(data)
|
||||
mainHandler.post { result.success(null) }
|
||||
} catch (error: Exception) {
|
||||
mainHandler.post {
|
||||
result.error("usb_write_failed", error.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findUsbDevice(portName: String): UsbDevice? {
|
||||
return usbManager.deviceList.values.firstOrNull { it.deviceName == portName }
|
||||
}
|
||||
|
||||
private fun openUsbDevice(
|
||||
device: UsbDevice,
|
||||
baudRate: Int,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
usbIoExecutor.execute {
|
||||
try {
|
||||
closeUsbConnection()
|
||||
|
||||
val config = resolvePortConfig(device)
|
||||
if (config == null) {
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_driver_missing",
|
||||
"No compatible USB serial interface for ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
val connection = usbManager.openDevice(device)
|
||||
if (connection == null) {
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_open_failed",
|
||||
"UsbManager could not open ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
if (!connection.claimInterface(config.dataInterface, true)) {
|
||||
connection.close()
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_open_failed",
|
||||
"Could not claim USB data interface for ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
if (config.controlInterface != null &&
|
||||
config.controlInterface.id != config.dataInterface.id &&
|
||||
!connection.claimInterface(config.controlInterface, true)
|
||||
) {
|
||||
connection.releaseInterface(config.dataInterface)
|
||||
connection.close()
|
||||
mainHandler.post {
|
||||
result.error(
|
||||
"usb_open_failed",
|
||||
"Could not claim USB control interface for ${device.deviceName}",
|
||||
null,
|
||||
)
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
configureDevice(connection, config, baudRate)
|
||||
|
||||
usbConnection = connection
|
||||
usbInEndpoint = config.inEndpoint
|
||||
usbOutEndpoint = config.outEndpoint
|
||||
controlInterface = config.controlInterface
|
||||
dataInterface = config.dataInterface
|
||||
connectedDeviceName = device.deviceName
|
||||
startReadLoop()
|
||||
|
||||
mainHandler.post {
|
||||
result.success(null)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
closeUsbConnection()
|
||||
mainHandler.post {
|
||||
result.error("usb_connect_failed", error.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
|
||||
var preferredDataInterface: UsbInterface? = null
|
||||
var preferredInEndpoint: UsbEndpoint? = null
|
||||
var preferredOutEndpoint: UsbEndpoint? = null
|
||||
var fallbackDataInterface: UsbInterface? = null
|
||||
var fallbackInEndpoint: UsbEndpoint? = null
|
||||
var fallbackOutEndpoint: UsbEndpoint? = null
|
||||
var preferredControlInterface: UsbInterface? = null
|
||||
|
||||
for (interfaceIndex in 0 until device.interfaceCount) {
|
||||
val usbInterface = device.getInterface(interfaceIndex)
|
||||
var inEndpoint: UsbEndpoint? = null
|
||||
var outEndpoint: UsbEndpoint? = null
|
||||
|
||||
for (endpointIndex in 0 until usbInterface.endpointCount) {
|
||||
val endpoint = usbInterface.getEndpoint(endpointIndex)
|
||||
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
|
||||
continue
|
||||
}
|
||||
when (endpoint.direction) {
|
||||
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
|
||||
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
val hasDataPair = inEndpoint != null && outEndpoint != null
|
||||
when {
|
||||
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
|
||||
preferredControlInterface == null -> {
|
||||
preferredControlInterface = usbInterface
|
||||
}
|
||||
hasDataPair &&
|
||||
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
|
||||
preferredDataInterface = usbInterface
|
||||
preferredInEndpoint = inEndpoint
|
||||
preferredOutEndpoint = outEndpoint
|
||||
}
|
||||
hasDataPair && fallbackDataInterface == null -> {
|
||||
fallbackDataInterface = usbInterface
|
||||
fallbackInEndpoint = inEndpoint
|
||||
fallbackOutEndpoint = outEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
|
||||
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
|
||||
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
|
||||
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
|
||||
}
|
||||
|
||||
private fun configureDevice(
|
||||
connection: UsbDeviceConnection,
|
||||
config: PortConfig,
|
||||
baudRate: Int,
|
||||
) {
|
||||
val control = config.controlInterface ?: return
|
||||
val lineCoding =
|
||||
byteArrayOf(
|
||||
(baudRate and 0xFF).toByte(),
|
||||
((baudRate shr 8) and 0xFF).toByte(),
|
||||
((baudRate shr 16) and 0xFF).toByte(),
|
||||
((baudRate shr 24) and 0xFF).toByte(),
|
||||
0, // stop bits: 1
|
||||
0, // parity: none
|
||||
8, // data bits
|
||||
)
|
||||
|
||||
val lineCodingResult =
|
||||
connection.controlTransfer(
|
||||
UsbConstants.USB_DIR_OUT or
|
||||
UsbConstants.USB_TYPE_CLASS or
|
||||
usbRecipientInterface,
|
||||
0x20,
|
||||
0,
|
||||
control.id,
|
||||
lineCoding,
|
||||
lineCoding.size,
|
||||
1000,
|
||||
)
|
||||
if (lineCodingResult < 0) {
|
||||
throw IllegalStateException("Failed to configure USB line coding")
|
||||
}
|
||||
|
||||
val controlLineResult =
|
||||
connection.controlTransfer(
|
||||
UsbConstants.USB_DIR_OUT or
|
||||
UsbConstants.USB_TYPE_CLASS or
|
||||
usbRecipientInterface,
|
||||
0x22,
|
||||
0x0001, // DTR on, RTS off
|
||||
control.id,
|
||||
null,
|
||||
0,
|
||||
1000,
|
||||
)
|
||||
if (controlLineResult < 0) {
|
||||
throw IllegalStateException("Failed to configure USB control line state")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startReadLoop() {
|
||||
val connection = usbConnection ?: return
|
||||
val endpoint = usbInEndpoint ?: return
|
||||
|
||||
isReading = true
|
||||
readThread =
|
||||
Thread({
|
||||
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
|
||||
val buffer = ByteArray(packetSize * 4)
|
||||
try {
|
||||
while (isReading) {
|
||||
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
|
||||
if (!isReading) {
|
||||
break
|
||||
}
|
||||
if (bytesRead <= 0) {
|
||||
continue
|
||||
}
|
||||
val packet = buffer.copyOf(bytesRead)
|
||||
mainHandler.post {
|
||||
eventSink?.success(packet)
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (isReading) {
|
||||
mainHandler.post {
|
||||
eventSink?.error(
|
||||
"usb_io_error",
|
||||
error.message ?: "USB serial I/O error",
|
||||
null,
|
||||
)
|
||||
}
|
||||
scheduleCloseUsbConnection()
|
||||
}
|
||||
}
|
||||
}, "MeshCoreUsbRead").also { thread ->
|
||||
thread.isDaemon = true
|
||||
thread.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeToDevice(data: ByteArray) {
|
||||
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
|
||||
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
|
||||
var offset = 0
|
||||
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
|
||||
while (offset < data.size) {
|
||||
val chunkSize = minOf(maxPacketSize, data.size - offset)
|
||||
val chunk = data.copyOfRange(offset, offset + chunkSize)
|
||||
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
|
||||
if (bytesWritten != chunkSize) {
|
||||
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
|
||||
}
|
||||
offset += chunkSize
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
|
||||
usbIoExecutor.execute {
|
||||
closeUsbConnection()
|
||||
if (onComplete != null) {
|
||||
mainHandler.post(onComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun closeUsbConnection() {
|
||||
isReading = false
|
||||
readThread?.interrupt()
|
||||
if (readThread != null && readThread !== Thread.currentThread()) {
|
||||
try {
|
||||
readThread?.join(300)
|
||||
} catch (_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
readThread = null
|
||||
|
||||
val connection = usbConnection
|
||||
val claimedControl = controlInterface
|
||||
val claimedData = dataInterface
|
||||
|
||||
usbInEndpoint = null
|
||||
usbOutEndpoint = null
|
||||
controlInterface = null
|
||||
dataInterface = null
|
||||
usbConnection = null
|
||||
|
||||
if (connection != null) {
|
||||
if (claimedControl != null) {
|
||||
try {
|
||||
connection.releaseInterface(claimedControl)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
if (claimedData != null && claimedData.id != claimedControl?.id) {
|
||||
try {
|
||||
connection.releaseInterface(claimedData)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
connection.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
connectedDeviceName = null
|
||||
}
|
||||
|
||||
private fun handleUsbDetached(intent: Intent) {
|
||||
val detachedDevice =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
||||
}
|
||||
|
||||
val detachedName = detachedDevice?.deviceName ?: return
|
||||
|
||||
if (pendingConnectPortName == detachedName) {
|
||||
pendingConnectResult?.error(
|
||||
"usb_device_detached",
|
||||
"USB device was removed before the connection completed",
|
||||
null,
|
||||
)
|
||||
pendingConnectResult = null
|
||||
pendingConnectPortName = null
|
||||
}
|
||||
|
||||
if (connectedDeviceName == detachedName) {
|
||||
scheduleCloseUsbConnection {
|
||||
eventSink?.error(
|
||||
"usb_device_detached",
|
||||
"USB device was disconnected",
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pendingIntentFlags(): Int {
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
return flags
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
bool _pendingInitialChannelSync = false;
|
||||
bool _pendingInitialContactsSync = false;
|
||||
bool _bleInitialSyncStarted = false;
|
||||
bool _pendingDeferredChannelSyncAfterContacts = false;
|
||||
bool _webInitialHandshakeRequestSent = false;
|
||||
bool _preserveContactsOnRefresh = false;
|
||||
static const int _defaultMaxContacts = 32;
|
||||
static const int _defaultMaxChannels = 8;
|
||||
@@ -364,6 +367,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort after merging persisted and in-memory messages so the
|
||||
// conversation window remains stable after optimistic inserts.
|
||||
mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final windowedMergedMessages = mergedMessages.length > _messageWindowSize
|
||||
? mergedMessages.sublist(mergedMessages.length - _messageWindowSize)
|
||||
@@ -820,6 +825,76 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_usbSerialService.setRequestPortLabel(label);
|
||||
}
|
||||
|
||||
Future<void> connectUsb({
|
||||
required String portName,
|
||||
int baudRate = 115200,
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPortKey = null;
|
||||
_activeUsbPortLabel = null;
|
||||
|
||||
await stopScan();
|
||||
_cancelReconnectTimer();
|
||||
_manualDisconnect = false;
|
||||
_resetConnectionHandshakeState();
|
||||
_activeTransport = MeshCoreTransportType.usb;
|
||||
_activeUsbPortKey = portName;
|
||||
_activeUsbPortLabel = portName;
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
|
||||
try {
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
|
||||
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
|
||||
_activeUsbPortLabel =
|
||||
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
|
||||
notifyListeners();
|
||||
if (PlatformInfo.isWeb) {
|
||||
await stopScan();
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
_usbFrameSubscription = _usbSerialService.frameStream.listen(
|
||||
_handleFrame,
|
||||
onError: (error, stackTrace) {
|
||||
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
onDone: () {
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
var gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
if (!gotSelfInfo) {
|
||||
throw StateError('Timed out waiting for SELF_INFO during connect');
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
} catch (error) {
|
||||
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
|
||||
await disconnect(manual: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect(BluetoothDevice device, {String? displayName}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
@@ -844,6 +919,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_lastDeviceDisplayName = _deviceDisplayName;
|
||||
_manualDisconnect = false;
|
||||
_cancelReconnectTimer();
|
||||
_bleInitialSyncStarted = false;
|
||||
if (PlatformInfo.isWeb) {
|
||||
_resetConnectionHandshakeState();
|
||||
}
|
||||
@@ -856,6 +932,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
'Starting connect to $connectLabel',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
await _connectionSubscription?.cancel();
|
||||
_connectionSubscription = null;
|
||||
await _notifySubscription?.cancel();
|
||||
_notifySubscription = null;
|
||||
_connectionSubscription = device.connectionState.listen((state) {
|
||||
if (state == BluetoothConnectionState.disconnected && isConnected) {
|
||||
_handleDisconnection();
|
||||
@@ -899,6 +979,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
if (PlatformInfo.isWeb &&
|
||||
error.toString().contains('GATT Server is disconnected')) {
|
||||
// Chrome Web Bluetooth intermittently disconnects between connect()
|
||||
// and service discovery; retry once to recover that transient state.
|
||||
_appDebugLogService?.warn(
|
||||
'retrying service discovery after transient web disconnect',
|
||||
tag: 'BLE Connect',
|
||||
@@ -995,42 +1077,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = true;
|
||||
}
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth) {
|
||||
// Chrome's Web Bluetooth stack commonly delays incoming notifications
|
||||
// until the non-blocking notify setup settles. Avoid stacking extra
|
||||
// startup writes while that is happening. Defer the clock sync until
|
||||
// the connection has had time to settle.
|
||||
unawaited(
|
||||
Future<void>(() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 5));
|
||||
if (!isConnected ||
|
||||
!PlatformInfo.isWeb ||
|
||||
_activeTransport != MeshCoreTransportType.bluetooth) {
|
||||
return;
|
||||
}
|
||||
await syncTime();
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
final gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
|
||||
unawaited(syncTime());
|
||||
}
|
||||
|
||||
// Fetch channels so we can track unread counts for incoming messages
|
||||
if (!_shouldGateInitialChannelSync) {
|
||||
unawaited(getChannels());
|
||||
}
|
||||
unawaited(Future<void>.microtask(() => _startBleInitialSync()));
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect');
|
||||
await disconnect(manual: false);
|
||||
@@ -1038,76 +1085,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connectUsb({
|
||||
required String portName,
|
||||
int baudRate = 115200,
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.connecting ||
|
||||
_state == MeshCoreConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeTransport = MeshCoreTransportType.bluetooth;
|
||||
_activeUsbPortKey = null;
|
||||
_activeUsbPortLabel = null;
|
||||
|
||||
await stopScan();
|
||||
_cancelReconnectTimer();
|
||||
_manualDisconnect = false;
|
||||
_resetConnectionHandshakeState();
|
||||
_activeTransport = MeshCoreTransportType.usb;
|
||||
_activeUsbPortKey = portName;
|
||||
_activeUsbPortLabel = portName;
|
||||
_setState(MeshCoreConnectionState.connecting);
|
||||
|
||||
try {
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
|
||||
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
|
||||
_activeUsbPortLabel =
|
||||
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
|
||||
notifyListeners();
|
||||
if (PlatformInfo.isWeb) {
|
||||
await stopScan();
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
_usbFrameSubscription = _usbSerialService.frameStream.listen(
|
||||
_handleFrame,
|
||||
onError: (error, stackTrace) {
|
||||
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
onDone: () {
|
||||
unawaited(disconnect(manual: false));
|
||||
},
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
_pendingInitialChannelSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
var gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
if (!gotSelfInfo) {
|
||||
throw StateError('Timed out waiting for SELF_INFO during connect');
|
||||
}
|
||||
|
||||
await syncTime();
|
||||
} catch (error) {
|
||||
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
|
||||
await disconnect(manual: false);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _waitForSelfInfo({required Duration timeout}) async {
|
||||
if (_selfPublicKey != null) return true;
|
||||
if (!isConnected) return false;
|
||||
@@ -1139,17 +1116,60 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _startBleInitialSync() async {
|
||||
if (_bleInitialSyncStarted ||
|
||||
!isConnected ||
|
||||
_activeTransport != MeshCoreTransportType.bluetooth) {
|
||||
return;
|
||||
}
|
||||
_bleInitialSyncStarted = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
// Keep Web BLE startup writes light while notifications settle.
|
||||
unawaited(
|
||||
Future<void>(() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 5));
|
||||
if (!isConnected ||
|
||||
!PlatformInfo.isWeb ||
|
||||
_activeTransport != MeshCoreTransportType.bluetooth) {
|
||||
return;
|
||||
}
|
||||
await syncTime();
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
if (!gotSelfInfo) {
|
||||
await refreshDeviceInfo();
|
||||
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
|
||||
}
|
||||
|
||||
unawaited(syncTime());
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
}
|
||||
|
||||
void _resetConnectionHandshakeState() {
|
||||
_selfPublicKey = null;
|
||||
_selfName = null;
|
||||
_selfLatitude = null;
|
||||
_selfLongitude = null;
|
||||
_awaitingSelfInfo = false;
|
||||
_webInitialHandshakeRequestSent = false;
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
_selfInfoRetryTimer = null;
|
||||
_hasReceivedDeviceInfo = false;
|
||||
_pendingInitialChannelSync = false;
|
||||
_pendingInitialContactsSync = false;
|
||||
_bleInitialSyncStarted = false;
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_webInitialHandshakeRequestSent = false;
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect =>
|
||||
@@ -1205,6 +1225,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> disconnect({bool manual = true}) async {
|
||||
if (_state == MeshCoreConnectionState.disconnecting) return;
|
||||
final transportAtDisconnect = _activeTransport;
|
||||
final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb
|
||||
? 'USB'
|
||||
: 'BLE';
|
||||
|
||||
_appDebugLogService?.info(
|
||||
'Starting disconnect transport=$transportLabel manual=$manual',
|
||||
tag: 'Connection',
|
||||
);
|
||||
|
||||
if (manual) {
|
||||
_manualDisconnect = true;
|
||||
@@ -1280,6 +1308,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_activeUsbPortLabel = null;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
_appDebugLogService?.info(
|
||||
'Disconnect complete transport=$transportLabel manual=$manual',
|
||||
tag: 'Connection',
|
||||
);
|
||||
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
@@ -1345,7 +1377,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> refreshDeviceInfo() async {
|
||||
if (!isConnected) return;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_webInitialHandshakeRequestSent &&
|
||||
_selfPublicKey == null) {
|
||||
return;
|
||||
}
|
||||
_awaitingSelfInfo = true;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_selfPublicKey == null) {
|
||||
_webInitialHandshakeRequestSent = true;
|
||||
}
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await requestBatteryStatus(force: true);
|
||||
@@ -1356,7 +1399,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> _requestDeviceInfo() async {
|
||||
if (!isConnected || _awaitingSelfInfo) return;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_webInitialHandshakeRequestSent &&
|
||||
_selfPublicKey == null) {
|
||||
return;
|
||||
}
|
||||
_awaitingSelfInfo = true;
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
_selfPublicKey == null) {
|
||||
_webInitialHandshakeRequestSent = true;
|
||||
}
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await sendFrame(buildGetCustomVarsFrame());
|
||||
@@ -2183,6 +2237,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_pendingQueueSync = false;
|
||||
unawaited(syncQueuedMessages(force: true));
|
||||
}
|
||||
if (_pendingDeferredChannelSyncAfterContacts &&
|
||||
(_activeTransport == MeshCoreTransportType.bluetooth ||
|
||||
_activeTransport == MeshCoreTransportType.usb)) {
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
unawaited(getChannels());
|
||||
}
|
||||
break;
|
||||
case respCodeContactMsgRecv:
|
||||
case respCodeContactMsgRecvV3:
|
||||
@@ -2294,6 +2354,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// [58+] = node_name
|
||||
if (frame.length < 4 + pubKeySize) return;
|
||||
|
||||
final wasAwaitingSelfInfo = _awaitingSelfInfo;
|
||||
|
||||
_currentTxPower = frame[2];
|
||||
_maxTxPower = frame[3];
|
||||
_selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize));
|
||||
@@ -2325,15 +2387,25 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfInfoRetryTimer = null;
|
||||
notifyListeners();
|
||||
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
!wasAwaitingSelfInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-fetch contacts after getting self info. On web BLE, defer this
|
||||
// until after channel 0 so startup writes stay serialized.
|
||||
if (PlatformInfo.isWeb &&
|
||||
_activeTransport == MeshCoreTransportType.bluetooth) {
|
||||
_pendingInitialContactsSync = true;
|
||||
} else if (_activeTransport == MeshCoreTransportType.usb) {
|
||||
_pendingDeferredChannelSyncAfterContacts = true;
|
||||
getContacts();
|
||||
} else {
|
||||
getContacts();
|
||||
}
|
||||
if (_shouldGateInitialChannelSync) {
|
||||
if (_shouldGateInitialChannelSync &&
|
||||
_activeTransport != MeshCoreTransportType.usb) {
|
||||
_maybeStartInitialChannelSync();
|
||||
}
|
||||
}
|
||||
@@ -2367,6 +2439,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
|
||||
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
|
||||
if (isConnected &&
|
||||
_selfPublicKey != null &&
|
||||
(!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) {
|
||||
unawaited(getChannels(maxChannels: nextMaxChannels));
|
||||
}
|
||||
@@ -3524,17 +3597,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// For 1:1 chats, sender is implicit (null)
|
||||
String? senderName;
|
||||
if (isRoomServer && !msg.isOutgoing) {
|
||||
// Treat a missing room-contact key as unknown instead of matching every
|
||||
// contact via an empty prefix.
|
||||
if (msg.fourByteRoomContactKey.length == 4) {
|
||||
final senderContact = _contacts.cast<Contact?>().firstWhere(
|
||||
(c) =>
|
||||
c != null &&
|
||||
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
|
||||
orElse: () => null,
|
||||
);
|
||||
senderName = senderContact?.name;
|
||||
}
|
||||
final senderContact = _contacts.cast<Contact?>().firstWhere(
|
||||
(c) =>
|
||||
c != null &&
|
||||
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
|
||||
orElse: () => null,
|
||||
);
|
||||
senderName = senderContact?.name;
|
||||
} else if (isRoomServer && msg.isOutgoing) {
|
||||
senderName = selfName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'meshcore_connector.dart';
|
||||
|
||||
class MeshCoreConnectorUsb {
|
||||
const MeshCoreConnectorUsb(this.connector);
|
||||
|
||||
final MeshCoreConnector connector;
|
||||
|
||||
MeshCoreConnectionState get state => connector.state;
|
||||
MeshCoreTransportType get activeTransport => connector.activeTransport;
|
||||
String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel;
|
||||
bool get isUsbTransportConnected => connector.isUsbTransportConnected;
|
||||
|
||||
void addListener(VoidCallback listener) => connector.addListener(listener);
|
||||
void removeListener(VoidCallback listener) =>
|
||||
connector.removeListener(listener);
|
||||
|
||||
Future<List<String>> listPorts() => connector.listUsbPorts();
|
||||
|
||||
void setRequestPortLabel(String label) {
|
||||
connector.setUsbRequestPortLabel(label);
|
||||
}
|
||||
|
||||
Future<void> connect({required String portName, int baudRate = 115200}) {
|
||||
return connector.connectUsb(portName: portName, baudRate: baudRate);
|
||||
}
|
||||
|
||||
Future<void> disconnect({bool manual = true}) {
|
||||
return connector.disconnect(manual: manual);
|
||||
}
|
||||
}
|
||||
+848
-850
File diff suppressed because it is too large
Load Diff
+277
-279
File diff suppressed because it is too large
Load Diff
+14
-16
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contacts",
|
||||
@@ -28,7 +28,7 @@
|
||||
"common_disable": "Disable",
|
||||
"common_reboot": "Reboot",
|
||||
"common_loading": "Loading...",
|
||||
"common_notAvailable": "—",
|
||||
"common_notAvailable": "—",
|
||||
"common_voltageValue": "{volts} V",
|
||||
"@common_voltageValue": {
|
||||
"placeholders": {
|
||||
@@ -46,8 +46,6 @@
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"connectionChoiceTitle": "Choose your connection method",
|
||||
"connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "Connect over USB",
|
||||
@@ -180,20 +178,20 @@
|
||||
"appSettings_language": "Language",
|
||||
"appSettings_languageSystem": "System default",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageSl": "SlovenÅ¡Äina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageZh": "䏿–‡",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageRu": "Русский",
|
||||
"appSettings_languageUk": "Українська",
|
||||
"appSettings_languageSk": "SlovenÄina",
|
||||
"appSettings_languageBg": "БългарÑки",
|
||||
"appSettings_languageRu": "РуÑÑкий",
|
||||
"appSettings_languageUk": "УкраїнÑька",
|
||||
"appSettings_enableMessageTracing": "Enable Message Tracing",
|
||||
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
|
||||
"appSettings_notifications": "Notifications",
|
||||
@@ -1341,7 +1339,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"@telemetry_temperatureValue": {
|
||||
"placeholders": {
|
||||
"celsius": {
|
||||
@@ -1391,7 +1389,7 @@
|
||||
"channelPath_repeatsLabel": "Repeats",
|
||||
"channelPath_pathLabel": "Path {index}",
|
||||
"channelPath_observedLabel": "Observed",
|
||||
"channelPath_observedPathTitle": "Observed path {index} • {hops}",
|
||||
"channelPath_observedPathTitle": "Observed path {index} • {hops}",
|
||||
"@channelPath_observedPathTitle": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
@@ -1466,7 +1464,7 @@
|
||||
},
|
||||
"channelPath_pathLabelTitle": "Path",
|
||||
"channelPath_observedPathHeader": "Observed Path",
|
||||
"channelPath_selectedPathLabel": "{label} • {prefixes}",
|
||||
"channelPath_selectedPathLabel": "{label} • {prefixes}",
|
||||
"@channelPath_selectedPathLabel": {
|
||||
"placeholders": {
|
||||
"label": {
|
||||
|
||||
+341
-343
File diff suppressed because it is too large
Load Diff
+509
-511
File diff suppressed because it is too large
Load Diff
+96
-98
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"common_disable": "Disattivare",
|
||||
"common_reboot": "Riavvia",
|
||||
"common_loading": "Caricamento...",
|
||||
"common_notAvailable": "—",
|
||||
"common_notAvailable": "—",
|
||||
"common_voltageValue": "{volts} V",
|
||||
"@common_voltageValue": {
|
||||
"placeholders": {
|
||||
@@ -98,11 +98,11 @@
|
||||
"settings_locationInvalid": "Latitudine o longitudine non valida.",
|
||||
"settings_latitude": "Latitudine",
|
||||
"settings_longitude": "Longitudine",
|
||||
"settings_privacyMode": "Modalità Privacy",
|
||||
"settings_privacyMode": "Modalità Privacy",
|
||||
"settings_privacyModeSubtitle": "Nascondere nome/luogo negli annunci",
|
||||
"settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.",
|
||||
"settings_privacyModeEnabled": "Modalità privacy abilitata",
|
||||
"settings_privacyModeDisabled": "Modalità privacy disabilitata",
|
||||
"settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.",
|
||||
"settings_privacyModeEnabled": "Modalità privacy abilitata",
|
||||
"settings_privacyModeDisabled": "Modalità privacy disabilitata",
|
||||
"settings_actions": "Azioni",
|
||||
"settings_sendAdvertisement": "Invia Annuncio",
|
||||
"settings_sendAdvertisementSubtitle": "Presenza trasmessa ora",
|
||||
@@ -165,18 +165,18 @@
|
||||
"appSettings_language": "Lingua",
|
||||
"appSettings_languageSystem": "Predefinito di sistema",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageSl": "SlovenÅ¡Äina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageZh": "䏿–‡",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageSk": "SlovenÄina",
|
||||
"appSettings_languageBg": "БългарÑки",
|
||||
"appSettings_notifications": "Notifiche",
|
||||
"appSettings_enableNotifications": "Abilita Notifiche",
|
||||
"appSettings_enableNotificationsSubtitle": "Ricevi notifiche per messaggi e annunci",
|
||||
@@ -195,7 +195,7 @@
|
||||
"appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.",
|
||||
"appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.",
|
||||
"appSettings_autoRouteRotation": "Rotazione Percorso Automatico",
|
||||
"appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione",
|
||||
"appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione",
|
||||
"appSettings_autoRouteRotationEnabled": "Rotazione percorso automatico abilitata",
|
||||
"appSettings_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata",
|
||||
"appSettings_battery": "Batteria",
|
||||
@@ -284,8 +284,8 @@
|
||||
},
|
||||
"contacts_newGroup": "Nuovo Gruppo",
|
||||
"contacts_groupName": "Nome gruppo",
|
||||
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
||||
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
||||
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
||||
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già .",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -345,7 +345,7 @@
|
||||
"channels_muteChannel": "Silenzia canale",
|
||||
"channels_unmuteChannel": "Attiva notifiche canale",
|
||||
"channels_deleteChannel": "Elimina canale",
|
||||
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
||||
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -477,7 +477,7 @@
|
||||
"debugLog_enableInSettings": "Abilita il logging di debug dell'app nelle impostazioni",
|
||||
"debugLog_frames": "Frame",
|
||||
"debugLog_rawLogRx": "Log Raw-RX",
|
||||
"debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.",
|
||||
"debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.",
|
||||
"debugFrame_length": "Lunghezza del Frame: {count} byte",
|
||||
"@debugFrame_length": {
|
||||
"placeholders": {
|
||||
@@ -542,11 +542,11 @@
|
||||
},
|
||||
"debugFrame_hexDump": "Dumpa Esadecimale:",
|
||||
"chat_pathManagement": "Gestione Percorsi",
|
||||
"chat_routingMode": "Modalità di routing",
|
||||
"chat_routingMode": "Modalità di routing",
|
||||
"chat_autoUseSavedPath": "Utilizza il percorso salvato",
|
||||
"chat_forceFloodMode": "Modalità Inondamento Forzato",
|
||||
"chat_forceFloodMode": "Modalità Inondamento Forzato",
|
||||
"chat_recentAckPaths": "Percorsi ACK Recenti (tocca per usare):",
|
||||
"chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.",
|
||||
"chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.",
|
||||
"chat_hopSingular": "salta",
|
||||
"chat_hopPlural": "salta",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}",
|
||||
@@ -559,15 +559,15 @@
|
||||
},
|
||||
"chat_successes": "successi",
|
||||
"chat_removePath": "Rimuovi percorso",
|
||||
"chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.",
|
||||
"chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.",
|
||||
"chat_pathActions": "Azioni Percorso:",
|
||||
"chat_setCustomPath": "Imposta Percorso Personalizzato",
|
||||
"chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing",
|
||||
"chat_clearPath": "Cancella Percorso",
|
||||
"chat_clearPathSubtitle": "Riprova la scoperta alla prossima invio",
|
||||
"chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.",
|
||||
"chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.",
|
||||
"chat_floodModeSubtitle": "Utilizza l'interruttore di routing nella barra delle applicazioni",
|
||||
"chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.",
|
||||
"chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.",
|
||||
"chat_fullPath": "Percorso Completo",
|
||||
"chat_pathDetailsNotAvailable": "I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.",
|
||||
"chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
|
||||
@@ -660,7 +660,7 @@
|
||||
"map_sendToChannel": "Invia al canale",
|
||||
"map_noChannelsAvailable": "Nessun canale disponibile",
|
||||
"map_publicLocationShare": "Condividi in una posizione pubblica",
|
||||
"map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.",
|
||||
"map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.",
|
||||
"@map_publicLocationShareConfirm": {
|
||||
"placeholders": {
|
||||
"channelLabel": {
|
||||
@@ -810,13 +810,13 @@
|
||||
"login_password": "Password",
|
||||
"login_enterPassword": "Inserisci password",
|
||||
"login_savePassword": "Salva password",
|
||||
"login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.",
|
||||
"login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.",
|
||||
"login_repeaterDescription": "Inserisci la password del ripetitore per accedere alle impostazioni e allo stato.",
|
||||
"login_roomDescription": "Inserisci la password della stanza per accedere alle impostazioni e allo stato.",
|
||||
"login_routing": "Instradamento",
|
||||
"login_routingMode": "Modalità di routing",
|
||||
"login_routingMode": "Modalità di routing",
|
||||
"login_autoUseSavedPath": "Utilizza il percorso salvato",
|
||||
"login_forceFloodMode": "Modalità Inondamento Forzato",
|
||||
"login_forceFloodMode": "Modalità Inondamento Forzato",
|
||||
"login_managePaths": "Gestisci Percorsi",
|
||||
"login_login": "Accedi",
|
||||
"login_attempt": "Prova {current}/{max}",
|
||||
@@ -838,7 +838,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.",
|
||||
"login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.",
|
||||
"common_reload": "Ricaricare",
|
||||
"common_clear": "Cancella",
|
||||
"path_currentPath": "Percorso corrente: {path}",
|
||||
@@ -862,7 +862,7 @@
|
||||
"path_hexPrefixInstructions": "Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.",
|
||||
"path_hexPrefixExample": "Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)",
|
||||
"path_labelHexPrefixes": "Prefisso esadecimale (percorso)",
|
||||
"path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)",
|
||||
"path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)",
|
||||
"path_selectFromContacts": "Seleziona da contatti:",
|
||||
"path_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.",
|
||||
"path_customPathsRequire": "I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.",
|
||||
@@ -874,7 +874,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.",
|
||||
"path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.",
|
||||
"path_setPath": "Imposta Percorso",
|
||||
"repeater_management": "Gestione Ripetitori",
|
||||
"repeater_managementTools": "Strumenti di Gestione",
|
||||
@@ -887,9 +887,9 @@
|
||||
"repeater_settings": "Impostazioni",
|
||||
"repeater_settingsSubtitle": "Configura i parametri del ripetitore",
|
||||
"repeater_statusTitle": "Stato del Ripetitore",
|
||||
"repeater_routingMode": "Modalità di routing",
|
||||
"repeater_routingMode": "Modalità di routing",
|
||||
"repeater_autoUseSavedPath": "Percorso salvato automatico",
|
||||
"repeater_forceFloodMode": "Modalità Inondamento Forzato",
|
||||
"repeater_forceFloodMode": "Modalità Inondamento Forzato",
|
||||
"repeater_pathManagement": "Gestione dei percorsi",
|
||||
"repeater_refresh": "Aggiorna",
|
||||
"repeater_statusRequestTimeout": "Richiesta stato scaduta.",
|
||||
@@ -904,7 +904,7 @@
|
||||
"repeater_systemInformation": "Informazioni di sistema",
|
||||
"repeater_battery": "Batteria",
|
||||
"repeater_clockAtLogin": "Orologio (all'accesso)",
|
||||
"repeater_uptime": "Disponibilità",
|
||||
"repeater_uptime": "Disponibilità ",
|
||||
"repeater_queueLength": "Lunghezza della coda",
|
||||
"repeater_debugFlags": "Impostazioni Debug",
|
||||
"repeater_radioStatistics": "Statistiche Radio",
|
||||
@@ -1007,10 +1007,10 @@
|
||||
"repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti",
|
||||
"repeater_guestAccess": "Accesso Ospite",
|
||||
"repeater_guestAccessSubtitle": "Consenti l'accesso ospite in sola lettura",
|
||||
"repeater_privacyMode": "Modalità Privacy",
|
||||
"repeater_privacyMode": "Modalità Privacy",
|
||||
"repeater_privacyModeSubtitle": "Nascondere nome/luogo negli annunci",
|
||||
"repeater_advertisementSettings": "Impostazioni Annuncio",
|
||||
"repeater_localAdvertInterval": "Intervallo Pubblicità Locale",
|
||||
"repeater_localAdvertInterval": "Intervallo Pubblicità Locale",
|
||||
"repeater_localAdvertIntervalMinutes": "{minutes} minuti",
|
||||
"@repeater_localAdvertIntervalMinutes": {
|
||||
"placeholders": {
|
||||
@@ -1019,7 +1019,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione",
|
||||
"repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione",
|
||||
"repeater_floodAdvertIntervalHours": "{hours} ore",
|
||||
"@repeater_floodAdvertIntervalHours": {
|
||||
"placeholders": {
|
||||
@@ -1033,13 +1033,13 @@
|
||||
"repeater_rebootRepeater": "Riavvia Ripetitore",
|
||||
"repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo ripetitore",
|
||||
"repeater_rebootRepeaterConfirm": "Sei sicuro di voler riavviare questo ripetitore?",
|
||||
"repeater_regenerateIdentityKey": "Rigenera Chiave Identità",
|
||||
"repeater_regenerateIdentityKey": "Rigenera Chiave Identità ",
|
||||
"repeater_regenerateIdentityKeySubtitle": "Genera una nuova coppia di chiavi pubblica/privata",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?",
|
||||
"repeater_eraseFileSystem": "Elimina File System",
|
||||
"repeater_eraseFileSystemSubtitle": "Formatta il file system del ripetitore",
|
||||
"repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!",
|
||||
"repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.",
|
||||
"repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!",
|
||||
"repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.",
|
||||
"repeater_commandSent": "Comando inviato: {command}",
|
||||
"@repeater_commandSent": {
|
||||
"placeholders": {
|
||||
@@ -1072,7 +1072,7 @@
|
||||
"repeater_refreshLocationSettings": "Aggiorna le Impostazioni della Posizione",
|
||||
"repeater_refreshPacketForwarding": "Aggiorna il inoltro pacchetti",
|
||||
"repeater_refreshGuestAccess": "Aggiorna Accesso Ospite",
|
||||
"repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy",
|
||||
"repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy",
|
||||
"repeater_refreshAdvertisementSettings": "Aggiorna le Impostazioni dell'Annuncio",
|
||||
"repeater_refreshed": "{label} aggiornato",
|
||||
"@repeater_refreshed": {
|
||||
@@ -1117,7 +1117,7 @@
|
||||
"repeater_cliQuickAdvertise": "Pubblicare",
|
||||
"repeater_cliQuickClock": "Orologio",
|
||||
"repeater_cliHelpAdvert": "Invia un pacchetto pubblicitario",
|
||||
"repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)",
|
||||
"repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)",
|
||||
"repeater_cliHelpClock": "Mostra l'ora corrente per l'orologio di ciascun dispositivo.",
|
||||
"repeater_cliHelpPassword": "Imposta una nuova password di amministratore per il dispositivo.",
|
||||
"repeater_cliHelpVersion": "Mostra la versione del dispositivo e la data di costruzione del firmware.",
|
||||
@@ -1125,12 +1125,12 @@
|
||||
"repeater_cliHelpSetAf": "Imposta il fattore di tempo di trasmissione.",
|
||||
"repeater_cliHelpSetTx": "Imposta la potenza di trasmissione LoRa in dBm (riavvia per applicare).",
|
||||
"repeater_cliHelpSetRepeat": "Abilita o disabilita il ruolo del ripetitore per questo nodo.",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).",
|
||||
"repeater_cliHelpSetFloodMax": "Imposta il numero massimo di salti per i pacchetti di inondazione in entrata (se >= max, il pacchetto non viene inoltrato)",
|
||||
"repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.",
|
||||
"repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.",
|
||||
"repeater_cliHelpSetAgcResetInterval": "Imposta l'intervallo per resettare il controllore Automatico del Guadagno. Imposta su 0 per disabilitare.",
|
||||
"repeater_cliHelpSetMultiAcks": "Abilita o disabilita la funzione 'double ACKs'.",
|
||||
"repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.",
|
||||
"repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Imposta l'intervallo del timer in ore per inviare un pacchetto pubblicitario di massa. Imposta su 0 per disabilitare.",
|
||||
"repeater_cliHelpSetGuestPassword": "Imposta/aggiorna la password dell'ospite. (per ripetitori, gli accessi degli ospiti possono inviare la richiesta \"Get Stats\")",
|
||||
"repeater_cliHelpSetName": "Imposta il nome dell'annuncio.",
|
||||
@@ -1138,33 +1138,33 @@
|
||||
"repeater_cliHelpSetLon": "Imposta la longitudine della mappa pubblicitaria. (gradi decimali)",
|
||||
"repeater_cliHelpSetRadio": "Imposta completamente nuovi parametri radio e li salva nelle preferenze. Richiede un comando \"reboot\" per l'applicazione.",
|
||||
"repeater_cliHelpSetRxDelay": "Impostazioni (experimental) base (deve essere > 1 per l'effetto) per applicare un leggero ritardo ai pacchetti ricevuti, in base alla forza del segnale/punteggio. Imposta a 0 per disabilitare.",
|
||||
"repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).",
|
||||
"repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.",
|
||||
"repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).",
|
||||
"repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.",
|
||||
"repeater_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.",
|
||||
"repeater_cliHelpSetBridgeDelay": "Imposta il ritardo prima di ritrasmettere i pacchetti.",
|
||||
"repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.",
|
||||
"repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.",
|
||||
"repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.",
|
||||
"repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.",
|
||||
"repeater_cliHelpSetBridgeSecret": "Imposta il segreto per i ponti espnow.",
|
||||
"repeater_cliHelpSetAdcMultiplier": "Imposta un fattore personalizzato per regolare la tensione della batteria riportata (supportato solo su schede selezionate).",
|
||||
"repeater_cliHelpTempRadio": "Imposta parametri radio temporanei per il numero specificato di minuti, per poi tornare ai parametri radio originali. (non salva nelle preferenze).",
|
||||
"repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)",
|
||||
"repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)",
|
||||
"repeater_cliHelpGetBridgeType": "Ottiene tipo ponte nessuno, rs232, espnow",
|
||||
"repeater_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.",
|
||||
"repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al file system.",
|
||||
"repeater_cliHelpLogErase": "Elimina i log del pacchetto dal file system.",
|
||||
"repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4",
|
||||
"repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4",
|
||||
"repeater_cliHelpNeighborRemove": "Rimuove la prima corrispondenza in base al prefisso (esadecimale) della pubkey, dalla lista dei vicini.",
|
||||
"repeater_cliHelpRegion": "(solo serie) Elenca tutte le regioni definite e le autorizzazioni di allagamento correnti.",
|
||||
"repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.",
|
||||
"repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.",
|
||||
"repeater_cliHelpRegionGet": "Cerca la regione con il prefisso del nome dato (o \"\" per l'ambito globale). Risponde con \"-> nome-regione (nome-genitore) 'F'\"",
|
||||
"repeater_cliHelpRegionPut": "Aggiunge o aggiorna una definizione di regione con il nome specificato.",
|
||||
"repeater_cliHelpRegionRemove": "Rimuove una definizione di regione con il dato nome. (deve corrispondere esattamente e non avere regioni figlio)",
|
||||
"repeater_cliHelpRegionAllowf": "Imposta il permesso di 'F'lood per la regione specificata. ('' per lo scope globale/legacy)",
|
||||
"repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).",
|
||||
"repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).",
|
||||
"repeater_cliHelpRegionHome": "Risposte con la regione 'home' corrente. (Nota applicata finora, riservata per il futuro)",
|
||||
"repeater_cliHelpRegionHomeSet": "Imposta la regione 'home'.",
|
||||
"repeater_cliHelpRegionSave": "Persiste l'elenco/mappa delle regioni all'archiviazione.",
|
||||
"repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.",
|
||||
"repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.",
|
||||
"repeater_cliHelpGpsOnOff": "Attiva/disattiva l'alimentazione del GPS.",
|
||||
"repeater_cliHelpGpsSync": "Sincronizza l'orario del nodo con l'orologio GPS.",
|
||||
"repeater_cliHelpGpsSetLoc": "Imposta la posizione del nodo alle coordinate GPS e salva le preferenze.",
|
||||
@@ -1180,7 +1180,7 @@
|
||||
"repeater_regionManagementRepeaterOnly": "Gestione Regione (solo Ripetitore)",
|
||||
"repeater_regionNote": "Sono state introdotte le comandi di regione per gestire le definizioni e le autorizzazioni delle regioni.",
|
||||
"repeater_gpsManagement": "Gestione GPS",
|
||||
"repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.",
|
||||
"repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.",
|
||||
"telemetry_receivedData": "Dati Telemetria Ricevuti",
|
||||
"telemetry_requestTimeout": "Richiesta di telemetria scaduta.",
|
||||
"telemetry_errorLoading": "Errore nel caricamento della telemetria: {error}",
|
||||
@@ -1232,7 +1232,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"@telemetry_temperatureValue": {
|
||||
"placeholders": {
|
||||
"celsius": {
|
||||
@@ -1254,7 +1254,7 @@
|
||||
"channelPath_repeatsLabel": "Ripeti",
|
||||
"channelPath_pathLabel": "Percorso {index}",
|
||||
"channelPath_observedLabel": "Osservato",
|
||||
"channelPath_observedPathTitle": "Percorso osservato {index} • {hops}",
|
||||
"channelPath_observedPathTitle": "Percorso osservato {index} • {hops}",
|
||||
"@channelPath_observedPathTitle": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
@@ -1329,7 +1329,7 @@
|
||||
},
|
||||
"channelPath_pathLabelTitle": "Percorso",
|
||||
"channelPath_observedPathHeader": "Percorso Osservato",
|
||||
"channelPath_selectedPathLabel": "{label} • {prefixes}",
|
||||
"channelPath_selectedPathLabel": "{label} • {prefixes}",
|
||||
"@channelPath_selectedPathLabel": {
|
||||
"placeholders": {
|
||||
"label": {
|
||||
@@ -1373,11 +1373,11 @@
|
||||
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
|
||||
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
|
||||
"channels_joinPublicChannel": "Unisciti al Canale Pubblico",
|
||||
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
|
||||
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
|
||||
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
|
||||
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
|
||||
"channels_scanQrCode": "Scansiona un codice QR",
|
||||
"channels_scanQrCodeComingSoon": "Arriverà presto",
|
||||
"channels_scanQrCodeComingSoon": "Arriverà presto",
|
||||
"channels_enterHashtag": "Inserisci hashtag",
|
||||
"channels_hashtagHint": "es. #team",
|
||||
"@neighbors_unknownContact": {
|
||||
@@ -1459,35 +1459,35 @@
|
||||
}
|
||||
},
|
||||
"common_ok": "OK",
|
||||
"community_title": "Comunità",
|
||||
"community_create": "Crea Comunità",
|
||||
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
|
||||
"community_title": "Comunità ",
|
||||
"community_create": "Crea Comunità ",
|
||||
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
|
||||
"community_join": "Unisciti",
|
||||
"community_joinTitle": "Unisciti alla Community",
|
||||
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
|
||||
"community_scanQr": "Scansiona il QR Code della Community",
|
||||
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità",
|
||||
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità ",
|
||||
"community_showQr": "Mostra il codice QR",
|
||||
"community_publicChannel": "Comunità Pubblica",
|
||||
"community_hashtagChannel": "Hashtag della Comunità",
|
||||
"community_name": "Nome della Comunità",
|
||||
"community_enterName": "Inserisci il nome della comunità",
|
||||
"community_created": "Comunità \"{name}\" creata",
|
||||
"community_joined": "Unito alla comunità \"{name}\"",
|
||||
"community_qrTitle": "Condividi Comunità",
|
||||
"community_publicChannel": "Comunità Pubblica",
|
||||
"community_hashtagChannel": "Hashtag della Comunità ",
|
||||
"community_name": "Nome della Comunità ",
|
||||
"community_enterName": "Inserisci il nome della comunità ",
|
||||
"community_created": "Comunità \"{name}\" creata",
|
||||
"community_joined": "Unito alla comunità \"{name}\"",
|
||||
"community_qrTitle": "Condividi Comunità ",
|
||||
"community_qrInstructions": "Scansiona questo codice QR per unirti a {name}",
|
||||
"community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community",
|
||||
"community_invalidQrCode": "Codice QR della community non valido",
|
||||
"community_alreadyMember": "Già membro",
|
||||
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
|
||||
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
|
||||
"community_alreadyMember": "Già membro",
|
||||
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
|
||||
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità ",
|
||||
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
|
||||
"community_noCommunities": "Nessun gruppo aggiunto finora",
|
||||
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
|
||||
"community_manageCommunities": "Gestisci Comunità",
|
||||
"community_delete": "Lascia la Comunità",
|
||||
"community_manageCommunities": "Gestisci Comunità ",
|
||||
"community_delete": "Lascia la Comunità ",
|
||||
"community_deleteConfirm": "Uscire da \"{name}\"?",
|
||||
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
|
||||
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -1495,14 +1495,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Hai lasciato la comunità \"{name}\"",
|
||||
"community_deleted": "Hai lasciato la comunità \"{name}\"",
|
||||
"community_addHashtagChannel": "Aggiungi Hashtag della Community",
|
||||
"community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
|
||||
"community_selectCommunity": "Seleziona Comunità",
|
||||
"community_selectCommunity": "Seleziona Comunità ",
|
||||
"community_regularHashtag": "Hashtag regolare",
|
||||
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
|
||||
"community_communityHashtag": "Hashtag della Comunità",
|
||||
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
|
||||
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
|
||||
"community_communityHashtag": "Hashtag della Comunità ",
|
||||
"community_communityHashtagDesc": "Visibile solo ai membri della comunità ",
|
||||
"community_forCommunity": "Per {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
@@ -1567,16 +1567,16 @@
|
||||
"contacts_floodAdvert": "Annuncio alluvionale",
|
||||
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
||||
"contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard",
|
||||
"contacts_clipboardEmpty": "La clipboard è vuota.",
|
||||
"contacts_clipboardEmpty": "La clipboard è vuota.",
|
||||
"contacts_ShareContact": "Copia contatto negli Appunti",
|
||||
"contacts_contactImported": "Il contatto è stato importato.",
|
||||
"contacts_contactImported": "Il contatto è stato importato.",
|
||||
"contacts_contactImportFailed": "Contatto non importato con successo.",
|
||||
"contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.",
|
||||
"contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.",
|
||||
"contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio",
|
||||
"contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.",
|
||||
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
|
||||
"notification_activityTitle": "Attività MeshCore",
|
||||
"notification_activityTitle": "Attività MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
|
||||
@@ -1587,7 +1587,7 @@
|
||||
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
|
||||
"settings_gpxExportNoContacts": "Nessun contatto da esportare.",
|
||||
"settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo",
|
||||
"settings_gpxExportError": "Si è verificato un errore durante l'esportazione.",
|
||||
"settings_gpxExportError": "Si è verificato un errore durante l'esportazione.",
|
||||
"settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.",
|
||||
"settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.",
|
||||
"settings_gpxExportAll": "Esporta tutti i contatti in GPX",
|
||||
@@ -1597,13 +1597,13 @@
|
||||
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
|
||||
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
|
||||
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
|
||||
"map_removeLast": "Rimuovi ultimo",
|
||||
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
|
||||
"pathTrace_clearTooltip": "Pulisci percorso",
|
||||
"map_runTrace": "Esegui Path Trace",
|
||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||
"scanner_chromeRequired": "Browser Chrome richiesto",
|
||||
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
|
||||
@@ -1612,10 +1612,10 @@
|
||||
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||
"chat_ShowAllPaths": "Mostra tutti i percorsi",
|
||||
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
||||
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
|
||||
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Unità",
|
||||
"appSettings_unitsTitle": "Unità ",
|
||||
"appSettings_unitsMetric": "Metrico (m/km)",
|
||||
"appSettings_unitsImperial": "Imperiale (ft / mi)",
|
||||
"map_lineOfSight": "Linea di vista",
|
||||
@@ -1631,7 +1631,7 @@
|
||||
},
|
||||
"losClearAllPoints": "Cancella tutti i punti",
|
||||
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
|
||||
"losMenuTitle": "Menù LOS",
|
||||
"losMenuTitle": "Menù LOS",
|
||||
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
|
||||
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
|
||||
"losCustomPoints": "Punti personalizzati",
|
||||
@@ -1722,7 +1722,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
|
||||
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
|
||||
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
|
||||
"losRenameCustomPoint": "Rinomina punto personalizzato",
|
||||
"losPointName": "Nome del punto",
|
||||
@@ -1734,7 +1734,7 @@
|
||||
"losLegendTerrain": "Terreno",
|
||||
"losFrequencyLabel": "Frequenza",
|
||||
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
|
||||
"losFrequencyDialogTitle": "Calcolo dell’orizzonte radio",
|
||||
"losFrequencyDialogTitle": "Calcolo dell’orizzonte radio",
|
||||
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
@@ -1802,11 +1802,9 @@
|
||||
"contacts_unread": "Non letti",
|
||||
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
|
||||
"connectionChoiceTitle": "Scegli il metodo di connessione che preferisci.",
|
||||
"connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
|
||||
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
|
||||
"usbScreenStatus": "Seleziona un dispositivo USB",
|
||||
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
|
||||
"usbScreenTitle": "Connessione tramite USB",
|
||||
|
||||
@@ -295,7 +295,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @common_notAvailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'—'**
|
||||
/// **'—'**
|
||||
String get common_notAvailable;
|
||||
|
||||
/// No description provided for @common_voltageValue.
|
||||
@@ -316,18 +316,6 @@ abstract class AppLocalizations {
|
||||
/// **'MeshCore Open'**
|
||||
String get scanner_title;
|
||||
|
||||
/// No description provided for @connectionChoiceTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose your connection method'**
|
||||
String get connectionChoiceTitle;
|
||||
|
||||
/// No description provided for @connectionChoiceSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select how you would like to reach your MeshCore device.'**
|
||||
String get connectionChoiceSubtitle;
|
||||
|
||||
/// No description provided for @connectionChoiceUsbLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -955,13 +943,13 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @appSettings_languageFr.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Français'**
|
||||
/// **'Français'**
|
||||
String get appSettings_languageFr;
|
||||
|
||||
/// No description provided for @appSettings_languageEs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Español'**
|
||||
/// **'Español'**
|
||||
String get appSettings_languageEs;
|
||||
|
||||
/// No description provided for @appSettings_languageDe.
|
||||
@@ -979,13 +967,13 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @appSettings_languageSl.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Slovenščina'**
|
||||
/// **'SlovenÅ¡Äina'**
|
||||
String get appSettings_languageSl;
|
||||
|
||||
/// No description provided for @appSettings_languagePt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Português'**
|
||||
/// **'Português'**
|
||||
String get appSettings_languagePt;
|
||||
|
||||
/// No description provided for @appSettings_languageIt.
|
||||
@@ -997,7 +985,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @appSettings_languageZh.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'中文'**
|
||||
/// **'䏿–‡'**
|
||||
String get appSettings_languageZh;
|
||||
|
||||
/// No description provided for @appSettings_languageSv.
|
||||
@@ -1015,25 +1003,25 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @appSettings_languageSk.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Slovenčina'**
|
||||
/// **'SlovenÄina'**
|
||||
String get appSettings_languageSk;
|
||||
|
||||
/// No description provided for @appSettings_languageBg.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Български'**
|
||||
/// **'БългарÑки'**
|
||||
String get appSettings_languageBg;
|
||||
|
||||
/// No description provided for @appSettings_languageRu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Русский'**
|
||||
/// **'РуÑÑкий'**
|
||||
String get appSettings_languageRu;
|
||||
|
||||
/// No description provided for @appSettings_languageUk.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Українська'**
|
||||
/// **'УкраїнÑька'**
|
||||
String get appSettings_languageUk;
|
||||
|
||||
/// No description provided for @appSettings_enableMessageTracing.
|
||||
@@ -4349,7 +4337,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @telemetry_temperatureValue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{celsius}°C / {fahrenheit}°F'**
|
||||
/// **'{celsius}°C / {fahrenheit}°F'**
|
||||
String telemetry_temperatureValue(String celsius, String fahrenheit);
|
||||
|
||||
/// No description provided for @neighbors_receivedData.
|
||||
@@ -4463,7 +4451,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @channelPath_observedPathTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Observed path {index} • {hops}'**
|
||||
/// **'Observed path {index} • {hops}'**
|
||||
String channelPath_observedPathTitle(int index, String hops);
|
||||
|
||||
/// No description provided for @channelPath_noLocationData.
|
||||
@@ -4547,7 +4535,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @channelPath_selectedPathLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{label} • {prefixes}'**
|
||||
/// **'{label} • {prefixes}'**
|
||||
String channelPath_selectedPathLabel(String label, String prefixes);
|
||||
|
||||
/// No description provided for @channelPath_noHopDetailsAvailable.
|
||||
|
||||
+1042
-857
File diff suppressed because it is too large
Load Diff
+278
-284
File diff suppressed because it is too large
Load Diff
@@ -93,7 +93,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get common_loading => 'Loading...';
|
||||
|
||||
@override
|
||||
String get common_notAvailable => '—';
|
||||
String get common_notAvailable => '—';
|
||||
|
||||
@override
|
||||
String common_voltageValue(String volts) {
|
||||
@@ -108,13 +108,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Choose your connection method';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Select how you would like to reach your MeshCore device.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@@ -455,10 +448,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get appSettings_languageEn => 'English';
|
||||
|
||||
@override
|
||||
String get appSettings_languageFr => 'Français';
|
||||
String get appSettings_languageFr => 'Français';
|
||||
|
||||
@override
|
||||
String get appSettings_languageEs => 'Español';
|
||||
String get appSettings_languageEs => 'Español';
|
||||
|
||||
@override
|
||||
String get appSettings_languageDe => 'Deutsch';
|
||||
@@ -467,16 +460,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get appSettings_languagePl => 'Polski';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSl => 'Slovenščina';
|
||||
String get appSettings_languageSl => 'SlovenÅ¡Äina';
|
||||
|
||||
@override
|
||||
String get appSettings_languagePt => 'Português';
|
||||
String get appSettings_languagePt => 'Português';
|
||||
|
||||
@override
|
||||
String get appSettings_languageIt => 'Italiano';
|
||||
|
||||
@override
|
||||
String get appSettings_languageZh => '中文';
|
||||
String get appSettings_languageZh => '䏿–‡';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSv => 'Svenska';
|
||||
@@ -485,16 +478,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get appSettings_languageNl => 'Nederlands';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSk => 'Slovenčina';
|
||||
String get appSettings_languageSk => 'SlovenÄina';
|
||||
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
String get appSettings_languageBg => 'БългарÑки';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Русский';
|
||||
String get appSettings_languageRu => 'РуÑÑкий';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
String get appSettings_languageUk => 'УкраїнÑька';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
|
||||
@@ -2431,7 +2424,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String telemetry_temperatureValue(String celsius, String fahrenheit) {
|
||||
return '$celsius°C / $fahrenheit°F';
|
||||
return '$celsius°C / $fahrenheit°F';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2499,7 +2492,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channelPath_observedPathTitle(int index, String hops) {
|
||||
return 'Observed path $index • $hops';
|
||||
return 'Observed path $index • $hops';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2554,7 +2547,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channelPath_selectedPathLabel(String label, String prefixes) {
|
||||
return '$label • $prefixes';
|
||||
return '$label • $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+347
-347
File diff suppressed because it is too large
Load Diff
+515
-516
File diff suppressed because it is too large
Load Diff
@@ -93,7 +93,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get common_loading => 'Caricamento...';
|
||||
|
||||
@override
|
||||
String get common_notAvailable => '—';
|
||||
String get common_notAvailable => '—';
|
||||
|
||||
@override
|
||||
String common_voltageValue(String volts) {
|
||||
@@ -108,14 +108,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle =>
|
||||
'Scegli il metodo di connessione che preferisci.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@@ -134,7 +126,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
|
||||
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
@@ -176,7 +168,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get scanner_scan => 'Scansiona';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
|
||||
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
@@ -273,7 +265,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_longitude => 'Longitudine';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Modalità Privacy';
|
||||
String get settings_privacyMode => 'Modalità Privacy';
|
||||
|
||||
@override
|
||||
String get settings_privacyModeSubtitle =>
|
||||
@@ -281,13 +273,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_privacyModeToggle =>
|
||||
'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.';
|
||||
'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.';
|
||||
|
||||
@override
|
||||
String get settings_privacyModeEnabled => 'Modalità privacy abilitata';
|
||||
String get settings_privacyModeEnabled => 'Modalità privacy abilitata';
|
||||
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
|
||||
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Azioni';
|
||||
@@ -425,7 +417,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
|
||||
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -460,10 +452,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get appSettings_languageEn => 'English';
|
||||
|
||||
@override
|
||||
String get appSettings_languageFr => 'Français';
|
||||
String get appSettings_languageFr => 'Français';
|
||||
|
||||
@override
|
||||
String get appSettings_languageEs => 'Español';
|
||||
String get appSettings_languageEs => 'Español';
|
||||
|
||||
@override
|
||||
String get appSettings_languageDe => 'Deutsch';
|
||||
@@ -472,16 +464,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get appSettings_languagePl => 'Polski';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSl => 'Slovenščina';
|
||||
String get appSettings_languageSl => 'SlovenÅ¡Äina';
|
||||
|
||||
@override
|
||||
String get appSettings_languagePt => 'Português';
|
||||
String get appSettings_languagePt => 'Português';
|
||||
|
||||
@override
|
||||
String get appSettings_languageIt => 'Italiano';
|
||||
|
||||
@override
|
||||
String get appSettings_languageZh => '中文';
|
||||
String get appSettings_languageZh => '䏿–‡';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSv => 'Svenska';
|
||||
@@ -490,10 +482,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get appSettings_languageNl => 'Nederlands';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSk => 'Slovenčina';
|
||||
String get appSettings_languageSk => 'SlovenÄina';
|
||||
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
String get appSettings_languageBg => 'БългарÑки';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Russo';
|
||||
@@ -576,7 +568,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get appSettings_autoRouteRotationSubtitle =>
|
||||
'Alterna tra i percorsi migliori e la modalità alluvione';
|
||||
'Alterna tra i percorsi migliori e la modalità alluvione';
|
||||
|
||||
@override
|
||||
String get appSettings_autoRouteRotationEnabled =>
|
||||
@@ -671,7 +663,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Unità';
|
||||
String get appSettings_unitsTitle => 'Unità ';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrico (m/km)';
|
||||
@@ -790,11 +782,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get contacts_groupName => 'Nome gruppo';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
||||
String get contacts_groupNameRequired =>
|
||||
'Il nome del gruppo è obbligatorio.';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Il gruppo \"$name\" esiste già.';
|
||||
return 'Il gruppo \"$name\" esiste già .';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -880,7 +873,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channels_deleteChannelConfirm(String name) {
|
||||
return 'Eliminare \"$name\"? Non può essere annullato.';
|
||||
return 'Eliminare \"$name\"? Non può essere annullato.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -977,20 +970,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Chiunque può unirsi a questo canale.';
|
||||
'Chiunque può unirsi a questo canale.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Chiunque può unirsi ai canali hashtag.';
|
||||
'Chiunque può unirsi ai canali hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scansiona un codice QR';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
|
||||
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Inserisci hashtag';
|
||||
@@ -1124,7 +1117,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get debugLog_rawLogRx => 'Log Raw-RX';
|
||||
|
||||
@override
|
||||
String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.';
|
||||
String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.';
|
||||
|
||||
@override
|
||||
String debugFrame_length(int count) {
|
||||
@@ -1180,20 +1173,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modalità di routing';
|
||||
String get chat_routingMode => 'Modalità di routing';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Utilizza il percorso salvato';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.';
|
||||
'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'salta';
|
||||
@@ -1220,7 +1213,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.';
|
||||
'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Azioni Percorso:';
|
||||
@@ -1241,7 +1234,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
|
||||
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
@@ -1249,7 +1242,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.';
|
||||
'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Percorso Completo';
|
||||
@@ -1424,7 +1417,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String map_publicLocationShareConfirm(String channelLabel) {
|
||||
return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.';
|
||||
return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1642,7 +1635,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get login_savePasswordSubtitle =>
|
||||
'La password verrà memorizzata in modo sicuro su questo dispositivo.';
|
||||
'La password verrà memorizzata in modo sicuro su questo dispositivo.';
|
||||
|
||||
@override
|
||||
String get login_repeaterDescription =>
|
||||
@@ -1656,13 +1649,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get login_routing => 'Instradamento';
|
||||
|
||||
@override
|
||||
String get login_routingMode => 'Modalità di routing';
|
||||
String get login_routingMode => 'Modalità di routing';
|
||||
|
||||
@override
|
||||
String get login_autoUseSavedPath => 'Utilizza il percorso salvato';
|
||||
|
||||
@override
|
||||
String get login_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
String get login_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
|
||||
@override
|
||||
String get login_managePaths => 'Gestisci Percorsi';
|
||||
@@ -1682,7 +1675,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get login_failedMessage =>
|
||||
'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.';
|
||||
'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.';
|
||||
|
||||
@override
|
||||
String get common_reload => 'Ricaricare';
|
||||
@@ -1725,7 +1718,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)';
|
||||
'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Seleziona da contatti:';
|
||||
@@ -1745,7 +1738,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
|
||||
'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Imposta Percorso';
|
||||
@@ -1797,13 +1790,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_statusTitle => 'Stato del Ripetitore';
|
||||
|
||||
@override
|
||||
String get repeater_routingMode => 'Modalità di routing';
|
||||
String get repeater_routingMode => 'Modalità di routing';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Percorso salvato automatico';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Gestione dei percorsi';
|
||||
@@ -1829,7 +1822,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_clockAtLogin => 'Orologio (all\'accesso)';
|
||||
|
||||
@override
|
||||
String get repeater_uptime => 'Disponibilità';
|
||||
String get repeater_uptime => 'Disponibilità ';
|
||||
|
||||
@override
|
||||
String get repeater_queueLength => 'Lunghezza della coda';
|
||||
@@ -1981,7 +1974,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Consenti l\'accesso ospite in sola lettura';
|
||||
|
||||
@override
|
||||
String get repeater_privacyMode => 'Modalità Privacy';
|
||||
String get repeater_privacyMode => 'Modalità Privacy';
|
||||
|
||||
@override
|
||||
String get repeater_privacyModeSubtitle =>
|
||||
@@ -1991,7 +1984,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_advertisementSettings => 'Impostazioni Annuncio';
|
||||
|
||||
@override
|
||||
String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale';
|
||||
String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale';
|
||||
|
||||
@override
|
||||
String repeater_localAdvertIntervalMinutes(int minutes) {
|
||||
@@ -2000,7 +1993,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_floodAdvertInterval =>
|
||||
'Intervallo Pubblicità Inondazione';
|
||||
'Intervallo Pubblicità Inondazione';
|
||||
|
||||
@override
|
||||
String repeater_floodAdvertIntervalHours(int hours) {
|
||||
@@ -2026,7 +2019,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Sei sicuro di voler riavviare questo ripetitore?';
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità';
|
||||
String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità ';
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKeySubtitle =>
|
||||
@@ -2034,7 +2027,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKeyConfirm =>
|
||||
'Questo genererà una nuova identità per il ripetitore. Procedere?';
|
||||
'Questo genererà una nuova identità per il ripetitore. Procedere?';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystem => 'Elimina File System';
|
||||
@@ -2045,11 +2038,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystemConfirm =>
|
||||
'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!';
|
||||
'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!';
|
||||
|
||||
@override
|
||||
String get repeater_eraseSerialOnly =>
|
||||
'Elimina è disponibile solo tramite console seriale.';
|
||||
'Elimina è disponibile solo tramite console seriale.';
|
||||
|
||||
@override
|
||||
String repeater_commandSent(String command) {
|
||||
@@ -2093,7 +2086,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_refreshGuestAccess => 'Aggiorna Accesso Ospite';
|
||||
|
||||
@override
|
||||
String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy';
|
||||
String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy';
|
||||
|
||||
@override
|
||||
String get repeater_refreshAdvertisementSettings =>
|
||||
@@ -2174,7 +2167,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpReboot =>
|
||||
'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)';
|
||||
'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpClock =>
|
||||
@@ -2206,7 +2199,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetAllowReadOnly =>
|
||||
'(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).';
|
||||
'(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetFloodMax =>
|
||||
@@ -2214,7 +2207,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetIntThresh =>
|
||||
'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.';
|
||||
'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetAgcResetInterval =>
|
||||
@@ -2226,7 +2219,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetAdvertInterval =>
|
||||
'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.';
|
||||
'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetFloodAdvertInterval =>
|
||||
@@ -2257,11 +2250,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetTxDelay =>
|
||||
'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).';
|
||||
'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetDirectTxDelay =>
|
||||
'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.';
|
||||
'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.';
|
||||
@@ -2272,11 +2265,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetBridgeSource =>
|
||||
'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.';
|
||||
'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetBridgeBaud =>
|
||||
'Imposta la velocità di trasmissione per i ponti rs232.';
|
||||
'Imposta la velocità di trasmissione per i ponti rs232.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetBridgeSecret =>
|
||||
@@ -2292,7 +2285,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetPerm =>
|
||||
'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)';
|
||||
'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetBridgeType =>
|
||||
@@ -2312,7 +2305,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpNeighbors =>
|
||||
'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4';
|
||||
'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpNeighborRemove =>
|
||||
@@ -2324,7 +2317,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionLoad =>
|
||||
'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.';
|
||||
'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionGet =>
|
||||
@@ -2344,7 +2337,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionDenyf =>
|
||||
'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).';
|
||||
'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionHome =>
|
||||
@@ -2359,7 +2352,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGps =>
|
||||
'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.';
|
||||
'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGpsOnOff =>
|
||||
@@ -2416,7 +2409,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_gpsNote =>
|
||||
'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.';
|
||||
'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.';
|
||||
|
||||
@override
|
||||
String get telemetry_receivedData => 'Dati Telemetria Ricevuti';
|
||||
@@ -2469,7 +2462,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String telemetry_temperatureValue(String celsius, String fahrenheit) {
|
||||
return '$celsius°C / $fahrenheit°F';
|
||||
return '$celsius°C / $fahrenheit°F';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2537,7 +2530,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channelPath_observedPathTitle(int index, String hops) {
|
||||
return 'Percorso osservato $index • $hops';
|
||||
return 'Percorso osservato $index • $hops';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2592,7 +2585,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channelPath_selectedPathLabel(String label, String prefixes) {
|
||||
return '$label • $prefixes';
|
||||
return '$label • $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2603,14 +2596,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
|
||||
|
||||
@override
|
||||
String get community_title => 'Comunità';
|
||||
String get community_title => 'Comunità ';
|
||||
|
||||
@override
|
||||
String get community_create => 'Crea Comunità';
|
||||
String get community_create => 'Crea Comunità ';
|
||||
|
||||
@override
|
||||
String get community_createDesc =>
|
||||
'Crea una nuova comunità e condividila tramite codice QR.';
|
||||
'Crea una nuova comunità e condividila tramite codice QR.';
|
||||
|
||||
@override
|
||||
String get community_join => 'Unisciti';
|
||||
@@ -2628,35 +2621,35 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get community_scanInstructions =>
|
||||
'Punta la fotocamera su un codice QR della comunità';
|
||||
'Punta la fotocamera su un codice QR della comunità ';
|
||||
|
||||
@override
|
||||
String get community_showQr => 'Mostra il codice QR';
|
||||
|
||||
@override
|
||||
String get community_publicChannel => 'Comunità Pubblica';
|
||||
String get community_publicChannel => 'Comunità Pubblica';
|
||||
|
||||
@override
|
||||
String get community_hashtagChannel => 'Hashtag della Comunità';
|
||||
String get community_hashtagChannel => 'Hashtag della Comunità ';
|
||||
|
||||
@override
|
||||
String get community_name => 'Nome della Comunità';
|
||||
String get community_name => 'Nome della Comunità ';
|
||||
|
||||
@override
|
||||
String get community_enterName => 'Inserisci il nome della comunità';
|
||||
String get community_enterName => 'Inserisci il nome della comunità ';
|
||||
|
||||
@override
|
||||
String community_created(String name) {
|
||||
return 'Comunità \"$name\" creata';
|
||||
return 'Comunità \"$name\" creata';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_joined(String name) {
|
||||
return 'Unito alla comunità \"$name\"';
|
||||
return 'Unito alla comunità \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_qrTitle => 'Condividi Comunità';
|
||||
String get community_qrTitle => 'Condividi Comunità ';
|
||||
|
||||
@override
|
||||
String community_qrInstructions(String name) {
|
||||
@@ -2671,16 +2664,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get community_invalidQrCode => 'Codice QR della community non valido';
|
||||
|
||||
@override
|
||||
String get community_alreadyMember => 'Già membro';
|
||||
String get community_alreadyMember => 'Già membro';
|
||||
|
||||
@override
|
||||
String community_alreadyMemberMessage(String name) {
|
||||
return 'Sei già un membro di \"$name\".';
|
||||
return 'Sei già un membro di \"$name\".';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_addPublicChannel =>
|
||||
'Aggiungi Canale Pubblico della Comunità';
|
||||
'Aggiungi Canale Pubblico della Comunità ';
|
||||
|
||||
@override
|
||||
String get community_addPublicChannelHint =>
|
||||
@@ -2694,10 +2687,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Scansiona un codice QR o crea una community per iniziare.';
|
||||
|
||||
@override
|
||||
String get community_manageCommunities => 'Gestisci Comunità';
|
||||
String get community_manageCommunities => 'Gestisci Comunità ';
|
||||
|
||||
@override
|
||||
String get community_delete => 'Lascia la Comunità';
|
||||
String get community_delete => 'Lascia la Comunità ';
|
||||
|
||||
@override
|
||||
String community_deleteConfirm(String name) {
|
||||
@@ -2706,12 +2699,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String community_deleteChannelsWarning(int count) {
|
||||
return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
|
||||
return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_deleted(String name) {
|
||||
return 'Hai lasciato la comunità \"$name\"';
|
||||
return 'Hai lasciato la comunità \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2751,21 +2744,21 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Aggiungi un canale con hashtag per questa community';
|
||||
|
||||
@override
|
||||
String get community_selectCommunity => 'Seleziona Comunità';
|
||||
String get community_selectCommunity => 'Seleziona Comunità ';
|
||||
|
||||
@override
|
||||
String get community_regularHashtag => 'Hashtag regolare';
|
||||
|
||||
@override
|
||||
String get community_regularHashtagDesc =>
|
||||
'Hashtag pubblico (chiunque può unirsi)';
|
||||
'Hashtag pubblico (chiunque può unirsi)';
|
||||
|
||||
@override
|
||||
String get community_communityHashtag => 'Hashtag della Comunità';
|
||||
String get community_communityHashtag => 'Hashtag della Comunità ';
|
||||
|
||||
@override
|
||||
String get community_communityHashtagDesc =>
|
||||
'Visibile solo ai membri della comunità';
|
||||
'Visibile solo ai membri della comunità ';
|
||||
|
||||
@override
|
||||
String community_forCommunity(String name) {
|
||||
@@ -2832,7 +2825,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Uno o più dei luppoli mancano di una posizione!';
|
||||
'Uno o più dei luppoli mancano di una posizione!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
||||
@@ -2854,7 +2847,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Eseguire LOS per visualizzare il profilo altimetrico';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menù LOS';
|
||||
String get losMenuTitle => 'Menù LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
@@ -2926,7 +2919,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Dati di elevazione non disponibili per uno o più campioni.';
|
||||
'Dati di elevazione non disponibili per uno o più campioni.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
@@ -2964,7 +2957,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
|
||||
String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
@@ -3004,13 +2997,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
|
||||
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Dati di contatto non validi';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Il contatto è stato importato.';
|
||||
String get contacts_contactImported => 'Il contatto è stato importato.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
@@ -3052,7 +3045,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Copia dell\'annuncio nella Clipboard non riuscita.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Attività MeshCore';
|
||||
String get notification_activityTitle => 'Attività MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
@@ -3130,7 +3123,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError =>
|
||||
'Si è verificato un errore durante l\'esportazione.';
|
||||
'Si è verificato un errore durante l\'esportazione.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
|
||||
@@ -69,7 +69,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get common_share => 'Delen';
|
||||
|
||||
@override
|
||||
String get common_copy => 'Kopiëren';
|
||||
String get common_copy => 'Kopiëren';
|
||||
|
||||
@override
|
||||
String get common_retry => 'Nogmaals proberen';
|
||||
@@ -93,7 +93,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get common_loading => 'Laden...';
|
||||
|
||||
@override
|
||||
String get common_notAvailable => '—';
|
||||
String get common_notAvailable => '—';
|
||||
|
||||
@override
|
||||
String common_voltageValue(String volts) {
|
||||
@@ -108,13 +108,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTitle => 'Kies uw verbindingsmethode';
|
||||
|
||||
@override
|
||||
String get connectionChoiceSubtitle =>
|
||||
'Kies hoe u uw MeshCore-apparaat wilt bereiken.';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@@ -126,7 +119,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
|
||||
'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Selecteer een USB-apparaat';
|
||||
@@ -238,7 +231,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_location => 'Locatie';
|
||||
|
||||
@override
|
||||
String get settings_locationSubtitle => 'GPS coördinaten';
|
||||
String get settings_locationSubtitle => 'GPS coördinaten';
|
||||
|
||||
@override
|
||||
String get settings_locationUpdated => 'Locatie bijgewerkt';
|
||||
@@ -457,10 +450,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_languageEn => 'English';
|
||||
|
||||
@override
|
||||
String get appSettings_languageFr => 'Français';
|
||||
String get appSettings_languageFr => 'Français';
|
||||
|
||||
@override
|
||||
String get appSettings_languageEs => 'Español';
|
||||
String get appSettings_languageEs => 'Español';
|
||||
|
||||
@override
|
||||
String get appSettings_languageDe => 'Deutsch';
|
||||
@@ -469,16 +462,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_languagePl => 'Polski';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSl => 'Slovenščina';
|
||||
String get appSettings_languageSl => 'SlovenÅ¡Äina';
|
||||
|
||||
@override
|
||||
String get appSettings_languagePt => 'Português';
|
||||
String get appSettings_languagePt => 'Português';
|
||||
|
||||
@override
|
||||
String get appSettings_languageIt => 'Italiano';
|
||||
|
||||
@override
|
||||
String get appSettings_languageZh => '中文';
|
||||
String get appSettings_languageZh => '䏿–‡';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSv => 'Svenska';
|
||||
@@ -487,16 +480,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_languageNl => 'Nederlands';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSk => 'Slovenčina';
|
||||
String get appSettings_languageSk => 'SlovenÄina';
|
||||
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
String get appSettings_languageBg => 'БългарÑки';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Russisch';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Oekraïens';
|
||||
String get appSettings_languageUk => 'Oekraïens';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
|
||||
@@ -854,7 +847,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get channels_public => 'Openbaar';
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privé';
|
||||
String get channels_private => 'Privé';
|
||||
|
||||
@override
|
||||
String get channels_publicChannel => 'Open kanaal';
|
||||
@@ -954,14 +947,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get channels_sortUnread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
|
||||
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Beveiligd met een geheime sleutel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
|
||||
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
@@ -1343,7 +1336,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get map_nodesNeedGps =>
|
||||
'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen';
|
||||
'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen';
|
||||
|
||||
@override
|
||||
String map_nodesCount(int count) {
|
||||
@@ -1371,7 +1364,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get map_pinDm => 'Verzenden als bericht (DM)';
|
||||
|
||||
@override
|
||||
String get map_pinPrivate => 'Beveiligd (Privé)';
|
||||
String get map_pinPrivate => 'Beveiligd (Privé)';
|
||||
|
||||
@override
|
||||
String get map_pinPublic => 'Openbaar spikken';
|
||||
@@ -2038,7 +2031,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_eraseSerialOnly =>
|
||||
'Verwijderen is alleen beschikbaar via de seriële console.';
|
||||
'Verwijderen is alleen beschikbaar via de seriële console.';
|
||||
|
||||
@override
|
||||
String repeater_commandSent(String command) {
|
||||
@@ -2266,7 +2259,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetBridgeBaud =>
|
||||
'Stel de seriële link baudrate in voor rs232 bruggen.';
|
||||
'Stel de seriële link baudrate in voor rs232 bruggen.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetBridgeSecret =>
|
||||
@@ -2282,7 +2275,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetPerm =>
|
||||
'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)';
|
||||
'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetBridgeType =>
|
||||
@@ -2314,7 +2307,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionLoad =>
|
||||
'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.';
|
||||
'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionGet =>
|
||||
@@ -2359,7 +2352,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGpsSetLoc =>
|
||||
'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.';
|
||||
'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGpsAdvert =>
|
||||
@@ -2397,14 +2390,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_regionNote =>
|
||||
'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.';
|
||||
'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.';
|
||||
|
||||
@override
|
||||
String get repeater_gpsManagement => 'Beheer GPS';
|
||||
|
||||
@override
|
||||
String get repeater_gpsNote =>
|
||||
'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.';
|
||||
'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.';
|
||||
|
||||
@override
|
||||
String get telemetry_receivedData => 'Ontvangen Telemetriedata';
|
||||
@@ -2457,7 +2450,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String telemetry_temperatureValue(String celsius, String fahrenheit) {
|
||||
return '$celsius°C / $fahrenheit°F';
|
||||
return '$celsius°C / $fahrenheit°F';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2526,7 +2519,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channelPath_observedPathTitle(int index, String hops) {
|
||||
return 'Waargenomen pad $index • $hops';
|
||||
return 'Waargenomen pad $index • $hops';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2581,7 +2574,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String channelPath_selectedPathLabel(String label, String prefixes) {
|
||||
return '$label • $prefixes';
|
||||
return '$label • $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2998,11 +2991,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Contact is geïmporteerd.';
|
||||
String get contacts_contactImported => 'Contact is geïmporteerd.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Contact kon niet geïmporteerd worden.';
|
||||
'Contact kon niet geïmporteerd worden.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Zero Hop Reclame';
|
||||
@@ -3011,14 +3004,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get contacts_floodAdvert => 'Overstromingsadvertentie';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
|
||||
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
'Contact uit klembord toevoegen';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
|
||||
@@ -3037,7 +3030,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Kopiëren van advertentie naar Clipboard is mislukt.';
|
||||
'Kopiëren van advertentie naar Clipboard is mislukt.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'MeshCore Activiteit';
|
||||
@@ -3106,7 +3099,8 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Exporteert alle contacten met een locatie naar een GPX-bestand.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.';
|
||||
String get settings_gpxExportSuccess =>
|
||||
'Succesvol GPX-bestand geëxporteerd.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.';
|
||||
@@ -3130,7 +3124,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Kaartgegevens geëxporteerd uit meshcore-open';
|
||||
'Kaartgegevens geëxporteerd uit meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
|
||||
+618
-609
File diff suppressed because it is too large
Load Diff
+398
-392
File diff suppressed because it is too large
Load Diff
+1060
-893
File diff suppressed because it is too large
Load Diff
+754
-738
File diff suppressed because it is too large
Load Diff
+401
-400
File diff suppressed because it is too large
Load Diff
+460
-454
File diff suppressed because it is too large
Load Diff
+1054
-891
File diff suppressed because it is too large
Load Diff
+981
-868
File diff suppressed because it is too large
Load Diff
+35
-37
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"common_create": "Maak",
|
||||
"common_continue": "Doorgaan",
|
||||
"common_share": "Delen",
|
||||
"common_copy": "Kopiëren",
|
||||
"common_copy": "Kopiëren",
|
||||
"common_retry": "Nogmaals proberen",
|
||||
"common_hide": "Verbergen",
|
||||
"common_remove": "Verwijderen",
|
||||
@@ -35,7 +35,7 @@
|
||||
"common_disable": "Uitschakelen",
|
||||
"common_reboot": "Herstarten",
|
||||
"common_loading": "Laden...",
|
||||
"common_notAvailable": "—",
|
||||
"common_notAvailable": "—",
|
||||
"common_voltageValue": "{volts} V",
|
||||
"@common_voltageValue": {
|
||||
"placeholders": {
|
||||
@@ -92,7 +92,7 @@
|
||||
"settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor",
|
||||
"settings_radioSettingsUpdated": "Radio instellingen bijgewerkt",
|
||||
"settings_location": "Locatie",
|
||||
"settings_locationSubtitle": "GPS coördinaten",
|
||||
"settings_locationSubtitle": "GPS coördinaten",
|
||||
"settings_locationUpdated": "Locatie bijgewerkt",
|
||||
"settings_locationBothRequired": "Voer zowel breedte- als lengtegraad in.",
|
||||
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
|
||||
@@ -165,18 +165,18 @@
|
||||
"appSettings_language": "Taal",
|
||||
"appSettings_languageSystem": "Standaardinstelling",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
"appSettings_languageDe": "Deutsch",
|
||||
"appSettings_languagePl": "Polski",
|
||||
"appSettings_languageSl": "Slovenščina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageSl": "SlovenÅ¡Äina",
|
||||
"appSettings_languagePt": "Português",
|
||||
"appSettings_languageIt": "Italiano",
|
||||
"appSettings_languageZh": "中文",
|
||||
"appSettings_languageZh": "䏿–‡",
|
||||
"appSettings_languageSv": "Svenska",
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageSk": "SlovenÄina",
|
||||
"appSettings_languageBg": "БългарÑки",
|
||||
"appSettings_notifications": "Notificaties",
|
||||
"appSettings_enableNotifications": "Notificaties inschakelen",
|
||||
"appSettings_enableNotificationsSubtitle": "Ontvang meldingen voor berichten en advertenties",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"channels_hashtagChannel": "Hashtag kanaal",
|
||||
"channels_public": "Openbaar",
|
||||
"channels_private": "Privé",
|
||||
"channels_private": "Privé",
|
||||
"channels_publicChannel": "Open kanaal",
|
||||
"channels_privateChannel": "Private kanaal",
|
||||
"channels_editChannel": "Kanaal bewerken",
|
||||
@@ -623,7 +623,7 @@
|
||||
"chat_invalidLink": "Ongeldig linkformaat",
|
||||
"map_title": "Node Map",
|
||||
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
|
||||
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
|
||||
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
|
||||
"map_nodesCount": "Nodes: {count}",
|
||||
"@map_nodesCount": {
|
||||
"placeholders": {
|
||||
@@ -645,7 +645,7 @@
|
||||
"map_room": "Ruimte",
|
||||
"map_sensor": "Sensor",
|
||||
"map_pinDm": "Verzenden als bericht (DM)",
|
||||
"map_pinPrivate": "Beveiligd (Privé)",
|
||||
"map_pinPrivate": "Beveiligd (Privé)",
|
||||
"map_pinPublic": "Openbaar spikken",
|
||||
"map_lastSeen": "Laaste keer gezien",
|
||||
"map_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?",
|
||||
@@ -1039,7 +1039,7 @@
|
||||
"repeater_eraseFileSystem": "Verwijder Besturingssysteem",
|
||||
"repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater",
|
||||
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!",
|
||||
"repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.",
|
||||
"repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.",
|
||||
"repeater_commandSent": "Commando verzonden: {command}",
|
||||
"@repeater_commandSent": {
|
||||
"placeholders": {
|
||||
@@ -1143,11 +1143,11 @@
|
||||
"repeater_cliHelpSetBridgeEnabled": "Poort inschakelen/uitschakelen.",
|
||||
"repeater_cliHelpSetBridgeDelay": "Verzend vertraging instellen voor pakketten.",
|
||||
"repeater_cliHelpSetBridgeSource": "Kies of de brug ontvangen pakketten of verzonden pakketten opnieuw moet versturen.",
|
||||
"repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.",
|
||||
"repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.",
|
||||
"repeater_cliHelpSetBridgeSecret": "Stel bridge-geheim in voor espnow bridges.",
|
||||
"repeater_cliHelpSetAdcMultiplier": "Stelt een aangepaste factor in om de gerapporteerde batterijspanning aan te passen (alleen ondersteund op selecte borden).",
|
||||
"repeater_cliHelpTempRadio": "Stelt tijdelijke radio parameters in voor het opgegeven aantal minuten, en keert daarna terug naar de originele radio parameters. (wordt niet opgeslagen in de voorkeuren).",
|
||||
"repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)",
|
||||
"repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)",
|
||||
"repeater_cliHelpGetBridgeType": "Ontvang brugtype: geen, rs232, espnow",
|
||||
"repeater_cliHelpLogStart": "Start pakketlogging naar het bestandssysteem.",
|
||||
"repeater_cliHelpLogStop": "Stoppen met het loggen van pakketten naar het bestandssysteem.",
|
||||
@@ -1155,7 +1155,7 @@
|
||||
"repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4",
|
||||
"repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.",
|
||||
"repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.",
|
||||
"repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.",
|
||||
"repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.",
|
||||
"repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"",
|
||||
"repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.",
|
||||
"repeater_cliHelpRegionRemove": "Verwijdert een regio-definitie met de gegeven naam. (moet exact overeenkomen en geen kindregio's hebben)",
|
||||
@@ -1167,7 +1167,7 @@
|
||||
"repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.",
|
||||
"repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.",
|
||||
"repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.",
|
||||
"repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.",
|
||||
"repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.",
|
||||
"repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen",
|
||||
"repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.",
|
||||
"repeater_commandsListTitle": "Commandenlijst",
|
||||
@@ -1178,9 +1178,9 @@
|
||||
"repeater_logging": "Logging",
|
||||
"repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)",
|
||||
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)",
|
||||
"repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.",
|
||||
"repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.",
|
||||
"repeater_gpsManagement": "Beheer GPS",
|
||||
"repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.",
|
||||
"repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.",
|
||||
"telemetry_receivedData": "Ontvangen Telemetriedata",
|
||||
"telemetry_requestTimeout": "Telemetryverzoek is uitgevallen.",
|
||||
"telemetry_errorLoading": "Fout bij het laden van de telemetrie: {error}",
|
||||
@@ -1232,7 +1232,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
|
||||
"@telemetry_temperatureValue": {
|
||||
"placeholders": {
|
||||
"celsius": {
|
||||
@@ -1254,7 +1254,7 @@
|
||||
"channelPath_repeatsLabel": "Repeats",
|
||||
"channelPath_pathLabel": "Pad {index}",
|
||||
"channelPath_observedLabel": "Waargenomen",
|
||||
"channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
|
||||
"channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
|
||||
"@channelPath_observedPathTitle": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
@@ -1329,7 +1329,7 @@
|
||||
},
|
||||
"channelPath_pathLabelTitle": "Pad",
|
||||
"channelPath_observedPathHeader": "Waargenomen Pad",
|
||||
"channelPath_selectedPathLabel": "{label} • {prefixes}",
|
||||
"channelPath_selectedPathLabel": "{label} • {prefixes}",
|
||||
"@channelPath_selectedPathLabel": {
|
||||
"placeholders": {
|
||||
"label": {
|
||||
@@ -1369,8 +1369,8 @@
|
||||
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
||||
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
|
||||
"channels_joinPublicChannel": "Sluit het Open Kanaal",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
|
||||
@@ -1558,22 +1558,22 @@
|
||||
"contacts_roomPing": "Ping kamer server",
|
||||
"contacts_chatTraceRoute": "Route traceren",
|
||||
"contacts_pathTraceTo": "Trace route to {name}",
|
||||
"appSettings_languageUk": "Oekraïens",
|
||||
"appSettings_languageUk": "Oekraïens",
|
||||
"contacts_invalidAdvertFormat": "Ongeldige contactgegevens",
|
||||
"contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.",
|
||||
"contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.",
|
||||
"contacts_zeroHopAdvert": "Zero Hop Reclame",
|
||||
"contacts_floodAdvert": "Overstromingsadvertentie",
|
||||
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||
"appSettings_languageRu": "Russisch",
|
||||
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
|
||||
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
|
||||
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
||||
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
||||
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||
"notification_activityTitle": "MeshCore Activiteit",
|
||||
@@ -1584,7 +1584,7 @@
|
||||
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
|
||||
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
|
||||
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
|
||||
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
|
||||
"settings_gpxExportNoContacts": "Geen contacten om te exporteren.",
|
||||
"settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem",
|
||||
"settings_gpxExportError": "Er was een fout bij het exporteren.",
|
||||
@@ -1595,7 +1595,7 @@
|
||||
"settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties",
|
||||
"settings_gpxExportChat": "Locaties van metgezellen",
|
||||
"settings_gpxExportAllContacts": "Alle contactlocaties",
|
||||
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
|
||||
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
|
||||
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
|
||||
"map_removeLast": "Verwijder Laatste",
|
||||
@@ -1802,11 +1802,9 @@
|
||||
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
|
||||
"connectionChoiceTitle": "Kies uw verbindingsmethode",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
|
||||
"usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
|
||||
"usbScreenStatus": "Selecteer een USB-apparaat",
|
||||
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
|
||||
"usbScreenTitle": "Verbind via USB",
|
||||
|
||||
+594
-596
File diff suppressed because it is too large
Load Diff
+385
-387
File diff suppressed because it is too large
Load Diff
+868
-870
File diff suppressed because it is too large
Load Diff
+726
-728
File diff suppressed because it is too large
Load Diff
+385
-387
File diff suppressed because it is too large
Load Diff
+448
-450
File diff suppressed because it is too large
Load Diff
+861
-863
File diff suppressed because it is too large
Load Diff
+865
-867
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart';
|
||||
import 'utils/platform_info.dart';
|
||||
|
||||
import 'connector/meshcore_connector.dart';
|
||||
import 'screens/connection_choice_screen.dart';
|
||||
import 'screens/scanner_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/message_retry_service.dart';
|
||||
import 'services/path_history_service.dart';
|
||||
@@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
},
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
: const ConnectionChoiceScreen(),
|
||||
: const ScannerScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import 'scanner_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
/// Entry point that lets the user choose between USB or Bluetooth.
|
||||
class ConnectionChoiceScreen extends StatelessWidget {
|
||||
const ConnectionChoiceScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(l10n.appTitle, textAlign: TextAlign.center),
|
||||
),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableHeight = constraints.maxHeight.isFinite
|
||||
? constraints.maxHeight
|
||||
: 600.0;
|
||||
final gap = math.max(
|
||||
8.0,
|
||||
math.min(20.0, availableHeight * 0.035),
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
l10n.connectionChoiceTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: math.max(4.0, gap * 0.5)),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
l10n.connectionChoiceSubtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: gap),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _ConnectionMethodButton(
|
||||
icon: Icons.usb,
|
||||
label: l10n.connectionChoiceUsbLabel,
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||
onPressed: usbSupported
|
||||
? () {
|
||||
debugPrint(
|
||||
'ConnectionChoiceScreen: USB selected, opening UsbScreen',
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const UsbScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
SizedBox(height: gap),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _ConnectionMethodButton(
|
||||
icon: Icons.bluetooth,
|
||||
label: l10n.connectionChoiceBluetoothLabel,
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
onPressed: () {
|
||||
debugPrint(
|
||||
'ConnectionChoiceScreen: Bluetooth selected, opening ScannerScreen',
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ScannerScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConnectionMethodButton extends StatelessWidget {
|
||||
const _ConnectionMethodButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
required this.color,
|
||||
required this.iconColor,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final Color color;
|
||||
final Color iconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
minimumSize: const Size.fromHeight(0),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableHeight = constraints.maxHeight.isFinite
|
||||
? constraints.maxHeight
|
||||
: 200.0;
|
||||
final availableWidth = constraints.maxWidth.isFinite
|
||||
? constraints.maxWidth
|
||||
: 320.0;
|
||||
final isCompact = availableHeight < 72.0 || availableWidth < 180.0;
|
||||
final useTightVertical = !isCompact && availableHeight < 120.0;
|
||||
final baseGap = isCompact
|
||||
? 8.0
|
||||
: (useTightVertical
|
||||
? math.max(4.0, math.min(8.0, availableHeight * 0.06))
|
||||
: 12.0);
|
||||
final labelStyle =
|
||||
(isCompact
|
||||
? theme.textTheme.titleMedium
|
||||
: (useTightVertical
|
||||
? theme.textTheme.titleMedium
|
||||
: theme.textTheme.titleLarge))
|
||||
?.copyWith(fontWeight: FontWeight.w600);
|
||||
final verticalIconSize = useTightVertical
|
||||
? math.max(32.0, math.min(48.0, availableHeight * 0.42))
|
||||
: 60.0;
|
||||
final content = isCompact
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 24.0, color: iconColor),
|
||||
SizedBox(width: baseGap),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: labelStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: verticalIconSize, color: iconColor),
|
||||
SizedBox(height: baseGap),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
style: labelStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.max(0, availableWidth - 12),
|
||||
maxHeight: math.max(0, availableHeight - 12),
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../l10n/l10n.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
/// Screen for scanning and connecting to MeshCore devices
|
||||
class ScannerScreen extends StatefulWidget {
|
||||
@@ -114,40 +115,67 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isScanning =
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
unawaited(
|
||||
connector.startScan().catchError((e) {
|
||||
debugPrint("Scanner screen startScan error: $e");
|
||||
}),
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (usbSupported)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
debugPrint(
|
||||
'ScannerScreen: USB selected, opening UsbScreen',
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning
|
||||
? context.l10n.scanner_stop
|
||||
: context.l10n.scanner_scan,
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'scanner_usb_action',
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
if (usbSupported) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
heroTag: 'scanner_ble_action',
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
unawaited(
|
||||
connector.startScan().catchError((e) {
|
||||
debugPrint(
|
||||
"Scanner screen startScan error: $e",
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning
|
||||
? context.l10n.scanner_stop
|
||||
: context.l10n.scanner_scan,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+38
-16
@@ -5,10 +5,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_connector_usb.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../utils/usb_port_labels.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
|
||||
class UsbScreen extends StatefulWidget {
|
||||
const UsbScreen({super.key});
|
||||
@@ -28,6 +30,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
String? _errorText;
|
||||
Timer? _hotPlugTimer;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final MeshCoreConnectorUsb _usbConnector;
|
||||
late final VoidCallback _connectionListener;
|
||||
|
||||
/// Whether the current platform supports dynamic hot-plug polling.
|
||||
@@ -40,12 +43,13 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
_usbConnector = MeshCoreConnectorUsb(_connector);
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
final activeUsbPortDisplayLabel = _connector.activeUsbPortDisplayLabel;
|
||||
final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel;
|
||||
final shouldUpdateDisplayLabel =
|
||||
activeUsbPortDisplayLabel != _connectedPortDisplayLabel;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
if (_usbConnector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
setState(() {
|
||||
_isConnecting = false;
|
||||
@@ -56,8 +60,8 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
_connectedPortDisplayLabel = activeUsbPortDisplayLabel;
|
||||
});
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isUsbTransportConnected &&
|
||||
if (_usbConnector.state == MeshCoreConnectionState.connected &&
|
||||
_usbConnector.isUsbTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
_navigatedToContacts = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
@@ -65,14 +69,14 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
);
|
||||
}
|
||||
};
|
||||
_connector.addListener(_connectionListener);
|
||||
_usbConnector.addListener(_connectionListener);
|
||||
_startHotPlugTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
if (!_didScheduleInitialLoad) {
|
||||
_didScheduleInitialLoad = true;
|
||||
unawaited(_loadPorts());
|
||||
@@ -83,12 +87,12 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
void dispose() {
|
||||
_hotPlugTimer?.cancel();
|
||||
_hotPlugTimer = null;
|
||||
_connector.removeListener(_connectionListener);
|
||||
_usbConnector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
_usbConnector.activeTransport == MeshCoreTransportType.usb &&
|
||||
_usbConnector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
unawaited(_usbConnector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
@@ -113,6 +117,23 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
if (PlatformInfo.isWeb ||
|
||||
PlatformInfo.isAndroid ||
|
||||
PlatformInfo.isIOS)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
debugPrint(
|
||||
'UsbScreen: Bluetooth selected, opening ScannerScreen',
|
||||
);
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ScannerScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
@@ -376,7 +397,8 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
final isSelected = port == _selectedPort;
|
||||
final displayName = _friendlyPortName(port);
|
||||
final rawName = normalizeUsbPortName(port);
|
||||
final showRawName = rawName != displayName;
|
||||
final showRawName =
|
||||
rawName != displayName && !rawName.startsWith('web:');
|
||||
return Material(
|
||||
color: isSelected
|
||||
? theme.colorScheme.primaryContainer
|
||||
@@ -433,7 +455,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
|
||||
Future<void> _loadPorts() async {
|
||||
if (!mounted) return;
|
||||
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
|
||||
setState(() {
|
||||
_isLoadingPorts = true;
|
||||
@@ -441,7 +463,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
});
|
||||
|
||||
try {
|
||||
final ports = await _connector.listUsbPorts();
|
||||
final ports = await _usbConnector.listPorts();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_ports
|
||||
@@ -470,8 +492,8 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
if (selectedPort == null || selectedPort.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
if (_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
if (_usbConnector.state != MeshCoreConnectionState.disconnected) {
|
||||
setState(() {
|
||||
_isConnecting = false;
|
||||
_errorText = null;
|
||||
@@ -486,7 +508,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
});
|
||||
|
||||
try {
|
||||
await _connector.connectUsb(portName: rawPortName);
|
||||
await _usbConnector.connect(portName: rawPortName);
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint(
|
||||
'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace',
|
||||
|
||||
@@ -280,6 +280,11 @@ class UsbSerialService {
|
||||
Future<void> disconnect() async {
|
||||
if (_status == UsbSerialStatus.disconnected) return;
|
||||
|
||||
final portLabel = _connectedPortLabel ?? _connectedPortKey;
|
||||
_debugLogService?.info(
|
||||
'USB disconnect starting port=${portLabel ?? 'unknown'}',
|
||||
tag: 'USB Serial',
|
||||
);
|
||||
_status = UsbSerialStatus.disconnecting;
|
||||
_connectedPortKey = null;
|
||||
_connectedPortLabel = null;
|
||||
@@ -319,6 +324,10 @@ class UsbSerialService {
|
||||
_dataSubscription = null;
|
||||
}
|
||||
_status = UsbSerialStatus.disconnected;
|
||||
_debugLogService?.info(
|
||||
'USB disconnect complete port=${portLabel ?? 'unknown'}',
|
||||
tag: 'USB Serial',
|
||||
);
|
||||
}
|
||||
|
||||
void setRequestPortLabel(String label) {
|
||||
|
||||
@@ -16,6 +16,10 @@ class UsbSerialService {
|
||||
'2886:1667': 'Seeed Wio Tracker L1',
|
||||
};
|
||||
static final Map<String, String> _deviceNamesByPortKey = <String, String>{};
|
||||
static final Map<String, String> _baseLabelsByPortKey = <String, String>{};
|
||||
static final Map<String, JSObject> _authorizedPortsByKey =
|
||||
<String, JSObject>{};
|
||||
static int _nextAuthorizedPortId = 1;
|
||||
|
||||
final StreamController<Uint8List> _frameController =
|
||||
StreamController<Uint8List>.broadcast();
|
||||
@@ -51,11 +55,12 @@ class UsbSerialService {
|
||||
return const <String>[];
|
||||
}
|
||||
|
||||
_resetPortCache();
|
||||
final ports = await _getAuthorizedPorts();
|
||||
if (ports.isEmpty) {
|
||||
return <String>[_requestPortLabel];
|
||||
return <String>[_requestPortListEntry];
|
||||
}
|
||||
return ports.map(_displayLabelForPort).toList(growable: false);
|
||||
return ports.map(_listEntryForPort).toList(growable: false);
|
||||
}
|
||||
|
||||
Future<void> connect({
|
||||
@@ -75,8 +80,12 @@ class UsbSerialService {
|
||||
|
||||
try {
|
||||
final requestedPortName = normalizeUsbPortName(portName);
|
||||
final selectedPortKey = requestedPortName.startsWith('web:port:')
|
||||
? requestedPortName
|
||||
: null;
|
||||
_port = _authorizedPortsByKey[requestedPortName];
|
||||
final authorizedPorts = await _getAuthorizedPorts();
|
||||
_port = _selectPort(authorizedPorts, requestedPortName);
|
||||
_port ??= _selectPort(authorizedPorts, requestedPortName);
|
||||
|
||||
_port ??= await _requestPort();
|
||||
if (_port == null) {
|
||||
@@ -84,8 +93,11 @@ class UsbSerialService {
|
||||
}
|
||||
|
||||
await _openPort(_port!, baudRate);
|
||||
_connectedPortKey = _portKeyFor(_port!);
|
||||
_connectedPortName = _buildDisplayLabel(_connectedPortKey!);
|
||||
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
|
||||
_connectedPortName = _displayLabelForPort(
|
||||
_port!,
|
||||
portKey: _connectedPortKey,
|
||||
);
|
||||
_writer = _getWriter(_port!);
|
||||
_reader = _getReader(_port!);
|
||||
_status = UsbSerialStatus.connected;
|
||||
@@ -122,6 +134,11 @@ class UsbSerialService {
|
||||
Future<void> disconnect() async {
|
||||
if (_status == UsbSerialStatus.disconnected) return;
|
||||
|
||||
final portLabel = _connectedPortName ?? _connectedPortKey;
|
||||
_debugLogService?.info(
|
||||
'USB disconnect starting port=${portLabel ?? 'unknown'}',
|
||||
tag: 'USB Serial',
|
||||
);
|
||||
_status = UsbSerialStatus.disconnecting;
|
||||
final reader = _reader;
|
||||
final writer = _writer;
|
||||
@@ -156,6 +173,10 @@ class UsbSerialService {
|
||||
}
|
||||
|
||||
_status = UsbSerialStatus.disconnected;
|
||||
_debugLogService?.info(
|
||||
'USB disconnect complete port=${portLabel ?? 'unknown'}',
|
||||
tag: 'USB Serial',
|
||||
);
|
||||
}
|
||||
|
||||
void updateConnectedLabel(String label) {
|
||||
@@ -210,9 +231,12 @@ class UsbSerialService {
|
||||
if (ports.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) {
|
||||
if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) {
|
||||
return ports.first;
|
||||
}
|
||||
if (requestedPortName.startsWith('web:port:')) {
|
||||
return null;
|
||||
}
|
||||
for (final port in ports) {
|
||||
final description = _describePort(port);
|
||||
if (description == requestedPortName) {
|
||||
@@ -368,10 +392,29 @@ class UsbSerialService {
|
||||
}
|
||||
|
||||
String _describePort(JSObject port) {
|
||||
final info = _portInfo(port);
|
||||
if (info == null) {
|
||||
return _requestPortLabel;
|
||||
}
|
||||
|
||||
final vendorId = info.usbVendorId;
|
||||
final productId = info.usbProductId;
|
||||
final hasVendor = vendorId != null;
|
||||
final hasProduct = productId != null;
|
||||
|
||||
return describeWebUsbPort(
|
||||
vendorId: hasVendor ? vendorId : null,
|
||||
productId: hasProduct ? productId : null,
|
||||
requestPortLabel: _requestPortLabel,
|
||||
knownUsbNames: _knownUsbNames,
|
||||
);
|
||||
}
|
||||
|
||||
_WebPortInfo? _portInfo(JSObject port) {
|
||||
try {
|
||||
final info = port.callMethod<JSAny?>('getInfo'.toJS);
|
||||
if (info == null) {
|
||||
return _requestPortLabel;
|
||||
return null;
|
||||
}
|
||||
final infoObject = info as JSObject;
|
||||
|
||||
@@ -381,32 +424,52 @@ class UsbSerialService {
|
||||
final productId = infoObject
|
||||
.getProperty<JSAny?>('usbProductId'.toJS)
|
||||
?.dartify();
|
||||
final hasVendor = vendorId is num;
|
||||
final hasProduct = productId is num;
|
||||
|
||||
return describeWebUsbPort(
|
||||
vendorId: hasVendor ? vendorId.toInt() : null,
|
||||
productId: hasProduct ? productId.toInt() : null,
|
||||
requestPortLabel: _requestPortLabel,
|
||||
knownUsbNames: _knownUsbNames,
|
||||
return _WebPortInfo(
|
||||
usbVendorId: vendorId is num ? vendorId.toInt() : null,
|
||||
usbProductId: productId is num ? productId.toInt() : null,
|
||||
);
|
||||
} catch (_) {
|
||||
return _requestPortLabel;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _portKeyFor(JSObject port) => _describePort(port);
|
||||
String _portKeyFor(JSObject port) {
|
||||
return _cachePort(port);
|
||||
}
|
||||
|
||||
String _displayLabelForPort(JSObject port) =>
|
||||
_buildDisplayLabel(_portKeyFor(port));
|
||||
String _cachePort(JSObject port, {String? preferredKey}) {
|
||||
final portKey = preferredKey ?? 'web:port:${_nextAuthorizedPortId++}';
|
||||
_baseLabelsByPortKey[portKey] = _describePort(port);
|
||||
_authorizedPortsByKey[portKey] = port;
|
||||
return portKey;
|
||||
}
|
||||
|
||||
String _displayLabelForPort(JSObject port, {String? portKey}) =>
|
||||
_buildDisplayLabel(portKey ?? _portKeyFor(port));
|
||||
|
||||
String _buildDisplayLabel(String portKey) {
|
||||
return buildUsbDisplayLabel(
|
||||
basePortLabel: portKey,
|
||||
basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey,
|
||||
deviceName: _deviceNamesByPortKey[portKey],
|
||||
);
|
||||
}
|
||||
|
||||
String _listEntryForPort(JSObject port) {
|
||||
final portKey = _portKeyFor(port);
|
||||
return '$portKey - ${_displayLabelForPort(port, portKey: portKey)}';
|
||||
}
|
||||
|
||||
String get _requestPortKey => 'web:request';
|
||||
|
||||
String get _requestPortListEntry => '$_requestPortKey - $_requestPortLabel';
|
||||
|
||||
void _resetPortCache() {
|
||||
_authorizedPortsByKey.clear();
|
||||
_baseLabelsByPortKey.clear();
|
||||
_deviceNamesByPortKey.clear();
|
||||
_nextAuthorizedPortId = 1;
|
||||
}
|
||||
|
||||
void _releaseLock(JSObject resource) {
|
||||
try {
|
||||
resource.callMethod<JSAny?>('releaseLock'.toJS);
|
||||
@@ -462,3 +525,10 @@ class UsbSerialService {
|
||||
}
|
||||
|
||||
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
|
||||
|
||||
final class _WebPortInfo {
|
||||
const _WebPortInfo({required this.usbVendorId, required this.usbProductId});
|
||||
|
||||
final int? usbVendorId;
|
||||
final int? usbProductId;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'app_logger.dart';
|
||||
|
||||
/// Shows a confirmation dialog before disconnecting from the device.
|
||||
/// Returns true if user confirmed and disconnect completed, false otherwise.
|
||||
@@ -28,6 +29,7 @@ Future<bool> showDisconnectDialog(
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
|
||||
await connector.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/l10n/app_localizations.dart';
|
||||
import 'package:meshcore_open/screens/connection_choice_screen.dart';
|
||||
import 'package:meshcore_open/screens/scanner_screen.dart';
|
||||
import 'package:meshcore_open/screens/usb_screen.dart';
|
||||
import 'package:meshcore_open/utils/platform_info.dart';
|
||||
|
||||
@@ -131,7 +131,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('ConnectionChoiceScreen USB button reflects platform support', (
|
||||
testWidgets('ScannerScreen USB action reflects platform support', (
|
||||
tester,
|
||||
) async {
|
||||
final connector = _FakeMeshCoreConnector();
|
||||
@@ -139,19 +139,15 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
connector: connector,
|
||||
child: const ConnectionChoiceScreen(),
|
||||
child: const ScannerScreen(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final usbButton = tester.widget<ElevatedButton>(
|
||||
find.widgetWithText(ElevatedButton, 'USB'),
|
||||
);
|
||||
|
||||
if (PlatformInfo.supportsUsbSerial) {
|
||||
expect(usbButton.onPressed, isNotNull);
|
||||
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
|
||||
} else {
|
||||
expect(usbButton.onPressed, isNull);
|
||||
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user