App 2.11.0, server 2.3.0: live data straight to ABRP; settings regrouped

ABRP keeps getting live data while the car server is down: with an ABRP
API key and user token set in the app (Live data tab, or abrp_api_key /
abrp_token in the config file), the car sends the newest sample to ABRP
itself at every upload. It tells the server the newest sample ABRP took
(abrpSentUtc), and the server forwards only newer ones, so ABRP gets
each sample once. Without credentials in the app the server forwards
everything, as before. The credentials sit in encrypted storage, go in
the request body, and neither they nor ABRP's answer are logged.

Settings are grouped by what they are for:
- Live data (was Upload): on/off, ABRP credentials, upload frequency.
- Settings (replaces the Wi-Fi tab): full screen and voice key,
  usable capacity and manual SoH (were on Consumption), charging price
  (was on Charging), Wi-Fi.
Charging and Live data get icons of their own.

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 12:15:21 +02:00
co-authored by Claude Opus 5.5
parent 087f193f8c
commit ca2f899403
36 changed files with 636 additions and 163 deletions
@@ -10,7 +10,8 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* One JSON request to the car server, shared by telemetry, consumption sync and pairing.
* One JSON request to the car server, shared by telemetry, consumption sync and pairing (and,
* through {@link #send}, the direct ABRP upload).
* Blocking, with short timeouts — call it off the main thread.
*
* The token goes in the Authorization header, never the URL, and is never logged. The answer
@@ -48,17 +49,26 @@ final class ServerHttp {
* {@code token} may be null for the unauthenticated pairing calls.
*/
static Response request(String method, String url, String token, JSONObject json) throws Exception {
byte[] body = json == null ? null : json.toString().getBytes(StandardCharsets.UTF_8);
return send(method, url, token == null ? null : "Bearer " + token, "application/json", body);
}
/**
* The request underneath, also used for ABRP ({@link AbrpApi}): {@code authorization} is the
* whole header value or null, {@code body} null for none.
*/
static Response send(String method, String url, String authorization, String contentType, byte[] body)
throws Exception {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
try {
connection.setRequestMethod(method);
connection.setConnectTimeout(TIMEOUT_MS);
connection.setReadTimeout(TIMEOUT_MS);
if (token != null) connection.setRequestProperty("Authorization", "Bearer " + token);
if (json != null) {
byte[] body = json.toString().getBytes(StandardCharsets.UTF_8);
if (authorization != null) connection.setRequestProperty("Authorization", authorization);
if (body != null) {
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(body.length);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Content-Type", contentType);
try (OutputStream out = connection.getOutputStream()) { out.write(body); }
}
int status = connection.getResponseCode();