diff --git a/SPECS.md b/SPECS.md
index 85c5a6f..0c16d2e 100644
--- a/SPECS.md
+++ b/SPECS.md
@@ -112,6 +112,10 @@ 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
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.
+- 2026-10-18 — App 2.17.0: "Speed, 5-min average" card under Speed. The top bar is one compact line
+ (title and subtitle); version, update check and About moved to an App card on the Settings tab.
+ The Overview fits the car's window without scrolling: checked on the emulator at 1778×640, the
+ layout height the car reports (less page padding, a lower tyre card).
- 2026-10-18 — App 2.16.4: drop-downs again, as Material exposed dropdown menus: their list leaves
the focus with the activity, so full screen stays (a Spinner's list is a focusable window of its
own without the full-screen flags). 2.16.3 had replaced them with buttons.
diff --git a/app/build.gradle b/app/build.gradle
index cb04d10..2ffdddb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
- versionCode 88
- versionName "2.16.4"
+ versionCode 89
+ versionName "2.17.0"
// 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.
diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java
index 3ddcdf4..0d5b314 100644
--- a/app/src/main/java/com/evsuite/abrp/MainActivity.java
+++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java
@@ -94,7 +94,8 @@ public class MainActivity extends AppCompatActivity {
vehTireFl, vehTireFr, vehTireRl, vehTireRr,
vehRangeTrip, vehRangeTripNote, vehRangeAverage, vehRangeAverageNote,
vehUsageTrip, vehUsageTripNote, vehUsageAverage, vehUsageAverageNote,
- vehEnergyTrip, vehEnergyTripNote, vehPowerAverage, vehPowerAverageNote;
+ vehEnergyTrip, vehEnergyTripNote, vehPowerAverage, vehPowerAverageNote,
+ vehSpeedAverage, vehSpeedAverageNote;
/** Usable pack capacity for the range estimates; the car does not publish it. */
static final String USABLE_KWH_KEY = "usable_kwh";
@@ -233,7 +234,6 @@ public class MainActivity extends AppCompatActivity {
getTheme().applyStyle(AccentColor.fromPref(prefs.getString(AccentColor.PREF_KEY, null)).overlay, true);
setContentView(R.layout.activity_main);
- ((TextView) findViewById(R.id.version_badge)).setText("v" + BuildConfig.VERSION_NAME);
// The pages are inflated here, once, and handed to the pager as fixed,
@@ -277,8 +277,10 @@ public class MainActivity extends AppCompatActivity {
if (savedInstanceState != null) {
((ViewPager2) findViewById(R.id.content)).setCurrentItem(savedInstanceState.getInt(STATE_PAGE, 0), false);
}
- findViewById(R.id.about_button).setOnClickListener(v -> showAbout());
- findViewById(R.id.update_button).setOnClickListener(v -> checkForUpdate(v));
+ ((TextView) settingsPane.findViewById(R.id.settings_version))
+ .setText(getString(R.string.settings_version, BuildConfig.VERSION_NAME));
+ settingsPane.findViewById(R.id.settings_about_button).setOnClickListener(v -> showAbout());
+ settingsPane.findViewById(R.id.settings_update_button).setOnClickListener(v -> checkForUpdate(v));
// Unstable builds check for a newer pre-release; the stable flavor's UpdateHook is
// a no-op and does not even contain the updater.
@@ -1288,6 +1290,8 @@ public class MainActivity extends AppCompatActivity {
vehEnergyTripNote = vehiclePane.findViewById(R.id.vehicle_energy_trip_note);
vehPowerAverage = vehiclePane.findViewById(R.id.vehicle_power_average);
vehPowerAverageNote = vehiclePane.findViewById(R.id.vehicle_power_average_note);
+ vehSpeedAverage = vehiclePane.findViewById(R.id.vehicle_speed_average);
+ vehSpeedAverageNote = vehiclePane.findViewById(R.id.vehicle_speed_average_note);
}
private double usableKwh() {
@@ -1308,6 +1312,9 @@ public class MainActivity extends AppCompatActivity {
Double power = estimator.averagePowerKw(monotonicMs);
vehPowerAverage.setText(power == null ? "—" : getString(R.string.power_kw, power));
vehPowerAverageNote.setText(power == null ? R.string.power_waiting : R.string.usage_last_5_min);
+ Double speed = estimator.averageSpeedKmh(monotonicMs);
+ vehSpeedAverage.setText(speed == null ? "—" : getString(R.string.speed_kmh, speed));
+ vehSpeedAverageNote.setText(speed == null ? R.string.power_waiting : R.string.usage_last_5_min);
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);
diff --git a/app/src/main/java/com/evsuite/abrp/RangeEstimator.java b/app/src/main/java/com/evsuite/abrp/RangeEstimator.java
index b982760..956fa97 100644
--- a/app/src/main/java/com/evsuite/abrp/RangeEstimator.java
+++ b/app/src/main/java/com/evsuite/abrp/RangeEstimator.java
@@ -177,6 +177,26 @@ final class RangeEstimator {
return energyKwh / (covered / 3_600_000.0);
}
+ /** Mean speed over the last five minutes, time-weighted, standing included; null below a minute. */
+ synchronized Double averageSpeedKmh(long nowMs) {
+ double distanceKm = 0;
+ long covered = 0;
+ Sample previous = null;
+ for (Sample s : samples) {
+ if (s.timeMs < nowMs - WINDOW_MS) continue;
+ if (previous != null) {
+ long dt = s.timeMs - previous.timeMs;
+ if (dt > 0 && dt <= MAX_GAP_MS) {
+ distanceKm += (previous.speedKmh + s.speedKmh) / 2 * (dt / 3_600_000.0);
+ covered += dt;
+ }
+ }
+ previous = s;
+ }
+ if (covered < MIN_WINDOW_MS) return null;
+ return distanceKm / (covered / 3_600_000.0);
+ }
+
synchronized void clear() {
samples.clear();
tripEnergyKwh = 0;
diff --git a/app/src/main/res/layout-w1000dp/activity_main.xml b/app/src/main/res/layout-w1000dp/activity_main.xml
index 9c564a4..ccf2bf9 100644
--- a/app/src/main/res/layout-w1000dp/activity_main.xml
+++ b/app/src/main/res/layout-w1000dp/activity_main.xml
@@ -16,14 +16,11 @@
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/layout-w1000dp/pane_settings.xml b/app/src/main/res/layout-w1000dp/pane_settings.xml
index 113c246..a07fd8e 100644
--- a/app/src/main/res/layout-w1000dp/pane_settings.xml
+++ b/app/src/main/res/layout-w1000dp/pane_settings.xml
@@ -53,6 +53,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-w1000dp/pane_vehicle.xml b/app/src/main/res/layout-w1000dp/pane_vehicle.xml
index 24acbed..ea5ac85 100644
--- a/app/src/main/res/layout-w1000dp/pane_vehicle.xml
+++ b/app/src/main/res/layout-w1000dp/pane_vehicle.xml
@@ -1,6 +1,6 @@
-
+
@@ -52,7 +52,11 @@
-
+
+
+
+
+
@@ -71,7 +75,7 @@
-
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 376a9f8..1a8270b 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -2,14 +2,11 @@
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/layout/pane_settings.xml b/app/src/main/res/layout/pane_settings.xml
index ced79f4..e76fcec 100644
--- a/app/src/main/res/layout/pane_settings.xml
+++ b/app/src/main/res/layout/pane_settings.xml
@@ -50,5 +50,11 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/pane_vehicle.xml b/app/src/main/res/layout/pane_vehicle.xml
index 009564c..5993153 100644
--- a/app/src/main/res/layout/pane_vehicle.xml
+++ b/app/src/main/res/layout/pane_vehicle.xml
@@ -1,6 +1,6 @@
-
+
@@ -49,6 +49,11 @@
+
+
+
+
+
@@ -66,7 +71,7 @@
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 567cc44..f54eb17 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -59,7 +59,7 @@
over the last 5 minutes
Usable battery capacity
Save capacity
- Used for the range estimates on the Vehicle tab. The 51 kWh MG4 battery has 50.8 kWh usable, the 64 kWh one 61.7, the 77 kWh one 74.4.
+ Used for the range estimates on the Overview. The 51 kWh MG4 battery has 50.8 kWh usable, the 64 kWh one 61.7, the 77 kWh one 74.4.
Usable capacity saved
Enter a value between 10 and 150 kWh
Speed
@@ -204,6 +204,11 @@
Check for updates
Checking for a new OTA version…
Software update
+ Check for updates
+ About Nils\' MG4
+ Version %1$s
+ Speed, 5-min average
+ %1$.0f km/h
Downloading and verifying…
Downloading… %1$d%%
About
diff --git a/app/src/main/res/values/strings_design.xml b/app/src/main/res/values/strings_design.xml
index d21a79c..99507ca 100644
--- a/app/src/main/res/values/strings_design.xml
+++ b/app/src/main/res/values/strings_design.xml
@@ -12,7 +12,7 @@
Live data.
Where your car\'s live data goes, and how often.
Settings.
- Display, battery, charging price and Wi-Fi.
+ Display, battery, charging price, Wi-Fi and updates.
Every upload, right here.
Status and details of recent uploads.
BATTERY
@@ -26,6 +26,7 @@
ABRP, DIRECTLY FROM THE CAR
DISPLAY AND STEERING WHEEL
BATTERY
+ APP
CHARGING PRICE
UPDATE FREQUENCY
Tire pressure positions on the vehicle
diff --git a/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java b/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java
index b5f4a62..47834ea 100644
--- a/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java
+++ b/app/src/test/java/com/evsuite/abrp/RangeEstimatorTest.java
@@ -140,4 +140,16 @@ public class RangeEstimatorTest {
for (long t = 315_000; t <= 450_000; t += 15_000) r.add(t, 2f, 0f);
assertEquals(11.0, r.averagePowerKw(450_000), 1.0);
}
+
+ @Test
+ public void averageSpeedOverTheLastFiveMinutes() {
+ RangeEstimator r = new RangeEstimator();
+ r.add(0, 10f, 60f);
+ assertNull(r.averageSpeedKmh(0));
+ for (long t = 15_000; t <= 300_000; t += 15_000) r.add(t, 10f, 60f);
+ assertEquals(60.0, r.averageSpeedKmh(300_000), 1e-6);
+ // Half the window standing at lights halves the average.
+ for (long t = 315_000; t <= 450_000; t += 15_000) r.add(t, 1f, 0f);
+ assertEquals(30.0, r.averageSpeedKmh(450_000), 4.0);
+ }
}