App 2.3.0: Server tab, live telemetry to the car server

- New Server tab: server URL, ingest token and installation id, with
  Save, Test connection (health, then a read-only token check) and the
  config-file import that upstream removed (app folder / USB stick first,
  document picker as fallback). Settings are kept encrypted and read on
  every use, so the service picks up a change without a restart. Each
  installation generates its own id; the inherited upstream UUID and
  the baked-in CONSUMPTION_API_* build fields are gone.
- Live telemetry restored from before ea8032a (TelemetryPayload,
  UploadCadence, UploadSettings, GPS) and sent in batches to
  POST /v1/telemetry instead of ABRP; the server forwards to ABRP. Adds
  ext_temp, voltage and current. Unsent samples queue in memory (about
  8 h) while offline.
- Upload tab: the live-telemetry switch and cadence controls work again;
  the dead service and autostart switches are removed.
- Manifest: location permissions and foreground service types.
- Unit tests compile again: 114 (stable) / 136 (unstable) pass. Tests
  for the deleted AbrpApi and HeadUnitWeather removed; new tests for
  ServerConfig and TelemetryQueue. Lint passes.

Not yet tried on the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 14:23:04 +02:00
co-authored by Claude Opus 5.5
parent c253876ed2
commit 6145736c97
27 changed files with 1590 additions and 287 deletions
@@ -24,11 +24,13 @@ import java.util.concurrent.TimeUnit;
/**
* Foreground service that polls vehicle telemetry every 15 s, integrates consumption locally,
* and syncs it to the cloud (MongoDB) in 30-minute delta chunks. Never sends anything to ABRP.
* syncs it to the car server in 2-minute delta chunks, and sends live telemetry samples that
* the server stores and forwards to ABRP ({@link TelemetryUploader}). The car itself never
* talks to ABRP and holds no ABRP credentials.
*
* Always-on by design: started at boot (BootReceiver) and on app open, never gated on
* credentials or a toggle. The only thing it needs is a network for the cloud sync, and a
* missing one is handled by caching chunks locally until the connection returns.
* credentials or a toggle. The only thing it needs is a network, and a missing one is
* handled by queueing locally until the connection returns.
*/
public class AbrpUploadService extends Service {
@@ -64,6 +66,7 @@ public class AbrpUploadService extends Service {
private ConsumptionTracker consumptionTracker;
private ConsumptionCloudClient consumptionCloudClient;
private ChargeSessionTracker chargeSessionTracker;
private TelemetryUploader telemetryUploader;
private WifiAutoConnect wifi;
private ScheduledExecutorService scheduler;
private SharedPreferences prefs;
@@ -94,6 +97,7 @@ public class AbrpUploadService extends Service {
consumptionCloudClient = new ConsumptionCloudClient(getApplicationContext());
chargeSessionTracker = ChargeSessionTracker.get(getApplicationContext());
wifi = new WifiAutoConnect(getApplicationContext());
telemetryUploader = new TelemetryUploader(getApplicationContext());
scheduler.scheduleWithFixedDelay(
this::safeUploadCycle,
@@ -111,15 +115,16 @@ public class AbrpUploadService extends Service {
}
/**
* Enters the foreground. No location type — this service no longer reads GPS. Foreground
* service types do not exist before API 29, so on this API-28 head unit a plain
* startForeground is the only correct call.
* Enters the foreground. Foreground service types do not exist before API 29, so on this
* API-28 head unit a plain startForeground is the only correct call; from API 29 the
* location type is what lets the GPS subscription run in the background.
*/
private void startInForeground() {
Notification notification = buildNotification(lastStatus);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
ServiceCompat.startForeground(
this, NOTIF_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
this, NOTIF_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC | ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
} else {
startForeground(NOTIF_ID, notification);
}
@@ -133,6 +138,7 @@ public class AbrpUploadService extends Service {
running = false;
prefs.edit().putBoolean("service_running", false).apply();
if (scheduler != null) scheduler.shutdownNow();
if (telemetryUploader != null) telemetryUploader.stop();
Log.i(TAG, "Service stopped");
}
@@ -159,7 +165,16 @@ public class AbrpUploadService extends Service {
wifi.ensureConnected(false);
}
updateNotification("Consumption tracking active");
UploadSettings settings = UploadSettings.from(prefs);
if (!ServerConfig.get(this).isConfigured()) {
updateNotification(getString(R.string.server_not_configured));
} else if (!settings.abrpUploadEnabled) {
updateNotification(getString(R.string.notif_tracking_locally));
} else {
telemetryUploader.tick(vehicle, chargeSessionTracker.snapshot(), settings);
int queued = telemetryUploader.queued();
updateNotification(queued > 0 ? "Sending telemetry — " + queued + " queued" : "Sending telemetry");
}
}
// ---------- Notification ----------