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
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
+2 -2
View File
@@ -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.
@@ -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"));