Thème
Profils Modbus & objets métier
Les profils standard (INIS-E16, AVISS) et les clés métier (incidents, commandes, sons, états système) sont définis en code. Les incidents, commandes et sons portent une clé d'enum ; les états système portent une constante de chaîne (SystemFlagKeys), une clé étant partagée par des centrales qui la servent à des bits différents. Les profils personnalisés créés depuis l'application sont stockés en base. Au démarrage, chaque profil standard est validé puis semé en base ; sur les incidents et les commandes, une incohérence — clé inconnue, doublon, son inexistant — fait échouer le démarrage avec un message clair. Les définitions d'états système ne passent pas par ce validateur : leur cohérence tient au typage et aux tests.
Où vivent les adresses Modbus
Les adresses de registres et les codes de commande des centrales standard sont déclarés dans une seule source :
dotnet/shared/Profiles/StandardPanelProfiles.csCe fichier expose une cartographie PanelRegisterMap par type de centrale (StandardPanelProfiles.InisE16, StandardPanelProfiles.Aviss) : adresses fil JBUS brutes (PointFireAddress, ZoneFireAddress, EvacuationAddress…), décalage d'adressage (AddressOffset), nombre de zones et codes de fonction des commandes (CmdReset, CmdSilence, CmdDisableZone…).
Cette cartographie est la source de vérité partagée par les deux côtés qui parlent Modbus :
| Consommateur | Point d'entrée | Rôle |
|---|---|---|
| Backend | ModbusProfile.CreateInisE16() / CreateAviss() (helper FromRegisterMap) | Construit le profil complet (adresses + incidents / commandes / états système) servi à l'application |
| Cœur du simulateur | ProfileLoader.GetProfile() dans dotnet/simulator.core/ProfileConfig.cs (remplit ProfileConfig) ; HandleCommand compare aux codes du profil | Émule une centrale aux mêmes adresses |
Le projet shared est une bibliothèque légère (aucune dépendance backend), référencée par le backend et par simulator.core. Les trois consommateurs du simulateur — l'interface graphique (dotnet/simulator), l'hôte sans interface (dotnet/simulator.headless) et les tests (dotnet/tests/simulator) — passent tous par simulator.core, donc par la même cartographie. Les adresses ne peuvent pas diverger.
Modifier une adresse ou un code de commande
Éditer la valeur dans StandardPanelProfiles (InisE16 ou Aviss) — un seul endroit. Le backend et le simulateur reflètent le changement au prochain build.
Le test ModbusProfileTests (dotnet/tests/backend/Domain/ValueObjects/) asservit les valeurs littérales des profils standard : le mettre à jour en même temps que la cartographie.
Identité de drapeau : incidents à comportement partagé
Le vocabulaire des comportements (ZoneStatus) est fermé — il commande la mécanique : priorité par défaut, danger appliqué sans délai à l'apparition, synthèses, couleur de repli. Mais l'identité d'un incident dans tout ce qui circule — drapeaux des instantanés, acquittements, journaux, affichage — est son FlagId (IncidentDefinition.FlagId) :
- un incident à clé livrée (
fire,fault,dm… — vocabulaireIncidentKey) s'identifie par le nom de son comportement (« Fire », « Fault »), son identité historique de bout en bout ; - un incident à clé libre s'identifie par sa clé.
Un profil peut donc déclarer plusieurs incidents partageant un comportement de base — « Défaut pompe » et « Défaut ventilation » sont deux dérangements — chacun sous sa clé : ils s'empilent dans les drapeaux (ActiveFlags, plan, flux, tooltips), s'acquittent séparément (FlagAckStatuses indexé par identité) et se journalisent sous leur nom déclaré. Le validateur refuse deux incidents de même identité, plus deux incidents de même comportement.
Côté frontend, le store des profils résout libellé/icône/son par identité (byFlag), et enregistre les identités déclarées auprès de useIncidentStyling (registerDeclaredStatuses) : famille de couleur — choisie dans l'échelle de sévérité partagée, jamais une couleur libre — icône et criticité. Un drapeau qui ne se résout plus (profil réédité pendant qu'un incident était actif) est traité en dérangement : jamais ignoré.
Portée réelle de trois réglages
Trois champs de l'éditeur n'agissent que sur une partie des profils ou dans une seule circonstance ; les livrer sans le dire fait croire à un réglage sans effet.
| Réglage | Ce qu'il commande | Où il agit |
|---|---|---|
ReadFunction, Encoding, RegisterLength, ActiveLow | Fonction Modbus de lecture, disposition des états, étendue lue, sens de lecture | GenericModbusAdapter seul, donc les profils déclarés (IsBuiltIn = false). PanelAdapterFactory aiguille sur ce seul critère : un profil livré — même modifié sur site, même rattaché à une centrale d'un autre type — garde l'adaptateur de son firmware, qui lit des bobines en dur |
IncidentDefinition.Acknowledgeable | Sort du drapeau à sa disparition : verrouillé en HardwareCleared jusqu'à effacement, ou retiré sans trace | Zone/PointManagementService.ProcessDisappearedFlagsAsync, via IncidentResolver.IsAcknowledgeable ; côté poste, incidentProfileStore.isAcknowledgeable masque l'action |
CommandDefinition.ConfirmRequired | Fenêtre de confirmation avant l'envoi | useSidebarCommands.confirmCommand. Une commande de portée Zone ou Point passe toujours par la fenêtre : sa cible s'y désigne |
Un drapeau qu'aucun profil ne déclare est réputé acquittable, serveur et poste alignés — mieux vaut une trace de trop qu'un incident qui s'évapore.
Sens de lecture
IncidentDefinition.ActiveLow retourne la lecture brute d'une table, dans ReadTableAsync, avant que la position de tête et les entités n'en soient extraites : un bit comme un registre par entité s'y plient, et la tête subit l'inversion comme le reste. C'est la convention des contacts à sécurité positive, où c'est l'ouverture de la boucle qui signale l'état.
Unpack ne rend que les positions réellement reçues, et non la fenêtre entière : compléter par des positions au repos les ferait passer pour lues, et une table à logique inverse les retournerait toutes en incidents fantômes. Une position non lue reste donc au repos, quel que soit le sens de lecture.
Éprouver une centrale déclarée sur une installation réelle
Deux outils lisent la centrale à partir du profil, et non de valeurs codées :
- Scan brut —
GET /api/modbus/scan-planssert, par profil, une table par incident déclaré : adresse, décalage de tête, étendue, fonction de lecture. Le scan couvre donc tout ce que la centrale expose, et chaque bit lu annonce l'entité qu'il décrit (entité système,zone n,point n) — c'est ce rattachement qui rend un décalage faux visible sur place. - Campagne de validation JBUS —
GET /api/panels/{id}/jbus-validationdérive du profil les plages à lire sur une centrale INIS-E16 connectée et confronte le modèle « zone IHM / DI IHM » aux bits réellement remontés. Strictement passive : aucune écriture n'en part.
Où est vérifié un profil déclaré
Trois couches, chacune couvrant ce que les autres ne peuvent pas atteindre :
| Couche | Fichier | Ce qu'elle fixe |
|---|---|---|
| Trame | dotnet/tests/backend/Infrastructure/Adapters/GenericModbusAdapterParameterMatrixTests.cs | Valeur par valeur : adresse interrogée, fonction Modbus employée, nombre d'unités demandé, zone décrite par chaque bit — fonctions de lecture, dispositions, étendue explicite, décalages de table, décalage d'adressage, adresse laissée vide, comportement partagé |
| Cycle de vie | dotnet/tests/backend/Integration/CustomProfileLifecycleTests.cs | Apparition, étiquette du journal, sévérité déclarée, acquittement d'un drapeau sans solder l'autre, cascade zone → points, verrouillage à la disparition, effacement, incident non acquittable, réapparition, statut principal par priorité, drapeau retiré du profil |
| Exploitation | vue/e2e/specs/custom-profile.spec.ts | Centrale simulée rattachée à un profil déclaré : étiquette du flux en direct, deux dérangements côte à côte, acquittement séparé, « à effacer » puis disparition, incident qui se solde seul, couleur de la famille déclarée sur le plan, trace à l'historique |
La campagne d'exploitation rattache la centrale INIS simulée au profil e2e-atelier puis la rend à inis-e16 : aucun de ses incidents ne porte de clé livrée, si bien qu'un libellé livré à l'écran signale une résolution retombée sur le code. Elle se lance avec le reste de la suite (cd vue && npm run test:e2e) et exige PostgreSQL et le jeu de démonstration.
Ajouter un type de centrale
Pour une nouvelle centrale compatible Modbus :
- Type : ajouter une valeur à
PanelType(dotnet/backend/Domain/Enums/PanelType.cs). - Adresses : ajouter une cartographie
PanelRegisterMapdansStandardPanelProfiles(adresses de registres,AddressOffset,ZoneCount, codes de commande). - Profil : ajouter une fabrique
ModbusProfile.CreateXxx()dérivant de la cartographie viaFromRegisterMap(StandardPanelProfiles.Xxx, "NOM", PanelType.Xxx). - Enregistrement : ajouter la fabrique au tableau de
PanelProfileRegistry— le profil est alors validé et semé au démarrage. - Comportement (si le mapping registres → état ou le timing diffèrent) : déclarer une
PanelTypePolicydansPanelAdapterFactoryet un adaptateur héritant deModbusFirePanelAdapterBase. Un type sans policy utilise l'adaptateur générique config-driven. - Front :
PanelTypeétant exposé à l'interface, régénérer les types (voir plus bas) et committerenums.ts. - Simulateur (facultatif, pour émuler la centrale) : dans
dotnet/simulator.core/, ajouter une valeur àSimulatorPanelType(SimulatorTypes.cs) et la brancher dansProfileLoader.GetProfile(ProfileConfig.cs) sur la cartographie partagée. L'interface graphique et l'hôte sans interface en héritent sans modification.
Ajouter / gérer un objet métier
Chaque objet a un seul endroit où l'ajouter. La validation et le typage garantissent le reste.
| Ajouter… | Où |
|---|---|
| Un incident, une commande ou un son | la valeur d'enum (IncidentKey / CommandKey / SoundKey) + l'entrée dans le BuildStandard* correspondant (ModbusProfile.cs) + une migration de reseed : le profil entier est semé par HasData, incidents et commandes compris |
| Un état système | la clé dans SystemFlagKeys (dotnet/shared/Profiles/), le bit dans la carte GlobalBits de chaque centrale qui le sert, la description dans ModbusProfile.BuildStandardSystemFlags, puis une migration de reseed — procédure détaillée dans États système et états d'exploitation |
| Un type de centrale | voir la procédure ci-dessus |
| Un protocole / transport | une implémentation d'IModbusClient |
| Un rôle | UserRole |
| Exposer un enum au frontend | la liste exported de dotnet/tools/TsEnumGenerator |
Fichiers : enums dans dotnet/backend/Domain/Enums/ ; ModbusProfile et ses définitions (IncidentDefinition, CommandDefinition, SystemFlagDefinition) dans dotnet/backend/Domain/ValueObjects/ ; validateur et registre des profils standard dans dotnet/backend/Domain/Profiles/ ; cartographies d'adresses dans dotnet/shared/Profiles/. L'écran d'édition des profils vit dans vue/src/features/settings/views/admin/panel-profiles/.
À fournir en plus (ressources référencées)
LabelKey→ traduction dansvue/src/i18n/locales/fr.jsoneten.json. Pour un état système, le nœud pointé portelabeletdetails.ShortLabelKey(états système) → libellé court des indicateurs de santé, où la place manque.Icon→ nom reconnu par le frontend (sinon icône par défaut).AudioKey→ une valeur deSoundKeyet un son réellement configuré.FunctionCodeinédit → un cas dans le simulateur (HandleCommand) pour le tester ; la vraie centrale réagit d'elle-même. Les codes déjà présents dans la cartographie sont reconnus automatiquement.
Types partagés front ↔ back
Les enums exposés à l'interface (ZoneStatus, PanelType, IncidentScope, CommandScope…) sont générés vers vue/src/config/generated/enums.ts, dont constants.ts fait dériver ZONE_STATUSES et PANEL_TYPES : leurs valeurs restent alignées sur le backend. Après modification d'un enum, régénérer :
powershell
./scripts/generate-ts-enums.ps1La CI régénère ce fichier et échoue si la version versionnée n'est pas à jour : committer enums.ts après régénération.
Exemple — ajouter une commande « Évacuation générale »
CommandKey: ajouterEvacuateAll.ModbusProfile.BuildStandardCommands: ajouter l'entrée (FunctionCode,Scope,LabelKey,Icon,Variant…).- Migration de reseed :
dotnet ef migrations add AddEvacuateAllCommand --project dotnet/backend/backend.csproj. Les profils standard étant semés parHasData, le modèle EF diverge du snapshot tant qu'elle manque. - i18n :
commands.evacuateAlldansfr.jsoneten.json. - Simulateur (si testé ainsi) : gérer le nouveau
FunctionCodedansHandleCommand. - Redémarrer le backend — le profil est validé, la commande apparaît.