Translate the remaining French and Turkish
mise.toml, the comments in app/build.gradle (corrected where they no longer described the build: the car build is signed by build-car, and the updater reads the version from build-car's file name), OTA.md's message table (now the updater's current English messages), the Turkish messages quoted in CHANGELOG.md and OTA.md (glossed in English), two test assertion messages, an icon comment, and the ignition-state comment in EVHardware.kt. The vendored EVHardware library still has about 120 half-French comment lines; noted in SPECS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
+2
-2
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
- Charging sessions are now metered on DC only (10 kW and above). An AC charge is an
|
- 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
|
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.
|
the previous DC session's figures next to it.
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- 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
|
`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
|
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
|
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
|
to reach our cache. Approach taken from merthankaraman/DriveHub_Dort, which solved the
|
||||||
same problem on the same head unit.
|
same problem on the same head unit.
|
||||||
|
|||||||
@@ -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
|
* CarPropertyManager first. But SWI68 does not expose IGNITION_STATE through CPM: the
|
||||||
* property revient vide, et l'état d'allumage passait pour illisible alors que Katman5
|
* property comes back empty, and the ignition state looked unreadable although Katman5
|
||||||
* le rapporte correctement — le log le montre (`Katman5: state initial = 2 (RUN/READY)`)
|
* reports it correctly — the log shows it (`Katman5: state initial = 2 (RUN/READY)`) while
|
||||||
* pendant que le diagnostic classait IGNITION_STATE en NOT_READABLE.
|
* the diagnostics classed IGNITION_STATE as NOT_READABLE.
|
||||||
*
|
*
|
||||||
* D'où le repli sur le dernier état livré par IVehicleConditionService, converti :
|
* Hence the fallback to the last state delivered by IVehicleConditionService, converted:
|
||||||
* Katman5 parle en [CarIgnitionItem], cette fonction rend du [IgnitionState], et les
|
* Katman5 speaks [CarIgnitionItem], this function returns [IgnitionState], and the two
|
||||||
* deux échelles ne coïncident pas (RUN vaut 2 chez l'un, OFF vaut 2 chez l'autre).
|
* 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 {
|
fun getCurrentIgnitionState(): Int {
|
||||||
val v0 = getIntPropertyCPM(PROP_IGNITION_STATE, 0)
|
val v0 = getIntPropertyCPM(PROP_IGNITION_STATE, 0)
|
||||||
@@ -356,7 +356,7 @@ object EVHardware {
|
|||||||
return vcmIgnitionAsAaos() ?: global
|
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) {
|
private fun vcmIgnitionAsAaos(): Int? = when (sLastVcmIgnitionState) {
|
||||||
CarIgnitionItem.OFF -> IgnitionState.OFF
|
CarIgnitionItem.OFF -> IgnitionState.OFF
|
||||||
CarIgnitionItem.ACCESSORY -> IgnitionState.ACC
|
CarIgnitionItem.ACCESSORY -> IgnitionState.ACC
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ release is a candidate only if all of these hold:
|
|||||||
| Marked **pre-release** | `release.optBoolean("prerelease")` | Skipped entirely |
|
| Marked **pre-release** | `release.optBoolean("prerelease")` | Skipped entirely |
|
||||||
| Asset name contains `unstable` | `name.contains("unstable")` | Asset ignored |
|
| Asset name contains `unstable` | `name.contains("unstable")` | Asset ignored |
|
||||||
| Asset name ends `-<version>.apk` | `ASSET_VERSION` regex | No version, asset ignored |
|
| Asset name ends `-<version>.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 |
|
| 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 |
|
| 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 |
|
| 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
|
## 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. |
|
| `Up to date (2.14.2-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. |
|
| `Update refused: no SHA-256 verification` | The APK has no matching sidecar, or the sidecar is invalid or unreachable. |
|
||||||
| `İndirme veya SHA-256 doğrulaması başarısız` | Download failed or its bytes do not match the published hash. |
|
| `Download or SHA-256 verification failed (…)` | The 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. |
|
| `Update check failed` | The check threw. `adb logcat -s UpdateHook OtaUpdater` if a cable is available; otherwise the in-app Logs tab. |
|
||||||
| `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. |
|
| `Update check already in progress` | A check is still running; wait for its result. |
|
||||||
| `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. |
|
|
||||||
|
|
||||||
## How the install works, and why it is not `pm install`
|
## 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
|
— 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
|
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
|
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
|
a signature error it had not earned. The `pm` output that would have said so was captured
|
||||||
and thrown away.
|
and thrown away.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
(`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.
|
of a volume. The car data itself is in InfluxDB and unaffected.
|
||||||
- [ ] **Telemetry queue survives restarts.** Unsent live samples are kept in memory only.
|
- [ ] **Telemetry queue survives restarts.** Unsent live samples are kept in memory only.
|
||||||
- [ ] **Translate the rest.** The docs (`CHANGELOG.md`, `OTA.md`'s
|
- [ ] **EVHardware's comments are partly French.** About 120 comment lines in the vendored
|
||||||
message table, comments in `mise.toml`/`app/build.gradle`) are still Turkish or French.
|
`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.
|
- [ ] **Move hardcoded UI strings into `strings.xml`.** Many are inline in Java and layouts.
|
||||||
|
|
||||||
## Done
|
## 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
|
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
|
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.
|
`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
|
- 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.
|
(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").
|
- 2026-10-18 — App 2.14.2: "Range, 5-min average", like the usage and power cards (was "Range at 5-min average").
|
||||||
|
|||||||
+18
-17
@@ -10,14 +10,14 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature de l'APK — rien à voir avec la clé d'API ABRP de l'utilisateur.
|
// APK signing — nothing to do with the user's ABRP credentials. These values are ONLY for
|
||||||
// Ces valeurs servent UNIQUEMENT au build : elles prouvent que l'APK vient bien de
|
// the build: they prove the APK comes from us and never enter the app. The ABRP API key and
|
||||||
// nous et n'entrent jamais dans l'application. La clé d'API et le token ABRP, eux,
|
// token are entered in the app and stored encrypted on the device (see SecurePrefs).
|
||||||
// sont saisis dans l'app et stockés chiffrés sur l'appareil (voir 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,
|
// A keystore holds several keys: SIGNING_STORE_PASSWORD opens the container,
|
||||||
// SIGNING_KEY_ALIAS choisit la clé, SIGNING_KEY_PASSWORD ouvre cette clé-là.
|
// SIGNING_KEY_ALIAS picks the key, SIGNING_KEY_PASSWORD opens that key.
|
||||||
// Lues depuis l'environnement (CI) ou gradle.properties local — JAMAIS commitées.
|
// Read from the environment (CI) or a local gradle.properties — NEVER committed.
|
||||||
def keystorePath = System.getenv("SIGNING_KEYSTORE") ?: project.findProperty("signing.keystore")
|
def keystorePath = System.getenv("SIGNING_KEYSTORE") ?: project.findProperty("signing.keystore")
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (keystorePath != null && file(keystorePath).exists()) {
|
if (keystorePath != null && file(keystorePath).exists()) {
|
||||||
@@ -43,15 +43,15 @@ android {
|
|||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
// null quand aucun keystore n'est configuré : le build reste possible sur une
|
// null when no keystore is configured: the build still works on a machine without
|
||||||
// machine sans secrets (APK non signé), au lieu d'échouer.
|
// secrets (unsigned APK) instead of failing.
|
||||||
def platformSigning = signingConfigs.findByName("platform")
|
def platformSigning = signingConfigs.findByName("platform")
|
||||||
|
|
||||||
// Deux canaux de distribution :
|
// Two distribution channels:
|
||||||
// - stable : diffusé en release taguée, AUCUN auto-update. L'utilisateur installe
|
// - stable: no self-update at all. The user installs it; nothing in the app fetches an
|
||||||
// lui-même ; rien dans l'app ne va chercher un APK.
|
// APK (the updater is not even in this APK).
|
||||||
// - unstable : pré-releases publiées à chaque push sur master, avec OTA pour que les testeurs
|
// - unstable: the car build (system UID, platform-signed), with the OTA updater, so the
|
||||||
// soient à jour sans manipulation. Installable à côté du stable.
|
// car can update itself from published pre-releases. Installs next to stable.
|
||||||
flavorDimensions += "channel"
|
flavorDimensions += "channel"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
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 {
|
androidComponents {
|
||||||
onVariants(selector().all()) { variant ->
|
onVariants(selector().all()) { variant ->
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
if (variant.buildType == "release") {
|
if (variant.buildType == "release") {
|
||||||
// Le canal unstable garde un nom de fichier stable : l'updater identifie
|
// A fixed name for the unstable release output. What is published for the car
|
||||||
// la version par le tag de la pré-release, jamais par le nom d'APK.
|
// comes from build-car, named NilsMG4-unstable-<version>.apk: that file name is
|
||||||
|
// where the updater reads a release's version from.
|
||||||
output.outputFileName.set(variant.flavorName == "unstable"
|
output.outputFileName.set(variant.flavorName == "unstable"
|
||||||
? "NilsMG4-unstable.apk"
|
? "NilsMG4-unstable.apk"
|
||||||
: "NilsMG4-${variant.flavorName}-${output.versionName.get()}.apk")
|
: "NilsMG4-${variant.flavorName}-${output.versionName.get()}.apk")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Terminal : onglet Log. Teinté par @color/nav_tab_foreground. -->
|
<!-- Terminal: the Logs tab. Tinted by @color/nav_tab_foreground. -->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class TelemetryPayloadTest {
|
|||||||
"batt_temp", "capacity", "odometer", "hvac_setpoint",
|
"batt_temp", "capacity", "odometer", "hvac_setpoint",
|
||||||
"tire_pressure_fl", "tire_pressure_fr",
|
"tire_pressure_fl", "tire_pressure_fr",
|
||||||
"tire_pressure_rl", "tire_pressure_rr"}) {
|
"tire_pressure_rl", "tire_pressure_rr"}) {
|
||||||
assertFalse("le champ " + field + " aurait dû être omis", json.contains(field));
|
assertFalse("field " + field + " should have been omitted", json.contains(field));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ public class TelemetryPayloadTest {
|
|||||||
TelemetryPayload t = new TelemetryPayload(1L);
|
TelemetryPayload t = new TelemetryPayload(1L);
|
||||||
t.speedKmh = 48.4f;
|
t.speedKmh = 48.4f;
|
||||||
JSONObject json = new JSONObject(t.build());
|
JSONObject json = new JSONObject(t.build());
|
||||||
assertFalse("soc illisible aurait dû être omis", json.has("soc"));
|
assertFalse("an unreadable soc should have been omitted", json.has("soc"));
|
||||||
assertEquals(48, json.getInt("speed"));
|
assertEquals(48, json.getInt("speed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
# Environnement de développement — EVABRPUploader.
|
# Development environment with mise (the Nix flake, `nix develop`, is the other way in).
|
||||||
#
|
#
|
||||||
# Commité volontairement : il décrit comment construire le projet, pas la machine de
|
# Committed on purpose: it describes how to build the project, not someone's machine. Paths
|
||||||
# quelqu'un. Les chemins passent par {{env.HOME}} / JAVA_HOME, jamais en dur.
|
# go through {{env.HOME}} / JAVA_HOME, never hard-coded. For a local override (keystore path,
|
||||||
# Pour une surcharge locale (chemin de keystore, SDK ailleurs), créer un mise.local.toml
|
# SDK elsewhere), create a mise.local.toml — that one stays ignored by git.
|
||||||
# — celui-là reste ignoré par git.
|
|
||||||
#
|
#
|
||||||
# mise install installe le JDK
|
# mise install installs the JDK
|
||||||
# mise run bootstrap installe le SDK Android si absent
|
# mise run bootstrap installs the Android SDK if missing
|
||||||
# mise run test tests unitaires
|
# mise run test unit tests
|
||||||
# mise tasks liste tout
|
# mise tasks lists everything
|
||||||
#
|
#
|
||||||
# Pourquoi épingler le JDK : le JDK par défaut de la machine (26) n'est pas supporté
|
# Why the JDK is pinned: the machine's default JDK (26) is not supported by AGP and fails the
|
||||||
# par AGP et fait échouer le build avec une erreur de version de classe peu parlante.
|
# build with an unhelpful class-version error.
|
||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
java = "17"
|
java = "17"
|
||||||
@@ -24,42 +23,40 @@ _.path = [
|
|||||||
"{{env.HOME}}/Android/Sdk/cmdline-tools/latest/bin",
|
"{{env.HOME}}/Android/Sdk/cmdline-tools/latest/bin",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Version du SDK requise par app/build.gradle (compileSdk = 36).
|
# SDK version app/build.gradle needs (compileSdk = 36).
|
||||||
COMPILE_SDK = "36"
|
COMPILE_SDK = "36"
|
||||||
BUILD_TOOLS = "36.0.0"
|
BUILD_TOOLS = "36.0.0"
|
||||||
|
|
||||||
# Secrets de SIGNATURE D'APK — sans rapport avec la clé d'API ABRP, qui se saisit dans
|
# APK SIGNING secrets — nothing to do with the ABRP API key, which is entered in the app.
|
||||||
# l'application. À définir hors du dépôt : décommenter dans un mise.local.toml (ignoré
|
# Set them outside the repository: uncomment in a mise.local.toml (ignored by git) and point
|
||||||
# par git) et pointer vers un keystore local, JAMAIS vers un fichier du dépôt.
|
# at a local keystore, NEVER at a file in the repository. The car build is signed by
|
||||||
|
# `build-car` (flake.nix) instead.
|
||||||
# SIGNING_KEYSTORE = "{{env.HOME}}/.keystores/signing.keystore"
|
# SIGNING_KEYSTORE = "{{env.HOME}}/.keystores/signing.keystore"
|
||||||
# SIGNING_STORE_PASSWORD = "…"
|
# SIGNING_STORE_PASSWORD = "…"
|
||||||
# SIGNING_KEY_ALIAS = "platform"
|
# SIGNING_KEY_ALIAS = "platform"
|
||||||
# SIGNING_KEY_PASSWORD = "…"
|
# SIGNING_KEY_PASSWORD = "…"
|
||||||
|
|
||||||
# Gradle doit tourner sur le JDK que mise vient d'installer. Sans -Dorg.gradle.java.home,
|
# Gradle must run on the JDK mise just installed. Without -Dorg.gradle.java.home, Gradle may
|
||||||
# Gradle peut réutiliser un daemon lancé par un autre JDK — typiquement celui embarqué par
|
# reuse a daemon started by another JDK — typically the one bundled with VS Code's Java
|
||||||
# l'extension Java de VS Code, un JRE 21 sans jlink : le build échoue alors sur
|
# extension, a JRE 21 without jlink: the build then fails in JdkImageTransform, with a message
|
||||||
# JdkImageTransform, avec un message qui n'a rien à voir avec le code.
|
# that has nothing to do with the code. JAVA_HOME is set by [tools] above; $JAVA_HOME is
|
||||||
# JAVA_HOME est posé par [tools] ci-dessus : $JAVA_HOME est résolu par le shell au moment
|
# resolved by the shell at run time, so this stays portable between machines.
|
||||||
# de l'exécution, donc ceci reste portable d'une machine à l'autre.
|
# ── Emulator ─────────────────────────────────────────────────────────────────────
|
||||||
# ── Émulateur ────────────────────────────────────────────────────────────────────
|
|
||||||
#
|
#
|
||||||
# ATTENTION sur la fidélité : le véhicule tourne en AAOS 9 (API 28). Google ne publie
|
# A WORD on fidelity: the car runs AAOS 9 (API 28). Google publishes NO Automotive system
|
||||||
# AUCUNE image système Automotive en dessous d'API 33 — vérifié avec sdkmanager --list.
|
# image below API 33 — checked with sdkmanager --list. So no emulator reproduces both the OS
|
||||||
# Il n'existe donc pas d'émulateur qui reproduise à la fois l'OS et l'API voiture du MG4.
|
# and the car API of the MG4. Hence two profiles, each faithful on one axis:
|
||||||
# D'où deux profils, chacun fidèle sur un axe :
|
|
||||||
#
|
#
|
||||||
# emulator-car API 33 Automotive — a bien un CarPropertyManager, mauvaise version
|
# emulator-car API 33 Automotive — has a CarPropertyManager, wrong OS version. For the
|
||||||
# d'OS. Pour tester le cycle de vie du service et le chemin voiture.
|
# service lifecycle and the car path. SAIC VENDOR properties (0x216xxxxx:
|
||||||
# Les propriétés VENDEUR SAIC (0x216xxxxx : SOC, autonomie) n'existent
|
# SOC, range) do not exist in the emulator: those reads fail, as expected.
|
||||||
# pas dans l'émulateur : ces lectures échoueront, c'est normal.
|
# emulator-screen API 28 tablet — right OS version and right screen (1920x720 @ 160 dpi,
|
||||||
# emulator-screen API 28 tablette — bonne version d'OS et bon écran (1920x720 @ 160dpi,
|
# 12.8" HD like the MG4), but NO car services. For the UI, layouts and
|
||||||
# 12,8" HD comme le MG4), mais AUCUN service voiture.
|
# density. (The AOSP API 28 image also shares the car's platform key, so the
|
||||||
# Pour l'UI, les layouts et la densité.
|
# car build installs there as the system user.)
|
||||||
#
|
#
|
||||||
# Rien de tout cela ne remplace un essai sur le véhicule.
|
# None of this replaces a test on the car.
|
||||||
# Densité cible du projet : 160 dpi.
|
# Target density: 160 dpi. Screen 12.8" 1920x720. Reference firmware: SWI68-29958-1300R69.
|
||||||
# Écran 12,8" 1920x720. Firmware de référence : SWI68-29958-1300R69.
|
|
||||||
|
|
||||||
EMU_CAR_AVD = "evabrp-car"
|
EMU_CAR_AVD = "evabrp-car"
|
||||||
EMU_CAR_IMAGE = "system-images;android-33;android-automotive;x86_64"
|
EMU_CAR_IMAGE = "system-images;android-33;android-automotive;x86_64"
|
||||||
@@ -70,16 +67,16 @@ EMU_HEIGHT = "720"
|
|||||||
EMU_DENSITY = "160"
|
EMU_DENSITY = "160"
|
||||||
|
|
||||||
[tasks.bootstrap]
|
[tasks.bootstrap]
|
||||||
description = "Installe le SDK Android (platform + build-tools) s'il manque"
|
description = "Installs the Android SDK (platform + build-tools) if missing"
|
||||||
run = """
|
run = """
|
||||||
set -e
|
set -e
|
||||||
if [ -d "$ANDROID_HOME/platforms/android-$COMPILE_SDK" ]; then
|
if [ -d "$ANDROID_HOME/platforms/android-$COMPILE_SDK" ]; then
|
||||||
echo "SDK $COMPILE_SDK déjà présent → rien à faire"
|
echo "SDK $COMPILE_SDK already present, nothing to do"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
mkdir -p "$ANDROID_HOME"
|
mkdir -p "$ANDROID_HOME"
|
||||||
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
||||||
echo "Téléchargement des commandline-tools…"
|
echo "Downloading the command-line tools…"
|
||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
curl -sSL -o "$tmp/tools.zip" \
|
curl -sSL -o "$tmp/tools.zip" \
|
||||||
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
||||||
@@ -92,59 +89,59 @@ yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_H
|
|||||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
|
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
|
||||||
"platform-tools" "platforms;android-$COMPILE_SDK" "build-tools;$BUILD_TOOLS"
|
"platform-tools" "platforms;android-$COMPILE_SDK" "build-tools;$BUILD_TOOLS"
|
||||||
echo "sdk.dir=$ANDROID_HOME" > local.properties
|
echo "sdk.dir=$ANDROID_HOME" > local.properties
|
||||||
echo "SDK installé, local.properties écrit"
|
echo "SDK installed, local.properties written"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.build]
|
[tasks.build]
|
||||||
description = "Compile l'APK debug"
|
description = "Builds the debug APK"
|
||||||
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME assembleStableDebug'
|
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME assembleStableDebug'
|
||||||
|
|
||||||
[tasks.test]
|
[tasks.test]
|
||||||
description = "Tests unitaires JVM"
|
description = "JVM unit tests"
|
||||||
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME testStableDebugUnitTest testUnstableDebugUnitTest'
|
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME testStableDebugUnitTest testUnstableDebugUnitTest'
|
||||||
|
|
||||||
[tasks.lint]
|
[tasks.lint]
|
||||||
description = "Analyse lint Android"
|
description = "Android lint"
|
||||||
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME lintStableDebug lintUnstableDebug'
|
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME lintStableDebug lintUnstableDebug'
|
||||||
|
|
||||||
[tasks.permissions]
|
[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"
|
run = "bash .github/security/check-permissions.sh"
|
||||||
|
|
||||||
[tasks.check]
|
[tasks.check]
|
||||||
description = "Ce que la CI exécute : permissions + lint + tests"
|
description = "What CI runs: permissions + lint + tests"
|
||||||
depends = ["permissions", "lint", "test"]
|
depends = ["permissions", "lint", "test"]
|
||||||
|
|
||||||
[tasks.install]
|
[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'
|
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME installStableDebug'
|
||||||
|
|
||||||
[tasks.logs]
|
[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"
|
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]
|
[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'
|
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME assembleStableRelease assembleUnstableRelease'
|
||||||
|
|
||||||
[tasks.clean]
|
[tasks.clean]
|
||||||
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME clean'
|
run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME clean'
|
||||||
|
|
||||||
[tasks.emulator-setup]
|
[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 = '''
|
run = '''
|
||||||
set -e
|
set -e
|
||||||
SDK="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
|
SDK="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
|
||||||
AVDM="$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager"
|
AVDM="$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager"
|
||||||
|
|
||||||
if [ ! -e /dev/kvm ]; then
|
if [ ! -e /dev/kvm ]; then
|
||||||
echo "ERREUR : /dev/kvm absent — pas d'accélération matérielle, l'émulateur sera inutilisable." >&2
|
echo "ERROR: /dev/kvm missing — no hardware acceleration, the emulator would be unusable." >&2
|
||||||
echo "Sous WSL2 : activer la virtualisation imbriquée côté Windows." >&2
|
echo "On WSL2: enable nested virtualisation on the Windows side." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! id -nG | tr ' ' '\n' | grep -qx kvm; then
|
if ! id -nG | tr ' ' '\n' | grep -qx kvm; then
|
||||||
echo "ATTENTION : utilisateur absent du groupe kvm. Corriger avec :" >&2
|
echo "WARNING: user is not in the kvm group. Fix with:" >&2
|
||||||
echo " sudo usermod -aG kvm $USER # puis se reconnecter" >&2
|
echo " sudo usermod -aG kvm $USER # then log in again" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
yes | "$SDK" --sdk_root="$ANDROID_HOME" --licenses > /dev/null
|
yes | "$SDK" --sdk_root="$ANDROID_HOME" --licenses > /dev/null
|
||||||
@@ -153,13 +150,13 @@ yes | "$SDK" --sdk_root="$ANDROID_HOME" --licenses > /dev/null
|
|||||||
create_avd() {
|
create_avd() {
|
||||||
name="$1"; image="$2"; device="$3"
|
name="$1"; image="$2"; device="$3"
|
||||||
if "$AVDM" list avd 2>/dev/null | grep -q "Name: $name"; then
|
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
|
else
|
||||||
echo no | "$AVDM" create avd -n "$name" -k "$image" -d "$device" --force
|
echo no | "$AVDM" create avd -n "$name" -k "$image" -d "$device" --force
|
||||||
fi
|
fi
|
||||||
cfg="$HOME/.android/avd/$name.avd/config.ini"
|
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
|
# The MG4's screen. Without this the AVD keeps the generic profile's density and the
|
||||||
# ne ressemblent pas à ce que voit le conducteur.
|
# layouts do not look like what the driver sees.
|
||||||
sed -i '/^hw.lcd/d;/^disk.dataPartition.size/d' "$cfg"
|
sed -i '/^hw.lcd/d;/^disk.dataPartition.size/d' "$cfg"
|
||||||
{
|
{
|
||||||
echo "hw.lcd.width=$EMU_WIDTH"
|
echo "hw.lcd.width=$EMU_WIDTH"
|
||||||
@@ -167,55 +164,55 @@ create_avd() {
|
|||||||
echo "hw.lcd.density=$EMU_DENSITY"
|
echo "hw.lcd.density=$EMU_DENSITY"
|
||||||
echo "disk.dataPartition.size=4G"
|
echo "disk.dataPartition.size=4G"
|
||||||
} >> "$cfg"
|
} >> "$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_CAR_AVD" "$EMU_CAR_IMAGE" "automotive_1024p_landscape"
|
||||||
create_avd "$EMU_SCREEN_AVD" "$EMU_SCREEN_IMAGE" "pixel_c"
|
create_avd "$EMU_SCREEN_AVD" "$EMU_SCREEN_IMAGE" "pixel_c"
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# swiftshader_indirect, pas « host » : WSL2 n'expose aucun GPU réel (uniquement Mesa
|
# swiftshader_indirect, not "host": WSL2 exposes no real GPU (only Mesa llvmpipe). With
|
||||||
# llvmpipe). Avec -gpu host l'émulateur meurt au démarrage sur « Your GPU cannot be used
|
# -gpu host the emulator dies at start-up on "Your GPU cannot be used for hardware
|
||||||
# for hardware rendering », avant qu'aucune fenêtre n'apparaisse — d'où l'icône de tray
|
# rendering", before any window appears — hence a tray icon and no visible emulator.
|
||||||
# sans émulateur visible. Le rendu logiciel est plus lent, mais il démarre.
|
# Software rendering is slower, but it starts.
|
||||||
[tasks.emulator-car]
|
[tasks.emulator-car]
|
||||||
description = "Lance l'émulateur Automotive (API 33 — a un CarPropertyManager)"
|
description = "Starts the Automotive emulator (API 33 — has a CarPropertyManager)"
|
||||||
run = '''
|
run = '''
|
||||||
set -e
|
set -e
|
||||||
"$ANDROID_HOME/emulator/emulator" -avd "$EMU_CAR_AVD" \
|
"$ANDROID_HOME/emulator/emulator" -avd "$EMU_CAR_AVD" \
|
||||||
-gpu swiftshader_indirect -no-snapshot-save -no-boot-anim &
|
-gpu swiftshader_indirect -no-snapshot-save -no-boot-anim &
|
||||||
"$ANDROID_HOME/platform-tools/adb" wait-for-device
|
"$ANDROID_HOME/platform-tools/adb" wait-for-device
|
||||||
echo "Émulateur voiture prêt."
|
echo "Car emulator ready."
|
||||||
'''
|
'''
|
||||||
|
|
||||||
[tasks.emulator-screen]
|
[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 = '''
|
run = '''
|
||||||
set -e
|
set -e
|
||||||
"$ANDROID_HOME/emulator/emulator" -avd "$EMU_SCREEN_AVD" \
|
"$ANDROID_HOME/emulator/emulator" -avd "$EMU_SCREEN_AVD" \
|
||||||
-gpu swiftshader_indirect -no-snapshot-save -no-boot-anim &
|
-gpu swiftshader_indirect -no-snapshot-save -no-boot-anim &
|
||||||
"$ANDROID_HOME/platform-tools/adb" wait-for-device
|
"$ANDROID_HOME/platform-tools/adb" wait-for-device
|
||||||
echo "Émulateur écran prêt."
|
echo "Screen emulator ready."
|
||||||
'''
|
'''
|
||||||
|
|
||||||
[tasks.emulator-stop]
|
[tasks.emulator-stop]
|
||||||
description = "Arrête l'émulateur en cours"
|
description = "Stops the running emulator"
|
||||||
run = '"$ANDROID_HOME/platform-tools/adb" emu kill || true'
|
run = '"$ANDROID_HOME/platform-tools/adb" emu kill || true'
|
||||||
|
|
||||||
[tasks.run]
|
[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"]
|
depends = ["build"]
|
||||||
run = '''
|
run = '''
|
||||||
set -e
|
set -e
|
||||||
ADB="$ANDROID_HOME/platform-tools/adb"
|
ADB="$ANDROID_HOME/platform-tools/adb"
|
||||||
if [ -z "$($ADB devices | sed '1d' | grep -w device || true)" ]; then
|
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 "No device connected. Start 'mise run emulator-car' or 'mise run emulator-screen' first," >&2
|
||||||
echo "ou brancher le véhicule en ADB." >&2
|
echo "or connect the car over ADB." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
APK=$(ls -t app/build/outputs/apk/stable/debug/*.apk | head -1)
|
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 install -r -g "$APK"
|
||||||
$ADB shell am start -n "com.stinnesbeck.nilsmg4/com.evsuite.abrp.MainActivity"
|
$ADB shell am start -n "com.stinnesbeck.nilsmg4/com.evsuite.abrp.MainActivity"
|
||||||
echo "Lancé. Logs : mise run logs"
|
echo "Started. Logs: mise run logs"
|
||||||
'''
|
'''
|
||||||
|
|||||||
Reference in New Issue
Block a user