Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt

263 lines
8.9 KiB
Kotlin

package com.geeksville.mesh.ui
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.databinding.FragmentDebugBinding
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat
import java.util.Locale
@AndroidEntryPoint
class DebugFragment : Fragment() {
private var _binding: FragmentDebugBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val model: DebugViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDebugBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.clearButton.setOnClickListener {
model.deleteAllLogs()
}
binding.closeButton.setOnClickListener {
parentFragmentManager.popBackStack()
}
binding.debugListView.setContent {
val listState = rememberLazyListState()
val logs by model.meshLog.collectAsStateWithLifecycle()
LaunchedEffect(logs) {
if (listState.firstVisibleItemIndex < 3 && !listState.isScrollInProgress) {
listState.scrollToItem(0)
}
}
AppTheme {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
) {
items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) }
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* Transform the input [MeshLog] by enhancing the raw message with annotations.
*/
private fun annotateMeshLog(meshLog: MeshLog): MeshLog {
val annotated = when (meshLog.message_type) {
"Packet" -> {
meshLog.meshPacket?.let { packet ->
annotateRawMessage(meshLog.raw_message, packet.from, packet.to)
}
}
"NodeInfo" -> {
meshLog.nodeInfo?.let { nodeInfo ->
annotateRawMessage(meshLog.raw_message, nodeInfo.num)
}
}
"MyNodeInfo" -> {
meshLog.myNodeInfo?.let { nodeInfo ->
annotateRawMessage(meshLog.raw_message, nodeInfo.myNodeNum)
}
}
else -> null
}
return if (annotated == null) {
meshLog
} else {
meshLog.copy(raw_message = annotated)
}
}
/**
* Annotate the raw message string with the node IDs provided, in hex, if they are present.
*/
private fun annotateRawMessage(rawMessage: String, vararg nodeIds: Int): String {
val msg = StringBuilder(rawMessage)
var mutated = false
nodeIds.forEach { nodeId ->
mutated = mutated or msg.annotateNodeId(nodeId)
}
return if (mutated) {
return msg.toString()
} else {
rawMessage
}
}
/**
* Look for a single node ID integer in the string and annotate it with the hex equivalent
* if found.
*/
private fun StringBuilder.annotateNodeId(nodeId: Int): Boolean {
val nodeIdStr = nodeId.toUInt().toString()
indexOf(nodeIdStr).takeIf { it >= 0 }?.let { idx ->
insert(idx + nodeIdStr.length, " (${nodeId.asNodeId()})")
return true
}
return false
}
private fun Int.asNodeId(): String {
return "!%08x".format(Locale.getDefault(), this)
}
}
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
@Composable
internal fun DebugItem(log: MeshLog) {
val timeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
elevation = 4.dp,
shape = RoundedCornerShape(12.dp),
) {
Surface {
Column(
modifier = Modifier.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = log.message_type,
modifier = Modifier.weight(1f),
style = TextStyle(fontWeight = FontWeight.Bold),
)
Icon(
painterResource(R.drawable.cloud_download_outline_24),
contentDescription = null,
tint = Color.Gray.copy(alpha = 0.6f),
modifier = Modifier.padding(end = 8.dp),
)
Text(
text = timeFormat.format(log.received_date),
style = TextStyle(fontWeight = FontWeight.Bold),
)
}
val style = SpanStyle(
color = colorResource(id = R.color.colorAnnotation),
fontStyle = FontStyle.Italic,
)
val annotatedString = buildAnnotatedString {
append(log.raw_message)
REGEX_ANNOTATED_NODE_ID.findAll(log.raw_message).toList().reversed().forEach {
addStyle(style = style, start = it.range.first, end = it.range.last + 1)
}
}
Text(
text = annotatedString,
softWrap = false,
style = TextStyle(
fontSize = 9.sp,
fontFamily = FontFamily.Monospace,
)
)
}
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DebugScreenPreview() {
AppTheme {
DebugItem(
MeshLog(
uuid = "",
message_type = "NodeInfo",
received_date = 1601251258000L,
raw_message = "from: 2885173132\n" +
"decoded {\n" +
" position {\n" +
" altitude: 60\n" +
" battery_level: 81\n" +
" latitude_i: 411111136\n" +
" longitude_i: -711111805\n" +
" time: 1600390966\n" +
" }\n" +
"}\n" +
"hop_limit: 3\n" +
"id: 1737414295\n" +
"rx_snr: 9.5\n" +
"rx_time: 316400569\n" +
"to: -1409790708",
)
)
}
}