mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
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:
parent
f08916764c
commit
993f659742
43 changed files with 36489 additions and 13 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue