Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,27 @@ Gradle needs JDK 17+ (Crashlytics plugin); the shell default may be JDK 11 and f
so it must stay in `TRIP_INFO_STATUS_PIDS`. Any PID injected into queries the same way needs adding there too.
* The Trip Info PID dialog offers the full registry, but the defaults and the current selection are
added unfiltered: a selected PID hidden by the ECU/stable filters would be dropped on save.
* Top grid: labels drawn by `AbstractDrawer.drawTitle`, 6 columns (`MAX_ITEM_IN_THE_ROW`), row height `1.8 × textSizeBase` — labels have no width clamp, and a 3+ line label would overlap the next row. Above 18 items `TripInfoMetrics.grid()` shrinks text and adds columns within the same 3-row height (min scale 0.75 = 8 × 4 = 32 tiles; past that the last cell is "+N" for the undrawn ones). The floor was 0.5 (72 tiles) until "select all" on an 800 × 480 DHU gave ~4 px labels — readability, not capacity, sets the floor; up to 18 the layout is unchanged. `maxItemWidth` uses `layoutCache.grid.columns` — Performance calls `drawMetric` without `drawScreen`, so it keeps 6.
* Top grid: labels drawn by `AbstractDrawer.drawTitle`, 6 columns (`MAX_ITEM_IN_THE_ROW`), row height `1.8 × textSizeBase` — labels have no width clamp, and a 3+ line label would overlap the next row. Above 18 items `TripInfoMetrics.grid()` shrinks text and adds columns within the same 3-row height (min scale 0.75 = 8 × 4 = 32 tiles; past that the last cell is "+N" for the undrawn ones). The floor was 0.5 (72 tiles) until "select all" on an 800 × 480 DHU gave ~4 px labels — readability, not capacity, sets the floor; up to 18 the layout is unchanged. `maxItemWidth` uses `layoutCache.grid.columns`, which is never laid out for Performance (it calls `drawMetric` without `drawScreen`) — so Performance passes its own column width as `drawMetric(maxWidth = …)`; any other reuse of `drawMetric` must too.
* Bottom row: max 4 (`MAX_BOTTOM_ITEMS`, extras go to the grid), drawn via `GiuliaDrawer.drawMetric`; text size is computed once in `calculateLayout` and cached. Any input that changes label geometry (area, top/bottom metric counts, bottom PID ids, break-label flag) must be part of `TripInfoLayoutCache.requiresLayoutUpdate` — the non-area ones live in the testable `TripInfoLabelLayout`. A count is not enough once PIDs are user-chosen: swapping a bottom PID keeps the count and left the old text size. Label width must be measured the same way it is drawn (split on `\n` only when breaking is enabled).
* `TripInfoMetrics.grid()` takes base columns/rows so Performance shares it; `drawHiddenCount` is non-private for the same reason.
* AA label splitting is controlled by `pref.aa.trip_info.break_label` (default `true`), independent of Giulia virtual screens. Phone Trip Info always splits (`TripInfoSettings`).

### Performance screen (AA + phone)
* `renderer/performance/PerformanceSurfaceRenderer.kt` wraps settings in the internal `PerformanceScreenSettings` delegate (same name as the `api.PerformanceScreenSettings` data class — mind the imports). Its top grid reuses `TripInfoDrawer.drawMetric`; gauges use the gauge drawer.
* AA label splitting is controlled by `pref.aa.performance.break_label` (default `true`) via that delegate. Phone Performance always splits (`PerformanceSettings`).
* **Draws any selected PID, like Trip Info** (`PerformanceMetrics.plan()`, spec `doc/specs/performance-dynamic-pids.md`).
Profile lists `pref.query.performance.top`/`bottom`/`hidden` stay the backward-compatible layout: top
PIDs first in their order, other selected PIDs after in dialog order. Gauges come from
`pref.aa.performance.bottom.pids.selected` (no XML defaultValue; unset = profile `bottom`, empty = none,
max `PERFORMANCE_MAX_GAUGES = 5`). `VEHICLE_STATUS` is excluded via `PERFORMANCE_STATUS_PIDS`.
Saving the main Performance selection prunes deselected PIDs from a *stored* gauge set (never
creates one), else a gauge came back as a gauge on reselect.
* `MetricsCache` reads the profile lists from `Prefs` when it rebuilds the plan, not from listener-updated
copies: the renderer's listener only flags the plan outdated, and a copy updated by a second listener
could be read half-applied. Grid fit reuses `TripInfoMetrics.grid(count, 5, 3)` (unchanged up to 15).
* `MetricsCache` serves phone and AA alike, so the AA gauge pref and order keys also drive the phone
screen, which has no setting of its own. Gauges past `PERFORMANCE_MAX_GAUGES` fall back into the grid,
not nowhere; the gauge dialog only warns (toast), it does not block.

