Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Dec 10, 2024
764 changes: 764 additions & 0 deletions app/src/main/assets/device_hardware.json

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt
Original file line number Diff line number Diff line change
@@ -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
)
73 changes: 72 additions & 1 deletion app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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
}
}
}
81 changes: 69 additions & 12 deletions app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -64,13 +67,15 @@ 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
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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -541,6 +595,9 @@ private fun NodeDetailsPreview(
node: NodeEntity
) {
AppTheme {
NodeDetailList(node, MetricsState.Empty)
NodeDetailList(
node = node,
metricsState = MetricsState.Empty,
)
}
}
Loading
Loading