From 380823dc9dbed1c3ba615b7463b5bd2f86e323b2 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Thu, 25 Jun 2026 12:56:45 +0200 Subject: [PATCH] =?UTF-8?q?[doc]=20TEST=5FTERRAIN.md=20:=20helper=20relay(?= =?UTF-8?q?)=20JSON-RPC=20+=20seuils=20SG-Ready=20+=20pr=C3=A9-vol=20?= =?UTF-8?q?=C2=A73.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TÂCHE A — relay() réécrit (JSON-RPC complet) : - JSONRPC.Hello obligatoire avant tout appel - auth optionnelle : Users.Authenticate → token (NYMEA_USER/NYMEA_PASS) - framing par id : boucle sur lignes newline-delimited, ignore notifications push - résolution stateTypeId 'power' via GetThingClasses (les states GetThings ne portent pas le nom, seulement le stateTypeId — l'ancien s.get('name') ne matchait jamais) - note ACCEPTANCE : tester relay isolément avant T1 TÂCHE B — seuils SG-Ready réels (rulebasedscheduler.cpp kForceMargin=1.2) : - table seuils en en-tête §4 : état 3 ≥1500, état 4 ≥3600, zone morte ≥3000 - T7 : arithmétique du recrédit explicitée (2500+1500=4000≥3600, pas d'incohérence) - T9 : annotations (budget ≈ XXXX) supprimées → sémantique surplus brut Batch précédent (session antérieure rejetée) : - ThingIds réels R500/R1000/R2000/K1/K2 + BOX/MPORT remplis partout - root@$BOX → etm@$BOX + sudo (§1.b, §1.c, helpers, logs()) - §3.5 pré-vol SG-Ready : polarité K1/K2 + gpioget nymead arrêté + multimètre NO-COM Co-Authored-By: Claude Sonnet 4.6 --- docs/TEST_TERRAIN.md | 212 ++++++++++++++++++++++++++++++++----------- 1 file changed, 161 insertions(+), 51 deletions(-) diff --git a/docs/TEST_TERRAIN.md b/docs/TEST_TERRAIN.md index c5acbae..4679fb8 100644 --- a/docs/TEST_TERRAIN.md +++ b/docs/TEST_TERRAIN.md @@ -1,8 +1,9 @@ # TEST_TERRAIN.md — Procédure de test Palier 1 (nymea-dev arm64) Test terrain du moteur **etm-powersync-energy-plugin-etm** sur banc : compteur **mock -forçable** (puissance injectée par HTTP) + **relais GPIO réels** (vérifiés par `gpioget`). -14 tests (T1–T14). Chaque test : **inject → log attendu → vérif GPIO → case ✓/✗**. +forçable** (puissance injectée par HTTP) + **relais GPIO réels** (état vérifié via +JSON-RPC nymead ou multimètre NO-COM). +14 tests (T1–T14). Chaque test : **inject → log attendu → vérif relais → case ✓/✗**. > Convention puissance compteur : `currentPower < 0` = **export** (surplus PV) ; `> 0` = **import**. > Le moteur calcule un surplus net SIGNÉ `(exportW − importW)` qui cascade par priorité. @@ -13,16 +14,19 @@ forçable** (puissance injectée par HTTP) + **relais GPIO réels** (vérifiés | Élément | Valeur | Source | |---|---|---| -| IP nymea-dev | `__________` | **[À REMPLIR]** (réseau banc) | -| Port JSON-RPC | `nymeas://:2222` (TCP+TLS) · `wss://:4444` | connu | +| IP / hostname | `192.168.1.75` — hostname `hems`, user `etm` (sudo) | briefing terrain | +| Port JSON-RPC | `nymeas://hems:2222` (TCP+TLS) · `wss://hems:4444` | connu | | Auth requise ? | `__________` | **[À LIRE SUR LA BOX]** `JSONRPC.Hello` → champ `authenticationRequired` | | Token (si auth) | `__________` | `Users.Authenticate {username,password,deviceName}` → `token` | | ThingClassId compteur mock | `2721a051-6e12-471a-baba-21d87c4cebc9` | connu (energymocks) | | ThingId compteur mock | `__________` | **[À LIRE]** après `Integrations.AddThing`, ou `Integrations.GetThings` | -| Port HTTP compteur mock | `26655` (param `port` par défaut) | connu | -| ThingId relais R500 / R1000 / R2000 | `__________` | **[À LIRE]** `Integrations.GetThings` (plugin GPIO relais réel) | -| ThingId relais SG-Ready K1 / K2 | `__________` | **[À LIRE]** idem | -| gpiochip + offsets relais | `__________` | **[À LIRE]** `gpiodetect` puis `gpioinfo gpiochip0` | +| Port HTTP compteur mock | `26655` | connu | +| ThingId R500 (Relay1 C1 BCM5, pin 29) | `8538782f-2c8c-4a30-bfce-c8140f791c9b` | briefing terrain | +| ThingId R1000 (Relay2 C2 BCM6, pin 31) | `2ebe6bef-829b-4695-9d11-ddb4c16c5448` | briefing terrain | +| ThingId R2000 (Relay3 C3 BCM13, pin 33) | `b033b212-1adb-4df0-ba2b-8fa477de52a2` | briefing terrain | +| ThingId K1 (Relay4 C4 BCM16, pin 36) | `beaf92e1-aedc-4b84-9ce4-e423648638cc` | briefing terrain | +| ThingId K2 (Relay5 C5 BCM19, pin 35) | `bf236e64-5ae7-4bf1-82a6-4ede03de75a6` | briefing terrain | +| gpiochip + BCM offsets | `gpiochip0` ; BCM R500=5, R1000=6, R2000=13, K1=16, K2=19 | vérifiés — **`gpioget` valide uniquement nymead ARRÊTÉ** | | Conteneur build cross-arm64 | `__________` | **[À LIRE DANS `etm-powersync-deploy`/DEPLOY.md]** | > `[À LIRE SUR LA BOX]` = dépend du déploiement, pas inventé ici. Tout le reste est figé. @@ -42,10 +46,10 @@ forçable** (puissance injectée par HTTP) + **relais GPIO réels** (vérifiés ### b. Déploiement sur la box ```bash -BOX= # [À REMPLIR] -scp nymea-energy-plugin-nymea_*_arm64.deb root@$BOX:/tmp/ -ssh root@$BOX 'dpkg -i /tmp/nymea-energy-plugin-nymea_*_arm64.deb && systemctl restart nymead' -ssh root@$BOX 'journalctl -u nymead -f' # suivre les logs +BOX=hems +scp nymea-energy-plugin-nymea_*_arm64.deb etm@$BOX:/tmp/ +ssh etm@$BOX 'sudo dpkg -i /tmp/nymea-energy-plugin-nymea_*_arm64.deb && sudo systemctl restart nymead' +ssh etm@$BOX 'sudo journalctl -u nymead -f' # suivre les logs ``` > `.so` installé dans `/usr/lib//nymea/energy/libnymea_energypluginnymea.so`. @@ -53,10 +57,10 @@ ssh root@$BOX 'journalctl -u nymead -f' # suivre les logs La catégorie est exactement **`NymeaEnergy`** (`NYMEA_LOGGING_CATEGORY(dcNymeaEnergy, "NymeaEnergy")`). Méthode robuste — drop-in systemd (Qt logging rules) : ```bash -ssh root@$BOX 'mkdir -p /etc/systemd/system/nymead.service.d && \ - printf "[Service]\nEnvironment=QT_LOGGING_RULES=NymeaEnergy.debug=true\n" \ - > /etc/systemd/system/nymead.service.d/etm-logging.conf && \ - systemctl daemon-reload && systemctl restart nymead' +ssh etm@$BOX 'sudo mkdir -p /etc/systemd/system/nymead.service.d && \ + printf "[Service]\nEnvironment=QT_LOGGING_RULES=NymeaEnergy.debug=true\n" | \ + sudo tee /etc/systemd/system/nymead.service.d/etm-logging.conf >/dev/null && \ + sudo systemctl daemon-reload && sudo systemctl restart nymead' ``` > Alternative selon la version : `nymead --logging ` ou la section logging de > `/etc/nymea/nymead.conf`. **[VÉRIFIER `nymead --help` SUR LA BOX]** si le drop-in ne suffit pas. @@ -78,10 +82,10 @@ Workflow : déployer une 1re fois → `Integrations.AddThing` le compteur mock + #ifdef ETM_ARBITRATOR { ThingManager *tm = thingManager(); - // --- ECS 3 relais (8 niveaux binaires) — ThingId réels [À REMPLIR] --- - const QString R500 = "{__________}"; // relais GPIO 500 W - const QString R1000 = "{__________}"; // relais GPIO 1000 W - const QString R2000 = "{__________}"; // relais GPIO 2000 W + // --- ECS 3 relais (8 niveaux binaires) --- + const QString R500 = "{8538782f-2c8c-4a30-bfce-c8140f791c9b}"; // Relay1 C1 BCM5 + const QString R1000 = "{2ebe6bef-829b-4695-9d11-ddb4c16c5448}"; // Relay2 C2 BCM6 + const QString R2000 = "{b033b212-1adb-4df0-ba2b-8fa477de52a2}"; // Relay3 C3 BCM13 auto *ecs = new EcsRelayAdapter( tm, "ecs-terrain", "ECS banc", QList({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}), @@ -90,9 +94,9 @@ Workflow : déployer une 1re fois → `Integrations.AddThing` le compteur mock + /*minOnS*/ 60, /*minOffS*/ 60, /*priority*/ 1, chargingManager); chargingManager->registerEcsAdapter(ecs); - // --- SG-Ready (2 bits K1/K2) — ThingId réels [À REMPLIR] --- - const QString K1 = "{__________}"; - const QString K2 = "{__________}"; + // --- SG-Ready (2 bits K1/K2) --- + const QString K1 = "{beaf92e1-aedc-4b84-9ce4-e423648638cc}"; // Relay4 C4 BCM16 + const QString K2 = "{bf236e64-5ae7-4bf1-82a6-4ede03de75a6}"; // Relay5 C5 BCM19 auto *pac = new SgReadyAdapter( tm, "pac-terrain", "PAC banc", QHash>({ {1,{K1}}, {2,{}}, {3,{K2}}, {4,{K1,K2}} }), @@ -109,17 +113,89 @@ Workflow : déployer une 1re fois → `Integrations.AddThing` le compteur mock + --- -## Helpers bash (poste dev ou box) +## Helpers bash (poste dev) + ```bash -BOX=; MPORT=26655 # [À REMPLIR] +BOX=hems; MPORT=26655 +# ThingIds relais (banc hems — pré-vérifiés, cf. §0) +TID_R500="8538782f-2c8c-4a30-bfce-c8140f791c9b" +TID_R1000="2ebe6bef-829b-4695-9d11-ddb4c16c5448" +TID_R2000="b033b212-1adb-4df0-ba2b-8fa477de52a2" +TID_K1="beaf92e1-aedc-4b84-9ce4-e423648638cc" +TID_K2="bf236e64-5ae7-4bf1-82a6-4ede03de75a6" # Injecter une puissance compteur (W). Négatif = export (surplus PV), positif = import. inject(){ curl -s "http://$BOX:$MPORT/setstates?connected=true¤tPowerPhaseA=$(($1/3))¤tPowerPhaseB=$(($1/3))¤tPowerPhaseC=$(($1/3))" >/dev/null; echo "compteur = $1 W"; } # Compteur MUET : on cesse d'injecter (>90 s) → watchdog L2 bascule (QTimer 30 s, seuil 90 s). mute(){ echo "NE PLUS injecter pendant >90 s…"; sleep 95; } -# Lire un relais GPIO réel (0/1) : relay -relay(){ ssh root@$BOX "gpioget $1 $2"; } # ex. relay gpiochip0 17 -# Suivre les décisions -logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgReady'"; } +# Suivre les décisions de l'arbitre. +logs(){ ssh etm@$BOX "sudo journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgReady'"; } +``` + +```bash +# relay → 1 (ON) | 0 (OFF) via JSON-RPC local nymead. +# Protocole complet : Hello → auth si requise → GetThingClasses (résolution stateTypeId +# 'power') → GetThings. Framing : boucle par id — ignore les notifications push nymea. +# gpioget inutilisable nymead actif (libgpiod v2 busy). Filet : multimètre NO-COM nymead arrêté. +# Variables optionnelles : NYMEA_USER (défaut admin) NYMEA_PASS (défaut vide). +# ACCEPTANCE avant T1 : forcer un relais ON via l'app nymea, vérifier relay $TID_Rxx → 1 ; +# forcer OFF → 0. Tant que ce test isolé ne passe pas, la colonne "vérif" de T1–T14 est aveugle. +relay(){ + local tid="$1" + local nu="${NYMEA_USER:-admin}" np="${NYMEA_PASS:-}" + ssh etm@$BOX "python3 - '$nu' '$np'" << GETSTATE +import ssl, socket, json, sys + +user, pw = sys.argv[1], sys.argv[2] + +ctx = ssl.create_default_context() +ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE +buf = b'' + +def call(s, rid, method, params=None, token=None): + global buf + req = {'id': rid, 'method': method} + if params: req['params'] = params + if token: req['token'] = token + s.sendall((json.dumps(req) + '\n').encode()) + while True: + buf += s.recv(4096) + lines = buf.split(b'\n'); buf = lines[-1] + for ln in lines[:-1]: + ln = ln.strip() + if not ln: continue + try: + m = json.loads(ln) + if m.get('id') == rid: return m + except: pass + +with socket.create_connection(('localhost', 2222), timeout=10) as raw, \ + ctx.wrap_socket(raw) as s: + hello = call(s, 1, 'JSONRPC.Hello') + token = None + if hello.get('params', {}).get('authenticationRequired', False): + r = call(s, 2, 'Users.Authenticate', + {'username': user, 'password': pw, 'deviceName': 'relay-chk'}) + p = r.get('params', {}) + if not p.get('success', True) or 'token' not in p: + sys.exit('AUTH_FAILED — définir NYMEA_USER / NYMEA_PASS') + token = p['token'] + # Résoudre stateTypeId de 'power' pour chaque ThingClass (les states dans GetThings + # ne portent pas le nom, seulement le stateTypeId — cf. nymea JSON-RPC spec). + tc = call(s, 3, 'Integrations.GetThingClasses', token=token) + power_stid = {} + for c in tc.get('params', {}).get('thingClasses', []): + for st in c.get('stateTypes', []): + if st.get('name') == 'power': + power_stid[c['id']] = st['id'] + resp = call(s, 4, 'Integrations.GetThings', token=token) + things = resp.get('params', {}).get('things', []) + t = next((x for x in things if x['id'] == '${tid}'), None) + if not t: sys.exit('THING_NOT_FOUND: ${tid}') + stid = power_stid.get(t.get('thingClassId', ''), '') + state = next((st for st in t.get('states', []) if st.get('stateTypeId') == stid), None) + print(1 if state and state.get('value') else 0) +GETSTATE +} ``` > Le compteur mock pose `currentPower = somme des 3 phases`. `RootMeter::currentPower()` le relit. @@ -130,13 +206,13 @@ logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgRea ### T1 — Montée sur surplus - **inject** : `inject -2500` - **log attendu** : `[Arbitre] … Surplus PV … ECS palier 1` puis `[EcsRelayAdapter] … → stage 1` -- **vérif GPIO** : `relay ` → **1** (ON) +- **vérif** : `relay $TID_R2000` → **1** (ON) - ✓ / ✗ : `____` ### T2 — Délestage (import) - **inject** : `inject 1000` (import → surplus net négatif) - **log attendu** : `Surplus insuffisant … ECS éteint` ; `→ stage 0` -- **vérif GPIO** : relais → **0** (OFF) +- **vérif** : `relay $TID_R2000` → **0** (OFF) - ✓ / ✗ : `____` --- @@ -146,50 +222,83 @@ logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgRea ### T3 — Cascade montante - **inject** : `inject -1700` (budget 1700 → palier **1500**) - **log** : `ECS palier 3 (1500 W)` -- **vérif GPIO** : R500=**1**, R1000=**1**, R2000=**0** +- **vérif** : `relay $TID_R500`→**1**, `relay $TID_R1000`→**1**, `relay $TID_R2000`→**0** - ✓ / ✗ : `____` ### T4 — Transition NON-CASCADÉE 1500 → 2000 (le test clé) - **contexte** : on part de T3 (palier 1500, l'ECS mesure 1500 W). - **inject** : `inject -700` (budget = 700 + 1500 recrédit = 2200 → palier **2000**) - **log** : `ECS palier 4 (2000 W)` ; commutation **off-before-on** (R500/R1000 coupés avant R2000) -- **vérif GPIO** : R500=**0**, R1000=**0**, R2000=**1** ← *set final R2000 SEUL, pas un état parasite durable* +- **vérif** : `relay $TID_R500`→**0**, `relay $TID_R1000`→**0**, `relay $TID_R2000`→**1** ← *set final R2000 SEUL, pas un état parasite durable* - ✓ / ✗ : `____` ### T5 — Protection minOn (anti court-cycling) - **contexte** : ECS vient de commuter (< minOn 60 s). - **inject** : `inject 1200` (import → le budget voudrait éteindre) - **log attendu** : `Verrou minOn — … maintenu palier …` -- **vérif GPIO** : relais **inchangés** (maintien). Attendre > 60 s puis re-`inject 1200` → délestage. +- **vérif** : relais **inchangés** (maintien). Attendre > 60 s puis re-`inject 1200` → délestage. - ✓ / ✗ : `____` ### T6 — Délestage complet - **inject** : `inject 2000` (import franc, > minOn écoulé) -- **vérif GPIO** : R500=R1000=R2000=**0** +- **vérif** : `relay $TID_R500`→**0**, `relay $TID_R1000`→**0**, `relay $TID_R2000`→**0** - ✓ / ✗ : `____` --- +## §3.5 — Pré-vol SG-Ready (**à cocher avant T7, nymead ARRÊTÉ**) + +> ⚠️ La polarité K1/K2 est le **seul point non vérifié proprement** de toute la chaîne. +> Le test initial a échoué sur `Device or resource busy` (nymead tient les lignes GPIO via +> libgpiod v2). Cette vérification doit se faire **nymead arrêté**, au multimètre, avant +> toute mise sous tension côté PAC. + +```bash +ssh etm@$BOX 'sudo systemctl stop nymead' +ssh etm@$BOX 'gpioget -c gpiochip0 16 19' # attendu : 0 0 (repos LOW BCM16/BCM19) +``` + +- [ ] `gpioget` répond **0 0** — K1 (BCM16) et K2 (BCM19) au repos LOW. ✓ +- [ ] **K1 repos = circuit ouvert** : multimètre en continuité NO-COM sur bornes K1 → **circuit ouvert** + = PAC interprète 0:0 = **état 2 « normal »** (jamais 1:0 = blocage au boot). ✓ +- [ ] **K2 repos = circuit ouvert** (idem). ✓ +- [ ] **Fail-safe boot confirmé** : nymead démarre K1=K2=0 (repos LOW, NO/NC → circuit ouvert → PAC = normal). ✓ +- [ ] `sudo systemctl start nymead` avant T7. + +--- + ## §4 — SG-Ready (PAC, 2 bits K1/K2 ; P3=1500, P4=3000) +> **Seuils code** (`rulebasedscheduler.cpp`, `kForceMargin=1.2`, P3=1500, P4=3000) : +> - État 3 si budgetW ≥ 1500 ; état 4 si budgetW ≥ P4×1,2 = **3600** ; zone morte état 4 si budgetW ≥ 3000. +> - `budgetW = exportW_brut + allocatedNow` ; allocatedNow = 0 (état 2), 1500 (état 3), 3000 (état 4). +> - Depuis état 2 (sans recrédit) : min. inject **≤ −3600** pour atteindre état 4 directement. +> - Depuis état 3 (recrédit 1500) : min. inject **≤ −2100** pour atteindre état 4. + ### T7 — Montée d'états 2 → 3 → 4 -- **inject** `-1000` → état **2** (K1=0,K2=0) ; **inject** `-2000` → état **3** (K2=1) ; - attendre > minStateHold (300 s) puis **inject** `-2500` → état **4** (K1=1,K2=1). +- `inject -1000` → état **2** (budget 1000 < 1500, K1=0,K2=0). +- `inject -2000` → état **3** (budget 2000 ≥ 1500, K2=1). Attendre > minStateHold (300 s). +- `inject -2500` → état **4** (budget = 2500 + 1500 recrédit = **4000 ≥ 3600**, K1=1,K2=1). - **log** : `… recommandée (état 3 …)` puis `… forcée (état 4 …)` -- **vérif GPIO** : état 3 → K1=0,K2=1 ; état 4 → K1=1,K2=1 +- **vérif** : état 3 → `relay $TID_K1`→**0**, `relay $TID_K2`→**1** ; état 4 → `relay $TID_K1`→**1**, `relay $TID_K2`→**1** - ✓ / ✗ : `____` ### T8 — Atomicité (transitoire bénin) - **contexte** : transition 2→4 (00→11) commute K1 ET K2. -- **vérif GPIO** : set **final** K1=**1**, K2=**1** (état 4). Le transitoire passe par K2 d'abord - (01=reco), **jamais** 10=blocage — sub-ms, non observable à `gpioget` mais **garanti code** - (`transientHarm`). Confirmer simplement le set final correct. +- **vérif** : set **final** `relay $TID_K1`→**1**, `relay $TID_K2`→**1** (état 4). Le transitoire + passe par K2 d'abord (01=reco), **jamais** 10=blocage — sub-ms, non observable, mais + **garanti code** (`transientHarm`). Confirmer simplement le set final correct. - ✓ / ✗ : `____` -### T9 — Hystérésis (pas de bascule 3↔4) -- **contexte** : PAC en état 4 ; faire osciller le surplus dans la zone morte. -- **inject** `-300` (budget ≈ 3300) → reste **4** ; `-500` (≈3500) → reste **4** ; `-100` (≈3100) → reste **4**. -- **vérif GPIO** : K1=K2=**1** stable (aucun clignotement 3↔4). Passer `inject 300` (budget < 3000) → sort en 3. +### T9 — Zone morte hystérésis (pas de bascule 4→3 sur fluctuation surplus) +- **contexte** : PAC en état 4 (K1=K2=1) depuis > minStateHold (300 s). + L'opérateur injecte le **surplus brut au compteur** — le moteur recrédite la puissance + allouée (3000 W déclarés) en interne ; ne pas l'ajouter à `inject`. +- Faire fluctuer le surplus : `inject -300` → reste **4** ; `inject -500` → reste **4** ; + `inject -100` → reste **4** ; `inject 0` (équilibré) → reste **4** (tout inject ≤ 0 tient l'état). +- **Sortie** : `inject 300` (import net) → **état 3**. +- **vérif** : `relay $TID_K1`→**1**, `relay $TID_K2`→**1** stable pendant la fluctuation ; + `relay $TID_K1`→**0** uniquement après `inject 300`. - ✓ / ✗ : `____` --- @@ -199,19 +308,20 @@ logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgRea ### T10 — Compteur muet → mode dégradé - **inject** `-2500` (ECS/PAC servis), puis **`mute`** (>90 s sans injection). - **log attendu** : `[Arbitre] Compteur muet depuis … mode dégradé L2` ; ECS palier 0 ; PAC **état 2**. -- **vérif GPIO** : relais ECS → **0** ; PAC K1=0,K2=0 (état 2, **jamais** blocage). +- **vérif** : `relay $TID_R500`→**0**, `relay $TID_R1000`→**0**, `relay $TID_R2000`→**0** ; + `relay $TID_K1`→**0**, `relay $TID_K2`→**0** (état 2, **jamais** blocage). - ✓ / ✗ : `____` ### T11 — Stabilité (faux surplus piège) - **contexte** : toujours muet ; la **dernière** valeur injectée (-2500) reste « collée » au compteur. -- **vérif GPIO** : sur plusieurs minutes muettes, ECS **reste 0** malgré ce surplus stale +- **vérif** : sur plusieurs minutes muettes, ECS **reste 0** malgré ce surplus stale (planification suspendue — pas de replanif sur cache mort). - ✓ / ✗ : `____` ### T12 — Reprise - **inject** `-2500` (le compteur re-parle). - **log attendu** : `Compteur de nouveau actif — sortie du mode dégradé` ; recalcul normal. -- **vérif GPIO** : ECS resuit le surplus (palier > 0). Pas de restauration d'ancienne consigne. +- **vérif** : ECS resuit le surplus (palier > 0). Pas de restauration d'ancienne consigne. - ✓ / ✗ : `____` --- @@ -223,7 +333,7 @@ logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgRea - **ECS prio 1 / PAC prio 2** : ECS se sert (palier), PAC voit le reliquat (600) → **état 2**. - **Inverser les priorités** (échanger `priority` dans le bloc §1.d, rebuild/redeploy) → PAC se sert (état 3), ECS voit le reliquat (1500 < 2400) → **palier 0**. -- **vérif GPIO** : le service s'inverse selon la priorité (preuve du waterfall unifié). +- **vérif** : le service s'inverse selon la priorité (preuve du waterfall unifié). - ✓ / ✗ : `____` --- @@ -241,8 +351,8 @@ logs(){ ssh root@$BOX "journalctl -u nymead -f | grep -E 'Arbitre|EcsRelay|SgRea ## Checklist sécurité (AVANT mise sous tension) - [ ] **Disjoncteur L0** du banc **accessible** et identifié (coupure physique immédiate). - [ ] Banc câblé hors tension ; sections/calibres relais conformes aux puissances (R2000 = 2000 W). -- [ ] **Polarité / repérage K1/K2** SG-Ready vérifié (1:0=blocage, 0:0=normal, 0:1=reco, 1:1=forcé). +- [ ] **Polarité SG-Ready vérifiée** (§3.5 fait et coché) — seul point non vérifié avant cette session. - [ ] Relais résistifs ECS : pas de charge inductive sur ces voies. -- [ ] `gpioinfo` relevé et **mapping offset↔relais consigné** avant tout test. +- [ ] Mapping BCM↔relais consigné (`gpioinfo gpiochip0`, nymead arrêté) avant tout test. - [ ] L1 (failsafe bornes/relais) configuré si applicable ; L0 reste le filet ultime. - [ ] Un opérateur à la main sur le disjoncteur pendant T1–T6 (montées de puissance).