diff --git a/SPECS.md b/SPECS.md index 3eb0e69..a1c48a3 100644 --- a/SPECS.md +++ b/SPECS.md @@ -109,6 +109,11 @@ 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.16.1: the Overview reads the car every 0.5 s (was 2 s), on a background + thread; the consumption and charging trackers stay on the 2-second tick (they integrate over + time, and the charging tracker writes its session to storage on every sample). Note for + emulator tests: uiautomator can no longer dump the screen while the Overview is open, since + the UI is never idle. - 2026-10-18 — Grafana's read-only InfluxDB token without a manual step: an init container in the Grafana pod creates it with the admin token at every start, deletes the previous one and leaves only the read-only token in a memory volume; the datasource reads it with diff --git a/app/build.gradle b/app/build.gradle index 97cbc52..ada442d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 84 - versionName "2.16.0" + versionCode 85 + versionName "2.16.1" // 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 e84325a..721c725 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -87,6 +87,7 @@ public class MainActivity extends AppCompatActivity { * page shows what the car reports, whether or not the upload service is running, and a * signal the vehicle does not publish must show a dash rather than a zero. */ + /** Used only on the overview-read thread. */ private EnergyTelemetryReader vehicleReader; private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer, vehCharging, vehHvac, @@ -214,7 +215,7 @@ public class MainActivity extends AppCompatActivity { refreshStatus(); refreshCallLog(); refreshSteeringKeys(); - refreshVehicle(); + sampleTrackers(); refreshConsumption(); refreshWifi(); uiHandler.postDelayed(this, 2_000L); @@ -330,6 +331,7 @@ public class MainActivity extends AppCompatActivity { super.onResume(); // Poll state and log only while the screen is up; onPause cancels it. uiHandler.post(uiRefresh); + uiHandler.post(overviewRefresh); } @Override @@ -376,6 +378,13 @@ public class MainActivity extends AppCompatActivity { super.onPause(); // Stop polling off-screen: this activity has no reason to spend cycles then. uiHandler.removeCallbacks(uiRefresh); + uiHandler.removeCallbacks(overviewRefresh); + } + + @Override + protected void onDestroy() { + carReader.shutdownNow(); + super.onDestroy(); } // ---------- Update check ---------- @@ -1315,16 +1324,73 @@ public class MainActivity extends AppCompatActivity { * is not work to repeat every two seconds. A read that throws leaves the cells as they * were rather than blanking the page — a transient miss is not news worth showing. */ - private void refreshVehicle() { + // ---------- Overview: read every half second, off the main thread ---------- + + /** How often the Overview reads the car while it is on screen. */ + private static final long OVERVIEW_REFRESH_MS = 500L; + + /** One reader thread; a read that is still running makes the next tick skip, not queue. */ + private final java.util.concurrent.ExecutorService carReader = + java.util.concurrent.Executors.newSingleThreadExecutor(r -> new Thread(r, "overview-read")); + private final java.util.concurrent.atomic.AtomicBoolean reading = + new java.util.concurrent.atomic.AtomicBoolean(); + /** The newest reading, for the slower work on the 2-second tick. */ + private volatile EnergySnapshot latestSnapshot; + + private final Runnable overviewRefresh = new Runnable() { + @Override public void run() { + readVehicle(); + uiHandler.postDelayed(this, OVERVIEW_REFRESH_MS); + } + }; + + /** + * About twenty calls into the car's services: on a background thread, so the screen never + * waits for them; the values are shown on the main thread once they are in. + */ + private void readVehicle() { + if (!reading.compareAndSet(false, true)) return; + try { + carReader.execute(() -> { + try { + if (vehicleReader == null) { + vehicleReader = new EnergyTelemetryReader(getApplicationContext()); + } + // The timestamp is passed explicitly: its Kotlin default is not visible from Java. + EnergySnapshot s = vehicleReader.read(System.currentTimeMillis()); + latestSnapshot = s; + runOnUiThread(() -> { + if (!isDestroyed()) showVehicle(s); + }); + } catch (Throwable ignored) { + // Leave the last good reading on screen. + } finally { + reading.set(false); + } + }); + } catch (java.util.concurrent.RejectedExecutionException e) { + reading.set(false); // the activity is going away + } + } + + /** + * The consumption and charging trackers stay on the 2-second tick: they integrate over + * time, so a faster rate adds nothing, and the charging tracker writes its session to + * storage on every sample while charging. + */ + private void sampleTrackers() { + EnergySnapshot s = latestSnapshot; + if (s == null) return; try { - if (vehicleReader == null) { - vehicleReader = new EnergyTelemetryReader(getApplicationContext()); - } - // The timestamp is passed explicitly: its Kotlin default is not visible from Java. - EnergySnapshot s = vehicleReader.read(System.currentTimeMillis()); ConsumptionTracker.get(this).sample(s); refreshCharging(s); + } catch (Throwable ignored) { + // A tracker failure must not stop the page. + } + } + private void showVehicle(EnergySnapshot s) { + try { vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%")); vehRange.setText(fmt(s.getRangeKm(), "%.0f km")); vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW"));