App 2.12.1: no scroll bars; energy since start

The pages' scroll bars never faded on the car: a ScrollView shows its
bar on every layout, and the live values lay the page out every two
seconds. StableScrollView, the root of every page, now has none.

A new "Energy since start" tile, left of "Usage since start", shows the
net kWh taken from the pack since the trip began, recuperation counted
back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 13:12:12 +02:00
co-authored by Claude Opus 5.5
parent d711e0cfda
commit 66fed48aae
9 changed files with 56 additions and 6 deletions
+3
View File
@@ -113,6 +113,9 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in
the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside
`kubectl apply -f k8s/`). Filled-in secrets live only there from now on. `kubectl apply -f k8s/`). Filled-in secrets live only there from now on.
- 2026-10-18 — App 2.12.1: no scroll bars (the 2-second refresh kept them from ever fading);
"Energy since start" tile (net kWh of the trip, recuperation counted back) left of "Usage since
start".
- 2026-10-18 — App 2.12.0, server 2.5.0: settings entered on a phone. "Enter on phone" shows a QR - 2026-10-18 — App 2.12.0, server 2.5.0: settings entered on a phone. "Enter on phone" shows a QR
code; the phone form on the server (approval password) takes the ABRP credentials, charging code; the phone form on the server (approval password) takes the ABRP credentials, charging
price and usable capacity, and the car collects them once (RFC 8628-style, like pairing; values price and usable capacity, and the car collects them once (RFC 8628-style, like pairing; values
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4" applicationId "com.stinnesbeck.nilsmg4"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 75 versionCode 76
versionName "2.12.0" versionName "2.12.1"
// Nothing about the server ships in the APK: its URL and ingest token are entered in // Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server. // the app's Server tab (ServerConfig), and the database is only reachable by the server.
@@ -92,7 +92,8 @@ public class MainActivity extends AppCompatActivity {
vehCharging, vehHvac, vehCharging, vehHvac,
vehTireFl, vehTireFr, vehTireRl, vehTireRr, vehTireFl, vehTireFr, vehTireRl, vehTireRr,
vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote, vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote,
vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote; vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote,
vehEnergyTrip, vehEnergyTripNote;
/** Usable pack capacity for the range estimates; the car does not publish it. */ /** Usable pack capacity for the range estimates; the car does not publish it. */
static final String USABLE_KWH_KEY = "usable_kwh"; static final String USABLE_KWH_KEY = "usable_kwh";
@@ -1187,6 +1188,8 @@ public class MainActivity extends AppCompatActivity {
vehUsageTripNote = vehiclePane.findViewById(R.id.vehicle_usage_trip_note); vehUsageTripNote = vehiclePane.findViewById(R.id.vehicle_usage_trip_note);
vehUsageAverage = vehiclePane.findViewById(R.id.vehicle_usage_average); vehUsageAverage = vehiclePane.findViewById(R.id.vehicle_usage_average);
vehUsageAverageNote = vehiclePane.findViewById(R.id.vehicle_usage_average_note); vehUsageAverageNote = vehiclePane.findViewById(R.id.vehicle_usage_average_note);
vehEnergyTrip = vehiclePane.findViewById(R.id.vehicle_energy_trip);
vehEnergyTripNote = vehiclePane.findViewById(R.id.vehicle_energy_trip_note);
} }
private double usableKwh() { private double usableKwh() {
@@ -1204,6 +1207,9 @@ public class MainActivity extends AppCompatActivity {
estimator.add(monotonicMs, s.getBatteryPowerKw(), s.getSpeedKmh()); estimator.add(monotonicMs, s.getBatteryPowerKw(), s.getSpeedKmh());
Double remaining = RangeEstimator.remainingKwh(s.getSocPercent(), usableKwh()); Double remaining = RangeEstimator.remainingKwh(s.getSocPercent(), usableKwh());
RangeEstimator.Estimate trip = estimator.sinceStart(remaining); RangeEstimator.Estimate trip = estimator.sinceStart(remaining);
Double energy = estimator.energySinceStart();
vehEnergyTrip.setText(energy == null ? "—" : getString(R.string.energy_kwh, energy));
vehEnergyTripNote.setText(energy == null ? R.string.energy_trip_waiting : R.string.energy_trip_note);
showEstimate(vehRangeTrip, vehRangeTripNote, vehUsageTrip, vehUsageTripNote, trip, showEstimate(vehRangeTrip, vehRangeTripNote, vehUsageTrip, vehUsageTripNote, trip,
trip == null ? "" : getString(R.string.usage_over_distance, trip.distanceKm), trip == null ? "" : getString(R.string.usage_over_distance, trip.distanceKm),
R.string.range_trip_waiting); R.string.range_trip_waiting);
@@ -110,6 +110,11 @@ final class RangeEstimator {
tripLast = s; tripLast = s;
} }
/** Net energy taken from the pack since start, in kWh; null before the trip's first sample. */
synchronized Double energySinceStart() {
return tripLast == null ? null : tripEnergyKwh;
}
/** Range at the consumption since start; null until the trip has some distance behind it. */ /** Range at the consumption since start; null until the trip has some distance behind it. */
synchronized Estimate sinceStart(Double remainingKwh) { synchronized Estimate sinceStart(Double remainingKwh) {
if (remainingKwh == null || tripDistanceKm < MIN_DISTANCE_KM) return null; if (remainingKwh == null || tripDistanceKm < MIN_DISTANCE_KM) return null;
@@ -16,18 +16,28 @@ import android.widget.ScrollView;
* Here only scrolling that is not part of a layout counts as the driver's position, and every * Here only scrolling that is not part of a layout counts as the driver's position, and every
* layout restores it. ScrollView.scrollTo clamps against the real content height, so a * layout restores it. ScrollView.scrollTo clamps against the real content height, so a
* position that no longer exists still ends at the bottom rather than past it. * position that no longer exists still ends at the bottom rather than past it.
*
* No scroll bar: a ScrollView shows its bar on every layout, and the live values lay the page
* out every two seconds, so on the car it never faded. The pages scroll by touch.
*/ */
public class StableScrollView extends ScrollView { public class StableScrollView extends ScrollView {
private int wantedScrollY; private int wantedScrollY;
private boolean inLayout; private boolean inLayout;
public StableScrollView(Context context) { super(context); } public StableScrollView(Context context) {
super(context);
setVerticalScrollBarEnabled(false);
}
public StableScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public StableScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
setVerticalScrollBarEnabled(false);
}
public StableScrollView(Context context, AttributeSet attrs, int defStyleAttr) { public StableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
setVerticalScrollBarEnabled(false);
} }
@Override @Override
@@ -32,7 +32,11 @@
</LinearLayout> </LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:orientation="horizontal" android:baselineAligned="false"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:orientation="horizontal" android:baselineAligned="false">
<Space android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" android:layout_marginEnd="10dp" /> <Space android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" android:layout_marginEnd="10dp" />
<Space android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" android:layout_marginEnd="10dp" /> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginEnd="10dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_energy_trip" android:textAppearance="@style/Text.EV.Secondary" />
<TextView android:id="@+id/vehicle_energy_trip" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
<TextView android:id="@+id/vehicle_energy_trip_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
</LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginEnd="10dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass"> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginEnd="10dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_usage_trip" android:textAppearance="@style/Text.EV.Secondary" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_usage_trip" android:textAppearance="@style/Text.EV.Secondary" />
<TextView android:id="@+id/vehicle_usage_trip" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" /> <TextView android:id="@+id/vehicle_usage_trip" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
+5
View File
@@ -21,6 +21,11 @@
<TextView android:id="@+id/vehicle_range_average" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" /> <TextView android:id="@+id/vehicle_range_average" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
<TextView android:id="@+id/vehicle_range_average_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" /> <TextView android:id="@+id/vehicle_range_average_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
</LinearLayout> </LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_columnWeight="1" android:layout_margin="4dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_energy_trip" android:textAppearance="@style/Text.EV.Secondary" />
<TextView android:id="@+id/vehicle_energy_trip" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
<TextView android:id="@+id/vehicle_energy_trip_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
</LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_columnWeight="1" android:layout_margin="4dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass"> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_columnWeight="1" android:layout_margin="4dp" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_usage_trip" android:textAppearance="@style/Text.EV.Secondary" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_usage_trip" android:textAppearance="@style/Text.EV.Secondary" />
<TextView android:id="@+id/vehicle_usage_trip" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" /> <TextView android:id="@+id/vehicle_usage_trip" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
+4
View File
@@ -34,6 +34,10 @@
<string name="vehicle_row_range_trip">Range since start</string> <string name="vehicle_row_range_trip">Range since start</string>
<string name="vehicle_row_range_average">Range at 5-min average</string> <string name="vehicle_row_range_average">Range at 5-min average</string>
<string name="vehicle_row_usage_trip">Usage since start</string> <string name="vehicle_row_usage_trip">Usage since start</string>
<string name="vehicle_row_energy_trip">Energy since start</string>
<string name="energy_kwh">%1$.1f kWh</string>
<string name="energy_trip_note">net, after recuperation</string>
<string name="energy_trip_waiting">once the car is on</string>
<string name="vehicle_row_usage_average">Usage, 5-min average</string> <string name="vehicle_row_usage_average">Usage, 5-min average</string>
<string name="range_consumption">%1$.1f kWh/100 km</string> <string name="range_consumption">%1$.1f kWh/100 km</string>
<string name="range_over">&gt; %1$d km</string> <string name="range_over">&gt; %1$d km</string>
@@ -115,4 +115,17 @@ public class RangeEstimatorTest {
assertEquals(-6.25, regen.kwhPer100Km, 0.01); assertEquals(-6.25, regen.kwhPer100Km, 0.01);
assertTrue(Double.isInfinite(regen.rangeKm)); assertTrue(Double.isInfinite(regen.rangeKm));
} }
@Test
public void energySinceStartIsTheNetEnergyOfTheTrip() {
RangeEstimator r = new RangeEstimator();
assertNull(r.energySinceStart());
r.add(0, 3f, 0f);
assertEquals(0.0, r.energySinceStart(), 1e-9); // the trip has begun
for (long t = 15_000; t <= 600_000; t += 15_000) r.add(t, 30f, 100f);
// 10 minutes at 30 kW = 5 kWh, plus the first 15 s ramp from 3 kW.
assertEquals(5.0, r.energySinceStart(), 0.1);
for (long t = 615_000; t <= 735_000; t += 15_000) r.add(t, -20f, 60f); // downhill
assertTrue(r.energySinceStart() < 5.0); // recuperation counts back
}
} }