EVHardware: translate the French comments, log texts and test names
The vendored library's French and half-translated comments, its French log messages and the French test names and assertion messages are now English. Comments, log texts and test names only; no code changed. The French resources, place names in test data and two quoted phrases stay; "Katman" (Turkish for layer) stays as the layers' name and is explained. SPECS: the Grafana host is confirmed; both items ticked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -30,19 +30,19 @@ class FirmwareSupportTest {
|
||||
ConditionType.FIRMWARE_GEN,
|
||||
ConditionType.LOCATION_WITHIN,
|
||||
ConditionType.PHYSICAL_BUTTON,
|
||||
// Lus dans Android, pas dans la voiture : rien à router par génération.
|
||||
// Read from Android, not from the car: nothing to route per generation.
|
||||
ConditionType.MEDIA_PLAYING,
|
||||
ConditionType.WIFI_SSID,
|
||||
ConditionType.IN_CALL,
|
||||
ConditionType.DRIVE_DURATION,
|
||||
ConditionType.RANDOM_CHANCE,
|
||||
// Services des applications du poste (adaptateur de navigation, météo) : ce ne sont
|
||||
// pas des services du SDK véhicule, rien ne les route par génération. Ils répondent
|
||||
// ou non, comme le SMS.
|
||||
// Services of the head unit's apps (navigation adapter, weather): these are not
|
||||
// vehicle-SDK services, nothing routes them per generation. They answer or they do
|
||||
// not, like SMS.
|
||||
ConditionType.ODOMETER,
|
||||
ConditionType.WEATHER_NOW,
|
||||
// La connexion du poste, comptée par Android : elle dépend d'une permission, pas
|
||||
// d'une génération de firmware.
|
||||
// The head unit's connection, as counted by Android: it depends on a permission, not
|
||||
// on a firmware generation.
|
||||
ConditionType.DATA_USED_TODAY,
|
||||
ConditionType.DATA_USED_MONTH
|
||||
)
|
||||
@@ -54,10 +54,10 @@ class FirmwareSupportTest {
|
||||
ActionType.WEBHOOK,
|
||||
ActionType.ASK_CONFIRM,
|
||||
ActionType.DELAY,
|
||||
// Le SMS part par le profil Bluetooth MAP, pas par un service SAIC : rien à router
|
||||
// par génération de firmware. Sa disponibilité se lit au bind, pas dans la matrice.
|
||||
// SMS goes out over the Bluetooth MAP profile, not through a SAIC service: nothing to
|
||||
// route per firmware generation. Whether it is available shows at bind, not in the matrix.
|
||||
ActionType.SEND_SMS,
|
||||
// Règles, touche média et radios du poste : rien de tout cela ne passe par la voiture.
|
||||
// Rules, the media key and the head unit's radios: none of this goes through the car.
|
||||
ActionType.ENABLE_RULE,
|
||||
ActionType.DISABLE_RULE,
|
||||
ActionType.MEDIA_CONTROL,
|
||||
|
||||
@@ -7,11 +7,11 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Ce que la preuve relevée sur une voiture change — et ce qu'elle ne change pas.
|
||||
* What evidence recorded on a car changes — and what it does not.
|
||||
*
|
||||
* L'enjeu : `writeProven` parle du projet, `effectProven` parle de la voiture. Confondre les
|
||||
* deux rendrait soit les actions vitres définitivement inaccessibles, soit accessibles sur des
|
||||
* voitures où rien n'a été constaté.
|
||||
* What is at stake: `writeProven` speaks about the project, `effectProven` about the car.
|
||||
* Mixing the two up would make the window actions either unreachable for good, or reachable on
|
||||
* cars where nothing was observed.
|
||||
*/
|
||||
class GlassEvidenceTest {
|
||||
|
||||
@@ -21,7 +21,7 @@ class GlassEvidenceTest {
|
||||
fun tearDown() = GlassEvidence.setForTest(null)
|
||||
|
||||
@Test
|
||||
fun `sans preuve, effectProven suit writeProven`() {
|
||||
fun `without evidence, effectProven follows writeProven`() {
|
||||
GlassEvidence.setForTest(null)
|
||||
ActionType.entries.forEach {
|
||||
assertTrue("${it.name}", it.writeProven == it.effectProven)
|
||||
@@ -29,29 +29,29 @@ class GlassEvidenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `la preuve debloque les actions vitres, et elles seules`() {
|
||||
fun `evidence unlocks the window actions, and only them`() {
|
||||
GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, generation, 0L))
|
||||
|
||||
GlassEvidence.ACTIONS.forEach {
|
||||
assertFalse("${it.name} reste non prouvée dans le catalogue", it.writeProven)
|
||||
assertTrue("${it.name} doit être débloquée par la preuve", it.effectProven)
|
||||
assertFalse("${it.name} stays unproven in the catalogue", it.writeProven)
|
||||
assertTrue("${it.name} must be unlocked by the evidence", it.effectProven)
|
||||
}
|
||||
// Une preuve sur les vitres ne dit rien d'une autre action : le catalogue reste seul
|
||||
// juge partout ailleurs.
|
||||
// Evidence about the windows says nothing about another action: everywhere else the
|
||||
// catalogue alone decides.
|
||||
ActionType.entries.filterNot { it in GlassEvidence.ACTIONS }.forEach {
|
||||
assertTrue("${it.name}", it.writeProven == it.effectProven)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `une preuve d'un autre firmware ne vaut rien`() {
|
||||
// Les commandes sont propres au SDK de la génération : ce qui ouvre sur l'une n'a
|
||||
// aucune raison d'ouvrir sur l'autre, et une mise à jour ne doit pas laisser derrière
|
||||
// elle une autorisation qui n'a plus de fondement.
|
||||
fun `evidence from another firmware counts for nothing`() {
|
||||
// The commands belong to each generation's SDK: what opens on one has no reason to
|
||||
// open on another, and an update must not leave behind a permission that no longer
|
||||
// has any grounds.
|
||||
GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, "SOME_OTHER_GEN", 0L))
|
||||
|
||||
GlassEvidence.ACTIONS.forEach {
|
||||
assertFalse("${it.name} ne doit pas hériter d'une preuve étrangère", it.effectProven)
|
||||
assertFalse("${it.name} must not inherit evidence from elsewhere", it.effectProven)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Ce que la sonde doit établir sans voiture : l'enchaînement des deux passes et les refus.
|
||||
* What the probe must establish without a car: the sequence of its two passes, and its refusals.
|
||||
*
|
||||
* La vitre simulée reproduit le comportement qui a rendu la sonde nécessaire : le service
|
||||
* accepte n'importe quelle commande et n'en applique que deux. Une passe unique ne peut donc
|
||||
* pas voir les deux sens — c'est exactement ce que ces tests vérifient.
|
||||
* The simulated window reproduces the behaviour that made the probe necessary: the service
|
||||
* accepts any command and applies only two of them. A single pass therefore cannot see both
|
||||
* directions — which is exactly what these tests check.
|
||||
*/
|
||||
class GlassProbeTest {
|
||||
|
||||
/** Vitre où [openCommand] ouvre en grand et [closeCommand] referme ; le reste est inerte. */
|
||||
/** A window where [openCommand] opens fully and [closeCommand] closes; the rest do nothing. */
|
||||
private class FakeGlass(
|
||||
private val openCommand: Int,
|
||||
private val closeCommand: Int,
|
||||
@@ -48,9 +48,9 @@ class GlassProbeTest {
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `les deux sens sont trouves depuis une vitre fermee`() {
|
||||
// Depuis 0 %, la commande qui ferme ne bouge rien : sans seconde passe elle passerait
|
||||
// pour inerte, et l'action « fermer » resterait à jamais non prouvée.
|
||||
fun `both directions are found from a closed window`() {
|
||||
// From 0 %, the closing command moves nothing: without a second pass it would pass
|
||||
// for inert, and the "close" action would stay unproven forever.
|
||||
val glass = FakeGlass(openCommand = 5, closeCommand = 2)
|
||||
val result = probe(glass)
|
||||
|
||||
@@ -58,27 +58,27 @@ class GlassProbeTest {
|
||||
assertEquals(2, result.closeCommand)
|
||||
assertTrue(result.proven)
|
||||
assertNull(result.refusal)
|
||||
// Repartie de 0 %, la vitre doit y revenir : une sonde qui laisse la vitre ouverte
|
||||
// est un défaut, pas un résultat.
|
||||
// Having started at 0 %, the window must end there: a probe that leaves the window
|
||||
// open is a defect, not a result.
|
||||
assertTrue(result.restored)
|
||||
assertEquals(0, glass.percent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `une commande deja classee n'est pas renvoyee`() {
|
||||
fun `a command already classified is not sent again`() {
|
||||
val glass = FakeGlass(openCommand = 0, closeCommand = 1)
|
||||
val result = probe(glass)
|
||||
|
||||
assertEquals(0, result.openCommand)
|
||||
assertEquals(1, result.closeCommand)
|
||||
// 0 ouvre, 1 referme, les deux sens sont connus : la sonde s'arrête là, plus une
|
||||
// remise en place. Rien au-delà de la commande 1 ne doit avoir été envoyé.
|
||||
// 0 opens, 1 closes, both directions are known: the probe stops there, plus one
|
||||
// restore. Nothing beyond command 1 may have been sent.
|
||||
assertEquals(listOf(0, 1), glass.sent.take(2))
|
||||
assertTrue(glass.sent.none { it > 1 })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `une vitre inerte ne prouve rien`() {
|
||||
fun `an inert window proves nothing`() {
|
||||
val glass = FakeGlass(openCommand = 99, closeCommand = 98)
|
||||
val result = probe(glass)
|
||||
|
||||
@@ -86,28 +86,28 @@ class GlassProbeTest {
|
||||
assertNull(result.closeCommand)
|
||||
assertEquals(GlassProbe.Refusal.NOTHING_MOVED, result.refusal)
|
||||
assertFalse(result.proven)
|
||||
// Les huit commandes, deux fois : la seconde passe ne peut être sautée sur la foi de
|
||||
// la première, qui ne voit pas les fermetures.
|
||||
// All eight commands, twice: the second pass cannot be skipped on the strength of
|
||||
// the first, which does not see closing.
|
||||
assertEquals(16, glass.sent.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `un seul sens trouve n'est pas une preuve`() {
|
||||
fun `one direction found is not proof`() {
|
||||
val glass = FakeGlass(openCommand = 3, closeCommand = 99)
|
||||
val result = probe(glass)
|
||||
|
||||
assertEquals(3, result.openCommand)
|
||||
assertNull(result.closeCommand)
|
||||
assertEquals(GlassProbe.Refusal.ONE_DIRECTION_ONLY, result.refusal)
|
||||
// Rien ne peut refermer la vitre : il faut le dire, pas laisser croire au contraire.
|
||||
// Nothing can close the window: that must be said, not left to look otherwise.
|
||||
assertFalse(result.restored)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `la voiture qui repart interrompt la sonde`() {
|
||||
fun `the car moving off interrupts the probe`() {
|
||||
val glass = FakeGlass(openCommand = 5, closeCommand = 2)
|
||||
var checks = 0
|
||||
// Arrêtée pour la lecture initiale et la première commande, puis en mouvement.
|
||||
// Stopped for the first reading and the first command, then moving.
|
||||
val result = probe(glass, stopped = { checks++ < 2 })
|
||||
|
||||
assertEquals(GlassProbe.Refusal.NOT_STOPPED, result.refusal)
|
||||
@@ -115,7 +115,7 @@ class GlassProbeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `contact coupe, aucune commande n'est envoyee`() {
|
||||
fun `ignition off, no command is sent`() {
|
||||
val glass = FakeGlass(openCommand = 5, closeCommand = 2)
|
||||
val result = probe(glass, ignitionRun = { false })
|
||||
|
||||
@@ -124,7 +124,7 @@ class GlassProbeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `une position illisible arrete tout`() {
|
||||
fun `an unreadable position stops everything`() {
|
||||
val glass = FakeGlass(openCommand = 5, closeCommand = 2).apply { readable = false }
|
||||
val result = probe(glass)
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ class PhysicalButtonEventDecoderTest {
|
||||
}
|
||||
|
||||
@Test fun `three presses give one double and then a single`() {
|
||||
// Sinon un troisième appui rendrait un second DOUBLE, et la règle partirait deux fois.
|
||||
// Otherwise a third press would make a second DOUBLE, and the rule would fire twice.
|
||||
val decoder = PhysicalButtonEventDecoder()
|
||||
val presses = listOf(0L, 100L, 200L).map {
|
||||
decoder.accept(17, true, false, atMillis = it)
|
||||
|
||||
+47
-48
@@ -13,9 +13,8 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Garde-fous sur le catalogue. Les erreurs visées ne cassent pas la compilation : elles
|
||||
* produisent une entrée d'interface qui ne fait rien, ou pire, une écriture véhicule qui
|
||||
* contourne le verrou de vitesse.
|
||||
* Guard rails on the catalogue. The mistakes they catch do not break the build: they produce a
|
||||
* UI entry that does nothing or, worse, a vehicle write that gets round the speed lock.
|
||||
*/
|
||||
class CatalogConsistencyTest {
|
||||
|
||||
@@ -30,42 +29,42 @@ class CatalogConsistencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `la destination et le point utilisent des controles distincts`() {
|
||||
// Un point a un rayon, une destination n'en a pas : deux ValueKind, sinon l'éditeur
|
||||
// demanderait un rayon pour une adresse.
|
||||
fun `the destination and the point use separate controls`() {
|
||||
// A point has a radius, a destination does not: two ValueKinds, or the editor would
|
||||
// ask for a radius for an address.
|
||||
assertEquals(ValueKind.DESTINATION, ActionType.NAVIGATE_TO.spec.kind)
|
||||
assertEquals(ValueKind.LOCATION, ConditionType.LOCATION_WITHIN.spec.kind)
|
||||
assertEquals(0, ActionType.NAVIGATE_TO.spec.max)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `le sms porte un destinataire et un message`() {
|
||||
// Le destinataire est un numéro (ou un contact du répertoire PBAP) et le message est
|
||||
// du texte libre : un seul contrôle ne pourrait pas porter les deux.
|
||||
fun `an sms carries a recipient and a message`() {
|
||||
// The recipient is a number (or a contact from the PBAP phone book) and the message is
|
||||
// free text: a single control could not carry both.
|
||||
assertEquals(ValueKind.SMS, ActionType.SEND_SMS.spec.kind)
|
||||
assertTrue(ActionType.SEND_SMS.spec.hintRes != 0)
|
||||
assertFalse("le SMS n'écrit rien dans le véhicule", ActionType.SEND_SMS.gated)
|
||||
assertFalse("SMS writes nothing to the vehicle", ActionType.SEND_SMS.gated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `la demande de confirmation porte sa question et son delai`() {
|
||||
// Le texte EST la question posée au conducteur ; un exemple est indispensable, la
|
||||
// formulation par défaut d'un champ vide n'apprend rien. Le nombre est l'attente en
|
||||
// secondes, bornée pour qu'une question reste lisible sans bloquer le cycle.
|
||||
fun `the confirmation request carries its question and its delay`() {
|
||||
// The text IS the question put to the driver; an example is essential, the default
|
||||
// wording of an empty field teaches nothing. The number is the wait in seconds,
|
||||
// bounded so that a question stays readable without blocking the cycle.
|
||||
assertEquals(ValueKind.CONFIRM, ActionType.ASK_CONFIRM.spec.kind)
|
||||
assertTrue(ActionType.ASK_CONFIRM.spec.hintRes != 0)
|
||||
assertTrue(ActionType.ASK_CONFIRM.spec.min > 0)
|
||||
assertTrue(ActionType.ASK_CONFIRM.spec.max > ActionType.ASK_CONFIRM.spec.min)
|
||||
// Le défaut des règles enregistrées sans délai doit rester dans les bornes offertes.
|
||||
// The default for rules saved without a delay must stay within the offered bounds.
|
||||
assertTrue(ActionType.ASK_CONFIRM_DEFAULT_SECONDS in
|
||||
ActionType.ASK_CONFIRM.spec.min..ActionType.ASK_CONFIRM.spec.max)
|
||||
assertFalse("la question ne touche pas le véhicule", ActionType.ASK_CONFIRM.gated)
|
||||
assertFalse("the question does not touch the vehicle", ActionType.ASK_CONFIRM.gated)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toute action vehicule cible une action du pont`() {
|
||||
// bridgeAction=null est réservé aux actions locales (lancer une app, notifier).
|
||||
// Une action véhicule sans cible serait silencieusement ignorée à l'exécution.
|
||||
fun `every vehicle action targets a bridge action`() {
|
||||
// bridgeAction=null is reserved for local actions (launch an app, notify).
|
||||
// A vehicle action without a target would be silently ignored when run.
|
||||
val localOnly = setOf(
|
||||
ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT,
|
||||
ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM,
|
||||
@@ -75,12 +74,12 @@ class CatalogConsistencyTest {
|
||||
)
|
||||
|
||||
ActionType.entries.filterNot { it in localOnly }.forEach { type ->
|
||||
assertNotNull("${type.name} n'a pas de bridgeAction", type.bridgeAction)
|
||||
assertNotNull("${type.name} has no bridgeAction", type.bridgeAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `les actions locales ne passent pas par le pont`() {
|
||||
fun `local actions do not go through the bridge`() {
|
||||
listOf(
|
||||
ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT,
|
||||
ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM,
|
||||
@@ -89,17 +88,17 @@ class CatalogConsistencyTest {
|
||||
ActionType.SET_BLUETOOTH, ActionType.SET_WIFI
|
||||
).forEach { type ->
|
||||
assertTrue(
|
||||
"${type.name} ne touche pas le véhicule et ne doit pas avoir de bridgeAction",
|
||||
"${type.name} does not touch the vehicle and must not have a bridgeAction",
|
||||
type.bridgeAction == null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `les ecritures de comportement routier sont marquees gated`() {
|
||||
// Le marquage ne remplace PAS le verrou côté EVProfile, qui reste l'autorité.
|
||||
// Il conditionne l'avertissement affiché à l'utilisateur au moment du choix :
|
||||
// une action gated non marquée se présenterait comme applicable en roulant.
|
||||
fun `writes that change driving behaviour are marked gated`() {
|
||||
// The marking does NOT replace the lock on the EVProfile side, which stays the authority.
|
||||
// It decides the warning shown to the user when choosing:
|
||||
// an unmarked gated action would present itself as usable while driving.
|
||||
val mustBeGated = setOf(
|
||||
ActionType.APPLY_PROFILE,
|
||||
ActionType.SET_DRIVE_MODE, ActionType.SET_REGEN_LEVEL, ActionType.SET_ONE_PEDAL,
|
||||
@@ -113,16 +112,16 @@ class CatalogConsistencyTest {
|
||||
)
|
||||
|
||||
mustBeGated.forEach { type ->
|
||||
assertTrue("${type.name} modifie le comportement routier et doit être gated", type.gated)
|
||||
assertTrue("${type.name} changes driving behaviour and must be gated", type.gated)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `couper le vehicule n est pas automatisable`() {
|
||||
// Une règle ne doit jamais pouvoir éteindre la voiture : c'est irréversible pour
|
||||
// le conducteur, et ça reste réservé à un geste humain dans EVProfile.
|
||||
fun `switching the vehicle off cannot be automated`() {
|
||||
// A rule must never be able to switch the car off: that is irreversible for the
|
||||
// driver, and it stays reserved for a human gesture in EVProfile.
|
||||
assertFalse(
|
||||
"aucune action du catalogue ne doit couper le véhicule",
|
||||
"no catalogue action may switch the vehicle off",
|
||||
ActionType.entries.any {
|
||||
it.bridgeAction?.contains("POWER_OFF", ignoreCase = true) == true
|
||||
}
|
||||
@@ -130,9 +129,9 @@ class CatalogConsistencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toute condition vehicule lit une cle de l instantane`() {
|
||||
// Les conditions calculées localement (heure, jour, Bluetooth) n'ont pas de clé ;
|
||||
// toutes les autres en ont besoin, sinon elles seraient toujours indisponibles.
|
||||
fun `every vehicle condition reads a snapshot key`() {
|
||||
// Conditions computed locally (time, day, Bluetooth) have no key; all the others need
|
||||
// one, or they would always be unavailable.
|
||||
val localOnly = setOf(
|
||||
ConditionType.BT_DEVICE_CONNECTED,
|
||||
ConditionType.ANY_BT_CONNECTED,
|
||||
@@ -142,12 +141,12 @@ class CatalogConsistencyTest {
|
||||
ConditionType.DAY_OF_WEEK,
|
||||
ConditionType.DATE,
|
||||
ConditionType.LOCATION_WITHIN,
|
||||
// Tiré au sort à l'évaluation : il n'y a rien à lire.
|
||||
// Drawn at random when evaluated: there is nothing to read.
|
||||
ConditionType.RANDOM_CHANCE
|
||||
)
|
||||
|
||||
ConditionType.entries.filterNot { it in localOnly }.forEach { type ->
|
||||
assertNotNull("${type.name} n'a pas de clé d'instantané", type.snapshotKey)
|
||||
assertNotNull("${type.name} has no snapshot key", type.snapshotKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,32 +362,32 @@ class CatalogConsistencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `les enumerations ont des options`() {
|
||||
// Une ENUM sans option produit une liste déroulante vide : l'utilisateur ne peut
|
||||
// rien choisir et la règle reste inutilisable.
|
||||
fun `enumerations have options`() {
|
||||
// An ENUM without options gives an empty drop-down: the user cannot choose anything
|
||||
// and the rule stays unusable.
|
||||
ConditionType.entries
|
||||
.filter { it.spec.kind == ValueKind.ENUM && it != ConditionType.FIRMWARE_GEN }
|
||||
.forEach { assertTrue("${it.name} : ENUM sans option", it.spec.options.isNotEmpty()) }
|
||||
.forEach { assertTrue("${it.name}: ENUM without options", it.spec.options.isNotEmpty()) }
|
||||
|
||||
// RADIO dessine aussi une liste déroulante — celle des bandes — donc la même règle
|
||||
// s'applique : sans option, l'action ne peut nommer aucune bande.
|
||||
// RADIO draws a drop-down too — the bands — so the same rule applies: without
|
||||
// options, the action cannot name any band.
|
||||
ActionType.entries
|
||||
.filter { it.spec.kind == ValueKind.ENUM || it.spec.kind == ValueKind.RADIO }
|
||||
.forEach { assertTrue("${it.name} : ENUM sans option", it.spec.options.isNotEmpty()) }
|
||||
.forEach { assertTrue("${it.name}: ENUM without options", it.spec.options.isNotEmpty()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `les bornes numeriques sont coherentes`() {
|
||||
fun `numeric bounds are consistent`() {
|
||||
val specs = ConditionType.entries.map { it.name to it.spec } +
|
||||
ActionType.entries.map { it.name to it.spec }
|
||||
|
||||
specs.filter { it.second.kind == ValueKind.NUMBER }.forEach { (name, spec) ->
|
||||
if (spec.max >= 0) {
|
||||
assertTrue("$name : max doit dépasser min", spec.max > spec.min)
|
||||
assertTrue("$name: max must exceed min", spec.max > spec.min)
|
||||
} else {
|
||||
// max = -1 : borne résolue à l'exécution. Le repli doit rester utilisable
|
||||
// même si le véhicule ne répond pas.
|
||||
assertTrue("$name : fallbackMax doit dépasser min", spec.fallbackMax > spec.min)
|
||||
// max = -1: the bound is resolved at run time. The fallback must stay usable
|
||||
// even if the vehicle does not answer.
|
||||
assertTrue("$name: fallbackMax must exceed min", spec.fallbackMax > spec.min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ on the cluster since 2026-10-25).
|
||||
(telemetry, consumption, charging, pairing, settings from the phone) and Grafana 13.2.3 (all
|
||||
dashboard queries return data).
|
||||
- [x] Manifests validated offline (kubeconform, with Traefik CRDs). **Applied by Nils only.**
|
||||
- [ ] Check Grafana's host in `k8s/ingress.yaml` (placeholder `car-grafana.k8s.stinnesbeck.com`).
|
||||
- [x] Grafana's host in `k8s/ingress.yaml` is `car-grafana.k8s.stinnesbeck.com`, the one in use
|
||||
(owner, 2026-10-25).
|
||||
- [x] Screen on the car: 1920×720 px at 160 dpi, app window 1778×720 px (Logs → SCREEN).
|
||||
- [x] Car-colour probe on the car: only `ro.build.mtk2712.vehicle_type = AS28`; the paint colour
|
||||
is not in the system properties. It may be in the VHAL (a debug-build VhalProbe listing
|
||||
@@ -88,9 +89,7 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
||||
write the file. To run more than one replica, first move Grafana to PostgreSQL or MySQL
|
||||
(`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.
|
||||
- [ ] **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.
|
||||
- [x] **EVHardware's comments are partly French.** Translated 2026-10-25 (see the done log).
|
||||
- [ ] **Idea: climate energy on the Overview (motor vs climate).** Nothing the app reads splits
|
||||
energy by consumer: battery power (voltage × current) is the total of motor, climate, 12 V
|
||||
and the rest. SAIC's charging service, which EVHardware already binds, has counters that
|
||||
@@ -139,6 +138,15 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||
- 2026-10-25 — EVHardware in English: the French comments (and the half-translated ones like
|
||||
"l'screen", "registersment", "initialisesd") in `EVHardware.kt` and five test files, the French
|
||||
log messages ("aucun init() fonctionnel", "introuvable", "state initial", "ERREUR"), the French
|
||||
test names and assertion messages (`CatalogConsistencyTest`, `GlassEvidenceTest`,
|
||||
`GlassProbeTest`). Comments, log texts and test names only: no code changed (checked in the
|
||||
diff). Kept on purpose: the French translation `values-fr/strings.xml`, place names in test
|
||||
data, the quoted French sentence in `ChargeStopPlan` and the voice phrase in `SaicNavGuidance`,
|
||||
and "Katman" (Turkish for layer), which names the layers in function names and logs — now
|
||||
explained at the top of `EVHardware.kt`. EVHardware, app tests and lint pass.
|
||||
- 2026-10-25 — README.md brought up to date: the tab screenshots retaken with 2.35.2 on the
|
||||
emulator right after installing (`screenshots/nilsmg4-2.35.2-*.png`, the 2.18.1 ones removed),
|
||||
"read-only" now names the drive mode exception, the upload frequency matches the app (1 min by
|
||||
|
||||
Reference in New Issue
Block a user