From 42cb2f080b362332a93d64959b764b4f938d50cb Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sun, 9 Aug 2026 12:34:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(sg-ready):=20ECS-110=20sans=20Q=5FASSERT=20?= =?UTF-8?q?=E2=80=94=20refus=20explicite=20au=20lieu=20d'une=20assertion?= =?UTF-8?q?=20muette?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les deux Q_ASSERT de SgReadyAdapter (states non vide, état 2 présent) gardaient un invariant de configuration. QT_NO_DEBUG les retire du binaire livré : le jour où la PAC passe à la configuration, ils ne gardent plus rien chez le client. Ils sont donc retirés AVANT ce basculement, remplacés par un drapeau m_usable calculé à la construction : journalisation critique, refus de toute commande, available faux. L'état 2 est le plancher de repli du mode dégradé L2 et de la désactivation (ECS-413, ECS-414) — une PAC qui ne peut pas l'exprimer ne doit pas piloter. Ajoute aussi claimedRelays(), accesseur des contacts revendiqués, consommé par le commit qui branche la fabrique depuis la configuration. Co-Authored-By: Claude Opus 5 (1M context) --- energyplugin/etm/adapters/sgreadyadapter.cpp | 27 +++++++++++++++++--- energyplugin/etm/adapters/sgreadyadapter.h | 12 +++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/energyplugin/etm/adapters/sgreadyadapter.cpp b/energyplugin/etm/adapters/sgreadyadapter.cpp index 354a3ba..b9b7de4 100644 --- a/energyplugin/etm/adapters/sgreadyadapter.cpp +++ b/energyplugin/etm/adapters/sgreadyadapter.cpp @@ -33,8 +33,21 @@ SgReadyAdapter::SgReadyAdapter(ThingManager *thingManager, { m_states = m_stateRelays.keys(); std::sort(m_states.begin(), m_states.end()); - Q_ASSERT(!m_states.isEmpty()); - Q_ASSERT(m_stateRelays.contains(2)); // état 2 (normal) = repli sûr obligatoire + + // ECS-110 — la présence de l'état 2 NE DOIT PAS reposer sur un Q_ASSERT : QT_NO_DEBUG est + // défini en release, l'assertion disparaît du binaire livré. Tant que les états venaient + // d'une construction codée en dur elle ne pouvait pas échouer ; dès qu'ils viennent de la + // configuration, elle serait le SEUL garde-fou sur le repli sûr du mode dégradé L2. + // + // La vérification vit donc dans LoadConfig::isValid() (refus explicite, message FR) et le + // contrôle ci-dessous n'est qu'un dernier filet pour les constructions directes (tests, + // enregistrement codé en dur) : il PARLE au lieu de disparaître. + m_usable = !m_states.isEmpty() && m_stateRelays.contains(2); + if (!m_usable) + qCCritical(dcNymeaEnergy()) << "[SgReadyAdapter]" << m_label + << "— encodage INUTILISABLE : l'état 2 (normal) n'est pas" + << "exprimable. C'est le repli sûr du mode dégradé L2 ;" + << "l'adaptateur refusera toute commande."; } LoadDescriptor SgReadyAdapter::descriptor() const @@ -56,7 +69,7 @@ LoadDescriptor SgReadyAdapter::descriptor() const LoadTelemetry SgReadyAdapter::telemetry() const { LoadTelemetry t; - t.available = !m_faulted; // ECS-414 : sort de l'ARBITRAGE, pas de la COMPTABILITÉ. + t.available = m_usable && !m_faulted; // ECS-414 : sort de l'ARBITRAGE, pas de la COMPTABILITÉ. t.lastActionAt = m_lastActionAt; // Base du recrédit budget = puissance ALLOUÉE de l'état (déclaré), pas la conso mesurée // (états 1/2 → 0 ; états 3/4 → P3/P4). Cf. invariant 8. @@ -82,7 +95,7 @@ LoadContext SgReadyAdapter::toLoadContext(const QDateTime &now) const ctx.limits = descriptor().limits; ctx.telemetry.currentPowerW = telemetry().currentPowerW; - ctx.telemetry.available = !m_faulted; + ctx.telemetry.available = m_usable && !m_faulted; ctx.telemetry.state = m_currentState; ctx.telemetry.lastSwitch = m_lastSwitch; if (m_faulted) { @@ -107,6 +120,12 @@ LoadAction SgReadyAdapter::applyAction(const LoadAction &action, const QDateTime return action; } + if (!m_usable) { + LoadAction refused = action; + refused.state = m_currentState; + return refused; // encodage sans état 2 : aucune commande, cf. constructeur + } + // ECS-414 — en défaut, plus aucune commande, y compris forcée : trois tentatives ont // déjà échoué, en réémettre masquerait l'état sans rien réparer. if (m_faulted) { diff --git a/energyplugin/etm/adapters/sgreadyadapter.h b/energyplugin/etm/adapters/sgreadyadapter.h index 93fb417..6335884 100644 --- a/energyplugin/etm/adapters/sgreadyadapter.h +++ b/energyplugin/etm/adapters/sgreadyadapter.h @@ -119,6 +119,14 @@ public: */ void applySafeState(const QDateTime &now) override; + /*! \brief Contacts que cet adaptateur revendique, tous états confondus. + * + * Sert à détecter qu'une PAC de configuration (LM-300) et une PAC enregistrée en dur + * commandent le même matériel — situation où deux adaptateurs se disputeraient les mêmes + * contacts et où le budget compterait la charge deux fois. + */ + QSet claimedRelays() const { return allRelays(); } + /*! * \brief État SG-Ready courant (1-4). * \return Dernier état réellement commuté ; valeur initiale tant qu'aucune transition @@ -188,6 +196,10 @@ private: int m_stateTarget = 2; Phase m_phase = PhaseNominale; bool m_faulted = false; + //! Faux si l'encodage ne permet pas d'exprimer l'état 2 — repli sûr du mode dégradé L2. + //! L'adaptateur refuse alors TOUTE commande, et le dit (ECS-110 : jamais un Q_ASSERT, + //! qui disparaît du binaire release). + bool m_usable = true; QDateTime m_lastSwitch; //!< Dernier changement d'état (null = jamais). QDateTime m_lastActionAt; };