fix(sg-ready): ECS-110 sans Q_ASSERT — refus explicite au lieu d'une assertion muette

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) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-08-09 12:34:39 +02:00
parent 9b4945bb58
commit 42cb2f080b
2 changed files with 35 additions and 4 deletions

View File

@ -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) {

View File

@ -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 deux adaptateurs se disputeraient les mêmes
* contacts et le budget compterait la charge deux fois.
*/
QSet<QString> 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;
};