### Connectors (`:datalogger/.../connectors`)
* One `AdapterConnection` per transport, chosen by `ConnectionManager.obtain()` on
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ android {
resValue "string", "DEFAULT_PROFILE", "profile_8"
resValue "string", "applicationId", "org.obd.graphs.my.giulia.aa"
applicationId "org.obd.graphs.my.giulia.aa"
versionCode 246
versionCode 247
}

giuliaPerformanceMonitor {
dimension "version"
resValue "string", "DEFAULT_PROFILE", "profile_8"
resValue "string", "applicationId", "org.obd.graphs.my.giulia.performance_monitor"
applicationId "org.obd.graphs.my.giulia.performance_monitor"
versionCode 126
versionCode 127
}

giulia {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
Expand Down Expand Up @@ -421,6 +422,11 @@ open class PidDefinitionDialogFragment(
val btnSave = root.findViewById<Button>(R.id.pid_list_save)
btnSave.visibility = if (dialogMode.isInteractive) View.GONE else View.VISIBLE
btnSave.setOnClickListener {
val overflow = viewModel.gaugeOverflow()
if (overflow > 0) {
val message = getString(R.string.pref_aa_performance_bottom_pids_overflow, overflow)
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
}
viewModel.persistSelection()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sealed class PidDefinitionDialogMode {
object TripInfo : PidDefinitionDialogMode()
object TripInfoBottom : PidDefinitionDialogMode()
object Performance : PidDefinitionDialogMode()
object PerformanceBottom : PidDefinitionDialogMode()
object LowPriority : PidDefinitionDialogMode()
object HighPriority : PidDefinitionDialogMode()
object Dashboard : PidDefinitionDialogMode()
Expand All @@ -47,6 +48,7 @@ sealed class PidDefinitionDialogMode {
"trip_info" -> TripInfo
"trip_info_bottom" -> TripInfoBottom
"performance" -> Performance
"performance_bottom" -> PerformanceBottom
"low" -> LowPriority
"high" -> HighPriority
"dashboard" -> Dashboard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,20 @@ import org.obd.graphs.bl.datalogger.VehicleCapabilitiesManager
import org.obd.graphs.bl.datalogger.dataLoggerSettings
import org.obd.graphs.bl.datalogger.isUserCustom
import org.obd.graphs.bl.datalogger.serialize
import org.obd.graphs.bl.query.PERFORMANCE_STATUS_PIDS
import org.obd.graphs.bl.query.PREF_QUERY_PERFORMANCE_BOTTOM_SELECTED
import org.obd.graphs.bl.query.PREF_QUERY_PERFORMANCE_HIDDEN
import org.obd.graphs.bl.query.PREF_QUERY_PERFORMANCE_SELECTED
import org.obd.graphs.bl.query.PREF_QUERY_TRIP_INFO_SELECTED
import org.obd.graphs.bl.query.Query
import org.obd.graphs.bl.query.QueryStrategyType
import org.obd.graphs.bl.query.TRIP_INFO_STATUS_PIDS
import org.obd.graphs.bl.query.performanceBottomDialogSelection
import org.obd.graphs.bl.query.performanceGaugeOverflow
import org.obd.graphs.bl.query.prunedPerformanceBottomSelection
import org.obd.graphs.bl.query.tripInfoBottomDialogSelection
import org.obd.graphs.preferences.Prefs
import org.obd.graphs.preferences.getLongSet
import org.obd.graphs.preferences.getStringSet
import org.obd.graphs.preferences.updateStringSet
import org.obd.graphs.sendBroadcastEvent
Expand Down Expand Up @@ -194,23 +202,42 @@ class PidDefinitionViewModel(
Log.i(LOG_TAG, "Persisting PID list for key=$key, new list=$newList")
sendBroadcastEvent("$key.event.changed")
Prefs.updateStringSet(key, newList)
if (dialogMode is PidDefinitionDialogMode.Performance) {
pruneGaugeSelection(newList.map { it.toLong() }.toSet())
}
} else {
Log.i(LOG_TAG, "Do not persist PID list for key=$key, it did not change")
}
}
}

// The bottom row dialog lists only the selected Trip Info PIDs; see tripInfoBottomDialogSelection.
// Checked PIDs the gauge row has no room for; they are drawn in the grid instead.
fun gaugeOverflow(): Int =
if (dialogMode is PidDefinitionDialogMode.PerformanceBottom) performanceGaugeOverflow(allMasterItems.count { it.checked }) else 0

private fun pruneGaugeSelection(selected: Set<Long>) {
val stored =
if (Prefs.contains(PREF_QUERY_PERFORMANCE_BOTTOM_SELECTED)) {
Prefs.getStringSet(PREF_QUERY_PERFORMANCE_BOTTOM_SELECTED).map { it.toLong() }.toSet()
} else {
null
}
prunedPerformanceBottomSelection(stored, selected)?.let { pruned ->
Log.i(LOG_TAG, "Dropping deselected PIDs from the gauges, new list=$pruned")
Prefs.updateStringSet(PREF_QUERY_PERFORMANCE_BOTTOM_SELECTED, pruned.map { it.toString() })
}
}

// The bottom row dialogs list only the PIDs selected for their screen; see tripInfoBottomDialogSelection.
private fun persistedSelection(listed: Collection<Long>): Collection<Long> =
if (dialogMode is PidDefinitionDialogMode.TripInfoBottom) {
tripInfoBottomDialogSelection(
persisted = if (Prefs.contains(key)) Prefs.getStringSet(key).map { s -> s.toLong() }.toSet() else null,
listed = listed
)
} else {
Prefs.getStringSet(key).map { s -> s.toLong() }
when (dialogMode) {
is PidDefinitionDialogMode.TripInfoBottom -> tripInfoBottomDialogSelection(storedOrNull(), listed)
is PidDefinitionDialogMode.PerformanceBottom -> performanceBottomDialogSelection(storedOrNull(), listed)
else -> Prefs.getStringSet(key).map { s -> s.toLong() }
}

private fun storedOrNull(): Set<Long>? = if (Prefs.contains(key)) Prefs.getStringSet(key).map { s -> s.toLong() }.toSet() else null

fun reorderItems(items: List<PidDefinitionDetails>) {
viewModelScope.launch(Dispatchers.IO) {
val serializer = viewPreferencesSerializer()
Expand Down Expand Up @@ -274,7 +301,20 @@ class PidDefinitionViewModel(
.map { PidDefinitionDetails(it, checked = false, supported = true) }
}
dialogMode is PidDefinitionDialogMode.Performance -> {
Query.instance(QueryStrategyType.PERFORMANCE_QUERY).getDefaultPIDs()
// Same as Trip Info: the profile's PIDs and the current selection bypass the registry filters.
val pinned = (
Query.instance(QueryStrategyType.PERFORMANCE_QUERY).getDefaultPIDs() +
Prefs.getStringSet(key).map { s -> s.toLong() }
).mapNotNull { pidRegistry.findBy(it) }
.map { PidDefinitionDetails(it, checked = false, supported = true) }
val pinnedIds = pinned.map { it.source.id }.toSet()
pinned + findPidDefinitionByPriority(all) { !pinnedIds.contains(it.id) }
}
dialogMode is PidDefinitionDialogMode.PerformanceBottom -> {
val hidden = Prefs.getLongSet(PREF_QUERY_PERFORMANCE_HIDDEN)
Prefs.getStringSet(PREF_QUERY_PERFORMANCE_SELECTED)
.map { it.toLong() }
.filter { !PERFORMANCE_STATUS_PIDS.contains(it) && !hidden.contains(it) }
.mapNotNull { pidRegistry.findBy(it) }
.map { PidDefinitionDetails(it, checked = false, supported = true) }
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@
<string name="pref.aa.performance.enabled_summary">Aktywuje ekran wydajności, który wyświetla predefiniowany zestaw metryk dostosowany do silników 2.0 GME.</string>
<string name="pref.aa.performance.break_label">Podziel etykiety PID na 2 linie</string>
<string name="pref.aa.performance.break_label_summary">Wyświetla etykiety PID na ekranie wydajności w dwóch liniach. Po wyłączeniu etykiety są wyświetlane w jednej linii.</string>
<string name="pref.aa.performance.bottom_pids">PID wskaźników</string>
<string name="pref.aa.performance.bottom_pids_summary">PID wyświetlane jako wskaźniki w dolnej części ekranu wydajności (maks. 5), wybierane spośród zaznaczonych PID. Pozostałe zaznaczone PID są wyświetlane w górnej siatce.</string>
<string name="pref.aa.performance.bottom_pids_overflow">Mieści się tylko 5 wskaźników; nadmiarowe (%1$d) zostaną wyświetlone w górnej siatce.</string>
<string name="pref.aa.performance.select_pids">Wybierz OBD2 PID wydajności</string>


Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@
<string name="pref.aa.performance.enabled_summary">Activates the Performance screen, which displays a predefined set of metrics tailored for 2.0 GME engines.</string>
<string name="pref.aa.performance.break_label">Split PIDs label into 2 lines</string>
<string name="pref.aa.performance.break_label_summary">Displays PID labels on the Performance screen in two lines. When disabled, labels are shown in a single line.</string>
<string name="pref.aa.performance.bottom_pids">Gauge PIDs</string>
<string name="pref.aa.performance.bottom_pids_summary">PIDs shown as gauges at the bottom of the Performance screen (max 5), chosen from the selected PIDs. All other selected PIDs are shown in the top grid.</string>
<string name="pref.aa.performance.bottom_pids_overflow">Only 5 gauges fit; %1$d more will be shown in the top grid.</string>
<string name="pref.aa.performance.select_pids">Select Performance OBD2 PIDs</string>


Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,15 @@
android:title="@string/pref.displayed_parameter_ids"
app:source="performance"
app:useSimpleSummaryProvider="true" />

<!-- No defaultValue: an unset key means the profile's gauges. -->
<org.obd.graphs.preferences.pid.PidDefinitionListPreferences
android:dialogTitle="@string/pref.aa.performance.bottom_pids"
android:key="pref.aa.performance.bottom.pids.selected"
android:summary="@string/pref.aa.performance.bottom_pids_summary"
android:title="@string/pref.aa.performance.bottom_pids"
app:source="performance_bottom"
app:singleLineTitle="false" />
</PreferenceCategory>
</PreferenceScreen>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const val GAUGE_VIRTUAL_SCREEN_4_SETTINGS_CHANGED = "pref.aa.gauge.pids.profile_
private const val AA_TRIP_INFO_PID_SELECTION_CHANGED_EVENT = "pref.aa.trip_info.pids.selected.event.changed"
private const val AA_TRIP_INFO_BOTTOM_PID_SELECTION_CHANGED_EVENT = "pref.aa.trip_info.bottom.pids.selected.event.changed"
private const val AA_PERFORMANCE_PID_SELECTION_CHANGED_EVENT = "pref.aa.performance.pids.selected.event.changed"
private const val AA_PERFORMANCE_BOTTOM_PID_SELECTION_CHANGED_EVENT = "pref.aa.performance.bottom.pids.selected.event.changed"

private enum class DefaultScreen(
private val code: Int
Expand Down Expand Up @@ -126,6 +127,7 @@ internal class SurfaceRendererScreen(
AA_TRIP_INFO_PID_SELECTION_CHANGED_EVENT,
AA_TRIP_INFO_BOTTOM_PID_SELECTION_CHANGED_EVENT,
AA_PERFORMANCE_PID_SELECTION_CHANGED_EVENT,
AA_PERFORMANCE_BOTTOM_PID_SELECTION_CHANGED_EVENT,
AA_HIGH_FREQ_PID_SELECTION_CHANGED_EVENT,
LOW_FREQ_PID_SELECTION_CHANGED_EVENT
-> {
Expand Down Expand Up @@ -283,6 +285,7 @@ internal class SurfaceRendererScreen(
it.addAction(AA_TRIP_INFO_PID_SELECTION_CHANGED_EVENT)
it.addAction(AA_TRIP_INFO_BOTTOM_PID_SELECTION_CHANGED_EVENT)
it.addAction(AA_PERFORMANCE_PID_SELECTION_CHANGED_EVENT)
it.addAction(AA_PERFORMANCE_BOTTOM_PID_SELECTION_CHANGED_EVENT)

it.addAction(GAUGE_VIRTUAL_SCREEN_1_SETTINGS_CHANGED)
it.addAction(GAUGE_VIRTUAL_SCREEN_2_SETTINGS_CHANGED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,51 @@
package org.obd.graphs.bl.query

import android.util.Log
import org.obd.graphs.bl.datalogger.Pid
import org.obd.graphs.preferences.Prefs
import org.obd.graphs.preferences.getLongList
import org.obd.graphs.preferences.getLongSet

private const val PERFORMANCE_QUERY_PREF_KEY = "pref.aa.performance.pids.selected"
const val PREF_QUERY_PERFORMANCE_SELECTED = "pref.aa.performance.pids.selected"

// The profile's layout: grid PIDs, gauge PIDs and PIDs never drawn.
const val PREF_QUERY_PERFORMANCE_TOP = "pref.query.performance.top"
const val PREF_QUERY_PERFORMANCE_BOTTOM = "pref.query.performance.bottom"
const val PREF_QUERY_PERFORMANCE_HIDDEN = "pref.query.performance.hidden"

// Which of the selected PIDs are drawn as gauges. Unset means the profile's PREF_QUERY_PERFORMANCE_BOTTOM;
// an empty set means the user removed them all.
const val PREF_QUERY_PERFORMANCE_BOTTOM_SELECTED = "pref.aa.performance.bottom.pids.selected"

// Added to every query by QueryStrategyOrchestrator when the status panel is on, never drawn.
val PERFORMANCE_STATUS_PIDS = setOf(Pid.VEHICLE_STATUS_PID_ID.id)

// Profiles define at most 5 gauges, so the cap leaves every existing layout as it was.
const val PERFORMANCE_MAX_GAUGES = 5

/** How many of [selectedCount] gauges do not fit and fall back into the grid. */
fun performanceGaugeOverflow(selectedCount: Int): Int = (selectedCount - PERFORMANCE_MAX_GAUGES).coerceAtLeast(0)

/**
* The stored gauge selection limited to the PIDs still selected for the screen, or null when
* nothing has to be written: the gauges were never set (the profile's apply) or none was dropped.
* Without it a deselected gauge PID stayed stored and came back as a gauge once selected again.
*/
fun prunedPerformanceBottomSelection(
stored: Set<Long>?,
selected: Set<Long>
): Set<Long>? = stored?.filter { selected.contains(it) }?.toSet()?.takeIf { it.size != stored.size }

/**
* The gauges as the gauge dialog shows them checked: the stored selection, or the profile's
* gauges when never set, limited to the PIDs the dialog lists. See [tripInfoBottomDialogSelection].
*
* @param persisted the gauge pref, or null when never set.
*/
fun performanceBottomDialogSelection(
persisted: Set<Long>?,
listed: Collection<Long>
): Set<Long> = (persisted ?: Prefs.getLongList(PREF_QUERY_PERFORMANCE_BOTTOM)).filter { listed.contains(it) }.toSet()

const val PREF_QUERY_PERFORMANCE_BRAKE_BOOSTING_GAS_METRIC =
"pref.query.performance.break_boosting.gas_pid"
Expand All @@ -40,7 +79,7 @@ internal class PerformanceQueryStrategy : QueryStrategy() {
Prefs.getInt(PREF_QUERY_PERFORMANCE_BRAKE_BOOSTING_ARBITRARY_METRIC, -1).toLong() +
Prefs.getInt(PREF_QUERY_PERFORMANCE_BRAKE_BOOSTING_VEHICLE_SPEED_METRIC, -1).toLong()

override fun getPIDs() = Prefs.getLongSet(PERFORMANCE_QUERY_PREF_KEY)
override fun getPIDs() = Prefs.getLongSet(PREF_QUERY_PERFORMANCE_SELECTED)

init {
Log.i("PerformanceQueryStrategy", "Read defaults=${getDefaultPIDs()}")
Expand Down
Loading
Loading