App 2.16.1: Overview values every half second

The Overview read the car every 2 s, on the main thread. It now reads
every 0.5 s on a background thread (one reader; a read still running
makes the next tick skip rather than queue) and shows the values on the
main thread. The consumption and charging trackers stay on the 2-second
tick with the newest reading: they integrate over time, and the
charging tracker writes its session to storage on every sample.

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 19:32:50 +02:00
co-authored by Claude Opus 5.5
parent 9226616261
commit c71fa4504e
3 changed files with 80 additions and 9 deletions
+5
View File
@@ -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 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.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 - 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 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 leaves only the read-only token in a memory volume; the datasource reads it with
+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 84 versionCode 85
versionName "2.16.0" versionName "2.16.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.
@@ -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 * 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. * 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 EnergyTelemetryReader vehicleReader;
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer, private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
vehCharging, vehHvac, vehCharging, vehHvac,
@@ -214,7 +215,7 @@ public class MainActivity extends AppCompatActivity {
refreshStatus(); refreshStatus();
refreshCallLog(); refreshCallLog();
refreshSteeringKeys(); refreshSteeringKeys();
refreshVehicle(); sampleTrackers();
refreshConsumption(); refreshConsumption();
refreshWifi(); refreshWifi();
uiHandler.postDelayed(this, 2_000L); uiHandler.postDelayed(this, 2_000L);
@@ -330,6 +331,7 @@ public class MainActivity extends AppCompatActivity {
super.onResume(); super.onResume();
// Poll state and log only while the screen is up; onPause cancels it. // Poll state and log only while the screen is up; onPause cancels it.
uiHandler.post(uiRefresh); uiHandler.post(uiRefresh);
uiHandler.post(overviewRefresh);
} }
@Override @Override
@@ -376,6 +378,13 @@ public class MainActivity extends AppCompatActivity {
super.onPause(); super.onPause();
// Stop polling off-screen: this activity has no reason to spend cycles then. // Stop polling off-screen: this activity has no reason to spend cycles then.
uiHandler.removeCallbacks(uiRefresh); uiHandler.removeCallbacks(uiRefresh);
uiHandler.removeCallbacks(overviewRefresh);
}
@Override
protected void onDestroy() {
carReader.shutdownNow();
super.onDestroy();
} }
// ---------- Update check ---------- // ---------- 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 * 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. * 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 { 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); ConsumptionTracker.get(this).sample(s);
refreshCharging(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 %%")); vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%"));
vehRange.setText(fmt(s.getRangeKm(), "%.0f km")); vehRange.setText(fmt(s.getRangeKm(), "%.0f km"));
vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW")); vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW"));