@@ -77,6 +77,7 @@ import androidx.compose.runtime.getValue
7777import androidx.compose.runtime.mutableStateMapOf
7878import androidx.compose.runtime.mutableStateOf
7979import androidx.compose.runtime.remember
80+ import androidx.compose.runtime.saveable.rememberSaveable
8081import androidx.compose.runtime.setValue
8182import androidx.compose.ui.Alignment
8283import androidx.compose.ui.Modifier
@@ -565,6 +566,30 @@ private fun CommandsTab(vm: WheelDiagnosticsViewModel) {
565566 }
566567}
567568
569+ /* *
570+ * Strip the family-display-name prefix from an inspect message-type string
571+ * so the dropdown label reads tightly. The family is already named in the
572+ * picker to the left, so "KingSong realtime" becomes "Realtime",
573+ * "InMotion V1 slow-info" becomes "Slow-info", and so on. Multi-prefix
574+ * families that don't share a common stem (InMotion V2: "V14 realtime",
575+ * "P6 realtime", "P6 detailed") fall through and render unchanged.
576+ */
577+ private fun shortInspectLabel (prefix : String , familyDisplayName : String ): String {
578+ val candidates = listOf (
579+ familyDisplayName,
580+ familyDisplayName.split(" / " ).first(),
581+ familyDisplayName.split(" " ).take(2 ).joinToString(" " ),
582+ familyDisplayName.split(" " ).first()
583+ ).filter { it.isNotBlank() }.distinct()
584+ for (c in candidates) {
585+ val trimmed = prefix.removePrefix(c).trimStart()
586+ if (trimmed != prefix && trimmed.isNotEmpty()) {
587+ return trimmed.replaceFirstChar { it.uppercase() }
588+ }
589+ }
590+ return prefix.replaceFirstChar { it.uppercase() }
591+ }
592+
568593/* *
569594 * Live byte interpreter. Picks the most recent NOTE entry whose text starts
570595 * with the selected message type prefix and renders every byte as a small
@@ -632,38 +657,36 @@ private fun InspectTab(vm: WheelDiagnosticsViewModel) {
632657 }
633658 }
634659 Spacer (Modifier .width(8 .dp))
635- // Single-prefix families (KingSong, Veteran, Begode, Ninebot, V1)
636- // just say "Realtime" — they only have one stream so the second
637- // dropdown would be a single-item menu. Multi-prefix families
638- // (InMotion V2: V14 / P6 realtime / P6 detailed) keep the picker.
639- if (types.size > 1 ) {
640- Box {
641- OutlinedButton (onClick = { menuExpanded = true }) {
642- Text (selected.ifEmpty { " (message)" })
643- Spacer (Modifier .width(6 .dp))
644- Icon (
645- imageVector = Icons .Default .KeyboardArrowDown ,
646- contentDescription = null
660+ // Always show the message-type dropdown; disable it when there's
661+ // only one option so the UI is consistent across families. The
662+ // label strips the family-name prefix (e.g. "KingSong realtime"
663+ // displays as "Realtime", "InMotion V1 slow-info" as "Slow-info")
664+ // since the family is already named in the picker to the left.
665+ val familyName = selectedFamily?.displayName ? : " "
666+ val singleOption = types.size <= 1
667+ Box {
668+ OutlinedButton (
669+ onClick = { if (! singleOption) menuExpanded = true },
670+ enabled = ! singleOption
671+ ) {
672+ Text (shortInspectLabel(selected, familyName).ifEmpty { " (message)" })
673+ Spacer (Modifier .width(6 .dp))
674+ Icon (
675+ imageVector = Icons .Default .KeyboardArrowDown ,
676+ contentDescription = null
677+ )
678+ }
679+ androidx.compose.material3.DropdownMenu (
680+ expanded = menuExpanded,
681+ onDismissRequest = { menuExpanded = false }
682+ ) {
683+ types.forEach { t ->
684+ androidx.compose.material3.DropdownMenuItem (
685+ text = { Text (shortInspectLabel(t, familyName)) },
686+ onClick = { selected = t; menuExpanded = false }
647687 )
648688 }
649- androidx.compose.material3.DropdownMenu (
650- expanded = menuExpanded,
651- onDismissRequest = { menuExpanded = false }
652- ) {
653- types.forEach { t ->
654- androidx.compose.material3.DropdownMenuItem (
655- text = { Text (t) },
656- onClick = { selected = t; menuExpanded = false }
657- )
658- }
659- }
660689 }
661- } else if (types.size == 1 ) {
662- Text (
663- " Realtime" ,
664- style = MaterialTheme .typography.labelMedium,
665- color = MaterialTheme .colorScheme.onSurfaceVariant
666- )
667690 }
668691 }
669692
@@ -788,46 +811,66 @@ private fun RawTab(vm: WheelDiagnosticsViewModel) {
788811 .verticalScroll(rememberScrollState())
789812 .padding(top = 4 .dp)
790813 ) {
814+ // Per-family preset library. Each family's chips draw from the
815+ // same DiagnosticCommand catalogue the Commands tab uses, so the
816+ // single source of truth holds and authoring a new family's
817+ // commands lights both tabs up at once. Tap a chip to seed the
818+ // input box (vs the Commands tab which fires immediately).
819+ val presetFamilies = remember { vm.allWheelFamilies().filter { it.commands.isNotEmpty() } }
820+ var presetFamilyIdx by rememberSaveable { mutableStateOf(0 ) }
821+ var presetMenuOpen by remember { mutableStateOf(false ) }
822+ val activePresetFamily = presetFamilies.getOrNull(presetFamilyIdx)
791823 CollapsibleSection (title = " Insert preset" , defaultExpanded = false ) {
792- Row (modifier = Modifier .horizontalScroll(rememberScrollState()).padding(top = 4 .dp)) {
793- val chips = listOf (
794- " 60 50 00 00" to " Light off" ,
795- " 60 50 01 01" to " Light on" ,
796- " 60 51 18 01" to " Horn" ,
797- " 60 2f 00" to " Auto-headlight off" ,
798- " 60 2f 01" to " Auto-headlight on" ,
799- " 60 4e 00" to " DRL? off" ,
800- " 60 4e 01" to " DRL? on" ,
801- " 60 24 00" to " 25 km/h clamp off" ,
802- " 60 24 01" to " 25 km/h clamp on" ,
803- " 60 31 01" to " Lock" ,
804- " 60 31 00" to " Unlock" ,
805- " 02 06" to " Info bundle" ,
806- " 02 07" to " Realtime" ,
807- " 20 20" to " Settings page A" ,
808- " 20 21" to " Settings B (untried)" ,
809- " 20 22" to " Settings C (untried)" ,
810- " 11" to " Total stats"
824+ if (activePresetFamily == null ) {
825+ Text (
826+ " No families publish presets yet." ,
827+ style = MaterialTheme .typography.bodySmall,
828+ color = MaterialTheme .colorScheme.onSurfaceVariant
811829 )
812- chips.forEach { (bytes, desc) ->
813- AssistChip (
814- onClick = { appendBytes(bytes) },
815- label = {
816- Column {
817- Text (
818- bytes,
819- style = MaterialTheme .typography.labelMedium
820- .copy(fontFamily = FontFamily .Monospace )
821- )
822- Text (
823- desc,
824- style = MaterialTheme .typography.labelSmall,
825- color = MaterialTheme .colorScheme.onSurfaceVariant
826- )
827- }
828- },
829- modifier = Modifier .padding(end = 6 .dp).height(56 .dp)
830- )
830+ } else {
831+ Box (modifier = Modifier .padding(top = 4 .dp, bottom = 6 .dp)) {
832+ OutlinedButton (onClick = { presetMenuOpen = true }) {
833+ Text (activePresetFamily.displayName)
834+ Spacer (Modifier .width(6 .dp))
835+ Icon (Icons .Default .KeyboardArrowDown , contentDescription = null )
836+ }
837+ androidx.compose.material3.DropdownMenu (
838+ expanded = presetMenuOpen,
839+ onDismissRequest = { presetMenuOpen = false }
840+ ) {
841+ presetFamilies.forEachIndexed { idx, fam ->
842+ androidx.compose.material3.DropdownMenuItem (
843+ text = { Text (fam.displayName) },
844+ onClick = { presetFamilyIdx = idx; presetMenuOpen = false }
845+ )
846+ }
847+ }
848+ }
849+ Row (modifier = Modifier .horizontalScroll(rememberScrollState())) {
850+ activePresetFamily.commands.forEach { cmd ->
851+ val hex = remember(cmd.bytes) {
852+ cmd.bytes.joinToString(" " ) { " %02x" .format(it) }
853+ }
854+ AssistChip (
855+ onClick = { appendBytes(hex) },
856+ label = {
857+ Column {
858+ Text (
859+ cmd.label,
860+ style = MaterialTheme .typography.labelMedium
861+ )
862+ Text (
863+ cmd.description,
864+ style = MaterialTheme .typography.labelSmall,
865+ color = MaterialTheme .colorScheme.onSurfaceVariant,
866+ maxLines = 1 ,
867+ overflow = TextOverflow .Ellipsis
868+ )
869+ }
870+ },
871+ modifier = Modifier .padding(end = 6 .dp).height(56 .dp)
872+ )
873+ }
831874 }
832875 }
833876 }
0 commit comments