docs(spec): spec_ecs 0.5.1 — câblage réel 500/1000/1500, les trois consignations s'inversent

La 0.5.0 consignait 500/1000/2000 sur la foi de la consigne d'étape 1. Le
simulateur du banc contredit cette donnée ; arbitrage : le simulateur reflète
l'installation, la donnée était fausse.

Câblage : 500/1000/1500 W, additif, 7 valeurs distinctes de 0 à 3000 W, avec UNE
redondance — 1500 W admet {R1500} et {R500+R1000}.

Trois corrections :

1. ECS-302 est EXERCÉ. Il redevient préalable structurel de l'étape 3 et reprend
   son rang devant tout ce qui s'appuiera sur m_relayMapping.

2. ECS-303 SUPPRIMÉ. Le « point dur » 1500→2000 n'est pas une exigence
   d'hystérésis, c'est une conséquence directe d'ECS-302 non traité :
   relayrouter.cpp:58 retient la première combinaison rencontrée (masque le plus
   bas), donc {R500+R1000} pour 1500 W, et jette {R1500}. La transition vers
   2000 W = {R500+R1500} demande alors d'ouvrir R1000 et de fermer R1500, avec le
   creux qu'impose l'ordre coupure-avant-fermeture. Avec {R1500} pour 1500 W, la
   même transition ne coûterait QU'UNE commutation : fermer R500. Aucune entrée
   ECS-303 ni ECS-605 de ce côté.

