//======================================================================================================================================================================== // CONFIG_RESEAU_K_BUS.QML // Ce fichier permet de gérer l'affectation des modules présents sur K-Bus par rapport à la configuration actuelle des circuits et des batteries. // La page est appelée depuis la page PARAMETRES_AVANCES //======================================================================================================================================================================== import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQml.Models 2.1 import qt.koriolan.shipheart 1.0 import"fonctions.js" as Fonctions Shipheart_page { id: page_config_reseau_k_bus // Mis en forme : readonly property double affichage_hauteur_utile: shipheart_hauteur_page - shipheart_hauteur_bandeau_haut - shipheart_hauteur_bandeau_bas readonly property int hauteur_bouton_droit: 80 readonly property int hauteur_ligne_formulaire: 60 readonly property double pourcentage_largeur_colonne_gauche: 0.3 readonly property double pourcentage_largeur_colonne_centre: 0.5 readonly property int espace_colonnes_principales: 20 readonly property double pourcentage_bandeau_bas_largeur_zone_retour: 0.5 readonly property double pourcentage_largeur_labels_formulaire: 0.5 readonly property int nombre_modules_affiches_sur_une_colonne: 6//8 readonly property int periode_init_raffraichissement_parametres: 1 // En ms readonly property int periode_raffraichissement_parametres: 1000 // En ms readonly property int action_carte_aucune: 0 readonly property int action_carte_cde: 1 readonly property int action_carte_etalonnage: 2 // Affichage : property int index_en_cours: -1 property int nb_modules_physiques: 0 property bool flag_init: true // Permet de bloquer certains affichage tant que l'init n'est pas terminée property bool flag_maj_en_cours: false // Modèles : // Note : les modèles chargés dans 'main.qml' n'ont pas à être rechargés dans les pages suivantes //=========================================================== // Timer periode raffraichissement parametres : //=========================================================== Timer { id: timer_raffraichissement_parametres interval: periode_init_raffraichissement_parametres triggeredOnStart: false running: true repeat: true onTriggered: { if (interval == periode_init_raffraichissement_parametres) // Premier passage, 1ms après le premier chargement { interval = periode_raffraichissement_parametres; init_page(); } else { mise_a_jour_modules_physiques(); mise_a_jour_modules_config(); } } } //=========================================================== // Lorsque la page est sur le point d'être affichée dans la pile : //=========================================================== StackView.onActivating: { // Le 'onActivating' permet de ne pas voir la mise à jour puisqu'elle est lancée dès que la page va être chargée // StackView.onActivated: { // Le 'onActivated' laisse voir la mise à jour (puisqu'elle est lancée quand la page est complètement affichée init_modules_configures(); // Utile au démarrage ET au retour d'autres pages si on a modifié la config des groupes de circuits entre temps... } // Titre de la page : Shipheart_titre_1 {titre:qsTr("Réseau ShipHeart"); hauteur: shipheart_hauteur_bandeau_haut} //------------------------------------------------------- // Ecran de gauche (liste des modules physiquement présents) : //------------------------------------------------------- Rectangle { id: zone_affichage_gauche x: 0 y: shipheart_hauteur_bandeau_haut; width: (shipheart_largeur_page * pourcentage_largeur_colonne_gauche) - (espace_colonnes_principales / 2) height: affichage_hauteur_utile color: "transparent" //------------------------------------------------------------------- // Titre de la liste : Rectangle { width: parent.width height: hauteur_ligne_formulaire color: "transparent" Text { anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Qt.AlignVCenter text: qsTr("Modules physiquement présents") color: couleur_gris_moyen font.pixelSize: 18 wrapMode: Label.Wrap } } //------------------------------------------------------------------- // Liste des modules physiquement présents : ListView { id: grille_modules_physiques y: hauteur_ligne_formulaire // Permet d'échapper la ligne de titre width: parent.width height: parent.height - hauteur_ligne_formulaire // interactive: false // Rend la liste NON scrollable à la main. C'est la liste de droite qui met à jour la position de celle de gauche. highlightRangeMode: ListView.StrictlyEnforceRange // Nécessaire pour récupérer 'currentIndex' et le répercuter sur la ListView des modules configurés snapMode: GridView.SnapToRow // L'arret de l'affichage se fera sur une rangée entière clip: true // Nécessaire pour que les Delegates s'arretent à l'intérieur de la GridView model: modele_modules_physiques delegate: Rectangle { width: grille_modules_physiques.width height: grille_modules_physiques.height / nombre_modules_affiches_sur_une_colonne color: "transparent" visible: !flag_init & !flag_maj_en_cours Rectangle { // c'est le bouton proprement dit... anchors.fill: parent; anchors.verticalCenter : parent.verticalCenter anchors.margins: 5 radius: shipheart_rayon_bouton color: "transparent" border.width: 2 border.color: shipheart_couleur_fond_bouton //----------------------- // Adresse : Rectangle { id: zone_adresse_module_physique x: 0 y: 0 width: 50 height: parent.height color: "transparent" radius: shipheart_rayon_bouton border.width: 2 border.color: shipheart_couleur_fond_bouton Text { anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: model.adresse color: couleur_blanc_casse font.pixelSize: 28 font.bold: true } } //----------------------- // Nom : Rectangle { id: zone_nom_module_physique x: zone_adresse_module_physique.width y: 0 width: parent.width - zone_adresse_module_physique.width height: parent.height / 3 color: "transparent" Text { // Type et nom du module anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left leftPadding: 10 rightPadding: 20 text: model.nom color: couleur_blanc_casse font.pixelSize: 18 font.bold: true } } } MouseArea { anchors.fill: parent onClicked: { shipheart_clic(); stackView.push("config_module_physique.qml", {"adresse_module": model.adresse}) } } } ListModel { id: modele_modules_physiques } onCurrentIndexChanged: { grille_modules_configures.positionViewAtIndex(currentIndex, ListView.Beginning); // Permet de faire défiler la liste des modules configurés "en face" des modules physiques. if ( (currentIndex >= grille_modules_configures.count) && // Si le module affiché est à une adresse > à l'adresse la plus élevée des modules configurés, on n'afficha plue la liste configurée (sinon, on reste bloqué sur le dernier module configuré qui est alors décalé par rapport au module physique affiché en face) (grille_modules_configures.count !== 0)) // Utile à l'ouverture de la page car on passe par l'état ou COUNT=0 et la liste configurée n'est plus visible avant le premier mouvement { grille_modules_configures.visible = false; } else grille_modules_configures.visible = true; // Si on revient à la situation où le module physique affiché a une adresse < nombre de modules configurés (donc plus de risque de décalage) } } } //------------------------------------------------------- // Barre de séparation verticale : //------------------------------------------------------- Rectangle { id: separation_verticale x: zone_affichage_gauche.width + (espace_colonnes_principales / 2) - (width / 2) y: shipheart_hauteur_bandeau_haut; width: 3 height: affichage_hauteur_utile color: couleur_gris_tres_pale_transparent } //------------------------------------------------------- // Ecran central (modules utilisés par la configuration actuelle) : //------------------------------------------------------- Rectangle { id: zone_affichage_centre x: zone_affichage_gauche.width + (espace_colonnes_principales) y: shipheart_hauteur_bandeau_haut; width: (shipheart_largeur_page * pourcentage_largeur_colonne_centre) - (espace_colonnes_principales) height: affichage_hauteur_utile color: "transparent" //------------------------------------------------------------------- // Titre de la liste : Rectangle { width: parent.width height: hauteur_ligne_formulaire color: "transparent" Text { anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Qt.AlignVCenter text: qsTr("Modules utilisés par la configuration actuelle") color: couleur_gris_pale font.pixelSize: 18 wrapMode: Label.Wrap } } //------------------------------------------------------------------- // Liste : ListView { property bool deplacement_en_cours: false // Indique si un déplacement (au global) est en cours ou pas property int save_current_index: 0 // Permet de garder le Current_index du début du déplacement et de compenser les changements qui sont fait automatiquement, notamment en début et fin de liste. Le principe est qu'une fois le mouvement déclenché, ON NE CHANGE PAS de CurrentIndex !! id: grille_modules_configures x: 0 y: hauteur_ligne_formulaire // Permet d'échapper la ligne de titre width: parent.width height: parent.height - hauteur_ligne_formulaire clip: true // Limite l'affichage des délégués dans la zone de ListView (sinon, on affiche au moins un délégué au-dessus et un en dessous...) highlightRangeMode: ListView.StrictlyEnforceRange // Permet de faire défiler la liste jusqu'à ne garder que le dernier élément affiché (et comme la liste des modules physiques est obligée d'utiliser ce paramètre, on fait pareil sur cette liste pour que les deux soient synchro) model: modele_modules_configures delegate: bouton_module displaced: Transition { // Gère la transition lors d'un déplacement NumberAnimation{ properties: "x,y" // Ici, on devrait pouvoir limiter à Y duration: 500 // Vitesse à laquelle se déplacent les boutons lorsqu'on les déplace } } Component { id: bouton_module MouseArea { id: dragArea // Zone correspondant à un seul bouton (on est dans le Delegate) width: grille_modules_configures.width height: grille_modules_configures.height / nombre_modules_affiches_sur_une_colonne property bool bouton_appuye: false // On ne peut pas utiliser la propriété globale "deplacement_en_cours" car il faut une propriété par délégué pressAndHoldInterval: 500; // Temporise avant de passer en mode "déplacement" drag.target: bouton_appuye ? contenu_bouton_module : undefined drag.axis: Drag.YAxis // On ne se déplace que sur l'axe Y onClicked: { shipheart_clic(); stackView.push("config_module.qml", {"adresse_module": adresse}) } onPressAndHold: { if (adresse !== 0) // ShipBase n'est pas "bougeable" { bouton_appuye = true // Indication (au niveau du bouton seul) du début de l'appui long donc du déplacement grille_modules_configures.deplacement_en_cours = true // Indication (au niveau de ListView) du début du déplacement grille_modules_configures.save_current_index = grille_modules_configures.currentIndex; // Sauvegarde du CurrentIndex au début du déplacement } } onReleased: { bouton_appuye = false // Indication (au niveau du bouton seul) de la fin de l'appui long donc du déplacement grille_modules_configures.deplacement_en_cours = false // Indication (au niveau de ListView) de la fin du déplacement grille_modules_configures.currentIndex = grille_modules_configures.save_current_index; // Restitution de la valeur initiale du CurrentIndex (a priori, pas nécessaire car mis à jour au fur et à mesure des changements dans "onCurrentIndexChanged") enregistrement(); // Le principe est denregistrer après chaque déplacement ; sinon, les appels aux menus inférieurs (détails de modume physique ou config) deviennent très compliqués... } Rectangle { id: contenu_bouton_module anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } width: dragArea.width - 10 height: dragArea.height - 10 radius: shipheart_rayon_bouton color: (adresse ===0) ? "transparent" // Incolore pour ShipBase (on ne peut pas le bouger) : ((model.erreur === 0) ? shipheart_couleur_fond_bouton // Couleur par défaut si pas d'erreur : (model.affecte === 0 ? couleur_orange_transparent // Orange si aucune affectation sur ce module : couleur_rouge_transparent)) // Rouge s'il y a une affectation sur ce module border.width: 2 border.color: shipheart_couleur_fond_bouton opacity: dragArea.bouton_appuye ? 0.5 : 0.8 //----------------------- // Adresse : Rectangle { id: zone_adresse_module x: 0 y: 0 width: 50 height: parent.height color: "transparent" radius: shipheart_rayon_bouton border.width: 2 border.color: shipheart_couleur_fond_bouton Text { anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: model.adresse color: (affecte === 1) ? couleur_blanc_casse : couleur_gris_fonce font.pixelSize: 28 font.bold: true } } //----------------------- // Nom : Rectangle { id: zone_nom_module x: zone_adresse_module.width y: 0 width: parent.width - zone_adresse_module.width height: parent.height / 3 color: "transparent" Text { // Type et nom du module anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left leftPadding: 10 rightPadding: 20 text: Fonctions.nom_type_module(model.type) + (model.nom === "" ? "" : " [" + model.nom + "]") color: (affecte === 1) ? couleur_blanc_casse : couleur_gris_fonce font.pixelSize: 18 font.bold: true } } //----------------------- // Usages principaux : Rectangle { id: zone_usage_principal x: zone_adresse_module.width y: (parent.height / 3) width: parent.width - zone_adresse_module.width height: parent.height / 3 color: "transparent" visible: (usage_principal !== "") Rectangle { id: icone_usage_principal x: 10 width: 20 height: parent.height color: "transparent" Image { source: (type === shipheart_type_module_shipBS) ? "/images/batterie_moyen.png" : "/images/icone_consommation_electrique.png" anchors.fill: parent fillMode: Image.PreserveAspectFit opacity: 0.7 } } Text { // Utilisation du module en config (liste des circuits, nom de la batterie, ...) anchors.verticalCenter: parent.verticalCenter anchors.left: icone_usage_principal.right leftPadding: 10 rightPadding: 20 width: zone_usage_principal.width - icone_usage_principal.width // Nécessaire pour que 'elide' fonctionne wrapMode: Text.Wrap // Utile si 'maximumLineCount' > 1 maximumLineCount: 1 // Nb de lignes sur lequel le texte sera affiché avant l'elide elide: Text.ElideRight // Affiche des [...] lorsque le texte est trop long text: usage_principal color: (affecte === 1) ? couleur_gris_tres_pale : couleur_gris_fonce font.pixelSize: 15 } } //----------------------- // Usages secondaires : Rectangle { id: zone_usage_secondaire x: zone_adresse_module.width y: (2 * parent.height / 3) width: parent.width - zone_adresse_module.width height: parent.height / 3 color: "transparent" visible: (usage_secondaire !== "") Rectangle { id: icone_usage_secondaire x: 10 width: 20 height: parent.height color: "transparent" Image { source: "/images/icone_auxiliaires.png" anchors.fill: parent fillMode: Image.PreserveAspectFit opacity: 0.7 } } Text { // Utilisation du module en config (liste des circuits, nom de la batterie, ...) anchors.verticalCenter: parent.verticalCenter anchors.left: icone_usage_secondaire.right leftPadding: 10 rightPadding: 20 width: zone_usage_secondaire.width - icone_usage_secondaire.width // Nécessaire pour que 'elide' fonctionne wrapMode: Text.Wrap // Utile si 'maximumLineCount' > 1 maximumLineCount: 1 // Nb de lignes sur lequel le texte sera affiché avant l'elide elide: Text.ElideRight // Affiche des [...] lorsque le texte est trop long text: usage_secondaire color: (affecte === 1) ? couleur_gris_tres_pale : couleur_gris_fonce font.pixelSize: 15 } } Drag.active: dragArea.bouton_appuye Drag.source: dragArea Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 states: State{ when: dragArea.bouton_appuye ParentChange { target: contenu_bouton_module parent: grille_modules_configures } AnchorChanges { target: contenu_bouton_module anchors { // Necessaire pour autoriser le mouvement horizontalCenter: undefined verticalCenter: undefined } } } } DropArea { enabled: (adresse !== 0) // Interdit de déposer un module sur l'adresse 0 (ShipBase) anchors.fill: parent onEntered: { if (drag.source.DelegateModel.itemsIndex !== dragArea.DelegateModel.itemsIndex) { //console.log("\tMouvement de Source index = " + drag.source.DelegateModel.itemsIndex + " vers Dest index = " + dragArea.DelegateModel.itemsIndex) modele_modules_configures.move(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex, 1) mise_a_jour_modules_config(); // Mise à jour des flags d'erreur (donc les modules teintés de rouge) suite au dernier déplacement. } } } } } ListModel { id: modele_modules_configures } onCurrentIndexChanged: { if (grille_modules_configures.deplacement_en_cours === false) { grille_modules_physiques.positionViewAtIndex(currentIndex, ListView.Beginning); // Permet de faire défiler la liste des modules physiques "en face" des modules configurés. Mais uniquement en dehors de déplacements puisque, pendant les déplacements, on force CurrentIndex à rester fixe. if ( (currentIndex >= grille_modules_physiques.count) && // Si le module affiché est à une adresse > à l'adresse la plus élevée des modules physiques, on n'afficha plue la liste physique (sinon, on reste bloqué sur le dernier module physique qui est alors décalé par rapport au module config affiché en face) (grille_modules_physiques.count !== 0)) // Utile à l'ouverture de la page car on passe par l'état ou COUNT=0 et la liste physique n'est plus visible avant le premier mouvement { grille_modules_physiques.visible = false; } else grille_modules_physiques.visible = true; // SI on revient à la situation où le module config affiché a une adresse < nombre de modules physiques (donc plus de risque de décalage) } else { grille_modules_configures.currentIndex = grille_modules_configures.save_current_index; // Permet de compenser les changements de CurrentIndex 'imposés' par le ListView. Notamment lors des déplacements en début et fin de liste. } } } //------------------------------------------------------------------- // Message si liste vide : Label { anchors.fill: parent horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter visible: modele_modules_configures.count === 0 text: qsTr("La configuration actuelle ne fait appel à aucun autre module que ShipBase...") color: couleur_rouge_transparent font.pixelSize: 15 font.bold: true wrapMode: Label.Wrap } } //------------------------------------------------------- // Ecran Droite : //------------------------------------------------------- Rectangle { id: zone_affichage_droite x: zone_affichage_gauche.width + espace_colonnes_principales + zone_affichage_centre.width + espace_colonnes_principales; y: shipheart_hauteur_bandeau_haut + hauteur_ligne_formulaire + 5; width: (shipheart_largeur_page * (1 - pourcentage_largeur_colonne_gauche - pourcentage_largeur_colonne_centre)) - (espace_colonnes_principales / 2); height: affichage_hauteur_utile color: "transparent" Column { anchors.fill: parent spacing: 10 //---------------------- // Bouton + : Rectangle { width: parent.width height: hauteur_bouton_droit color: shipheart_couleur_fond_bouton radius: shipheart_rayon_bouton visible: !clavier.active Text { anchors { verticalCenter: parent.verticalCenter horizontalCenter: parent.horizontalCenter } font.pixelSize: 40 text: qsTr("+") color: couleur_blanc_casse } MouseArea { anchors.fill: parent onClicked: { shipheart_clic(); stackView.push("config_module_creation.qml", {"adresse_module": modele_modules_configures.count}); } } } //----------------------------------- // Bouton de rechargement de config : Rectangle { width: parent.width height: hauteur_bouton_droit color: shipheart_couleur_fond_bouton radius: shipheart_rayon_bouton visible: !clavier.active Image { source: "/images/cycle.png" sourceSize.height: (parent.height * 0.5) anchors { verticalCenter: parent.verticalCenter horizontalCenter: parent.horizontalCenter } } MouseArea { anchors.fill: parent onClicked: { shipheart_clic(); synchro_bdd(shipheart_type_synchro_config); timer_tempo_reconfiguration.restart(); // Lancement du timer d'attente de fin de reconfig } } } } } //------------------------------------------------------- // Bandeau bas : //------------------------------------------------------- Row { x: 0 y: shipheart_hauteur_page - shipheart_hauteur_bandeau_bas; width: shipheart_largeur_page; height: shipheart_hauteur_bandeau_bas; spacing: 5 padding: 5 //................................................. // Bouton retour //................................................. Rectangle { id: bandeau_bas_zone_retour width: parent.width - (2 * parent.padding) height: shipheart_hauteur_bandeau_bas - parent.topPadding - parent.bottomPadding; radius: shipheart_rayon_bouton color: shipheart_couleur_fond_bouton Image { source: "/images/icone_retour.png" sourceSize.height: 30 anchors { verticalCenter: parent.verticalCenter horizontalCenter: parent.horizontalCenter } } MouseArea { // Zone d'interaction. On est obligé de la mettre dans la grille pour que Rectangle et MouseArea soient frères ou parent/enfant ET qu'on n'occupe qu'un espace dans la grille. id: mouse_area_bandeau_bas_zone_retour anchors.fill: parent onClicked: { shipheart_clic(); stackView.pop(); } } } //................................................. // Timer de reconfiguration //................................................. Timer { id: timer_tempo_reconfiguration interval: 250 // Cette tempo n'est utilisée que pour le premier cycle triggeredOnStart: false running: false repeat: true onTriggered: { if (bdd.check_fin_configuration()) { timer_tempo_reconfiguration.stop(); maj_flag_defaut_config_k_bus(); init_modules_configures(); } else timer_tempo_reconfiguration.interval = 100 // Tempo plus courte pour les lectures suivantes") } } } //=========================================================== // Busy Indicator : //=========================================================== Shipheart_busy_indicator { id: busy_indicator running: timer_tempo_reconfiguration.running } // Note : il est inutile de prévoir un Mousearea pour sortir de l'économiseur sur cette page puisqu'il // est systématiquement bloqué pour toutes les pages lancées depuis 'Paramètres Avancés'. //============================================================================================================= // Init de la page //============================================================================================================= function init_page() { modele_modules_physiques.clear(); // Doit être ici car la maj est appelée régulièrement mise_a_jour_modules_physiques(); // L'init des modules de config est lancée directement à la fin de la mise à jour des modules physiques flag_init = false; } //============================================================================================================= // Mise à jour périodique (et à l'init) des informations des modules physiquement présents //============================================================================================================= function mise_a_jour_modules_physiques() { var req, res; var i; var nb; //-------------------------------------------------------------------- // Vérification de la dispo du CM3+/CM4 : //-------------------------------------------------------------------- // Vérification que le CM3+/CM4 est bien On (sinon, il est fort probable qu'il soit en cours de démarrage ou de raffraichissement de son annuaire et // on va récupérer des données partielles... req = 'SELECT Valeur FROM TB_Systeme WHERE Nom="Etat_cm3p" '; res = bdd.requete_unique(req); if (Number(res[0]) === 0) { //console.log("CM3+/CM4 non disponible. La mise à jour est avortée...") return; } flag_maj_en_cours = true; //-------------------------------------------------------------------- // Mise à jour de la liste des modules physiques : //-------------------------------------------------------------------- req = 'SELECT Adresse_k_bus, Type FROM TB_Modules_physiques ORDER BY Adresse_k_bus'; //console.log("req =" + req); nb = bdd.requete(req); for (i = 0; i < nb; i ++) { res = bdd.lecture_ligne(); if (!res.isEmpty) { if (Number(res[0]) > modele_modules_physiques.count) { modele_modules_physiques.append({"adresse": Number(res[0]), "nom": Fonctions.nom_type_module(Number(res[1])), "type": Number(res[1])}); } else { modele_modules_physiques.set(Number(res[0]), {"adresse": Number(res[0]), "nom": Fonctions.nom_type_module(Number(res[1])), "type": Number(res[1])}); } } else break; // Ne devrait pas arriver.... } //-------------------------------------------------------------------- // Suppression du modèle des éventuels modules physiques supprimés : //-------------------------------------------------------------------- while(modele_modules_physiques.count > nb) { modele_modules_physiques.remove((modele_modules_physiques.count - 1), 1); } //-------------------------------------------------------------------- // Détection d'un changement de nombre de modules physiques : //-------------------------------------------------------------------- if (nb_modules_physiques !== modele_modules_physiques.count) { nb_modules_physiques = modele_modules_physiques.count; } flag_maj_en_cours = false; } //============================================================================================================= // Mise à jour périodique des informations sur l'utilisation des modules par la config actuelle //============================================================================================================= function mise_a_jour_modules_config() { var req, res; var i; var nb; var erreur; //-------------------------------------------------------------------- // Vérification de la dispo du CM3+/CM4 : //-------------------------------------------------------------------- // Vérification que le CM3+/CM4 est bien On (sinon, il est fort probable qu'il soit en cours de démarrage ou de raffraichissement de son annuaire et // on va récupérer des données partielles... req = 'SELECT Valeur FROM TB_Systeme WHERE Nom="Etat_cm3p" '; res = bdd.requete_unique(req); if (Number(res[0]) === 0) { //console.log("CM3+/CM4 non disponible. La mise à jour est avortée...") return; } flag_maj_en_cours = true; //-------------------------------------------------------------------- // Recherche des modules en erreur (différence entre le type réellement présent et le type en config) : //-------------------------------------------------------------------- // NB : on ne peut pas utiliser le flag Presence dand TB_Modules_config comme à l'init car on a pu bouger les modules de mise_a_jour_modules_config() // sans avoir encore mis à jour la BDD (tant qu'on n'a pas enregistré les modifs). Il faut donc faire cette vérification entre le modèle physique // qui, lui, est mis à jour en temps réel via TB_Modules_physiques et le modèle config qui est mis à jour par les déplacements après l'init. for (i=0; i