Merge branch 'main' into release/2.7.0

This commit is contained in:
James Rich 2025-09-05 15:12:26 -05:00 committed by GitHub
commit 35b7845b7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 777 additions and 299 deletions

View file

@ -232,6 +232,7 @@ dependencies {
implementation(libs.bundles.adaptive)
implementation(libs.bundles.lifecycle)
implementation(libs.bundles.navigation)
implementation(libs.bundles.navigation3)
implementation(libs.bundles.coroutines)
implementation(libs.bundles.datastore)
implementation(libs.bundles.room)

View file

@ -193,12 +193,6 @@
"title": "the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS",
"page_url": "https://github.com/meshtastic/firmware/pull/7658",
"zip_url": "https://github.com/meshtastic/firmware/actions/runs/17074730483"
},
{
"id": "7583",
"title": "chore(deps): update meshtastic/web to v2.6.6",
"page_url": "https://github.com/meshtastic/firmware/pull/7583",
"zip_url": "https://github.com/meshtastic/firmware/actions/runs/17070663764"
}
]
}

View file

@ -28,7 +28,6 @@ import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
@ -47,7 +46,6 @@ import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.DEEP_LINK_BASE_URI
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
@ -117,11 +115,7 @@ class MainActivity :
},
)
} else {
MainScreen(
uIViewModel = model,
bluetoothViewModel = bluetoothViewModel,
onAction = ::onMainMenuAction,
)
MainScreen(uIViewModel = model, bluetoothViewModel = bluetoothViewModel)
}
}
}
@ -204,30 +198,7 @@ class MainActivity :
return resultPendingIntent!!
}
private val createRangetestLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { file_uri -> model.saveRangetestCSV(file_uri) }
}
}
private fun showSettingsPage() {
createSettingsIntent().send()
}
private fun onMainMenuAction(action: MainMenuAction) {
when (action) {
MainMenuAction.EXPORT_RANGETEST -> {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
}
createRangetestLauncher.launch(intent)
}
else -> warn("Unexpected action: $action")
}
}
}

View file

