mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Compare commits
10 commits
v2.7.14-in
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f21d8af9ae | ||
|
|
a90cb2d89e | ||
|
|
7492a33cf8 | ||
|
|
2b47da3b61 | ||
|
|
3322257cfd | ||
|
|
99e7407a90 | ||
|
|
9dd57725f2 | ||
|
|
2c1984ace5 | ||
|
|
94856d257f | ||
|
|
84fe24467f |
37 changed files with 488 additions and 94 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -55,3 +55,4 @@ wireless-install.sh
|
||||||
firebase-debug.log
|
firebase-debug.log
|
||||||
.agent_plans/
|
.agent_plans/
|
||||||
.agent_refs/
|
.agent_refs/
|
||||||
|
.agent_artifacts/
|
||||||
|
|
|
||||||
46
Gemfile.lock
46
Gemfile.lock
|
|
@ -3,13 +3,13 @@ GEM
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.8)
|
CFPropertyList (3.0.8)
|
||||||
abbrev (0.1.2)
|
abbrev (0.1.2)
|
||||||
addressable (2.8.8)
|
addressable (2.9.0)
|
||||||
public_suffix (>= 2.0.2, < 8.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1213.0)
|
aws-partitions (1.1240.0)
|
||||||
aws-sdk-core (3.242.0)
|
aws-sdk-core (3.245.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
|
@ -17,11 +17,11 @@ GEM
|
||||||
bigdecimal
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.121.0)
|
aws-sdk-kms (1.123.0)
|
||||||
aws-sdk-core (~> 3, >= 3.241.4)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.213.0)
|
aws-sdk-s3 (1.219.0)
|
||||||
aws-sdk-core (~> 3, >= 3.241.4)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.12.1)
|
||||||
|
|
@ -29,7 +29,7 @@ GEM
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
benchmark (0.5.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (4.0.1)
|
bigdecimal (4.1.2)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
|
|
@ -68,11 +68,11 @@ GEM
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.4)
|
||||||
faraday_middleware (1.2.1)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.4.0)
|
fastimage (2.4.1)
|
||||||
fastlane (2.232.2)
|
fastlane (2.233.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
abbrev (~> 0.1.2)
|
abbrev (~> 0.1.2)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
|
@ -92,7 +92,7 @@ GEM
|
||||||
faraday-cookie_jar (~> 0.0.6)
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
faraday_middleware (~> 1.0)
|
faraday_middleware (~> 1.0)
|
||||||
fastimage (>= 2.1.0, < 3.0.0)
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
fastlane-sirp (>= 1.0.0)
|
fastlane-sirp (>= 1.1.0)
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-apis-androidpublisher_v3 (~> 0.3)
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
|
@ -122,10 +122,9 @@ GEM
|
||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.4.1)
|
xcpretty (~> 0.4.1)
|
||||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-sirp (1.0.0)
|
fastlane-sirp (1.1.0)
|
||||||
sysrandom (~> 1.0)
|
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.95.0)
|
google-apis-androidpublisher_v3 (0.99.0)
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (0.18.0)
|
google-apis-core (0.18.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
|
@ -139,15 +138,15 @@ GEM
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.17.0)
|
google-apis-playcustomapp_v1 (0.17.0)
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.59.0)
|
google-apis-storage_v1 (0.61.0)
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-cloud-core (1.8.0)
|
google-cloud-core (1.8.0)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (2.1.1)
|
google-cloud-env (2.1.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.5.0)
|
google-cloud-errors (1.6.0)
|
||||||
google-cloud-storage (1.58.0)
|
google-cloud-storage (1.59.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-core (>= 0.18, < 2)
|
google-apis-core (>= 0.18, < 2)
|
||||||
|
|
@ -169,13 +168,13 @@ GEM
|
||||||
httpclient (2.9.0)
|
httpclient (2.9.0)
|
||||||
mutex_m
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.18.1)
|
json (2.19.4)
|
||||||
jwt (2.10.2)
|
jwt (2.10.2)
|
||||||
base64
|
base64
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
multi_json (1.19.1)
|
multi_json (1.20.1)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
nanaimo (0.4.0)
|
nanaimo (0.4.0)
|
||||||
|
|
@ -185,13 +184,13 @@ GEM
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
plist (3.7.2)
|
plist (3.7.2)
|
||||||
public_suffix (7.0.2)
|
public_suffix (7.0.5)
|
||||||
rake (13.3.1)
|
rake (13.4.2)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.4.1)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rouge (3.28.0)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
|
|
@ -205,7 +204,6 @@ GEM
|
||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
sysrandom (1.0.5)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
|
||||||
import org.meshtastic.app.ui.MainScreen
|
import org.meshtastic.app.ui.MainScreen
|
||||||
import org.meshtastic.core.barcode.rememberBarcodeScanner
|
import org.meshtastic.core.barcode.rememberBarcodeScanner
|
||||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||||
|
import org.meshtastic.core.network.repository.UsbRepository
|
||||||
import org.meshtastic.core.nfc.NfcScannerEffect
|
import org.meshtastic.core.nfc.NfcScannerEffect
|
||||||
import org.meshtastic.core.resources.Res
|
import org.meshtastic.core.resources.Res
|
||||||
import org.meshtastic.core.resources.channel_invalid
|
import org.meshtastic.core.resources.channel_invalid
|
||||||
|
|
@ -91,6 +92,8 @@ import org.meshtastic.feature.node.metrics.TracerouteMapScreen
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val model: UIViewModel by viewModel()
|
private val model: UIViewModel by viewModel()
|
||||||
|
|
||||||
|
private val usbRepository: UsbRepository by inject()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers
|
* Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers
|
||||||
* itself as a LifecycleObserver in its init block.
|
* itself as a LifecycleObserver in its init block.
|
||||||
|
|
@ -166,6 +169,16 @@ class MainActivity : ComponentActivity() {
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// Belt-and-suspenders for the Android 12+ attach-intent quirk: if the activity is
|
||||||
|
// resumed while a USB device is already attached (e.g. process restart, returning
|
||||||
|
// from another app), the manifest-declared attach intent may have already fired
|
||||||
|
// before UsbRepository was constructed. Re-poll deviceList here so the UI reflects
|
||||||
|
// reality without requiring the user to physically replug.
|
||||||
|
usbRepository.refreshState()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppCompositionLocals(content: @Composable () -> Unit) {
|
private fun AppCompositionLocals(content: @Composable () -> Unit) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
|
@ -257,6 +270,11 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||||
Logger.d { "USB device attached" }
|
Logger.d { "USB device attached" }
|
||||||
|
// Android 12+ delivers ACTION_USB_DEVICE_ATTACHED only to manifest-declared
|
||||||
|
// receivers, so the runtime-registered UsbBroadcastReceiver inside UsbRepository
|
||||||
|
// never sees this event. Forward it explicitly so the serialDevices StateFlow
|
||||||
|
// refreshes and the device shows up in the Connect → Serial tab.
|
||||||
|
usbRepository.refreshState()
|
||||||
showSettingsPage()
|
showSettingsPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ pluginManagement {
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.gradle.develocity") version("4.4.0")
|
id("com.gradle.develocity") version("4.4.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ import com.juul.kable.UnmetRequirementException
|
||||||
/**
|
/**
|
||||||
* Classification of a BLE-layer exception for the transport layer to act on.
|
* Classification of a BLE-layer exception for the transport layer to act on.
|
||||||
*
|
*
|
||||||
* @property isPermanent `true` if the condition won't resolve without user intervention (e.g. Bluetooth disabled).
|
* @property isPermanent `true` if the condition cannot resolve without explicit user re-selection of the device.
|
||||||
|
* Currently always `false` — all known BLE exceptions can resolve without user intervention (BT toggling, permission
|
||||||
|
* grants, transient GATT errors). Reserved for future use.
|
||||||
* @property gattStatus the platform GATT status code when available (Android-specific).
|
* @property gattStatus the platform GATT status code when available (Android-specific).
|
||||||
* @property message a human-readable description of the failure.
|
* @property message a human-readable description of the failure.
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,6 +52,9 @@ fun Throwable.classifyBleException(): BleExceptionInfo? = when (this) {
|
||||||
is GattRequestRejectedException ->
|
is GattRequestRejectedException ->
|
||||||
BleExceptionInfo(isPermanent = false, message = "GATT request rejected (busy)")
|
BleExceptionInfo(isPermanent = false, message = "GATT request rejected (busy)")
|
||||||
is UnmetRequirementException ->
|
is UnmetRequirementException ->
|
||||||
BleExceptionInfo(isPermanent = true, message = message ?: "Bluetooth LE unavailable")
|
// Bluetooth disabled or runtime permission missing. Both can resolve without re-selecting the
|
||||||
|
// device (user re-enables BT, or grants permission). Surface as transient so the transport keeps
|
||||||
|
// retrying; UI can show a hint based on the message.
|
||||||
|
BleExceptionInfo(isPermanent = false, message = message ?: "Bluetooth LE unavailable")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import org.meshtastic.proto.AdminMessage
|
||||||
import org.meshtastic.proto.Config
|
import org.meshtastic.proto.Config
|
||||||
import org.meshtastic.proto.Telemetry
|
import org.meshtastic.proto.Telemetry
|
||||||
import org.meshtastic.proto.ToRadio
|
import org.meshtastic.proto.ToRadio
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
@ -211,11 +212,11 @@ class MeshConnectionManagerImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startHandshakeStallGuard(stage: Int, action: () -> Unit) {
|
private fun startHandshakeStallGuard(stage: Int, timeout: Duration, action: () -> Unit) {
|
||||||
handshakeTimeout?.cancel()
|
handshakeTimeout?.cancel()
|
||||||
handshakeTimeout =
|
handshakeTimeout =
|
||||||
scope.handledLaunch {
|
scope.handledLaunch {
|
||||||
delay(HANDSHAKE_TIMEOUT)
|
delay(timeout)
|
||||||
if (serviceRepository.connectionState.value is ConnectionState.Connecting) {
|
if (serviceRepository.connectionState.value is ConnectionState.Connecting) {
|
||||||
// Attempt one retry. Note: the firmware silently drops identical consecutive
|
// Attempt one retry. Note: the firmware silently drops identical consecutive
|
||||||
// writes (per-connection dedup). If the first want_config_id was received and
|
// writes (per-connection dedup). If the first want_config_id was received and
|
||||||
|
|
@ -291,13 +292,13 @@ class MeshConnectionManagerImpl(
|
||||||
|
|
||||||
override fun startConfigOnly() {
|
override fun startConfigOnly() {
|
||||||
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) }
|
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) }
|
||||||
startHandshakeStallGuard(1, action)
|
startHandshakeStallGuard(1, HANDSHAKE_TIMEOUT_STAGE1, action)
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startNodeInfoOnly() {
|
override fun startNodeInfoOnly() {
|
||||||
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) }
|
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) }
|
||||||
startHandshakeStallGuard(2, action)
|
startHandshakeStallGuard(2, HANDSHAKE_TIMEOUT_STAGE2, action)
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -404,7 +405,14 @@ class MeshConnectionManagerImpl(
|
||||||
*/
|
*/
|
||||||
private const val PRE_HANDSHAKE_SETTLE_MS = 100L
|
private const val PRE_HANDSHAKE_SETTLE_MS = 100L
|
||||||
|
|
||||||
private val HANDSHAKE_TIMEOUT = 30.seconds
|
private val HANDSHAKE_TIMEOUT_STAGE1 = 30.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 2 drains the full node database, which can be significantly larger than Stage 1 config on big meshes.
|
||||||
|
* 60 s matches the meshtastic-client SDK timeout and avoids premature stall-guard triggers on meshes with 50+
|
||||||
|
* nodes.
|
||||||
|
*/
|
||||||
|
private val HANDSHAKE_TIMEOUT_STAGE2 = 60.seconds
|
||||||
|
|
||||||
// Shorter window for the retry attempt: if the device genuinely didn't receive the
|
// Shorter window for the retry attempt: if the device genuinely didn't receive the
|
||||||
// first want_config_id the retry completes within a few seconds. Waiting another 30s
|
// first want_config_id the retry completes within a few seconds. Waiting another 30s
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,10 @@ class SerialRadioTransport(
|
||||||
"Uptime: ${uptime}ms, " +
|
"Uptime: ${uptime}ms, " +
|
||||||
"Packets RX: $packetsReceived ($bytesReceived bytes)"
|
"Packets RX: $packetsReceived ($bytesReceived bytes)"
|
||||||
}
|
}
|
||||||
onDeviceDisconnect(false)
|
// USB unplug / cable error is transient — the transport will reconnect when
|
||||||
|
// the device is replugged or the OS re-enumerates the port. Only an explicit
|
||||||
|
// close() (user disconnects) should signal a permanent disconnect.
|
||||||
|
onDeviceDisconnect(waitForStopped = false, isPermanent = false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,11 @@ internal class SerialConnectionImpl(
|
||||||
|
|
||||||
port.open(usbDeviceConnection)
|
port.open(usbDeviceConnection)
|
||||||
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
||||||
|
|
||||||
|
// Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as
|
||||||
|
// present and starts its serial-side Meshtastic protocol. Empirically, omitting these
|
||||||
|
// signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at
|
||||||
|
// Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion.
|
||||||
port.dtr = true
|
port.dtr = true
|
||||||
port.rts = true
|
port.rts = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,7 @@ class UsbRepository(
|
||||||
_serialDevices
|
_serialDevices
|
||||||
.mapLatest { serialDevices ->
|
.mapLatest { serialDevices ->
|
||||||
val serialProber = usbSerialProberLazy.value
|
val serialProber = usbSerialProberLazy.value
|
||||||
buildMap {
|
buildMap { serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { put(k, it) } } }
|
||||||
serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
|
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
|
||||||
|
|
||||||
|
|
@ -83,6 +81,8 @@ class UsbRepository(
|
||||||
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
|
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshStateInternal() =
|
private suspend fun refreshStateInternal() = withContext(dispatchers.default) {
|
||||||
withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.value?.deviceList ?: emptyMap()) }
|
val devices = usbManagerLazy.value?.deviceList ?: emptyMap()
|
||||||
|
_serialDevices.emit(devices)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,11 @@ class BleRadioTransport(
|
||||||
|
|
||||||
@Volatile private var isFullyConnected = false
|
@Volatile private var isFullyConnected = false
|
||||||
private var connectionJob: Job? = null
|
private var connectionJob: Job? = null
|
||||||
private val reconnectPolicy = BleReconnectPolicy()
|
|
||||||
|
// Never give up while the user has this device selected. Higher layers (SharedRadioInterfaceService)
|
||||||
|
// own the explicit-disconnect lifecycle and will close() us when the user picks a different device or
|
||||||
|
// toggles the connection off; until then, retry forever with the policy's exponential-backoff cap (60 s).
|
||||||
|
private val reconnectPolicy = BleReconnectPolicy(maxFailures = Int.MAX_VALUE)
|
||||||
|
|
||||||
private val heartbeatSender =
|
private val heartbeatSender =
|
||||||
HeartbeatSender(
|
HeartbeatSender(
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
/**
|
/**
|
||||||
* Encapsulates the BLE reconnection policy with exponential backoff.
|
* Encapsulates the BLE reconnection policy with exponential backoff.
|
||||||
*
|
*
|
||||||
* The policy tracks consecutive failures and decides whether to retry, signal a transient disconnect (DeviceSleep), or
|
* The policy tracks consecutive failures and decides whether to retry or signal a transient disconnect (DeviceSleep).
|
||||||
* give up permanently.
|
* When [maxFailures] is reached the [execute] loop invokes [execute]'s `onPermanentDisconnect` callback and returns;
|
||||||
|
* set [maxFailures] to [Int.MAX_VALUE] (as [BleRadioTransport] does) to disable the give-up path entirely.
|
||||||
*
|
*
|
||||||
* @param maxFailures maximum consecutive failures before giving up permanently
|
* @param maxFailures maximum consecutive failures before giving up; use [Int.MAX_VALUE] to retry indefinitely
|
||||||
* @param failureThreshold after this many consecutive failures, signal a transient disconnect
|
* @param failureThreshold after this many consecutive failures, signal a transient disconnect
|
||||||
* @param settleDelay delay before each connection attempt to let the BLE stack settle
|
* @param settleDelay delay before each connection attempt to let the BLE stack settle
|
||||||
* @param minStableConnection minimum time a connection must stay up to be considered "stable"
|
* @param minStableConnection minimum time a connection must stay up to be considered "stable"
|
||||||
|
|
@ -148,7 +149,18 @@ class BleReconnectPolicy(
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_MAX_FAILURES = 10
|
const val DEFAULT_MAX_FAILURES = 10
|
||||||
const val DEFAULT_FAILURE_THRESHOLD = 3
|
const val DEFAULT_FAILURE_THRESHOLD = 3
|
||||||
val DEFAULT_SETTLE_DELAY = 1.seconds
|
|
||||||
|
/**
|
||||||
|
* Delay applied before every connection attempt (including the first) so the BLE stack and the firmware-side
|
||||||
|
* GATT session have time to settle.
|
||||||
|
*
|
||||||
|
* Empirically validated against the meshtastic-client KMP SDK probes (Apr 2026): with a 1.5 s pause between
|
||||||
|
* disconnect→reconnect cycles, 3/5–4/5 attempts failed mid-handshake (Stage1Draining timeouts) because the
|
||||||
|
* firmware had not yet released its GATT session from the previous cycle. With ≥ 5 s pause, success rate rose
|
||||||
|
* to 5/5 against a strong (-53 dBm) link. 3 s is a conservative compromise on Android, whose BLE stack is more
|
||||||
|
* mature than btleplug+CoreBluetooth, but the firmware-side cleanup constraint is the same.
|
||||||
|
*/
|
||||||
|
val DEFAULT_SETTLE_DELAY = 3.seconds
|
||||||
val DEFAULT_MIN_STABLE_CONNECTION = 5.seconds
|
val DEFAULT_MIN_STABLE_CONNECTION = 5.seconds
|
||||||
|
|
||||||
internal val RECONNECT_BASE_DELAY = 5.seconds
|
internal val RECONNECT_BASE_DELAY = 5.seconds
|
||||||
|
|
|
||||||
|
|
@ -37,18 +37,20 @@ abstract class StreamTransport(protected val callback: RadioTransportCallback, p
|
||||||
|
|
||||||
override suspend fun close() {
|
override suspend fun close() {
|
||||||
Logger.d { "Closing stream for good" }
|
Logger.d { "Closing stream for good" }
|
||||||
onDeviceDisconnect(true)
|
onDeviceDisconnect(waitForStopped = true, isPermanent = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify the transport callback that our device has gone away, but wait for it to come back.
|
* Signals the transport callback that the device has disconnected and optionally waits for the transport to stop.
|
||||||
*
|
*
|
||||||
* @param waitForStopped if true we should wait for the transport to finish - must be false if called from inside
|
* @param waitForStopped if true we should wait for the transport to finish - must be false if called from inside
|
||||||
* transport callbacks
|
* transport callbacks
|
||||||
* @param isPermanent true if the device is definitely gone (e.g. USB unplugged), false if it may come back (e.g.
|
* @param isPermanent true only when the user has explicitly disconnected (e.g. [close] was called). USB unplug, I/O
|
||||||
* TCP transient disconnect). Defaults to true for serial — subclasses may override with false.
|
* errors, and similar conditions are transient — the transport may recover when the device is replugged or the OS
|
||||||
|
* re-enumerates. Defaults to false so callbacks default to "may come back"; [close] passes true explicitly to
|
||||||
|
* signal a user-initiated terminal disconnect.
|
||||||
*/
|
*/
|
||||||
protected open fun onDeviceDisconnect(waitForStopped: Boolean, isPermanent: Boolean = true) {
|
protected open fun onDeviceDisconnect(waitForStopped: Boolean, isPermanent: Boolean = false) {
|
||||||
callback.onDisconnect(isPermanent = isPermanent)
|
callback.onDisconnect(isPermanent = isPermanent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import dev.mokkery.every
|
||||||
import dev.mokkery.matcher.any
|
import dev.mokkery.matcher.any
|
||||||
import dev.mokkery.mock
|
import dev.mokkery.mock
|
||||||
import dev.mokkery.verify
|
import dev.mokkery.verify
|
||||||
|
import dev.mokkery.verify.VerifyMode
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
|
@ -95,10 +96,10 @@ class BleRadioTransportTest {
|
||||||
* [RadioInterfaceService.onDisconnect] must be called so the higher layers can react (e.g. start the device-sleep
|
* [RadioInterfaceService.onDisconnect] must be called so the higher layers can react (e.g. start the device-sleep
|
||||||
* timeout in [MeshConnectionManagerImpl]).
|
* timeout in [MeshConnectionManagerImpl]).
|
||||||
*
|
*
|
||||||
* Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3): t = 1 000 ms — iteration 1 settle delay elapses,
|
* Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3, DEFAULT_SETTLE_DELAY = 3 s): t = 3 000 ms — iteration 1
|
||||||
* connectAndAwait throws, backoff 5 s starts t = 6 000 ms — backoff ends t = 7 000 ms — iteration 2 settle delay
|
* settle delay elapses, connectAndAwait throws, backoff 5 s starts t = 8 000 ms — backoff ends t = 11 000 ms —
|
||||||
* elapses, connectAndAwait throws, backoff 10 s starts t = 17 000 ms — backoff ends t = 18 000 ms — iteration 3
|
* iteration 2 settle delay elapses, connectAndAwait throws, backoff 10 s starts t = 21 000 ms — backoff ends t = 24
|
||||||
* settle delay elapses, connectAndAwait throws → onDisconnect called
|
* 000 ms — iteration 3 settle delay elapses, connectAndAwait throws → onDisconnect called
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun `onDisconnect is called after DEFAULT_FAILURE_THRESHOLD consecutive failures`() = runTest {
|
fun `onDisconnect is called after DEFAULT_FAILURE_THRESHOLD consecutive failures`() = runTest {
|
||||||
|
|
@ -119,10 +120,10 @@ class BleRadioTransportTest {
|
||||||
)
|
)
|
||||||
bleTransport.start()
|
bleTransport.start()
|
||||||
|
|
||||||
// Advance through exactly 3 failure iterations (≈18 001 ms virtual time).
|
// Advance through exactly 3 failure iterations (≈24 001 ms virtual time).
|
||||||
// The 4th iteration's backoff hasn't elapsed yet, so the coroutine is suspended
|
// The 4th iteration's backoff hasn't elapsed yet, so the coroutine is suspended
|
||||||
// and advanceTimeBy returns cleanly.
|
// and advanceTimeBy returns cleanly.
|
||||||
advanceTimeBy(18_001L)
|
advanceTimeBy(24_001L)
|
||||||
|
|
||||||
verify { service.onDisconnect(any(), any()) }
|
verify { service.onDisconnect(any(), any()) }
|
||||||
|
|
||||||
|
|
@ -131,16 +132,17 @@ class BleRadioTransportTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After [BleReconnectPolicy.DEFAULT_MAX_FAILURES] (10) consecutive failures, the reconnect loop should stop and
|
* Reconnect policy must NEVER give up on its own. The transport is only ever instantiated for the user-selected
|
||||||
* signal a permanent disconnect. This prevents infinite battery drain when the device is genuinely offline.
|
* device, and explicit-disconnect is owned by the service layer (close()). Even after a sustained failure storm —
|
||||||
|
* well beyond the legacy [BleReconnectPolicy.DEFAULT_MAX_FAILURES] — the transport must keep retrying and must
|
||||||
|
* never call `onDisconnect(isPermanent = true)` from the give-up path.
|
||||||
*
|
*
|
||||||
* Time budget for 10 failures with bonded device (no scan): Each iteration = 1s settle + connectAndAwait throw +
|
* Time budget for 15 failures with bonded device (no scan): each iteration ≈ 3 s settle + immediate throw +
|
||||||
* backoff Backoffs: 5s, 10s, 20s, 40s, 60s, 60s, 60s, 60s, 60s, (exit at failure 10 before backoff) Total ≈ 10×1s
|
* backoff. Backoffs cap at 60 s after failure 5: 5+10+20+40+60+60+60+60+60+60+60+60+60+60+60 = 735 s, plus 15×3 s
|
||||||
* settle + 5+10+20+40+60+60+60+60+60 = 10 + 375 = 385s ≈ 385_000ms We use a generous 400_000ms to cover any timing
|
* settle = 45 s, total ≈ 780 s. Use 800_000 ms to cover variance.
|
||||||
* variance.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun `reconnect loop stops after DEFAULT_MAX_FAILURES with permanent disconnect`() = runTest {
|
fun `reconnect loop never gives up - no permanent disconnect from policy`() = runTest {
|
||||||
val device = FakeBleDevice(address = address, name = "Test Device")
|
val device = FakeBleDevice(address = address, name = "Test Device")
|
||||||
bluetoothRepository.bond(device)
|
bluetoothRepository.bond(device)
|
||||||
|
|
||||||
|
|
@ -158,11 +160,13 @@ class BleRadioTransportTest {
|
||||||
)
|
)
|
||||||
bleTransport.start()
|
bleTransport.start()
|
||||||
|
|
||||||
// Advance enough time for all 10 failures to occur.
|
// Run well past where the legacy policy (maxFailures = 10) would have given up.
|
||||||
advanceTimeBy(400_001L)
|
advanceTimeBy(800_001L)
|
||||||
|
|
||||||
// Should have been called with isPermanent=true at least once (the final call).
|
// Transient disconnects (isPermanent = false) are expected once the failure threshold is hit;
|
||||||
verify { service.onDisconnect(isPermanent = true, errorMessage = any()) }
|
// the policy must NEVER signal a permanent disconnect on its own. Only explicit close()
|
||||||
|
// (verified separately by the service layer) may emit isPermanent = true.
|
||||||
|
verify(mode = VerifyMode.not) { service.onDisconnect(isPermanent = true, errorMessage = any()) }
|
||||||
|
|
||||||
bleTransport.close()
|
bleTransport.close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,11 @@ open class TcpRadioTransport(
|
||||||
Logger.d { "[$address] Closing TCP transport" }
|
Logger.d { "[$address] Closing TCP transport" }
|
||||||
closing = true
|
closing = true
|
||||||
transport.stop()
|
transport.stop()
|
||||||
callback.onDisconnect(isPermanent = true)
|
// Do NOT emit onDisconnect(isPermanent = true) here. The explicit-disconnect signal is the
|
||||||
|
// service layer's responsibility (SharedRadioInterfaceService.stopTransportLocked); emitting
|
||||||
|
// it from close() caused a double-disconnect and prevented the auto-reconnect loop from
|
||||||
|
// owning its own lifecycle. The `closing` guard above suppresses the listener's transient
|
||||||
|
// disconnect during teardown.
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun keepAlive() {
|
override fun keepAlive() {
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,10 @@ private constructor(
|
||||||
// Ignore errors during port close
|
// Ignore errors during port close
|
||||||
}
|
}
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
onDeviceDisconnect(true)
|
// Serial read loop ended unexpectedly (cable unplug, I/O error). Treat as
|
||||||
|
// transient — the user did not explicitly disconnect, and the port may come
|
||||||
|
// back when the device is replugged or the OS re-enumerates it.
|
||||||
|
onDeviceDisconnect(waitForStopped = true, isPermanent = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,8 +172,10 @@ private constructor(
|
||||||
private const val READ_TIMEOUT_MS = 100
|
private const val READ_TIMEOUT_MS = 100
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a permanent
|
* Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a transient
|
||||||
* disconnect to the [callback] and returns the (non-connected) instance.
|
* disconnect to the [callback] and returns the (non-connected) instance. The open failure is treated as
|
||||||
|
* non-permanent so higher-layer reconnect orchestration can retry (e.g. when the device is replugged or the
|
||||||
|
* user grants permission); only an explicit close should signal a permanent disconnect.
|
||||||
*/
|
*/
|
||||||
fun open(
|
fun open(
|
||||||
portName: String,
|
portName: String,
|
||||||
|
|
@ -183,7 +188,7 @@ private constructor(
|
||||||
if (!transport.startConnection()) {
|
if (!transport.startConnection()) {
|
||||||
val errorMessage = diagnoseOpenFailure(portName)
|
val errorMessage = diagnoseOpenFailure(portName)
|
||||||
Logger.w { "[$portName] Serial port could not be opened; signalling disconnect. $errorMessage" }
|
Logger.w { "[$portName] Serial port could not be opened; signalling disconnect. $errorMessage" }
|
||||||
callback.onDisconnect(isPermanent = true, errorMessage = errorMessage)
|
callback.onDisconnect(isPermanent = false, errorMessage = errorMessage)
|
||||||
}
|
}
|
||||||
return transport
|
return transport
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -486,8 +486,17 @@
|
||||||
<string name="frequency_slot">Честотен слот</string>
|
<string name="frequency_slot">Честотен слот</string>
|
||||||
<string name="ignore_mqtt">Игнориране на MQTT</string>
|
<string name="ignore_mqtt">Игнориране на MQTT</string>
|
||||||
<string name="mqtt_config">Конфигуриране на MQTT</string>
|
<string name="mqtt_config">Конфигуриране на MQTT</string>
|
||||||
|
<string name="mqtt_status_inactive">Неактивен</string>
|
||||||
<string name="mqtt_status_disconnected">Прекъсната връзка</string>
|
<string name="mqtt_status_disconnected">Прекъсната връзка</string>
|
||||||
|
<string name="mqtt_status_connecting">Свързване…</string>
|
||||||
<string name="mqtt_status_connected">Свързано</string>
|
<string name="mqtt_status_connected">Свързано</string>
|
||||||
|
<string name="mqtt_status_reconnecting">Повторно свързване…</string>
|
||||||
|
<string name="mqtt_status_reconnecting_with_attempt">Повторно свързване (опит %1$d) — %2$s</string>
|
||||||
|
<string name="mqtt_test_connection">Тестване на връзката</string>
|
||||||
|
<string name="mqtt_probe_success">Достъпен. Брокерът е приел идентификационните данни.</string>
|
||||||
|
<string name="mqtt_probe_success_with_info">Достъпен (%1$s)</string>
|
||||||
|
<string name="mqtt_probe_dns_failure">Хостът не е намерен</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Връзката е неуспешна</string>
|
||||||
<string name="mqtt_enabled">MQTT е активиран</string>
|
<string name="mqtt_enabled">MQTT е активиран</string>
|
||||||
<string name="address">Адрес</string>
|
<string name="address">Адрес</string>
|
||||||
<string name="username">Потребителско име</string>
|
<string name="username">Потребителско име</string>
|
||||||
|
|
@ -969,4 +978,5 @@
|
||||||
<string name="desktop_notification_title">Meshtastic</string>
|
<string name="desktop_notification_title">Meshtastic</string>
|
||||||
<string name="filter_icon">Филтър</string>
|
<string name="filter_icon">Филтър</string>
|
||||||
<string name="action_select_device">Изберете устройство</string>
|
<string name="action_select_device">Изберете устройство</string>
|
||||||
|
<string name="action_select_network">Изберете мрежа</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -609,8 +609,23 @@
|
||||||
<string name="ignore_mqtt">MQTT ignorieren</string>
|
<string name="ignore_mqtt">MQTT ignorieren</string>
|
||||||
<string name="ok_to_mqtt">OK für MQTT</string>
|
<string name="ok_to_mqtt">OK für MQTT</string>
|
||||||
<string name="mqtt_config">MQTT Einstellungen</string>
|
<string name="mqtt_config">MQTT Einstellungen</string>
|
||||||
|
<string name="mqtt_status_inactive">Inaktiv</string>
|
||||||
<string name="mqtt_status_disconnected">Verbindung getrennt</string>
|
<string name="mqtt_status_disconnected">Verbindung getrennt</string>
|
||||||
|
<string name="mqtt_status_disconnected_with_reason">Verbindung getrennt - %1$s</string>
|
||||||
|
<string name="mqtt_status_connecting">Wird verbunden</string>
|
||||||
<string name="mqtt_status_connected">Verbunden</string>
|
<string name="mqtt_status_connected">Verbunden</string>
|
||||||
|
<string name="mqtt_status_reconnecting">Erneut verbinden</string>
|
||||||
|
<string name="mqtt_status_reconnecting_with_attempt">Erneut verbinden (Versuch %1$d) - %2$s</string>
|
||||||
|
<string name="mqtt_test_connection">Verbindung testen</string>
|
||||||
|
<string name="mqtt_probe_running">Broker prüfen.</string>
|
||||||
|
<string name="mqtt_probe_success">Erreichbar. Broker akzeptierte Anmeldedaten.</string>
|
||||||
|
<string name="mqtt_probe_success_with_info">Erreichbar (%1$s)</string>
|
||||||
|
<string name="mqtt_probe_rejected">Broker abgelehnt: %1$s</string>
|
||||||
|
<string name="mqtt_probe_dns_failure">Host nicht gefunden</string>
|
||||||
|
<string name="mqtt_probe_tcp_failure">Broker (TCP) nicht erreichbar</string>
|
||||||
|
<string name="mqtt_probe_tls_failure">TLS Handshake fehlgeschlagen</string>
|
||||||
|
<string name="mqtt_probe_timeout">Zeitüberschreitung nach %1$d ms</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Verbindung fehlgeschlagen</string>
|
||||||
<string name="mqtt_enabled">MQTT aktiviert</string>
|
<string name="mqtt_enabled">MQTT aktiviert</string>
|
||||||
<string name="address">Adresse</string>
|
<string name="address">Adresse</string>
|
||||||
<string name="username">Benutzername</string>
|
<string name="username">Benutzername</string>
|
||||||
|
|
@ -1206,7 +1221,21 @@
|
||||||
<string name="wifi_provision_ssid_placeholder">Netzwerk eingeben oder auswählen</string>
|
<string name="wifi_provision_ssid_placeholder">Netzwerk eingeben oder auswählen</string>
|
||||||
<string name="wifi_provision_status_applied">WLAN erfolgreich konfiguriert!</string>
|
<string name="wifi_provision_status_applied">WLAN erfolgreich konfiguriert!</string>
|
||||||
<string name="wifi_provision_status_failed">WLAN Konfiguration konnte nicht angewendet werden</string>
|
<string name="wifi_provision_status_failed">WLAN Konfiguration konnte nicht angewendet werden</string>
|
||||||
|
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
|
||||||
|
<string name="desktop_tray_show">Meshtastic anzeigen</string>
|
||||||
|
<string name="desktop_tray_quit">Beenden</string>
|
||||||
<string name="desktop_notification_title">Meshtastic</string>
|
<string name="desktop_notification_title">Meshtastic</string>
|
||||||
|
<string name="export_tak_data_package">TAK Datenpaket exportieren</string>
|
||||||
|
<string name="clear_time_zone">Zeitzone löschen</string>
|
||||||
<string name="filter_icon">Filter</string>
|
<string name="filter_icon">Filter</string>
|
||||||
|
<string name="remove_filter">Filter entfernen</string>
|
||||||
|
<string name="show_iaq_legend">Legende für Luftqualität anzeigen</string>
|
||||||
|
<string name="action_show_message_status">Nachrichtenstatus anzeigen</string>
|
||||||
|
<string name="action_send_reply">Antwort senden</string>
|
||||||
|
<string name="action_copy_message">Nachricht kopieren</string>
|
||||||
|
<string name="action_select_message">Nachricht auswählen</string>
|
||||||
|
<string name="action_delete_message">Nachricht löschen</string>
|
||||||
|
<string name="action_react_with_emoji">Mit Emoji reagieren</string>
|
||||||
<string name="action_select_device">Gerät auswählen</string>
|
<string name="action_select_device">Gerät auswählen</string>
|
||||||
|
<string name="action_select_network">Wählen Sie ein Netzwerk</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -611,9 +611,21 @@
|
||||||
<string name="mqtt_config">MQTT sätted</string>
|
<string name="mqtt_config">MQTT sätted</string>
|
||||||
<string name="mqtt_status_inactive">Mitteaktiivne</string>
|
<string name="mqtt_status_inactive">Mitteaktiivne</string>
|
||||||
<string name="mqtt_status_disconnected">Ühendus katkenud</string>
|
<string name="mqtt_status_disconnected">Ühendus katkenud</string>
|
||||||
|
<string name="mqtt_status_disconnected_with_reason">Ühendus katkenud — %1$s</string>
|
||||||
<string name="mqtt_status_connecting">Ühendan…</string>
|
<string name="mqtt_status_connecting">Ühendan…</string>
|
||||||
<string name="mqtt_status_connected">Ühendatud</string>
|
<string name="mqtt_status_connected">Ühendatud</string>
|
||||||
<string name="mqtt_status_reconnecting">Taas ühendan…</string>
|
<string name="mqtt_status_reconnecting">Taas ühendan…</string>
|
||||||
|
<string name="mqtt_status_reconnecting_with_attempt">Ühendan uuesti (katse %1$d) — %2$s</string>
|
||||||
|
<string name="mqtt_test_connection">Test ühendus</string>
|
||||||
|
<string name="mqtt_probe_running">Kontrollin vahendajat…</string>
|
||||||
|
<string name="mqtt_probe_success">Ühendus õnnestus. Vahendaja aktsepteeris kasutajateave.</string>
|
||||||
|
<string name="mqtt_probe_success_with_info">Kättesaadav (%1$s)</string>
|
||||||
|
<string name="mqtt_probe_rejected">Vahendaja lükkas tagasi: %1$s</string>
|
||||||
|
<string name="mqtt_probe_dns_failure">Hosti ei leitud</string>
|
||||||
|
<string name="mqtt_probe_tcp_failure">Vahendajaga ei saa ühendust (TCP)</string>
|
||||||
|
<string name="mqtt_probe_tls_failure">TLS ühendus ebaõnnestus</string>
|
||||||
|
<string name="mqtt_probe_timeout">Ajaline katkestus peale %1$d ms</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Ühendus ebaõnnestus</string>
|
||||||
<string name="mqtt_enabled">MQTT lubatud</string>
|
<string name="mqtt_enabled">MQTT lubatud</string>
|
||||||
<string name="address">Aadress</string>
|
<string name="address">Aadress</string>
|
||||||
<string name="username">Kasutajatunnus</string>
|
<string name="username">Kasutajatunnus</string>
|
||||||
|
|
@ -1213,5 +1225,17 @@
|
||||||
<string name="desktop_tray_show">Näita Meshtastic</string>
|
<string name="desktop_tray_show">Näita Meshtastic</string>
|
||||||
<string name="desktop_tray_quit">Sule</string>
|
<string name="desktop_tray_quit">Sule</string>
|
||||||
<string name="desktop_notification_title">Kärgvõrgustik</string>
|
<string name="desktop_notification_title">Kärgvõrgustik</string>
|
||||||
|
<string name="export_tak_data_package">Ekspordi TAK andmepakett</string>
|
||||||
|
<string name="clear_time_zone">Eemalda ajatsoon</string>
|
||||||
<string name="filter_icon">Filtreeri</string>
|
<string name="filter_icon">Filtreeri</string>
|
||||||
|
<string name="remove_filter">Eemalda filter</string>
|
||||||
|
<string name="show_iaq_legend">Näita õhukvaliteedi ajalugu</string>
|
||||||
|
<string name="action_show_message_status">Kuva sõnumi olek</string>
|
||||||
|
<string name="action_send_reply">Saada vastus</string>
|
||||||
|
<string name="action_copy_message">Kopeeri sõnum</string>
|
||||||
|
<string name="action_select_message">Vali sõnum</string>
|
||||||
|
<string name="action_delete_message">Kustuta sõnum</string>
|
||||||
|
<string name="action_react_with_emoji">Vasta emotikoniga</string>
|
||||||
|
<string name="action_select_device">Vali seade</string>
|
||||||
|
<string name="action_select_network">Vali võrk</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -611,9 +611,21 @@
|
||||||
<string name="mqtt_config">MQTT asetukset</string>
|
<string name="mqtt_config">MQTT asetukset</string>
|
||||||
<string name="mqtt_status_inactive">Passiivinen</string>
|
<string name="mqtt_status_inactive">Passiivinen</string>
|
||||||
<string name="mqtt_status_disconnected">Ei yhdistetty</string>
|
<string name="mqtt_status_disconnected">Ei yhdistetty</string>
|
||||||
|
<string name="mqtt_status_disconnected_with_reason">Yhteys katkaistu — %1$s</string>
|
||||||
<string name="mqtt_status_connecting">Yhdistetään…</string>
|
<string name="mqtt_status_connecting">Yhdistetään…</string>
|
||||||
<string name="mqtt_status_connected">Yhdistetty</string>
|
<string name="mqtt_status_connected">Yhdistetty</string>
|
||||||
<string name="mqtt_status_reconnecting">Yhdistetään uudelleen…</string>
|
<string name="mqtt_status_reconnecting">Yhdistetään uudelleen…</string>
|
||||||
|
<string name="mqtt_status_reconnecting_with_attempt">Yhdistetään uudelleen (yritys %1$d) — %2$s</string>
|
||||||
|
<string name="mqtt_test_connection">Testaa yhteys</string>
|
||||||
|
<string name="mqtt_probe_running">Tarkistetaan välityspalvelinta…</string>
|
||||||
|
<string name="mqtt_probe_success">Yhteys onnistui. Välityspalvelin hyväksyi tunnistetiedot.</string>
|
||||||
|
<string name="mqtt_probe_success_with_info">Yhteys onnistui (%1$s)</string>
|
||||||
|
<string name="mqtt_probe_rejected">Välityspalvelin ei hyväksynyt: %1$s</string>
|
||||||
|
<string name="mqtt_probe_dns_failure">Palvelinta ei löytynyt</string>
|
||||||
|
<string name="mqtt_probe_tcp_failure">Yhteyttä välityspalvelimeen ei saada (TCP)</string>
|
||||||
|
<string name="mqtt_probe_tls_failure">TLS-yhteyden muodostus epäonnistui</string>
|
||||||
|
<string name="mqtt_probe_timeout">Aikakatkaistu %1$d ms jälkeen</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Yhdistäminen epäonnistui</string>
|
||||||
<string name="mqtt_enabled">MQTT käytössä</string>
|
<string name="mqtt_enabled">MQTT käytössä</string>
|
||||||
<string name="address">Osoite</string>
|
<string name="address">Osoite</string>
|
||||||
<string name="username">Käyttäjänimi</string>
|
<string name="username">Käyttäjänimi</string>
|
||||||
|
|
@ -1214,6 +1226,17 @@
|
||||||
<string name="desktop_tray_show">Näytä Meshtastic</string>
|
<string name="desktop_tray_show">Näytä Meshtastic</string>
|
||||||
<string name="desktop_tray_quit">Lopeta</string>
|
<string name="desktop_tray_quit">Lopeta</string>
|
||||||
<string name="desktop_notification_title">Meshtastic</string>
|
<string name="desktop_notification_title">Meshtastic</string>
|
||||||
|
<string name="export_tak_data_package">Vie TAK-datapaketti</string>
|
||||||
|
<string name="clear_time_zone">Tyhjennä aikavyöhyke</string>
|
||||||
<string name="filter_icon">Suodatus</string>
|
<string name="filter_icon">Suodatus</string>
|
||||||
|
<string name="remove_filter">Poista suodatin</string>
|
||||||
|
<string name="show_iaq_legend">Näytä ilmanlaadun selite</string>
|
||||||
|
<string name="action_show_message_status">Näytä viestin tila</string>
|
||||||
|
<string name="action_send_reply">Lähetä vastaus</string>
|
||||||
|
<string name="action_copy_message">Kopioi viesti</string>
|
||||||
|
<string name="action_select_message">Valitse viesti</string>
|
||||||
|
<string name="action_delete_message">Poista viesti</string>
|
||||||
|
<string name="action_react_with_emoji">Reaktio emojin kanssa</string>
|
||||||
<string name="action_select_device">Valitse laite</string>
|
<string name="action_select_device">Valitse laite</string>
|
||||||
|
<string name="action_select_network">Valitse verkko</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -614,6 +614,8 @@
|
||||||
<string name="mqtt_status_connecting">Connexion…</string>
|
<string name="mqtt_status_connecting">Connexion…</string>
|
||||||
<string name="mqtt_status_connected">Connecté</string>
|
<string name="mqtt_status_connected">Connecté</string>
|
||||||
<string name="mqtt_status_reconnecting">Reconnexion…</string>
|
<string name="mqtt_status_reconnecting">Reconnexion…</string>
|
||||||
|
<string name="mqtt_test_connection">Test de la connexion</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Échec de la connexion</string>
|
||||||
<string name="mqtt_enabled">MQTT activé</string>
|
<string name="mqtt_enabled">MQTT activé</string>
|
||||||
<string name="address">Adresse</string>
|
<string name="address">Adresse</string>
|
||||||
<string name="username">Nom d'utilisateur</string>
|
<string name="username">Nom d'utilisateur</string>
|
||||||
|
|
|
||||||
|
|
@ -617,8 +617,23 @@
|
||||||
<string name="ignore_mqtt">Игнорировать MQTT</string>
|
<string name="ignore_mqtt">Игнорировать MQTT</string>
|
||||||
<string name="ok_to_mqtt">ОК в MQTT</string>
|
<string name="ok_to_mqtt">ОК в MQTT</string>
|
||||||
<string name="mqtt_config">Настройка MQTT</string>
|
<string name="mqtt_config">Настройка MQTT</string>
|
||||||
|
<string name="mqtt_status_inactive">Неактивно</string>
|
||||||
<string name="mqtt_status_disconnected">Отключено</string>
|
<string name="mqtt_status_disconnected">Отключено</string>
|
||||||
|
<string name="mqtt_status_disconnected_with_reason">Отключено — %1$s</string>
|
||||||
|
<string name="mqtt_status_connecting">Подключение...</string>
|
||||||
<string name="mqtt_status_connected">Подключено</string>
|
<string name="mqtt_status_connected">Подключено</string>
|
||||||
|
<string name="mqtt_status_reconnecting">Переподключение...</string>
|
||||||
|
<string name="mqtt_status_reconnecting_with_attempt">Переподключение (попытка %1$d) — %2$s</string>
|
||||||
|
<string name="mqtt_test_connection">Проверить соединение</string>
|
||||||
|
<string name="mqtt_probe_running">Проверяем брокер…</string>
|
||||||
|
<string name="mqtt_probe_success">Доступно. Брокер принял учетные данные.</string>
|
||||||
|
<string name="mqtt_probe_success_with_info">Доступно (%1$s)</string>
|
||||||
|
<string name="mqtt_probe_rejected">Брокер отклонен: %1$s</string>
|
||||||
|
<string name="mqtt_probe_dns_failure">Узел не найден</string>
|
||||||
|
<string name="mqtt_probe_tcp_failure">Не удается подключиться к брокеру (TCP)</string>
|
||||||
|
<string name="mqtt_probe_tls_failure">Сбой TLS-рукопожатия</string>
|
||||||
|
<string name="mqtt_probe_timeout">Тайм-аут после %1$d мс</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Соединение не удалось</string>
|
||||||
<string name="mqtt_enabled">MQTT включен</string>
|
<string name="mqtt_enabled">MQTT включен</string>
|
||||||
<string name="address">Адрес</string>
|
<string name="address">Адрес</string>
|
||||||
<string name="username">Имя пользователя</string>
|
<string name="username">Имя пользователя</string>
|
||||||
|
|
@ -1222,7 +1237,21 @@
|
||||||
<string name="wifi_provision_ssid_placeholder">Введите или выберите сеть</string>
|
<string name="wifi_provision_ssid_placeholder">Введите или выберите сеть</string>
|
||||||
<string name="wifi_provision_status_applied">Wi-Fi успешно настроен!</string>
|
<string name="wifi_provision_status_applied">Wi-Fi успешно настроен!</string>
|
||||||
<string name="wifi_provision_status_failed">Не удалось применить настройку Wi-Fi</string>
|
<string name="wifi_provision_status_failed">Не удалось применить настройку Wi-Fi</string>
|
||||||
|
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
|
||||||
|
<string name="desktop_tray_show">Показать Meshtastic</string>
|
||||||
|
<string name="desktop_tray_quit">Выход</string>
|
||||||
<string name="desktop_notification_title">Meshtastic</string>
|
<string name="desktop_notification_title">Meshtastic</string>
|
||||||
|
<string name="export_tak_data_package">Экспорт пакета данных TAK</string>
|
||||||
|
<string name="clear_time_zone">Очистить часовой пояс</string>
|
||||||
<string name="filter_icon">Фильтр</string>
|
<string name="filter_icon">Фильтр</string>
|
||||||
|
<string name="remove_filter">Удалить фильтр</string>
|
||||||
|
<string name="show_iaq_legend">Показать легенду качества воздуха</string>
|
||||||
|
<string name="action_show_message_status">Показать статус сообщения</string>
|
||||||
|
<string name="action_send_reply">Отправить ответ</string>
|
||||||
|
<string name="action_copy_message">Скопировать сообщение</string>
|
||||||
|
<string name="action_select_message">Выбрать сообщение</string>
|
||||||
|
<string name="action_delete_message">Удалить сообщение</string>
|
||||||
|
<string name="action_react_with_emoji">Отреагировать эмодзи</string>
|
||||||
<string name="action_select_device">Выберите устройство</string>
|
<string name="action_select_device">Выберите устройство</string>
|
||||||
|
<string name="action_select_network">Выбрать сеть</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
<string name="unrecognized">Okänd</string>
|
<string name="unrecognized">Okänd</string>
|
||||||
<string name="message_status_enroute">Inväntar kvittens</string>
|
<string name="message_status_enroute">Inväntar kvittens</string>
|
||||||
<string name="message_status_queued">Kvittens köad</string>
|
<string name="message_status_queued">Kvittens köad</string>
|
||||||
|
<string name="message_status_delivered">Levererad till nät</string>
|
||||||
<string name="message_status_unknown">Okänd</string>
|
<string name="message_status_unknown">Okänd</string>
|
||||||
<string name="routing_error_none">Kvitterad</string>
|
<string name="routing_error_none">Kvitterad</string>
|
||||||
<string name="routing_error_no_route">Ingen rutt</string>
|
<string name="routing_error_no_route">Ingen rutt</string>
|
||||||
|
|
@ -370,6 +371,7 @@
|
||||||
<string name="traceroute_duration">Varaktighet: %1$s s</string>
|
<string name="traceroute_duration">Varaktighet: %1$s s</string>
|
||||||
<string name="traceroute_route_towards_dest">Rutt spårad mot destination:\n\n</string>
|
<string name="traceroute_route_towards_dest">Rutt spårad mot destination:\n\n</string>
|
||||||
<string name="traceroute_route_back_to_us">Rutten spårad tillbaka till oss:\n\n</string>
|
<string name="traceroute_route_back_to_us">Rutten spårad tillbaka till oss:\n\n</string>
|
||||||
|
<string name="traceroute_no_response">Inget svar</string>
|
||||||
<string name="one_hour_short">1h</string>
|
<string name="one_hour_short">1h</string>
|
||||||
<string name="twenty_four_hours">24T</string>
|
<string name="twenty_four_hours">24T</string>
|
||||||
<string name="one_week">1V</string>
|
<string name="one_week">1V</string>
|
||||||
|
|
@ -528,6 +530,8 @@
|
||||||
<string name="mqtt_config">MQTT-konfiguration</string>
|
<string name="mqtt_config">MQTT-konfiguration</string>
|
||||||
<string name="mqtt_status_disconnected">Frånkopplad</string>
|
<string name="mqtt_status_disconnected">Frånkopplad</string>
|
||||||
<string name="mqtt_status_connected">Ansluten</string>
|
<string name="mqtt_status_connected">Ansluten</string>
|
||||||
|
<string name="mqtt_test_connection">Testa anslutningen</string>
|
||||||
|
<string name="mqtt_probe_other_failure">Anslutningen misslyckades</string>
|
||||||
<string name="mqtt_enabled">MQTT är aktiverat</string>
|
<string name="mqtt_enabled">MQTT är aktiverat</string>
|
||||||
<string name="address">Adress</string>
|
<string name="address">Adress</string>
|
||||||
<string name="username">Användarnamn</string>
|
<string name="username">Användarnamn</string>
|
||||||
|
|
|
||||||
|
|
@ -401,6 +401,7 @@
|
||||||
<string name="mqtt_config">Налаштування MQTT</string>
|
<string name="mqtt_config">Налаштування MQTT</string>
|
||||||
<string name="mqtt_status_disconnected">Відключено</string>
|
<string name="mqtt_status_disconnected">Відключено</string>
|
||||||
<string name="mqtt_status_connected">Під’єднано</string>
|
<string name="mqtt_status_connected">Під’єднано</string>
|
||||||
|
<string name="mqtt_test_connection">Перевірка зʼєднання</string>
|
||||||
<string name="mqtt_enabled">MQTT увімкнений</string>
|
<string name="mqtt_enabled">MQTT увімкнений</string>
|
||||||
<string name="address">Адреса</string>
|
<string name="address">Адреса</string>
|
||||||
<string name="username">Ім'я користувача</string>
|
<string name="username">Ім'я користувача</string>
|
||||||
|
|
|
||||||
|
|
@ -567,6 +567,7 @@
|
||||||
<string name="mqtt_config">MQTT设置</string>
|
<string name="mqtt_config">MQTT设置</string>
|
||||||
<string name="mqtt_status_disconnected">已断开连接</string>
|
<string name="mqtt_status_disconnected">已断开连接</string>
|
||||||
<string name="mqtt_status_connected">已连接</string>
|
<string name="mqtt_status_connected">已连接</string>
|
||||||
|
<string name="mqtt_test_connection">连接测试</string>
|
||||||
<string name="mqtt_enabled">启用MQTT</string>
|
<string name="mqtt_enabled">启用MQTT</string>
|
||||||
<string name="address">地址</string>
|
<string name="address">地址</string>
|
||||||
<string name="username">用户名</string>
|
<string name="username">用户名</string>
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,11 @@
|
||||||
<string name="a11y_label_value">%1$s: %2$s</string>
|
<string name="a11y_label_value">%1$s: %2$s</string>
|
||||||
<string name="a11y_message_from">來自 %1$s 的訊息:%2$s</string>
|
<string name="a11y_message_from">來自 %1$s 的訊息:%2$s</string>
|
||||||
<string name="preview_header">標頭</string>
|
<string name="preview_header">標頭</string>
|
||||||
|
<string name="preview_footer">標尾</string>
|
||||||
|
<string name="preview_dot">點形</string>
|
||||||
|
<string name="preview_text">文字</string>
|
||||||
|
<string name="preview_gauge">儀表板</string>
|
||||||
|
<string name="preview_gradient">梯度</string>
|
||||||
<string name="preview_custom_composable_line_one">這是一個一個一個可客製化的組合元件</string>
|
<string name="preview_custom_composable_line_one">這是一個一個一個可客製化的組合元件</string>
|
||||||
<string name="preview_custom_composable_line_two">還支援多行文字與多種樣式</string>
|
<string name="preview_custom_composable_line_two">還支援多行文字與多種樣式</string>
|
||||||
<string name="message_delivery_status">訊息傳遞狀態</string>
|
<string name="message_delivery_status">訊息傳遞狀態</string>
|
||||||
|
|
@ -407,6 +412,12 @@
|
||||||
<string name="traceroute_return_hops">回程跳數</string>
|
<string name="traceroute_return_hops">回程跳數</string>
|
||||||
<string name="traceroute_round_trip">來回跳數</string>
|
<string name="traceroute_round_trip">來回跳數</string>
|
||||||
<string name="traceroute_no_response">無回應</string>
|
<string name="traceroute_no_response">無回應</string>
|
||||||
|
<string name="load_1_min">1分鐘負載</string>
|
||||||
|
<string name="load_5_min">5分鐘負載</string>
|
||||||
|
<string name="load_15_min">15分鐘負載</string>
|
||||||
|
<string name="load_1_min_description">1分鐘系統負載平均值</string>
|
||||||
|
<string name="load_5_min_description">5分鐘系統負載平均值</string>
|
||||||
|
<string name="load_15_min_description">15分鐘系統負載平均值</string>
|
||||||
<string name="free_memory_description">可用系統記憶體(位元組)</string>
|
<string name="free_memory_description">可用系統記憶體(位元組)</string>
|
||||||
<string name="one_hour_short">1小時</string>
|
<string name="one_hour_short">1小時</string>
|
||||||
<string name="twenty_four_hours">二十四小時</string>
|
<string name="twenty_four_hours">二十四小時</string>
|
||||||
|
|
@ -592,8 +603,23 @@
|
||||||
<string name="ignore_mqtt">無視MQTT</string>
|
<string name="ignore_mqtt">無視MQTT</string>
|
||||||
<string name="ok_to_mqtt">允許轉發至 MQTT</string>
|
<string name="ok_to_mqtt">允許轉發至 MQTT</string>
|
||||||
<string name="mqtt_config">MQTT配置</string>
|
<string name="mqtt_config">MQTT配置</string>
|
||||||
|
<string name="mqtt_status_inactive">已停用</string>
|
||||||
<string name="mqtt_status_disconnected">已中斷連線</string>
|
<string name="mqtt_status_disconnected">已中斷連線</string>
|
||||||
|
<string name="mqtt_status_disconnected_with_reason">已斷線 — %1$s</string>
|
||||||
|
<string name="mqtt_status_connecting">正在連接…</string>
|
||||||
<string name="mqtt_status_connected">已連線</string>
|
<string name="mqtt_status_connected">已連線</string>
|
||||||
|
<string name="mqtt_status_reconnecting">重新連接中…</string>
|
||||||
|
<string name="mqtt_status_reconnecting_with_attempt">重新連接中(第 %1$d 次嘗試) — %2$s</string>
|
||||||
|
<string name="mqtt_test_connection">測試連線</string>
|
||||||
|
<string name="mqtt_probe_running">正在查詢 Broker…</string>
|
||||||
|
<string name="mqtt_probe_success">可供連線,Broker 已驗證並接受憑證。</string>
|
||||||
|
<string name="mqtt_probe_success_with_info">可供連線(%1$s)</string>
|
||||||
|
<string name="mqtt_probe_rejected">Broker 遭拒:%1$s</string>
|
||||||
|
<string name="mqtt_probe_dns_failure">找不到伺服器</string>
|
||||||
|
<string name="mqtt_probe_tcp_failure">無法連線至 Broker 中繼伺服器(TCP)</string>
|
||||||
|
<string name="mqtt_probe_tls_failure">TLS 握手失敗</string>
|
||||||
|
<string name="mqtt_probe_timeout">經過 %1$d 毫秒後逾時</string>
|
||||||
|
<string name="mqtt_probe_other_failure">測試失敗</string>
|
||||||
<string name="mqtt_enabled">啟用MQTT服務器</string>
|
<string name="mqtt_enabled">啟用MQTT服務器</string>
|
||||||
<string name="address">地址</string>
|
<string name="address">地址</string>
|
||||||
<string name="username">用戶名</string>
|
<string name="username">用戶名</string>
|
||||||
|
|
@ -792,6 +818,9 @@
|
||||||
<string name="show_waypoints">顯示路徑</string>
|
<string name="show_waypoints">顯示路徑</string>
|
||||||
<string name="show_precision_circle">顯示定位精準度</string>
|
<string name="show_precision_circle">顯示定位精準度</string>
|
||||||
<string name="client_notification">客户端通知</string>
|
<string name="client_notification">客户端通知</string>
|
||||||
|
<string name="key_verification_title">金鑰驗證</string>
|
||||||
|
<string name="key_verification_request_title">金鑰驗證請求</string>
|
||||||
|
<string name="key_verification_final_title">金鑰驗證已完成</string>
|
||||||
<string name="duplicated_public_key_title">偵測到重複的公鑰</string>
|
<string name="duplicated_public_key_title">偵測到重複的公鑰</string>
|
||||||
<string name="low_entropy_key_title">偵測到加密金鑰強度不足</string>
|
<string name="low_entropy_key_title">偵測到加密金鑰強度不足</string>
|
||||||
<string name="compromised_keys">偵測到金鑰已洩漏,點選確定後重新產生金鑰。</string>
|
<string name="compromised_keys">偵測到金鑰已洩漏,點選確定後重新產生金鑰。</string>
|
||||||
|
|
@ -1160,6 +1189,8 @@
|
||||||
<string name="note">注意</string>
|
<string name="note">注意</string>
|
||||||
<string name="device_storage_ui_title">裝置儲存空間與使用者介面(唯讀)</string>
|
<string name="device_storage_ui_title">裝置儲存空間與使用者介面(唯讀)</string>
|
||||||
<string name="device_theme_language">主題 %1$s,語言 %2$s</string>
|
<string name="device_theme_language">主題 %1$s,語言 %2$s</string>
|
||||||
|
<string name="files_available">可使用檔案(%1$d):</string>
|
||||||
|
<string name="file_entry">- %1$s(%2$d 位元)</string>
|
||||||
<string name="no_files_manifested">未發現任何檔案。</string>
|
<string name="no_files_manifested">未發現任何檔案。</string>
|
||||||
<string name="connect">連線</string>
|
<string name="connect">連線</string>
|
||||||
<string name="done">完成</string>
|
<string name="done">完成</string>
|
||||||
|
|
@ -1168,6 +1199,7 @@
|
||||||
<string name="wifi_provision_mpwrd_disclaimer">進一步了解 mPWRD-OS 專案\nhttps://github.com/mPWRD-OS</string>
|
<string name="wifi_provision_mpwrd_disclaimer">進一步了解 mPWRD-OS 專案\nhttps://github.com/mPWRD-OS</string>
|
||||||
<string name="wifi_provision_scanning_ble">正在搜尋裝置…</string>
|
<string name="wifi_provision_scanning_ble">正在搜尋裝置…</string>
|
||||||
<string name="wifi_provision_device_found">找到裝置</string>
|
<string name="wifi_provision_device_found">找到裝置</string>
|
||||||
|
<string name="wifi_provision_device_found_detail">準備好掃描 Wi-Fi 網路了。</string>
|
||||||
<string name="wifi_provision_scan_networks">搜尋網路</string>
|
<string name="wifi_provision_scan_networks">搜尋網路</string>
|
||||||
<string name="wifi_provision_scanning_wifi">正在搜尋…</string>
|
<string name="wifi_provision_scanning_wifi">正在搜尋…</string>
|
||||||
<string name="wifi_provision_sending_credentials">正在套用 Wi-Fi 設定…</string>
|
<string name="wifi_provision_sending_credentials">正在套用 Wi-Fi 設定…</string>
|
||||||
|
|
@ -1180,9 +1212,21 @@
|
||||||
<string name="wifi_provision_ssid_placeholder">手動輸入或選擇一個網路</string>
|
<string name="wifi_provision_ssid_placeholder">手動輸入或選擇一個網路</string>
|
||||||
<string name="wifi_provision_status_applied">Wi-Fi 已設定完成!</string>
|
<string name="wifi_provision_status_applied">Wi-Fi 已設定完成!</string>
|
||||||
<string name="wifi_provision_status_failed">無法套用 Wi-Fi 設定</string>
|
<string name="wifi_provision_status_failed">無法套用 Wi-Fi 設定</string>
|
||||||
|
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
|
||||||
<string name="desktop_tray_show">顯示 Meshtastic</string>
|
<string name="desktop_tray_show">顯示 Meshtastic</string>
|
||||||
<string name="desktop_tray_quit">離開</string>
|
<string name="desktop_tray_quit">離開</string>
|
||||||
<string name="desktop_notification_title">Meshtastic</string>
|
<string name="desktop_notification_title">Meshtastic</string>
|
||||||
|
<string name="export_tak_data_package">匯出 TAK 資料封包</string>
|
||||||
|
<string name="clear_time_zone">清除時區</string>
|
||||||
<string name="filter_icon">過濾器</string>
|
<string name="filter_icon">過濾器</string>
|
||||||
|
<string name="remove_filter">移除篩選條件</string>
|
||||||
|
<string name="show_iaq_legend">顯示空氣品質圖例</string>
|
||||||
|
<string name="action_show_message_status">顯示訊息狀態</string>
|
||||||
|
<string name="action_send_reply">傳送回覆</string>
|
||||||
|
<string name="action_copy_message">複製訊息</string>
|
||||||
|
<string name="action_select_message">選擇訊息</string>
|
||||||
|
<string name="action_delete_message">刪除訊息</string>
|
||||||
|
<string name="action_react_with_emoji">使用表情符號回應</string>
|
||||||
<string name="action_select_device">選擇裝置</string>
|
<string name="action_select_device">選擇裝置</string>
|
||||||
|
<string name="action_select_network">選擇網路</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,14 @@ class AndroidGetDiscoveredDevicesUseCase(
|
||||||
override fun invoke(showMock: Boolean): Flow<DiscoveredDevices> {
|
override fun invoke(showMock: Boolean): Flow<DiscoveredDevices> {
|
||||||
val nodeDb = nodeRepository.nodeDBbyNum
|
val nodeDb = nodeRepository.nodeDBbyNum
|
||||||
|
|
||||||
val bondedBleFlow = bluetoothRepository.state.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } }
|
// Filter out non-Meshtastic peripherals (headphones, cars, watches, etc.).
|
||||||
|
// BluetoothAdapter.bondedDevices returns every bonded device on the phone, so we
|
||||||
|
// must restrict the picker to entries whose advertised name matches the
|
||||||
|
// Meshtastic firmware pattern (see MeshtasticBleConstants.BLE_NAME_PATTERN).
|
||||||
|
val bondedBleFlow =
|
||||||
|
bluetoothRepository.state.map { ble ->
|
||||||
|
ble.bondedDevices.filter { it.getMeshtasticShortName() != null }.map { DeviceListEntry.Ble(it) }
|
||||||
|
}
|
||||||
|
|
||||||
val processedTcpFlow =
|
val processedTcpFlow =
|
||||||
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) {
|
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) {
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,7 @@ internal fun handleNodeAction(
|
||||||
val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode)
|
val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode)
|
||||||
navigateToMessages(route)
|
navigateToMessages(route)
|
||||||
}
|
}
|
||||||
is NodeMenuAction.Remove -> {
|
is NodeMenuAction.Remove -> viewModel.handleNodeMenuAction(menuAction, onNavigateUp)
|
||||||
viewModel.handleNodeMenuAction(menuAction)
|
|
||||||
onNavigateUp()
|
|
||||||
}
|
|
||||||
else -> viewModel.handleNodeMenuAction(menuAction)
|
else -> viewModel.handleNodeMenuAction(menuAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,10 @@ class NodeDetailViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dispatches high-level node management actions like removal, muting, or favoriting. */
|
/** Dispatches high-level node management actions like removal, muting, or favoriting. */
|
||||||
fun handleNodeMenuAction(action: NodeMenuAction) {
|
fun handleNodeMenuAction(action: NodeMenuAction, onAfterRemove: () -> Unit = {}) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is NodeMenuAction.Remove -> nodeManagementActions.requestRemoveNode(viewModelScope, action.node)
|
is NodeMenuAction.Remove ->
|
||||||
|
nodeManagementActions.requestRemoveNode(viewModelScope, action.node, onAfterRemove)
|
||||||
is NodeMenuAction.Ignore -> nodeManagementActions.requestIgnoreNode(viewModelScope, action.node)
|
is NodeMenuAction.Ignore -> nodeManagementActions.requestIgnoreNode(viewModelScope, action.node)
|
||||||
is NodeMenuAction.Mute -> nodeManagementActions.requestMuteNode(viewModelScope, action.node)
|
is NodeMenuAction.Mute -> nodeManagementActions.requestMuteNode(viewModelScope, action.node)
|
||||||
is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node)
|
is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node)
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,14 @@ constructor(
|
||||||
private val radioController: RadioController,
|
private val radioController: RadioController,
|
||||||
private val alertManager: AlertManager,
|
private val alertManager: AlertManager,
|
||||||
) {
|
) {
|
||||||
open fun requestRemoveNode(scope: CoroutineScope, node: Node) {
|
open fun requestRemoveNode(scope: CoroutineScope, node: Node, onAfterRemove: () -> Unit = {}) {
|
||||||
alertManager.showAlert(
|
alertManager.showAlert(
|
||||||
titleRes = Res.string.remove,
|
titleRes = Res.string.remove,
|
||||||
messageRes = Res.string.remove_node_text,
|
messageRes = Res.string.remove_node_text,
|
||||||
onConfirm = { removeNode(scope, node.num) },
|
onConfirm = {
|
||||||
|
removeNode(scope, node.num)
|
||||||
|
onAfterRemove()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Meshtastic LLC
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.meshtastic.feature.node.detail
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import dev.mokkery.answering.returns
|
||||||
|
import dev.mokkery.every
|
||||||
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.meshtastic.core.model.Node
|
||||||
|
import org.meshtastic.core.repository.ServiceRepository
|
||||||
|
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||||
|
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
|
||||||
|
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||||
|
import org.meshtastic.proto.User
|
||||||
|
import kotlin.test.AfterTest
|
||||||
|
import kotlin.test.BeforeTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class HandleNodeActionTest {
|
||||||
|
|
||||||
|
private val testDispatcher = UnconfinedTestDispatcher()
|
||||||
|
private val nodeManagementActions: NodeManagementActions = mock()
|
||||||
|
private val nodeRequestActions: NodeRequestActions = mock()
|
||||||
|
private val serviceRepository: ServiceRepository = mock()
|
||||||
|
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
|
||||||
|
|
||||||
|
@BeforeTest
|
||||||
|
fun setUp() {
|
||||||
|
Dispatchers.setMain(testDispatcher)
|
||||||
|
every { getNodeDetailsUseCase(any()) } returns emptyFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterTest
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `remove action delegates to viewModel and does not navigate up immediately`() = runTest(testDispatcher) {
|
||||||
|
val node = Node(num = 1234, user = User(id = "!1234"))
|
||||||
|
every { nodeManagementActions.requestRemoveNode(any(), any(), any()) } returns Unit
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
var navigateUpCalled = false
|
||||||
|
|
||||||
|
handleNodeAction(
|
||||||
|
action = NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(node)),
|
||||||
|
uiState = NodeDetailUiState(),
|
||||||
|
navigateToMessages = {},
|
||||||
|
onNavigateUp = { navigateUpCalled = true },
|
||||||
|
onNavigate = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
verify { nodeManagementActions.requestRemoveNode(any(), node, any()) }
|
||||||
|
assertFalse(navigateUpCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel() = NodeDetailViewModel(
|
||||||
|
savedStateHandle = SavedStateHandle(mapOf("destNum" to 1234)),
|
||||||
|
nodeManagementActions = nodeManagementActions,
|
||||||
|
nodeRequestActions = nodeRequestActions,
|
||||||
|
serviceRepository = serviceRepository,
|
||||||
|
getNodeDetailsUseCase = getNodeDetailsUseCase,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ import org.meshtastic.core.testing.FakeRadioController
|
||||||
import org.meshtastic.core.ui.util.AlertManager
|
import org.meshtastic.core.ui.util.AlertManager
|
||||||
import org.meshtastic.proto.User
|
import org.meshtastic.proto.User
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class NodeManagementActionsTest {
|
class NodeManagementActionsTest {
|
||||||
|
|
@ -69,4 +70,23 @@ class NodeManagementActionsTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun requestRemoveNode_invokes_onAfterRemove_when_user_confirms() {
|
||||||
|
val realAlertManager = AlertManager()
|
||||||
|
val actionsWithRealAlert =
|
||||||
|
NodeManagementActions(
|
||||||
|
nodeRepository = nodeRepository,
|
||||||
|
serviceRepository = serviceRepository,
|
||||||
|
radioController = radioController,
|
||||||
|
alertManager = realAlertManager,
|
||||||
|
)
|
||||||
|
val node = Node(num = 123, user = User(long_name = "Test Node"))
|
||||||
|
var afterRemoveCalled = false
|
||||||
|
|
||||||
|
actionsWithRealAlert.requestRemoveNode(testScope, node) { afterRemoveCalled = true }
|
||||||
|
realAlertManager.currentAlert.value?.onConfirm?.invoke()
|
||||||
|
|
||||||
|
assertTrue(afterRemoveCalled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,48 @@
|
||||||
package org.meshtastic.feature.widget
|
package org.meshtastic.feature.widget
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
import androidx.glance.appwidget.updateAll
|
import androidx.glance.appwidget.updateAll
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
import org.meshtastic.core.repository.AppWidgetUpdater
|
import org.meshtastic.core.repository.AppWidgetUpdater
|
||||||
|
|
||||||
|
private const val WIDGET_UPDATE_DEBOUNCE_MS = 500L
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class AndroidAppWidgetUpdater(private val context: Context) : AppWidgetUpdater {
|
class AndroidAppWidgetUpdater(private val context: Context, stateProvider: LocalStatsWidgetStateProvider) :
|
||||||
|
AppWidgetUpdater {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Observe state changes and trigger a widget re-render whenever the data changes.
|
||||||
|
// Glance compositions are ephemeral — the widget cannot self-update via collectAsState()
|
||||||
|
// alone, so we must call updateAll() externally to drive re-renders.
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
scope.launch {
|
||||||
|
stateProvider.state
|
||||||
|
.debounce(WIDGET_UPDATE_DEBOUNCE_MS)
|
||||||
|
.distinctUntilChanged { old, new -> old.copy(updateTimeMillis = 0) == new.copy(updateTimeMillis = 0) }
|
||||||
|
.collect { if (hasWidgetInstances()) updateAll() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun hasWidgetInstances(): Boolean =
|
||||||
|
GlanceAppWidgetManager(context).getGlanceIds(LocalStatsWidget::class.java).isNotEmpty()
|
||||||
|
|
||||||
override suspend fun updateAll() {
|
override suspend fun updateAll() {
|
||||||
// Kickstart the widget composition.
|
|
||||||
// The widget internally uses collectAsState() and its own sampled StateFlow
|
|
||||||
// to drive updates automatically without excessive IPC and recreation.
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
try {
|
try {
|
||||||
LocalStatsWidget().updateAll(context)
|
LocalStatsWidget().updateAll(context)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
co.touchlab.kermit.Logger.e(e) { "Failed to update widgets" }
|
Logger.e(e) { "Failed to update widgets" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,6 @@ data class LocalStatsWidgetUiState(
|
||||||
val updateTimeMillis: Long = 0,
|
val updateTimeMillis: Long = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val WIDGET_SUBSCRIPTION_TIMEOUT_MS = 5_000L
|
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepository: ServiceRepository) {
|
class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepository: ServiceRepository) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
|
@ -100,12 +98,7 @@ class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepos
|
||||||
.map { input ->
|
.map { input ->
|
||||||
mapToUiState(input.connectionState, input.totalNodes, input.onlineNodes, input.stats, input.localNode)
|
mapToUiState(input.connectionState, input.totalNodes, input.onlineNodes, input.stats, input.localNode)
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = LocalStatsWidgetUiState())
|
||||||
.stateIn(
|
|
||||||
scope = scope,
|
|
||||||
started = SharingStarted.WhileSubscribed(WIDGET_SUBSCRIPTION_TIMEOUT_MS),
|
|
||||||
initialValue = LocalStatsWidgetUiState(),
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class StateInput(
|
private data class StateInput(
|
||||||
val connectionState: ConnectionState,
|
val connectionState: ConnectionState,
|
||||||
|
|
|
||||||
20
feature/widget/src/main/res/values/strings.xml
Normal file
20
feature/widget/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2025-2026 Meshtastic LLC
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="widget_local_stats_label">Meshtastic</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:label="@string/widget_local_stats_label"
|
||||||
android:initialLayout="@layout/glance_default_loading_layout"
|
android:initialLayout="@layout/glance_default_loading_layout"
|
||||||
android:previewLayout="@layout/widget_local_stats_preview"
|
android:previewLayout="@layout/widget_local_stats_preview"
|
||||||
android:minWidth="110dp"
|
android:minWidth="110dp"
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ uri-kmp = "0.0.21"
|
||||||
osmdroid-android = "6.1.20"
|
osmdroid-android = "6.1.20"
|
||||||
spotless = "8.4.0"
|
spotless = "8.4.0"
|
||||||
wire = "6.2.0"
|
wire = "6.2.0"
|
||||||
vico = "3.1.0"
|
vico = "3.2.0-next.1"
|
||||||
kable = "0.42.0"
|
kable = "0.42.0"
|
||||||
mqttastic = "0.2.0"
|
mqttastic = "0.2.0"
|
||||||
jmdns = "3.6.3"
|
jmdns = "3.6.3"
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ dependencyResolutionManagement {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.gradle.toolchains.foojay-resolver") version "1.0.0"
|
id("org.gradle.toolchains.foojay-resolver") version "1.0.0"
|
||||||
id("com.gradle.develocity") version("4.4.0")
|
id("com.gradle.develocity") version("4.4.1")
|
||||||
id("com.gradle.common-custom-user-data-gradle-plugin") version "2.6.0"
|
id("com.gradle.common-custom-user-data-gradle-plugin") version "2.6.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue