App 2.22.0: Overview style Cockpit, segmented rings and a power trace

A third Overview style beside Tiles and Dashboard: rings for battery,
power (zero at the top, regeneration anticlockwise in green) with the
speed and gear inside, and 5-minute usage with a tick for the average
since start; below, the power of the last two minutes, the clock,
climate and odometer. Unread values light nothing and show "—"; the
trace leaves gaps (PowerTrace, unit-tested). Emulator only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-24 16:48:49 +02:00
co-authored by Claude Opus 5.5
parent 234a7e1b60
commit cd0dfcd612
11 changed files with 634 additions and 13 deletions
@@ -67,6 +67,7 @@ public class MainActivity extends AppCompatActivity {
private View logPane;
private View vehiclePane;
private View dashboardPane;
private View cockpitPane;
private View consumptionPane;
private View chargingPane;
/** Display, battery, charging price and Wi-Fi. */
@@ -244,6 +245,7 @@ public class MainActivity extends AppCompatActivity {
serverPane = inflatePane(R.layout.pane_server);
vehiclePane = inflatePane(R.layout.pane_vehicle);
dashboardPane = inflatePane(R.layout.pane_dashboard);
cockpitPane = inflatePane(R.layout.pane_cockpit);
// The Overview page holds both styles; Settings → Overview style picks the one shown.
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
@@ -251,6 +253,7 @@ public class MainActivity extends AppCompatActivity {
android.view.ViewGroup.LayoutParams.MATCH_PARENT));
overviewPage.addView(vehiclePane);
overviewPage.addView(dashboardPane);
overviewPage.addView(cockpitPane);
consumptionPane = inflatePane(R.layout.pane_consumption);
chargingPane = inflatePane(R.layout.pane_charging);
settingsPane = inflatePane(R.layout.pane_settings);
@@ -261,6 +264,7 @@ public class MainActivity extends AppCompatActivity {
UpdateLog.noteVersion(this, BuildConfig.VERSION_NAME);
bindVehiclePane();
bindDashboard();
bindCockpit();
bindConsumptionPane();
bindChargingPane();
bindServerPane();
@@ -1307,6 +1311,8 @@ public class MainActivity extends AppCompatActivity {
showEstimate(vehRangeAverage, vehRangeAverageNote, vehUsageAverage, vehUsageAverageNote,
average, getString(R.string.usage_last_5_min), R.string.range_waiting);
dashTripKm = trip == null ? null : trip.distanceKm;
cockpitTrip = trip;
cockpitAverage = average;
if (dashUsage != null) dashUsage.setText(average == null ? "—"
: String.format(java.util.Locale.US, "%.1f kWh/100 km", average.kwhPer100Km));
}
@@ -1427,22 +1433,30 @@ public class MainActivity extends AppCompatActivity {
/** Kilometres since start, from the range estimator; null until it has a distance. */
private Double dashTripKm;
private boolean dashboardStyle() {
return "DASHBOARD".equals(prefs.getString(PREF_OVERVIEW_STYLE, "TILES"));
/** The stored values of the Overview styles, in the order of the drop-down. */
private static final String[] OVERVIEW_STYLES = { "TILES", "DASHBOARD", "COCKPIT" };
/** The chosen style's index in {@link #OVERVIEW_STYLES}; anything unknown is Tiles. */
private int overviewStyle() {
String style = prefs.getString(PREF_OVERVIEW_STYLE, "TILES");
for (int i = 0; i < OVERVIEW_STYLES.length; i++) if (OVERVIEW_STYLES[i].equals(style)) return i;
return 0;
}
private void applyOverviewStyle() {
boolean dash = dashboardStyle();
dashboardPane.setVisibility(dash ? View.VISIBLE : View.GONE);
vehiclePane.setVisibility(dash ? View.GONE : View.VISIBLE);
int style = overviewStyle();
vehiclePane.setVisibility(style == 0 ? View.VISIBLE : View.GONE);
dashboardPane.setVisibility(style == 1 ? View.VISIBLE : View.GONE);
cockpitPane.setVisibility(style == 2 ? View.VISIBLE : View.GONE);
}
/** Tiles (the cards) or Dashboard (gauges); both are fed on every reading. */
/** Tiles (the cards), Dashboard (gauges) or Cockpit (rings); all are fed on every reading. */
private void bindOverviewStyle() {
String[] labels = { getString(R.string.overview_style_tiles), getString(R.string.overview_style_dashboard) };
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, dashboardStyle() ? 1 : 0,
String[] labels = { getString(R.string.overview_style_tiles), getString(R.string.overview_style_dashboard),
getString(R.string.overview_style_cockpit) };
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, overviewStyle(),
position -> {
prefs.edit().putString(PREF_OVERVIEW_STYLE, position == 1 ? "DASHBOARD" : "TILES").apply();
prefs.edit().putString(PREF_OVERVIEW_STYLE, OVERVIEW_STYLES[position]).apply();
applyOverviewStyle();
});
}
@@ -1461,9 +1475,69 @@ public class MainActivity extends AppCompatActivity {
dashBattery.configure(0f, 100f, 25f, 5, getString(R.string.dash_unit_battery));
dashBattery.setEndLabels("E", "F");
dashBattery.setWarnBelow(20f);
}
// ---------- Cockpit (Overview style) ----------
/** Power over the last two minutes, a slot per second. */
private final PowerTrace cockpitTrace = new PowerTrace(120_000, 120);
private RingView cockpitBattery, cockpitPower, cockpitUsage;
private TraceView cockpitTraceView;
private TextView cockpitClimate, cockpitOdometer;
/** From the range estimator: since start and the last 5 minutes; null until known. */
private RangeEstimator.Estimate cockpitTrip, cockpitAverage;
private void bindCockpit() {
cockpitBattery = cockpitPane.findViewById(R.id.cockpit_battery);
cockpitPower = cockpitPane.findViewById(R.id.cockpit_power);
cockpitUsage = cockpitPane.findViewById(R.id.cockpit_usage);
cockpitTraceView = cockpitPane.findViewById(R.id.cockpit_trace);
cockpitClimate = cockpitPane.findViewById(R.id.cockpit_climate);
cockpitOdometer = cockpitPane.findViewById(R.id.cockpit_odometer);
cockpitBattery.configure(0f, 100f, 40);
cockpitBattery.setWarnBelow(20f);
cockpitBattery.setBigScale(0.36f);
cockpitBattery.setScaleLabels("0", null, "100");
cockpitPower.setCentred(-50f, 150f, 60);
cockpitPower.setTopPill(true);
cockpitPower.setBigScale(0.44f);
cockpitPower.setScaleLabels("−50", "0", "150 kW");
cockpitUsage.configure(0f, 30f, 40);
cockpitUsage.setBigScale(0.36f);
cockpitUsage.setScaleLabels("0", null, "30");
applyOverviewStyle();
}
/** The rings and the strip; every value null-safe ("—", nothing lit, gaps in the trace). */
private void showCockpit(EnergySnapshot s) {
if (cockpitPower == null) return;
java.util.Locale us = java.util.Locale.US;
Float soc = s.getSocPercent();
cockpitBattery.setValue(soc, getString(R.string.cockpit_battery),
soc == null ? null : String.format(us, "%.0f", soc), "%",
fmt(s.getRangeKm(), "%.0f km"), null);
Float power = s.getBatteryPowerKw();
Float speed = s.getSpeedKmh();
Boolean parked = s.getParked();
cockpitPower.setValue(power, parked == null ? "—" : parked ? "P" : "D",
speed == null ? null : String.format(us, "%.0f", speed), getString(R.string.dash_unit_kmh),
power == null ? "—" : String.format(us, "%.1f kW", power), null);
cockpitTrace.add(android.os.SystemClock.elapsedRealtime(), power);
cockpitTraceView.setValues(cockpitTrace.snapshot());
RangeEstimator.Estimate now = cockpitAverage, trip = cockpitTrip;
cockpitUsage.setValue(now == null ? null : (float) now.kwhPer100Km, getString(R.string.cockpit_usage),
now == null ? null : String.format(us, "%.1f", now.kwhPer100Km), getString(R.string.cockpit_usage_unit),
trip == null ? getString(R.string.cockpit_usage_trip, "—")
: getString(R.string.cockpit_usage_trip, String.format(us, "%.1f", trip.kwhPer100Km)),
trip == null ? null : String.format(us, "%.1f km", trip.distanceKm));
cockpitUsage.setMarker(trip == null ? null : (float) trip.kwhPer100Km);
cockpitClimate.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
cockpitOdometer.setText(fmt(s.getOdometerKm(), "%,.0f km"));
}
/** The gauges and the strip; every value null-safe ("—", no needle). */
private void showDashboard(EnergySnapshot s) {
if (dashPower == null) return;
@@ -1504,6 +1578,7 @@ public class MainActivity extends AppCompatActivity {
vehTireRl.setText(unit.format(s.getTirePressures().getRearLeftKpa()));
vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa()));
showDashboard(s);
showCockpit(s);
} catch (Throwable ignored) {
// Leave the last good reading on screen.
}