@ -63,7 +63,6 @@ import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.util.getShortDate
import com.geeksville.mesh.util.positionToMeter
@ -848,7 +847,7 @@ constructor(
/** Write the persisted packet data out to a CSV file in the specified location. */
@Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod")
fun saveRangetestCSV(uri: Uri) {
fun saveRangeTestCsv(uri: Uri) {
viewModelScope.launch(Dispatchers.Main) {
// Extract distances to this device from position messages and put (node,SNR,distance)
// in
@ -1001,12 +1000,8 @@ constructor(
private val _showAppIntro: MutableStateFlow<Boolean> = MutableStateFlow(!uiPrefs.appIntroCompleted)
val showAppIntro: StateFlow<Boolean> = _showAppIntro.asStateFlow()
fun onMainMenuAction(action: MainMenuAction) {
when (action) {
MainMenuAction.SHOW_INTRO -> _showAppIntro.update { true }
else -> Unit
}
fun showAppIntro() {
_showAppIntro.update { true }
}
// endregion

View file

@ -1177,7 +1177,9 @@ class MeshService :
// Generate our own hopsAway, comparing hopStart to hopLimit.
it.hopsAway =
if (packet.hopStart == 0 || packet.hopLimit > packet.hopStart) {
if (packet.decoded.portnumValue == Portnums.PortNum.RANGE_TEST_APP_VALUE) {
0 // These don't come with the .hop params, but do not propogate, so they must be 0
} else if (packet.hopStart == 0 || packet.hopLimit > packet.hopStart) {
-1
} else {
packet.hopStart - packet.hopLimit

View file

@ -94,7 +94,6 @@ import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
@ -146,7 +145,6 @@ fun MainScreen(
uIViewModel: UIViewModel = hiltViewModel(),
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
scanModel: BTScanModel = hiltViewModel(),
onAction: (MainMenuAction) -> Unit,
) {
val navController = rememberNavController()
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
@ -349,27 +347,19 @@ fun MainScreen(
viewModel = uIViewModel,
navController = navController,
onAction = { action ->
if (action is MainMenuAction) {
when (action) {
MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat)
MainMenuAction.SHOW_INTRO -> uIViewModel.onMainMenuAction(action)
else -> onAction(action)
when (action) {
is NodeMenuAction.MoreDetails -> {
navController.navigate(
NodesRoutes.NodeDetailGraph(action.node.num),
{
launchSingleTop = true
restoreState = true
},
)
}
} else if (action is NodeMenuAction) {
when (action) {
is NodeMenuAction.MoreDetails -> {
navController.navigate(
NodesRoutes.NodeDetailGraph(action.node.num),
{
launchSingleTop = true
restoreState = true
},
)
}
is NodeMenuAction.Share -> sharedContact = action.node
else -> {}
}
is NodeMenuAction.Share -> sharedContact = action.node
else -> {}
}
},
)

View file

@ -17,27 +17,21 @@
package com.geeksville.mesh.ui.common.components
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@ -45,6 +39,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hasRoute
@ -61,7 +56,7 @@ import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.debug.DebugMenuActions
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.settings.radio.RadioConfigMenuActions
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@Suppress("CyclomaticComplexMethod")
@Composable
@ -69,7 +64,7 @@ fun MainAppBar(
modifier: Modifier = Modifier,
viewModel: UIViewModel = hiltViewModel(),
navController: NavHostController,
onAction: (Any?) -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination
@ -117,13 +112,7 @@ fun MainAppBar(
actions = {
currentDestination?.let {
when {
it.isTopLevel() -> MainMenuActions(onAction)
currentDestination.hasRoute<SettingsRoutes.DebugPanel>() -> DebugMenuActions()
currentDestination.hasRoute<SettingsRoutes.Settings>() ->
RadioConfigMenuActions(viewModel = viewModel)
else -> {}
}
}
@ -144,7 +133,7 @@ private fun MainAppBar(
canNavigateUp: Boolean,
onNavigateUp: () -> Unit,
actions: @Composable () -> Unit,
onAction: (Any?) -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
TopAppBar(
title = {
@ -195,44 +184,21 @@ private fun TopBarActions(
isConnected: Boolean,
showNodeChip: Boolean,
actions: @Composable () -> Unit,
onAction: (Any?) -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
AnimatedVisibility(showNodeChip) {
ourNode?.let { NodeChip(node = it, isThisNode = true, isConnected = isConnected, onAction = onAction) }
}
actions()
}
enum class MainMenuAction(@StringRes val stringRes: Int) {
EXPORT_RANGETEST(R.string.save_rangetest),
SHOW_INTRO(R.string.intro_show),
QUICK_CHAT(R.string.quick_chat),
}
@Composable
private fun MainMenuActions(onAction: (MainMenuAction) -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = { showMenu = true }) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu))
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(colorScheme.background.copy(alpha = 1f)),
) {
MainMenuAction.entries.forEach { action ->
DropdownMenuItem(
text = { Text(stringResource(id = action.stringRes)) },
onClick = {
onAction(action)
showMenu = false
},
enabled = true,
AnimatedVisibility(visible = showNodeChip, enter = fadeIn(), exit = fadeOut()) {
ourNode?.let {
NodeChip(
modifier = Modifier.padding(horizontal = 16.dp),
node = it,
isThisNode = true,
isConnected = isConnected,
onAction = onAction,
)
}
}
actions()
}
@PreviewLightDark
@ -247,7 +213,7 @@ private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavig
showNodeChip = true,
canNavigateUp = canNavigateUp,
onNavigateUp = {},
actions = { MainMenuActions(onAction = {}) },
actions = {},
) {}
}
}

View file

@ -17,7 +17,6 @@
package com.geeksville.mesh.ui.common.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
@ -68,7 +67,7 @@ fun PositionPrecisionPreference(
},
padding = PaddingValues(0.dp),
)
AnimatedVisibility(visible = value != POSITION_DISABLED) {
if (value != POSITION_DISABLED) {
SwitchPreference(
title = stringResource(R.string.precise_location),
checked = value == POSITION_ENABLED,
@ -80,7 +79,7 @@ fun PositionPrecisionPreference(
padding = PaddingValues(0.dp),
)
}
AnimatedVisibility(visible = value in (POSITION_DISABLED + 1)..<POSITION_ENABLED) {
if (value in (POSITION_DISABLED + 1) until POSITION_ENABLED) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Slider(
value = value.toFloat(),

View file

@ -15,6 +15,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions")
package com.geeksville.mesh.ui.common.components
import androidx.annotation.StringRes
@ -59,6 +61,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.getChannel
@ -160,7 +164,7 @@ private fun SecurityIconDisplay(
Icon(
imageVector = badgeIcon,
contentDescription = stringResource(R.string.security_icon_badge_warning_description),
tint = badgeIconColor ?: MaterialTheme.colorScheme.onError, // Default for contrast
tint = badgeIconColor ?: colorScheme.onError, // Default for contrast
modifier = Modifier.size(16.dp), // Adjusted badge icon size
)
}
@ -291,6 +295,29 @@ fun SecurityIcon(
externalOnClick = externalOnClick,
)
/**
* Overload for [SecurityIcon] that enables recomposition when making changes to the [ChannelSettings].
*
* @param baseContentDescription The base content description for the icon.
* @param externalOnClick Optional lambda for external actions, invoked when the icon is clicked.
*/
@Composable
fun SecurityIcon(
channelSettings: ChannelSettings,
loraConfig: LoRaConfig,
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
externalOnClick: (() -> Unit)? = null,
) {
val channel = Channel(channelSettings, loraConfig)
SecurityIcon(
isLowEntropyKey = channel.isLowEntropyKey,
isPreciseLocation = channel.isPreciseLocation,
isMqttEnabled = channel.isMqttEnabled,
baseContentDescription = baseContentDescription,
externalOnClick = externalOnClick,
)
}
/**
* Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel index. If the channel at the given
* index is not found, nothing is rendered.

View file

@ -25,7 +25,6 @@ import android.net.InetAddresses
import android.os.Build
import android.util.Patterns
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
@ -77,7 +76,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.android.gpsDisabled
@ -472,14 +470,12 @@ fun ConnectionsScreen(
}
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Row(
Text(
text = scanStatusText.orEmpty(),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = BuildConfig.VERSION_NAME, fontSize = 10.sp, textAlign = TextAlign.Start)
Text(text = scanStatusText.orEmpty(), fontSize = 10.sp, textAlign = TextAlign.End)
}
fontSize = 10.sp,
textAlign = TextAlign.End,
)
}
}
}

View file

@ -23,14 +23,17 @@ import android.os.Build
import android.provider.Settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.serialization.Serializable
/**
* Composable function for the main application introduction screen. This screen guides the user through initial setup
@ -43,7 +46,6 @@ import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun AppIntroductionScreen(onDone: () -> Unit) {
val context = LocalContext.current
val navController = rememberNavController()
val notificationPermissionState: PermissionState? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -56,56 +58,76 @@ fun AppIntroductionScreen(onDone: () -> Unit) {
listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
val locationPermissionState = rememberMultiplePermissionsState(permissions = locationPermissions)
NavHost(navController = navController, startDestination = IntroRoute.Welcome.route) {
composable(IntroRoute.Welcome.route) {
WelcomeScreen(onGetStarted = { navController.navigate(IntroRoute.Notifications.route) })
}
composable(IntroRoute.Notifications.route) {
val notificationsAlreadyGranted = notificationPermissionState?.status?.isGranted ?: true
NotificationsScreen(
showNextButton = notificationsAlreadyGranted,
onSkip = { navController.navigate(IntroRoute.Location.route) },
onConfigure = {
if (notificationsAlreadyGranted) {
navController.navigate(IntroRoute.CriticalAlerts.route)
} else {
// For Android Tiramisu (API 33) and above, this requests POST_NOTIFICATIONS
// For lower versions, notificationPermissionState will be null, and this branch isn't taken.
notificationPermissionState.launchPermissionRequest()
}
},
)
}
composable(IntroRoute.CriticalAlerts.route) {
CriticalAlertsScreen(
onSkip = { navController.navigate(IntroRoute.Location.route) },
onConfigure = {
// Intent to open the specific notification channel settings for "my_alerts"
// This allows the user to enable critical alerts if they were initially denied
// or to adjust settings for notifications that can bypass Do Not Disturb.
val intent =
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, "my_alerts")
val backStack = rememberNavBackStack(Welcome)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider =
entryProvider {
entry<Welcome> { WelcomeScreen(onGetStarted = { backStack.add(Notifications) }) }
entry<Notifications> {
val notificationsAlreadyGranted = notificationPermissionState?.status?.isGranted ?: true
NotificationsScreen(
showNextButton = notificationsAlreadyGranted,
onSkip = {
// Skip this screen and the Critical Alerts screen. Proceed to Location screen.
backStack.add(Location)
},
onConfigure = {
if (notificationsAlreadyGranted) {
backStack.add(CriticalAlerts)
} else {
// For Android Tiramisu (API 33) and above, this requests POST_NOTIFICATIONS
// For lower versions, notificationPermissionState will be null, and this branch isn't
// taken.
notificationPermissionState?.launchPermissionRequest()
}
context.startActivity(intent)
navController.navigate(IntroRoute.Location.route)
},
)
}
composable(IntroRoute.Location.route) {
val locationAlreadyGranted = locationPermissionState.allPermissionsGranted
LocationScreen(
showNextButton = locationAlreadyGranted,
onSkip = onDone, // Callback to signify completion of the intro flow
onConfigure = {
if (locationAlreadyGranted) {
onDone() // Permissions already granted, proceed to finish
} else {
locationPermissionState.launchMultiplePermissionRequest()
}
},
)
}
}
},
)
}
entry<CriticalAlerts> {
CriticalAlertsScreen(
onSkip = { backStack.add(Location) },
onConfigure = {
// Intent to open the specific notification channel settings for "my_alerts"
// This allows the user to enable critical alerts if they were initially denied
// or to adjust settings for notifications that can bypass Do Not Disturb.
val intent =
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, "my_alerts")
}
context.startActivity(intent)
backStack.add(Location)
},
)
}
entry<Location> {
val locationAlreadyGranted = locationPermissionState.allPermissionsGranted
LocationScreen(
showNextButton = locationAlreadyGranted,
onSkip = onDone, // Callback to signify completion of the intro flow
onConfigure = {
if (locationAlreadyGranted) {
onDone() // Permissions already granted, proceed to finish
} else {
locationPermissionState.launchMultiplePermissionRequest()
}
},
)
}
},
)
}
@Serializable private data object Welcome : NavKey
@Serializable private data object Notifications : NavKey
@Serializable private data object CriticalAlerts : NavKey
@Serializable private data object Location : NavKey

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.intro
/** Sealed class defining type-safe navigation routes for the app introduction flow. */
sealed class IntroRoute(val route: String) {
object Welcome : IntroRoute("welcome")
object Notifications : IntroRoute("notifications")
object Location : IntroRoute("location")
object CriticalAlerts : IntroRoute("critical_alerts")
}

View file

@ -256,7 +256,7 @@ private fun VoltageCurrentDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics
Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = "%s %.2f A".format(stringResource(R.string.current), current),
text = "%s %.2f mA".format(stringResource(R.string.current), current),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)

View file

@ -19,8 +19,10 @@ package com.geeksville.mesh.ui.settings
import android.app.Activity
import android.content.Intent
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -30,8 +32,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.rounded.FormatPaint
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Output
import androidx.compose.material.icons.rounded.WavingHand
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -41,8 +48,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.DeviceUIProtos.Language
import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.model.UIViewModel
@ -51,12 +58,15 @@ import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import com.geeksville.mesh.util.LanguageUtils
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@ -165,7 +175,7 @@ fun SettingsScreen(
onNavigate = onNavigate,
)
TitledCard(title = stringResource(R.string.phone_settings), modifier = Modifier.padding(top = 16.dp)) {
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
SettingsItemSwitch(
text = stringResource(R.string.analytics_okay),
@ -212,6 +222,74 @@ fun SettingsScreen(
choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } },
)
}
val exportRangeTestLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { uri -> uiViewModel.saveRangeTestCsv(uri) }
}
}
SettingsItem(
text = stringResource(R.string.save_rangetest),
leadingIcon = Icons.Rounded.Output,
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
}
exportRangeTestLauncher.launch(intent)
}
SettingsItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
trailingIcon = null,
) {
uiViewModel.showAppIntro()
}
AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() }
}
}
}
private const val UNLOCK_CLICK_COUNT = 5 // Number of clicks required to unlock excluded modules.
private const val UNLOCKED_CLICK_COUNT = 3 // Number of clicks before we toast that modules are already unlocked.
private const val UNLOCK_TIMEOUT_SECONDS = 1 // Timeout in seconds to reset the click counter.
/** A button to display the app version. Clicking it 5 times will unlock the excluded modules. */
@Composable
private fun AppVersionButton(excludedModulesUnlocked: Boolean, onUnlockExcludedModules: () -> Unit) {
val context = LocalContext.current
var clickCount by remember { mutableIntStateOf(0) }
LaunchedEffect(clickCount) {
if (clickCount in 1..<UNLOCK_CLICK_COUNT) {
delay(UNLOCK_TIMEOUT_SECONDS.seconds)
clickCount = 0
}
}
SettingsItemDetail(
text = stringResource(R.string.app_version),
icon = Icons.Rounded.Memory,
trailingText = BuildConfig.VERSION_NAME,
) {
clickCount = clickCount.inc().coerceIn(0, UNLOCK_CLICK_COUNT)
when {
clickCount == UNLOCKED_CLICK_COUNT && excludedModulesUnlocked -> {
clickCount = 0
Toast.makeText(context, context.getString(R.string.modules_already_unlocked), Toast.LENGTH_LONG).show()
}
clickCount == UNLOCK_CLICK_COUNT -> {
clickCount = 0
onUnlockExcludedModules()
Toast.makeText(context, context.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
}
}
}
}

View file

@ -17,7 +17,6 @@
package com.geeksville.mesh.ui.settings.radio
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@ -25,24 +24,19 @@ import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.CleaningServices
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.AdminRoute
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
@ -53,8 +47,6 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.radio.components.WarningDialog
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@ -168,32 +160,6 @@ fun RadioConfigItemList(
}
}
private const val UNLOCK_CLICK_COUNT = 5 // Number of clicks required to unlock excluded modules.
private const val UNLOCK_TIMEOUT_SECONDS = 3 // Timeout in seconds to reset the click counter.
@Composable
fun RadioConfigMenuActions(modifier: Modifier = Modifier, viewModel: UIViewModel = hiltViewModel()) {
val context = LocalContext.current
var counter by remember { mutableIntStateOf(0) }
LaunchedEffect(counter) {
if (counter > 0 && counter < UNLOCK_CLICK_COUNT) {
delay(UNLOCK_TIMEOUT_SECONDS.seconds)
counter = 0
}
}
IconButton(
enabled = counter < UNLOCK_CLICK_COUNT,
onClick = {
counter++
if (counter == UNLOCK_CLICK_COUNT) {
viewModel.unlockExcludedModules()
Toast.makeText(context, context.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
}
},
modifier = modifier,
) {}
}
@Preview(showBackground = true)
@Composable
private fun RadioSettingsScreenPreview() = AppTheme {

View file

@ -0,0 +1,180 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.settings.radio.components
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.model.DeviceVersion
/**
* At this firmware version periodic position sharing on a secondary channel was implemented. To enable this feature the
* user must disable position on the primary channel and enable on a secondary channel. The lowest indexed secondary
* channel with the position enabled will conduct the automatic position broadcasts.
*/
internal const val SECONDARY_CHANNEL_EPOCH = "2.6.10"
internal enum class ChannelIcons(
val icon: ImageVector,
@StringRes val descriptionResId: Int,
@StringRes val additionalInfoResId: Int,
) {
LOCATION(
icon = Icons.Filled.LocationOn,
descriptionResId = R.string.location_sharing,
additionalInfoResId = R.string.periodic_position_broadcast,
),
UPLINK(
icon = Icons.Filled.CloudUpload,
descriptionResId = R.string.uplink_enabled,
additionalInfoResId = R.string.uplink_feature_description,
),
DOWNLINK(
icon = Icons.Filled.CloudDownload,
descriptionResId = R.string.downlink_enabled,
additionalInfoResId = R.string.downlink_feature_description,
),
}
@Composable
internal fun ChannelLegend(onClick: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth().clickable { onClick.invoke() },
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Row {
Icon(imageVector = Icons.Filled.Info, contentDescription = stringResource(R.string.info))
Text(
text = stringResource(R.string.primary),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp),
)
}
Text(
text = stringResource(R.string.secondary),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(start = 16.dp),
)
}
}
@Composable
internal fun ChannelLegendDialog(firmwareVersion: DeviceVersion, onDismiss: () -> Unit) {
AlertDialog(
modifier = Modifier.fillMaxSize(),
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.channel_features)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
Text(
text = stringResource(R.string.primary),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleMedium,
)
Text(
text = "- ${stringResource(R.string.primary_channel_feature)}",
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringResource(R.string.secondary),
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.titleMedium,
)
Text(
text = "- ${stringResource(R.string.secondary_no_telemetry)}",
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.bodyMedium,
)
Text(
text =
if (firmwareVersion >= DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {
/* 2.6.10+ */
"- ${stringResource(R.string.secondary_channel_position_feature)}"
} else {
"- ${stringResource(R.string.manual_position_request)}"
},
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.bodyMedium,
)
IconDefinitions()
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.security_icon_help_dismiss)) }
}
},
)
}
@Composable
private fun IconDefinitions() {
Text(text = stringResource(R.string.icon_meanings), style = MaterialTheme.typography.titleLarge)
ChannelIcons.entries.forEach { icon ->
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = icon.icon, contentDescription = stringResource(icon.descriptionResId))
Column(modifier = Modifier.padding(start = 16.dp)) {
Text(text = stringResource(icon.descriptionResId), style = MaterialTheme.typography.titleMedium)
Text(text = stringResource(icon.additionalInfoResId), style = MaterialTheme.typography.bodyMedium)
}
}
if (icon != ChannelIcons.entries.lastOrNull()) {
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}
}
@Preview
@Composable
private fun PreviewChannelLegendDialog() {
ChannelLegendDialog(firmwareVersion = DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {}
}

View file

@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
@ -59,6 +58,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
@ -73,6 +73,7 @@ import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SecurityIcon
@ -89,18 +90,20 @@ private fun ChannelItem(
onClick: () -> Unit = {},
content: @Composable RowScope.() -> Unit,
) {
val fontColor = if (index == 0) MaterialTheme.colorScheme.primary else Color.Unspecified
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp).clickable(enabled = enabled) { onClick() }) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp),
) {
AssistChip(onClick = onClick, label = { Text(text = "$index") })
AssistChip(onClick = onClick, label = { Text(text = "$index", color = fontColor) })
Text(
text = title,
modifier = Modifier.weight(1f),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.bodyLarge,
color = fontColor,
)
content()
}
@ -112,11 +115,34 @@ private fun ChannelCard(
index: Int,
title: String,
enabled: Boolean,
channelSettings: ChannelSettings,
loraConfig: LoRaConfig,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
channel: Channel,
sharesLocation: Boolean,
) = ChannelItem(index = index, title = title, enabled = enabled, onClick = onEditClick) {
SecurityIcon(channel)
if (sharesLocation) {
Icon(
imageVector = ChannelIcons.LOCATION.icon,
contentDescription = stringResource(ChannelIcons.LOCATION.descriptionResId),
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
)
}
if (channelSettings.uplinkEnabled) {
Icon(
imageVector = ChannelIcons.UPLINK.icon,
contentDescription = stringResource(ChannelIcons.UPLINK.descriptionResId),
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
)
}
if (channelSettings.downlinkEnabled) {
Icon(
imageVector = ChannelIcons.DOWNLINK.icon,
contentDescription = stringResource(ChannelIcons.DOWNLINK.descriptionResId),
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
)
}
SecurityIcon(channelSettings, loraConfig)
Spacer(modifier = Modifier.width(10.dp))
IconButton(onClick = { onDeleteClick() }) {
Icon(
@ -135,7 +161,7 @@ fun ChannelSelection(
isSelected: Boolean,
onSelected: (Boolean) -> Unit,
channel: Channel,
) = ChannelItem(index = index, title = title, enabled = enabled, onClick = {}) {
) = ChannelItem(index = index, title = title, enabled = enabled) {
SecurityIcon(channel)
Spacer(modifier = Modifier.width(10.dp))
Checkbox(enabled = enabled, checked = isSelected, onCheckedChange = onSelected)
@ -152,25 +178,28 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
ChannelSettingsItemList(
settingsList = state.channelList,
loraConfig = state.radioConfig.lora,
enabled = state.connected,
maxChannels = viewModel.maxChannels,
firmwareVersion = state.metadata?.firmwareVersion ?: "0.0.0",
enabled = state.connected,
onPositiveClicked = { channelListInput -> viewModel.updateChannels(channelListInput, state.channelList) },
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun ChannelSettingsItemList(
private fun ChannelSettingsItemList(
settingsList: List<ChannelSettings>,
loraConfig: LoRaConfig,
maxChannels: Int = 8,
firmwareVersion: String,
enabled: Boolean,
onNegativeClicked: () -> Unit = {},
onPositiveClicked: (List<ChannelSettings>) -> Unit,
) {
val primarySettings = settingsList.getOrNull(0) ?: return
val modemPresetName by remember(loraConfig) { mutableStateOf(Channel(loraConfig = loraConfig).name) }
val primaryChannel by remember(loraConfig) { mutableStateOf(Channel(primarySettings, loraConfig)) }
val fwVersion by
remember(firmwareVersion) { mutableStateOf(DeviceVersion(firmwareVersion.substringBeforeLast("."))) }
val focusManager = LocalFocusManager.current
val settingsListInput =
@ -180,7 +209,7 @@ fun ChannelSettingsItemList(
val listState = rememberLazyListState()
val dragDropState =
rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
rememberDragDropState(listState) { fromIndex, toIndex ->
if (toIndex in settingsListInput.indices && fromIndex in settingsListInput.indices) {
settingsListInput.apply { add(toIndex, removeAt(fromIndex)) }
}
@ -191,6 +220,7 @@ fun ChannelSettingsItemList(
settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 }
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
var showChannelLegendDialog by rememberSaveable { mutableStateOf(false) }
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
@ -209,6 +239,10 @@ fun ChannelSettingsItemList(
)
}
if (showChannelLegendDialog) {
ChannelLegendDialog(fwVersion) { showChannelLegendDialog = false }
}
Box(modifier = Modifier.fillMaxSize().clickable(onClick = {}, enabled = false)) {
Column {
ChannelsConfigHeader(
@ -230,11 +264,11 @@ fun ChannelSettingsItemList(
fontSize = 11.sp,
modifier = Modifier.padding(start = 16.dp),
)
Text(
text = stringResource(R.string.primary),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp),
)
ChannelLegend { showChannelLegendDialog = true }
val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList())
LazyColumn(
modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
state = listState,
@ -245,38 +279,16 @@ fun ChannelSettingsItemList(
channel,
isDragging,
->
val channelObj = Channel(channel, loraConfig)
ChannelCard(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
channelSettings = channel,
loraConfig = loraConfig,
onEditClick = { showEditChannelDialog = index },
onDeleteClick = { settingsListInput.removeAt(index) },
channel = channelObj,
sharesLocation = locationChannel == index,
)
if (index == 0 && !isDragging) {
Text(
text = stringResource(R.string.primary_channel_feature),
color = MaterialTheme.colorScheme.primary,
fontSize = 10.sp,
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = stringResource(R.string.secondary), color = MaterialTheme.colorScheme.onBackground)
}
}
item {
Column {
Text(
text = stringResource(R.string.secondary_no_telemetry),
color = MaterialTheme.colorScheme.onBackground,
fontSize = 10.sp,
)
Text(
text = stringResource(R.string.manual_position_request),
color = MaterialTheme.colorScheme.onBackground,
fontSize = 10.sp,
)
}
}
item {
PreferenceFooter(
@ -286,7 +298,6 @@ fun ChannelSettingsItemList(
focusManager.clearFocus()
settingsListInput.clear()
settingsListInput.addAll(settingsList)
onNegativeClicked()
},
positiveText = R.string.send,
onPositiveClicked = {
@ -338,6 +349,33 @@ private fun ChannelsConfigHeader(frequency: Float, slot: Int) {
}
}
/**
* Determines what [Channel] if any is enabled to conduct automatic location sharing.
*
* @param firmwareVersion of the connected node.
* @param settingsList Current list of channels on the node.
* @return the index of the channel within `settingsList`.
*/
private fun determineLocationSharingChannel(firmwareVersion: DeviceVersion, settingsList: List<ChannelSettings>): Int {
var output = -1
if (firmwareVersion >= DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {
/* Essentially the first index with the setting enabled */
for ((i, settings) in settingsList.withIndex()) {
if (settings.moduleSettings.positionPrecision > 0) {
output = i
break
}
}
} else {
/* Only the primary channel at index 0 can share locations automatically */
val primary = settingsList[0]
if (primary.moduleSettings.positionPrecision > 0) {
output = 0
}
}
return output
}
@Preview(showBackground = true)
@Composable
private fun ChannelSettingsPreview() {
@ -351,6 +389,7 @@ private fun ChannelSettingsPreview() {
channelSettings { name = stringResource(R.string.channel_name) },
),
loraConfig = Channel.default.loraConfig,
firmwareVersion = "1.3.2",
enabled = true,
onPositiveClicked = {},
)

View file

@ -782,4 +782,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -776,4 +776,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">Шаблон за URL</string>
<string name="track_point">track point</string>
<string name="phone_settings">Настройки на телефона</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Vorlage</string>
<string name="track_point">Verlaufspunkt</string>
<string name="phone_settings">Telefoneinstellungen</string>
<string name="channel_features">Kanalfunktionen</string>
<string name="location_sharing">Standortfreigabe</string>
<string name="periodic_position_broadcast">Regelmäßige Standortübertragung</string>
<string name="uplink_feature_description">Wenn aktiviert, werden Nachrichten aus dem Mesh über das konfigurierte Gateway eines beliebigen Knotens an das **öffentliche** Internet gesendet.</string>
<string name="downlink_feature_description">Nachrichten von einem öffentlichen Internet Gateway werden an das lokale Mesh weitergeleitet. Aufgrund der Nullsprungrichtlinie wird der Datenverkehr vom MQTT Standardserver nicht über dieses Gerät hinaus weitergeleitet.</string>
<string name="icon_meanings">Symbolbedeutung</string>
<string name="secondary_channel_position_feature">Durch Deaktivieren des Standortes auf dem primären Kanal werden regelmäßige Standortübertragungen auf dem ersten sekundären Kanal mit aktiviertem Standort ermöglicht, andernfalls ist eine manuelle Standortanforderung erforderlich.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -775,4 +775,11 @@ Rango de Valores 0 - 500.</string>
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL mall</string>
<string name="track_point">jälgimispunkt</string>
<string name="phone_settings">Telefoni seaded</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL-mallipohja</string>
<string name="track_point">seurantapiste</string>
<string name="phone_settings">Puhelimen asetukset</string>
<string name="channel_features">Kanavan ominaisuudet</string>
<string name="location_sharing">Sijainnin jakaminen</string>
<string name="periodic_position_broadcast">Sijainnin toistuva lähetys</string>
<string name="uplink_feature_description">Verkosta tulevat viestit lähetetään julkiseen internetiin minkä tahansa laitteen määritetyn yhdyskäytävän kautta.</string>
<string name="downlink_feature_description">Julkisesta internet-yhdyskäytävästä tulevat viestit välitetään paikalliseen mesh-verkkoon. Nollahyppysääntöjen vuoksi oletuksena MQTT-palvelimelta tuleva liikenne ei etene tätä laitetta pidemmälle.</string>
<string name="icon_meanings">Kuvakkeiden merkitykset</string>
<string name="secondary_channel_position_feature">Sijainnin poistaminen käytöstä ensisijaisella kanavalla mahdollistaa sijainnin jaksottaisen lähetyksen ensimmäisellä toissijaisella kanavalla, jossa sijainti on käytössä. Muussa tapauksessa vaaditaan manuaalinen sijaintipyyntö.</string>
</resources>

View file

@ -771,4 +771,11 @@
<string name="url_template">Modèle d\'URL</string>
<string name="track_point">Point de suivi</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -780,4 +780,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -776,4 +776,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">Template dell\'URL</string>
<string name="track_point">punto di interesse</string>
<string name="phone_settings">Impostazioni telefono</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -773,4 +773,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -772,4 +772,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">Modelo de URL</string>
<string name="track_point">ponto de rastreamento</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -776,4 +776,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -775,4 +775,11 @@
<string name="url_template">Шаблон URL</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -776,4 +776,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -774,4 +774,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -778,4 +778,11 @@
<string name="url_template">URL Template</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -773,4 +773,11 @@
<string name="url_template">URL 模板</string>
<string name="track_point">轨迹点</string>
<string name="phone_settings">Phone Settings</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -312,7 +312,7 @@
<string name="udp_config">UDP設置</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>最後接收: %2$s<br>最後位置: %3$s<br>電量: %4$s]]></string>
<string name="toggle_my_position">切換我的位置</string>
<string name="orient_north">Orient north</string>
<string name="orient_north">以北為上</string>
<string name="user">用戶</string>
<string name="channels">頻道</string>
<string name="device">裝置</string>
@ -337,7 +337,7 @@
<string name="detection_sensor">檢測傳感器</string>
<string name="paxcounter">客流量計數</string>
<string name="audio_config">音頻設置</string>
<string name="codec_2_enabled">啟用 CODEC2</string>
<string name="codec_2_enabled">CODEC 2 已啟用</string>
<string name="ptt_pin">PTT針腳</string>
<string name="codec2_sample_rate">CODEC2 取樣率</string>
<string name="i2s_word_select">I2S WS 訊號選擇</string>
@ -475,8 +475,8 @@
<string name="subnet">子網</string>
<string name="paxcounter_config">Paxcount設置</string>
<string name="paxcounter_enabled">啟用Paxcount</string>
<string name="wifi_rssi_threshold_defaults_to_80">WiFi RSSI 閾值(缺省-80)</string>
<string name="ble_rssi_threshold_defaults_to_80">藍牙 RSSI 閾值(缺省-80)</string>
<string name="wifi_rssi_threshold_defaults_to_80">WiFi RSSI 閾值(預設為-80)</string>
<string name="ble_rssi_threshold_defaults_to_80">藍牙 RSSI 閾值(預設為-80)</string>
<string name="position_config">位置設定</string>
<string name="position_broadcast_interval_seconds">位置廣播間隔(秒)</string>
<string name="smart_position_enabled">啟用智慧位置</string>
@ -772,4 +772,11 @@
<string name="url_template">URL 範本</string>
<string name="track_point">軌跡點</string>
<string name="phone_settings">手機設定</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -653,6 +653,7 @@
<string name="export_keys">Export Keys</string>
<string name="export_keys_confirmation">Exports public and private keys to a file. Please store somewhere securely.</string>
<string name="modules_unlocked">Modules unlocked</string>
<string name="modules_already_unlocked">Modules already unlocked</string>
<string name="remote">Remote</string>
<string name="node_count_template">(%1$d online / %2$d total)</string>
<string name="react">React</string>
@ -801,5 +802,13 @@
<string name="url_template">URL Template</string>
<string name="url_template_hint" translatable="false">https://a.tile.openstreetmap.org/{z}/{x}/{y}.png</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="app_settings">App</string>
<string name="app_version">Version</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
</resources>

View file

@ -32,12 +32,13 @@ kotlinx-coroutines-android = "1.10.2"
kotlinx-serialization-json = "1.9.0"
lifecycle = "2.9.3"
location-services = "21.3.0"
maps-compose = "6.8.0"
maps-compose = "6.9.0"
markdownRenderer = "0.35.0"
material = "1.13.0"
material3 = "1.5.0-alpha03"
mgrs = "2.1.3"
navigation = "2.9.3"
navigation3 = "1.0.0-alpha08"
okhttp = "5.1.0"
org-eclipse-paho-client-mqttv3 = "1.2.5"
osmbonuspack = "6.9.0"
@ -129,6 +130,8 @@ material = { group = "com.google.android.material", name = "material", version.r
mgrs = { group = "mil.nga", name = "mgrs", version.ref = "mgrs" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigation" }
navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" }
navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }
okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
org-eclipse-paho-client-mqttv3 = { group = "org.eclipse.paho", name = "org.eclipse.paho.client.mqttv3", version.ref = "org-eclipse-paho-client-mqttv3" }
@ -166,6 +169,9 @@ lifecycle = ["lifecycle-runtime-ktx", "lifecycle-livedata-ktx", "lifecycle-viewm
# Navigation
navigation = ["navigation-compose"]
# Navigation 3
navigation3 = ["navigation3-runtime", "navigation3-ui"]
# Coroutines
coroutines = ["kotlinx-coroutines-android", "kotlinx-coroutines-guava"]