feat: add support for Device Hardware json and svg graphics (#1449)

* feat: add support for Device Hardware json and svg graphics

Allows for better hardware device display names, graphics, and indication of support.

* make detekt happy

* Fix: Use first image name to find vector drawable

Use the first image name from the `images` list (after removing the ".svg" suffix) to find the corresponding vector drawable resource.

* Refactor: Update device detail layout

Updated the device detail layout to group device-specific information under a "Device" category.
Added a circular background with device-specific color behind the device icon.
Moved hardware, support status details to the Device section.

* Refactor: Move device hardware logic to MetricsViewModel

Moves the logic for retrieving device hardware information and image resources from NodeDetail to MetricsViewModel.

Also replaces id lookup with when statement for image resource id mapping.

* fix: cache deviceHardwareList, add exception handling

* refactor: mutable list unnecessary

* default to hw_unknown device image
This commit is contained in:
James Rich 2024-12-10 09:02:57 -06:00 committed by GitHub
parent f08916764c
commit 993f659742
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 36489 additions and 13 deletions

View file

@ -0,0 +1,17 @@
package com.geeksville.mesh.model
import kotlinx.serialization.Serializable
@Serializable
data class DeviceHardware(
val hwModel: Int,
val hwModelSlug: String,
val platformioTarget: String,
val architecture: String,
val activelySupported: Boolean,
val supportLevel: Int? = null,
val displayName: String,
val tags: List<String>? = listOf(),
val images: List<String>? = listOf(),
val requiresDfu: Boolean? = null
)

View file

@ -20,6 +20,7 @@ package com.geeksville.mesh.model
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -29,6 +30,7 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos.HardwareModel
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums.PortNum
@ -56,9 +58,11 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -75,6 +79,8 @@ data class MetricsState(
val tracerouteRequests: List<MeshLog> = emptyList(),
val tracerouteResults: List<MeshPacket> = emptyList(),
val positionLogs: List<Position> = emptyList(),
val deviceHardware: DeviceHardware? = null,
@DrawableRes val deviceImageRes: Int = R.drawable.hw_unknown,
) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
@ -212,7 +218,17 @@ class MetricsViewModel @Inject constructor(
radioConfigRepository.nodeDBbyNum
.mapLatest { nodes -> nodes[destNum] }
.distinctUntilChanged()
.onEach { node -> _state.update { state -> state.copy(node = node) } }
.onEach { node ->
_state.update { state -> state.copy(node = node) }
node?.user?.hwModel?.let { hwModel ->
_state.update { state ->
state.copy(
deviceHardware = getDeviceHardwareFromHardwareModel(hwModel),
deviceImageRes = getDeviceVectorImageFromHardwareModel(hwModel)
)
}
}
}
.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach { profile ->
@ -316,4 +332,59 @@ class MetricsViewModel @Inject constructor(
errormsg("Can't write file error: ${ex.message}")
}
}
private var deviceHardwareList: List<DeviceHardware> = listOf()
private fun getDeviceHardwareFromHardwareModel(
hwModel: HardwareModel
): DeviceHardware? {
if (deviceHardwareList.isEmpty()) {
try {
val json =
app.assets.open("device_hardware.json").bufferedReader().use { it.readText() }
deviceHardwareList = Json.decodeFromString<List<DeviceHardware>>(json)
} catch (ex: IOException) {
errormsg("Can't read device_hardware.json error: ${ex.message}")
}
}
return deviceHardwareList.find { it.hwModel == hwModel.number }
}
@Suppress("CyclomaticComplexMethod")
private fun getDeviceVectorImageFromHardwareModel(hwModel: HardwareModel): Int {
return when (hwModel) {
HardwareModel.DIY_V1 -> R.drawable.hw_diy
HardwareModel.HELTEC_HT62 -> R.drawable.hw_heltec_ht62_esp32c3_sx1262
HardwareModel.HELTEC_MESH_NODE_T114 -> R.drawable.hw_heltec_mesh_node_t114
HardwareModel.HELTEC_V3 -> R.drawable.hw_heltec_v3
HardwareModel.HELTEC_VISION_MASTER_E213 -> R.drawable.hw_heltec_vision_master_e213
HardwareModel.HELTEC_VISION_MASTER_E290 -> R.drawable.hw_heltec_vision_master_e290
HardwareModel.HELTEC_VISION_MASTER_T190 -> R.drawable.hw_heltec_vision_master_t190
HardwareModel.HELTEC_WIRELESS_PAPER -> R.drawable.hw_heltec_wireless_paper
HardwareModel.HELTEC_WIRELESS_TRACKER -> R.drawable.hw_heltec_wireless_tracker
HardwareModel.HELTEC_WIRELESS_TRACKER_V1_0 -> R.drawable.hw_heltec_wireless_tracker_v1_0
HardwareModel.HELTEC_WSL_V3 -> R.drawable.hw_heltec_wsl_v3
HardwareModel.NANO_G2_ULTRA -> R.drawable.hw_nano_g2_ultra
HardwareModel.RPI_PICO -> R.drawable.hw_pico
HardwareModel.NRF52_PROMICRO_DIY -> R.drawable.hw_promicro
HardwareModel.RAK11310 -> R.drawable.hw_rak11310
HardwareModel.RAK4631 -> R.drawable.hw_rak4631
HardwareModel.RPI_PICO2 -> R.drawable.hw_rpipicow
HardwareModel.SENSECAP_INDICATOR -> R.drawable.hw_seeed_sensecap_indicator
HardwareModel.SEEED_XIAO_S3 -> R.drawable.hw_seeed_xiao_s3
HardwareModel.STATION_G2 -> R.drawable.hw_station_g2
HardwareModel.T_DECK -> R.drawable.hw_t_deck
HardwareModel.T_ECHO -> R.drawable.hw_t_echo
HardwareModel.T_WATCH_S3 -> R.drawable.hw_t_watch_s3
HardwareModel.TBEAM -> R.drawable.hw_tbeam
HardwareModel.LILYGO_TBEAM_S3_CORE -> R.drawable.hw_tbeam_s3_core
HardwareModel.TLORA_C6 -> R.drawable.hw_tlora_c6
HardwareModel.TLORA_T3_S3 -> R.drawable.hw_tlora_t3s3_v1
HardwareModel.TLORA_V2_1_1P6 -> R.drawable.hw_tlora_v2_1_1_6
HardwareModel.TLORA_V2_1_1P8 -> R.drawable.hw_tlora_v2_1_1_8
HardwareModel.TRACKER_T1000_E -> R.drawable.hw_tracker_t1000_e
HardwareModel.WIO_WM1110 -> R.drawable.hw_wio_tracker_wm1110
HardwareModel.WISMESH_TAP -> R.drawable.hw_rak_wismeshtap
else -> R.drawable.hw_unknown
}
}
}

View file

@ -15,10 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions")
@file:Suppress("TooManyFunctions", "LongMethod")
package com.geeksville.mesh.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -36,6 +38,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
@ -64,6 +67,7 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Thermostat
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material.icons.filled.Work
import androidx.compose.material.icons.outlined.Navigation
@ -71,6 +75,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
@ -98,8 +103,8 @@ import kotlin.math.ln
@Composable
fun NodeDetailScreen(
viewModel: MetricsViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
viewModel: MetricsViewModel = hiltViewModel(),
onNavigate: (Any) -> Unit,
) {
val state by viewModel.state.collectAsStateWithLifecycle()
@ -124,15 +129,20 @@ fun NodeDetailScreen(
@Composable
private fun NodeDetailList(
modifier: Modifier = Modifier,
node: NodeEntity,
metricsState: MetricsState,
modifier: Modifier = Modifier,
onNavigate: (Any) -> Unit = {},
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp),
) {
item {
PreferenceCategory("Device") {
DeviceDetailsContent(metricsState)
}
}
item {
PreferenceCategory("Details") {
NodeDetailsContent(node)
@ -176,7 +186,12 @@ private fun NodeDetailList(
}
@Composable
private fun NodeDetailRow(label: String, icon: ImageVector, value: String) {
private fun NodeDetailRow(
label: String,
icon: ImageVector,
value: String,
iconTint: Color = MaterialTheme.colors.onSurface
) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -186,7 +201,8 @@ private fun NodeDetailRow(label: String, icon: ImageVector, value: String) {
Icon(
imageVector = icon,
contentDescription = label,
modifier = Modifier.size(24.dp)
modifier = Modifier.size(24.dp),
tint = iconTint
)
Spacer(modifier = Modifier.width(8.dp))
Text(label)
@ -196,7 +212,50 @@ private fun NodeDetailRow(label: String, icon: ImageVector, value: String) {
}
@Composable
private fun NodeDetailsContent(node: NodeEntity) {
private fun DeviceDetailsContent(
state: MetricsState,
) {
val node = state.node ?: return
val deviceHardware = state.deviceHardware ?: return
val deviceImageRes = state.deviceImageRes
val hwModelName = deviceHardware.displayName
val isSupported = deviceHardware.activelySupported
Box(
modifier = Modifier
.size(100.dp)
.padding(4.dp)
.clip(CircleShape)
.background(
color = Color(node.colors.second).copy(alpha = .5f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.padding(16.dp),
imageVector = ImageVector.vectorResource(deviceImageRes),
contentDescription = hwModelName,
)
}
NodeDetailRow(
label = "Hardware",
icon = Icons.Default.Router,
value = hwModelName
)
if (isSupported) {
NodeDetailRow(
label = "Supported",
icon = Icons.Default.Verified,
value = "",
iconTint = Color.Green
)
}
}
@Composable
private fun NodeDetailsContent(
node: NodeEntity,
) {
if (node.mismatchKey) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
@ -232,11 +291,6 @@ private fun NodeDetailsContent(node: NodeEntity) {
icon = Icons.Default.Work,
value = node.user.role.name
)
NodeDetailRow(
label = "Hardware",
icon = Icons.Default.Router,
value = node.user.hwModel.name
)
if (node.deviceMetrics.uptimeSeconds > 0) {
NodeDetailRow(
label = "Uptime",
@ -541,6 +595,9 @@ private fun NodeDetailsPreview(
node: NodeEntity
) {
AppTheme {
NodeDetailList(node, MetricsState.Empty)
NodeDetailList(
node = node,
metricsState = MetricsState.Empty,
)
}
}