mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(wire): migrate from protobuf -> wire (#4401)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9dbc8b7fbf
commit
25657e8f8f
239 changed files with 7149 additions and 6144 deletions
|
|
@ -1,33 +1,48 @@
|
|||
<?xml version="1.0" ?>
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>LongMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:EditDeviceProfileDialog.kt$@Suppress("LongMethod") @OptIn(ExperimentalLayoutApi::class) @Composable fun EditDeviceProfileDialog( title: String, deviceProfile: DeviceProfile, onConfirm: (DeviceProfile) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, )</ID>
|
||||
<ID>CyclomaticComplexMethod:ExternalNotificationConfigItemList.kt$@Suppress("LongMethod", "TooGenericExceptionCaught") @Composable fun ExternalNotificationConfigScreen( onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: RadioConfigViewModel = hiltViewModel(), )</ID>
|
||||
<ID>CyclomaticComplexMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$fun installProfile(protobuf: DeviceProfile)</ID>
|
||||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)</ID>
|
||||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun setRemoteModuleConfig(destNum: Int, config: ModuleConfig)</ID>
|
||||
<ID>CyclomaticComplexMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LargeClass:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel</ID>
|
||||
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)</ID>
|
||||
<ID>LongMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>MagicNumber:Debug.kt$3</ID>
|
||||
<ID>MagicNumber:EditChannelDialog.kt$16</ID>
|
||||
<ID>MagicNumber:EditChannelDialog.kt$32</ID>
|
||||
<ID>MagicNumber:EditDeviceProfileDialog.kt$ProfileField.CHANNEL_URL$3</ID>
|
||||
<ID>MagicNumber:EditDeviceProfileDialog.kt$ProfileField.CONFIG$4</ID>
|
||||
<ID>MagicNumber:EditDeviceProfileDialog.kt$ProfileField.FIXED_POSITION$6</ID>
|
||||
<ID>MagicNumber:EditDeviceProfileDialog.kt$ProfileField.MODULE_CONFIG$5</ID>
|
||||
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
|
||||
<ID>NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)</ID>
|
||||
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshPacket)</ID>
|
||||
<ID>TooGenericExceptionCaught:DebugViewModel.kt$DebugViewModel$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception</ID>
|
||||
<ID>TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel</ID>
|
||||
<ID>UnusedPrivateMember:RadioConfigViewModel.kt$RadioConfigViewModel$private fun setChannels(channelUrl: String)</ID>
|
||||
<ID>UnusedPrivateProperty:SettingsViewModel.kt$SettingsViewModel$val capabilities = Capabilities(node.metadata?.firmware_version)</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule
|
|||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.meshtastic.core.strings.getString
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
|
|
@ -30,28 +29,22 @@ import org.junit.runner.RunWith
|
|||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.proto.ClientOnlyProtos.DeviceProfile
|
||||
import org.meshtastic.proto.deviceProfile
|
||||
import org.meshtastic.proto.position
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class EditDeviceProfileDialogTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
private fun getString(id: Int): String = InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
|
||||
|
||||
private val title = "Export configuration"
|
||||
private val deviceProfile = deviceProfile {
|
||||
longName = "Long name"
|
||||
shortName = "Short name"
|
||||
channelUrl = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ"
|
||||
fixedPosition = position {
|
||||
latitudeI = 327766650
|
||||
longitudeI = -967969890
|
||||
altitude = 138
|
||||
}
|
||||
}
|
||||
private val deviceProfile =
|
||||
DeviceProfile(
|
||||
long_name = "Long name",
|
||||
short_name = "Short name",
|
||||
channel_url = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ",
|
||||
fixed_position = Position(latitude_i = 327766650, longitude_i = -967969890, altitude = 138),
|
||||
)
|
||||
|
||||
private fun testEditDeviceProfileDialog(onDismiss: () -> Unit = {}, onConfirm: (DeviceProfile) -> Unit = {}) =
|
||||
composeTestRule.setContent {
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.rounded.AppSettingsAlt
|
||||
import androidx.compose.material.icons.rounded.BugReport
|
||||
import androidx.compose.material.icons.rounded.FilterList
|
||||
import androidx.compose.material.icons.rounded.FormatPaint
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
|
|
@ -56,7 +54,6 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -119,7 +116,7 @@ import org.meshtastic.feature.settings.radio.component.EditDeviceProfileDialog
|
|||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.feature.settings.util.LanguageUtils
|
||||
import org.meshtastic.feature.settings.util.LanguageUtils.languageMap
|
||||
import org.meshtastic.proto.ClientOnlyProtos.DeviceProfile
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
|
@ -139,7 +136,7 @@ fun SettingsScreen(
|
|||
val ourNode by settingsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by settingsViewModel.isConnected.collectAsStateWithLifecycle(false)
|
||||
val isOtaCapable by settingsViewModel.isOtaCapable.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsState()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
var isWaiting by remember { mutableStateOf(false) }
|
||||
if (isWaiting) {
|
||||
|
|
@ -192,7 +189,7 @@ fun SettingsScreen(
|
|||
viewModel.installProfile(it)
|
||||
} else {
|
||||
deviceProfile = it
|
||||
val nodeName = it.shortName.ifBlank { "node" }
|
||||
val nodeName = (it.short_name ?: "").ifBlank { "node" }
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.getDefault())
|
||||
val dateStr = dateFormat.format(java.util.Date())
|
||||
val fileName = "Meshtastic_${nodeName}_${dateStr}_nodeConfig.cfg"
|
||||
|
|
@ -231,9 +228,9 @@ fun SettingsScreen(
|
|||
title = stringResource(Res.string.bottom_nav_settings),
|
||||
subtitle =
|
||||
if (state.isLocal) {
|
||||
ourNode?.user?.longName
|
||||
ourNode?.user?.long_name
|
||||
} else {
|
||||
val remoteName = destNode?.user?.longName ?: ""
|
||||
val remoteName = destNode?.user?.long_name ?: ""
|
||||
stringResource(Res.string.remotely_administrating, remoteName)
|
||||
},
|
||||
ourNode = ourNode,
|
||||
|
|
@ -248,7 +245,7 @@ fun SettingsScreen(
|
|||
Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(paddingValues).padding(16.dp)) {
|
||||
RadioConfigItemList(
|
||||
state = state,
|
||||
isManaged = localConfig.security.isManaged,
|
||||
isManaged = localConfig.security?.is_managed ?: false,
|
||||
node = destNode,
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
isOtaCapable = isOtaCapable,
|
||||
|
|
@ -277,180 +274,169 @@ fun SettingsScreen(
|
|||
|
||||
val context = LocalContext.current
|
||||
|
||||
if (state.isLocal) {
|
||||
TitledCard(title = stringResource(Res.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
|
||||
if (state.analyticsAvailable) {
|
||||
val allowed by viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(false)
|
||||
SwitchListItem(
|
||||
text = stringResource(Res.string.analytics_okay),
|
||||
checked = allowed,
|
||||
leadingIcon = Icons.Rounded.BugReport,
|
||||
onClick = { viewModel.toggleAnalyticsAllowed() },
|
||||
)
|
||||
}
|
||||
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
val isGpsDisabled = context.gpsDisabled()
|
||||
val provideLocation by settingsViewModel.provideLocation.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) {
|
||||
if (provideLocation) {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
if (!isGpsDisabled) {
|
||||
settingsViewModel.meshService?.startProvideLocation()
|
||||
} else {
|
||||
context.showToast(Res.string.location_disabled)
|
||||
}
|
||||
} else {
|
||||
// Request permissions if not granted and user wants to provide location
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
} else {
|
||||
settingsViewModel.meshService?.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
|
||||
TitledCard(title = stringResource(Res.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
|
||||
if (state.analyticsAvailable) {
|
||||
val allowed by viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(false)
|
||||
SwitchListItem(
|
||||
text = stringResource(Res.string.provide_location_to_mesh),
|
||||
leadingIcon = Icons.Rounded.LocationOn,
|
||||
enabled = !isGpsDisabled,
|
||||
checked = provideLocation,
|
||||
onClick = { settingsViewModel.setProvideLocation(!provideLocation) },
|
||||
text = stringResource(Res.string.analytics_okay),
|
||||
checked = allowed,
|
||||
leadingIcon = Icons.Default.BugReport,
|
||||
onClick = { viewModel.toggleAnalyticsAllowed() },
|
||||
)
|
||||
}
|
||||
|
||||
val settingsLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
) {}
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
val isGpsDisabled = context.gpsDisabled()
|
||||
val provideLocation by settingsViewModel.provideLocation.collectAsStateWithLifecycle()
|
||||
|
||||
// On Android 12 and below, system app settings for language are not available. Use the in-app
|
||||
// language
|
||||
// picker for these devices.
|
||||
val useInAppLangPicker = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
ListItem(
|
||||
text = stringResource(Res.string.preferences_language),
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
trailingIcon = if (useInAppLangPicker) null else Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
||||
) {
|
||||
if (useInAppLangPicker) {
|
||||
showLanguagePickerDialog = true
|
||||
} else {
|
||||
val intent = Intent(ACTION_APP_LOCALE_SETTINGS, "package:${context.packageName}".toUri())
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
settingsLauncher.launch(intent)
|
||||
LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) {
|
||||
if (provideLocation) {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
if (!isGpsDisabled) {
|
||||
settingsViewModel.meshService?.startProvideLocation()
|
||||
} else {
|
||||
// Fall back to the in-app picker
|
||||
showLanguagePickerDialog = true
|
||||
context.showToast(Res.string.location_disabled)
|
||||
}
|
||||
} else {
|
||||
// Request permissions if not granted and user wants to provide location
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
} else {
|
||||
settingsViewModel.meshService?.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
|
||||
SwitchListItem(
|
||||
text = stringResource(Res.string.provide_location_to_mesh),
|
||||
leadingIcon = Icons.Rounded.LocationOn,
|
||||
enabled = !isGpsDisabled,
|
||||
checked = provideLocation,
|
||||
onClick = { settingsViewModel.setProvideLocation(!provideLocation) },
|
||||
)
|
||||
|
||||
val settingsLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {}
|
||||
|
||||
// On Android 12 and below, system app settings for language are not available. Use the in-app language
|
||||
// picker for these devices.
|
||||
val useInAppLangPicker = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
ListItem(
|
||||
text = stringResource(Res.string.preferences_language),
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
trailingIcon = if (useInAppLangPicker) null else Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
||||
) {
|
||||
if (useInAppLangPicker) {
|
||||
showLanguagePickerDialog = true
|
||||
} else {
|
||||
val intent = Intent(ACTION_APP_LOCALE_SETTINGS, "package:${context.packageName}".toUri())
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
settingsLauncher.launch(intent)
|
||||
} else {
|
||||
// Fall back to the in-app picker
|
||||
showLanguagePickerDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListItem(
|
||||
text = stringResource(Res.string.theme),
|
||||
leadingIcon = Icons.Rounded.FormatPaint,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
showThemePickerDialog = true
|
||||
ListItem(
|
||||
text = stringResource(Res.string.theme),
|
||||
leadingIcon = Icons.Rounded.FormatPaint,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
showThemePickerDialog = true
|
||||
}
|
||||
|
||||
// Node DB cache limit (App setting)
|
||||
val cacheLimit = settingsViewModel.dbCacheLimit.collectAsStateWithLifecycle().value
|
||||
val cacheItems = remember {
|
||||
(DatabaseConstants.MIN_CACHE_LIMIT..DatabaseConstants.MAX_CACHE_LIMIT).map {
|
||||
it.toLong() to it.toString()
|
||||
}
|
||||
}
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.device_db_cache_limit),
|
||||
enabled = true,
|
||||
items = cacheItems,
|
||||
selectedItem = cacheLimit.toLong(),
|
||||
onItemSelected = { selected -> settingsViewModel.setDbCacheLimit(selected.toInt()) },
|
||||
summary = stringResource(Res.string.device_db_cache_limit_summary),
|
||||
)
|
||||
|
||||
ListItem(
|
||||
text = "Message Filter",
|
||||
leadingIcon = Icons.Rounded.FilterList,
|
||||
trailingIcon = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
||||
) {
|
||||
onNavigate(SettingsRoutes.FilterSettings)
|
||||
} // Node DB cache limit (App setting)
|
||||
val cacheLimit = settingsViewModel.dbCacheLimit.collectAsStateWithLifecycle().value
|
||||
val cacheItems = remember {
|
||||
(DatabaseConstants.MIN_CACHE_LIMIT..DatabaseConstants.MAX_CACHE_LIMIT).map {
|
||||
it.toLong() to it.toString()
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val nodeName = ourNode?.user?.short_name ?: ""
|
||||
|
||||
val exportRangeTestLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
}
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.device_db_cache_limit),
|
||||
enabled = true,
|
||||
items = cacheItems,
|
||||
selectedItem = cacheLimit.toLong(),
|
||||
onItemSelected = { selected -> settingsViewModel.setDbCacheLimit(selected.toInt()) },
|
||||
summary = stringResource(Res.string.device_db_cache_limit_summary),
|
||||
)
|
||||
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val nodeName = ourNode?.user?.shortName ?: ""
|
||||
|
||||
val exportRangeTestLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.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, "Meshtastic_rangetest_${nodeName}_$timestamp.csv")
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.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, "Meshtastic_rangetest_${nodeName}_$timestamp.csv")
|
||||
}
|
||||
exportRangeTestLauncher.launch(intent)
|
||||
}
|
||||
exportRangeTestLauncher.launch(intent)
|
||||
}
|
||||
|
||||
val exportDataLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
val exportDataLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.string.export_data_csv),
|
||||
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, "Meshtastic_datalog_${nodeName}_$timestamp.csv")
|
||||
}
|
||||
exportDataLauncher.launch(intent)
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.string.export_data_csv),
|
||||
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, "Meshtastic_datalog_${nodeName}_$timestamp.csv")
|
||||
}
|
||||
exportDataLauncher.launch(intent)
|
||||
}
|
||||
|
||||
ListItem(
|
||||
text = stringResource(Res.string.intro_show),
|
||||
leadingIcon = Icons.Rounded.WavingHand,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
settingsViewModel.showAppIntro()
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.string.intro_show),
|
||||
leadingIcon = Icons.Rounded.WavingHand,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
settingsViewModel.showAppIntro()
|
||||
}
|
||||
|
||||
ListItem(
|
||||
text = stringResource(Res.string.system_settings),
|
||||
leadingIcon = Icons.Rounded.AppSettingsAlt,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.fromParts("package", context.packageName, null)
|
||||
settingsLauncher.launch(intent)
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.string.system_settings),
|
||||
leadingIcon = Icons.Rounded.AppSettingsAlt,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.fromParts("package", context.packageName, null)
|
||||
settingsLauncher.launch(intent)
|
||||
}
|
||||
|
||||
ListItem(
|
||||
text = stringResource(Res.string.acknowledgements),
|
||||
leadingIcon = Icons.Rounded.Info,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
onNavigate(SettingsRoutes.About)
|
||||
}
|
||||
ListItem(
|
||||
text = stringResource(Res.string.acknowledgements),
|
||||
leadingIcon = Icons.Rounded.Info,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
onNavigate(SettingsRoutes.About)
|
||||
}
|
||||
|
||||
AppVersionButton(
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
appVersionName = settingsViewModel.appVersionName,
|
||||
) {
|
||||
settingsViewModel.unlockExcludedModules()
|
||||
}
|
||||
AppVersionButton(
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
appVersionName = settingsViewModel.appVersionName,
|
||||
) {
|
||||
settingsViewModel.unlockExcludedModules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,15 +58,15 @@ import org.meshtastic.core.prefs.ui.UiPrefs
|
|||
import org.meshtastic.core.service.IMeshService
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.PortNum
|
||||
import java.io.BufferedWriter
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileWriter
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
|
|
@ -97,7 +97,7 @@ constructor(
|
|||
serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false)
|
||||
|
||||
val localConfig: StateFlow<LocalConfig> =
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance())
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
||||
val meshService: IMeshService?
|
||||
get() = serviceRepository.meshService
|
||||
|
|
@ -126,20 +126,17 @@ constructor(
|
|||
if (node == null || !connectionState.isConnected()) {
|
||||
flowOf(false)
|
||||
} else if (radioPrefs.isBle() || radioPrefs.isSerial() || radioPrefs.isTcp()) {
|
||||
val hwModel = node.user.hwModel.number
|
||||
val hwModel = node.user.hw_model.value
|
||||
val hw = deviceHardwareRepository.getDeviceHardwareByModel(hwModel).getOrNull()
|
||||
val capabilities = Capabilities(node.metadata?.firmwareVersion)
|
||||
val isSerial = radioPrefs.isSerial()
|
||||
// Support both Nordic DFU (requiresDfu) and ESP32 Unified OTA (supportsUnifiedOta)
|
||||
val capabilities = Capabilities(node.metadata?.firmware_version)
|
||||
|
||||
// ESP32 Unified OTA is only supported via BLE or WiFi (TCP), not USB Serial.
|
||||
val isEsp32OtaSupported = hw?.isEsp32Arc == true && capabilities.supportsEsp32Ota && !isSerial
|
||||
// TODO: Re-enable when supportsUnifiedOta is added to DeviceHardware
|
||||
val isEsp32OtaSupported = false
|
||||
// hw?.supportsUnifiedOta == true && capabilities.supportsEsp32Ota && !radioPrefs.isSerial()
|
||||
|
||||
// Nordic DFU/USB update is supported for NRF52/RP2040.
|
||||
// For ESP32, we do NOT support Serial updates from the app yet, even if requiresDfu is true
|
||||
// (which might be set for S3 native USB, but is currently unused by our handlers).
|
||||
val isDfuSupported = hw?.requiresDfu == true && hw.isEsp32Arc != true
|
||||
|
||||
flow { emit(isDfuSupported || isEsp32OtaSupported) }
|
||||
flow { emit(hw?.requiresDfu == true || isEsp32OtaSupported) }
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
|
|
@ -214,14 +211,14 @@ constructor(
|
|||
// Capture the current node value while we're still on main thread
|
||||
val nodes = nodeRepository.nodeDBbyNum.value
|
||||
|
||||
// Converts a MeshProtos.Position (nullable) to a Position, but only if it's valid, otherwise returns null.
|
||||
// Converts a ProtoPosition (nullable) to a Position, but only if it's valid, otherwise returns null.
|
||||
// The returned Position is guaranteed to be non-null and valid, or null if the input was null or invalid.
|
||||
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
|
||||
val positionToPos: (ProtoPosition?) -> Position? = { meshPosition ->
|
||||
meshPosition?.let { Position(it) }?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
writeToUri(uri) { writer ->
|
||||
val nodePositions = mutableMapOf<Int, MeshProtos.Position?>()
|
||||
val nodePositions = mutableMapOf<Int, ProtoPosition?>()
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
writer.appendLine(
|
||||
|
|
@ -249,12 +246,12 @@ constructor(
|
|||
|
||||
// packets must have rxSNR, and optionally match the filter given as a param.
|
||||
if (
|
||||
(filterPortnum == null || proto.decoded.portnumValue == filterPortnum) &&
|
||||
proto.rxSnr != 0.0f
|
||||
(filterPortnum == null || (proto.decoded?.portnum?.value ?: 0) == filterPortnum) &&
|
||||
(proto.rx_snr ?: 0f) != 0.0f
|
||||
) {
|
||||
val rxDateTime = dateFormat.format(packet.received_date)
|
||||
val rxFrom = proto.from.toUInt()
|
||||
val senderName = nodes[proto.from]?.user?.longName ?: ""
|
||||
val senderName = nodes[proto.from]?.user?.long_name ?: ""
|
||||
|
||||
// sender lat & long
|
||||
val senderPosition = nodePositions[proto.from]
|
||||
|
|
@ -268,7 +265,7 @@ constructor(
|
|||
val rxLat = rxPos?.latitude ?: ""
|
||||
val rxLong = rxPos?.longitude ?: ""
|
||||
val rxAlt = rxPos?.altitude ?: ""
|
||||
val rxSnr = proto.rxSnr
|
||||
val rxSnr = proto.rx_snr
|
||||
|
||||
// Calculate the distance if both positions are valid
|
||||
|
||||
|
|
@ -286,19 +283,19 @@ constructor(
|
|||
.toString()
|
||||
}
|
||||
|
||||
val hopLimit = proto.hopLimit
|
||||
val hopLimit = proto.hop_limit ?: 0
|
||||
|
||||
val decoded = proto.decoded
|
||||
val encrypted = proto.encrypted
|
||||
val payload =
|
||||
when {
|
||||
proto.decoded.portnumValue !in
|
||||
setOf(
|
||||
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
Portnums.PortNum.RANGE_TEST_APP_VALUE,
|
||||
) -> "<${proto.decoded.portnum}>"
|
||||
(decoded?.portnum?.value ?: 0) !in
|
||||
setOf(PortNum.TEXT_MESSAGE_APP.value, PortNum.RANGE_TEST_APP.value) ->
|
||||
"<${decoded?.portnum}>"
|
||||
|
||||
proto.hasDecoded() -> proto.decoded.payload.toStringUtf8().replace("\"", "\"\"")
|
||||
decoded != null -> decoded.payload.utf8().replace("\"", "\"\"")
|
||||
|
||||
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
|
||||
encrypted != null -> "${encrypted.size} encrypted bytes"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -39,14 +38,23 @@ import org.meshtastic.core.data.repository.NodeRepository
|
|||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.toReadableString
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.Portnums.PortNum
|
||||
import org.meshtastic.proto.StoreAndForwardProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.RouteDiscovery
|
||||
import org.meshtastic.proto.Routing
|
||||
import org.meshtastic.proto.StoreAndForward
|
||||
import org.meshtastic.proto.StoreForwardPlusPlus
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
|
@ -277,7 +285,7 @@ constructor(
|
|||
combine(searchManager.searchText, filterManager.filteredLogs) { searchText, logs ->
|
||||
searchManager.findSearchMatches(searchText, logs)
|
||||
}
|
||||
.collect { matches ->
|
||||
.collect {
|
||||
searchManager.updateMatches(searchManager.searchText.value, filterManager.filteredLogs.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -302,32 +310,30 @@ constructor(
|
|||
|
||||
/** Transform the input [MeshLog] by enhancing the raw message with annotations. */
|
||||
private fun annotateMeshLogMessage(meshLog: MeshLog): String = when (meshLog.message_type) {
|
||||
"LogRecord" -> meshLog.fromRadio.logRecord.toString().replace("\\n\"", "\"")
|
||||
"LogRecord" -> meshLog.fromRadio.log_record.toString().replace("\\n\"", "\"")
|
||||
"Packet" -> meshLog.meshPacket?.let { packet -> annotatePacketLog(packet) } ?: meshLog.raw_message
|
||||
"NodeInfo" ->
|
||||
meshLog.nodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.num) }
|
||||
?: meshLog.raw_message
|
||||
"MyNodeInfo" ->
|
||||
meshLog.myNodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.myNodeNum) }
|
||||
meshLog.myNodeInfo?.let { nodeInfo -> annotateRawMessage(meshLog.raw_message, nodeInfo.my_node_num) }
|
||||
?: meshLog.raw_message
|
||||
else -> meshLog.raw_message
|
||||
}
|
||||
|
||||
private fun annotatePacketLog(packet: MeshProtos.MeshPacket): String {
|
||||
val builder = packet.toBuilder()
|
||||
val hasDecoded = builder.hasDecoded()
|
||||
val decoded = if (hasDecoded) builder.decoded else null
|
||||
if (hasDecoded) builder.clearDecoded()
|
||||
val baseText = builder.build().toString().trimEnd()
|
||||
private fun annotatePacketLog(packet: MeshPacket): String {
|
||||
val decoded = packet.decoded
|
||||
val basePacket = packet.copy(decoded = null)
|
||||
val baseText = basePacket.toString().trimEnd()
|
||||
var result =
|
||||
if (hasDecoded && decoded != null) {
|
||||
if (decoded != null) {
|
||||
val decodedText = decoded.toString().trimEnd().prependIndent(" ")
|
||||
"$baseText\ndecoded {\n$decodedText\n}"
|
||||
} else {
|
||||
baseText
|
||||
}
|
||||
|
||||
val relayNode = packet.relayNode
|
||||
val relayNode = packet.relay_node ?: 0
|
||||
var relayNodeAnnotation: String? = null
|
||||
val placeholder = "___RELAY_NODE___"
|
||||
|
||||
|
|
@ -337,7 +343,7 @@ constructor(
|
|||
|
||||
Packet.getRelayNode(relayNode, nodeList, myNodeNum)?.let { node ->
|
||||
val relayId = node.user.id
|
||||
val relayName = node.user.longName
|
||||
val relayName = node.user.long_name
|
||||
val regex = Regex("""\brelay_node: ${relayNode.toUInt()}\b""")
|
||||
if (regex.containsMatchIn(result)) {
|
||||
relayNodeAnnotation = "relay_node: $relayName ($relayId)"
|
||||
|
|
@ -364,7 +370,7 @@ constructor(
|
|||
var mutated = false
|
||||
nodeIds.toSet().forEach { nodeId -> mutated = mutated or msg.annotateNodeId(nodeId) }
|
||||
return if (mutated) {
|
||||
return msg.toString()
|
||||
msg.toString()
|
||||
} else {
|
||||
rawMessage
|
||||
}
|
||||
|
|
@ -375,12 +381,10 @@ constructor(
|
|||
val nodeIdStr = nodeId.toUInt().toString()
|
||||
// Only match if whitespace before and after
|
||||
val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""")
|
||||
regex.find(this)?.let { matchResult ->
|
||||
matchResult.groupValues.let { _ ->
|
||||
regex.findAll(this).toList().asReversed().forEach { match ->
|
||||
val idx = match.range.last + 1
|
||||
insert(idx, " (${nodeId.asNodeId()})")
|
||||
}
|
||||
regex.find(this)?.let { _ ->
|
||||
regex.findAll(this).toList().asReversed().forEach { match ->
|
||||
val idx = match.range.last + 1
|
||||
insert(idx, " (${nodeId.asNodeId()})")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -434,70 +438,82 @@ constructor(
|
|||
* @return A human-readable string representation of the decoded payload, or an error message if decoding fails, or
|
||||
* null if the log does not contain a decodable packet.
|
||||
*/
|
||||
@Suppress("detekt:CyclomaticComplexMethod") // large switch that detekt doesn't parse well.
|
||||
@Suppress("CyclomaticComplexMethod", "NestedBlockDepth")
|
||||
private fun decodePayloadFromMeshLog(log: MeshLog): String? {
|
||||
var result: String? = null
|
||||
val packet = log.meshPacket
|
||||
if (packet == null || !packet.hasDecoded()) {
|
||||
result = null
|
||||
} else {
|
||||
val portnum = packet.decoded.portnumValue
|
||||
val payload = packet.decoded.payload.toByteArray()
|
||||
result =
|
||||
try {
|
||||
when (portnum) {
|
||||
PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
PortNum.ALERT_APP_VALUE,
|
||||
-> payload.toString(Charsets.UTF_8)
|
||||
PortNum.POSITION_APP_VALUE -> MeshProtos.Position.parseFrom(payload).toString()
|
||||
PortNum.WAYPOINT_APP_VALUE -> MeshProtos.Waypoint.parseFrom(payload).toString()
|
||||
PortNum.NODEINFO_APP_VALUE -> MeshProtos.User.parseFrom(payload).toString()
|
||||
PortNum.TELEMETRY_APP_VALUE -> TelemetryProtos.Telemetry.parseFrom(payload).toString()
|
||||
PortNum.ROUTING_APP_VALUE -> MeshProtos.Routing.parseFrom(payload).toString()
|
||||
PortNum.ADMIN_APP_VALUE -> AdminProtos.AdminMessage.parseFrom(payload).toString()
|
||||
PortNum.PAXCOUNTER_APP_VALUE -> PaxcountProtos.Paxcount.parseFrom(payload).toString()
|
||||
PortNum.STORE_FORWARD_APP_VALUE ->
|
||||
StoreAndForwardProtos.StoreAndForward.parseFrom(payload).toString()
|
||||
PortNum.STORE_FORWARD_PLUSPLUS_APP_VALUE ->
|
||||
MeshProtos.StoreForwardPlusPlus.parseFrom(payload).toString()
|
||||
PortNum.NEIGHBORINFO_APP_VALUE -> decodeNeighborInfo(payload)
|
||||
PortNum.TRACEROUTE_APP_VALUE -> decodeTraceroute(packet, payload)
|
||||
else -> payload.joinToString(" ") { HEX_FORMAT.format(it) }
|
||||
}
|
||||
} catch (e: InvalidProtocolBufferException) {
|
||||
"Failed to decode payload: ${e.message}"
|
||||
}
|
||||
val decoded = packet?.decoded ?: return null
|
||||
|
||||
val portnumValue = decoded.portnum.value
|
||||
val payload = decoded.payload.toByteArray()
|
||||
return try {
|
||||
when (portnumValue) {
|
||||
PortNum.TEXT_MESSAGE_APP.value,
|
||||
PortNum.ALERT_APP.value,
|
||||
-> payload.toString(Charsets.UTF_8)
|
||||
PortNum.POSITION_APP.value ->
|
||||
Position.ADAPTER.decodeOrNull(payload)?.let { Position.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode Position"
|
||||
PortNum.WAYPOINT_APP.value ->
|
||||
Waypoint.ADAPTER.decodeOrNull(payload)?.let { Waypoint.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode Waypoint"
|
||||
PortNum.NODEINFO_APP.value ->
|
||||
User.ADAPTER.decodeOrNull(payload)?.let { User.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode User"
|
||||
PortNum.TELEMETRY_APP.value ->
|
||||
Telemetry.ADAPTER.decodeOrNull(payload)?.let { Telemetry.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode Telemetry"
|
||||
PortNum.ROUTING_APP.value ->
|
||||
Routing.ADAPTER.decodeOrNull(payload)?.let { Routing.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode Routing"
|
||||
PortNum.ADMIN_APP.value ->
|
||||
AdminMessage.ADAPTER.decodeOrNull(payload)?.let { AdminMessage.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode AdminMessage"
|
||||
PortNum.PAXCOUNTER_APP.value ->
|
||||
Paxcount.ADAPTER.decodeOrNull(payload)?.let { Paxcount.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode Paxcount"
|
||||
PortNum.STORE_FORWARD_APP.value ->
|
||||
StoreAndForward.ADAPTER.decodeOrNull(payload)?.let { StoreAndForward.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode StoreAndForward"
|
||||
PortNum.STORE_FORWARD_PLUSPLUS_APP.value ->
|
||||
StoreForwardPlusPlus.ADAPTER.decodeOrNull(payload)?.let {
|
||||
StoreForwardPlusPlus.ADAPTER.toReadableString(it)
|
||||
} ?: "Failed to decode StoreForwardPlusPlus"
|
||||
PortNum.NEIGHBORINFO_APP.value -> decodeNeighborInfo(payload)
|
||||
PortNum.TRACEROUTE_APP.value -> decodeTraceroute(packet, payload)
|
||||
else -> payload.joinToString(" ") { HEX_FORMAT.format(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"Failed to decode payload: ${e.message}"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun formatNodeWithShortName(nodeNum: Int): String {
|
||||
val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user
|
||||
val shortName = user?.shortName?.takeIf { it.isNotEmpty() } ?: ""
|
||||
val shortName = user?.short_name?.takeIf { it.isNotEmpty() } ?: ""
|
||||
val nodeId = "!%08x".format(nodeNum)
|
||||
return if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId
|
||||
}
|
||||
|
||||
private fun decodeNeighborInfo(payload: ByteArray): String {
|
||||
val info = MeshProtos.NeighborInfo.parseFrom(payload)
|
||||
val info = NeighborInfo.ADAPTER.decode(payload)
|
||||
return buildString {
|
||||
appendLine("NeighborInfo:")
|
||||
appendLine(" node_id: ${formatNodeWithShortName(info.nodeId)}")
|
||||
appendLine(" last_sent_by_id: ${formatNodeWithShortName(info.lastSentById)}")
|
||||
appendLine(" node_broadcast_interval_secs: ${info.nodeBroadcastIntervalSecs}")
|
||||
if (info.neighborsCount > 0) {
|
||||
appendLine(" node_id: ${formatNodeWithShortName(info.node_id ?: 0)}")
|
||||
appendLine(" last_sent_by_id: ${formatNodeWithShortName(info.last_sent_by_id ?: 0)}")
|
||||
appendLine(" node_broadcast_interval_secs: ${info.node_broadcast_interval_secs}")
|
||||
if (info.neighbors.isNotEmpty()) {
|
||||
appendLine(" neighbors:")
|
||||
info.neighborsList.forEach { n ->
|
||||
appendLine(" - node_id: ${formatNodeWithShortName(n.nodeId)} snr: ${n.snr}")
|
||||
info.neighbors.forEach { n ->
|
||||
appendLine(" - node_id: ${formatNodeWithShortName(n.node_id ?: 0)} snr: ${n.snr}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeTraceroute(packet: MeshProtos.MeshPacket, payload: ByteArray): String {
|
||||
private fun decodeTraceroute(packet: MeshPacket, payload: ByteArray): String {
|
||||
val getUsername: (Int) -> String = { nodeNum -> formatNodeWithShortName(nodeNum) }
|
||||
return packet.getTracerouteResponse(getUsername)
|
||||
?: runCatching { MeshProtos.RouteDiscovery.parseFrom(payload).toString() }.getOrNull()
|
||||
?: runCatching { RouteDiscovery.ADAPTER.decode(payload).toString() }.getOrNull()
|
||||
?: payload.joinToString(" ") { HEX_FORMAT.format(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ package org.meshtastic.feature.settings.navigation
|
|||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.rounded.Bluetooth
|
||||
import androidx.compose.material.icons.rounded.CellTower
|
||||
import androidx.compose.material.icons.rounded.DisplaySettings
|
||||
import androidx.compose.material.icons.rounded.LocationOn
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Power
|
||||
import androidx.compose.material.icons.rounded.Router
|
||||
import androidx.compose.material.icons.rounded.Security
|
||||
import androidx.compose.material.icons.rounded.Wifi
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.CellTower
|
||||
import androidx.compose.material.icons.filled.DisplaySettings
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Power
|
||||
import androidx.compose.material.icons.filled.Router
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Wifi
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.navigation.Route
|
||||
|
|
@ -42,59 +42,44 @@ import org.meshtastic.core.strings.position
|
|||
import org.meshtastic.core.strings.power
|
||||
import org.meshtastic.core.strings.security
|
||||
import org.meshtastic.core.strings.user
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.MeshProtos.DeviceMetadata
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
|
||||
enum class ConfigRoute(val title: StringResource, val route: Route, val icon: ImageVector?, val type: Int = 0) {
|
||||
USER(Res.string.user, SettingsRoutes.User, Icons.Rounded.Person, 0),
|
||||
USER(Res.string.user, SettingsRoutes.User, Icons.Default.Person, 0),
|
||||
CHANNELS(Res.string.channels, SettingsRoutes.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
|
||||
DEVICE(
|
||||
Res.string.device,
|
||||
SettingsRoutes.Device,
|
||||
Icons.Rounded.Router,
|
||||
AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG_VALUE,
|
||||
),
|
||||
DEVICE(Res.string.device, SettingsRoutes.Device, Icons.Default.Router, AdminMessage.ConfigType.DEVICE_CONFIG.value),
|
||||
POSITION(
|
||||
Res.string.position,
|
||||
SettingsRoutes.Position,
|
||||
Icons.Rounded.LocationOn,
|
||||
AdminProtos.AdminMessage.ConfigType.POSITION_CONFIG_VALUE,
|
||||
),
|
||||
POWER(
|
||||
Res.string.power,
|
||||
SettingsRoutes.Power,
|
||||
Icons.Rounded.Power,
|
||||
AdminProtos.AdminMessage.ConfigType.POWER_CONFIG_VALUE,
|
||||
Icons.Default.LocationOn,
|
||||
AdminMessage.ConfigType.POSITION_CONFIG.value,
|
||||
),
|
||||
POWER(Res.string.power, SettingsRoutes.Power, Icons.Default.Power, AdminMessage.ConfigType.POWER_CONFIG.value),
|
||||
NETWORK(
|
||||
Res.string.network,
|
||||
SettingsRoutes.Network,
|
||||
Icons.Rounded.Wifi,
|
||||
AdminProtos.AdminMessage.ConfigType.NETWORK_CONFIG_VALUE,
|
||||
Icons.Default.Wifi,
|
||||
AdminMessage.ConfigType.NETWORK_CONFIG.value,
|
||||
),
|
||||
DISPLAY(
|
||||
Res.string.display,
|
||||
SettingsRoutes.Display,
|
||||
Icons.Rounded.DisplaySettings,
|
||||
AdminProtos.AdminMessage.ConfigType.DISPLAY_CONFIG_VALUE,
|
||||
),
|
||||
LORA(
|
||||
Res.string.lora,
|
||||
SettingsRoutes.LoRa,
|
||||
Icons.Rounded.CellTower,
|
||||
AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE,
|
||||
Icons.Default.DisplaySettings,
|
||||
AdminMessage.ConfigType.DISPLAY_CONFIG.value,
|
||||
),
|
||||
LORA(Res.string.lora, SettingsRoutes.LoRa, Icons.Default.CellTower, AdminMessage.ConfigType.LORA_CONFIG.value),
|
||||
BLUETOOTH(
|
||||
Res.string.bluetooth,
|
||||
SettingsRoutes.Bluetooth,
|
||||
Icons.Rounded.Bluetooth,
|
||||
AdminProtos.AdminMessage.ConfigType.BLUETOOTH_CONFIG_VALUE,
|
||||
Icons.Default.Bluetooth,
|
||||
AdminMessage.ConfigType.BLUETOOTH_CONFIG.value,
|
||||
),
|
||||
SECURITY(
|
||||
Res.string.security,
|
||||
SettingsRoutes.Security,
|
||||
Icons.Rounded.Security,
|
||||
AdminProtos.AdminMessage.ConfigType.SECURITY_CONFIG_VALUE,
|
||||
Icons.Default.Security,
|
||||
AdminMessage.ConfigType.SECURITY_CONFIG.value,
|
||||
),
|
||||
;
|
||||
|
||||
|
|
@ -102,8 +87,8 @@ enum class ConfigRoute(val title: StringResource, val route: Route, val icon: Im
|
|||
private fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
|
||||
when {
|
||||
metadata == null -> true // Include all routes if metadata is null
|
||||
it == BLUETOOTH -> metadata.hasBluetooth
|
||||
it == NETWORK -> metadata.hasWifi || metadata.hasEthernet
|
||||
it == BLUETOOTH -> metadata.hasBluetooth == true
|
||||
it == NETWORK -> metadata.hasWifi == true || metadata.hasEthernet == true
|
||||
else -> true // Include all other routes by default
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ import org.meshtastic.core.strings.serial
|
|||
import org.meshtastic.core.strings.status_message
|
||||
import org.meshtastic.core.strings.store_forward
|
||||
import org.meshtastic.core.strings.telemetry
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.MeshProtos.DeviceMetadata
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
|
||||
enum class ModuleRoute(
|
||||
val title: StringResource,
|
||||
|
|
@ -60,89 +60,84 @@ enum class ModuleRoute(
|
|||
val type: Int = 0,
|
||||
val isSupported: (Capabilities) -> Boolean = { true },
|
||||
) {
|
||||
MQTT(
|
||||
Res.string.mqtt,
|
||||
SettingsRoutes.MQTT,
|
||||
Icons.Rounded.Cloud,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.MQTT_CONFIG_VALUE,
|
||||
),
|
||||
MQTT(Res.string.mqtt, SettingsRoutes.MQTT, Icons.Rounded.Cloud, AdminMessage.ModuleConfigType.MQTT_CONFIG.value),
|
||||
SERIAL(
|
||||
Res.string.serial,
|
||||
SettingsRoutes.Serial,
|
||||
Icons.Rounded.Usb,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.SERIAL_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.SERIAL_CONFIG.value,
|
||||
),
|
||||
EXT_NOTIFICATION(
|
||||
Res.string.external_notification,
|
||||
SettingsRoutes.ExtNotification,
|
||||
Icons.Rounded.Notifications,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG.value,
|
||||
),
|
||||
STORE_FORWARD(
|
||||
Res.string.store_forward,
|
||||
SettingsRoutes.StoreForward,
|
||||
Icons.AutoMirrored.Default.Forward,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG.value,
|
||||
),
|
||||
RANGE_TEST(
|
||||
Res.string.range_test,
|
||||
SettingsRoutes.RangeTest,
|
||||
Icons.Rounded.Speed,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.RANGETEST_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.RANGETEST_CONFIG.value,
|
||||
),
|
||||
TELEMETRY(
|
||||
Res.string.telemetry,
|
||||
SettingsRoutes.Telemetry,
|
||||
Icons.Rounded.DataUsage,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.TELEMETRY_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.TELEMETRY_CONFIG.value,
|
||||
),
|
||||
CANNED_MESSAGE(
|
||||
Res.string.canned_message,
|
||||
SettingsRoutes.CannedMessage,
|
||||
Icons.AutoMirrored.Default.Message,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG.value,
|
||||
),
|
||||
AUDIO(
|
||||
Res.string.audio,
|
||||
SettingsRoutes.Audio,
|
||||
Icons.AutoMirrored.Default.VolumeUp,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.AUDIO_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.AUDIO_CONFIG.value,
|
||||
),
|
||||
REMOTE_HARDWARE(
|
||||
Res.string.remote_hardware,
|
||||
SettingsRoutes.RemoteHardware,
|
||||
Icons.Rounded.SettingsRemote,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG.value,
|
||||
),
|
||||
NEIGHBOR_INFO(
|
||||
Res.string.neighbor_info,
|
||||
SettingsRoutes.NeighborInfo,
|
||||
Icons.Rounded.People,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG.value,
|
||||
),
|
||||
AMBIENT_LIGHTING(
|
||||
Res.string.ambient_lighting,
|
||||
SettingsRoutes.AmbientLighting,
|
||||
Icons.Rounded.LightMode,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG.value,
|
||||
),
|
||||
DETECTION_SENSOR(
|
||||
Res.string.detection_sensor,
|
||||
SettingsRoutes.DetectionSensor,
|
||||
Icons.Rounded.Sensors,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG.value,
|
||||
),
|
||||
PAXCOUNTER(
|
||||
Res.string.paxcounter,
|
||||
SettingsRoutes.Paxcounter,
|
||||
Icons.Rounded.PermScanWifi,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG.value,
|
||||
),
|
||||
STATUS_MESSAGE(
|
||||
Res.string.status_message,
|
||||
SettingsRoutes.StatusMessage,
|
||||
Icons.AutoMirrored.Default.Message,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG_VALUE,
|
||||
AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG.value,
|
||||
isSupported = { it.supportsStatusMessage },
|
||||
),
|
||||
;
|
||||
|
|
@ -152,9 +147,10 @@ enum class ModuleRoute(
|
|||
|
||||
companion object {
|
||||
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> {
|
||||
val capabilities = Capabilities(metadata?.firmwareVersion)
|
||||
val capabilities = Capabilities(metadata?.firmware_version)
|
||||
return entries.filter {
|
||||
val isExcluded = metadata != null && (metadata.excludedModules and it.bitfield != 0)
|
||||
val excludedModules = metadata?.excluded_modules ?: 0
|
||||
val isExcluded = (excludedModules and it.bitfield) != 0
|
||||
!isExcluded && it.isSupported(capabilities)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.protobuf.MessageLite
|
||||
import com.meshtastic.core.strings.getString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -65,24 +63,24 @@ import org.meshtastic.core.service.IMeshService
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cant_shutdown
|
||||
import org.meshtastic.core.strings.fetching_channel_indexed
|
||||
import org.meshtastic.core.strings.fetching_config
|
||||
import org.meshtastic.core.ui.util.getChannelList
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.ModuleRoute
|
||||
import org.meshtastic.feature.settings.util.UiText
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ClientOnlyProtos.DeviceProfile
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.ConfigProtos.Config.SecurityConfig
|
||||
import org.meshtastic.proto.ConnStatusProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.ModuleConfigProtos
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.deviceProfile
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceConnectionStatus
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Routing
|
||||
import org.meshtastic.proto.User
|
||||
import java.io.FileOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -91,14 +89,14 @@ data class RadioConfigState(
|
|||
val isLocal: Boolean = false,
|
||||
val connected: Boolean = false,
|
||||
val route: String = "",
|
||||
val metadata: MeshProtos.DeviceMetadata? = null,
|
||||
val userConfig: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||
val channelList: List<ChannelProtos.ChannelSettings> = emptyList(),
|
||||
val radioConfig: ConfigProtos.Config = config {},
|
||||
val moduleConfig: ModuleConfigProtos.ModuleConfig = moduleConfig {},
|
||||
val metadata: DeviceMetadata? = null,
|
||||
val userConfig: User = User(),
|
||||
val channelList: List<ChannelSettings> = emptyList(),
|
||||
val radioConfig: Config = Config(),
|
||||
val moduleConfig: LocalModuleConfig = LocalModuleConfig(),
|
||||
val ringtone: String = "",
|
||||
val cannedMessageMessages: String = "",
|
||||
val deviceConnectionStatus: ConnStatusProtos.DeviceConnectionStatus? = null,
|
||||
val deviceConnectionStatus: DeviceConnectionStatus? = null,
|
||||
val responseState: ResponseState<Boolean> = ResponseState.Empty,
|
||||
val analyticsAvailable: Boolean = true,
|
||||
val analyticsEnabled: Boolean = false,
|
||||
|
|
@ -129,12 +127,15 @@ constructor(
|
|||
analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
|
||||
}
|
||||
|
||||
private val destNum = savedStateHandle.toRoute<SettingsRoutes.Settings>().destNum
|
||||
private val destNum =
|
||||
savedStateHandle.get<Int>("destNum")
|
||||
?: runCatching { savedStateHandle.toRoute<SettingsRoutes.Settings>().destNum }.getOrNull()
|
||||
|
||||
private val _destNode = MutableStateFlow<Node?>(null)
|
||||
val destNode: StateFlow<Node?>
|
||||
get() = _destNode
|
||||
|
||||
private val requestIds = MutableStateFlow(emptySet<Int>())
|
||||
private val requestIds = MutableStateFlow(hashSetOf<Int>())
|
||||
private val _radioConfigState = MutableStateFlow(RadioConfigState())
|
||||
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ constructor(
|
|||
viewModelScope.launch { _radioConfigState.update { it.copy(nodeDbResetPreserveFavorites = preserveFavorites) } }
|
||||
}
|
||||
|
||||
private val _currentDeviceProfile = MutableStateFlow(deviceProfile {})
|
||||
private val _currentDeviceProfile = MutableStateFlow(DeviceProfile())
|
||||
val currentDeviceProfile
|
||||
get() = _currentDeviceProfile.value
|
||||
|
||||
|
|
@ -195,14 +196,13 @@ constructor(
|
|||
|
||||
val hasPaFan: Boolean
|
||||
get() =
|
||||
destNode.value?.user?.hwModel in
|
||||
destNode.value?.user?.hw_model in
|
||||
setOf(
|
||||
null,
|
||||
MeshProtos.HardwareModel.UNRECOGNIZED,
|
||||
MeshProtos.HardwareModel.UNSET,
|
||||
MeshProtos.HardwareModel.BETAFPV_2400_TX,
|
||||
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT_NANO,
|
||||
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT,
|
||||
HardwareModel.UNSET,
|
||||
HardwareModel.BETAFPV_2400_TX,
|
||||
HardwareModel.RADIOMASTER_900_BANDIT_NANO,
|
||||
HardwareModel.RADIOMASTER_900_BANDIT,
|
||||
)
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
@ -216,12 +216,11 @@ constructor(
|
|||
val packetId = service.packetId
|
||||
try {
|
||||
requestAction(service, packetId, destNum)
|
||||
requestIds.update { it + packetId }
|
||||
requestIds.update { it.apply { add(packetId) } }
|
||||
_radioConfigState.update { state ->
|
||||
val currentState = state.responseState
|
||||
if (currentState is ResponseState.Loading) {
|
||||
val total = maxOf(currentState.total, requestIds.value.size)
|
||||
state.copy(responseState = currentState.copy(total = total))
|
||||
if (state.responseState is ResponseState.Loading) {
|
||||
val total = maxOf(requestIds.value.size, state.responseState.total)
|
||||
state.copy(responseState = state.responseState.copy(total = total))
|
||||
} else {
|
||||
state.copy(
|
||||
route = "", // setter (response is PortNum.ROUTING_APP)
|
||||
|
|
@ -235,25 +234,15 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setOwner(user: MeshProtos.User) {
|
||||
val targetNode = destNode.value ?: return
|
||||
// Ensure we are setting the owner for the intended target node
|
||||
// This prevents accidentally updating the local node if the user object has the wrong ID
|
||||
val fixedUser =
|
||||
if (targetNode.user.id.isNotEmpty() && targetNode.user.id != user.id) {
|
||||
Logger.w { "Fixing user ID mismatch in setOwner: form=${user.id} target=${targetNode.user.id}" }
|
||||
user.toBuilder().setId(targetNode.user.id).build()
|
||||
} else {
|
||||
user
|
||||
}
|
||||
setRemoteOwner(targetNode.num, fixedUser)
|
||||
fun setOwner(user: User) {
|
||||
setRemoteOwner(destNode.value?.num ?: return, user)
|
||||
}
|
||||
|
||||
private fun setRemoteOwner(destNum: Int, user: MeshProtos.User) = request(
|
||||
private fun setRemoteOwner(destNum: Int, user: User) = request(
|
||||
destNum,
|
||||
{ service, packetId, _ ->
|
||||
_radioConfigState.update { it.copy(userConfig = user) }
|
||||
service.setRemoteOwner(packetId, destNum, user.toByteArray())
|
||||
service.setRemoteOwner(packetId, destNum, user.encode())
|
||||
},
|
||||
"Request setOwner error",
|
||||
)
|
||||
|
|
@ -264,7 +253,7 @@ constructor(
|
|||
"Request getOwner error",
|
||||
)
|
||||
|
||||
fun updateChannels(new: List<ChannelProtos.ChannelSettings>, old: List<ChannelProtos.ChannelSettings>) {
|
||||
fun updateChannels(new: List<ChannelSettings>, old: List<ChannelSettings>) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
getChannelList(new, old).forEach { setRemoteChannel(destNum, it) }
|
||||
|
||||
|
|
@ -280,12 +269,12 @@ constructor(
|
|||
private fun setChannels(channelUrl: String) = viewModelScope.launch {
|
||||
val new = channelUrl.toUri().toChannelSet()
|
||||
val old = radioConfigRepository.channelSetFlow.firstOrNull() ?: return@launch
|
||||
updateChannels(new.settingsList, old.settingsList)
|
||||
updateChannels(new.settings, old.settings)
|
||||
}
|
||||
|
||||
private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) = request(
|
||||
private fun setRemoteChannel(destNum: Int, channel: Channel) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.setRemoteChannel(packetId, dest, channel.toByteArray()) },
|
||||
{ service, packetId, dest -> service.setRemoteChannel(packetId, dest, channel.encode()) },
|
||||
"Request setRemoteChannel error",
|
||||
)
|
||||
|
||||
|
|
@ -295,15 +284,29 @@ constructor(
|
|||
"Request getChannel error",
|
||||
)
|
||||
|
||||
fun setConfig(config: ConfigProtos.Config) {
|
||||
fun setConfig(config: Config) {
|
||||
setRemoteConfig(destNode.value?.num ?: return, config)
|
||||
}
|
||||
|
||||
private fun setRemoteConfig(destNum: Int, config: ConfigProtos.Config) = request(
|
||||
private fun setRemoteConfig(destNum: Int, config: Config) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest ->
|
||||
_radioConfigState.update { it.copy(radioConfig = config) }
|
||||
service.setRemoteConfig(packetId, dest, config.toByteArray())
|
||||
_radioConfigState.update { state ->
|
||||
state.copy(
|
||||
radioConfig =
|
||||
state.radioConfig.copy(
|
||||
device = config.device ?: state.radioConfig.device,
|
||||
position = config.position ?: state.radioConfig.position,
|
||||
power = config.power ?: state.radioConfig.power,
|
||||
network = config.network ?: state.radioConfig.network,
|
||||
display = config.display ?: state.radioConfig.display,
|
||||
lora = config.lora ?: state.radioConfig.lora,
|
||||
bluetooth = config.bluetooth ?: state.radioConfig.bluetooth,
|
||||
security = config.security ?: state.radioConfig.security,
|
||||
),
|
||||
)
|
||||
}
|
||||
service.setRemoteConfig(packetId, dest, config.encode())
|
||||
},
|
||||
"Request setConfig error",
|
||||
)
|
||||
|
|
@ -314,17 +317,38 @@ constructor(
|
|||
"Request getConfig error",
|
||||
)
|
||||
|
||||
fun setModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
|
||||
setModuleConfig(destNode.value?.num ?: return, config)
|
||||
fun setModuleConfig(config: ModuleConfig) {
|
||||
setRemoteModuleConfig(destNode.value?.num ?: return, config)
|
||||
}
|
||||
|
||||
private fun setModuleConfig(destNum: Int, config: ModuleConfigProtos.ModuleConfig) = request(
|
||||
private fun setRemoteModuleConfig(destNum: Int, config: ModuleConfig) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest ->
|
||||
_radioConfigState.update { it.copy(moduleConfig = config) }
|
||||
service.setModuleConfig(packetId, dest, config.toByteArray())
|
||||
_radioConfigState.update { state ->
|
||||
state.copy(
|
||||
moduleConfig =
|
||||
state.moduleConfig.copy(
|
||||
mqtt = config.mqtt ?: state.moduleConfig.mqtt,
|
||||
serial = config.serial ?: state.moduleConfig.serial,
|
||||
external_notification =
|
||||
config.external_notification ?: state.moduleConfig.external_notification,
|
||||
store_forward = config.store_forward ?: state.moduleConfig.store_forward,
|
||||
range_test = config.range_test ?: state.moduleConfig.range_test,
|
||||
telemetry = config.telemetry ?: state.moduleConfig.telemetry,
|
||||
canned_message = config.canned_message ?: state.moduleConfig.canned_message,
|
||||
audio = config.audio ?: state.moduleConfig.audio,
|
||||
remote_hardware = config.remote_hardware ?: state.moduleConfig.remote_hardware,
|
||||
neighbor_info = config.neighbor_info ?: state.moduleConfig.neighbor_info,
|
||||
ambient_lighting = config.ambient_lighting ?: state.moduleConfig.ambient_lighting,
|
||||
detection_sensor = config.detection_sensor ?: state.moduleConfig.detection_sensor,
|
||||
paxcounter = config.paxcounter ?: state.moduleConfig.paxcounter,
|
||||
statusmessage = config.statusmessage ?: state.moduleConfig.statusmessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
service.setModuleConfig(packetId, dest, config.encode())
|
||||
},
|
||||
"Request setConfig error",
|
||||
"Request setModuleConfig error",
|
||||
)
|
||||
|
||||
private fun getModuleConfig(destNum: Int, configType: Int) = request(
|
||||
|
|
@ -418,7 +442,7 @@ constructor(
|
|||
AdminRoute.REBOOT.name -> requestReboot(destNum)
|
||||
AdminRoute.SHUTDOWN.name ->
|
||||
with(radioConfigState.value) {
|
||||
if (metadata != null && !metadata.canShutdown) {
|
||||
if (metadata != null && metadata.canShutdown != true) {
|
||||
sendError(Res.string.cant_shutdown)
|
||||
} else {
|
||||
requestShutdown(destNum)
|
||||
|
|
@ -444,8 +468,8 @@ constructor(
|
|||
fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openInputStream(uri).use { inputStream ->
|
||||
val bytes = inputStream?.readBytes()
|
||||
val protobuf = DeviceProfile.parseFrom(bytes)
|
||||
val bytes = inputStream?.readBytes() ?: ByteArray(0)
|
||||
val protobuf = DeviceProfile.ADAPTER.decode(bytes)
|
||||
onResult(protobuf)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
|
|
@ -456,11 +480,11 @@ constructor(
|
|||
|
||||
fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch { writeToUri(uri, profile) }
|
||||
|
||||
private suspend fun writeToUri(uri: Uri, message: MessageLite) = withContext(Dispatchers.IO) {
|
||||
private suspend fun writeToUri(uri: Uri, message: com.squareup.wire.Message<*, *>) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream ->
|
||||
message.writeTo(outputStream)
|
||||
outputStream.write(message.encode())
|
||||
}
|
||||
}
|
||||
setResponseStateSuccess()
|
||||
|
|
@ -470,16 +494,16 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun exportSecurityConfig(uri: Uri, securityConfig: SecurityConfig) =
|
||||
fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) =
|
||||
viewModelScope.launch { writeSecurityKeysJsonToUri(uri, securityConfig) }
|
||||
|
||||
private val indentSpaces = 4
|
||||
|
||||
private suspend fun writeSecurityKeysJsonToUri(uri: Uri, securityConfig: SecurityConfig) =
|
||||
private suspend fun writeSecurityKeysJsonToUri(uri: Uri, securityConfig: Config.SecurityConfig) =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val publicKeyBytes = securityConfig.publicKey.toByteArray()
|
||||
val privateKeyBytes = securityConfig.privateKey.toByteArray()
|
||||
val publicKeyBytes = securityConfig.public_key?.toByteArray() ?: ByteArray(0)
|
||||
val privateKeyBytes = securityConfig.private_key?.toByteArray() ?: ByteArray(0)
|
||||
|
||||
// Convert byte arrays to Base64 strings for human readability in JSON
|
||||
val publicKeyBase64 = Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
||||
|
|
@ -509,74 +533,57 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun installProfile(protobuf: DeviceProfile) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
with(protobuf) {
|
||||
meshService?.beginEditSettings(destNum)
|
||||
if (hasLongName() || hasShortName()) {
|
||||
if (long_name != null || short_name != null) {
|
||||
destNode.value?.user?.let {
|
||||
val user =
|
||||
MeshProtos.User.newBuilder()
|
||||
.setId(it.id)
|
||||
.setLongName(if (hasLongName()) longName else it.longName)
|
||||
.setShortName(if (hasShortName()) shortName else it.shortName)
|
||||
.setIsLicensed(it.isLicensed)
|
||||
.build()
|
||||
val user = it.copy(long_name = long_name ?: it.long_name, short_name = short_name ?: it.short_name)
|
||||
setOwner(user)
|
||||
}
|
||||
}
|
||||
if (hasChannelUrl()) {
|
||||
try {
|
||||
setChannels(channelUrl)
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "DeviceProfile channel import error" }
|
||||
sendError(ex.customMessage)
|
||||
}
|
||||
config?.let { lc ->
|
||||
lc.device?.let { setConfig(Config(device = it)) }
|
||||
lc.position?.let { setConfig(Config(position = it)) }
|
||||
lc.power?.let { setConfig(Config(power = it)) }
|
||||
lc.network?.let { setConfig(Config(network = it)) }
|
||||
lc.display?.let { setConfig(Config(display = it)) }
|
||||
lc.lora?.let { setConfig(Config(lora = it)) }
|
||||
lc.bluetooth?.let { setConfig(Config(bluetooth = it)) }
|
||||
lc.security?.let { setConfig(Config(security = it)) }
|
||||
}
|
||||
if (hasConfig()) {
|
||||
val descriptor = ConfigProtos.Config.getDescriptor()
|
||||
config.allFields.forEach { (field, value) ->
|
||||
val newConfig =
|
||||
ConfigProtos.Config.newBuilder().setField(descriptor.findFieldByName(field.name), value).build()
|
||||
setConfig(newConfig)
|
||||
}
|
||||
if (fixed_position != null) {
|
||||
setFixedPosition(Position(fixed_position!!))
|
||||
}
|
||||
if (hasFixedPosition()) {
|
||||
setFixedPosition(Position(fixedPosition))
|
||||
}
|
||||
if (hasModuleConfig()) {
|
||||
val descriptor = ModuleConfigProtos.ModuleConfig.getDescriptor()
|
||||
moduleConfig.allFields.forEach { (field, value) ->
|
||||
val newConfig =
|
||||
ModuleConfigProtos.ModuleConfig.newBuilder()
|
||||
.setField(descriptor.findFieldByName(field.name), value)
|
||||
.build()
|
||||
setModuleConfig(newConfig)
|
||||
}
|
||||
module_config?.let { lmc ->
|
||||
lmc.mqtt?.let { setModuleConfig(ModuleConfig(mqtt = it)) }
|
||||
lmc.serial?.let { setModuleConfig(ModuleConfig(serial = it)) }
|
||||
lmc.external_notification?.let { setModuleConfig(ModuleConfig(external_notification = it)) }
|
||||
lmc.store_forward?.let { setModuleConfig(ModuleConfig(store_forward = it)) }
|
||||
lmc.range_test?.let { setModuleConfig(ModuleConfig(range_test = it)) }
|
||||
lmc.telemetry?.let { setModuleConfig(ModuleConfig(telemetry = it)) }
|
||||
lmc.canned_message?.let { setModuleConfig(ModuleConfig(canned_message = it)) }
|
||||
lmc.audio?.let { setModuleConfig(ModuleConfig(audio = it)) }
|
||||
lmc.remote_hardware?.let { setModuleConfig(ModuleConfig(remote_hardware = it)) }
|
||||
lmc.neighbor_info?.let { setModuleConfig(ModuleConfig(neighbor_info = it)) }
|
||||
lmc.ambient_lighting?.let { setModuleConfig(ModuleConfig(ambient_lighting = it)) }
|
||||
lmc.detection_sensor?.let { setModuleConfig(ModuleConfig(detection_sensor = it)) }
|
||||
lmc.paxcounter?.let { setModuleConfig(ModuleConfig(paxcounter = it)) }
|
||||
lmc.statusmessage?.let { setModuleConfig(ModuleConfig(statusmessage = it)) }
|
||||
}
|
||||
meshService?.commitEditSettings(destNum)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPacketResponse() {
|
||||
requestIds.value = emptySet()
|
||||
requestIds.value = hashSetOf()
|
||||
_radioConfigState.update { it.copy(responseState = ResponseState.Empty) }
|
||||
}
|
||||
|
||||
private fun getTitleForRoute(route: Enum<*>) = when (route) {
|
||||
is ConfigRoute -> route.title
|
||||
is ModuleRoute -> route.title
|
||||
is AdminRoute -> route.title
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun setResponseStateLoading(route: Enum<*>) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
|
||||
val title = getTitleForRoute(route)
|
||||
|
||||
_radioConfigState.update {
|
||||
RadioConfigState(
|
||||
isLocal = it.isLocal,
|
||||
|
|
@ -584,10 +591,7 @@ constructor(
|
|||
route = route.name,
|
||||
metadata = it.metadata,
|
||||
nodeDbResetPreserveFavorites = it.nodeDbResetPreserveFavorites,
|
||||
responseState =
|
||||
ResponseState.Loading(
|
||||
status = title?.let { t -> getString(Res.string.fetching_config, getString(t)) },
|
||||
),
|
||||
responseState = ResponseState.Loading(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -602,7 +606,7 @@ constructor(
|
|||
}
|
||||
|
||||
is AdminRoute -> {
|
||||
getConfig(destNum, AdminProtos.AdminMessage.ConfigType.SESSIONKEY_CONFIG_VALUE)
|
||||
getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value)
|
||||
setResponseStateTotal(2)
|
||||
}
|
||||
|
||||
|
|
@ -634,11 +638,10 @@ constructor(
|
|||
mapConsentPrefs.setShouldReportLocation(nodeNum, shouldReportLocation)
|
||||
}
|
||||
|
||||
private fun setResponseStateTotal(newTotal: Int) {
|
||||
private fun setResponseStateTotal(total: Int) {
|
||||
_radioConfigState.update { state ->
|
||||
val currentState = state.responseState
|
||||
if (currentState is ResponseState.Loading) {
|
||||
state.copy(responseState = currentState.copy(total = newTotal))
|
||||
if (state.responseState is ResponseState.Loading) {
|
||||
state.copy(responseState = state.responseState.copy(total = total))
|
||||
} else {
|
||||
state // Return the unchanged state for other response states
|
||||
}
|
||||
|
|
@ -666,35 +669,32 @@ constructor(
|
|||
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
|
||||
}
|
||||
|
||||
private fun incrementCompleted(status: String? = null) {
|
||||
private fun incrementCompleted() {
|
||||
_radioConfigState.update { state ->
|
||||
if (state.responseState is ResponseState.Loading) {
|
||||
val increment = state.responseState.completed + 1
|
||||
state.copy(
|
||||
responseState =
|
||||
state.responseState.copy(completed = increment, status = status ?: state.responseState.status),
|
||||
)
|
||||
state.copy(responseState = state.responseState.copy(completed = increment))
|
||||
} else {
|
||||
state // Return the unchanged state for other response states
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processPacketResponse(packet: MeshProtos.MeshPacket) {
|
||||
val data = packet.decoded
|
||||
if (data.requestId !in requestIds.value) return
|
||||
private fun processPacketResponse(packet: MeshPacket) {
|
||||
val data = packet.decoded ?: return
|
||||
if (data.request_id !in requestIds.value) return
|
||||
val route = radioConfigState.value.route
|
||||
|
||||
val destNum = destNode.value?.num ?: return
|
||||
val debugMsg = "requestId: ${data.requestId.toUInt()} to: ${destNum.toUInt()} received %s"
|
||||
val debugMsg = "requestId: ${data.request_id.toUInt()} to: ${destNum.toUInt()} received %s"
|
||||
|
||||
if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) {
|
||||
val parsed = MeshProtos.Routing.parseFrom(data.payload)
|
||||
Logger.d { debugMsg.format(parsed.errorReason.name) }
|
||||
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
|
||||
sendError(getStringResFrom(parsed.errorReasonValue))
|
||||
if (data.portnum == PortNum.ROUTING_APP) {
|
||||
val parsed = Routing.ADAPTER.decode(data.payload)
|
||||
Logger.d { debugMsg.format(parsed.error_reason?.name) }
|
||||
if (parsed.error_reason != Routing.Error.NONE) {
|
||||
sendError(getStringResFrom(parsed.error_reason?.value ?: 0))
|
||||
} else if (packet.from == destNum && route.isEmpty()) {
|
||||
requestIds.update { it - data.requestId }
|
||||
requestIds.update { it.apply { remove(data.request_id) } }
|
||||
if (requestIds.value.isEmpty()) {
|
||||
setResponseStateSuccess()
|
||||
} else {
|
||||
|
|
@ -702,91 +702,125 @@ constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
|
||||
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
|
||||
Logger.d { debugMsg.format(parsed.payloadVariantCase.name) }
|
||||
if (data.portnum == PortNum.ADMIN_APP) {
|
||||
val parsed = AdminMessage.ADAPTER.decode(data.payload)
|
||||
// Explicitly log the non-null field name for clarity
|
||||
val variant =
|
||||
when {
|
||||
parsed.get_device_metadata_response != null -> "get_device_metadata_response"
|
||||
parsed.get_channel_response != null -> "get_channel_response"
|
||||
parsed.get_owner_response != null -> "get_owner_response"
|
||||
parsed.get_config_response != null -> "get_config_response"
|
||||
parsed.get_module_config_response != null -> "get_module_config_response"
|
||||
parsed.get_canned_message_module_messages_response != null ->
|
||||
"get_canned_message_module_messages_response"
|
||||
parsed.get_ringtone_response != null -> "get_ringtone_response"
|
||||
parsed.get_device_connection_status_response != null -> "get_device_connection_status_response"
|
||||
else -> "unknown"
|
||||
}
|
||||
Logger.d { debugMsg.format(variant) }
|
||||
if (destNum != packet.from) {
|
||||
sendError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
|
||||
return
|
||||
}
|
||||
when (parsed.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
|
||||
_radioConfigState.update { it.copy(metadata = parsed.getDeviceMetadataResponse) }
|
||||
when {
|
||||
parsed.get_device_metadata_response != null -> {
|
||||
_radioConfigState.update { it.copy(metadata = parsed.get_device_metadata_response) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
val response = parsed.getChannelResponse
|
||||
parsed.get_channel_response != null -> {
|
||||
val response = parsed.get_channel_response!!
|
||||
// Stop once we get to the first disabled entry
|
||||
if (response.role != ChannelProtos.Channel.Role.DISABLED) {
|
||||
if (response.role != Channel.Role.DISABLED) {
|
||||
_radioConfigState.update { state ->
|
||||
state.copy(
|
||||
channelList =
|
||||
state.channelList.toMutableList().apply { add(response.index, response.settings) },
|
||||
state.channelList.toMutableList().apply {
|
||||
val index = response.index ?: 0
|
||||
val settings = response.settings ?: ChannelSettings()
|
||||
// Make sure list is large enough
|
||||
while (size <= index) add(ChannelSettings())
|
||||
set(index, settings)
|
||||
},
|
||||
)
|
||||
}
|
||||
incrementCompleted(
|
||||
getString(Res.string.fetching_channel_indexed, response.index + 1, maxChannels),
|
||||
)
|
||||
if (response.index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) {
|
||||
incrementCompleted()
|
||||
val index = response.index ?: 0
|
||||
if (index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) {
|
||||
// Not done yet, request next channel
|
||||
getChannel(destNum, response.index + 1)
|
||||
getChannel(destNum, index + 1)
|
||||
}
|
||||
} else {
|
||||
// Received last channel, update total and start channel editor
|
||||
setResponseStateTotal(response.index + 1)
|
||||
setResponseStateTotal((response.index ?: 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_OWNER_RESPONSE -> {
|
||||
_radioConfigState.update { it.copy(userConfig = parsed.getOwnerResponse) }
|
||||
parsed.get_owner_response != null -> {
|
||||
_radioConfigState.update { it.copy(userConfig = parsed.get_owner_response!!) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
|
||||
val response = parsed.getConfigResponse
|
||||
if (response.payloadVariantCase.number == 0) { // PAYLOADVARIANT_NOT_SET
|
||||
sendError(response.payloadVariantCase.name)
|
||||
}
|
||||
parsed.get_config_response != null -> {
|
||||
val response = parsed.get_config_response!!
|
||||
_radioConfigState.update { it.copy(radioConfig = response) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_MODULE_CONFIG_RESPONSE -> {
|
||||
val response = parsed.getModuleConfigResponse
|
||||
if (response.payloadVariantCase.number == 0) { // PAYLOADVARIANT_NOT_SET
|
||||
sendError(response.payloadVariantCase.name)
|
||||
parsed.get_module_config_response != null -> {
|
||||
val response = parsed.get_module_config_response!!
|
||||
_radioConfigState.update { state ->
|
||||
state.copy(
|
||||
moduleConfig =
|
||||
state.moduleConfig.copy(
|
||||
mqtt = response.mqtt ?: state.moduleConfig.mqtt,
|
||||
serial = response.serial ?: state.moduleConfig.serial,
|
||||
external_notification =
|
||||
response.external_notification ?: state.moduleConfig.external_notification,
|
||||
store_forward = response.store_forward ?: state.moduleConfig.store_forward,
|
||||
range_test = response.range_test ?: state.moduleConfig.range_test,
|
||||
telemetry = response.telemetry ?: state.moduleConfig.telemetry,
|
||||
canned_message = response.canned_message ?: state.moduleConfig.canned_message,
|
||||
audio = response.audio ?: state.moduleConfig.audio,
|
||||
remote_hardware = response.remote_hardware ?: state.moduleConfig.remote_hardware,
|
||||
neighbor_info = response.neighbor_info ?: state.moduleConfig.neighbor_info,
|
||||
ambient_lighting = response.ambient_lighting ?: state.moduleConfig.ambient_lighting,
|
||||
detection_sensor = response.detection_sensor ?: state.moduleConfig.detection_sensor,
|
||||
paxcounter = response.paxcounter ?: state.moduleConfig.paxcounter,
|
||||
statusmessage = response.statusmessage ?: state.moduleConfig.statusmessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
_radioConfigState.update { it.copy(moduleConfig = response) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CANNED_MESSAGE_MODULE_MESSAGES_RESPONSE -> {
|
||||
parsed.get_canned_message_module_messages_response != null -> {
|
||||
_radioConfigState.update {
|
||||
it.copy(cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse)
|
||||
it.copy(cannedMessageMessages = parsed.get_canned_message_module_messages_response!!)
|
||||
}
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> {
|
||||
_radioConfigState.update { it.copy(ringtone = parsed.getRingtoneResponse) }
|
||||
parsed.get_ringtone_response != null -> {
|
||||
_radioConfigState.update { it.copy(ringtone = parsed.get_ringtone_response!!) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_CONNECTION_STATUS_RESPONSE -> {
|
||||
parsed.get_device_connection_status_response != null -> {
|
||||
_radioConfigState.update {
|
||||
it.copy(deviceConnectionStatus = parsed.getDeviceConnectionStatusResponse)
|
||||
it.copy(deviceConnectionStatus = parsed.get_device_connection_status_response)
|
||||
}
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
else -> Logger.d { "No custom processing needed for ${parsed.payloadVariantCase}" }
|
||||
else -> Logger.d { "No custom processing needed for $parsed" }
|
||||
}
|
||||
|
||||
if (AdminRoute.entries.any { it.name == route }) {
|
||||
sendAdminRequest(destNum)
|
||||
}
|
||||
requestIds.update { it - data.requestId }
|
||||
requestIds.update { it.apply { remove(data.request_id) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,9 +73,8 @@ import org.meshtastic.feature.settings.radio.channel.component.ChannelLegend
|
|||
import org.meshtastic.feature.settings.radio.channel.component.ChannelLegendDialog
|
||||
import org.meshtastic.feature.settings.radio.channel.component.EditChannelDialog
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
|
||||
import org.meshtastic.proto.channelSettings
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun ChannelConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
|
|
@ -89,9 +88,9 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
title = stringResource(Res.string.channels),
|
||||
onBack = onBack,
|
||||
settingsList = state.channelList,
|
||||
loraConfig = state.radioConfig.lora,
|
||||
loraConfig = state.radioConfig.lora ?: Config.LoRaConfig(),
|
||||
maxChannels = viewModel.maxChannels,
|
||||
firmwareVersion = state.metadata?.firmwareVersion ?: "0.0.0",
|
||||
firmwareVersion = state.metadata?.firmware_version ?: "0.0.0",
|
||||
enabled = state.connected,
|
||||
onPositiveClicked = { channelListInput -> viewModel.updateChannels(channelListInput, state.channelList) },
|
||||
)
|
||||
|
|
@ -103,7 +102,7 @@ private fun ChannelConfigScreen(
|
|||
title: String,
|
||||
onBack: () -> Unit,
|
||||
settingsList: List<ChannelSettings>,
|
||||
loraConfig: LoRaConfig,
|
||||
loraConfig: Config.LoRaConfig,
|
||||
maxChannels: Int = 8,
|
||||
firmwareVersion: String,
|
||||
enabled: Boolean,
|
||||
|
|
@ -138,7 +137,7 @@ private fun ChannelConfigScreen(
|
|||
if (showEditChannelDialog != null) {
|
||||
val index = showEditChannelDialog ?: return
|
||||
EditChannelDialog(
|
||||
channelSettings = with(settingsListInput) { if (size > index) get(index) else channelSettings {} },
|
||||
channelSettings = with(settingsListInput) { if (size > index) get(index) else ChannelSettings() },
|
||||
modemPresetName = modemPresetName,
|
||||
onAddClick = {
|
||||
if (settingsListInput.size > index) {
|
||||
|
|
@ -173,7 +172,7 @@ private fun ChannelConfigScreen(
|
|||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (maxChannels > settingsListInput.size) {
|
||||
settingsListInput.add(channelSettings { psk = Channel.default.settings.psk })
|
||||
settingsListInput.add(ChannelSettings(psk = Channel.default.settings.psk))
|
||||
showEditChannelDialog = settingsListInput.lastIndex
|
||||
}
|
||||
},
|
||||
|
|
@ -188,14 +187,14 @@ private fun ChannelConfigScreen(
|
|||
Column {
|
||||
ChannelConfigHeader(
|
||||
frequency =
|
||||
if (loraConfig.overrideFrequency != 0f) {
|
||||
loraConfig.overrideFrequency
|
||||
if (loraConfig.override_frequency != 0f) {
|
||||
loraConfig.override_frequency
|
||||
} else {
|
||||
primaryChannel.radioFreq
|
||||
},
|
||||
slot =
|
||||
if (loraConfig.channelNum != 0) {
|
||||
loraConfig.channelNum
|
||||
if (loraConfig.channel_num != 0) {
|
||||
loraConfig.channel_num
|
||||
} else {
|
||||
primaryChannel.channelNum
|
||||
},
|
||||
|
|
@ -282,7 +281,7 @@ private fun determineLocationSharingChannel(capabilities: Capabilities, settings
|
|||
if (capabilities.supportsSecondaryChannelLocation) {
|
||||
/* Essentially the first index with the setting enabled */
|
||||
for ((i, settings) in settingsList.withIndex()) {
|
||||
if (settings.moduleSettings.positionPrecision > 0) {
|
||||
if ((settings.module_settings?.position_precision ?: 0) > 0) {
|
||||
output = i
|
||||
break
|
||||
}
|
||||
|
|
@ -290,7 +289,7 @@ private fun determineLocationSharingChannel(capabilities: Capabilities, settings
|
|||
} else {
|
||||
/* Only the primary channel at index 0 can share locations automatically */
|
||||
val primary = settingsList[0]
|
||||
if (primary.moduleSettings.positionPrecision > 0) {
|
||||
if ((primary.module_settings?.position_precision ?: 0) > 0) {
|
||||
output = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -305,11 +304,8 @@ private fun ChannelConfigScreenPreview() {
|
|||
onBack = {},
|
||||
settingsList =
|
||||
listOf(
|
||||
channelSettings {
|
||||
psk = Channel.default.settings.psk
|
||||
name = Channel.default.name
|
||||
},
|
||||
channelSettings { name = stringResource(Res.string.channel_name) },
|
||||
ChannelSettings(psk = Channel.default.settings.psk, name = Channel.default.name),
|
||||
ChannelSettings(name = stringResource(Res.string.channel_name)),
|
||||
),
|
||||
loraConfig = Channel.default.loraConfig,
|
||||
firmwareVersion = "1.3.2",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.channel.component
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -35,10 +34,8 @@ import org.meshtastic.core.strings.delete
|
|||
import org.meshtastic.core.ui.component.ChannelItem
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.ConfigKt.loRaConfig
|
||||
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
|
||||
import org.meshtastic.proto.channelSettings
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
internal fun ChannelCard(
|
||||
|
|
@ -46,7 +43,7 @@ internal fun ChannelCard(
|
|||
title: String,
|
||||
enabled: Boolean,
|
||||
channelSettings: ChannelSettings,
|
||||
loraConfig: LoRaConfig,
|
||||
loraConfig: Config.LoRaConfig,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
sharesLocation: Boolean,
|
||||
|
|
@ -58,14 +55,14 @@ internal fun ChannelCard(
|
|||
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
|
||||
)
|
||||
}
|
||||
if (channelSettings.uplinkEnabled) {
|
||||
if (channelSettings.uplink_enabled) {
|
||||
Icon(
|
||||
imageVector = ChannelIcons.UPLINK.icon,
|
||||
contentDescription = stringResource(ChannelIcons.UPLINK.descriptionResId),
|
||||
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
|
||||
)
|
||||
}
|
||||
if (channelSettings.downlinkEnabled) {
|
||||
if (channelSettings.downlink_enabled) {
|
||||
Icon(
|
||||
imageVector = ChannelIcons.DOWNLINK.icon,
|
||||
contentDescription = stringResource(ChannelIcons.DOWNLINK.descriptionResId),
|
||||
|
|
@ -91,12 +88,8 @@ private fun ChannelCardPreview() {
|
|||
index = 0,
|
||||
title = "Medium Fast",
|
||||
enabled = true,
|
||||
channelSettings =
|
||||
channelSettings {
|
||||
uplinkEnabled = true
|
||||
downlinkEnabled = true
|
||||
},
|
||||
loraConfig = loRaConfig {},
|
||||
channelSettings = ChannelSettings(uplink_enabled = true, downlink_enabled = true),
|
||||
loraConfig = Config.LoRaConfig(),
|
||||
onEditClick = {},
|
||||
onDeleteClick = {},
|
||||
sharesLocation = true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.channel.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -54,15 +53,14 @@ import org.meshtastic.core.ui.component.EditBase64Preference
|
|||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.component.PositionPrecisionPreference
|
||||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.channelSettings
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.ModuleSettings
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun EditChannelDialog(
|
||||
channelSettings: ChannelProtos.ChannelSettings,
|
||||
onAddClick: (ChannelProtos.ChannelSettings) -> Unit,
|
||||
channelSettings: ChannelSettings,
|
||||
onAddClick: (ChannelSettings) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
modemPresetName: String = stringResource(Res.string.default_),
|
||||
|
|
@ -76,10 +74,15 @@ fun EditChannelDialog(
|
|||
onDismissRequest = onDismissRequest,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
text = {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.channel_name),
|
||||
value = if (isFocused) channelInput.name else channelInput.name.ifEmpty { modemPresetName },
|
||||
value =
|
||||
if (isFocused) {
|
||||
(channelInput.name ?: "")
|
||||
} else {
|
||||
(channelInput.name ?: "").ifEmpty { modemPresetName }
|
||||
},
|
||||
maxSize = 11, // name max_size:12
|
||||
enabled = true,
|
||||
isError = false,
|
||||
|
|
@ -87,51 +90,55 @@ fun EditChannelDialog(
|
|||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
channelInput =
|
||||
channelInput.copy {
|
||||
name = it.trim()
|
||||
if (psk == Channel.default.settings.psk) psk = Channel.getRandomKey()
|
||||
val defaultPsk = Channel.default.settings.psk
|
||||
val newPsk =
|
||||
if (channelInput.psk == defaultPsk) {
|
||||
Channel.getRandomKey()
|
||||
} else {
|
||||
(channelInput.psk ?: okio.ByteString.EMPTY)
|
||||
}
|
||||
channelInput = channelInput.copy(name = it.trim(), psk = newPsk)
|
||||
},
|
||||
onFocusChanged = { isFocused = it.isFocused },
|
||||
)
|
||||
|
||||
EditBase64Preference(
|
||||
title = "PSK",
|
||||
value = channelInput.psk,
|
||||
value = channelInput.psk ?: okio.ByteString.EMPTY,
|
||||
enabled = true,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChange = {
|
||||
val fullPsk = Channel(channelSettings { psk = it }).psk
|
||||
if (fullPsk.size() in setOf(0, 16, 32)) {
|
||||
channelInput = channelInput.copy { psk = it }
|
||||
val fullPsk = Channel(ChannelSettings(psk = it)).psk
|
||||
if (fullPsk.size in setOf(0, 16, 32)) {
|
||||
channelInput = channelInput.copy(psk = it)
|
||||
}
|
||||
},
|
||||
onGenerateKey = { channelInput = channelInput.copy { psk = Channel.getRandomKey() } },
|
||||
onGenerateKey = { channelInput = channelInput.copy(psk = Channel.getRandomKey()) },
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.uplink_enabled),
|
||||
checked = channelInput.uplinkEnabled,
|
||||
checked = channelInput.uplink_enabled ?: false,
|
||||
enabled = true,
|
||||
onCheckedChange = { channelInput = channelInput.copy { uplinkEnabled = it } },
|
||||
onCheckedChange = { channelInput = channelInput.copy(uplink_enabled = it) },
|
||||
padding = PaddingValues(0.dp),
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.downlink_enabled),
|
||||
checked = channelInput.downlinkEnabled,
|
||||
checked = channelInput.downlink_enabled ?: false,
|
||||
enabled = true,
|
||||
onCheckedChange = { channelInput = channelInput.copy { downlinkEnabled = it } },
|
||||
onCheckedChange = { channelInput = channelInput.copy(downlink_enabled = it) },
|
||||
padding = PaddingValues(0.dp),
|
||||
)
|
||||
|
||||
val moduleSettings = channelInput.module_settings ?: ModuleSettings()
|
||||
PositionPrecisionPreference(
|
||||
enabled = true,
|
||||
value = channelInput.moduleSettings.positionPrecision,
|
||||
value = moduleSettings.position_precision ?: 0,
|
||||
onValueChanged = {
|
||||
val module = channelInput.moduleSettings.copy { positionPrecision = it }
|
||||
channelInput = channelInput.copy { moduleSettings = module }
|
||||
val updatedModule = moduleSettings.copy(position_precision = it)
|
||||
channelInput = channelInput.copy(module_settings = updatedModule)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -156,11 +163,7 @@ fun EditChannelDialog(
|
|||
@Composable
|
||||
private fun EditChannelDialogPreview() {
|
||||
EditChannelDialog(
|
||||
channelSettings =
|
||||
channelSettings {
|
||||
psk = Channel.default.settings.psk
|
||||
name = Channel.default.name
|
||||
},
|
||||
channelSettings = ChannelSettings(psk = Channel.default.settings.psk, name = Channel.default.name),
|
||||
onAddClick = {},
|
||||
onDismissRequest = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -38,13 +37,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val ambientLightingConfig = state.moduleConfig.ambientLighting
|
||||
val ambientLightingConfig = state.moduleConfig.ambient_lighting ?: ModuleConfig.AmbientLightingConfig()
|
||||
val formState = rememberConfigState(initialValue = ambientLightingConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -56,7 +54,7 @@ fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { ambientLighting = it }
|
||||
val config = ModuleConfig(ambient_lighting = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -64,40 +62,40 @@ fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(
|
|||
TitledCard(title = stringResource(Res.string.ambient_lighting_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.led_state),
|
||||
checked = formState.value.ledState,
|
||||
checked = formState.value.led_state ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { ledState = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(led_state = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.current),
|
||||
value = formState.value.current,
|
||||
value = formState.value.current ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { current = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(current = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.red),
|
||||
value = formState.value.red,
|
||||
value = formState.value.red ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { red = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(red = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.green),
|
||||
value = formState.value.green,
|
||||
value = formState.value.green ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { green = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(green = it) },
|
||||
)
|
||||
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.blue),
|
||||
value = formState.value.blue,
|
||||
value = formState.value.blue ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { blue = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(blue = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -41,14 +40,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig.AudioConfig
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val audioConfig = state.moduleConfig.audio
|
||||
val audioConfig = state.moduleConfig.audio ?: ModuleConfig.AudioConfig()
|
||||
val formState = rememberConfigState(initialValue = audioConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -60,7 +57,7 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { audio = it }
|
||||
val config = ModuleConfig(audio = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -68,57 +65,54 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
TitledCard(title = stringResource(Res.string.audio_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.codec_2_enabled),
|
||||
checked = formState.value.codec2Enabled,
|
||||
checked = formState.value.codec2_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { codec2Enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(codec2_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.ptt_pin),
|
||||
value = formState.value.pttPin,
|
||||
value = formState.value.ptt_pin ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { pttPin = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(ptt_pin = it) },
|
||||
)
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.codec2_sample_rate),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
AudioConfig.Audio_Baud.entries
|
||||
.filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.bitrate,
|
||||
onItemSelected = { formState.value = formState.value.copy { bitrate = it } },
|
||||
items = ModuleConfig.AudioConfig.Audio_Baud.entries.map { it to it.name },
|
||||
selectedItem = formState.value.bitrate ?: ModuleConfig.AudioConfig.Audio_Baud.CODEC2_DEFAULT,
|
||||
onItemSelected = { formState.value = formState.value.copy(bitrate = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.i2s_word_select),
|
||||
value = formState.value.i2SWs,
|
||||
value = formState.value.i2s_ws ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { i2SWs = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(i2s_ws = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.i2s_data_in),
|
||||
value = formState.value.i2SSd,
|
||||
value = formState.value.i2s_sd ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { i2SSd = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(i2s_sd = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.i2s_data_out),
|
||||
value = formState.value.i2SDin,
|
||||
value = formState.value.i2s_din ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { i2SDin = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(i2s_din = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.i2s_clock),
|
||||
value = formState.value.i2SSck,
|
||||
value = formState.value.i2s_sck ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { i2SSck = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(i2s_sck = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -37,14 +36,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.ConfigProtos.Config.BluetoothConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val bluetoothConfig = state.radioConfig.bluetooth
|
||||
val bluetoothConfig = state.radioConfig.bluetooth ?: Config.BluetoothConfig()
|
||||
val formState = rememberConfigState(initialValue = bluetoothConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -56,7 +53,7 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { bluetooth = it }
|
||||
val config = Config(bluetooth = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -66,7 +63,7 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
title = stringResource(Res.string.bluetooth_enabled),
|
||||
checked = formState.value.enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -74,21 +71,23 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
title = stringResource(Res.string.pairing_mode),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
BluetoothConfig.PairingMode.entries
|
||||
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
|
||||
Config.BluetoothConfig.PairingMode.entries
|
||||
.filter { it.name != "UNRECOGNIZED" }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.mode,
|
||||
onItemSelected = { formState.value = formState.value.copy { mode = it } },
|
||||
selectedItem =
|
||||
formState.value.mode?.takeUnless { it.name == "UNRECOGNIZED" }
|
||||
?: Config.BluetoothConfig.PairingMode.RANDOM_PIN,
|
||||
onItemSelected = { formState.value = formState.value.copy(mode = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.fixed_pin),
|
||||
value = formState.value.fixedPin,
|
||||
value = formState.value.fixed_pin ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
if (it.toString().length == 6) { // ensure 6 digits
|
||||
formState.value = formState.value.copy { fixedPin = it }
|
||||
formState.value = formState.value.copy(fixed_pin = it)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -52,14 +51,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val cannedMessageConfig = state.moduleConfig.cannedMessage
|
||||
val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig()
|
||||
val messages = state.cannedMessageMessages
|
||||
val formState = rememberConfigState(initialValue = cannedMessageConfig)
|
||||
var messagesInput by rememberSaveable(messages) { mutableStateOf(messages) }
|
||||
|
|
@ -79,7 +76,7 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
viewModel.setCannedMessages(messagesInput)
|
||||
}
|
||||
if (formState.value != cannedMessageConfig) {
|
||||
val config = moduleConfig { cannedMessage = formState.value }
|
||||
val config = ModuleConfig(canned_message = formState.value)
|
||||
viewModel.setModuleConfig(config)
|
||||
}
|
||||
},
|
||||
|
|
@ -90,96 +87,90 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
title = stringResource(Res.string.canned_message_enabled),
|
||||
checked = formState.value.enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.rotary_encoder_1_enabled),
|
||||
checked = formState.value.rotary1Enabled,
|
||||
checked = formState.value.rotary1_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { rotary1Enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(rotary1_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.gpio_pin_for_rotary_encoder_a_port),
|
||||
value = formState.value.inputbrokerPinA,
|
||||
value = formState.value.inputbroker_pin_a ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinA = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(inputbroker_pin_a = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.gpio_pin_for_rotary_encoder_b_port),
|
||||
value = formState.value.inputbrokerPinB,
|
||||
value = formState.value.inputbroker_pin_b ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinB = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(inputbroker_pin_b = it) },
|
||||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.gpio_pin_for_rotary_encoder_press_port),
|
||||
value = formState.value.inputbrokerPinPress,
|
||||
value = formState.value.inputbroker_pin_press ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinPress = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(inputbroker_pin_press = it) },
|
||||
)
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.generate_input_event_on_press),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
CannedMessageConfig.InputEventChar.entries
|
||||
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.inputbrokerEventPress,
|
||||
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventPress = it } },
|
||||
items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name },
|
||||
selectedItem =
|
||||
formState.value.inputbroker_event_press ?: ModuleConfig.CannedMessageConfig.InputEventChar.NONE,
|
||||
onItemSelected = { formState.value = formState.value.copy(inputbroker_event_press = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.generate_input_event_on_cw),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
CannedMessageConfig.InputEventChar.entries
|
||||
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.inputbrokerEventCw,
|
||||
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCw = it } },
|
||||
items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name },
|
||||
selectedItem =
|
||||
formState.value.inputbroker_event_cw ?: ModuleConfig.CannedMessageConfig.InputEventChar.NONE,
|
||||
onItemSelected = { formState.value = formState.value.copy(inputbroker_event_cw = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.generate_input_event_on_ccw),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
CannedMessageConfig.InputEventChar.entries
|
||||
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.inputbrokerEventCcw,
|
||||
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCcw = it } },
|
||||
items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name },
|
||||
selectedItem =
|
||||
formState.value.inputbroker_event_ccw ?: ModuleConfig.CannedMessageConfig.InputEventChar.NONE,
|
||||
onItemSelected = { formState.value = formState.value.copy(inputbroker_event_ccw = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.up_down_select_input_enabled),
|
||||
checked = formState.value.updown1Enabled,
|
||||
checked = formState.value.updown1_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { updown1Enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(updown1_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.allow_input_source),
|
||||
value = formState.value.allowInputSource,
|
||||
value = formState.value.allow_input_source ?: "",
|
||||
maxSize = 63, // allow_input_source max_size:16
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { allowInputSource = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(allow_input_source = it) },
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.send_bell),
|
||||
checked = formState.value.sendBell,
|
||||
checked = formState.value.send_bell ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(send_bell = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -23,7 +22,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.google.protobuf.MessageLite
|
||||
import com.squareup.wire.Message
|
||||
|
||||
/**
|
||||
* A state holder for managing config data within a Composable.
|
||||
|
|
@ -32,10 +31,10 @@ import com.google.protobuf.MessageLite
|
|||
* whether the current value has been modified ("dirty"), and provides simple methods to save the changes or reset to
|
||||
* the initial state.
|
||||
*
|
||||
* @param T The type of the data being managed, typically a Protobuf message.
|
||||
* @param T The type of the data being managed, typically a Wire message.
|
||||
* @property initialValue The original, unmodified value of the config data.
|
||||
*/
|
||||
class ConfigState<T : MessageLite>(private val initialValue: T) {
|
||||
class ConfigState<T : Message<T, *>>(private val initialValue: T) {
|
||||
var value by mutableStateOf(initialValue)
|
||||
|
||||
val isDirty: Boolean
|
||||
|
|
@ -46,14 +45,9 @@ class ConfigState<T : MessageLite>(private val initialValue: T) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun <T : MessageLite> saver(initialValue: T): Saver<ConfigState<T>, ByteArray> = Saver(
|
||||
save = { it.value.toByteArray() },
|
||||
restore = {
|
||||
ConfigState(initialValue).apply {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
value = initialValue.parserForType.parseFrom(it) as T
|
||||
}
|
||||
},
|
||||
fun <T : Message<T, *>> saver(initialValue: T): Saver<ConfigState<T>, ByteArray> = Saver(
|
||||
save = { it.value.adapter.encode(it.value) },
|
||||
restore = { ConfigState(initialValue).apply { value = initialValue.adapter.decode(it) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -66,5 +60,5 @@ class ConfigState<T : MessageLite>(private val initialValue: T) {
|
|||
* across recompositions.
|
||||
*/
|
||||
@Composable
|
||||
fun <T : MessageLite> rememberConfigState(initialValue: T): ConfigState<T> =
|
||||
fun <T : Message<T, *>> rememberConfigState(initialValue: T): ConfigState<T> =
|
||||
rememberSaveable(initialValue, saver = ConfigState.saver(initialValue)) { ConfigState(initialValue) }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -49,14 +48,12 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.gpioPins
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val detectionSensorConfig = state.moduleConfig.detectionSensor
|
||||
val detectionSensorConfig = state.moduleConfig.detection_sensor ?: ModuleConfig.DetectionSensorConfig()
|
||||
val formState = rememberConfigState(initialValue = detectionSensorConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -68,7 +65,7 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { detectionSensor = it }
|
||||
val config = ModuleConfig(detection_sensor = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -78,7 +75,7 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(
|
|||
title = stringResource(Res.string.detection_sensor_enabled),
|
||||
checked = formState.value.enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -87,66 +84,65 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(
|
|||
}
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.minimum_broadcast_seconds),
|
||||
selectedItem = formState.value.minimumBroadcastSecs.toLong(),
|
||||
selectedItem = (formState.value.minimum_broadcast_secs ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = minimumBroadcastIntervals.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { minimumBroadcastSecs = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(minimum_broadcast_secs = it.toInt()) },
|
||||
)
|
||||
|
||||
val stateBroadcastIntervals = remember { IntervalConfiguration.DETECTION_SENSOR_STATE.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.state_broadcast_seconds),
|
||||
selectedItem = formState.value.stateBroadcastSecs.toLong(),
|
||||
selectedItem = (formState.value.state_broadcast_secs ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = stateBroadcastIntervals.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { stateBroadcastSecs = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(state_broadcast_secs = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.send_bell_with_alert_message),
|
||||
checked = formState.value.sendBell,
|
||||
checked = formState.value.send_bell,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(send_bell = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.friendly_name),
|
||||
value = formState.value.name,
|
||||
value = formState.value.name ?: "",
|
||||
maxSize = 19, // name max_size:20
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { name = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(name = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
val pins = remember { gpioPins }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.gpio_pin_to_monitor),
|
||||
items = pins,
|
||||
selectedItem = formState.value.monitorPin,
|
||||
selectedItem = formState.value.monitor_pin ?: 0,
|
||||
enabled = state.connected,
|
||||
onItemSelected = { formState.value = formState.value.copy { monitorPin = it } },
|
||||
onItemSelected = { formState.value = formState.value.copy(monitor_pin = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.detection_trigger_type),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
ModuleConfig.DetectionSensorConfig.TriggerType.entries
|
||||
.filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.detectionTriggerType,
|
||||
onItemSelected = { formState.value = formState.value.copy { detectionTriggerType = it } },
|
||||
items = ModuleConfig.DetectionSensorConfig.TriggerType.entries.map { it to it.name },
|
||||
selectedItem =
|
||||
formState.value.detection_trigger_type
|
||||
?: ModuleConfig.DetectionSensorConfig.TriggerType.LOGIC_LOW,
|
||||
onItemSelected = { formState.value = formState.value.copy(detection_trigger_type = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_input_pullup_mode),
|
||||
checked = formState.value.usePullup,
|
||||
checked = formState.value.use_pullup ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { usePullup = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(use_pullup = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
|
|||
|
|
@ -119,39 +119,38 @@ import org.meshtastic.core.ui.timezone.toPosixString
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.ConfigProtos.Config.DeviceConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
import java.time.ZoneId
|
||||
|
||||
private val DeviceConfig.Role.description: StringResource
|
||||
private val Config.DeviceConfig.Role.description: StringResource
|
||||
get() =
|
||||
when (this) {
|
||||
DeviceConfig.Role.CLIENT -> Res.string.role_client_desc
|
||||
DeviceConfig.Role.CLIENT_BASE -> Res.string.role_client_base_desc
|
||||
DeviceConfig.Role.CLIENT_MUTE -> Res.string.role_client_mute_desc
|
||||
DeviceConfig.Role.ROUTER -> Res.string.role_router_desc
|
||||
DeviceConfig.Role.ROUTER_CLIENT -> Res.string.role_router_client_desc
|
||||
DeviceConfig.Role.REPEATER -> Res.string.role_repeater_desc
|
||||
DeviceConfig.Role.TRACKER -> Res.string.role_tracker_desc
|
||||
DeviceConfig.Role.SENSOR -> Res.string.role_sensor_desc
|
||||
DeviceConfig.Role.TAK -> Res.string.role_tak_desc
|
||||
DeviceConfig.Role.CLIENT_HIDDEN -> Res.string.role_client_hidden_desc
|
||||
DeviceConfig.Role.LOST_AND_FOUND -> Res.string.role_lost_and_found_desc
|
||||
DeviceConfig.Role.TAK_TRACKER -> Res.string.role_tak_tracker_desc
|
||||
DeviceConfig.Role.ROUTER_LATE -> Res.string.role_router_late_desc
|
||||
Config.DeviceConfig.Role.CLIENT -> Res.string.role_client_desc
|
||||
Config.DeviceConfig.Role.CLIENT_BASE -> Res.string.role_client_base_desc
|
||||
Config.DeviceConfig.Role.CLIENT_MUTE -> Res.string.role_client_mute_desc
|
||||
Config.DeviceConfig.Role.ROUTER -> Res.string.role_router_desc
|
||||
Config.DeviceConfig.Role.ROUTER_CLIENT -> Res.string.role_router_client_desc
|
||||
Config.DeviceConfig.Role.REPEATER -> Res.string.role_repeater_desc
|
||||
Config.DeviceConfig.Role.TRACKER -> Res.string.role_tracker_desc
|
||||
Config.DeviceConfig.Role.SENSOR -> Res.string.role_sensor_desc
|
||||
Config.DeviceConfig.Role.TAK -> Res.string.role_tak_desc
|
||||
Config.DeviceConfig.Role.CLIENT_HIDDEN -> Res.string.role_client_hidden_desc
|
||||
Config.DeviceConfig.Role.LOST_AND_FOUND -> Res.string.role_lost_and_found_desc
|
||||
Config.DeviceConfig.Role.TAK_TRACKER -> Res.string.role_tak_tracker_desc
|
||||
Config.DeviceConfig.Role.ROUTER_LATE -> Res.string.role_router_late_desc
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
private val DeviceConfig.RebroadcastMode.description: StringResource
|
||||
private val Config.DeviceConfig.RebroadcastMode.description: StringResource
|
||||
get() =
|
||||
when (this) {
|
||||
DeviceConfig.RebroadcastMode.ALL -> Res.string.rebroadcast_mode_all_desc
|
||||
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> Res.string.rebroadcast_mode_all_skip_decoding_desc
|
||||
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> Res.string.rebroadcast_mode_local_only_desc
|
||||
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> Res.string.rebroadcast_mode_known_only_desc
|
||||
DeviceConfig.RebroadcastMode.NONE -> Res.string.rebroadcast_mode_none_desc
|
||||
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> Res.string.rebroadcast_mode_core_portnums_only_desc
|
||||
Config.DeviceConfig.RebroadcastMode.ALL -> Res.string.rebroadcast_mode_all_desc
|
||||
Config.DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> Res.string.rebroadcast_mode_all_skip_decoding_desc
|
||||
Config.DeviceConfig.RebroadcastMode.LOCAL_ONLY -> Res.string.rebroadcast_mode_local_only_desc
|
||||
Config.DeviceConfig.RebroadcastMode.KNOWN_ONLY -> Res.string.rebroadcast_mode_known_only_desc
|
||||
Config.DeviceConfig.RebroadcastMode.NONE -> Res.string.rebroadcast_mode_none_desc
|
||||
Config.DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY ->
|
||||
Res.string.rebroadcast_mode_core_portnums_only_desc
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
|
|
@ -159,19 +158,19 @@ private val DeviceConfig.RebroadcastMode.description: StringResource
|
|||
@Composable
|
||||
fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val deviceConfig = state.radioConfig.device
|
||||
val deviceConfig = state.radioConfig.device ?: Config.DeviceConfig()
|
||||
val formState = rememberConfigState(initialValue = deviceConfig)
|
||||
var selectedRole by rememberSaveable { mutableStateOf(formState.value.role) }
|
||||
var selectedRole by rememberSaveable { mutableStateOf(formState.value.role ?: Config.DeviceConfig.Role.CLIENT) }
|
||||
val infrastructureRoles =
|
||||
listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.ROUTER_LATE, DeviceConfig.Role.REPEATER)
|
||||
listOf(Config.DeviceConfig.Role.ROUTER, Config.DeviceConfig.Role.ROUTER_LATE, Config.DeviceConfig.Role.REPEATER)
|
||||
if (selectedRole != formState.value.role) {
|
||||
if (selectedRole in infrastructureRoles) {
|
||||
RouterRoleConfirmationDialog(
|
||||
onDismiss = { selectedRole = formState.value.role },
|
||||
onConfirm = { formState.value = formState.value.copy { role = selectedRole } },
|
||||
onDismiss = { selectedRole = formState.value.role ?: Config.DeviceConfig.Role.CLIENT },
|
||||
onConfirm = { formState.value = formState.value.copy(role = selectedRole) },
|
||||
)
|
||||
} else {
|
||||
formState.value = formState.value.copy { role = selectedRole }
|
||||
formState.value = formState.value.copy(role = selectedRole)
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
|
@ -183,28 +182,30 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { device = it }
|
||||
val config = Config(device = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
item {
|
||||
TitledCard(title = stringResource(Res.string.options)) {
|
||||
val currentRole = formState.value.role ?: Config.DeviceConfig.Role.CLIENT
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.role),
|
||||
enabled = state.connected,
|
||||
selectedItem = formState.value.role,
|
||||
selectedItem = currentRole,
|
||||
onItemSelected = { selectedRole = it },
|
||||
summary = stringResource(formState.value.role.description),
|
||||
summary = stringResource(currentRole.description),
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
val currentRebroadcastMode = formState.value.rebroadcast_mode ?: Config.DeviceConfig.RebroadcastMode.ALL
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.rebroadcast_mode),
|
||||
enabled = state.connected,
|
||||
selectedItem = formState.value.rebroadcastMode,
|
||||
onItemSelected = { formState.value = formState.value.copy { rebroadcastMode = it } },
|
||||
summary = stringResource(formState.value.rebroadcastMode.description),
|
||||
selectedItem = currentRebroadcastMode,
|
||||
onItemSelected = { formState.value = formState.value.copy(rebroadcast_mode = it) },
|
||||
summary = stringResource(currentRebroadcastMode.description),
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
|
@ -212,10 +213,10 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
val nodeInfoBroadcastIntervals = remember { IntervalConfiguration.NODE_INFO_BROADCAST.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.nodeinfo_broadcast_interval),
|
||||
selectedItem = formState.value.nodeInfoBroadcastSecs.toLong(),
|
||||
selectedItem = (formState.value.node_info_broadcast_secs ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = nodeInfoBroadcastIntervals.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { nodeInfoBroadcastSecs = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(node_info_broadcast_secs = it.toInt()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -225,9 +226,9 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.double_tap_as_button_press),
|
||||
summary = stringResource(Res.string.config_device_doubleTapAsButtonPress_summary),
|
||||
checked = formState.value.doubleTapAsButtonPress,
|
||||
checked = formState.value.double_tap_as_button_press,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { doubleTapAsButtonPress = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(double_tap_as_button_press = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
|
||||
|
|
@ -236,9 +237,9 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.triple_click_adhoc_ping),
|
||||
summary = stringResource(Res.string.config_device_tripleClickAsAdHocPing_summary),
|
||||
checked = !formState.value.disableTripleClick,
|
||||
checked = !formState.value.disable_triple_click,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { disableTripleClick = !it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(disable_triple_click = !it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
|
||||
|
|
@ -247,9 +248,9 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.led_heartbeat),
|
||||
summary = stringResource(Res.string.config_device_ledHeartbeatEnabled_summary),
|
||||
checked = !formState.value.ledHeartbeatDisabled,
|
||||
checked = !formState.value.led_heartbeat_disabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { ledHeartbeatDisabled = !it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(led_heartbeat_disabled = !it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -273,7 +274,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
|
||||
EditTextPreference(
|
||||
title = "",
|
||||
value = formState.value.tzdef,
|
||||
value = formState.value.tzdef ?: "",
|
||||
summary = stringResource(Res.string.config_device_tzdef_summary),
|
||||
maxSize = 64, // tzdef max_size:65
|
||||
enabled = state.connected,
|
||||
|
|
@ -281,9 +282,9 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { tzdef = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(tzdef = it) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { formState.value = formState.value.copy { tzdef = "" } }) {
|
||||
IconButton(onClick = { formState.value = formState.value.copy(tzdef = "") }) {
|
||||
Icon(imageVector = Icons.Rounded.Clear, contentDescription = null)
|
||||
}
|
||||
},
|
||||
|
|
@ -295,7 +296,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
modifier = Modifier.height(MediumContainerHeight).fillMaxWidth(),
|
||||
enabled = state.connected,
|
||||
shape = RectangleShape,
|
||||
onClick = { formState.value = formState.value.copy { tzdef = appTzPosixString } },
|
||||
onClick = { formState.value = formState.value.copy(tzdef = appTzPosixString) },
|
||||
) {
|
||||
Icon(imageVector = Icons.Rounded.PhoneAndroid, contentDescription = null)
|
||||
|
||||
|
|
@ -310,20 +311,20 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
TitledCard(title = stringResource(Res.string.gpio)) {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.button_gpio),
|
||||
value = formState.value.buttonGpio,
|
||||
value = formState.value.button_gpio ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { buttonGpio = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(button_gpio = it) },
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.buzzer_gpio),
|
||||
value = formState.value.buzzerGpio,
|
||||
value = formState.value.buzzer_gpio ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { buzzerGpio = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(buzzer_gpio = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.material3.CardDefaults
|
||||
|
|
@ -56,14 +55,12 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val displayConfig = state.radioConfig.display
|
||||
val displayConfig = state.radioConfig.display ?: Config.DisplayConfig()
|
||||
val formState = rememberConfigState(initialValue = displayConfig)
|
||||
|
||||
RadioConfigScreenList(
|
||||
|
|
@ -74,7 +71,7 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { display = it }
|
||||
val config = Config(display = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -83,9 +80,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.always_point_north),
|
||||
summary = stringResource(Res.string.config_display_compass_north_top_summary),
|
||||
checked = formState.value.compassNorthTop,
|
||||
checked = formState.value.compass_north_top ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { compassNorthTop = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(compass_north_top = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -93,17 +90,17 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
title = stringResource(Res.string.use_12h_format),
|
||||
summary = stringResource(Res.string.display_time_in_12h_format),
|
||||
enabled = state.connected,
|
||||
checked = formState.value.use12HClock,
|
||||
onCheckedChange = { formState.value = formState.value.copy { use12HClock = it } },
|
||||
checked = formState.value.use_12h_clock ?: false,
|
||||
onCheckedChange = { formState.value = formState.value.copy(use_12h_clock = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.bold_heading),
|
||||
summary = stringResource(Res.string.config_display_heading_bold_summary),
|
||||
checked = formState.value.headingBold,
|
||||
checked = formState.value.heading_bold ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { headingBold = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(heading_bold = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -111,12 +108,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
title = stringResource(Res.string.display_units),
|
||||
summary = stringResource(Res.string.config_display_units_summary),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
DisplayConfig.DisplayUnits.entries
|
||||
.filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.units,
|
||||
onItemSelected = { formState.value = formState.value.copy { units = it } },
|
||||
items = Config.DisplayConfig.DisplayUnits.entries.map { it to it.name },
|
||||
selectedItem = formState.value.units ?: Config.DisplayConfig.DisplayUnits.METRIC,
|
||||
onItemSelected = { formState.value = formState.value.copy(units = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -130,9 +124,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
enabled = state.connected,
|
||||
items = screenOnIntervals.map { it to it.toDisplayString() },
|
||||
selectedItem =
|
||||
screenOnIntervals.find { it.value == formState.value.screenOnSecs.toLong() }
|
||||
screenOnIntervals.find { it.value == (formState.value.screen_on_secs ?: 0).toLong() }
|
||||
?: screenOnIntervals.first(),
|
||||
onItemSelected = { formState.value = formState.value.copy { screenOnSecs = it.value.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(screen_on_secs = it.value.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
|
|
@ -141,28 +135,28 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
enabled = state.connected,
|
||||
items = carouselIntervals.map { it to it.toDisplayString() },
|
||||
selectedItem =
|
||||
carouselIntervals.find { it.value == formState.value.autoScreenCarouselSecs.toLong() }
|
||||
carouselIntervals.find { it.value == (formState.value.auto_screen_carousel_secs ?: 0).toLong() }
|
||||
?: carouselIntervals.first(),
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.copy { autoScreenCarouselSecs = it.value.toInt() }
|
||||
formState.value = formState.value.copy(auto_screen_carousel_secs = it.value.toInt())
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.wake_on_tap_or_motion),
|
||||
summary = stringResource(Res.string.config_display_wake_on_tap_or_motion_summary),
|
||||
checked = formState.value.wakeOnTapOrMotion,
|
||||
checked = formState.value.wake_on_tap_or_motion ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { wakeOnTapOrMotion = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(wake_on_tap_or_motion = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.flip_screen),
|
||||
summary = stringResource(Res.string.config_display_flip_screen_summary),
|
||||
checked = formState.value.flipScreen,
|
||||
checked = formState.value.flip_screen ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { flipScreen = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(flip_screen = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -170,35 +164,27 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
title = stringResource(Res.string.display_mode),
|
||||
summary = stringResource(Res.string.config_display_displaymode_summary),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
DisplayConfig.DisplayMode.entries
|
||||
.filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.displaymode,
|
||||
onItemSelected = { formState.value = formState.value.copy { displaymode = it } },
|
||||
items = Config.DisplayConfig.DisplayMode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.displaymode ?: Config.DisplayConfig.DisplayMode.DEFAULT,
|
||||
onItemSelected = { formState.value = formState.value.copy(displaymode = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.oled_type),
|
||||
summary = stringResource(Res.string.config_display_oled_summary),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
DisplayConfig.OledType.entries
|
||||
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.oled,
|
||||
onItemSelected = { formState.value = formState.value.copy { oled = it } },
|
||||
items = Config.DisplayConfig.OledType.entries.map { it to it.name },
|
||||
selectedItem = formState.value.oled ?: Config.DisplayConfig.OledType.OLED_AUTO,
|
||||
onItemSelected = { formState.value = formState.value.copy(oled = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.compass_orientation),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
DisplayConfig.CompassOrientation.entries
|
||||
.filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.compassOrientation,
|
||||
onItemSelected = { formState.value = formState.value.copy { compassOrientation = it } },
|
||||
items = Config.DisplayConfig.CompassOrientation.entries.map { it to it.name },
|
||||
selectedItem =
|
||||
formState.value.compass_orientation ?: Config.DisplayConfig.CompassOrientation.DEGREES_0,
|
||||
onItemSelected = { formState.value = formState.value.copy(compass_orientation = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -39,15 +38,28 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.Descriptors
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.channel_url
|
||||
import org.meshtastic.core.strings.fixed_position
|
||||
import org.meshtastic.core.strings.long_name
|
||||
import org.meshtastic.core.strings.module_settings
|
||||
import org.meshtastic.core.strings.radio_configuration
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.strings.short_name
|
||||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.proto.ClientOnlyProtos.DeviceProfile
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
|
||||
private const val SUPPORTED_FIELDS = 7
|
||||
private enum class ProfileField(val tag: Int, val labelRes: StringResource) {
|
||||
LONG_NAME(1, Res.string.long_name),
|
||||
SHORT_NAME(2, Res.string.short_name),
|
||||
CHANNEL_URL(3, Res.string.channel_url),
|
||||
CONFIG(4, Res.string.radio_configuration),
|
||||
MODULE_CONFIG(5, Res.string.module_settings),
|
||||
FIXED_POSITION(6, Res.string.fixed_position),
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
|
@ -60,12 +72,19 @@ fun EditDeviceProfileDialog(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val state = remember {
|
||||
val fields =
|
||||
deviceProfile.descriptorForType.fields.filter {
|
||||
it.number < SUPPORTED_FIELDS
|
||||
} // TODO add ringtone & canned messages
|
||||
mutableStateMapOf<Descriptors.FieldDescriptor, Boolean>().apply {
|
||||
putAll(fields.associateWith(deviceProfile::hasField))
|
||||
mutableStateMapOf<ProfileField, Boolean>().apply {
|
||||
putAll(
|
||||
ProfileField.entries.associateWith { field ->
|
||||
when (field) {
|
||||
ProfileField.LONG_NAME -> deviceProfile.long_name != null
|
||||
ProfileField.SHORT_NAME -> deviceProfile.short_name != null
|
||||
ProfileField.CHANNEL_URL -> deviceProfile.channel_url != null
|
||||
ProfileField.CONFIG -> deviceProfile.config != null
|
||||
ProfileField.MODULE_CONFIG -> deviceProfile.module_config != null
|
||||
ProfileField.FIXED_POSITION -> deviceProfile.fixed_position != null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,17 +103,24 @@ fun EditDeviceProfileDialog(
|
|||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
||||
)
|
||||
HorizontalDivider()
|
||||
state.keys
|
||||
.sortedBy { it.number }
|
||||
.forEach { field ->
|
||||
SwitchPreference(
|
||||
title = field.name,
|
||||
checked = state[field] == true,
|
||||
enabled = deviceProfile.hasField(field),
|
||||
onCheckedChange = { state[field] = it },
|
||||
padding = PaddingValues(0.dp),
|
||||
)
|
||||
}
|
||||
ProfileField.entries.forEach { field ->
|
||||
val isAvailable =
|
||||
when (field) {
|
||||
ProfileField.LONG_NAME -> deviceProfile.long_name != null
|
||||
ProfileField.SHORT_NAME -> deviceProfile.short_name != null
|
||||
ProfileField.CHANNEL_URL -> deviceProfile.channel_url != null
|
||||
ProfileField.CONFIG -> deviceProfile.config != null
|
||||
ProfileField.MODULE_CONFIG -> deviceProfile.module_config != null
|
||||
ProfileField.FIXED_POSITION -> deviceProfile.fixed_position != null
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(field.labelRes),
|
||||
checked = state[field] == true,
|
||||
enabled = isAvailable,
|
||||
onCheckedChange = { state[field] = it },
|
||||
padding = PaddingValues(0.dp),
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
},
|
||||
|
|
@ -109,13 +135,29 @@ fun EditDeviceProfileDialog(
|
|||
Button(
|
||||
modifier = modifier.weight(1f),
|
||||
onClick = {
|
||||
val builder = DeviceProfile.newBuilder()
|
||||
deviceProfile.allFields.forEach { (field, value) ->
|
||||
if (state[field] == true) {
|
||||
builder.setField(field, value)
|
||||
}
|
||||
}
|
||||
onConfirm(builder.build())
|
||||
val result =
|
||||
DeviceProfile(
|
||||
long_name =
|
||||
if (state[ProfileField.LONG_NAME] == true) deviceProfile.long_name else null,
|
||||
short_name =
|
||||
if (state[ProfileField.SHORT_NAME] == true) deviceProfile.short_name else null,
|
||||
channel_url =
|
||||
if (state[ProfileField.CHANNEL_URL] == true) deviceProfile.channel_url else null,
|
||||
config = if (state[ProfileField.CONFIG] == true) deviceProfile.config else null,
|
||||
module_config =
|
||||
if (state[ProfileField.MODULE_CONFIG] == true) {
|
||||
deviceProfile.module_config
|
||||
} else {
|
||||
null
|
||||
},
|
||||
fixed_position =
|
||||
if (state[ProfileField.FIXED_POSITION] == true) {
|
||||
deviceProfile.fixed_position
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
onConfirm(result)
|
||||
},
|
||||
enabled = state.values.any { it },
|
||||
) {
|
||||
|
|
@ -131,7 +173,7 @@ fun EditDeviceProfileDialog(
|
|||
private fun EditDeviceProfileDialogPreview() {
|
||||
EditDeviceProfileDialog(
|
||||
title = "Export configuration",
|
||||
deviceProfile = DeviceProfile.getDefaultInstance(),
|
||||
deviceProfile = DeviceProfile(),
|
||||
onConfirm = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.FolderOpen
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -77,8 +77,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.gpioPins
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import java.io.File
|
||||
|
||||
private const val MAX_RINGTONE_SIZE = 230
|
||||
|
|
@ -91,7 +90,7 @@ fun ExternalNotificationConfigScreen(
|
|||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val extNotificationConfig = state.moduleConfig.externalNotification
|
||||
val extNotificationConfig = state.moduleConfig.external_notification ?: ModuleConfig.ExternalNotificationConfig()
|
||||
val ringtone = state.ringtone
|
||||
val formState = rememberConfigState(initialValue = extNotificationConfig)
|
||||
var ringtoneInput by rememberSaveable(ringtone) { mutableStateOf(ringtone) }
|
||||
|
|
@ -136,7 +135,7 @@ fun ExternalNotificationConfigScreen(
|
|||
viewModel.setRingtone(ringtoneInput)
|
||||
}
|
||||
if (formState.value != extNotificationConfig) {
|
||||
val config = moduleConfig { externalNotification = formState.value }
|
||||
val config = ModuleConfig(external_notification = formState.value)
|
||||
viewModel.setModuleConfig(config)
|
||||
}
|
||||
},
|
||||
|
|
@ -145,9 +144,9 @@ fun ExternalNotificationConfigScreen(
|
|||
TitledCard(title = stringResource(Res.string.external_notification_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.external_notification_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -157,25 +156,25 @@ fun ExternalNotificationConfigScreen(
|
|||
TitledCard(title = stringResource(Res.string.notifications_on_message_receipt)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.alert_message_led),
|
||||
checked = formState.value.alertMessage,
|
||||
checked = formState.value.alert_message ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { alertMessage = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(alert_message = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.alert_message_buzzer),
|
||||
checked = formState.value.alertMessageBuzzer,
|
||||
checked = formState.value.alert_message_buzzer ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { alertMessageBuzzer = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(alert_message_buzzer = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.alert_message_vibra),
|
||||
checked = formState.value.alertMessageVibra,
|
||||
checked = formState.value.alert_message_vibra ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { alertMessageVibra = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(alert_message_vibra = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -185,25 +184,25 @@ fun ExternalNotificationConfigScreen(
|
|||
TitledCard(title = stringResource(Res.string.notifications_on_alert_bell_receipt)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.alert_bell_led),
|
||||
checked = formState.value.alertBell,
|
||||
checked = formState.value.alert_bell ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { alertBell = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(alert_bell = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.alert_bell_buzzer),
|
||||
checked = formState.value.alertBellBuzzer,
|
||||
checked = formState.value.alert_bell_buzzer ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { alertBellBuzzer = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(alert_bell_buzzer = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.alert_bell_vibra),
|
||||
checked = formState.value.alertBellVibra,
|
||||
checked = formState.value.alert_bell_vibra ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { alertBellVibra = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(alert_bell_vibra = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -215,17 +214,17 @@ fun ExternalNotificationConfigScreen(
|
|||
DropDownPreference(
|
||||
title = stringResource(Res.string.output_led_gpio),
|
||||
items = gpio,
|
||||
selectedItem = formState.value.output,
|
||||
selectedItem = (formState.value.output ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
onItemSelected = { formState.value = formState.value.copy { output = it } },
|
||||
onItemSelected = { formState.value = formState.value.copy(output = it.toInt()) },
|
||||
)
|
||||
if (formState.value.output != 0) {
|
||||
if (formState.value.output ?: 0 != 0) {
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.output_led_active_high),
|
||||
checked = formState.value.active,
|
||||
checked = formState.value.active ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { active = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(active = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -233,17 +232,17 @@ fun ExternalNotificationConfigScreen(
|
|||
DropDownPreference(
|
||||
title = stringResource(Res.string.output_buzzer_gpio),
|
||||
items = gpio,
|
||||
selectedItem = formState.value.outputBuzzer,
|
||||
selectedItem = (formState.value.output_buzzer ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
onItemSelected = { formState.value = formState.value.copy { outputBuzzer = it } },
|
||||
onItemSelected = { formState.value = formState.value.copy(output_buzzer = it.toInt()) },
|
||||
)
|
||||
if (formState.value.outputBuzzer != 0) {
|
||||
if (formState.value.output_buzzer ?: 0 != 0) {
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_pwm_buzzer),
|
||||
checked = formState.value.usePwm,
|
||||
checked = formState.value.use_pwm ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { usePwm = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(use_pwm = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -251,27 +250,27 @@ fun ExternalNotificationConfigScreen(
|
|||
DropDownPreference(
|
||||
title = stringResource(Res.string.output_vibra_gpio),
|
||||
items = gpio,
|
||||
selectedItem = formState.value.outputVibra,
|
||||
selectedItem = (formState.value.output_vibra ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
onItemSelected = { formState.value = formState.value.copy { outputVibra = it } },
|
||||
onItemSelected = { formState.value = formState.value.copy(output_vibra = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
val outputItems = remember { IntervalConfiguration.OUTPUT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.output_duration_milliseconds),
|
||||
items = outputItems.map { it.value to it.toDisplayString() },
|
||||
selectedItem = formState.value.outputMs.toLong(),
|
||||
selectedItem = (formState.value.output_ms ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
onItemSelected = { formState.value = formState.value.copy { outputMs = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(output_ms = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
val nagItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.nag_timeout_seconds),
|
||||
items = nagItems.map { it.value to it.toDisplayString() },
|
||||
selectedItem = formState.value.nagTimeout.toLong(),
|
||||
selectedItem = (formState.value.nag_timeout ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
onItemSelected = { formState.value = formState.value.copy { nagTimeout = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(nag_timeout = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
|
|
@ -288,7 +287,7 @@ fun ExternalNotificationConfigScreen(
|
|||
Row {
|
||||
IconButton(onClick = { launcher.launch("*/*") }, enabled = state.connected) {
|
||||
Icon(
|
||||
Icons.Rounded.FolderOpen,
|
||||
Icons.Default.FolderOpen,
|
||||
contentDescription = stringResource(Res.string.import_label),
|
||||
)
|
||||
}
|
||||
|
|
@ -312,7 +311,7 @@ fun ExternalNotificationConfigScreen(
|
|||
},
|
||||
enabled = state.connected,
|
||||
) {
|
||||
Icon(Icons.Rounded.PlayArrow, contentDescription = stringResource(Res.string.play))
|
||||
Icon(Icons.Default.PlayArrow, contentDescription = stringResource(Res.string.play))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -320,9 +319,9 @@ fun ExternalNotificationConfigScreen(
|
|||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_i2s_as_buzzer),
|
||||
checked = formState.value.useI2SAsBuzzer,
|
||||
checked = formState.value.use_i2s_as_buzzer ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { useI2SAsBuzzer = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(use_i2s_as_buzzer = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,16 +60,14 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
|
||||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.util.labelRes
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.hopLimits
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val loraConfig = state.radioConfig.lora
|
||||
val loraConfig = state.radioConfig.lora ?: Config.LoRaConfig()
|
||||
val primarySettings = state.channelList.getOrNull(0) ?: return
|
||||
val formState = rememberConfigState(initialValue = loraConfig)
|
||||
|
||||
|
|
@ -84,7 +82,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { lora = it }
|
||||
val config = Config(lora = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -96,49 +94,49 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
enabled = state.connected,
|
||||
items = RegionInfo.entries.map { it.regionCode to it.description },
|
||||
selectedItem = formState.value.region,
|
||||
onItemSelected = { formState.value = formState.value.copy { region = it } },
|
||||
onItemSelected = { formState.value = formState.value.copy(region = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_modem_preset),
|
||||
checked = formState.value.usePreset,
|
||||
checked = formState.value.use_preset,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(use_preset = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
if (formState.value.usePreset) {
|
||||
if (formState.value.use_preset) {
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.modem_preset),
|
||||
summary = stringResource(Res.string.config_lora_modem_preset_summary),
|
||||
enabled = state.connected && formState.value.usePreset,
|
||||
items = ChannelOption.entries.map { it.modemPreset to stringResource(it.labelRes) },
|
||||
selectedItem = formState.value.modemPreset,
|
||||
onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
|
||||
enabled = state.connected && formState.value.use_preset,
|
||||
items = ChannelOption.entries.map { it.modemPreset to it.modemPreset.name },
|
||||
selectedItem = formState.value.modem_preset,
|
||||
onItemSelected = { formState.value = formState.value.copy(modem_preset = it) },
|
||||
)
|
||||
} else {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.bandwidth),
|
||||
value = formState.value.bandwidth,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
enabled = state.connected && !formState.value.use_preset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(bandwidth = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.spread_factor),
|
||||
value = formState.value.spreadFactor,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
value = formState.value.spread_factor,
|
||||
enabled = state.connected && !formState.value.use_preset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(spread_factor = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.coding_rate),
|
||||
value = formState.value.codingRate,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
value = formState.value.coding_rate,
|
||||
enabled = state.connected && !formState.value.use_preset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(coding_rate = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -148,33 +146,33 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
TitledCard(title = stringResource(Res.string.advanced)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.ignore_mqtt),
|
||||
checked = formState.value.ignoreMqtt,
|
||||
checked = formState.value.ignore_mqtt,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(ignore_mqtt = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.ok_to_mqtt),
|
||||
checked = formState.value.configOkToMqtt,
|
||||
checked = formState.value.config_ok_to_mqtt,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(config_ok_to_mqtt = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.tx_enabled),
|
||||
checked = formState.value.txEnabled,
|
||||
checked = formState.value.tx_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(tx_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.override_duty_cycle),
|
||||
checked = formState.value.overrideDutyCycle,
|
||||
checked = formState.value.override_duty_cycle,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { overrideDutyCycle = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(override_duty_cycle = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -183,8 +181,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
title = stringResource(Res.string.hop_limit),
|
||||
summary = stringResource(Res.string.config_lora_hop_limit_summary),
|
||||
items = hopLimitItems,
|
||||
selectedItem = formState.value.hopLimit,
|
||||
onItemSelected = { formState.value = formState.value.copy { hopLimit = it } },
|
||||
selectedItem = formState.value.hop_limit.toLong(),
|
||||
onItemSelected = { formState.value = formState.value.copy(hop_limit = it.toInt()) },
|
||||
enabled = state.connected,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -193,8 +191,8 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
title = stringResource(Res.string.frequency_slot),
|
||||
summary = stringResource(Res.string.config_lora_frequency_slot_summary),
|
||||
value =
|
||||
if (isFocusedSlot || formState.value.channelNum != 0) {
|
||||
formState.value.channelNum
|
||||
if (isFocusedSlot || formState.value.channel_num != 0) {
|
||||
formState.value.channel_num
|
||||
} else {
|
||||
primaryChannel.channelNum
|
||||
},
|
||||
|
|
@ -203,16 +201,16 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
onFocusChanged = { isFocusedSlot = it.isFocused },
|
||||
onValueChanged = {
|
||||
if (it <= formState.value.numChannels) { // total num of LoRa channels
|
||||
formState.value = formState.value.copy { channelNum = it }
|
||||
formState.value = formState.value.copy(channel_num = it)
|
||||
}
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.sx126x_rx_boosted_gain),
|
||||
checked = formState.value.sx126XRxBoostedGain,
|
||||
checked = formState.value.sx126x_rx_boosted_gain,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(sx126x_rx_boosted_gain = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -220,31 +218,31 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
EditTextPreference(
|
||||
title = stringResource(Res.string.override_frequency_mhz),
|
||||
value =
|
||||
if (isFocusedOverride || formState.value.overrideFrequency != 0f) {
|
||||
formState.value.overrideFrequency
|
||||
if (isFocusedOverride || formState.value.override_frequency != 0f) {
|
||||
formState.value.override_frequency
|
||||
} else {
|
||||
primaryChannel.radioFreq
|
||||
},
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocusedOverride = it.isFocused },
|
||||
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(override_frequency = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SignedIntegerEditTextPreference(
|
||||
title = stringResource(Res.string.tx_power_dbm),
|
||||
value = formState.value.txPower,
|
||||
value = formState.value.tx_power,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(tx_power = it) },
|
||||
)
|
||||
if (viewModel.hasPaFan) {
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.pa_fan_disabled),
|
||||
checked = formState.value.paFanDisabled,
|
||||
checked = formState.value.pa_fan_disabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(pa_fan_disabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("LongMethod")
|
||||
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
|
|
@ -50,29 +49,26 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val destNum = destNode?.num
|
||||
val mqttConfig = state.moduleConfig.mqtt
|
||||
val mqttConfig = state.moduleConfig.mqtt ?: ModuleConfig.MQTTConfig()
|
||||
val formState = rememberConfigState(initialValue = mqttConfig)
|
||||
|
||||
if (!formState.value.mapReportSettings.shouldReportLocation) {
|
||||
val settings =
|
||||
formState.value.mapReportSettings.copy {
|
||||
this.shouldReportLocation = viewModel.shouldReportLocation(destNum)
|
||||
}
|
||||
formState.value = formState.value.copy { mapReportSettings = settings }
|
||||
val currentMapReportSettings = formState.value.map_report_settings ?: ModuleConfig.MapReportSettings()
|
||||
if (!(currentMapReportSettings.should_report_location ?: false)) {
|
||||
val settings = currentMapReportSettings.copy(should_report_location = viewModel.shouldReportLocation(destNum))
|
||||
formState.value = formState.value.copy(map_report_settings = settings)
|
||||
}
|
||||
|
||||
val consentValid =
|
||||
if (formState.value.mapReportingEnabled) {
|
||||
formState.value.mapReportSettings.shouldReportLocation &&
|
||||
formState.value.mapReportSettings.publishIntervalSecs >= MIN_INTERVAL_SECS
|
||||
if (formState.value.map_reporting_enabled ?: false) {
|
||||
(formState.value.map_report_settings?.should_report_location ?: false) &&
|
||||
(formState.value.map_report_settings?.publish_interval_secs ?: 0) >= MIN_INTERVAL_SECS
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
@ -86,7 +82,7 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { mqtt = it }
|
||||
val config = ModuleConfig(mqtt = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -94,89 +90,91 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
TitledCard(title = stringResource(Res.string.mqtt_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.mqtt_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.address),
|
||||
value = formState.value.address,
|
||||
value = formState.value.address ?: "",
|
||||
maxSize = 63, // address max_size:64
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { address = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(address = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.username),
|
||||
value = formState.value.username,
|
||||
value = formState.value.username ?: "",
|
||||
maxSize = 63, // username max_size:64
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { username = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(username = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditPasswordPreference(
|
||||
title = stringResource(Res.string.password),
|
||||
value = formState.value.password,
|
||||
value = formState.value.password ?: "",
|
||||
maxSize = 63, // password max_size:64
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { password = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(password = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.encryption_enabled),
|
||||
checked = formState.value.encryptionEnabled,
|
||||
checked = formState.value.encryption_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { encryptionEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(encryption_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.json_output_enabled),
|
||||
checked = formState.value.jsonEnabled,
|
||||
checked = formState.value.json_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { jsonEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(json_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val defaultAddress = stringResource(Res.string.default_mqtt_address)
|
||||
val isDefault = formState.value.address.isEmpty() || formState.value.address.contains(defaultAddress)
|
||||
val enforceTls = isDefault && formState.value.proxyToClientEnabled
|
||||
val isDefault =
|
||||
(formState.value.address ?: "").isEmpty() ||
|
||||
(formState.value.address ?: "").contains(defaultAddress)
|
||||
val enforceTls = isDefault && (formState.value.proxy_to_client_enabled ?: false)
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.tls_enabled),
|
||||
checked = formState.value.tlsEnabled || enforceTls,
|
||||
checked = (formState.value.tls_enabled ?: false) || enforceTls,
|
||||
enabled = state.connected && !enforceTls,
|
||||
onCheckedChange = { formState.value = formState.value.copy { tlsEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(tls_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.root_topic),
|
||||
value = formState.value.root,
|
||||
value = formState.value.root ?: "",
|
||||
maxSize = 31, // root max_size:32
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { root = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(root = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.proxy_to_client_enabled),
|
||||
checked = formState.value.proxyToClientEnabled,
|
||||
checked = formState.value.proxy_to_client_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { proxyToClientEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(proxy_to_client_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -184,26 +182,27 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
|
||||
item {
|
||||
TitledCard(title = stringResource(Res.string.map_reporting)) {
|
||||
val mapReportSettings = formState.value.map_report_settings ?: ModuleConfig.MapReportSettings()
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = formState.value.mapReportingEnabled,
|
||||
mapReportingEnabled = formState.value.map_reporting_enabled ?: false,
|
||||
onMapReportingEnabledChanged = {
|
||||
formState.value = formState.value.copy { mapReportingEnabled = it }
|
||||
formState.value = formState.value.copy(map_reporting_enabled = it)
|
||||
},
|
||||
shouldReportLocation = formState.value.mapReportSettings.shouldReportLocation,
|
||||
shouldReportLocation = mapReportSettings.should_report_location ?: false,
|
||||
onShouldReportLocationChanged = {
|
||||
viewModel.setShouldReportLocation(destNum, it)
|
||||
val settings = formState.value.mapReportSettings.copy { this.shouldReportLocation = it }
|
||||
formState.value = formState.value.copy { mapReportSettings = settings }
|
||||
val settings = mapReportSettings.copy(should_report_location = it)
|
||||
formState.value = formState.value.copy(map_report_settings = settings)
|
||||
},
|
||||
positionPrecision = formState.value.mapReportSettings.positionPrecision,
|
||||
positionPrecision = mapReportSettings.position_precision ?: 0,
|
||||
onPositionPrecisionChanged = {
|
||||
val settings = formState.value.mapReportSettings.copy { positionPrecision = it }
|
||||
formState.value = formState.value.copy { mapReportSettings = settings }
|
||||
val settings = mapReportSettings.copy(position_precision = it)
|
||||
formState.value = formState.value.copy(map_report_settings = settings)
|
||||
},
|
||||
publishIntervalSecs = formState.value.mapReportSettings.publishIntervalSecs,
|
||||
publishIntervalSecs = mapReportSettings.publish_interval_secs ?: 0,
|
||||
onPublishIntervalSecsChanged = {
|
||||
val settings = formState.value.mapReportSettings.copy { publishIntervalSecs = it }
|
||||
formState.value = formState.value.copy { mapReportSettings = settings }
|
||||
val settings = mapReportSettings.copy(publish_interval_secs = it)
|
||||
formState.value = formState.value.copy(map_report_settings = settings)
|
||||
},
|
||||
enabled = state.connected,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -37,13 +36,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val neighborInfoConfig = state.moduleConfig.neighborInfo
|
||||
val neighborInfoConfig = state.moduleConfig.neighbor_info ?: ModuleConfig.NeighborInfoConfig()
|
||||
val formState = rememberConfigState(initialValue = neighborInfoConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -55,7 +53,7 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { neighborInfo = it }
|
||||
val config = ModuleConfig(neighbor_info = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -63,26 +61,26 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
TitledCard(title = stringResource(Res.string.neighbor_info_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.neighbor_info_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.update_interval_seconds),
|
||||
value = formState.value.updateInterval,
|
||||
value = formState.value.update_interval ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { updateInterval = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(update_interval = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.transmit_over_lora),
|
||||
summary = stringResource(Res.string.config_device_transmitOverLora_summary),
|
||||
checked = formState.value.transmitOverLora,
|
||||
checked = formState.value.transmit_over_lora ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { transmitOverLora = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(transmit_over_lora = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
|
|
@ -61,7 +60,6 @@ import org.meshtastic.core.strings.password
|
|||
import org.meshtastic.core.strings.rsyslog_server
|
||||
import org.meshtastic.core.strings.ssid
|
||||
import org.meshtastic.core.strings.subnet
|
||||
import org.meshtastic.core.strings.udp_config
|
||||
import org.meshtastic.core.strings.udp_enabled
|
||||
import org.meshtastic.core.strings.wifi_config
|
||||
import org.meshtastic.core.strings.wifi_enabled
|
||||
|
|
@ -77,9 +75,7 @@ import org.meshtastic.core.ui.component.SimpleAlertDialog
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.ConfigProtos.Config.NetworkConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
|
||||
|
|
@ -88,7 +84,7 @@ private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
|
|||
@Composable
|
||||
fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val networkConfig = state.radioConfig.network
|
||||
val networkConfig = state.radioConfig.network ?: Config.NetworkConfig()
|
||||
val formState = rememberConfigState(initialValue = networkConfig)
|
||||
|
||||
var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) }
|
||||
|
|
@ -101,11 +97,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
if (result.contents != null) {
|
||||
val (ssid, psk) = extractWifiCredentials(result.contents)
|
||||
if (ssid != null && psk != null) {
|
||||
formState.value =
|
||||
formState.value.copy {
|
||||
wifiSsid = ssid
|
||||
wifiPsk = psk
|
||||
}
|
||||
formState.value = formState.value.copy(wifi_ssid = ssid, wifi_psk = psk)
|
||||
} else {
|
||||
showScanErrorDialog = true
|
||||
}
|
||||
|
|
@ -132,32 +124,31 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { network = it }
|
||||
val config = Config(network = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
// Display device connection status
|
||||
state.deviceConnectionStatus?.let { connectionStatus ->
|
||||
if (
|
||||
connectionStatus.wifi?.status?.isConnected == true ||
|
||||
connectionStatus.ethernet?.status?.isConnected == true
|
||||
) {
|
||||
val ws = connectionStatus.wifi?.status
|
||||
val es = connectionStatus.ethernet?.status
|
||||
if (ws?.is_connected == true || es?.is_connected == true) {
|
||||
item {
|
||||
TitledCard(title = stringResource(Res.string.connection_status)) {
|
||||
connectionStatus.wifi?.let { wifiStatus ->
|
||||
if (wifiStatus.status.isConnected) {
|
||||
ws?.let { wifiStatus ->
|
||||
if (wifiStatus.is_connected) {
|
||||
ListItem(
|
||||
text = stringResource(Res.string.wifi_ip),
|
||||
supportingText = formatIpAddress(wifiStatus.status.ipAddress),
|
||||
supportingText = formatIpAddress(wifiStatus.ip_address ?: 0),
|
||||
trailingIcon = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
connectionStatus.ethernet?.let { ethernetStatus ->
|
||||
if (ethernetStatus.status.isConnected) {
|
||||
es?.let { ethernetStatus ->
|
||||
if (ethernetStatus.is_connected) {
|
||||
ListItem(
|
||||
text = stringResource(Res.string.ethernet_ip),
|
||||
supportingText = formatIpAddress(ethernetStatus.status.ipAddress),
|
||||
supportingText = formatIpAddress(ethernetStatus.ip_address ?: 0),
|
||||
trailingIcon = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -172,31 +163,31 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.wifi_enabled),
|
||||
summary = stringResource(Res.string.config_network_wifi_enabled_summary),
|
||||
checked = formState.value.wifiEnabled,
|
||||
checked = formState.value.wifi_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { wifiEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(wifi_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.ssid),
|
||||
value = formState.value.wifiSsid,
|
||||
value = formState.value.wifi_ssid ?: "",
|
||||
maxSize = 32, // wifi_ssid max_size:33
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { wifiSsid = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(wifi_ssid = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditPasswordPreference(
|
||||
title = stringResource(Res.string.password),
|
||||
value = formState.value.wifiPsk,
|
||||
value = formState.value.wifi_psk ?: "",
|
||||
maxSize = 64, // wifi_psk max_size:65
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { wifiPsk = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(wifi_psk = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
Button(
|
||||
|
|
@ -215,9 +206,9 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.ethernet_enabled),
|
||||
summary = stringResource(Res.string.config_network_eth_enabled_summary),
|
||||
checked = formState.value.ethEnabled,
|
||||
checked = formState.value.eth_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { ethEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(eth_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -226,15 +217,14 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
|
||||
if (state.metadata?.hasEthernet == true || state.metadata?.hasWifi == true) {
|
||||
item {
|
||||
TitledCard(title = stringResource(Res.string.udp_config)) {
|
||||
TitledCard(title = stringResource(Res.string.network)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.udp_enabled),
|
||||
summary = stringResource(Res.string.config_network_udp_enabled_summary),
|
||||
checked = formState.value.enabledProtocols == 1,
|
||||
checked = (formState.value.enabled_protocols ?: 0) == 1,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = {
|
||||
formState.value =
|
||||
formState.value.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
|
||||
formState.value = formState.value.copy(enabled_protocols = if (it) 1 else 0)
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
|
|
@ -246,81 +236,71 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
|
|||
TitledCard(title = stringResource(Res.string.advanced)) {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.ntp_server),
|
||||
value = formState.value.ntpServer,
|
||||
value = formState.value.ntp_server ?: "",
|
||||
maxSize = 32, // ntp_server max_size:33
|
||||
enabled = state.connected,
|
||||
isError = formState.value.ntpServer.isEmpty(),
|
||||
isError = formState.value.ntp_server?.isEmpty() ?: true,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { ntpServer = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(ntp_server = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.rsyslog_server),
|
||||
value = formState.value.rsyslogServer,
|
||||
value = formState.value.rsyslog_server ?: "",
|
||||
maxSize = 32, // rsyslog_server max_size:33
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { rsyslogServer = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(rsyslog_server = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.ipv4_mode),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
NetworkConfig.AddressMode.entries
|
||||
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.addressMode,
|
||||
onItemSelected = { formState.value = formState.value.copy { addressMode = it } },
|
||||
items = Config.NetworkConfig.AddressMode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.address_mode ?: Config.NetworkConfig.AddressMode.DHCP,
|
||||
onItemSelected = { formState.value = formState.value.copy(address_mode = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
val ipv4 = formState.value.ipv4_config ?: Config.NetworkConfig.IpV4Config()
|
||||
EditIPv4Preference(
|
||||
title = stringResource(Res.string.ip),
|
||||
value = formState.value.ipv4Config.ip,
|
||||
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
value = ipv4.ip ?: 0,
|
||||
enabled =
|
||||
state.connected && formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = formState.value.ipv4Config.copy { ip = it }
|
||||
formState.value = formState.value.copy { ipv4Config = ipv4 }
|
||||
},
|
||||
onValueChanged = { formState.value = formState.value.copy(ipv4_config = ipv4.copy(ip = it)) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditIPv4Preference(
|
||||
title = stringResource(Res.string.gateway),
|
||||
value = formState.value.ipv4Config.gateway,
|
||||
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
value = ipv4.gateway ?: 0,
|
||||
enabled =
|
||||
state.connected && formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = formState.value.ipv4Config.copy { gateway = it }
|
||||
formState.value = formState.value.copy { ipv4Config = ipv4 }
|
||||
},
|
||||
onValueChanged = { formState.value = formState.value.copy(ipv4_config = ipv4.copy(gateway = it)) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditIPv4Preference(
|
||||
title = stringResource(Res.string.subnet),
|
||||
value = formState.value.ipv4Config.subnet,
|
||||
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
value = ipv4.subnet ?: 0,
|
||||
enabled =
|
||||
state.connected && formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = formState.value.ipv4Config.copy { subnet = it }
|
||||
formState.value = formState.value.copy { ipv4Config = ipv4 }
|
||||
},
|
||||
onValueChanged = { formState.value = formState.value.copy(ipv4_config = ipv4.copy(subnet = it)) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditIPv4Preference(
|
||||
title = "DNS",
|
||||
value = formState.value.ipv4Config.dns,
|
||||
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
value = ipv4.dns ?: 0,
|
||||
enabled =
|
||||
state.connected && formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = formState.value.ipv4Config.copy { dns = it }
|
||||
formState.value = formState.value.copy { ipv4Config = ipv4 }
|
||||
},
|
||||
onValueChanged = { formState.value = formState.value.copy(ipv4_config = ipv4.copy(dns = it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -41,13 +40,12 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val paxcounterConfig = state.moduleConfig.paxcounter
|
||||
val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig()
|
||||
val formState = rememberConfigState(initialValue = paxcounterConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -59,7 +57,7 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), on
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { paxcounter = it }
|
||||
val config = ModuleConfig(paxcounter = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -67,37 +65,37 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), on
|
|||
TitledCard(title = stringResource(Res.string.paxcounter_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.paxcounter_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val items = remember { IntervalConfiguration.PAX_COUNTER.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.update_interval_seconds),
|
||||
selectedItem = formState.value.paxcounterUpdateInterval.toLong(),
|
||||
selectedItem = (formState.value.paxcounter_update_interval ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = items.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.copy { paxcounterUpdateInterval = it.toInt() }
|
||||
formState.value = formState.value.copy(paxcounter_update_interval = it.toInt())
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SignedIntegerEditTextPreference(
|
||||
title = stringResource(Res.string.wifi_rssi_threshold_defaults_to_80),
|
||||
value = formState.value.wifiThreshold,
|
||||
value = formState.value.wifi_threshold ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { wifiThreshold = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(wifi_threshold = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SignedIntegerEditTextPreference(
|
||||
title = stringResource(Res.string.ble_rssi_threshold_defaults_to_80),
|
||||
value = formState.value.bleThreshold,
|
||||
value = formState.value.ble_threshold ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { bleThreshold = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(ble_threshold = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import android.Manifest
|
||||
|
|
@ -78,9 +77,7 @@ import org.meshtastic.feature.settings.util.FixedUpdateIntervals
|
|||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.gpioPins
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.ConfigProtos.Config.PositionConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
|
|
@ -96,22 +93,23 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
altitude = node?.position?.altitude ?: 0,
|
||||
time = 1, // ignore time for fixed_position
|
||||
)
|
||||
val positionConfig = state.radioConfig.position
|
||||
val positionConfig = state.radioConfig.position ?: Config.PositionConfig()
|
||||
val sanitizedPositionConfig =
|
||||
remember(positionConfig) {
|
||||
val positionItems = IntervalConfiguration.POSITION.allowedIntervals
|
||||
val smartBroadcastItems = IntervalConfiguration.SMART_BROADCAST_MINIMUM.allowedIntervals
|
||||
positionConfig.copy {
|
||||
if (FixedUpdateIntervals.fromValue(positionBroadcastSecs.toLong()) == null) {
|
||||
positionBroadcastSecs = positionItems.first().value.toInt()
|
||||
}
|
||||
if (FixedUpdateIntervals.fromValue(broadcastSmartMinimumIntervalSecs.toLong()) == null) {
|
||||
broadcastSmartMinimumIntervalSecs = smartBroadcastItems.first().value.toInt()
|
||||
}
|
||||
if (FixedUpdateIntervals.fromValue(gpsUpdateInterval.toLong()) == null) {
|
||||
gpsUpdateInterval = positionItems.first().value.toInt()
|
||||
}
|
||||
var updated = positionConfig
|
||||
if (FixedUpdateIntervals.fromValue((updated.position_broadcast_secs ?: 0).toLong()) == null) {
|
||||
updated = updated.copy(position_broadcast_secs = positionItems.first().value.toInt())
|
||||
}
|
||||
if (FixedUpdateIntervals.fromValue((updated.broadcast_smart_minimum_interval_secs ?: 0).toLong()) == null) {
|
||||
updated =
|
||||
updated.copy(broadcast_smart_minimum_interval_secs = smartBroadcastItems.first().value.toInt())
|
||||
}
|
||||
if (FixedUpdateIntervals.fromValue((updated.gps_update_interval ?: 0).toLong()) == null) {
|
||||
updated = updated.copy(gps_update_interval = positionItems.first().value.toInt())
|
||||
}
|
||||
updated
|
||||
}
|
||||
val formState = rememberConfigState(initialValue = sanitizedPositionConfig)
|
||||
var locationInput by rememberSaveable { mutableStateOf(currentPosition) }
|
||||
|
|
@ -152,17 +150,17 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
additionalDirtyCheck = { locationInput != currentPosition },
|
||||
onDiscard = { locationInput = currentPosition },
|
||||
onSave = {
|
||||
if (formState.value.fixedPosition) {
|
||||
if (formState.value.fixed_position) {
|
||||
if (locationInput != currentPosition) {
|
||||
viewModel.setFixedPosition(locationInput)
|
||||
}
|
||||
} else {
|
||||
if (positionConfig.fixedPosition) {
|
||||
if (positionConfig.fixed_position) {
|
||||
// fixed position changed from enabled to disabled
|
||||
viewModel.removeFixedPosition()
|
||||
}
|
||||
}
|
||||
val config = config { position = it }
|
||||
val config = Config(position = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -175,20 +173,21 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
enabled = state.connected,
|
||||
items = items.map { it to it.toDisplayString() },
|
||||
selectedItem =
|
||||
FixedUpdateIntervals.fromValue(formState.value.positionBroadcastSecs.toLong()) ?: items.first(),
|
||||
FixedUpdateIntervals.fromValue((formState.value.position_broadcast_secs ?: 0).toLong())
|
||||
?: items.first(),
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.copy { positionBroadcastSecs = it.value.toInt() }
|
||||
formState.value = formState.value.copy(position_broadcast_secs = it.value.toInt())
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.smart_position),
|
||||
checked = formState.value.positionBroadcastSmartEnabled,
|
||||
checked = formState.value.position_broadcast_smart_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { positionBroadcastSmartEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(position_broadcast_smart_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
if (formState.value.positionBroadcastSmartEnabled) {
|
||||
if (formState.value.position_broadcast_smart_enabled ?: false) {
|
||||
HorizontalDivider()
|
||||
val smartItems = remember { IntervalConfiguration.SMART_BROADCAST_MINIMUM.allowedIntervals }
|
||||
DropDownPreference(
|
||||
|
|
@ -198,22 +197,23 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
enabled = state.connected,
|
||||
items = smartItems.map { it to it.toDisplayString() },
|
||||
selectedItem =
|
||||
FixedUpdateIntervals.fromValue(formState.value.broadcastSmartMinimumIntervalSecs.toLong())
|
||||
?: smartItems.first(),
|
||||
FixedUpdateIntervals.fromValue(
|
||||
(formState.value.broadcast_smart_minimum_interval_secs ?: 0).toLong(),
|
||||
) ?: smartItems.first(),
|
||||
onItemSelected = {
|
||||
formState.value =
|
||||
formState.value.copy { broadcastSmartMinimumIntervalSecs = it.value.toInt() }
|
||||
formState.value.copy(broadcast_smart_minimum_interval_secs = it.value.toInt())
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.minimum_distance),
|
||||
summary = stringResource(Res.string.config_position_broadcast_smart_minimum_distance_summary),
|
||||
value = formState.value.broadcastSmartMinimumDistance,
|
||||
value = formState.value.broadcast_smart_minimum_distance ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
formState.value = formState.value.copy { broadcastSmartMinimumDistance = it }
|
||||
formState.value = formState.value.copy(broadcast_smart_minimum_distance = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -223,12 +223,12 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
TitledCard(title = stringResource(Res.string.device_gps)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.fixed_position),
|
||||
checked = formState.value.fixedPosition,
|
||||
checked = formState.value.fixed_position ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { fixedPosition = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(fixed_position = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
if (formState.value.fixedPosition) {
|
||||
if (formState.value.fixed_position ?: false) {
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.latitude),
|
||||
|
|
@ -273,12 +273,9 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
DropDownPreference(
|
||||
title = stringResource(Res.string.gps_mode),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
PositionConfig.GpsMode.entries
|
||||
.filter { it != PositionConfig.GpsMode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.gpsMode,
|
||||
onItemSelected = { formState.value = formState.value.copy { gpsMode = it } },
|
||||
items = Config.PositionConfig.GpsMode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.gps_mode ?: Config.PositionConfig.GpsMode.DISABLED,
|
||||
onItemSelected = { formState.value = formState.value.copy(gps_mode = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
val items = remember { IntervalConfiguration.GPS_UPDATE.allowedIntervals }
|
||||
|
|
@ -288,9 +285,10 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
enabled = state.connected,
|
||||
items = items.map { it to it.toDisplayString() },
|
||||
selectedItem =
|
||||
FixedUpdateIntervals.fromValue(formState.value.gpsUpdateInterval.toLong()) ?: items.first(),
|
||||
FixedUpdateIntervals.fromValue((formState.value.gps_update_interval ?: 0).toLong())
|
||||
?: items.first(),
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.copy { gpsUpdateInterval = it.value.toInt() }
|
||||
formState.value = formState.value.copy(gps_update_interval = it.value.toInt())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -301,16 +299,13 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
BitwisePreference(
|
||||
title = stringResource(Res.string.position_flags),
|
||||
summary = stringResource(Res.string.config_position_flags_summary),
|
||||
value = formState.value.positionFlags,
|
||||
value = formState.value.position_flags ?: 0,
|
||||
enabled = state.connected,
|
||||
items =
|
||||
PositionConfig.PositionFlags.entries
|
||||
.filter {
|
||||
it != PositionConfig.PositionFlags.UNSET &&
|
||||
it != PositionConfig.PositionFlags.UNRECOGNIZED
|
||||
}
|
||||
.map { it.number to it.name },
|
||||
onItemSelected = { formState.value = formState.value.copy { positionFlags = it } },
|
||||
Config.PositionConfig.PositionFlags.entries
|
||||
.filter { it != Config.PositionConfig.PositionFlags.UNSET }
|
||||
.map { it.value to it.name },
|
||||
onItemSelected = { formState.value = formState.value.copy(position_flags = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -321,24 +316,24 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
title = stringResource(Res.string.gps_receive_gpio),
|
||||
enabled = state.connected,
|
||||
items = pins,
|
||||
selectedItem = formState.value.rxGpio,
|
||||
onItemSelected = { formState.value = formState.value.copy { rxGpio = it } },
|
||||
selectedItem = formState.value.rx_gpio ?: 0,
|
||||
onItemSelected = { formState.value = formState.value.copy(rx_gpio = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.gps_transmit_gpio),
|
||||
enabled = state.connected,
|
||||
items = pins,
|
||||
selectedItem = formState.value.txGpio,
|
||||
onItemSelected = { formState.value = formState.value.copy { txGpio = it } },
|
||||
selectedItem = formState.value.tx_gpio ?: 0,
|
||||
onItemSelected = { formState.value = formState.value.copy(tx_gpio = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.gps_en_gpio),
|
||||
enabled = state.connected,
|
||||
items = pins,
|
||||
selectedItem = formState.value.gpsEnGpio,
|
||||
onItemSelected = { formState.value = formState.value.copy { gpsEnGpio = it } },
|
||||
selectedItem = formState.value.gps_en_gpio ?: 0,
|
||||
onItemSelected = { formState.value = formState.value.copy(gps_en_gpio = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -46,13 +45,12 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val powerConfig = state.radioConfig.power
|
||||
val powerConfig = state.radioConfig.power ?: Config.PowerConfig()
|
||||
val formState = rememberConfigState(initialValue = powerConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -64,7 +62,7 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { power = it }
|
||||
val config = Config(power = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -73,57 +71,57 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.enable_power_saving_mode),
|
||||
summary = stringResource(Res.string.config_power_is_power_saving_summary),
|
||||
checked = formState.value.isPowerSaving,
|
||||
checked = formState.value.is_power_saving ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { isPowerSaving = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(is_power_saving = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val items = remember { IntervalConfiguration.ALL.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.shutdown_on_power_loss),
|
||||
selectedItem = formState.value.onBatteryShutdownAfterSecs.toLong(),
|
||||
selectedItem = (formState.value.on_battery_shutdown_after_secs ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = items.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.copy { onBatteryShutdownAfterSecs = it.toInt() }
|
||||
formState.value = formState.value.copy(on_battery_shutdown_after_secs = it.toInt())
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.adc_multiplier_override),
|
||||
checked = formState.value.adcMultiplierOverride > 0f,
|
||||
checked = (formState.value.adc_multiplier_override ?: 0f) > 0f,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = {
|
||||
formState.value = formState.value.copy { adcMultiplierOverride = if (it) 1.0f else 0.0f }
|
||||
formState.value = formState.value.copy(adc_multiplier_override = if (it) 1.0f else 0.0f)
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
if (formState.value.adcMultiplierOverride > 0f) {
|
||||
if ((formState.value.adc_multiplier_override ?: 0f) > 0f) {
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.adc_multiplier_override_ratio),
|
||||
value = formState.value.adcMultiplierOverride,
|
||||
value = formState.value.adc_multiplier_override ?: 0f,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { adcMultiplierOverride = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(adc_multiplier_override = it) },
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
val waitBluetoothItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.wait_for_bluetooth_duration_seconds),
|
||||
selectedItem = formState.value.waitBluetoothSecs.toLong(),
|
||||
selectedItem = (formState.value.wait_bluetooth_secs ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = waitBluetoothItems.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { waitBluetoothSecs = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(wait_bluetooth_secs = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
val sdsSecsItems = remember { IntervalConfiguration.ALL.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.super_deep_sleep_duration_seconds),
|
||||
selectedItem = formState.value.sdsSecs.toLong(),
|
||||
onItemSelected = { formState.value = formState.value.copy { sdsSecs = it.toInt() } },
|
||||
selectedItem = (formState.value.sds_secs ?: 0).toLong(),
|
||||
onItemSelected = { formState.value = formState.value.copy(sds_secs = it.toInt()) },
|
||||
enabled = state.connected,
|
||||
items = sdsSecsItems.map { it.value to it.toDisplayString() },
|
||||
)
|
||||
|
|
@ -131,18 +129,18 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
val minWakeItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.minimum_wake_time_seconds),
|
||||
selectedItem = formState.value.minWakeSecs.toLong(),
|
||||
selectedItem = (formState.value.min_wake_secs ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = minWakeItems.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { minWakeSecs = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(min_wake_secs = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.battery_ina_2xx_i2c_address),
|
||||
value = formState.value.deviceBatteryInaAddress,
|
||||
value = formState.value.device_battery_ina_address ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { deviceBatteryInaAddress = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(device_battery_ina_address = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -33,7 +32,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.MessageLite
|
||||
import com.squareup.wire.Message
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.discard_changes
|
||||
|
|
@ -44,7 +43,7 @@ import org.meshtastic.feature.settings.radio.ResponseState
|
|||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun <T : MessageLite> RadioConfigScreenList(
|
||||
fun <T : Message<T, *>> RadioConfigScreenList(
|
||||
title: String,
|
||||
onBack: () -> Unit,
|
||||
responseState: ResponseState<Any>,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.material3.CardDefaults
|
||||
|
|
@ -37,13 +36,12 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val rangeTestConfig = state.moduleConfig.rangeTest
|
||||
val rangeTestConfig = state.moduleConfig.range_test ?: ModuleConfig.RangeTestConfig()
|
||||
val formState = rememberConfigState(initialValue = rangeTestConfig)
|
||||
|
||||
RadioConfigScreenList(
|
||||
|
|
@ -54,7 +52,7 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { rangeTest = it }
|
||||
val config = ModuleConfig(range_test = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -62,26 +60,26 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
TitledCard(title = stringResource(Res.string.range_test_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.range_test_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val rangeItems = remember { IntervalConfiguration.RANGE_TEST_SENDER.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.sender_message_interval_seconds),
|
||||
selectedItem = formState.value.sender.toLong(),
|
||||
selectedItem = (formState.value.sender ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = rangeItems.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { sender = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(sender = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.save_csv_in_storage_esp32_only),
|
||||
checked = formState.value.save,
|
||||
checked = formState.value.save ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { save = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(save = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -36,13 +35,12 @@ import org.meshtastic.core.ui.component.EditListPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val remoteHardwareConfig = state.moduleConfig.remoteHardware
|
||||
val remoteHardwareConfig = state.moduleConfig.remote_hardware ?: ModuleConfig.RemoteHardwareConfig()
|
||||
val formState = rememberConfigState(initialValue = remoteHardwareConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -54,7 +52,7 @@ fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { remoteHardware = it }
|
||||
val config = ModuleConfig(remote_hardware = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -62,33 +60,27 @@ fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()
|
|||
TitledCard(title = stringResource(Res.string.remote_hardware_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.remote_hardware_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.allow_undefined_pin_access),
|
||||
checked = formState.value.allowUndefinedPinAccess,
|
||||
checked = formState.value.allow_undefined_pin_access ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { allowUndefinedPinAccess = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(allow_undefined_pin_access = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditListPreference(
|
||||
title = stringResource(Res.string.available_pins),
|
||||
list = formState.value.availablePinsList,
|
||||
list = formState.value.available_pins,
|
||||
maxCount = 4, // available_pins max_count:4
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValuesChanged = { list ->
|
||||
formState.value =
|
||||
formState.value.copy {
|
||||
availablePins.clear()
|
||||
availablePins.addAll(list)
|
||||
}
|
||||
},
|
||||
onValuesChanged = { list -> formState.value = formState.value.copy(available_pins = list) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import android.app.Activity
|
||||
|
|
@ -42,10 +41,10 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.protobuf.ByteString
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.encodeToString
|
||||
import org.meshtastic.core.model.util.toByteString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.admin_key
|
||||
import org.meshtastic.core.strings.admin_keys
|
||||
|
|
@ -77,9 +76,7 @@ import org.meshtastic.core.ui.component.EditListPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.ConfigProtos.Config.SecurityConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Config
|
||||
import java.security.SecureRandom
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
|
@ -87,15 +84,15 @@ import java.security.SecureRandom
|
|||
fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val node by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val securityConfig = state.radioConfig.security
|
||||
val securityConfig = state.radioConfig.security ?: Config.SecurityConfig()
|
||||
val formState = rememberConfigState(initialValue = securityConfig)
|
||||
|
||||
var publicKey by rememberSaveable { mutableStateOf(formState.value.publicKey) }
|
||||
LaunchedEffect(formState.value.privateKey) {
|
||||
if (formState.value.privateKey != securityConfig.privateKey) {
|
||||
publicKey = "".toByteString()
|
||||
} else if (formState.value.privateKey == securityConfig.privateKey) {
|
||||
publicKey = securityConfig.publicKey
|
||||
var publicKey by rememberSaveable { mutableStateOf(formState.value.public_key) }
|
||||
LaunchedEffect(formState.value.private_key) {
|
||||
if (formState.value.private_key != securityConfig.private_key) {
|
||||
publicKey = ByteString.EMPTY
|
||||
} else if (formState.value.private_key == securityConfig.private_key) {
|
||||
publicKey = securityConfig.public_key
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +109,7 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
onConfirm = {
|
||||
formState.value = it
|
||||
showKeyGenerationDialog = false
|
||||
val config = config { security = formState.value }
|
||||
val config = Config(security = formState.value)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
onDismiss = { showKeyGenerationDialog = false },
|
||||
|
|
@ -133,7 +130,7 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
type = "application/*"
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"${node?.user?.shortName}_keys_${System.currentTimeMillis()}.json",
|
||||
"${node?.user?.short_name}_keys_${System.currentTimeMillis()}.json",
|
||||
)
|
||||
}
|
||||
exportConfigLauncher.launch(intent)
|
||||
|
|
@ -154,7 +151,7 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = config { security = it }
|
||||
val config = Config(security = it)
|
||||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -168,25 +165,25 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
readOnly = true,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChange = {
|
||||
if (it.size() == 32) {
|
||||
formState.value = formState.value.copy { this.publicKey = it }
|
||||
if (it.size == 32) {
|
||||
formState.value = formState.value.copy(public_key = it)
|
||||
}
|
||||
},
|
||||
trailingIcon = { CopyIconButton(valueToCopy = formState.value.publicKey.encodeToString()) },
|
||||
trailingIcon = { CopyIconButton(valueToCopy = formState.value.public_key.encodeToString()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditBase64Preference(
|
||||
title = stringResource(Res.string.private_key),
|
||||
summary = stringResource(Res.string.config_security_private_key),
|
||||
value = formState.value.privateKey,
|
||||
value = formState.value.private_key,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChange = {
|
||||
if (it.size() == 32) {
|
||||
formState.value = formState.value.copy { privateKey = it }
|
||||
if (it.size == 32) {
|
||||
formState.value = formState.value.copy(private_key = it)
|
||||
}
|
||||
},
|
||||
trailingIcon = { CopyIconButton(valueToCopy = formState.value.privateKey.encodeToString()) },
|
||||
trailingIcon = { CopyIconButton(valueToCopy = formState.value.private_key.encodeToString()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
NodeActionButton(
|
||||
|
|
@ -211,17 +208,11 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
EditListPreference(
|
||||
title = stringResource(Res.string.admin_key),
|
||||
summary = stringResource(Res.string.config_security_admin_key),
|
||||
list = formState.value.adminKeyList,
|
||||
list = formState.value.admin_key,
|
||||
maxCount = 3,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValuesChanged = {
|
||||
formState.value =
|
||||
formState.value.copy {
|
||||
adminKey.clear()
|
||||
adminKey.addAll(it)
|
||||
}
|
||||
},
|
||||
onValuesChanged = { formState.value = formState.value.copy(admin_key = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -230,18 +221,18 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.serial_console),
|
||||
summary = stringResource(Res.string.config_security_serial_enabled),
|
||||
checked = formState.value.serialEnabled,
|
||||
checked = formState.value.serial_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { serialEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(serial_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.debug_log_api_enabled),
|
||||
summary = stringResource(Res.string.config_security_debug_log_api_enabled),
|
||||
checked = formState.value.debugLogApiEnabled,
|
||||
checked = formState.value.debug_log_api_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { debugLogApiEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(debug_log_api_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -251,17 +242,17 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.managed_mode),
|
||||
summary = stringResource(Res.string.config_security_is_managed),
|
||||
checked = formState.value.isManaged,
|
||||
enabled = state.connected && formState.value.adminKeyCount > 0,
|
||||
onCheckedChange = { formState.value = formState.value.copy { isManaged = it } },
|
||||
checked = formState.value.is_managed,
|
||||
enabled = state.connected && formState.value.admin_key.isNotEmpty(),
|
||||
onCheckedChange = { formState.value = formState.value.copy(is_managed = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.legacy_admin_channel),
|
||||
checked = formState.value.adminChannelEnabled,
|
||||
checked = formState.value.admin_channel_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { adminChannelEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(admin_channel_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
@ -273,7 +264,7 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
@Composable
|
||||
fun PrivateKeyRegenerateDialog(
|
||||
showKeyGenerationDialog: Boolean,
|
||||
onConfirm: (SecurityConfig) -> Unit,
|
||||
onConfirm: (Config.SecurityConfig) -> Unit,
|
||||
onDismiss: () -> Unit = {},
|
||||
) {
|
||||
if (showKeyGenerationDialog) {
|
||||
|
|
@ -284,22 +275,16 @@ fun PrivateKeyRegenerateDialog(
|
|||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
// Generate a random "f" value
|
||||
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
|
||||
// Adjust the value to make it valid as an "s" value for eval().
|
||||
// According to the specification we need to mask off the 3
|
||||
// right-most bits of f[0], mask off the left-most bit of f[31],
|
||||
// and set the second to left-most bit of f[31].
|
||||
f[0] = (f[0].toInt() and 0xF8).toByte()
|
||||
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
|
||||
val securityInput =
|
||||
SecurityConfig.newBuilder()
|
||||
.apply {
|
||||
clearPrivateKey()
|
||||
clearPublicKey()
|
||||
// Generate a random "f" value
|
||||
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
|
||||
// Adjust the value to make it valid as an "s" value for eval().
|
||||
// According to the specification we need to mask off the 3
|
||||
// right-most bits of f[0], mask off the left-most bit of f[31],
|
||||
// and set the second to left-most bit of f[31].
|
||||
f[0] = (f[0].toInt() and 0xF8).toByte()
|
||||
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
|
||||
privateKey = ByteString.copyFrom(f)
|
||||
}
|
||||
.build()
|
||||
Config.SecurityConfig(private_key = f.toByteString(), public_key = ByteString.EMPTY)
|
||||
onConfirm(securityInput)
|
||||
},
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -40,14 +39,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig.SerialConfig
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val serialConfig = state.moduleConfig.serial
|
||||
val serialConfig = state.moduleConfig.serial ?: ModuleConfig.SerialConfig()
|
||||
val formState = rememberConfigState(initialValue = serialConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -59,7 +56,7 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { serial = it }
|
||||
val config = ModuleConfig(serial = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -67,71 +64,65 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
TitledCard(title = stringResource(Res.string.serial_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.serial_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.echo_enabled),
|
||||
checked = formState.value.echo,
|
||||
checked = formState.value.echo ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { echo = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(echo = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = "RX",
|
||||
value = formState.value.rxd,
|
||||
value = formState.value.rxd ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { rxd = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(rxd = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = "TX",
|
||||
value = formState.value.txd,
|
||||
value = formState.value.txd ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { txd = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(txd = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.serial_baud_rate),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
SerialConfig.Serial_Baud.entries
|
||||
.filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.baud,
|
||||
onItemSelected = { formState.value = formState.value.copy { baud = it } },
|
||||
items = ModuleConfig.SerialConfig.Serial_Baud.entries.map { it to it.name },
|
||||
selectedItem = formState.value.baud ?: ModuleConfig.SerialConfig.Serial_Baud.BAUD_DEFAULT,
|
||||
onItemSelected = { formState.value = formState.value.copy(baud = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.timeout),
|
||||
value = formState.value.timeout,
|
||||
value = formState.value.timeout ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { timeout = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(timeout = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.serial_mode),
|
||||
enabled = state.connected,
|
||||
items =
|
||||
SerialConfig.Serial_Mode.entries
|
||||
.filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.mode,
|
||||
onItemSelected = { formState.value = formState.value.copy { mode = it } },
|
||||
items = ModuleConfig.SerialConfig.Serial_Mode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.mode ?: ModuleConfig.SerialConfig.Serial_Mode.DEFAULT,
|
||||
onItemSelected = { formState.value = formState.value.copy(mode = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.override_console_serial_port),
|
||||
checked = formState.value.overrideConsoleSerialPort,
|
||||
checked = formState.value.override_console_serial_port ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { overrideConsoleSerialPort = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(override_console_serial_port = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -45,7 +44,7 @@ import org.meshtastic.core.strings.send
|
|||
import org.meshtastic.core.strings.shutdown_node_name
|
||||
import org.meshtastic.core.strings.shutdown_warning
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@Composable
|
||||
fun ShutdownConfirmationDialog(
|
||||
|
|
@ -56,7 +55,7 @@ fun ShutdownConfirmationDialog(
|
|||
icon: ImageVector? = Icons.Rounded.Warning,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
val nodeLongName = node?.user?.longName ?: "Unknown Node"
|
||||
val nodeLongName = node?.user?.long_name ?: "Unknown Node"
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {},
|
||||
|
|
@ -104,11 +103,7 @@ private fun ShutdownDialogContent(nodeLongName: String, isShutdown: Boolean) {
|
|||
@Preview
|
||||
@Composable
|
||||
private fun ShutdownConfirmationDialogPreview() {
|
||||
val mockNode =
|
||||
Node(
|
||||
num = 123,
|
||||
user = MeshProtos.User.newBuilder().setLongName("Rooftop Router Node").setShortName("ROOF").build(),
|
||||
)
|
||||
val mockNode = Node(num = 123, user = User(long_name = "Rooftop Router Node", short_name = "ROOF"))
|
||||
|
||||
AppTheme { ShutdownConfirmationDialog(title = "Shutdown?", node = mockNode, onDismiss = {}, onConfirm = {}) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,13 +33,12 @@ import org.meshtastic.core.strings.status_message_config
|
|||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
|
||||
@Composable
|
||||
fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val statusMessageConfig = state.moduleConfig.statusmessage
|
||||
val statusMessageConfig =
|
||||
state.moduleConfig.statusmessage ?: org.meshtastic.proto.ModuleConfig.StatusMessageConfig()
|
||||
val formState = rememberConfigState(initialValue = statusMessageConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { statusmessage = it }
|
||||
val config = org.meshtastic.proto.ModuleConfig(statusmessage = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -59,14 +58,14 @@ fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
TitledCard(title = stringResource(Res.string.status_message_config)) {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.node_status_summary),
|
||||
value = formState.value.nodeStatus,
|
||||
value = formState.value.node_status ?: "",
|
||||
maxSize = 80, // status_message max_size:80
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { nodeStatus = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(node_status = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -39,13 +38,12 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val storeForwardConfig = state.moduleConfig.storeForward
|
||||
val storeForwardConfig = state.moduleConfig.store_forward ?: ModuleConfig.StoreForwardConfig()
|
||||
val formState = rememberConfigState(initialValue = storeForwardConfig)
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -57,7 +55,7 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { storeForward = it }
|
||||
val config = ModuleConfig(store_forward = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -65,49 +63,49 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(),
|
|||
TitledCard(title = stringResource(Res.string.store_forward_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.store_forward_enabled),
|
||||
checked = formState.value.enabled,
|
||||
checked = formState.value.enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.heartbeat),
|
||||
checked = formState.value.heartbeat,
|
||||
checked = formState.value.heartbeat ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { heartbeat = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(heartbeat = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.number_of_records),
|
||||
value = formState.value.records,
|
||||
value = formState.value.records ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { records = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(records = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.history_return_max),
|
||||
value = formState.value.historyReturnMax,
|
||||
value = formState.value.history_return_max ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { historyReturnMax = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(history_return_max = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.history_return_window),
|
||||
value = formState.value.historyReturnWindow,
|
||||
value = formState.value.history_return_window ?: 0,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { historyReturnWindow = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(history_return_window = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.server),
|
||||
checked = formState.value.isServer,
|
||||
checked = formState.value.is_server ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { isServer = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(is_server = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,16 +46,16 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val telemetryConfig = state.moduleConfig.telemetry
|
||||
val telemetryConfig = state.moduleConfig.telemetry ?: ModuleConfig.TelemetryConfig()
|
||||
val formState = rememberConfigState(initialValue = telemetryConfig)
|
||||
|
||||
val capabilities = remember(state.metadata?.firmwareVersion) { Capabilities(state.metadata?.firmwareVersion) }
|
||||
val firmwareVersion = state.metadata?.firmware_version
|
||||
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
|
||||
|
||||
RadioConfigScreenList(
|
||||
title = stringResource(Res.string.telemetry),
|
||||
|
|
@ -65,7 +65,7 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
responseState = state.responseState,
|
||||
onDismissPacketResponse = viewModel::clearPacketResponse,
|
||||
onSave = {
|
||||
val config = moduleConfig { telemetry = it }
|
||||
val config = ModuleConfig(telemetry = it)
|
||||
viewModel.setModuleConfig(config)
|
||||
},
|
||||
) {
|
||||
|
|
@ -75,9 +75,9 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
SwitchPreference(
|
||||
title = stringResource(Res.string.device_telemetry_enabled),
|
||||
summary = stringResource(Res.string.device_telemetry_enabled_summary),
|
||||
checked = formState.value.deviceTelemetryEnabled,
|
||||
checked = formState.value.device_telemetry_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { deviceTelemetryEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(device_telemetry_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -85,86 +85,86 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onB
|
|||
val items = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.device_metrics_update_interval_seconds),
|
||||
selectedItem = formState.value.deviceUpdateInterval.toLong(),
|
||||
selectedItem = (formState.value.device_update_interval ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = items.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { deviceUpdateInterval = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(device_update_interval = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.environment_metrics_module_enabled),
|
||||
checked = formState.value.environmentMeasurementEnabled,
|
||||
checked = formState.value.environment_measurement_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { environmentMeasurementEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(environment_measurement_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val envItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.environment_metrics_update_interval_seconds),
|
||||
selectedItem = formState.value.environmentUpdateInterval.toLong(),
|
||||
selectedItem = (formState.value.environment_update_interval ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = envItems.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.copy { environmentUpdateInterval = it.toInt() }
|
||||
formState.value = formState.value.copy(environment_update_interval = it.toInt())
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.environment_metrics_on_screen_enabled),
|
||||
checked = formState.value.environmentScreenEnabled,
|
||||
checked = formState.value.environment_screen_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { environmentScreenEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(environment_screen_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.environment_metrics_use_fahrenheit),
|
||||
checked = formState.value.environmentDisplayFahrenheit,
|
||||
checked = formState.value.environment_display_fahrenheit ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { environmentDisplayFahrenheit = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(environment_display_fahrenheit = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.air_quality_metrics_module_enabled),
|
||||
checked = formState.value.airQualityEnabled,
|
||||
checked = formState.value.air_quality_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { airQualityEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(air_quality_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val airItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.air_quality_metrics_update_interval_seconds),
|
||||
selectedItem = formState.value.airQualityInterval.toLong(),
|
||||
selectedItem = (formState.value.air_quality_interval ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = airItems.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { airQualityInterval = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(air_quality_interval = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.power_metrics_module_enabled),
|
||||
checked = formState.value.powerMeasurementEnabled,
|
||||
checked = formState.value.power_measurement_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { powerMeasurementEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(power_measurement_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
val powerItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.power_metrics_update_interval_seconds),
|
||||
selectedItem = formState.value.powerUpdateInterval.toLong(),
|
||||
selectedItem = (formState.value.power_update_interval ?: 0).toLong(),
|
||||
enabled = state.connected,
|
||||
items = powerItems.map { it.value to it.toDisplayString() },
|
||||
onItemSelected = { formState.value = formState.value.copy { powerUpdateInterval = it.toInt() } },
|
||||
onItemSelected = { formState.value = formState.value.copy(power_update_interval = it.toInt()) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.power_metrics_on_screen_enabled),
|
||||
checked = formState.value.powerScreenEnabled,
|
||||
checked = formState.value.power_screen_enabled ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { powerScreenEnabled = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(power_screen_enabled = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,17 +47,17 @@ import org.meshtastic.core.ui.component.RegularPreference
|
|||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.proto.copy
|
||||
|
||||
@Composable
|
||||
fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val userConfig = state.userConfig
|
||||
val formState = rememberConfigState(initialValue = userConfig)
|
||||
val capabilities = remember(state.metadata?.firmwareVersion) { Capabilities(state.metadata?.firmwareVersion) }
|
||||
val firmwareVersion = state.metadata?.firmware_version
|
||||
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
|
||||
|
||||
val validLongName = formState.value.longName.isNotBlank()
|
||||
val validShortName = formState.value.shortName.isNotBlank()
|
||||
val validLongName = (formState.value.long_name ?: "").isNotBlank()
|
||||
val validShortName = (formState.value.short_name ?: "").isNotBlank()
|
||||
val validNames = validLongName && validShortName
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -74,37 +74,37 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
TitledCard(title = stringResource(Res.string.user_config)) {
|
||||
RegularPreference(
|
||||
title = stringResource(Res.string.node_id),
|
||||
subtitle = formState.value.id,
|
||||
subtitle = formState.value.id ?: "",
|
||||
onClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.long_name),
|
||||
value = formState.value.longName,
|
||||
value = formState.value.long_name ?: "",
|
||||
maxSize = 39, // long_name max_size:40
|
||||
enabled = state.connected,
|
||||
isError = !validLongName,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { longName = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(long_name = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.short_name),
|
||||
value = formState.value.shortName,
|
||||
value = formState.value.short_name ?: "",
|
||||
maxSize = 4, // short_name max_size:5
|
||||
enabled = state.connected,
|
||||
isError = !validShortName,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { shortName = it } },
|
||||
onValueChanged = { formState.value = formState.value.copy(short_name = it) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
RegularPreference(
|
||||
title = stringResource(Res.string.hardware_model),
|
||||
subtitle = formState.value.hwModel.name,
|
||||
subtitle = formState.value.hw_model?.name ?: "",
|
||||
onClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
|
@ -112,19 +112,19 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
|
|||
title = stringResource(Res.string.unmessageable),
|
||||
summary = stringResource(Res.string.unmonitored_or_infrastructure),
|
||||
checked =
|
||||
formState.value.isUnmessagable ||
|
||||
(formState.value.is_unmessagable ?: false) ||
|
||||
(!capabilities.canToggleUnmessageable && formState.value.role.isUnmessageableRole()),
|
||||
enabled = formState.value.hasIsUnmessagable() || capabilities.canToggleUnmessageable,
|
||||
onCheckedChange = { formState.value = formState.value.copy { isUnmessagable = it } },
|
||||
enabled = formState.value.is_unmessagable != null || capabilities.canToggleUnmessageable,
|
||||
onCheckedChange = { formState.value = formState.value.copy(is_unmessagable = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.licensed_amateur_radio),
|
||||
summary = stringResource(Res.string.licensed_amateur_radio_text),
|
||||
checked = formState.value.isLicensed,
|
||||
checked = formState.value.is_licensed ?: false,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { isLicensed = it } },
|
||||
onCheckedChange = { formState.value = formState.value.copy(is_licensed = it) },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue