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:
@@ -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
@@ -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 {
|
try {
|
||||||
if (vehicleReader == null) {
|
if (vehicleReader == null) {
|
||||||
vehicleReader = new EnergyTelemetryReader(getApplicationContext());
|
vehicleReader = new EnergyTelemetryReader(getApplicationContext());
|
||||||
}
|
}
|
||||||
// The timestamp is passed explicitly: its Kotlin default is not visible from Java.
|
// The timestamp is passed explicitly: its Kotlin default is not visible from Java.
|
||||||
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
|
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 {
|
||||||
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"));
|
||||||
|
|||||||
Reference in New Issue
Block a user