diff --git a/FIRMWARE.md b/FIRMWARE.md index c74c5a0..ae2cea7 100644 --- a/FIRMWARE.md +++ b/FIRMWARE.md @@ -11,7 +11,7 @@ development. It must not be treated as a vendor specification. | Android | 9 (Pie) — **API 28** | | Chipset | MediaTek **MT2712** | | Device | `mt2712_saic_eh32` (eh32 = MG4) | -| Display | **1920×720 px @ 160 dpi**; the app window is **1778×720 px** (≈1778×640 dp for layouts), the car's own sidebar taking the rest. Confirmed on the car 2026-10-18 (Logs → SCREEN, `SWI68`). | +| Display | **1920×720 px @ 160 dpi**; the app window is **1778×720 px** (≈1778×640 dp for layouts), the car's own sidebar taking the rest. Confirmed on the car 2026-10-18 (read with app 2.16.2, `SWI68`). | | Emulator display profile | **1920×720 @ 160 dpi** | | `ro.build.mtk2712.vehicle_type` | `AS28` (a vehicle type code; the paint colour is not among the system properties) | diff --git a/README.md b/README.md index e222ac5..2c122bd 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,13 @@ with the server using a token of its own, obtained by pairing with a phone (belo | Tab | What it shows | |---|---| -| **Overview** | Battery, range, range since start and at the 5-minute average with their usage, power, speed; odometer, climate setpoint and tyre pressures | +| **Overview** | Battery, range, range since start and at the 5-minute average with their usage, energy since start, power and speed with their 5-minute averages; odometer, climate setpoint and tyre pressures | | **Consumption** | Daily and period consumption, efficiency | | **Charging** | Charging sessions and curves, cost in euro | | **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency | | **Server** | Server URL, pairing by QR code, ingest token, installation ID, config import | -| **Settings** | Full screen, voice key, battery (usable capacity, manual SoH), charging price, Wi-Fi | -| **Logs** | Upload log and the last steering-wheel key reports | +| **Settings** | Full screen, voice key, tyre pressure unit, accent colour, battery (usable capacity, manual SoH), charging price, version, update check, About | +| **Logs** | Upload log | - **Opening the app:** the MG4 launcher does not list third-party apps. **Double-press the voice key** on the steering wheel and the app comes to the front (switch on the Settings diff --git a/SPECS.md b/SPECS.md index 0c16d2e..7da3875 100644 --- a/SPECS.md +++ b/SPECS.md @@ -112,6 +112,10 @@ 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.17.1: Settings without the Wi-Fi and saved-networks cards, in three columns + (display and steering wheel | battery | charging price and app). The background Wi-Fi + auto-connect stays on, now without a switch. Logs keeps only the upload log: the steering-key + reports, the car-colour probe and the screen line are gone, their questions answered. - 2026-10-18 — App 2.17.0: "Speed, 5-min average" card under Speed. The top bar is one compact line (title and subtitle); version, update check and About moved to an App card on the Settings tab. The Overview fits the car's window without scrolling: checked on the emulator at 1778×640, the diff --git a/app/build.gradle b/app/build.gradle index 2ffdddb..b6db3a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 89 - versionName "2.17.0" + versionCode 90 + versionName "2.17.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. diff --git a/app/src/main/java/com/evsuite/abrp/CarColorProbe.java b/app/src/main/java/com/evsuite/abrp/CarColorProbe.java deleted file mode 100644 index 3800e07..0000000 --- a/app/src/main/java/com/evsuite/abrp/CarColorProbe.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.evsuite.abrp; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Looks for the car's paint colour among the head unit's system properties, for the Logs tab. - * - * The MG launcher draws the car in its own colour, so the head unit knows it somewhere; where is - * not documented. This lists every property whose name or value mentions a colour or paint, so - * a photo of the Logs tab shows whether (and how) it can be read. Read-only: it runs the - * platform's own getprop, which only reads. - */ -final class CarColorProbe { - - private static final Pattern LINE = Pattern.compile("^\\[([^\\]]*)\\]: \\[(.*)\\]$"); - private static final Pattern INTERESTING = Pattern.compile( - "(?i)colou?r|paint|body_?col|exterior|car_?model|vehicle_?(config|type|model)"); - - private CarColorProbe() { } - - /** getprop's output, filtered; at most {@code max} lines. Never throws. */ - static List run(int max) { - try { - Process p = new ProcessBuilder("getprop").redirectErrorStream(true).start(); - StringBuilder out = new StringBuilder(); - try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = r.readLine()) != null && out.length() < 512 * 1024) out.append(line).append('\n'); - } - p.waitFor(); - return filter(out.toString(), max); - } catch (Exception e) { - List failed = new ArrayList<>(); - failed.add("getprop failed: " + e.getClass().getSimpleName()); - return failed; - } - } - - /** The "name = value" lines whose name or value looks colour- or model-related. Pure. */ - static List filter(String getpropOutput, int max) { - List found = new ArrayList<>(); - for (String line : getpropOutput.split("\n")) { - Matcher m = LINE.matcher(line.trim()); - if (!m.matches()) continue; - String name = m.group(1), value = m.group(2); - if (INTERESTING.matcher(name).find() || INTERESTING.matcher(value).find()) { - found.add(name + " = " + value); - if (found.size() >= max) break; - } - } - return found; - } -} diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 0d5b314..a532bb6 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -35,7 +35,11 @@ import com.evsuite.hardware.telemetry.EnergyTelemetryReader; public class MainActivity extends AppCompatActivity { - /** Shared with {@link AbrpUploadService}, which reads it on every tick. */ + /** + * Rejoin a saved network (the phone hotspot) in the background; read by + * {@link AbrpUploadService} on every tick. On by default; the Wi-Fi card with its switch was + * removed from Settings (2.17.1), so it now stays on. + */ static final String WIFI_AUTO_KEY = "wifi_auto_connect"; private static final String REPOSITORY_URL = "https://git.stinnesbeck.com/nils/mg4"; @@ -76,9 +80,6 @@ public class MainActivity extends AppCompatActivity { /** The pages in navigation order. Parallel to {@link #TAB_IDS}. */ private View[] panes; - /** Wi-Fi page. The helper is the same one the upload service uses on its own tick. */ - private WifiAutoConnect wifi; - private TextView wifiSsid, wifiState, wifiSavedList; /** * Value cells of the Vehicle page, looked up once and refreshed on the UI tick. @@ -126,7 +127,6 @@ public class MainActivity extends AppCompatActivity { /** Upload page: live telemetry switch and cadence. */ private SwitchMaterial liveTelemetrySwitch, boostSwitch, voiceKeySwitch, fullScreenSwitch; - private TextView steeringKeysText; private com.google.android.material.textfield.MaterialAutoCompleteTextView intervalChoices; private TextInputLayout lowSocLayout; private TextInputEditText lowSocInput; @@ -215,10 +215,8 @@ public class MainActivity extends AppCompatActivity { @Override public void run() { refreshStatus(); refreshCallLog(); - refreshSteeringKeys(); sampleTrackers(); refreshConsumption(); - refreshWifi(); uiHandler.postDelayed(this, 2_000L); } }; @@ -254,22 +252,12 @@ public class MainActivity extends AppCompatActivity { bindVehiclePane(); bindConsumptionPane(); bindChargingPane(); - bindWifiPane(); bindServerPane(); bindServicePane(); requestLocationPermissionIfNeeded(); statusText = servicePane.findViewById(R.id.status_text); callLogText = logPane.findViewById(R.id.call_log_text); - steeringKeysText = logPane.findViewById(R.id.steering_keys_text); - TextView carColorText = logPane.findViewById(R.id.car_color_text); - showScreenInfo(logPane.findViewById(R.id.screen_text)); - logPane.findViewById(R.id.car_color_button).setOnClickListener(v -> new Thread(() -> { - java.util.List found = CarColorProbe.run(60); - String shown = found.isEmpty() ? getString(R.string.car_color_none) - : android.text.TextUtils.join("\n", found); - runOnUiThread(() -> carColorText.setText(shown)); - }, "car-color-probe").start()); logPane.findViewById(R.id.flush_now_button).setOnClickListener(v -> manualFlush()); setUpPager(); @@ -292,21 +280,6 @@ public class MainActivity extends AppCompatActivity { startForegroundService(new Intent(this, AbrpUploadService.class)); } - /** - * What the head unit reports about its screen, for the Logs tab: the panel in pixels and its - * density, and this app's window in pixels and in dp, which is what layouts are chosen by. - * Sources disagree on the MG4's numbers (1920×720, 1280×480, a 1778×720 window). - */ - @SuppressWarnings("deprecation") // getRealMetrics: the WindowMetrics API starts at 30; this is 28 - private void showScreenInfo(TextView view) { - android.util.DisplayMetrics panel = new android.util.DisplayMetrics(); - getWindowManager().getDefaultDisplay().getRealMetrics(panel); - android.util.DisplayMetrics window = getResources().getDisplayMetrics(); - android.content.res.Configuration config = getResources().getConfiguration(); - view.setText(getString(R.string.screen_info, panel.widthPixels, panel.heightPixels, panel.densityDpi, - window.widthPixels, window.heightPixels, config.screenWidthDp, config.screenHeightDp)); - } - /** * Shows a dialog without bringing the car's bars back. A dialog is a window of its own, and * the system shows the bars when a window without the full-screen flags takes focus. So it @@ -1224,45 +1197,6 @@ public class MainActivity extends AppCompatActivity { } } - // ---------- Wi-Fi page ---------- - - private void bindWifiPane() { - wifi = new WifiAutoConnect(getApplicationContext()); - wifiSsid = settingsPane.findViewById(R.id.wifi_ssid); - wifiState = settingsPane.findViewById(R.id.wifi_state); - wifiSavedList = settingsPane.findViewById(R.id.wifi_saved_list); - - SwitchMaterial auto = settingsPane.findViewById(R.id.wifi_auto_switch); - auto.setChecked(prefs.getBoolean(WIFI_AUTO_KEY, true)); - auto.setOnCheckedChangeListener((btn, checked) -> - prefs.edit().putBoolean(WIFI_AUTO_KEY, checked).apply()); - - settingsPane.findViewById(R.id.wifi_connect_button).setOnClickListener(v -> { - // force: the driver is asking now, so the rate limit does not apply. - WifiAutoConnect.Result r = wifi.ensureConnected(true); - wifiState.setText(r.message); - refreshWifi(); - }); - } - - private void refreshWifi() { - if (wifi == null) return; - String ssid = wifi.currentSsid(); - wifiSsid.setText(ssid == null ? getString(R.string.wifi_not_connected) : ssid); - if (ssid != null) { - wifiState.setText(R.string.wifi_connected); - } else if (!wifi.isWifiEnabled()) { - wifiState.setText(R.string.wifi_off); - } - // Left alone otherwise: a message the button just set says more about what is - // happening than "not connected" would. - - java.util.List saved = wifi.savedNetworkNames(); - wifiSavedList.setText(saved.isEmpty() - ? getString(R.string.wifi_saved_empty) - : android.text.TextUtils.join("\n", saved)); - } - // ---------- Vehicle page ---------- /** Looks up the value cells once; the labels are static and never touched from code. */ @@ -1752,15 +1686,6 @@ public class MainActivity extends AppCompatActivity { }, "cloud-refresh").start(); } - /** What the car's steering-wheel key broadcasts looked like, newest first. */ - private void refreshSteeringKeys() { - if (steeringKeysText == null) return; - java.util.List keys = SteeringKeyListener.recent(); - String text = keys.isEmpty() ? getString(R.string.steering_keys_empty) - : android.text.TextUtils.join("\n", keys); - if (!text.contentEquals(steeringKeysText.getText())) steeringKeysText.setText(text); - } - private void refreshCallLog() { java.util.List entries = AbrpUploadService.log().recent(); if (entries.isEmpty()) { diff --git a/app/src/main/java/com/evsuite/abrp/SteeringKeyListener.java b/app/src/main/java/com/evsuite/abrp/SteeringKeyListener.java index ec377f6..080955b 100644 --- a/app/src/main/java/com/evsuite/abrp/SteeringKeyListener.java +++ b/app/src/main/java/com/evsuite/abrp/SteeringKeyListener.java @@ -9,20 +9,12 @@ import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; -import java.text.SimpleDateFormat; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; - /** * Opens the app on a double press of the steering wheel's voice key. * * The head unit reports steering-wheel keys as the broadcast {@link #ACTION}; code 287 is the * voice key (see EVHardware's PhysicalButtonEventDecoder). The extras' format was read off the - * car (see {@link SteeringKeyReport}); every report is still kept raw for the Logs tab, in case - * another firmware sends them differently. + * car (see {@link SteeringKeyReport}). * * Registered by the always-on service on the main thread; never throws into the head unit. */ @@ -31,9 +23,6 @@ final class SteeringKeyListener { static final String ACTION = "com.saic.keyevent.hardkey.report"; static final int VOICE_KEY = 287; private static final String TAG = "NilsMG4.SteeringKey"; - private static final int MAX_LOG = 20; - - private static final ArrayDeque RECENT = new ArrayDeque<>(); private final Context context; private final SharedPreferences prefs; @@ -79,18 +68,6 @@ final class SteeringKeyListener { registered = false; } - /** The last broadcasts, newest first, for the Logs tab. */ - static synchronized List recent() { - List out = new ArrayList<>(RECENT); - java.util.Collections.reverse(out); - return out; - } - - private static synchronized void remember(String line) { - if (RECENT.size() >= MAX_LOG) RECENT.removeFirst(); - RECENT.addLast(line); - } - private void handle(Intent intent) { Bundle extras = intent.getExtras(); SteeringKeyReport key = read(extras); @@ -98,11 +75,6 @@ final class SteeringKeyListener { if (key.code != null && UploadSettings.voiceKeyOpensApp(prefs)) { opens = detector.accept(key.code, key.down, key.longPress, android.os.SystemClock.elapsedRealtime()); } - remember(new SimpleDateFormat("HH:mm:ss.SSS", Locale.US).format(new Date()) - + " " + describe(extras) - + "\n -> code " + (key.code == null ? "?" : key.code) - + ", down " + (key.down == null ? "?" : key.down) - + ", long " + key.longPress + (opens ? " OPENS APP" : "")); if (opens) { try { context.startActivity(new Intent(context, MainActivity.class) @@ -129,17 +101,4 @@ final class SteeringKeyListener { } return SteeringKeyReport.parse(values); } - - /** All extras as name=value(type), so the Logs tab shows the real format. */ - static String describe(Bundle extras) { - if (extras == null || extras.isEmpty()) return "(no extras)"; - StringBuilder sb = new StringBuilder(); - for (String k : extras.keySet()) { - Object v = extras.get(k); - if (sb.length() > 0) sb.append(", "); - sb.append(k).append('=').append(v).append(" (") - .append(v == null ? "null" : v.getClass().getSimpleName()).append(')'); - } - return sb.toString(); - } } diff --git a/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java b/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java index ea2bfcc..2661d36 100644 --- a/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java +++ b/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java @@ -58,34 +58,6 @@ final class WifiAutoConnect { } } - boolean isWifiEnabled() { - try { - return wifi != null && wifi.isWifiEnabled(); - } catch (Throwable t) { - return false; - } - } - - /** Saved network names, for the UI to show what it can reach. Empty if the OS refuses. */ - List savedNetworkNames() { - java.util.List names = new java.util.ArrayList<>(); - try { - if (wifi == null) return names; - List saved = wifi.getConfiguredNetworks(); - if (saved == null) return names; - for (WifiConfiguration c : saved) { - if (c == null || c.SSID == null) continue; - String name = c.SSID.replace("\"", "").trim(); - if (!name.isEmpty() && !names.contains(name)) names.add(name); - } - } catch (SecurityException e) { - Log.w(TAG, "Saved networks unavailable: permission denied"); - } catch (Throwable t) { - Log.w(TAG, "getConfiguredNetworks refused: " + t.getMessage()); - } - return names; - } - /** * One attempt to get onto a saved network, rate-limited. * diff --git a/app/src/main/res/layout-w1000dp/pane_settings.xml b/app/src/main/res/layout-w1000dp/pane_settings.xml index a07fd8e..2c54b3c 100644 --- a/app/src/main/res/layout-w1000dp/pane_settings.xml +++ b/app/src/main/res/layout-w1000dp/pane_settings.xml @@ -1,5 +1,6 @@ + @@ -17,7 +18,9 @@ - + + + @@ -30,7 +33,9 @@ - + + + @@ -39,20 +44,6 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/pane_log.xml b/app/src/main/res/layout/pane_log.xml index 95f2da8..7bfc7fc 100644 --- a/app/src/main/res/layout/pane_log.xml +++ b/app/src/main/res/layout/pane_log.xml @@ -4,14 +4,5 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/pane_settings.xml b/app/src/main/res/layout/pane_settings.xml index e76fcec..eea34d5 100644 --- a/app/src/main/res/layout/pane_settings.xml +++ b/app/src/main/res/layout/pane_settings.xml @@ -38,18 +38,6 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f54eb17..6380df6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,16 +14,6 @@ Settings Logs - - WI-FI - Connected network - Status - Connect Now - Auto-connect in the background - While the upload service is running, it tries to reconnect to saved networks on its own when the connection drops. - SAVED NETWORKS - Could not read saved networks - Not connected Live Vehicle Data @@ -40,12 +30,6 @@ needs a minute of data Tyre pressure unit Accent colour - SCREEN - Panel %1$d × %2$d px at %3$d dpi\nApp window %4$d × %5$d px = %6$d × %7$d dp - CAR COLOUR - Lists the head unit\'s settings that mention a colour, paint or car model, to find out whether the car\'s colour can be read. A photo of this list helps. - Look for the car colour - Nothing colour-related found %1$.1f kWh net, after recuperation once the car is on @@ -134,9 +118,6 @@ Hides the car\'s top and bottom bars while this app is open. Swipe in from the top or bottom edge to show them briefly, e.g. for the climate controls. Double-press the voice key to open this app Press the voice key on the steering wheel twice quickly. The voice assistant may still react to the presses. - Steering-wheel keys - The last key reports the car sent, newest first. If double-pressing the voice key does not open the app, a photo of this list shows how the car reports it. - No key reports received yet. Press a steering-wheel key. Send live data To your server, which stores it, and straight to ABRP when the ABRP credentials below are set. Consumption and charging tracking keep running when this is off. The car sends live data to ABRP itself with your ABRP API key and user token. Without them, ABRP gets no live data; your server only stores it. @@ -260,8 +241,6 @@ Unit price €/kWh Save price - Connected ♥ - Wi-Fi off Reset %1$s? This counter\'s recorded consumption history will be permanently deleted. Reset diff --git a/app/src/main/res/values/strings_design.xml b/app/src/main/res/values/strings_design.xml index 99507ca..22d31ec 100644 --- a/app/src/main/res/values/strings_design.xml +++ b/app/src/main/res/values/strings_design.xml @@ -12,7 +12,7 @@ Live data. Where your car\'s live data goes, and how often. Settings. - Display, battery, charging price, Wi-Fi and updates. + Display, battery, charging price and updates. Every upload, right here. Status and details of recent uploads. BATTERY diff --git a/app/src/test/java/com/evsuite/abrp/CarColorProbeTest.java b/app/src/test/java/com/evsuite/abrp/CarColorProbeTest.java deleted file mode 100644 index 7ac9479..0000000 --- a/app/src/test/java/com/evsuite/abrp/CarColorProbeTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.evsuite.abrp; - -import static org.junit.Assert.assertEquals; - -import java.util.Arrays; - -import org.junit.Test; - -/** Picking colour-related system properties out of getprop's output — pure JVM. */ -public class CarColorProbeTest { - - @Test - public void onlyColourOrModelRelatedPropertiesAreListed() { - String getprop = "[ro.build.version.sdk]: [28]\n" - + "[persist.saic.vehicle.color]: [3]\n" - + "[ro.product.model]: [EH32]\n" - + "[persist.car.bodyCol]: [RED]\n" - + "[sys.launcher.car_model]: [mg4_red.glb]\n" - + "garbage line\n"; - assertEquals(Arrays.asList("persist.saic.vehicle.color = 3", "persist.car.bodyCol = RED", - "sys.launcher.car_model = mg4_red.glb"), CarColorProbe.filter(getprop, 50)); - assertEquals(1, CarColorProbe.filter(getprop, 1).size()); - } -}