mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(ui): compose resources, domain layer (#4628)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
96adc70401
commit
2676a51647
322 changed files with 3031 additions and 2790 deletions
|
|
@ -32,16 +32,16 @@ import org.meshtastic.core.database.entity.asDeviceVersion
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.administration
|
||||
import org.meshtastic.core.resources.firmware
|
||||
import org.meshtastic.core.resources.firmware_edition
|
||||
import org.meshtastic.core.resources.installed_firmware_version
|
||||
import org.meshtastic.core.resources.latest_alpha_firmware
|
||||
import org.meshtastic.core.resources.latest_stable_firmware
|
||||
import org.meshtastic.core.resources.remote_admin
|
||||
import org.meshtastic.core.resources.request_metadata
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.administration
|
||||
import org.meshtastic.core.strings.firmware
|
||||
import org.meshtastic.core.strings.firmware_edition
|
||||
import org.meshtastic.core.strings.installed_firmware_version
|
||||
import org.meshtastic.core.strings.latest_alpha_firmware
|
||||
import org.meshtastic.core.strings.latest_stable_firmware
|
||||
import org.meshtastic.core.strings.remote_admin
|
||||
import org.meshtastic.core.strings.request_metadata
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
|
|
|
|||
|
|
@ -56,20 +56,20 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.compass_bearing
|
||||
import org.meshtastic.core.strings.compass_bearing_na
|
||||
import org.meshtastic.core.strings.compass_distance
|
||||
import org.meshtastic.core.strings.compass_location_disabled
|
||||
import org.meshtastic.core.strings.compass_no_location_fix
|
||||
import org.meshtastic.core.strings.compass_no_location_permission
|
||||
import org.meshtastic.core.strings.compass_no_magnetometer
|
||||
import org.meshtastic.core.strings.compass_title
|
||||
import org.meshtastic.core.strings.compass_uncertainty
|
||||
import org.meshtastic.core.strings.compass_uncertainty_unknown
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.strings.exchange_position
|
||||
import org.meshtastic.core.strings.last_position_update
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.compass_bearing
|
||||
import org.meshtastic.core.resources.compass_bearing_na
|
||||
import org.meshtastic.core.resources.compass_distance
|
||||
import org.meshtastic.core.resources.compass_location_disabled
|
||||
import org.meshtastic.core.resources.compass_no_location_fix
|
||||
import org.meshtastic.core.resources.compass_no_location_permission
|
||||
import org.meshtastic.core.resources.compass_no_magnetometer
|
||||
import org.meshtastic.core.resources.compass_title
|
||||
import org.meshtastic.core.resources.compass_uncertainty
|
||||
import org.meshtastic.core.resources.compass_uncertainty_unknown
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.exchange_position
|
||||
import org.meshtastic.core.resources.last_position_update
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.feature.node.compass.CompassUiState
|
||||
import org.meshtastic.feature.node.compass.CompassWarning
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.actions
|
||||
import org.meshtastic.core.strings.direct_message
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.share_contact
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.actions
|
||||
import org.meshtastic.core.resources.direct_message
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.mute_notifications
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.share_contact
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.SwitchListItem
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
|
|
|
|||
|
|
@ -36,21 +36,20 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.device
|
||||
import org.meshtastic.core.strings.hardware
|
||||
import org.meshtastic.core.strings.supported
|
||||
import org.meshtastic.core.strings.supported_by_community
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.device
|
||||
import org.meshtastic.core.resources.hardware
|
||||
import org.meshtastic.core.resources.ic_unverified
|
||||
import org.meshtastic.core.resources.img_hw_unknown
|
||||
import org.meshtastic.core.resources.supported
|
||||
import org.meshtastic.core.resources.supported_by_community
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
|
|
@ -119,7 +118,7 @@ private fun SupportStatusItem(isSupported: Boolean) {
|
|||
if (isSupported) {
|
||||
Icons.TwoTone.Verified
|
||||
} else {
|
||||
ImageVector.vectorResource(org.meshtastic.feature.node.R.drawable.unverified)
|
||||
org.jetbrains.compose.resources.vectorResource(org.meshtastic.core.resources.Res.drawable.ic_unverified)
|
||||
},
|
||||
leadingIconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
|
||||
trailingIcon = null,
|
||||
|
|
@ -134,9 +133,12 @@ private fun DeviceHardwareImage(deviceHardware: DeviceHardware, modifier: Modifi
|
|||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).build(),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = deviceHardware.displayName,
|
||||
placeholder = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
error = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
fallback = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
placeholder =
|
||||
org.jetbrains.compose.resources.painterResource(org.meshtastic.core.resources.Res.drawable.img_hw_unknown),
|
||||
error =
|
||||
org.jetbrains.compose.resources.painterResource(org.meshtastic.core.resources.Res.drawable.img_hw_unknown),
|
||||
fallback =
|
||||
org.jetbrains.compose.resources.painterResource(org.meshtastic.core.resources.Res.drawable.img_hw_unknown),
|
||||
modifier = modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.distance
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.distance
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.altitude
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.altitude
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.ui.icon.Elevation
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
|
|||
|
|
@ -40,23 +40,27 @@ import org.meshtastic.core.model.util.UnitConversions
|
|||
import org.meshtastic.core.model.util.UnitConversions.toTempString
|
||||
import org.meshtastic.core.model.util.toSmallDistanceString
|
||||
import org.meshtastic.core.model.util.toSpeedString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.dew_point
|
||||
import org.meshtastic.core.strings.distance
|
||||
import org.meshtastic.core.strings.gas_resistance
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.lux
|
||||
import org.meshtastic.core.strings.pressure
|
||||
import org.meshtastic.core.strings.radiation
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.strings.uv_lux
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.strings.weight
|
||||
import org.meshtastic.core.strings.wind
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.dew_point
|
||||
import org.meshtastic.core.resources.distance
|
||||
import org.meshtastic.core.resources.gas_resistance
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.ic_dew_point
|
||||
import org.meshtastic.core.resources.ic_radioactive
|
||||
import org.meshtastic.core.resources.ic_soil_moisture
|
||||
import org.meshtastic.core.resources.ic_soil_temperature
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.pressure
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
import org.meshtastic.core.resources.uv_lux
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.resources.weight
|
||||
import org.meshtastic.core.resources.wind
|
||||
import org.meshtastic.feature.node.model.DrawableMetricInfo
|
||||
import org.meshtastic.feature.node.model.VectorMetricInfo
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -136,7 +140,7 @@ internal fun EnvironmentMetrics(
|
|||
DrawableMetricInfo(
|
||||
Res.string.dew_point,
|
||||
dewPoint.toTempString(isFahrenheit),
|
||||
org.meshtastic.feature.node.R.drawable.ic_outlined_dew_point_24,
|
||||
Res.drawable.ic_dew_point,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -147,7 +151,7 @@ internal fun EnvironmentMetrics(
|
|||
DrawableMetricInfo(
|
||||
Res.string.soil_temperature,
|
||||
st.toTempString(isFahrenheit),
|
||||
org.meshtastic.feature.node.R.drawable.soil_temperature,
|
||||
Res.drawable.ic_soil_temperature,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -157,16 +161,16 @@ internal fun EnvironmentMetrics(
|
|||
DrawableMetricInfo(
|
||||
Res.string.soil_moisture,
|
||||
"%d%%".format(sm),
|
||||
org.meshtastic.feature.node.R.drawable.soil_moisture,
|
||||
Res.drawable.ic_soil_moisture,
|
||||
),
|
||||
)
|
||||
}
|
||||
radiation?.let { r ->
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
Res.string.radiation,
|
||||
"%.1f µR/h".format(r),
|
||||
org.meshtastic.feature.node.R.drawable.ic_filled_radioactive_24,
|
||||
label = Res.string.radiation,
|
||||
value = "%.1f µR/h".format(r),
|
||||
icon = Res.drawable.ic_radioactive,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ import com.mikepenz.markdown.m3.Markdown
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.download
|
||||
import org.meshtastic.core.strings.error_no_app_to_handle_link
|
||||
import org.meshtastic.core.strings.view_release
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.download
|
||||
import org.meshtastic.core.resources.error_no_app_to_handle_link
|
||||
import org.meshtastic.core.resources.view_release
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.hops_away
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.hops_away
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -42,15 +41,16 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.thenIf
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -58,9 +58,9 @@ import org.meshtastic.core.ui.util.thenIf
|
|||
fun InfoCard(
|
||||
text: String,
|
||||
value: String,
|
||||
icon: ImageVector? = null,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector? = null,
|
||||
iconRes: DrawableResource? = null,
|
||||
rotateIcon: Float = 0f,
|
||||
) {
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
|
|
@ -120,6 +120,6 @@ fun InfoCard(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun DrawableInfoCard(@DrawableRes iconRes: Int, text: String, value: String, rotateIcon: Float = 0f) {
|
||||
internal fun DrawableInfoCard(iconRes: DrawableResource, text: String, value: String, rotateIcon: Float = 0f) {
|
||||
InfoCard(iconRes = iconRes, text = text, value = value, rotateIcon = rotateIcon)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.R
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ic_antenna
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ fun LastHeardInfo(
|
|||
) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
|
||||
icon = vectorResource(Res.drawable.ic_antenna),
|
||||
contentDescription = stringResource(Res.string.node_sort_last_heard),
|
||||
label = if (showLabel) stringResource(Res.string.node_sort_last_heard) else null,
|
||||
text = formatAgo(lastHeard),
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.model.util.GPSFormat
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.strings.last_position_update
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.last_position_update
|
||||
import org.meshtastic.core.ui.component.BasicListItem
|
||||
import org.meshtastic.core.ui.component.icon
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ import androidx.compose.ui.unit.dp
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
|
||||
@Composable
|
||||
internal fun SectionCard(
|
||||
|
|
|
|||
|
|
@ -58,26 +58,26 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.strings.details
|
||||
import org.meshtastic.core.strings.encryption_error
|
||||
import org.meshtastic.core.strings.encryption_error_text
|
||||
import org.meshtastic.core.strings.error
|
||||
import org.meshtastic.core.strings.hops_away
|
||||
import org.meshtastic.core.strings.node_id
|
||||
import org.meshtastic.core.strings.node_number
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.strings.public_key
|
||||
import org.meshtastic.core.strings.role
|
||||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.short_name
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.strings.status_message
|
||||
import org.meshtastic.core.strings.supported
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.user_id
|
||||
import org.meshtastic.core.strings.via_mqtt
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.details
|
||||
import org.meshtastic.core.resources.encryption_error
|
||||
import org.meshtastic.core.resources.encryption_error_text
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.hops_away
|
||||
import org.meshtastic.core.resources.node_id
|
||||
import org.meshtastic.core.resources.node_number
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.resources.public_key
|
||||
import org.meshtastic.core.resources.role
|
||||
import org.meshtastic.core.resources.rssi
|
||||
import org.meshtastic.core.resources.short_name
|
||||
import org.meshtastic.core.resources.snr
|
||||
import org.meshtastic.core.resources.status_message
|
||||
import org.meshtastic.core.resources.supported
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.user_id
|
||||
import org.meshtastic.core.resources.via_mqtt
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.icon.ArrowCircleUp
|
||||
import org.meshtastic.core.ui.icon.ChannelUtilization
|
||||
|
|
|
|||
|
|
@ -61,18 +61,18 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.desc_node_filter_clear
|
||||
import org.meshtastic.core.strings.node_filter_exclude_infrastructure
|
||||
import org.meshtastic.core.strings.node_filter_ignored
|
||||
import org.meshtastic.core.strings.node_filter_include_unknown
|
||||
import org.meshtastic.core.strings.node_filter_only_direct
|
||||
import org.meshtastic.core.strings.node_filter_only_online
|
||||
import org.meshtastic.core.strings.node_filter_placeholder
|
||||
import org.meshtastic.core.strings.node_filter_show_ignored
|
||||
import org.meshtastic.core.strings.node_filter_title
|
||||
import org.meshtastic.core.strings.node_sort_button
|
||||
import org.meshtastic.core.strings.node_sort_title
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.desc_node_filter_clear
|
||||
import org.meshtastic.core.resources.node_filter_exclude_infrastructure
|
||||
import org.meshtastic.core.resources.node_filter_ignored
|
||||
import org.meshtastic.core.resources.node_filter_include_unknown
|
||||
import org.meshtastic.core.resources.node_filter_only_direct
|
||||
import org.meshtastic.core.resources.node_filter_only_online
|
||||
import org.meshtastic.core.resources.node_filter_placeholder
|
||||
import org.meshtastic.core.resources.node_filter_show_ignored
|
||||
import org.meshtastic.core.resources.node_filter_title
|
||||
import org.meshtastic.core.resources.node_sort_button
|
||||
import org.meshtastic.core.resources.node_sort_title
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
|||
|
|
@ -55,15 +55,15 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.air_utilization
|
||||
import org.meshtastic.core.strings.channel_utilization
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.ui.component.AirQualityInfo
|
||||
import org.meshtastic.core.ui.component.ChannelInfo
|
||||
import org.meshtastic.core.ui.component.DistanceInfo
|
||||
|
|
|
|||
|
|
@ -37,16 +37,16 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.connected
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.unmessageable
|
||||
import org.meshtastic.core.resources.unmonitored_or_infrastructure
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.connected
|
||||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.device_sleeping
|
||||
import org.meshtastic.core.strings.disconnected
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.mute_always
|
||||
import org.meshtastic.core.strings.unmessageable
|
||||
import org.meshtastic.core.strings.unmonitored_or_infrastructure
|
||||
import org.meshtastic.core.ui.icon.CloudDone
|
||||
import org.meshtastic.core.ui.icon.CloudOffTwoTone
|
||||
import org.meshtastic.core.ui.icon.CloudSync
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_a_note
|
||||
import org.meshtastic.core.strings.notes
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_a_note
|
||||
import org.meshtastic.core.resources.notes
|
||||
import org.meshtastic.core.resources.save
|
||||
|
||||
@Composable
|
||||
fun NotesSection(node: Node, onSaveNotes: (Int, String) -> Unit, modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ import androidx.compose.ui.unit.dp
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.exchange_position
|
||||
import org.meshtastic.core.strings.open_compass
|
||||
import org.meshtastic.core.strings.position
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.exchange_position
|
||||
import org.meshtastic.core.resources.open_compass
|
||||
import org.meshtastic.core.resources.position
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_1
|
||||
import org.meshtastic.core.strings.channel_2
|
||||
import org.meshtastic.core.strings.channel_3
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_1
|
||||
import org.meshtastic.core.resources.channel_2
|
||||
import org.meshtastic.core.resources.channel_3
|
||||
import org.meshtastic.feature.node.model.VectorMetricInfo
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.sats
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ import org.jetbrains.compose.resources.StringResource
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.logs
|
||||
import org.meshtastic.core.strings.request_air_quality_metrics
|
||||
import org.meshtastic.core.strings.request_telemetry
|
||||
import org.meshtastic.core.strings.telemetry
|
||||
import org.meshtastic.core.strings.userinfo
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.logs
|
||||
import org.meshtastic.core.resources.request_air_quality_metrics
|
||||
import org.meshtastic.core.resources.request_telemetry
|
||||
import org.meshtastic.core.resources.telemetry
|
||||
import org.meshtastic.core.resources.userinfo
|
||||
import org.meshtastic.core.ui.icon.AirQuality
|
||||
import org.meshtastic.core.ui.icon.LineAxis
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
|
|||
|
|
@ -33,17 +33,17 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.env_metrics_log
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.node_id
|
||||
import org.meshtastic.core.strings.pax
|
||||
import org.meshtastic.core.strings.pax_metrics_log
|
||||
import org.meshtastic.core.strings.role
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.node_id
|
||||
import org.meshtastic.core.resources.pax
|
||||
import org.meshtastic.core.resources.pax_metrics_log
|
||||
import org.meshtastic.core.resources.role
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
|
||||
@Composable
|
||||
fun TemperatureInfo(
|
||||
|
|
|
|||
|
|
@ -61,10 +61,9 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.details
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.loading
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.details
|
||||
import org.meshtastic.core.resources.loading
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.SharedContactDialog
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
|
|
@ -100,20 +99,19 @@ fun NodeDetailScreen(
|
|||
onNavigate: (Route) -> Unit = {},
|
||||
onNavigateUp: () -> Unit = {},
|
||||
) {
|
||||
viewModel.start(nodeId)
|
||||
LaunchedEffect(nodeId) { viewModel.start(nodeId) }
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
if (effect is NodeRequestEffect.ShowFeedback) {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
NodeDetailScaffold(
|
||||
modifier = modifier,
|
||||
uiState = uiState,
|
||||
|
|
@ -149,8 +147,8 @@ private fun NodeDetailScaffold(
|
|||
modifier = modifier,
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = getString(Res.string.details),
|
||||
subtitle = node?.user?.long_name ?: "",
|
||||
title = stringResource(Res.string.details),
|
||||
subtitle = uiState.nodeName.asString(),
|
||||
ourNode = uiState.ourNode,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.toRoute
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -31,44 +30,26 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
|
||||
import org.meshtastic.core.model.util.isDirectSignal
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.fallback_node_name
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
|
||||
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* UI state for the Node Details screen.
|
||||
*
|
||||
* @property node The node being viewed, or null if loading.
|
||||
* @property nodeName The display name for the node, resolved in the UI.
|
||||
* @property ourNode Information about the locally connected node.
|
||||
* @property metricsState Aggregated sensor and signal metrics.
|
||||
* @property environmentState Standardized environmental sensor data.
|
||||
|
|
@ -76,8 +57,10 @@ import javax.inject.Inject
|
|||
* @property lastTracerouteTime Timestamp of the last successful traceroute request.
|
||||
* @property lastRequestNeighborsTime Timestamp of the last successful neighbor info request.
|
||||
*/
|
||||
@androidx.compose.runtime.Stable
|
||||
data class NodeDetailUiState(
|
||||
val node: Node? = null,
|
||||
val nodeName: UiText = UiText.DynamicString(""),
|
||||
val ourNode: Node? = null,
|
||||
val metricsState: MetricsState = MetricsState.Empty,
|
||||
val environmentState: EnvironmentMetricsState = EnvironmentMetricsState(),
|
||||
|
|
@ -93,17 +76,12 @@ data class NodeDetailUiState(
|
|||
@HiltViewModel
|
||||
class NodeDetailViewModel
|
||||
@Inject
|
||||
@Suppress("LongParameterList")
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val nodeManagementActions: NodeManagementActions,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
private val nodeIdFromRoute: Int? =
|
||||
|
|
@ -120,166 +98,10 @@ constructor(
|
|||
activeNodeId
|
||||
.flatMapLatest { nodeId ->
|
||||
if (nodeId == null) return@flatMapLatest flowOf(NodeDetailUiState())
|
||||
buildUiStateFlow(nodeId)
|
||||
getNodeDetailsUseCase(nodeId)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NodeDetailUiState())
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun buildUiStateFlow(nodeId: Int): Flow<NodeDetailUiState> {
|
||||
val nodeFlow =
|
||||
nodeRepository.nodeDBbyNum
|
||||
.map { it[nodeId] ?: Node.createFallback(nodeId, getString(Res.string.fallback_node_name)) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
// 1. Logs & Metrics Data (fetches telemetry, packets, paxcount, and response history)
|
||||
val metricsLogsFlow =
|
||||
combine(
|
||||
meshLogRepository.getTelemetryFrom(nodeId),
|
||||
meshLogRepository.getMeshPacketsFrom(nodeId),
|
||||
meshLogRepository.getMeshPacketsFrom(nodeId, PortNum.POSITION_APP.value),
|
||||
meshLogRepository.getLogsFrom(nodeId, PortNum.PAXCOUNTER_APP.value),
|
||||
meshLogRepository.getLogsFrom(nodeId, PortNum.TRACEROUTE_APP.value),
|
||||
meshLogRepository.getLogsFrom(nodeId, PortNum.NEIGHBORINFO_APP.value),
|
||||
) { args: Array<List<Any?>> ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
LogsGroup(
|
||||
telemetry = args[0] as List<Telemetry>,
|
||||
packets = args[1] as List<MeshPacket>,
|
||||
posPackets = args[2] as List<MeshPacket>,
|
||||
pax = args[3] as List<MeshLog>,
|
||||
trRes = args[4] as List<MeshLog>,
|
||||
niRes = args[5] as List<MeshLog>,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Identity & Config (local device info and radio profile)
|
||||
val identityFlow =
|
||||
combine(nodeRepository.ourNodeInfo, nodeRepository.myNodeInfo, radioConfigRepository.deviceProfileFlow) {
|
||||
ourNode,
|
||||
myInfo,
|
||||
profile,
|
||||
->
|
||||
IdentityGroup(ourNode, myInfo?.toMyNodeInfo(), profile)
|
||||
}
|
||||
|
||||
// 3. Metadata & Request Timestamps (firmware versions and last request times)
|
||||
val metadataFlow =
|
||||
combine(
|
||||
meshLogRepository.getMyNodeInfo().map { it?.firmware_edition }.distinctUntilChanged(),
|
||||
firmwareReleaseRepository.stableRelease,
|
||||
firmwareReleaseRepository.alphaRelease,
|
||||
nodeRequestActions.lastTracerouteTimes.map { it[nodeId] },
|
||||
nodeRequestActions.lastRequestNeighborTimes.map { it[nodeId] },
|
||||
) { args: Array<Any?> ->
|
||||
MetadataGroup(
|
||||
edition = args[0] as? FirmwareEdition,
|
||||
stable = args[1] as? FirmwareRelease,
|
||||
alpha = args[2] as? FirmwareRelease,
|
||||
trTime = args[3] as? Long,
|
||||
niTime = args[4] as? Long,
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Requests History (tracking traceroute and neighbor info requests sent from this device)
|
||||
val requestsFlow =
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.TRACEROUTE_APP),
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.NEIGHBORINFO_APP),
|
||||
) { trReqs, niReqs ->
|
||||
trReqs to niReqs
|
||||
}
|
||||
|
||||
// Assemble final UI state
|
||||
return combine(nodeFlow, metricsLogsFlow, identityFlow, metadataFlow, requestsFlow) {
|
||||
node,
|
||||
logs,
|
||||
identity,
|
||||
metadata,
|
||||
requests,
|
||||
->
|
||||
val (trReqs, niReqs) = requests
|
||||
val isLocal = node.num == identity.ourNode?.num
|
||||
val pioEnv = if (isLocal) identity.myInfo?.pioEnv else null
|
||||
val hw = deviceHardwareRepository.getDeviceHardwareByModel(node.user.hw_model.value, pioEnv).getOrNull()
|
||||
|
||||
val moduleConfig = identity.profile.module_config
|
||||
val displayUnits = identity.profile.config?.display?.units ?: Config.DisplayConfig.DisplayUnits.METRIC
|
||||
|
||||
val metricsState =
|
||||
MetricsState(
|
||||
node = node,
|
||||
isLocal = isLocal,
|
||||
deviceHardware = hw,
|
||||
reportedTarget = pioEnv,
|
||||
isManaged = identity.profile.config?.security?.is_managed ?: false,
|
||||
isFahrenheit =
|
||||
moduleConfig?.telemetry?.environment_display_fahrenheit == true ||
|
||||
(displayUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL),
|
||||
displayUnits = displayUnits,
|
||||
deviceMetrics = logs.telemetry.filter { it.device_metrics != null },
|
||||
powerMetrics = logs.telemetry.filter { it.power_metrics != null },
|
||||
hostMetrics = logs.telemetry.filter { it.host_metrics != null },
|
||||
signalMetrics = logs.packets.filter { it.isDirectSignal() },
|
||||
positionLogs = logs.posPackets.mapNotNull { it.toPosition() },
|
||||
paxMetrics = logs.pax,
|
||||
tracerouteRequests = trReqs,
|
||||
tracerouteResults = logs.trRes,
|
||||
neighborInfoRequests = niReqs,
|
||||
neighborInfoResults = logs.niRes,
|
||||
firmwareEdition = metadata.edition,
|
||||
latestStableFirmware = metadata.stable ?: FirmwareRelease(),
|
||||
latestAlphaFirmware = metadata.alpha ?: FirmwareRelease(),
|
||||
)
|
||||
|
||||
val environmentState =
|
||||
EnvironmentMetricsState(environmentMetrics = logs.telemetry.filter { it.hasValidEnvironmentMetrics() })
|
||||
|
||||
val availableLogs = buildSet {
|
||||
if (metricsState.hasDeviceMetrics()) add(LogsType.DEVICE)
|
||||
if (metricsState.hasPositionLogs()) {
|
||||
add(LogsType.NODE_MAP)
|
||||
add(LogsType.POSITIONS)
|
||||
}
|
||||
if (environmentState.hasEnvironmentMetrics()) add(LogsType.ENVIRONMENT)
|
||||
if (metricsState.hasSignalMetrics()) add(LogsType.SIGNAL)
|
||||
if (metricsState.hasPowerMetrics()) add(LogsType.POWER)
|
||||
if (metricsState.hasTracerouteLogs()) add(LogsType.TRACEROUTE)
|
||||
if (metricsState.hasNeighborInfoLogs()) add(LogsType.NEIGHBOR_INFO)
|
||||
if (metricsState.hasHostMetrics()) add(LogsType.HOST)
|
||||
if (metricsState.hasPaxMetrics()) add(LogsType.PAX)
|
||||
}
|
||||
|
||||
NodeDetailUiState(
|
||||
node = node,
|
||||
ourNode = identity.ourNode,
|
||||
metricsState = metricsState,
|
||||
environmentState = environmentState,
|
||||
availableLogs = availableLogs,
|
||||
lastTracerouteTime = metadata.trTime,
|
||||
lastRequestNeighborsTime = metadata.niTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class LogsGroup(
|
||||
val telemetry: List<Telemetry>,
|
||||
val packets: List<MeshPacket>,
|
||||
val posPackets: List<MeshPacket>,
|
||||
val pax: List<MeshLog>,
|
||||
val trRes: List<MeshLog>,
|
||||
val niRes: List<MeshLog>,
|
||||
)
|
||||
|
||||
private data class IdentityGroup(val ourNode: Node?, val myInfo: MyNodeInfo?, val profile: DeviceProfile)
|
||||
|
||||
private data class MetadataGroup(
|
||||
val edition: FirmwareEdition?,
|
||||
val stable: FirmwareRelease?,
|
||||
val alpha: FirmwareRelease?,
|
||||
val trTime: Long?,
|
||||
val niTime: Long?,
|
||||
)
|
||||
|
||||
val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects
|
||||
|
||||
fun start(nodeId: Int) {
|
||||
|
|
|
|||
|
|
@ -24,21 +24,21 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.favorite_add
|
||||
import org.meshtastic.core.resources.favorite_remove
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.ignore_add
|
||||
import org.meshtastic.core.resources.ignore_remove
|
||||
import org.meshtastic.core.resources.mute_add
|
||||
import org.meshtastic.core.resources.mute_notifications
|
||||
import org.meshtastic.core.resources.mute_remove
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.remove_node_text
|
||||
import org.meshtastic.core.resources.unmute
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.favorite_add
|
||||
import org.meshtastic.core.strings.favorite_remove
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.ignore_add
|
||||
import org.meshtastic.core.strings.ignore_remove
|
||||
import org.meshtastic.core.strings.mute_add
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.mute_remove
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.remove_node_text
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
|
|||
|
|
@ -27,29 +27,29 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.resources.neighbor_info
|
||||
import org.meshtastic.core.resources.position
|
||||
import org.meshtastic.core.resources.request_air_quality_metrics
|
||||
import org.meshtastic.core.resources.request_device_metrics
|
||||
import org.meshtastic.core.resources.request_environment_metrics
|
||||
import org.meshtastic.core.resources.request_host_metrics
|
||||
import org.meshtastic.core.resources.request_pax_metrics
|
||||
import org.meshtastic.core.resources.request_power_metrics
|
||||
import org.meshtastic.core.resources.requesting_from
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.user_info
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.position
|
||||
import org.meshtastic.core.strings.request_air_quality_metrics
|
||||
import org.meshtastic.core.strings.request_device_metrics
|
||||
import org.meshtastic.core.strings.request_environment_metrics
|
||||
import org.meshtastic.core.strings.request_host_metrics
|
||||
import org.meshtastic.core.strings.request_pax_metrics
|
||||
import org.meshtastic.core.strings.request_power_metrics
|
||||
import org.meshtastic.core.strings.requesting_from
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.user_info
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed class NodeRequestEffect {
|
||||
data class ShowFeedback(val resource: StringResource, val args: List<Any> = emptyList()) : NodeRequestEffect()
|
||||
data class ShowFeedback(val text: UiText) : NodeRequestEffect()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
|
|
@ -70,7 +70,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
try {
|
||||
serviceRepository.meshService?.requestUserInfo(destNum)
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.user_info, longName)),
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request NodeInfo error: ${ex.message}" }
|
||||
|
|
@ -87,8 +89,7 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
Res.string.requesting_from,
|
||||
listOf(Res.string.neighbor_info, longName),
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
|
|
@ -108,7 +109,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
try {
|
||||
serviceRepository.meshService?.requestPosition(destNum, position)
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.position, longName)),
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.position, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request position error: ${ex.message}" }
|
||||
|
|
@ -134,7 +137,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
TelemetryType.PAX -> Res.string.request_pax_metrics
|
||||
}
|
||||
|
||||
_effects.emit(NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(typeRes, longName)))
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request telemetry error: ${ex.message}" }
|
||||
}
|
||||
|
|
@ -149,7 +154,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
|
||||
_lastTracerouteTimes.update { it + (destNum to nowMillis) }
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.traceroute, longName)),
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request traceroute error: ${ex.message}" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.node.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.feature.node.list.NodeFilterState
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.proto.Config
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetFilteredNodesUseCase @Inject constructor(private val nodeRepository: NodeRepository) {
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
operator fun invoke(filter: NodeFilterState, sort: NodeSortOption): Flow<List<Node>> = nodeRepository
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter.filterText,
|
||||
includeUnknown = filter.includeUnknown,
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list ->
|
||||
list
|
||||
.filter { node -> node.isIgnored == filter.showIgnored }
|
||||
.filter { node ->
|
||||
if (filter.excludeInfrastructure) {
|
||||
val role = node.user.role
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val infrastructureRoles =
|
||||
listOf(
|
||||
Config.DeviceConfig.Role.ROUTER,
|
||||
Config.DeviceConfig.Role.REPEATER,
|
||||
Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
Config.DeviceConfig.Role.CLIENT_BASE,
|
||||
)
|
||||
role !in infrastructureRoles && !node.isEffectivelyUnmessageable
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.node.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
|
||||
import org.meshtastic.core.model.util.isDirectSignal
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.resources.fallback_node_name
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.feature.node.detail.NodeDetailUiState
|
||||
import org.meshtastic.feature.node.detail.NodeRequestActions
|
||||
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetNodeDetailsUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
operator fun invoke(nodeId: Int): Flow<NodeDetailUiState> =
|
||||
nodeRepository.effectiveLogNodeId(nodeId).flatMapLatest { effectiveNodeId ->
|
||||
buildFlow(nodeId, effectiveNodeId)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun buildFlow(nodeId: Int, effectiveNodeId: Int): Flow<NodeDetailUiState> {
|
||||
val nodeFlow =
|
||||
nodeRepository.nodeDBbyNum.map { it[nodeId] ?: Node.createFallback(nodeId, "") }.distinctUntilChanged()
|
||||
|
||||
// 1. Logs & Metrics Data
|
||||
val metricsLogsFlow =
|
||||
combine(
|
||||
meshLogRepository.getTelemetryFrom(effectiveNodeId).onStart { emit(emptyList()) },
|
||||
meshLogRepository.getMeshPacketsFrom(effectiveNodeId).onStart { emit(emptyList()) },
|
||||
meshLogRepository.getMeshPacketsFrom(effectiveNodeId, PortNum.POSITION_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
meshLogRepository.getLogsFrom(effectiveNodeId, PortNum.PAXCOUNTER_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
meshLogRepository.getLogsFrom(effectiveNodeId, PortNum.TRACEROUTE_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
meshLogRepository.getLogsFrom(effectiveNodeId, PortNum.NEIGHBORINFO_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
) { args: Array<List<Any?>> ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
LogsGroup(
|
||||
telemetry = args[0] as List<Telemetry>,
|
||||
packets = args[1] as List<MeshPacket>,
|
||||
posPackets = args[2] as List<MeshPacket>,
|
||||
pax = args[3] as List<MeshLog>,
|
||||
trRes = args[4] as List<MeshLog>,
|
||||
niRes = args[5] as List<MeshLog>,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Identity & Config
|
||||
val identityFlow =
|
||||
combine(
|
||||
nodeRepository.ourNodeInfo,
|
||||
nodeRepository.myNodeInfo,
|
||||
radioConfigRepository.deviceProfileFlow.onStart { emit(DeviceProfile()) },
|
||||
) { ourNode, myInfo, profile ->
|
||||
IdentityGroup(ourNode, myInfo?.toMyNodeInfo(), profile)
|
||||
}
|
||||
|
||||
// 3. Metadata & Request Timestamps
|
||||
val metadataFlow =
|
||||
combine(
|
||||
meshLogRepository
|
||||
.getMyNodeInfo()
|
||||
.map { it?.firmware_edition }
|
||||
.distinctUntilChanged()
|
||||
.onStart { emit(null) },
|
||||
firmwareReleaseRepository.stableRelease,
|
||||
firmwareReleaseRepository.alphaRelease,
|
||||
nodeRequestActions.lastTracerouteTimes.map { it[nodeId] },
|
||||
nodeRequestActions.lastRequestNeighborTimes.map { it[nodeId] },
|
||||
) { edition, stable, alpha, trTime, niTime ->
|
||||
MetadataGroup(edition = edition, stable = stable, alpha = alpha, trTime = trTime, niTime = niTime)
|
||||
}
|
||||
|
||||
// 4. Requests History (we still query request logs by the target nodeId)
|
||||
val requestsFlow =
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.TRACEROUTE_APP).onStart { emit(emptyList()) },
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.NEIGHBORINFO_APP).onStart { emit(emptyList()) },
|
||||
) { trReqs, niReqs ->
|
||||
trReqs to niReqs
|
||||
}
|
||||
|
||||
// Assemble final UI state
|
||||
return combine(nodeFlow, metricsLogsFlow, identityFlow, metadataFlow, requestsFlow) {
|
||||
node,
|
||||
logs,
|
||||
identity,
|
||||
metadata,
|
||||
requests,
|
||||
->
|
||||
val (trReqs, niReqs) = requests
|
||||
val isLocal = node.num == identity.ourNode?.num
|
||||
val pioEnv = if (isLocal) identity.myInfo?.pioEnv else null
|
||||
val hw = deviceHardwareRepository.getDeviceHardwareByModel(node.user.hw_model.value, pioEnv).getOrNull()
|
||||
|
||||
val moduleConfig = identity.profile.module_config
|
||||
val displayUnits = identity.profile.config?.display?.units ?: Config.DisplayConfig.DisplayUnits.METRIC
|
||||
|
||||
val metricsState =
|
||||
MetricsState(
|
||||
node = node,
|
||||
isLocal = isLocal,
|
||||
deviceHardware = hw,
|
||||
reportedTarget = pioEnv,
|
||||
isManaged = identity.profile.config?.security?.is_managed ?: false,
|
||||
isFahrenheit =
|
||||
moduleConfig?.telemetry?.environment_display_fahrenheit == true ||
|
||||
(displayUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL),
|
||||
displayUnits = displayUnits,
|
||||
deviceMetrics = logs.telemetry.filter { it.device_metrics != null },
|
||||
powerMetrics = logs.telemetry.filter { it.power_metrics != null },
|
||||
hostMetrics = logs.telemetry.filter { it.host_metrics != null },
|
||||
signalMetrics = logs.packets.filter { it.isDirectSignal() },
|
||||
positionLogs = logs.posPackets.mapNotNull { it.toPosition() },
|
||||
paxMetrics = logs.pax,
|
||||
tracerouteRequests = trReqs,
|
||||
tracerouteResults = logs.trRes,
|
||||
neighborInfoRequests = niReqs,
|
||||
neighborInfoResults = logs.niRes,
|
||||
firmwareEdition = metadata.edition,
|
||||
latestStableFirmware = metadata.stable ?: FirmwareRelease(),
|
||||
latestAlphaFirmware = metadata.alpha ?: FirmwareRelease(),
|
||||
)
|
||||
|
||||
val environmentState =
|
||||
EnvironmentMetricsState(environmentMetrics = logs.telemetry.filter { it.hasValidEnvironmentMetrics() })
|
||||
|
||||
val availableLogs = buildSet {
|
||||
if (metricsState.hasDeviceMetrics()) add(LogsType.DEVICE)
|
||||
if (metricsState.hasPositionLogs()) {
|
||||
add(LogsType.NODE_MAP)
|
||||
add(LogsType.POSITIONS)
|
||||
}
|
||||
if (environmentState.hasEnvironmentMetrics()) add(LogsType.ENVIRONMENT)
|
||||
if (metricsState.hasSignalMetrics()) add(LogsType.SIGNAL)
|
||||
if (metricsState.hasPowerMetrics()) add(LogsType.POWER)
|
||||
if (metricsState.hasTracerouteLogs()) add(LogsType.TRACEROUTE)
|
||||
if (metricsState.hasNeighborInfoLogs()) add(LogsType.NEIGHBOR_INFO)
|
||||
if (metricsState.hasHostMetrics()) add(LogsType.HOST)
|
||||
if (metricsState.hasPaxMetrics()) add(LogsType.PAX)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val nodeName =
|
||||
node.user.long_name?.takeIf { it.isNotBlank() }?.let { UiText.DynamicString(it) }
|
||||
?: UiText.Resource(Res.string.fallback_node_name, node.user.id.takeLast(4))
|
||||
|
||||
NodeDetailUiState(
|
||||
node = node,
|
||||
nodeName = nodeName,
|
||||
ourNode = identity.ourNode,
|
||||
metricsState = metricsState,
|
||||
environmentState = environmentState,
|
||||
availableLogs = availableLogs,
|
||||
lastTracerouteTime = metadata.trTime,
|
||||
lastRequestNeighborsTime = metadata.niTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class LogsGroup(
|
||||
val telemetry: List<Telemetry>,
|
||||
val packets: List<MeshPacket>,
|
||||
val posPackets: List<MeshPacket>,
|
||||
val pax: List<MeshLog>,
|
||||
val trRes: List<MeshLog>,
|
||||
val niRes: List<MeshLog>,
|
||||
)
|
||||
|
||||
private data class IdentityGroup(val ourNode: Node?, val myInfo: MyNodeInfo?, val profile: DeviceProfile)
|
||||
|
||||
private data class MetadataGroup(
|
||||
val edition: FirmwareEdition?,
|
||||
val stable: FirmwareRelease?,
|
||||
val alpha: FirmwareRelease?,
|
||||
val trTime: Long?,
|
||||
val niTime: Long?,
|
||||
)
|
||||
}
|
||||
|
|
@ -68,18 +68,18 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_favorite
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.node_count_template
|
||||
import org.meshtastic.core.resources.nodes
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.remove_favorite
|
||||
import org.meshtastic.core.resources.remove_ignored
|
||||
import org.meshtastic.core.resources.unmute
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_favorite
|
||||
import org.meshtastic.core.strings.channel_invalid
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.mute_always
|
||||
import org.meshtastic.core.strings.node_count_template
|
||||
import org.meshtastic.core.strings.nodes
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.remove_favorite
|
||||
import org.meshtastic.core.strings.remove_ignored
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticImportFAB
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
|
|
@ -39,12 +38,13 @@ import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.node.detail.NodeManagementActions
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class NodeListViewModel
|
||||
@Inject
|
||||
|
|
@ -54,6 +54,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
val nodeManagementActions: NodeManagementActions,
|
||||
private val getFilteredNodesUseCase: GetFilteredNodesUseCase,
|
||||
val nodeFilterPreferences: NodeFilterPreferences,
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -116,35 +117,7 @@ constructor(
|
|||
|
||||
val nodeList: StateFlow<List<Node>> =
|
||||
combine(nodeFilter, nodeSortOption, ::Pair)
|
||||
.flatMapLatest { (filter, sort) ->
|
||||
nodeRepository
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter.filterText,
|
||||
includeUnknown = filter.includeUnknown,
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list ->
|
||||
list
|
||||
.filter { node -> node.isIgnored == filter.showIgnored }
|
||||
.filter { node ->
|
||||
if (filter.excludeInfrastructure) {
|
||||
val role = node.user.role
|
||||
val infrastructureRoles =
|
||||
listOf(
|
||||
Config.DeviceConfig.Role.ROUTER,
|
||||
Config.DeviceConfig.Role.REPEATER,
|
||||
Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
Config.DeviceConfig.Role.CLIENT_BASE,
|
||||
)
|
||||
role !in infrastructureRoles && !node.isEffectivelyUnmessageable
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatMapLatest { (filter, sort) -> getFilteredNodesUseCase.invoke(filter, sort) }
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val unfilteredNodeList: StateFlow<List<Node>> =
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.info
|
||||
import org.meshtastic.core.strings.logs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.info
|
||||
import org.meshtastic.core.resources.logs
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Refresh
|
||||
|
|
|
|||
|
|
@ -63,12 +63,12 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.info
|
||||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.info
|
||||
import org.meshtastic.core.resources.rssi
|
||||
import org.meshtastic.core.resources.snr
|
||||
import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import java.text.DateFormat
|
||||
|
|
|
|||
|
|
@ -66,16 +66,15 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.air_util_definition
|
||||
import org.meshtastic.core.strings.air_utilization
|
||||
import org.meshtastic.core.strings.battery
|
||||
import org.meshtastic.core.strings.ch_util_definition
|
||||
import org.meshtastic.core.strings.channel_utilization
|
||||
import org.meshtastic.core.strings.device_metrics_log
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_util_definition
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
import org.meshtastic.core.resources.battery
|
||||
import org.meshtastic.core.resources.ch_util_definition
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.device_metrics_log
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.component.MaterialBatteryInfo
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Cyan
|
||||
|
|
@ -141,7 +140,7 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,15 +33,15 @@ import com.patrykandpatrick.vico.compose.cartesian.data.lineSeries
|
|||
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.baro_pressure
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.lux
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.strings.uv_lux
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.baro_pressure
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
import org.meshtastic.core.resources.uv_lux
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
|||
|
|
@ -51,21 +51,20 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.env_metrics_log
|
||||
import org.meshtastic.core.strings.gas_resistance
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.iaq_definition
|
||||
import org.meshtastic.core.strings.lux
|
||||
import org.meshtastic.core.strings.radiation
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.strings.uv_lux
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.gas_resistance
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.iaq_definition
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
import org.meshtastic.core.resources.uv_lux
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.component.IaqDisplayMode
|
||||
import org.meshtastic.core.ui.component.IndoorAirQuality
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
|
@ -87,7 +86,7 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNa
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,13 +59,12 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.disk_free_indexed
|
||||
import org.meshtastic.core.strings.free_memory
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.load_indexed
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.user_string
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.disk_free_indexed
|
||||
import org.meshtastic.core.resources.free_memory
|
||||
import org.meshtastic.core.resources.load_indexed
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.user_string
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.DataArray
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -88,7 +87,7 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), o
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,18 +27,17 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -46,11 +45,8 @@ import org.jetbrains.compose.resources.StringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
|
@ -58,26 +54,21 @@ import org.meshtastic.core.di.CoroutineDispatchers
|
|||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.view_on_map
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.fallback_node_name
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.view_on_map
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import org.meshtastic.core.ui.util.toMessageRes
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.feature.node.detail.NodeRequestActions
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.TimeFrame
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import java.io.BufferedWriter
|
||||
|
|
@ -89,8 +80,6 @@ import java.util.Locale
|
|||
import javax.inject.Inject
|
||||
import org.meshtastic.proto.Paxcount as ProtoPaxcount
|
||||
|
||||
private fun MeshPacket.hasValidSignal(): Boolean = rx_time > 0 && (rx_snr != 0f || rx_rssi != 0)
|
||||
|
||||
/**
|
||||
* ViewModel responsible for managing and graphing metrics (telemetry, signal strength, paxcount) for a specific node.
|
||||
*/
|
||||
|
|
@ -103,27 +92,111 @@ constructor(
|
|||
private val app: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
private val alertManager: AlertManager,
|
||||
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||
) : ViewModel() {
|
||||
private var destNum: Int? =
|
||||
|
||||
private val nodeIdFromRoute: Int? =
|
||||
runCatching { savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum }.getOrNull()
|
||||
|
||||
private var jobs: Job? = null
|
||||
private val manualNodeId = MutableStateFlow<Int?>(null)
|
||||
private val activeNodeId =
|
||||
combine(MutableStateFlow(nodeIdFromRoute), manualNodeId) { fromRoute, manual -> manual ?: fromRoute }
|
||||
|
||||
private val tracerouteOverlayCache = MutableStateFlow<Map<Int, TracerouteOverlay>>(emptyMap())
|
||||
|
||||
val state: StateFlow<MetricsState> =
|
||||
activeNodeId
|
||||
.flatMapLatest { nodeId ->
|
||||
if (nodeId == null) return@flatMapLatest flowOf(MetricsState.Empty)
|
||||
getNodeDetailsUseCase(nodeId).map { it.metricsState }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MetricsState.Empty)
|
||||
|
||||
private val environmentState: StateFlow<EnvironmentMetricsState> =
|
||||
activeNodeId
|
||||
.flatMapLatest { nodeId ->
|
||||
if (nodeId == null) return@flatMapLatest flowOf(EnvironmentMetricsState())
|
||||
getNodeDetailsUseCase(nodeId).map { it.environmentState }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), EnvironmentMetricsState())
|
||||
|
||||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
|
||||
/** The active time window for filtering graphed data. */
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
/** Returns the list of time frames that are actually available based on the oldest data point. */
|
||||
val availableTimeFrames: StateFlow<List<TimeFrame>> =
|
||||
combine(state, environmentState) { currentState, envState ->
|
||||
val stateOldest = currentState.oldestTimestampSeconds()
|
||||
val envOldest = envState.environmentMetrics.minOfOrNull { (it.time ?: 0).toLong() }?.takeIf { it > 0 }
|
||||
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
|
||||
TimeFrame.entries.filter { it.isAvailable(oldest) }
|
||||
}
|
||||
.stateInWhileSubscribed(TimeFrame.entries)
|
||||
|
||||
fun setTimeFrame(timeFrame: TimeFrame) {
|
||||
_timeFrame.value = timeFrame
|
||||
}
|
||||
|
||||
/** Exposes filtered and unit-converted environment metrics for the UI. */
|
||||
val filteredEnvironmentMetrics: StateFlow<List<Telemetry>> =
|
||||
combine(environmentState, _timeFrame, state) { envState, timeFrame, currentState ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
val data = envState.environmentMetrics.filter { (it.time ?: 0).toLong() >= threshold }
|
||||
if (currentState.isFahrenheit) {
|
||||
data.map { telemetry ->
|
||||
val em = telemetry.environment_metrics ?: return@map telemetry
|
||||
telemetry.copy(
|
||||
environment_metrics =
|
||||
em.copy(
|
||||
temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
soil_temperature =
|
||||
em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
data
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
/** Exposes graphing data specifically for the filtered environment metrics. */
|
||||
val environmentGraphingData: StateFlow<EnvironmentGraphingData> =
|
||||
filteredEnvironmentMetrics
|
||||
.map { filtered -> EnvironmentMetricsState(filtered).environmentMetricsForGraphing(useFahrenheit = false) }
|
||||
.stateInWhileSubscribed(EnvironmentGraphingData(emptyList(), emptyList()))
|
||||
|
||||
/** Exposes filtered and decoded pax metrics for the UI. */
|
||||
val filteredPaxMetrics: StateFlow<List<Pair<MeshLog, ProtoPaxcount>>> =
|
||||
combine(state, _timeFrame) { currentState, timeFrame ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
currentState.paxMetrics
|
||||
.filter { (it.received_date / 1000) >= threshold }
|
||||
.mapNotNull { log -> decodePaxFromLog(log)?.let { log to it } }
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects
|
||||
|
||||
val lastTraceRouteTime: StateFlow<Long?> =
|
||||
combine(nodeRequestActions.lastTracerouteTimes, activeNodeId) { map, id -> id?.let { map[it] } }
|
||||
.stateInWhileSubscribed(null)
|
||||
|
||||
val lastRequestNeighborsTime: StateFlow<Long?> =
|
||||
combine(nodeRequestActions.lastRequestNeighborTimes, activeNodeId) { map, id -> id?.let { map[it] } }
|
||||
.stateInWhileSubscribed(null)
|
||||
|
||||
fun getUser(nodeNum: Int) = nodeRepository.getUser(nodeNum)
|
||||
|
||||
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) { meshLogRepository.deleteLog(uuid) }
|
||||
|
||||
/** Returns the map overlay for a specific traceroute request ID. */
|
||||
fun getTracerouteOverlay(requestId: Int): TracerouteOverlay? {
|
||||
val cached = tracerouteOverlayCache.value[requestId]
|
||||
if (cached != null) return cached
|
||||
|
|
@ -170,103 +243,35 @@ constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
Logger.d { "MetricsViewModel created" }
|
||||
}
|
||||
|
||||
fun clearPosition() = viewModelScope.launch(dispatchers.io) {
|
||||
destNum?.let { meshLogRepository.deleteLogs(it, PortNum.POSITION_APP.value) }
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
meshLogRepository.deleteLogs(it, PortNum.POSITION_APP.value)
|
||||
}
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(MetricsState.Empty)
|
||||
|
||||
/** Current aggregated metrics state, including signal history and sensor logs. */
|
||||
val state: StateFlow<MetricsState> = _state
|
||||
|
||||
private val environmentState = MutableStateFlow(EnvironmentMetricsState())
|
||||
|
||||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
|
||||
/** The active time window for filtering graphed data. */
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
/** Returns the list of time frames that are actually available based on the oldest data point. */
|
||||
val availableTimeFrames: StateFlow<List<TimeFrame>> =
|
||||
combine(_state, environmentState) { state, envState ->
|
||||
val stateOldest = state.oldestTimestampSeconds()
|
||||
val envOldest = envState.environmentMetrics.minOfOrNull { (it.time ?: 0).toLong() }?.takeIf { it > 0 }
|
||||
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
|
||||
TimeFrame.entries.filter { it.isAvailable(oldest) }
|
||||
}
|
||||
.stateInWhileSubscribed(TimeFrame.entries)
|
||||
|
||||
fun setTimeFrame(timeFrame: TimeFrame) {
|
||||
_timeFrame.value = timeFrame
|
||||
}
|
||||
|
||||
/** Exposes filtered and unit-converted environment metrics for the UI. */
|
||||
val filteredEnvironmentMetrics: StateFlow<List<Telemetry>> =
|
||||
combine(environmentState, _timeFrame, _state) { envState, timeFrame, state ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
val data = envState.environmentMetrics.filter { (it.time ?: 0).toLong() >= threshold }
|
||||
if (state.isFahrenheit) {
|
||||
data.map { telemetry ->
|
||||
val em = telemetry.environment_metrics ?: return@map telemetry
|
||||
telemetry.copy(
|
||||
environment_metrics =
|
||||
em.copy(
|
||||
temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
soil_temperature =
|
||||
em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
data
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
/** Exposes graphing data specifically for the filtered environment metrics. */
|
||||
val environmentGraphingData: StateFlow<EnvironmentGraphingData> =
|
||||
filteredEnvironmentMetrics
|
||||
.map { filtered -> EnvironmentMetricsState(filtered).environmentMetricsForGraphing(useFahrenheit = false) }
|
||||
.stateInWhileSubscribed(EnvironmentGraphingData(emptyList(), emptyList()))
|
||||
|
||||
/** Exposes filtered and decoded pax metrics for the UI. */
|
||||
val filteredPaxMetrics: StateFlow<List<Pair<MeshLog, ProtoPaxcount>>> =
|
||||
combine(_state, _timeFrame) { state, timeFrame ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
state.paxMetrics
|
||||
.filter { (it.received_date / 1000) >= threshold }
|
||||
.mapNotNull { log -> decodePaxFromLog(log)?.let { log to it } }
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects
|
||||
|
||||
val lastTraceRouteTime: StateFlow<Long?> =
|
||||
nodeRequestActions.lastTracerouteTimes.map { it[destNum] }.stateInWhileSubscribed(null)
|
||||
|
||||
val lastRequestNeighborsTime: StateFlow<Long?> =
|
||||
nodeRequestActions.lastRequestNeighborTimes.map { it[destNum] }.stateInWhileSubscribed(null)
|
||||
|
||||
fun requestPosition() {
|
||||
destNum?.let { nodeRequestActions.requestPosition(viewModelScope, it, state.value.node?.user?.long_name ?: "") }
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestPosition(viewModelScope, it, state.value.node?.user?.long_name ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTelemetry(type: TelemetryType) {
|
||||
destNum?.let {
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestTelemetry(viewModelScope, it, state.value.node?.user?.long_name ?: "", type)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTraceroute() {
|
||||
destNum?.let {
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestTraceroute(viewModelScope, it, state.value.node?.user?.long_name ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestNeighborInfo() {
|
||||
destNum?.let {
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestNeighborInfo(viewModelScope, it, state.value.node?.user?.long_name ?: "")
|
||||
}
|
||||
}
|
||||
|
|
@ -278,7 +283,6 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/** Shows the detail dialog for a traceroute result, with an option to view on the map. */
|
||||
fun showTracerouteDetail(
|
||||
annotatedMessage: AnnotatedString,
|
||||
requestId: Int,
|
||||
|
|
@ -318,188 +322,17 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initializeFlows()
|
||||
}
|
||||
|
||||
fun setNodeId(id: Int) {
|
||||
if (destNum != id) {
|
||||
destNum = id
|
||||
initializeFlows()
|
||||
if (manualNodeId.value != id) {
|
||||
manualNodeId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun initializeFlows() {
|
||||
jobs?.cancel()
|
||||
val currentDestNum = destNum
|
||||
jobs =
|
||||
viewModelScope.launch {
|
||||
if (currentDestNum != null) {
|
||||
val logNodeIdFlow = nodeRepository.effectiveLogNodeId(currentDestNum)
|
||||
|
||||
launch {
|
||||
combine(nodeRepository.nodeDBbyNum, nodeRepository.myNodeInfo) { nodes, myInfo ->
|
||||
nodes[currentDestNum] to (nodes.keys.firstOrNull() to myInfo)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { (node, localData) ->
|
||||
val (ourNodeNum, myInfo) = localData
|
||||
// Create a fallback node if not found in database (for hidden clients, etc.)
|
||||
val actualNode =
|
||||
node
|
||||
?: Node.createFallback(currentDestNum, getString(Res.string.fallback_node_name))
|
||||
val pioEnv = if (currentDestNum == ourNodeNum) myInfo?.pioEnv else null
|
||||
val hwModel = actualNode.user.hw_model.value
|
||||
val deviceHardware =
|
||||
deviceHardwareRepository.getDeviceHardwareByModel(hwModel, target = pioEnv)
|
||||
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
node = actualNode,
|
||||
isLocal = currentDestNum == ourNodeNum,
|
||||
deviceHardware = deviceHardware.getOrNull(),
|
||||
reportedTarget = pioEnv,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
radioConfigRepository.deviceProfileFlow.collect { profile ->
|
||||
val moduleConfig = profile.module_config
|
||||
val displayUnits = profile.config?.display?.units
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
isManaged = profile.config?.security?.is_managed ?: false,
|
||||
isFahrenheit =
|
||||
moduleConfig?.telemetry?.environment_display_fahrenheit == true ||
|
||||
(displayUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL),
|
||||
displayUnits = displayUnits ?: Config.DisplayConfig.DisplayUnits.METRIC,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getTelemetryFrom(it) }
|
||||
.collect { telemetry ->
|
||||
val device = mutableListOf<Telemetry>()
|
||||
val power = mutableListOf<Telemetry>()
|
||||
val host = mutableListOf<Telemetry>()
|
||||
val env = mutableListOf<Telemetry>()
|
||||
|
||||
for (item in telemetry) {
|
||||
if (item.device_metrics != null) device.add(item)
|
||||
if (item.power_metrics != null) power.add(item)
|
||||
if (item.host_metrics != null) host.add(item)
|
||||
if (item.hasValidEnvironmentMetrics()) env.add(item)
|
||||
}
|
||||
|
||||
_state.update { state ->
|
||||
state.copy(deviceMetrics = device, powerMetrics = power, hostMetrics = host)
|
||||
}
|
||||
environmentState.update { it.copy(environmentMetrics = env) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getMeshPacketsFrom(it) }
|
||||
.collect { meshPackets ->
|
||||
_state.update { state ->
|
||||
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(currentDestNum, PortNum.TRACEROUTE_APP),
|
||||
logNodeIdFlow.flatMapLatest {
|
||||
meshLogRepository.getLogsFrom(it, PortNum.TRACEROUTE_APP.value)
|
||||
},
|
||||
) { request, response ->
|
||||
_state.update { state ->
|
||||
state.copy(tracerouteRequests = request, tracerouteResults = response)
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
launch {
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(currentDestNum, PortNum.NEIGHBORINFO_APP),
|
||||
logNodeIdFlow.flatMapLatest {
|
||||
meshLogRepository.getLogsFrom(it, PortNum.NEIGHBORINFO_APP.value)
|
||||
},
|
||||
) { request, response ->
|
||||
_state.update { state ->
|
||||
state.copy(neighborInfoRequests = request, neighborInfoResults = response)
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getMeshPacketsFrom(it, PortNum.POSITION_APP.value) }
|
||||
.collect { packets ->
|
||||
val distinctPositions =
|
||||
packets
|
||||
.mapNotNull { it.toPosition() }
|
||||
.asFlow()
|
||||
.distinctUntilChanged { old, new ->
|
||||
old.time == new.time ||
|
||||
(old.latitude_i == new.latitude_i && old.longitude_i == new.longitude_i)
|
||||
}
|
||||
.toList()
|
||||
_state.update { state -> state.copy(positionLogs = distinctPositions) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getLogsFrom(it, PortNum.PAXCOUNTER_APP.value) }
|
||||
.collect { logs -> _state.update { state -> state.copy(paxMetrics = logs) } }
|
||||
}
|
||||
|
||||
launch {
|
||||
firmwareReleaseRepository.stableRelease.filterNotNull().collect { latestStable ->
|
||||
_state.update { state -> state.copy(latestStableFirmware = latestStable) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
firmwareReleaseRepository.alphaRelease.filterNotNull().collect { latestAlpha ->
|
||||
_state.update { state -> state.copy(latestAlphaFirmware = latestAlpha) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
meshLogRepository
|
||||
.getMyNodeInfo()
|
||||
.map { it?.firmware_edition }
|
||||
.distinctUntilChanged()
|
||||
.collect { firmwareEdition ->
|
||||
_state.update { state -> state.copy(firmwareEdition = firmwareEdition) }
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d { "MetricsViewModel created" }
|
||||
} else {
|
||||
Logger.d { "MetricsViewModel: destNum is null, skipping metrics flows initialization." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Logger.d { "MetricsViewModel cleared" }
|
||||
}
|
||||
|
||||
/** Write the persisted Position data out to a CSV file in the specified location. */
|
||||
fun savePositionCSV(uri: Uri) = viewModelScope.launch(dispatchers.main) {
|
||||
val positions = state.value.positionLogs
|
||||
writeToUri(uri) { writer ->
|
||||
|
|
@ -518,7 +351,6 @@ constructor(
|
|||
val speed = position.ground_speed
|
||||
val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
|
||||
|
||||
// date,time,latitude,longitude,altitude,satsInView,speed,heading
|
||||
writer.appendLine(
|
||||
"$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"",
|
||||
)
|
||||
|
|
@ -541,12 +373,10 @@ constructor(
|
|||
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount")
|
||||
fun decodePaxFromLog(log: MeshLog): ProtoPaxcount? {
|
||||
// First, try to parse from the binary fromRadio field (robust, like telemetry)
|
||||
try {
|
||||
val packet = log.fromRadio.packet
|
||||
val decoded = packet?.decoded
|
||||
if (packet != null && decoded != null && decoded.portnum == PortNum.PAXCOUNTER_APP) {
|
||||
// Requests for paxcount (want_response = true) should not be logged as data points.
|
||||
if (decoded.want_response == true) return null
|
||||
val pax = ProtoPaxcount.ADAPTER.decode(decoded.payload)
|
||||
if ((pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0 || (pax.uptime ?: 0) != 0) return pax
|
||||
|
|
@ -554,10 +384,9 @@ constructor(
|
|||
} catch (e: IOException) {
|
||||
Logger.e(e) { "Failed to parse Paxcount from binary data" }
|
||||
}
|
||||
// Fallback: Attempt to parse Paxcount from raw_message as base64 or hex string.
|
||||
try {
|
||||
val base64 = log.raw_message.trim()
|
||||
if (base64.matches(Regex("^[A-Za-z0-9+/=\r\n]+$"))) {
|
||||
if (base64.matches(Regex("^[A-Za-z0-9+/=\\r\\n]+$"))) {
|
||||
val bytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
|
||||
return ProtoPaxcount.ADAPTER.decode(bytes)
|
||||
} else if (base64.matches(Regex("^[0-9a-fA-F]+$")) && base64.length % 2 == 0) {
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.getNeighborInfoResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.neighbor_info
|
||||
import org.meshtastic.core.resources.routing_error_no_response
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.Groups
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -77,7 +76,7 @@ fun NeighborInfoLogScreen(
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +136,7 @@ fun NeighborInfoLogScreen(
|
|||
)
|
||||
val text = if (result != null) "Success" else stringResource(Res.string.routing_error_no_response)
|
||||
val icon = if (result != null) MeshtasticIcons.Groups else MeshtasticIcons.PersonOff
|
||||
val header = stringResource(Res.string.neighbor_info)
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
|
|
@ -149,10 +149,7 @@ fun NeighborInfoLogScreen(
|
|||
result
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.getNeighborInfoResponse(
|
||||
::getUsername,
|
||||
header = getString(Res.string.neighbor_info),
|
||||
)
|
||||
?.getNeighborInfoResponse(::getUsername, header = header)
|
||||
?.let {
|
||||
val message =
|
||||
annotateNeighborInfo(
|
||||
|
|
|
|||
|
|
@ -59,14 +59,13 @@ import org.meshtastic.core.common.util.toInstant
|
|||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.ble_devices
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.no_pax_metrics_logs
|
||||
import org.meshtastic.core.strings.pax
|
||||
import org.meshtastic.core.strings.pax_metrics_log
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.wifi_devices
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ble_devices
|
||||
import org.meshtastic.core.resources.no_pax_metrics_logs
|
||||
import org.meshtastic.core.resources.pax
|
||||
import org.meshtastic.core.resources.pax_metrics_log
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.wifi_devices
|
||||
import org.meshtastic.core.ui.component.IconInfo
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Paxcount
|
||||
|
|
@ -189,7 +188,7 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,17 +65,16 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alt
|
||||
import org.meshtastic.core.strings.clear
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.heading
|
||||
import org.meshtastic.core.strings.latitude
|
||||
import org.meshtastic.core.strings.longitude
|
||||
import org.meshtastic.core.strings.sats
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.strings.speed
|
||||
import org.meshtastic.core.strings.timestamp
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.alt
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.heading
|
||||
import org.meshtastic.core.resources.latitude
|
||||
import org.meshtastic.core.resources.longitude
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.resources.save
|
||||
import org.meshtastic.core.resources.speed
|
||||
import org.meshtastic.core.resources.timestamp
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -182,7 +181,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateU
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,13 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_1
|
||||
import org.meshtastic.core.strings.channel_2
|
||||
import org.meshtastic.core.strings.channel_3
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.power_metrics_log
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_1
|
||||
import org.meshtastic.core.resources.channel_2
|
||||
import org.meshtastic.core.resources.channel_3
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.power_metrics_log
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Gold
|
||||
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
|
@ -121,7 +120,7 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigate
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,13 +58,12 @@ import com.patrykandpatrick.vico.compose.cartesian.data.lineSeries
|
|||
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.rssi_definition
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.strings.snr_definition
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.rssi
|
||||
import org.meshtastic.core.resources.rssi_definition
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.snr
|
||||
import org.meshtastic.core.resources.snr_definition
|
||||
import org.meshtastic.core.ui.component.LoraSignalIndicator
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Blue
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Green
|
||||
|
|
@ -98,7 +97,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,18 +52,17 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.traceroute_diff
|
||||
import org.meshtastic.core.strings.traceroute_direct
|
||||
import org.meshtastic.core.strings.traceroute_duration
|
||||
import org.meshtastic.core.strings.traceroute_hops
|
||||
import org.meshtastic.core.strings.traceroute_log
|
||||
import org.meshtastic.core.strings.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.strings.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.strings.traceroute_time_and_text
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.routing_error_no_response
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.traceroute_diff
|
||||
import org.meshtastic.core.resources.traceroute_direct
|
||||
import org.meshtastic.core.resources.traceroute_duration
|
||||
import org.meshtastic.core.resources.traceroute_hops
|
||||
import org.meshtastic.core.resources.traceroute_log
|
||||
import org.meshtastic.core.resources.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.resources.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.resources.traceroute_time_and_text
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.Group
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -98,7 +97,7 @@ fun TracerouteLogScreen(
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,6 +141,8 @@ fun TracerouteLogScreen(
|
|||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
items(state.tracerouteRequests, key = { it.uuid }) { log ->
|
||||
val headerTowardsStr = stringResource(Res.string.traceroute_route_towards_dest)
|
||||
val headerBackStr = stringResource(Res.string.traceroute_route_back_to_us)
|
||||
val result =
|
||||
remember(state.tracerouteRequests, log.fromRadio.packet?.id) {
|
||||
state.tracerouteResults.find {
|
||||
|
|
@ -169,7 +170,7 @@ fun TracerouteLogScreen(
|
|||
res.fromRadio.packet?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = stringResource(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = stringResource(Res.string.traceroute_route_back_to_us),
|
||||
headerBack = headerBackStr,
|
||||
),
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
|
|
@ -186,7 +187,7 @@ fun TracerouteLogScreen(
|
|||
?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = stringResource(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = stringResource(Res.string.traceroute_route_back_to_us),
|
||||
headerBack = headerBackStr,
|
||||
)
|
||||
?.let { AnnotatedString(it) }
|
||||
}
|
||||
|
|
@ -214,8 +215,8 @@ fun TracerouteLogScreen(
|
|||
?.packet
|
||||
?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = getString(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = getString(Res.string.traceroute_route_back_to_us),
|
||||
headerTowards = headerTowardsStr,
|
||||
headerBack = headerBackStr,
|
||||
)
|
||||
?.let {
|
||||
annotateTraceroute(
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.traceroute_outgoing_route
|
||||
import org.meshtastic.core.strings.traceroute_return_route
|
||||
import org.meshtastic.core.strings.traceroute_showing_nodes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.traceroute_outgoing_route
|
||||
import org.meshtastic.core.resources.traceroute_return_route
|
||||
import org.meshtastic.core.resources.traceroute_showing_nodes
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Route
|
||||
|
|
|
|||
|
|
@ -29,17 +29,17 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.device_metrics_log
|
||||
import org.meshtastic.core.strings.env_metrics_log
|
||||
import org.meshtastic.core.strings.host_metrics_log
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.node_map
|
||||
import org.meshtastic.core.strings.pax_metrics_log
|
||||
import org.meshtastic.core.strings.position_log
|
||||
import org.meshtastic.core.strings.power_metrics_log
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.traceroute_log
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.device_metrics_log
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.host_metrics_log
|
||||
import org.meshtastic.core.resources.neighbor_info
|
||||
import org.meshtastic.core.resources.node_map
|
||||
import org.meshtastic.core.resources.pax_metrics_log
|
||||
import org.meshtastic.core.resources.position_log
|
||||
import org.meshtastic.core.resources.power_metrics_log
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.traceroute_log
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Paxcount
|
||||
import org.meshtastic.core.ui.icon.Route
|
||||
|
|
|
|||
|
|
@ -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,11 +14,10 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.node.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
internal data class VectorMetricInfo(
|
||||
|
|
@ -31,6 +30,6 @@ internal data class VectorMetricInfo(
|
|||
internal data class DrawableMetricInfo(
|
||||
val label: StringResource,
|
||||
val value: String,
|
||||
@DrawableRes val icon: Int,
|
||||
val icon: DrawableResource,
|
||||
val rotateIcon: Float = 0f,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ package org.meshtastic.feature.node.model
|
|||
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.all_time
|
||||
import org.meshtastic.core.strings.one_hour_short
|
||||
import org.meshtastic.core.strings.one_month
|
||||
import org.meshtastic.core.strings.one_week
|
||||
import org.meshtastic.core.strings.twenty_four_hours
|
||||
import org.meshtastic.core.strings.two_weeks
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.all_time
|
||||
import org.meshtastic.core.resources.one_hour_short
|
||||
import org.meshtastic.core.resources.one_month
|
||||
import org.meshtastic.core.resources.one_week
|
||||
import org.meshtastic.core.resources.twenty_four_hours
|
||||
import org.meshtastic.core.resources.two_weeks
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class TimeFrame(val strRes: StringResource, val seconds: Long) {
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="909.88dp"
|
||||
android:height="546.86dp"
|
||||
android:viewportWidth="909.88"
|
||||
android:viewportHeight="546.86">
|
||||
<path
|
||||
android:pathData="m898.52,135.44h4.69a5.67,5.67 0,0 1,5.67 5.67v84.65a5.67,5.67 0,0 1,-5.67 5.67h-4.69"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="M12.7,104.75L886.82,104.75A11.7,11.7 0,0 1,898.52 116.45L898.52,534.16A11.7,11.7 0,0 1,886.82 545.86L12.7,545.86A11.7,11.7 0,0 1,1 534.16L1,116.45A11.7,11.7 0,0 1,12.7 104.75z"
|
||||
android:fillColor="#cbcccb"/>
|
||||
<path
|
||||
android:pathData="m34.47,104.75v113.48a3.67,3.67 0,0 0,3.67 3.67h41a2.35,2.35 0,0 1,2.35 2.35L81.49,545.86L870.95,545.86L870.95,104.75ZM845.99,520.86L106.53,520.86L106.53,213.96a17.06,17.06 0,0 0,-17.06 -17.06h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5h784z"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="M845.99,129.75L845.99,520.86L106.53,520.86L106.53,213.96a17,17 0,0 0,-7.2 -13.92v-70.29z"
|
||||
android:fillColor="#cbcccb"/>
|
||||
<path
|
||||
android:pathData="m99.33,129.75v70.29a17,17 0,0 0,-9.86 -3.14h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5z"
|
||||
android:fillColor="#b7b7b7"/>
|
||||
<path
|
||||
android:pathData="M25.45,253.39h13.53v148.4h-13.53z"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="m430.64,95.71h71.71a2.55,2.55 0,0 1,2.55 2.55v6.48h-76.8v-6.48a2.55,2.55 0,0 1,2.54 -2.55z"
|
||||
android:fillColor="#b1a368"/>
|
||||
<path
|
||||
android:pathData="m436.27,3.17h60.88a6.2,4.85 0,0 1,6.2 4.85v77.33h-73.28v-77.33a6.2,4.85 0,0 1,6.2 -4.85z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M425.88,20.14L506.28,20.14A5.55,5.55 0,0 1,511.83 25.69L511.83,70.55A5.55,5.55 0,0 1,506.28 76.1L425.88,76.1A5.55,5.55 0,0 1,420.33 70.55L420.33,25.69A5.55,5.55 0,0 1,425.88 20.14z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m511.8,24.48v47.25a5.52,4.31 0,0 1,-5.55 4.34h-80.37a5.55,4.34 0,0 1,-5.59 -4.34v-47.25a5.55,4.34 0,0 1,5.59 -4.34h80.51a5.52,4.31 0,0 1,5.41 4.34z"
|
||||
android:strokeWidth="3.16706"
|
||||
android:fillColor="#9f9f9e"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M433.29,85.68h65.99v10.03h-65.99z"
|
||||
android:fillColor="#b1a368"/>
|
||||
<path
|
||||
android:pathData="M845.99,129.75L845.99,520.86L106.53,520.86L106.53,213.96a17.06,17.06 0,0 0,-17.06 -17.06h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5h784m25,-25L34.47,104.75v113.48a3.68,3.68 0,0 0,3.67 3.67h41a2.35,2.35 0,0 1,2.35 2.35L81.49,545.86L870.95,545.86L870.95,104.75Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M99.34,200.04L99.34,129.75"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M25.45,253.39h13.53v148.4h-13.53z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m898.52,135.44h4.69a5.67,5.67 0,0 1,5.67 5.67v84.65a5.67,5.67 0,0 1,-5.67 5.67h-4.69"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m430.64,95.71h71.71a2.55,2.55 0,0 1,2.55 2.55v6.48h-76.8v-6.48a2.55,2.55 0,0 1,2.54 -2.55z"
|
||||
android:strokeWidth="2.04"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M433.29,85.68h65.99v10.03h-65.99z"
|
||||
android:strokeWidth="1.99"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m78.62,152.33a14,14 0,1 0,14 14,13.95 13.95,0 0,0 -14,-14zM78.62,173.83a7.55,7.55 0,1 1,7.54 -7.55,7.55 7.55,0 0,1 -7.54,7.55z"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="M78.62,166.28m-7.55,0a7.55,7.55 0,1 1,15.1 0a7.55,7.55 0,1 1,-15.1 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M78.62,166.28m-13.95,0a13.95,13.95 0,1 1,27.9 0a13.95,13.95 0,1 1,-27.9 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m445.36,440.05c0,11.52 10.38,20.86 23.19,20.86 12.81,0 23.19,-9.34 23.19,-20.86 0,-11.52 -10.38,-20.86 -23.19,-20.86 -12.81,0 -23.19,9.34 -23.19,20.86z"
|
||||
android:strokeWidth="0.458227"
|
||||
android:fillColor="#4d4d4d"/>
|
||||
<path
|
||||
android:pathData="m469.4,538.4c-119.83,0 -217.32,-93.41 -217.32,-208.23 0,-114.82 97.48,-208.23 217.32,-208.23 119.83,0 217.32,93.41 217.32,208.23 0,114.82 -97.48,208.23 -217.32,208.23zM469.4,151.82c-102.64,0 -186.13,80.01 -186.13,178.35 0,98.33 83.5,178.35 186.13,178.35 102.62,0 186.13,-80.02 186.13,-178.35 0,-98.34 -83.51,-178.35 -186.13,-178.35z"
|
||||
android:strokeWidth="0.474832"
|
||||
android:fillColor="#4d4d4d"/>
|
||||
<path
|
||||
android:pathData="m468.56,391.97c-8.54,0 -15.46,-6.23 -15.46,-13.91v-23.51c0,-22.75 19.33,-40.13 36.4,-55.47 12.51,-11.26 25.45,-22.89 25.45,-32.16 0,-23.18 -20.81,-42.04 -46.39,-42.04 -26.01,0 -46.39,18.05 -46.39,41.09 0,7.68 -6.93,13.91 -15.46,13.91 -8.54,0 -15.46,-6.23 -15.46,-13.91 0,-37.99 34.68,-68.9 77.31,-68.9 42.63,0 77.31,31.33 77.31,69.85 0,20.82 -17.55,36.59 -34.51,51.84 -13.45,12.07 -27.34,24.56 -27.34,35.78v23.51c0,7.68 -6.93,13.92 -15.46,13.92z"
|
||||
android:strokeWidth="0.458227"
|
||||
android:fillColor="#4d4d4d"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M12.7,104.75L886.82,104.75A11.7,11.7 0,0 1,898.52 116.45L898.52,534.16A11.7,11.7 0,0 1,886.82 545.86L12.7,545.86A11.7,11.7 0,0 1,1 534.16L1,116.45A11.7,11.7 0,0 1,12.7 104.75z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
|
||||
android:strokeWidth="0.92"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
|
||||
android:strokeWidth="0.92"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
|
||||
android:strokeWidth="0.92"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0"/>
|
||||
</vector>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21,11a1,1 0,0 1,1 1a10,10 0,0 1,-5 8.656a1,1 0,0 1,-1.302 -0.268l-0.064,-0.098l-3,-5.19a0.995,0.995 0,0 1,-0.133 -0.542l0.01,-0.11l0.023,-0.106l0.034,-0.106l0.046,-0.1l0.056,-0.094l0.067,-0.089a0.994,0.994 0,0 1,0.165 -0.155l0.098,-0.064a2,2 0,0 0,0.993 -1.57l0.007,-0.163a1,1 0,0 1,0.883 -0.994l0.117,-0.007h6z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,3.344a10,10 0,0 1,10 0a1,1 0,0 1,0.418 1.262l-0.052,0.104l-3,5.19l-0.064,0.098a0.994,0.994 0,0 1,-0.155 0.165l-0.089,0.067a1,1 0,0 1,-0.195 0.102l-0.105,0.034l-0.107,0.022a1.003,1.003 0,0 1,-0.547 -0.07l-0.104,-0.052a2,2 0,0 0,-1.842 -0.082l-0.158,0.082a1,1 0,0 1,-1.302 -0.268l-0.064,-0.098l-3,-5.19a1,1 0,0 1,0.366 -1.366z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,11a1,1 0,0 1,0.993 0.884l0.007,0.117a2,2 0,0 0,0.861 1.645l0.237,0.152a0.994,0.994 0,0 1,0.165 0.155l0.067,0.089l0.056,0.095l0.045,0.099c0.014,0.036 0.026,0.07 0.035,0.106l0.022,0.107l0.011,0.11a0.994,0.994 0,0 1,-0.08 0.437l-0.053,0.104l-3,5.19a1,1 0,0 1,-1.366 0.366a10,10 0,0 1,-5 -8.656a1,1 0,0 1,0.883 -0.993l0.117,-0.007h6z" />
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M620,440q-25,0 -42.5,-17.5T560,380q0,-17 9.5,-34.5t20.5,-32q11,-14.5 20.5,-24l9.5,-9.5 9.5,9.5q9.5,9.5 20.5,24t20.5,32Q680,363 680,380q0,25 -17.5,42.5T620,440ZM780,320q-25,0 -42.5,-17.5T720,260q0,-17 9.5,-34.5t20.5,-32q11,-14.5 20.5,-24l9.5,-9.5 9.5,9.5q9.5,9.5 20.5,24t20.5,32Q840,243 840,260q0,25 -17.5,42.5T780,320ZM780,560q-25,0 -42.5,-17.5T720,500q0,-17 9.5,-34.5t20.5,-32q11,-14.5 20.5,-24l9.5,-9.5 9.5,9.5q9.5,9.5 20.5,24t20.5,32Q840,483 840,500q0,25 -17.5,42.5T780,560ZM360,840q-83,0 -141.5,-58.5T160,640q0,-48 21,-89.5t59,-70.5v-240q0,-50 35,-85t85,-35q50,0 85,35t35,85v240q38,29 59,70.5t21,89.5q0,83 -58.5,141.5T360,840ZM240,640h240q0,-29 -12.5,-54T432,544l-32,-24v-280q0,-17 -11.5,-28.5T360,200q-17,0 -28.5,11.5T320,240v280l-32,24q-23,17 -35.5,42T240,640Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
</vector>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="32" android:viewportWidth="32" android:width="200dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M24.5,30a5.202,5.202 0,0 1,-4.626 -8.08L23.49,16.538a1.217,1.217 0,0 1,2.02 0L29.06,21.815A5.492,5.492 0,0 1,30 24.751,5.385 5.385,0 0,1 24.5,30ZM24.5,18.62 L21.564,22.987A3.208,3.208 0,0 0,24.5 28,3.385 3.385,0 0,0 28,24.751a3.435,3.435 0,0 0,-0.63 -1.867Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M11,16V11h1a4.004,4.004 0,0 0,4 -4V4H13a3.978,3.978 0,0 0,-2.747 1.107A6.003,6.003 0,0 0,5 2H2V5a6.007,6.007 0,0 0,6 6H9v5H2v2H16V16ZM13,6h1V7a2.002,2.002 0,0 1,-2 2H11V8A2.002,2.002 0,0 1,13 6ZM8,9A4.004,4.004 0,0 1,4 5V4H5A4.004,4.004 0,0 1,9 8V9Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,21h14v2h-14z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,26h14v2h-14z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="32" android:viewportWidth="32" android:width="200dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M11,16V11h1a4.004,4.004 0,0 0,4 -4V4H13a3.978,3.978 0,0 0,-2.747 1.107A6.003,6.003 0,0 0,5 2H2V5a6.007,6.007 0,0 0,6 6H9v5H2v2H16V16ZM13,6h1V7a2.002,2.002 0,0 1,-2 2H11V8A2.002,2.002 0,0 1,13 6ZM8,9A4.004,4.004 0,0 1,4 5V4H5A4.004,4.004 0,0 1,9 8V9Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,21h14v2h-14z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,26h14v2h-14z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M25,30a4.986,4.986 0,0 1,-3 -8.98L22,15a3,3 0,0 1,6 0v6.02A4.986,4.986 0,0 1,25 30ZM25,14a1.001,1.001 0,0 0,-1 1v7.13l-0.497,0.289A2.968,2.968 0,0 0,22 25a3,3 0,0 0,6 0,2.968 2.968,0 0,0 -1.503,-2.581L26,22.13L26,15A1.001,1.001 0,0 0,25 14Z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M23,11.99l-2.44,-2.79l0.34,-3.69l-3.61,-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,11.99l2.44,2.79l-0.34,3.7l3.61,0.82L8.6,22.5l3.4,-1.47l3.4,1.46l1.89,-3.19l3.61,-0.82l-0.34,-3.69L23,11.99zM19.05,13.47l-0.56,0.65l0.08,0.85l0.18,1.95l-1.9,0.43l-0.84,0.19l-0.44,0.74l-0.99,1.68l-1.78,-0.77L12,18.85l-0.79,0.34l-1.78,0.77l-0.99,-1.67l-0.44,-0.74l-0.84,-0.19l-1.9,-0.43l0.18,-1.96l0.08,-0.85l-0.56,-0.65l-1.29,-1.47l1.29,-1.48l0.56,-0.65L5.43,9.01L5.25,7.07l1.9,-0.43l0.84,-0.19l0.44,-0.74l0.99,-1.68l1.78,0.77L12,5.14l0.79,-0.34l1.78,-0.77l0.99,1.68l0.44,0.74l0.84,0.19l1.9,0.43l-0.18,1.95l-0.08,0.85l0.56,0.65l1.29,1.47L19.05,13.47z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.node.domain.usecase
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.feature.node.list.NodeFilterState
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
class GetFilteredNodesUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var useCase: GetFilteredNodesUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeRepository = mockk()
|
||||
useCase = GetFilteredNodesUseCase(nodeRepository)
|
||||
}
|
||||
|
||||
private fun createNode(
|
||||
num: Int,
|
||||
role: Config.DeviceConfig.Role = Config.DeviceConfig.Role.CLIENT,
|
||||
ignored: Boolean = false,
|
||||
name: String = "Node$num",
|
||||
): Node {
|
||||
val user = User(id = "!$num", long_name = name, short_name = "N$num", role = role)
|
||||
return Node(num = num, user = user, isIgnored = ignored)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke applies repository filters and returns nodes`() = runTest {
|
||||
// Arrange
|
||||
val nodes = listOf(createNode(1), createNode(2))
|
||||
val filter = NodeFilterState(filterText = "Node", includeUnknown = true)
|
||||
|
||||
every {
|
||||
nodeRepository.getNodes(
|
||||
sort = NodeSortOption.LAST_HEARD,
|
||||
filter = "Node",
|
||||
includeUnknown = true,
|
||||
onlyOnline = false,
|
||||
onlyDirect = false,
|
||||
)
|
||||
} returns flowOf(nodes)
|
||||
|
||||
// Act
|
||||
val result = useCase(filter, NodeSortOption.LAST_HEARD).first()
|
||||
|
||||
// Assert
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(1, result[0].num)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke filters out ignored nodes if showIgnored is false`() = runTest {
|
||||
// Arrange
|
||||
val normalNode = createNode(1, ignored = false)
|
||||
val ignoredNode = createNode(2, ignored = true)
|
||||
val filter = NodeFilterState(showIgnored = false)
|
||||
|
||||
every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns
|
||||
flowOf(listOf(normalNode, ignoredNode))
|
||||
|
||||
// Act
|
||||
val result = useCase(filter, NodeSortOption.LAST_HEARD).first()
|
||||
|
||||
// Assert
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(1, result.first().num)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke filters out infrastructure nodes if excludeInfrastructure is true`() = runTest {
|
||||
// Arrange
|
||||
val clientNode = createNode(1, role = Config.DeviceConfig.Role.CLIENT)
|
||||
val routerNode = createNode(2, role = Config.DeviceConfig.Role.ROUTER)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val repeaterNode = createNode(3, role = Config.DeviceConfig.Role.REPEATER)
|
||||
val clientBaseNode = createNode(4, role = Config.DeviceConfig.Role.CLIENT_BASE)
|
||||
val filter = NodeFilterState(excludeInfrastructure = true)
|
||||
|
||||
every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns
|
||||
flowOf(listOf(clientNode, routerNode, repeaterNode, clientBaseNode))
|
||||
|
||||
// Act
|
||||
val result = useCase(filter, NodeSortOption.LAST_HEARD).first()
|
||||
|
||||
// Assert
|
||||
// Should only keep the CLIENT node, others are infrastructure
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(1, result.first().num)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,8 +27,8 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.device_metrics_log
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.device_metrics_log
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue