diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb90af..106465e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - Charging sessions are now metered on DC only (10 kW and above). An AC charge is an overnight affair the head unit sleeps through, so its curve, duration and energy were - guesses; the charging screen labels one as `AC şarj (ölçülmüyor)` rather than showing + guesses; the charging screen labels one as `AC şarj (ölçülmüyor)` ("AC charging (not metered)") rather than showing the previous DC session's figures next to it. ### Fixed @@ -246,7 +246,7 @@ All notable changes to this project are documented here. Format follows - OTA installs now go through the platform `PackageInstaller` instead of shelling out to `pm install -r`. The old path handed the package manager service a file inside the app's private cache, which it cannot read from its own process, and every such failure was - reported as `Kurulum reddedildi (imza uyuşmuyor?)` — a signature verdict on a correctly + reported as `Kurulum reddedildi (imza uyuşmuyor?)` ("installation refused (signature mismatch?)") — a signature verdict on a correctly signed build. A session streams the archive from this process, so no second process has to reach our cache. Approach taken from merthankaraman/DriveHub_Dort, which solved the same problem on the same head unit. diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt index f6e50c6..9ce7fb5 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt @@ -335,18 +335,18 @@ object EVHardware { } /** - * État d'ignition courant, en valeurs [IgnitionState] (AAOS). + * Current ignition state, as [IgnitionState] (AAOS) values. * - * CarPropertyManager d'abord. Mais SWI68 n'expose pas IGNITION_STATE via CPM : la - * property revient vide, et l'état d'allumage passait pour illisible alors que Katman5 - * le rapporte correctement — le log le montre (`Katman5: state initial = 2 (RUN/READY)`) - * pendant que le diagnostic classait IGNITION_STATE en NOT_READABLE. + * CarPropertyManager first. But SWI68 does not expose IGNITION_STATE through CPM: the + * property comes back empty, and the ignition state looked unreadable although Katman5 + * reports it correctly — the log shows it (`Katman5: state initial = 2 (RUN/READY)`) while + * the diagnostics classed IGNITION_STATE as NOT_READABLE. * - * D'où le repli sur le dernier état livré par IVehicleConditionService, converti : - * Katman5 parle en [CarIgnitionItem], cette fonction rend du [IgnitionState], et les - * deux échelles ne coïncident pas (RUN vaut 2 chez l'un, OFF vaut 2 chez l'autre). + * Hence the fallback to the last state delivered by IVehicleConditionService, converted: + * Katman5 speaks [CarIgnitionItem], this function returns [IgnitionState], and the two + * scales do not line up (RUN is 2 in one, OFF is 2 in the other). * - * Returns -1 si rien n'est lisible, 0 si la property existe mais ne dit rien. + * Returns -1 if nothing is readable, 0 if the property exists but says nothing. */ fun getCurrentIgnitionState(): Int { val v0 = getIntPropertyCPM(PROP_IGNITION_STATE, 0) @@ -356,7 +356,7 @@ object EVHardware { return vcmIgnitionAsAaos() ?: global } - /** Le dernier état Katman5 traduit en [IgnitionState], ou null s'il n'y en a pas eu. */ + /** The last Katman5 state translated into [IgnitionState], or null if there was none. */ private fun vcmIgnitionAsAaos(): Int? = when (sLastVcmIgnitionState) { CarIgnitionItem.OFF -> IgnitionState.OFF CarIgnitionItem.ACCESSORY -> IgnitionState.ACC diff --git a/OTA.md b/OTA.md index 79d4c66..240b6c7 100644 --- a/OTA.md +++ b/OTA.md @@ -39,7 +39,7 @@ release is a candidate only if all of these hold: | Marked **pre-release** | `release.optBoolean("prerelease")` | Skipped entirely | | Asset name contains `unstable` | `name.contains("unstable")` | Asset ignored | | Asset name ends `-.apk` | `ASSET_VERSION` regex | No version, asset ignored | -| Version strictly higher than installed | `isNewer()` | Nothing to do, reports "Güncel" | +| Version strictly higher than installed | `isNewer()` | Nothing to do, reports "Up to date" | | Download host on the allowlist | `isAllowedUrl()` | Refused and logged | | APK signed with the **same certificate** as the running app | Android `PackageInstaller` | Installation is rejected by the platform | | Matching `.sha256` asset or `SHA256SUMS` entry | `fetchExpectedSha256()` | Update ignored/refused | @@ -115,17 +115,13 @@ The version comes from the **asset filename**, not the git tag. The tag can stay ## When it says something else -| Toast | Meaning | +| Message | Meaning | |---|---| -| `Güncel (2.2.2.0-unstable)` | No release beat the installed version. Check the pre-release tick and the asset filename. | -| `Güncelleme reddedildi: SHA-256 doğrulaması yok` | APK has no matching sidecar, or the sidecar is invalid/unreachable. | -| `İndirme veya SHA-256 doğrulaması başarısız` | Download failed or its bytes do not match the published hash. | -| `Kuruluyor: 2.2.3 — ekrandaki onayı bekleyin` | The session was committed. Not done yet: watch for the platform's own toast, or a confirmation dialog to accept. | -| `Kurulum reddedildi: imza uyuşmuyor` | Genuinely the wrong key — rebuild and re-sign with `platform.pk8`. Unlike the old catch-all, this one means what it says. | -| `Kurulum reddedildi: APK okunamadı` | The archive does not parse, or it is built for another package. | -| `Kurulum başlatılamadı … Log sayfasına bakın` | The session could not be created or committed. `adb logcat -s EVABRP.Update`. | -| `Kurulum başarısız: INSTALL_FAILED_…` | From `OtaInstallResultReceiver` — the package manager's own words for why it refused. | -| `Güncelleme kontrolü başarısız` | The check threw. `adb logcat -s EVABRP.Update OtaUpdater` if a cable is available; otherwise the in-app Log page. | +| `Up to date (2.14.2-unstable)` | No release beat the installed version. Check the pre-release tick and the asset filename. | +| `Update refused: no SHA-256 verification` | The APK has no matching sidecar, or the sidecar is invalid or unreachable. | +| `Download or SHA-256 verification failed (…)` | The download failed, or its bytes do not match the published hash. | +| `Update check failed` | The check threw. `adb logcat -s UpdateHook OtaUpdater` if a cable is available; otherwise the in-app Logs tab. | +| `Update check already in progress` | A check is still running; wait for its result. | ## How the install works, and why it is not `pm install` @@ -137,7 +133,7 @@ The downloaded APK lives in `context.getCacheDir()`. `pm` does not install the f — it passes the path to the package manager service, which opens it from *its* process and SELinux context, where an app's private cache is not readable. The install failed, and because `install()` returned a bare boolean, the caller reported the only failure it knew -how to name: `Kurulum reddedildi (imza uyuşmuyor?)`. A correctly signed build, refused with +how to name: `Kurulum reddedildi (imza uyuşmuyor?)` ("installation refused (signature mismatch?)"). A correctly signed build, refused with a signature error it had not earned. The `pm` output that would have said so was captured and thrown away. diff --git a/SPECS.md b/SPECS.md index 8185669..e2155c8 100644 --- a/SPECS.md +++ b/SPECS.md @@ -84,8 +84,9 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * (`GF_DATABASE_TYPE`, `GF_DATABASE_HOST`, …); the replicas then share that database instead of a volume. The car data itself is in InfluxDB and unaffected. - [ ] **Telemetry queue survives restarts.** Unsent live samples are kept in memory only. -- [ ] **Translate the rest.** The docs (`CHANGELOG.md`, `OTA.md`'s - message table, comments in `mise.toml`/`app/build.gradle`) are still Turkish or French. +- [ ] **EVHardware's comments are partly French.** About 120 comment lines in the vendored + `EVHardware/` library (mostly `EVHardware.kt`, some tests), half-translated upstream. The + app, the docs and the build files are English. - [ ] **Move hardcoded UI strings into `strings.xml`.** Many are inline in Java and layouts. ## Done @@ -116,6 +117,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 — Translated the rest: `mise.toml`, the comments in `app/build.gradle`, `OTA.md`'s + message table (now the updater's current English messages), the Turkish messages quoted in + `CHANGELOG.md` and `OTA.md` (glossed), two test messages, the ignition-state comment in + `EVHardware.kt`. - 2026-10-18 — About dialog: link and QR code point to https://git.stinnesbeck.com/nils/mg4 (were Fatih's GitHub). Status section brought up to date. - 2026-10-18 — App 2.14.2: "Range, 5-min average", like the usage and power cards (was "Range at 5-min average"). diff --git a/app/build.gradle b/app/build.gradle index e9e9959..3122615 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,14 +10,14 @@ android { } } - // Signature de l'APK — rien à voir avec la clé d'API ABRP de l'utilisateur. - // Ces valeurs servent UNIQUEMENT au build : elles prouvent que l'APK vient bien de - // nous et n'entrent jamais dans l'application. La clé d'API et le token ABRP, eux, - // sont saisis dans l'app et stockés chiffrés sur l'appareil (voir SecurePrefs). + // APK signing — nothing to do with the user's ABRP credentials. These values are ONLY for + // the build: they prove the APK comes from us and never enter the app. The ABRP API key and + // token are entered in the app and stored encrypted on the device (see SecurePrefs). + // The car build is signed by build-car (flake.nix) with the AOSP platform key instead. // - // Un keystore contient plusieurs clés : SIGNING_STORE_PASSWORD ouvre le conteneur, - // SIGNING_KEY_ALIAS choisit la clé, SIGNING_KEY_PASSWORD ouvre cette clé-là. - // Lues depuis l'environnement (CI) ou gradle.properties local — JAMAIS commitées. + // A keystore holds several keys: SIGNING_STORE_PASSWORD opens the container, + // SIGNING_KEY_ALIAS picks the key, SIGNING_KEY_PASSWORD opens that key. + // Read from the environment (CI) or a local gradle.properties — NEVER committed. def keystorePath = System.getenv("SIGNING_KEYSTORE") ?: project.findProperty("signing.keystore") signingConfigs { if (keystorePath != null && file(keystorePath).exists()) { @@ -43,15 +43,15 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - // null quand aucun keystore n'est configuré : le build reste possible sur une - // machine sans secrets (APK non signé), au lieu d'échouer. + // null when no keystore is configured: the build still works on a machine without + // secrets (unsigned APK) instead of failing. def platformSigning = signingConfigs.findByName("platform") - // Deux canaux de distribution : - // - stable : diffusé en release taguée, AUCUN auto-update. L'utilisateur installe - // lui-même ; rien dans l'app ne va chercher un APK. - // - unstable : pré-releases publiées à chaque push sur master, avec OTA pour que les testeurs - // soient à jour sans manipulation. Installable à côté du stable. + // Two distribution channels: + // - stable: no self-update at all. The user installs it; nothing in the app fetches an + // APK (the updater is not even in this APK). + // - unstable: the car build (system UID, platform-signed), with the OTA updater, so the + // car can update itself from published pre-releases. Installs next to stable. flavorDimensions += "channel" productFlavors { stable { @@ -111,13 +111,14 @@ tasks.register('printUnstableVersion') { } } -// Nomme les APK par canal pour que les artefacts de release soient lisibles. +// Names the APKs by channel so the build outputs are readable. androidComponents { onVariants(selector().all()) { variant -> variant.outputs.each { output -> if (variant.buildType == "release") { - // Le canal unstable garde un nom de fichier stable : l'updater identifie - // la version par le tag de la pré-release, jamais par le nom d'APK. + // A fixed name for the unstable release output. What is published for the car + // comes from build-car, named NilsMG4-unstable-.apk: that file name is + // where the updater reads a release's version from. output.outputFileName.set(variant.flavorName == "unstable" ? "NilsMG4-unstable.apk" : "NilsMG4-${variant.flavorName}-${output.versionName.get()}.apk") diff --git a/app/src/main/res/drawable/ic_tab_log.xml b/app/src/main/res/drawable/ic_tab_log.xml index 67ce1ca..feb44c6 100644 --- a/app/src/main/res/drawable/ic_tab_log.xml +++ b/app/src/main/res/drawable/ic_tab_log.xml @@ -1,5 +1,5 @@ - + local.properties -echo "SDK installé, local.properties écrit" +echo "SDK installed, local.properties written" """ [tasks.build] -description = "Compile l'APK debug" +description = "Builds the debug APK" run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME assembleStableDebug' [tasks.test] -description = "Tests unitaires JVM" +description = "JVM unit tests" run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME testStableDebugUnitTest testUnstableDebugUnitTest' [tasks.lint] -description = "Analyse lint Android" +description = "Android lint" run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME lintStableDebug lintUnstableDebug' [tasks.permissions] -description = "Contrôle de dérive des permissions (le même que la CI)" +description = "Permission drift check (the same as CI)" run = "bash .github/security/check-permissions.sh" [tasks.check] -description = "Ce que la CI exécute : permissions + lint + tests" +description = "What CI runs: permissions + lint + tests" depends = ["permissions", "lint", "test"] [tasks.install] -description = "Installe l'APK debug sur le véhicule connecté en ADB" +description = "Installs the debug APK on the car connected over ADB" run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME installStableDebug' [tasks.logs] -description = "Suit les logs de l'app sur le véhicule" +description = "Follows the app's logs on the car" run = "adb logcat -s AbrpUploadService:V CarPropertyAdapter:V VhalProbe:V SecurePrefs:V OtaUpdater:V UpdateHook:V NilsMG4.Telemetry:V NilsMG4.Mongo:V NilsMG4.SteeringKey:V EVABRP.Boot:V EVABRP.Wifi:V" [tasks.release] -description = "APK release des deux canaux (stable sans OTA, unstable avec OTA)" +description = "Release APKs of both channels (stable without OTA, unstable with OTA)" run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME assembleStableRelease assembleUnstableRelease' [tasks.clean] run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME clean' [tasks.emulator-setup] -description = "Installe l'émulateur + les images système et crée les deux AVD" +description = "Installs the emulator and system images and creates both AVDs" run = ''' set -e SDK="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" AVDM="$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" if [ ! -e /dev/kvm ]; then - echo "ERREUR : /dev/kvm absent — pas d'accélération matérielle, l'émulateur sera inutilisable." >&2 - echo "Sous WSL2 : activer la virtualisation imbriquée côté Windows." >&2 + echo "ERROR: /dev/kvm missing — no hardware acceleration, the emulator would be unusable." >&2 + echo "On WSL2: enable nested virtualisation on the Windows side." >&2 exit 1 fi if ! id -nG | tr ' ' '\n' | grep -qx kvm; then - echo "ATTENTION : utilisateur absent du groupe kvm. Corriger avec :" >&2 - echo " sudo usermod -aG kvm $USER # puis se reconnecter" >&2 + echo "WARNING: user is not in the kvm group. Fix with:" >&2 + echo " sudo usermod -aG kvm $USER # then log in again" >&2 fi yes | "$SDK" --sdk_root="$ANDROID_HOME" --licenses > /dev/null @@ -153,13 +150,13 @@ yes | "$SDK" --sdk_root="$ANDROID_HOME" --licenses > /dev/null create_avd() { name="$1"; image="$2"; device="$3" if "$AVDM" list avd 2>/dev/null | grep -q "Name: $name"; then - echo "AVD $name déjà présent" + echo "AVD $name already exists" else echo no | "$AVDM" create avd -n "$name" -k "$image" -d "$device" --force fi cfg="$HOME/.android/avd/$name.avd/config.ini" - # Écran du MG4. Sans ça l'AVD garde la densité du profil générique et les layouts - # ne ressemblent pas à ce que voit le conducteur. + # The MG4's screen. Without this the AVD keeps the generic profile's density and the + # layouts do not look like what the driver sees. sed -i '/^hw.lcd/d;/^disk.dataPartition.size/d' "$cfg" { echo "hw.lcd.width=$EMU_WIDTH" @@ -167,55 +164,55 @@ create_avd() { echo "hw.lcd.density=$EMU_DENSITY" echo "disk.dataPartition.size=4G" } >> "$cfg" - echo "AVD $name configuré en ${EMU_WIDTH}x${EMU_HEIGHT} @ ${EMU_DENSITY}dpi" + echo "AVD $name set to ${EMU_WIDTH}x${EMU_HEIGHT} @ ${EMU_DENSITY}dpi" } create_avd "$EMU_CAR_AVD" "$EMU_CAR_IMAGE" "automotive_1024p_landscape" create_avd "$EMU_SCREEN_AVD" "$EMU_SCREEN_IMAGE" "pixel_c" ''' -# swiftshader_indirect, pas « host » : WSL2 n'expose aucun GPU réel (uniquement Mesa -# llvmpipe). Avec -gpu host l'émulateur meurt au démarrage sur « Your GPU cannot be used -# for hardware rendering », avant qu'aucune fenêtre n'apparaisse — d'où l'icône de tray -# sans émulateur visible. Le rendu logiciel est plus lent, mais il démarre. +# swiftshader_indirect, not "host": WSL2 exposes no real GPU (only Mesa llvmpipe). With +# -gpu host the emulator dies at start-up on "Your GPU cannot be used for hardware +# rendering", before any window appears — hence a tray icon and no visible emulator. +# Software rendering is slower, but it starts. [tasks.emulator-car] -description = "Lance l'émulateur Automotive (API 33 — a un CarPropertyManager)" +description = "Starts the Automotive emulator (API 33 — has a CarPropertyManager)" run = ''' set -e "$ANDROID_HOME/emulator/emulator" -avd "$EMU_CAR_AVD" \ -gpu swiftshader_indirect -no-snapshot-save -no-boot-anim & "$ANDROID_HOME/platform-tools/adb" wait-for-device -echo "Émulateur voiture prêt." +echo "Car emulator ready." ''' [tasks.emulator-screen] -description = "Lance l'émulateur écran MG4 (API 28, 1920x720 @ 160dpi — pas de service voiture)" +description = "Starts the MG4 screen emulator (API 28, 1920x720 @ 160dpi — no car services)" run = ''' set -e "$ANDROID_HOME/emulator/emulator" -avd "$EMU_SCREEN_AVD" \ -gpu swiftshader_indirect -no-snapshot-save -no-boot-anim & "$ANDROID_HOME/platform-tools/adb" wait-for-device -echo "Émulateur écran prêt." +echo "Screen emulator ready." ''' [tasks.emulator-stop] -description = "Arrête l'émulateur en cours" +description = "Stops the running emulator" run = '"$ANDROID_HOME/platform-tools/adb" emu kill || true' [tasks.run] -description = "Compile, installe et lance l'APK sur l'émulateur ou le véhicule connecté" +description = "Builds, installs and starts the APK on the emulator or the connected car" depends = ["build"] run = ''' set -e ADB="$ANDROID_HOME/platform-tools/adb" if [ -z "$($ADB devices | sed '1d' | grep -w device || true)" ]; then - echo "Aucun appareil connecté. Lancer d'abord 'mise run emulator-car' ou 'mise run emulator-screen'," >&2 - echo "ou brancher le véhicule en ADB." >&2 + echo "No device connected. Start 'mise run emulator-car' or 'mise run emulator-screen' first," >&2 + echo "or connect the car over ADB." >&2 exit 1 fi APK=$(ls -t app/build/outputs/apk/stable/debug/*.apk | head -1) -echo "Installation de $APK" +echo "Installing $APK" $ADB install -r -g "$APK" $ADB shell am start -n "com.stinnesbeck.nilsmg4/com.evsuite.abrp.MainActivity" -echo "Lancé. Logs : mise run logs" +echo "Started. Logs: mise run logs" '''