From b2a5b32b08a14fcf7c6de6e58c1d139fd06c52cf Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Sat, 17 Oct 2026 13:26:32 +0200 Subject: [PATCH] Add on-screen diagnostics to the Logs tab Without adb there was no way to see why vehicle values stay blank: refreshVehicle() swallowed every exception. The Logs tab now shows the package, version, UID (and whether it is the system UID), signer SHA-256, firmware, car permission grants, the last snapshot and read error, and the newest 200 entries of the vehicle library's AppLogger. Co-Authored-By: Claude Opus 5.5 --- SPECS.md | 2 + .../java/com/evsuite/abrp/MainActivity.java | 102 +++++++++++++++++- app/src/main/res/layout/pane_log.xml | 3 + app/src/main/res/values/strings.xml | 2 + 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/SPECS.md b/SPECS.md index aa56f6f..b067264 100644 --- a/SPECS.md +++ b/SPECS.md @@ -42,6 +42,8 @@ what has been changed, and what is still to do. - 2026-10-17 — Renamed to "Nils' MG4": package ID `com.stinnesbeck.nilsmg4` (`.unstable` for the car build), APK names `NilsMG4-*.apk`. It installs next to the old FatihsMG4 app instead of replacing it, and starts with empty settings and history. +- 2026-10-17 — Diagnostics section on the Logs tab: UID, signer, car permissions, last vehicle + read and its error, and the vehicle library's in-app log. For debugging without adb. ## Installing on the car diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 475686c..8d96209 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -102,6 +102,16 @@ public class MainActivity extends AppCompatActivity { private ChargeSessionTracker chargeSessionTracker; private ChargingGraphView chargeGraph; + /** + * What the vehicle reads last did, for the diagnostics section of the Logs page. A failed + * read leaves the cells showing a dash, which looks the same as a signal the car does not + * publish; this is the only on-screen trace of why. + */ + private TextView diagnosticsText; + private volatile EnergySnapshot lastVehicleSnapshot; + private volatile long lastVehicleReadOkMs; + private volatile String lastVehicleReadError; + /** * Inflates a page with no parent, then gives it the MATCH_PARENT/MATCH_PARENT layout * params ViewPager2 requires of its items — inflating detached leaves them null, and @@ -168,6 +178,7 @@ public class MainActivity extends AppCompatActivity { } ((TextView) findViewById(R.id.header_title)).setText(PAGE_TITLES[position]); ((TextView) findViewById(R.id.header_subtitle)).setText(PAGE_SUBTITLES[position]); + if (panes[position] == logPane) refreshDiagnostics(); } /** Refreshes state + call log while the screen is visible. */ @@ -216,6 +227,9 @@ public class MainActivity extends AppCompatActivity { statusText = servicePane.findViewById(R.id.status_text); callLogText = logPane.findViewById(R.id.call_log_text); logPane.findViewById(R.id.flush_now_button).setOnClickListener(v -> manualFlush()); + diagnosticsText = logPane.findViewById(R.id.diagnostics_text); + logPane.findViewById(R.id.diagnostics_refresh_button) + .setOnClickListener(v -> refreshDiagnostics()); setUpPager(); findViewById(R.id.about_button).setOnClickListener(v -> showAbout()); @@ -407,6 +421,8 @@ public class MainActivity extends AppCompatActivity { } // The timestamp is passed explicitly: its Kotlin default is not visible from Java. EnergySnapshot s = vehicleReader.read(System.currentTimeMillis()); + lastVehicleSnapshot = s; + lastVehicleReadOkMs = System.currentTimeMillis(); ConsumptionTracker.get(this).sample(s); refreshCharging(s); @@ -423,8 +439,12 @@ public class MainActivity extends AppCompatActivity { vehTireFr.setText(fmt(s.getTirePressures().getFrontRightKpa(), "%.0f kPa")); vehTireRl.setText(fmt(s.getTirePressures().getRearLeftKpa(), "%.0f kPa")); vehTireRr.setText(fmt(s.getTirePressures().getRearRightKpa(), "%.0f kPa")); - } catch (Throwable ignored) { - // Leave the last good reading on screen. + } catch (Throwable t) { + // Leave the last good reading on screen, but keep the reason for the diagnostics. + String trace = android.util.Log.getStackTraceString(t); + lastVehicleReadError = new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US) + .format(new java.util.Date()) + " " + + (trace.length() > 2000 ? trace.substring(0, 2000) + "…" : trace); } } @@ -749,6 +769,84 @@ public class MainActivity extends AppCompatActivity { callLogText.setText(sb.toString()); } + // ---------- Diagnostics ---------- + + /** + * Fills the diagnostics section: who the app runs as, how it is signed, which car + * permissions it holds, what the last vehicle read returned, and the in-app log of the + * vehicle library. On the head unit there is no adb, so this is what can be photographed. + */ + private void refreshDiagnostics() { + if (diagnosticsText == null) return; + java.text.SimpleDateFormat time = new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US); + StringBuilder sb = new StringBuilder(); + int uid = android.os.Process.myUid(); + sb.append("Package ").append(getPackageName()).append('\n'); + sb.append("Version ").append(BuildConfig.VERSION_NAME).append('\n'); + sb.append("UID ").append(uid) + .append(uid == android.os.Process.SYSTEM_UID ? " (system — OK)" : " (NOT system)").append('\n'); + sb.append("Firmware ").append(safe(() -> com.evsuite.hardware.FirmwareInfo.INSTANCE.getDetectedString())) + .append('\n'); + PackageManager pm = getPackageManager(); + try { + @SuppressWarnings("deprecation") + android.content.pm.PackageInfo info = pm.getPackageInfo(getPackageName(), + PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS); + if (info.signatures != null && info.signatures.length > 0) { + byte[] digest = java.security.MessageDigest.getInstance("SHA-256") + .digest(info.signatures[0].toByteArray()); + StringBuilder hex = new StringBuilder(); + for (byte b : digest) hex.append(String.format(java.util.Locale.US, "%02x", b)); + sb.append("Signer ").append(hex).append('\n'); + } + if (info.requestedPermissions != null) { + sb.append("Permissions\n"); + for (String p : info.requestedPermissions) { + if (!p.startsWith("android.car.")) continue; + boolean granted = checkSelfPermission(p) == PackageManager.PERMISSION_GRANTED; + sb.append(" ").append(granted ? "granted " : "DENIED ") + .append(p.substring(p.lastIndexOf('.') + 1)).append('\n'); + } + } + } catch (Exception e) { + sb.append("Package info unavailable: ").append(e).append('\n'); + } + + sb.append('\n').append("Last vehicle read ") + .append(lastVehicleReadOkMs > 0 ? time.format(new java.util.Date(lastVehicleReadOkMs)) : "never") + .append('\n'); + EnergySnapshot s = lastVehicleSnapshot; + if (s != null) { + sb.append(" SOC ").append(s.getSocPercent()) + .append(" range ").append(s.getRangeKm()) + .append(" speed ").append(s.getSpeedKmh()) + .append(" power ").append(s.getBatteryPowerKw()).append('\n') + .append(" odometer ").append(s.getOdometerKm()) + .append(" charge status ").append(s.getChargingStatus()) + .append(" port ").append(s.getChargePortConnected()).append('\n'); + } + String error = lastVehicleReadError; + if (error != null) sb.append("Last read error ").append(error).append('\n'); + + java.util.List entries = + com.evsuite.hardware.AppLogger.INSTANCE.getEntries(); + int shown = Math.min(entries.size(), 200); + sb.append('\n').append("Vehicle library log (newest first, ").append(shown) + .append(" of ").append(entries.size()).append(")\n"); + for (int i = entries.size() - 1; i >= entries.size() - shown; i--) { + com.evsuite.hardware.AppLogger.Entry e = entries.get(i); + sb.append(e.getTime()).append(' ').append(e.getLevel().name().charAt(0)).append('/') + .append(e.getTag()).append(": ").append(e.getMsg()).append('\n'); + } + diagnosticsText.setText(sb.toString()); + } + + private interface StringSupplier { String get() throws Exception; } + + private static String safe(StringSupplier supplier) { + try { return supplier.get(); } catch (Throwable t) { return "unavailable (" + t + ")"; } + } + // ---------- Service status ---------- private void refreshStatus() { diff --git a/app/src/main/res/layout/pane_log.xml b/app/src/main/res/layout/pane_log.xml index 31ea5d8..7049563 100644 --- a/app/src/main/res/layout/pane_log.xml +++ b/app/src/main/res/layout/pane_log.xml @@ -4,5 +4,8 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 341259d..11039fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,6 +107,8 @@ Recent uploads No upload attempted yet. + Diagnostics + Refresh diagnostics Upload now Starting… Running — waiting for first upload