3. 7 valeurs et plafond 3000 W, non 8 et 3500. ECS-305 reste valable, comptes
   corrigés : par balayage complet R500 commute 5 fois, R1000 3 fois, R1500 une
   seule (vérifié sur l'encodage effectivement retenu par le code actuel).

RÉSERVE INSCRITE : la source est le simulateur (sim/sim_ecs_router.py:38, sommé
dans _power_w(), confirmé en vol sur MQTT), PAS une mesure physique. Ce chiffre a
déjà changé trois fois et il commande désormais l'ordre de l'étape 3 : il doit
être confirmé sur plaque signalétique ou au pince ampèremétrique AVANT
l'ouverture de cette étape. Consigné en §13-3 et en garde-fou sous l'étape 3.

Aucun code touché.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-08-08 12:15:40 +02:00
parent 1e80a83df9
commit 1011a6ccd7

View File

@ -1,6 +1,6 @@
# SPEC — ECS multi-palier
Version : 0.5.0
Version : 0.5.1
Dépôt : `etm-powersync-energy-plugin-etm` (experience-plugin ems)
Branche : `feature/beta-rulebased`
Contrats faisant autorité : `docs/OPTIMIZER_PROTOCOL.md`, `docs/SAFETY.md`, `AGENTS.md`
@ -33,8 +33,13 @@ remonte. Il ne tranche pas seul.
>
> **0.5.0** — §13-1 et §13-2 **tranchés et consignés** ; l'étape 1 est débloquée.
> ECS-412 réduit à `m_lastSwitch` (lecture (b)) et redirigé vers sa cause racine.
> Câblage du banc confirmé **500/1000/2000** : ECS-302 n'est plus exercé par ce
> matériel, ECS-305 monte, ECS-303 créé pour le point dur 1500→2000 W.
>
> **0.5.1 — CORRECTION du câblage.** La 0.5.0 consignait 500/1000/**2000** ;
> c'était faux. Le câblage réel est **500/1000/1500**. Trois consignations
> s'inversent : ECS-302 **est** exercé et redevient préalable structurel de
> l'étape 3 ; **ECS-303 est supprimé** (le « point dur » était une conséquence
> d'ECS-302 non traité, pas une exigence d'hystérésis) ; 7 valeurs et plafond
> 3000 W, non 8 et 3500. Ne pas se fier à une copie 0.5.0.
---
@ -159,8 +164,14 @@ unitaire et conditionne l'étape 3.
> parle à aucun matériel. L'étape 2 ne sort **aucune** classe de ce genre : elle
> extrait le noyau de calcul **interne au mécanisme relais**. Rien à voir.
**Étape 3 — ECS-302 / ECS-305.** Changement de structure de `m_relayMapping`.
Doit précéder tout ce qui s'appuiera dessus.
**Étape 3 — ECS-302 / ECS-305.** Changement de structure de `m_relayMapping` :
un palier doit pouvoir porter PLUSIEURS combinaisons. **Préalable structurel**
doit précéder tout ce qui s'appuiera dessus. Exercé par le câblage réel
(1500 W a deux encodages, §4).
> **Ne pas ouvrir l'étape 3 avant confirmation physique du câblage** (§13-3) :
> c'est lui qui justifie l'ordre de cette étape, et il provient aujourd'hui du
> simulateur, pas d'une mesure.
**Étape 4 — ECS-410** (échec d'écriture). ECS-411 a été **remonté en étape 1**
(§13-2). ECS-410 reste ici : il touche le même chemin `applyRelayStage` /
@ -250,44 +261,47 @@ fenêtre de verrou active ».
## §4 — Combinatoire (`RelayRouter`)
> **Câblage du banc CONFIRMÉ le 2026-08-08 : 500 / 1000 / 2000 W.** Pondération
> binaire — huit valeurs distinctes de 0 à 3500 W au pas de 500 W, **aucune
> redondance**, un seul encodage par palier.
> **Câblage CONFIRMÉ le 2026-08-08 : 500 / 1000 / 1500 W.** Additif (les relais
> se somment), **7 valeurs distinctes** de 0 à **3000 W**, avec **une
> redondance** : 1500 W a DEUX encodages.
>
> | Palier | Relais ON | W |
> |---|---|---|
> | 0 | — | 0 |
> | 1 | 500 | 500 |
> | 2 | 1000 | 1000 |
> | 3 | 500+1000 | 1500 |
> | 4 | 2000 | 2000 |
> | 5 | 500+2000 | 2500 |
> | 6 | 1000+2000 | 3000 |
> | 7 | 500+1000+2000 | 3500 |
> | W | Encodage(s) |
> |---|---|
> | 0 | — |
> | 500 | R500 |
> | 1000 | R1000 |
> | **1500** | **R1500** *ou* **R500+R1000** ← deux encodages |
> | 2000 | R500+R1500 |
> | 2500 | R1000+R1500 |
> | 3000 | R500+R1000+R1500 |
>
> Ceci clôt le point ouvert « 500/1000/1500 ou 500/1000/2000 » et déplace trois
> exigences — voir ECS-302, ECS-305 et ECS-303 ci-dessous.
> **Source : le simulateur du banc** (`etm-powersync-hems-sim`,
> `sim/sim_ecs_router.py:38``PALIERS = {1: 500.0, 2: 1000.0, 3: 1500.0}`,
> sommés dans `_power_w()`), confirmé en vol sur MQTT (relais 2+3 fermés →
> 2500 W publiés, ce qu'une cascade ne produirait pas). **Ce n'est PAS une
> mesure physique.** Cette valeur a déjà changé trois fois et elle commande
> désormais l'ordre de l'étape 3 : elle DOIT être confirmée sur plaque
> signalétique ou au pince ampèremétrique **avant l'ouverture de l'étape 3**.
**ECS-302 — Encodages équivalents. NON EXERCÉ PAR CE MATÉRIEL.** Avec une
pondération binaire il n'y a **rien à arbitrer** : un seul encodage par palier.
L'exigence reste valable pour un câblage 1-2-3 ailleurs (deux encodages pour
1500 W), mais **le changement de structure de `m_relayMapping` perd sa
justification immédiate** : il redescend dans l'ordre de l'étape 3 et **cesse
d'être un préalable structurel**. Ne plus le traiter comme tel.
**ECS-302 — Encodages équivalents. EXERCÉ PAR CE MATÉRIEL.** 1500 W admet
`{R1500}` et `{R500+R1000}` : il y a bien un arbitrage à rendre. L'exigence
**redevient un préalable structurel de l'étape 3** et reprend son rang **devant**
tout ce qui s'appuiera sur `m_relayMapping`.
**ECS-303 — Point dur 1500 → 2000 W.** Cette transition bascule **les trois
relais d'un coup** (R500 et R1000 s'ouvrent, R2000 se ferme). Avec l'ordre
coupure-avant-fermeture d'ECS-402, elle produit un **trou de puissance à chaque
franchissement**, en plein milieu de la plage de surplus utile.
Ce que fait le code aujourd'hui, et ce que ça coûte : le constructeur retient la
**première** combinaison rencontrée, c'est-à-dire le masque le plus bas
(`relayrouter.cpp:58`, `if (!byPower.contains(sum))`). Pour 1500 W il retient
donc `{R500+R1000}` et **jette `{R1500}`**. Conséquence directe sur la transition
1500 → 2000 W (`{R500+R1500}`) : il faut ouvrir R1000, garder R500, fermer
R1500 — et comme l'encodage retenu impose de passer par l'ordre
coupure-avant-fermeture d'ECS-402, la charge traverse un creux. Avec l'encodage
`{R1500}` pour 1500 W, la même transition ne demanderait qu'**une seule
commutation** : fermer R500.
La parade n'est PAS dans la combinatoire — aucun réordonnancement ne supprime le
trou, puisque aucun sous-ensemble intermédiaire ne vaut ni 1500 ni 2000 W. Elle
est dans un **seuil d'hystérésis élargi à cette frontière précise**. Entrée
commune avec ECS-605 (hystérésis paramétrable par charge). **À MESURER au banc
avant de coder quoi que ce soit** : c'est le relevé de commutations qui dira
quelle largeur donner au seuil.
**ECS-302 (suite) — Encodages équivalents.** Un palier DOIT pouvoir porter **plusieurs**
> **Il n'y a donc pas d'exigence d'hystérésis à ouvrir de ce côté.** Le « point
> dur 1500→2000 » qu'envisageait la 0.5.0 est une **conséquence d'ECS-302 non
> traité**, pas un phénomène propre au câblage. Aucune entrée pour ECS-303 (créé
> puis **supprimé** en 0.5.1) ni pour ECS-605. Un palier DOIT pouvoir porter **plusieurs**
combinaisons de relais, et la combinaison retenue DOIT être celle dont le coût de
transition depuis l'état courant est le plus faible : d'abord le nombre de relais
à basculer, puis le relais dont le compteur de commutations est le moins entamé.
@ -301,15 +315,17 @@ rencontrée gagne, c'est-à-dire le masque le plus bas. Sur un câblage
porter deux combinaisons. ECS-302 est donc un changement de structure, pas
l'ajout d'un critère.
**ECS-305 — Compteurs de commutations. IMPORTANCE RELEVÉE** par le câblage
binaire confirmé. Chaque relais DOIT porter un compteur de commutations, exposé
en télémétrie.
**ECS-305 — Compteurs de commutations.** Chaque relais DOIT porter un compteur
de commutations, exposé en télémétrie.
Sur un balayage complet des huit paliers, **R500 commute sept fois, R1000 trois
fois, R2000 une seule** : le bit de poids faible s'use sept fois plus vite que le
bit de poids fort. Le comptage par relais devient **la seule façon de savoir
quand le contacteur 500 W arrivera en fin de vie** — la puissance commutée, elle,
ne le dit pas.
Sur un balayage complet des sept paliers (0 → 3000 W), avec l'encodage retenu
aujourd'hui : **R500 commute 5 fois, R1000 3 fois, R1500 une seule**. Le relais
de plus faible puissance s'use cinq fois plus vite que le plus gros. Le comptage
par relais est **la seule façon de savoir quand le contacteur 500 W arrivera en
fin de vie** — la puissance commutée, elle, ne le dit pas.
\note Ces comptes dépendent de l'encodage choisi : traiter ECS-302 les modifie.
Raison de plus pour instrumenter avant d'optimiser.
**ECS-307 — Grain des temporisations.** `minOn`/`minOff` sont aujourd'hui **par
charge** (un unique `m_lastSwitch`, `relayrouter.h:107`), ce qui satisfait
@ -446,9 +462,8 @@ mécanique reste **L0**. Perte de sonde → mode sans sonde, pas arrêt d'urgenc
|---|---|---|
| ECS-306 | simulation | `testEcsBudgetUnderLock` |
| ECS-412 | simulation | `testEcsRebuildPreservesLock` |
| ECS-303 | banc, puis simulation | relevé de commutations à la frontière 1500/2000 — **mesurer avant de coder** |
| ECS-305 | unitaire | `testEcsSwitchCount` — compteur par relais |
| ECS-302 | unitaire | `testEcsSwitchCost`**non exerçable sur le câblage 500/1000/2000** (encodage unique) |
| ECS-302 | unitaire | `testEcsSwitchCost`**exerçable** : 1500 W = `{R1500}` ou `{R500+R1000}` |
| ECS-304 | unitaire | `testEcsLevelsPure` |
| ECS-410 | simulation | `testEcsPartialFailure` |
| ECS-411 | simulation | `testEcsRestartRecovery` |
@ -521,7 +536,10 @@ protocole dans le même lot.**
| 2026-08-08 | **§13-2 CLOS, lecture (b)** — ECS-412 réduit à `m_lastSwitch` + cause racine (rebuild incrémental) ; ECS-411 remonté en étape 1 |
| 2026-08-08 | **Étape 1 OUVERTE** — trois exigences : ECS-306, ECS-411, ECS-412 |
| 2026-08-08 | ECS-412 : au démarrage à froid, le verrou est **ARMÉ** (défaut sûr), jamais purgé |
| 2026-08-08 | Câblage banc confirmé **500/1000/2000** — ECS-302 non exercé, ECS-305 relevé, **ECS-303 créé** (point dur 1500→2000) |
| 2026-08-08 | ~~Câblage 500/1000/2000~~**donnée fausse**, corrigée le jour même (cf. ligne suivante) |
| 2026-08-08 | **Câblage 500/1000/1500** — 7 valeurs, plafond 3000 W, 1500 W à deux encodages. **Source : le simulateur** (`sim/sim_ecs_router.py:38`), **PAS une mesure** — à confirmer sur plaque ou au pince avant l'ouverture de l'étape 3. Troisième volte-face sur ce chiffre, et il commande désormais l'ordre de l'étape 3 |
| 2026-08-08 | **ECS-302 exercé** — redevient préalable structurel de l'étape 3, devant tout ce qui s'appuie sur `m_relayMapping` |
| 2026-08-08 | **ECS-303 SUPPRIMÉ** — le « point dur » 1500→2000 est une conséquence d'ECS-302 non traité, pas une exigence d'hystérésis. Aucune entrée ECS-605 de ce côté |
---
@ -529,11 +547,13 @@ protocole dans le même lot.**
1. **ECS-307** — grain des temporisations par relais : nécessaire, ou le compteur
de commutations suffit-il ? Le câblage binaire rend la question plus concrète
(R500 commute 7× plus que R2000), sans la trancher.
2. **ECS-303 / ECS-605** — largeur de l'hystérésis à la frontière 1500/2000 W,
et asymétrie montée/descente. **À mesurer au banc avant de coder** : c'est le
relevé de commutations qui donne le chiffre.
3. **Conflit d'écriture**`connectIO()` est le mécanisme natif si l'on veut un
(R500 commute 5× plus que R1500), sans la trancher.
2. **ECS-605** — asymétrie de l'hystérésis montée/descente, à régler à l'usage.
Sans rapport avec la frontière 1500/2000 : celle-ci relève d'ECS-302.
3. **Câblage à confirmer physiquement** — 500/1000/1500 provient du simulateur,
pas d'une mesure. À vérifier sur plaque ou au pince ampèremétrique **avant
l'ouverture de l'étape 3**, dont il commande l'ordre.
4. **Conflit d'écriture**`connectIO()` est le mécanisme natif si l'on veut un
jour exposer un relais ECS au pilotage manuel de l'utilisateur. Ce serait
alors un conflit d'écriture à arbitrer, pas une aide.
@ -548,8 +568,9 @@ protocole dans le même lot.**
ECS-411 remonte en étape 1. L'étape 1 porte donc trois exigences.
`testEcsRebuildPreservesLock` couvre obligatoirement « `SetLoadConfig` pendant
une fenêtre de verrou active ».
- **Câblage du banc** — confirmé **500/1000/2000 W** (cf. §4). ECS-302 n'est pas
exercé par ce matériel ; ECS-305 monte en importance ; ECS-303 créé.
- **Câblage du banc****500/1000/1500 W**, source simulateur (cf. §4, et la
réserve : à confirmer physiquement avant l'étape 3). ECS-302 **est** exercé et
redevient préalable structurel de l'étape 3.
**CLOS par l'audit** : le waterfall ne choisit pas entre encodages équivalents
(il n'en voit qu'un) ; `ioConnections()`/`connectIO()` est un lien 1↔1