From f807c2b320ee3aafc7e5395fdcacaace4484a007 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 14 Aug 2026 13:06:13 +0200 Subject: [PATCH] =?UTF-8?q?Affichage=20des=204=20domaines=20(circuit/batte?= =?UTF-8?q?rie/capteur/reservoir)=20+=20commande=20circuit.=20Mod=C3=A8les?= =?UTF-8?q?=20d'=C3=A9l=C3=A9ments=20s=C3=A9par=C3=A9s=20par=20domaine=20(?= =?UTF-8?q?pi=C3=A8ge=20des=20r=C3=B4les=20QML=20fig=C3=A9s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dataprovider.cpp | 27 +++++- dataprovider.h | 18 ++-- qml/home.qml | 218 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 191 insertions(+), 72 deletions(-) diff --git a/dataprovider.cpp b/dataprovider.cpp index b90eb74..e7c77de 100644 --- a/dataprovider.cpp +++ b/dataprovider.cpp @@ -18,7 +18,7 @@ void DataProvider::get_familles(const QString &type) url.setQuery(query); auto reply = m_manager.get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, type]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit erreur(reply->errorString()); @@ -30,7 +30,7 @@ void DataProvider::get_familles(const QString &type) emit erreur(obj["message"].toString()); return; } - emit famillesRecues(obj["familles"].toArray()); + emit famillesRecues(type, obj["familles"].toArray()); }); } @@ -43,7 +43,7 @@ void DataProvider::get_elements(const QString &type, int famille) url.setQuery(query); auto reply = m_manager.get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + connect(reply, &QNetworkReply::finished, this, [this, reply, type, famille]() { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { emit erreur(reply->errorString()); @@ -55,6 +55,25 @@ void DataProvider::get_elements(const QString &type, int famille) emit erreur(obj["message"].toString()); return; } - emit elementsRecus(obj["elements"].toArray()); + emit elementsRecus(type, famille, obj["elements"].toArray()); + }); +} + +void DataProvider::set_circuit(int id, int consigne) +{ + QUrl url(m_baseUrl + "/elements/circuit/" + QString::number(id)); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QJsonObject body; + body["consigne"] = consigne; + const QByteArray data = QJsonDocument(body).toJson(); + + auto reply = m_manager.sendCustomRequest(request, "PATCH", data); + connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { + reply->deleteLater(); + const bool succes = (reply->error() == QNetworkReply::NoError); + if (!succes) emit erreur(reply->errorString()); + emit circuitModifie(id, succes); }); } diff --git a/dataprovider.h b/dataprovider.h index b107886..8c14f2e 100644 --- a/dataprovider.h +++ b/dataprovider.h @@ -11,8 +11,8 @@ // HTTP/JSON est encapsulée ici - le QML ne voit que des signaux avec des // tableaux déjà prêts à l'emploi. // -// Portée actuelle : uniquement le domaine "circuit" (première tranche -// verticale). À généraliser aux 3 autres domaines une fois ce pattern validé. +// Portée actuelle : les 4 domaines (circuit, batterie, capteur, reservoir) +// en lecture, écriture (consigne/mode auto) sur circuit uniquement. // ============================================================================ class DataProvider : public QObject { @@ -23,17 +23,23 @@ public: // TODO : rendre l'adresse de base configurable (IP locale du CM5 en cas 2, // domaine VPS en cas 3 - cf QR code d'accès, §"Architecture cible"/Cas 2-3 - // de la synthèse) - en dur sur l'IP locale pour cette première tranche. + // de la synthèse) - en dur sur le relais VPS pour cette étape. Q_INVOKABLE void get_familles(const QString &type); Q_INVOKABLE void get_elements(const QString &type, int famille); + // Écriture rapide sur un circuit (bouton d'accueil) - PATCH /elements/circuit/. + Q_INVOKABLE void set_circuit(int id, int consigne); + signals: - void famillesRecues(const QJsonArray &familles); - void elementsRecus(const QJsonArray &elements); + // Le type (et, pour les éléments, la famille) sont désormais inclus dans + // chaque signal - indispensable avec plusieurs domaines actifs en + // parallèle, pour que le QML sache à quelle réponse il a affaire. + void famillesRecues(const QString &type, const QJsonArray &familles); + void elementsRecus(const QString &type, int famille, const QJsonArray &elements); + void circuitModifie(int id, bool succes); void erreur(const QString &message); private: QNetworkAccessManager m_manager; -// QString m_baseUrl = QStringLiteral("http://192.168.1.220:8080"); QString m_baseUrl = QStringLiteral("https://api.ship2net.fr"); }; diff --git a/qml/home.qml b/qml/home.qml index 32e9b7c..15c87d3 100644 --- a/qml/home.qml +++ b/qml/home.qml @@ -5,64 +5,94 @@ import QtQuick.Layouts 1.12 import qt.koriolan.shipheart 1.0 // ============================================================================ -// home.qml (EMBRYON - POC WASM production, domaine circuit uniquement) -// ---------------------------------------------------------------------------- -// Version très réduite du vrai home.qml (~3880 lignes, tous domaines) : -// couvre uniquement l'affichage des circuits, via l'API réelle (DataProvider) -// au lieu de bdd.*. Objectif : valider la chaîne complète (HTTP réel, JSON, -// affichage QML) sur un seul domaine avant de généraliser. +// home.qml (client shipheart_wasm) - 4 domaines (circuit/batterie/capteur/ +// reservoir), via l'API réelle (DataProvider). Écriture (consigne) sur +// circuit uniquement pour l'instant. // -// Simplifications volontaires par rapport au vrai home.qml : -// - Lecture seule (pas de changement de consigne au clic - l'endpoint -// d'écriture n'est pas encore construit côté API, cf synthèse §20.7) -// - Un seul domaine affiché (pas de batteries/réservoirs/capteurs/pages -// personnalisées/messages/réveils/minuteur) -// - Délégué d'affichage simplifié (pas Shipheart_bouton_circuit, qui -// embarque des actions d'écriture non encore branchées) -// - mise_a_jour_onglets_circuits() n'existe plus : l'API renvoie déjà -// l'état agrégé par famille dans /elements/familles (cf §20.7/21.5) +// Architecture : les onglets (familles) partagent UN SEUL ListModel (forme +// identique dans les 4 domaines : id/libelle/etat/actif). Les ÉLÉMENTS, en +// revanche, utilisent UN MODÈLE SÉPARÉ PAR DOMAINE - piège QML rencontré en +// pratique : un ListModel fige les champs reconnus ("rôles") dès sa première +// utilisation, même après clear() ; réutiliser un même modèle pour des formes +// JSON différentes (tension/intensite pour circuit, valeur/unite pour +// capteur...) laisse les champs propres à chaque domaine à 'undefined'. // ============================================================================ Shipheart_page { id: page_accueil - readonly property int deport_haut_masquage_onglets: 20 - readonly property int rayon_onglets: 20 - property int nombre_colonnes_affichage_circuits: 2 - property int nombre_circuits_affiches_sur_une_colonne: 4 - property int id_groupe_circuits: 0 + readonly property var domaines: ["circuit", "batterie", "capteur", "reservoir"] + readonly property var domaines_libelles: [qsTr("Circuits"), qsTr("Batteries"), qsTr("Capteurs"), qsTr("Réservoirs")] + property int domaine_actif_index: 0 + readonly property string domaine_actif: domaines[domaine_actif_index] - ListModel { id: modele_onglets_circuits } - ListModel { id: modele_circuits } + readonly property int espace_onglets: 10 + readonly property int rayon_onglets: 20 + + property int id_groupe: 0 + + ListModel { id: modele_onglets } + + // Un ListModel par domaine, pour éviter le piège des "rôles" figés de QML : + ListModel { id: modele_elements_circuit } + ListModel { id: modele_elements_batterie } + ListModel { id: modele_elements_capteur } + ListModel { id: modele_elements_reservoir } + + property var modele_elements_actuel: modele_elements_circuit + + function modele_elements_pour(type) { + switch (type) { + case "circuit": return modele_elements_circuit; + case "batterie": return modele_elements_batterie; + case "capteur": return modele_elements_capteur; + case "reservoir": return modele_elements_reservoir; + } + return null; + } property DataProvider dataProvider: DataProvider {} + function charger_domaine(index) { + domaine_actif_index = index; + id_groupe = 0; + modele_onglets.clear(); + modele_elements_actuel = modele_elements_pour(domaine_actif); + modele_elements_actuel.clear(); + dataProvider.get_familles(domaine_actif); + } + Connections { target: dataProvider - function onFamillesRecues(familles) { - modele_onglets_circuits.clear(); + function onFamillesRecues(type, familles) { + if (type !== domaine_actif) return; // Réponse d'un domaine quitté entretemps - ignorée + modele_onglets.clear(); for (var i = 0; i < familles.length; i++) { var f = familles[i]; - modele_onglets_circuits.append({ + modele_onglets.append({ "id": f.id, "libelle": f.nom, "etat": f.etat, - "actif": (f.id === id_groupe_circuits) + "actif": (f.id === id_groupe) }); - // Sélectionne la première famille par défaut si aucune n'est choisie : - if (id_groupe_circuits === 0 && i === 0) { - id_groupe_circuits = f.id; - dataProvider.get_elements("circuit", id_groupe_circuits); + if (id_groupe === 0 && i === 0) { + id_groupe = f.id; + dataProvider.get_elements(domaine_actif, id_groupe); } } } - function onElementsRecus(elements) { - modele_circuits.clear(); - for (var j = 0; j < elements.length; j++) { - modele_circuits.append(elements[j]); - } + function onElementsRecus(type, famille, elements) { + if (type !== domaine_actif || famille !== id_groupe) return; + var m = modele_elements_pour(type); + m.clear(); + for (var j = 0; j < elements.length; j++) m.append(elements[j]); + } + + function onCircuitModifie(id, succes) { + if (!succes) console.log("Échec de la commande sur le circuit " + id); + if (domaine_actif === "circuit" && id_groupe > 0) dataProvider.get_elements("circuit", id_groupe); } function onErreur(message) { @@ -70,17 +100,17 @@ Shipheart_page { } } - Component.onCompleted: { - dataProvider.get_familles("circuit"); - } + Component.onCompleted: charger_domaine(0) - // Rafraîchissement périodique (remplace mise_a_jour_circuits(), appelée chaque seconde côté natif) : Timer { interval: 1000 running: true repeat: true onTriggered: { - if (id_groupe_circuits > 0) dataProvider.get_elements("circuit", id_groupe_circuits); + if (id_groupe > 0) { + dataProvider.get_elements(domaine_actif, id_groupe); + dataProvider.get_familles(domaine_actif); + } } } @@ -89,27 +119,59 @@ Shipheart_page { spacing: 10 //----------------------------------------------------------- - // Onglets (familles) : + // Sélecteur de domaine : + Row { + Layout.fillWidth: true + height: shipheart_hauteur_bandeau_haut + spacing: espace_onglets + + Repeater { + model: domaines_libelles + delegate: Rectangle { + width: (page_accueil.width - 3 * espace_onglets) / 4 + height: shipheart_hauteur_bandeau_haut + radius: rayon_onglets + color: (index === domaine_actif_index) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: modelData + color: couleur_blanc_casse + font.pixelSize: 13 + font.bold: (index === domaine_actif_index) + } + + MouseArea { + anchors.fill: parent + onClicked: charger_domaine(index) + } + } + } + } + + //----------------------------------------------------------- + // Onglets (familles du domaine actif) : Rectangle { - id: onglets_circuits Layout.fillWidth: true height: shipheart_hauteur_bandeau_haut color: "transparent" - visible: modele_onglets_circuits.count > 1 + visible: modele_onglets.count > 1 GridView { - id: grille_onglets_circuits + id: grille_onglets anchors.fill: parent - cellWidth: width / Math.max(1, Math.min(modele_onglets_circuits.count, 5)) + cellWidth: width / Math.max(1, Math.min(modele_onglets.count, 5)) cellHeight: shipheart_hauteur_bandeau_haut - model: modele_onglets_circuits + model: modele_onglets clip: true delegate: Rectangle { - width: grille_onglets_circuits.cellWidth - 2 - height: grille_onglets_circuits.cellHeight + width: grille_onglets.cellWidth - 2 + height: grille_onglets.cellHeight radius: rayon_onglets color: model.actif ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + border.color: (model.etat === 2) ? "red" : (model.etat === 1) ? "orange" : "transparent" + border.width: 2 Text { text: model.libelle @@ -122,11 +184,11 @@ Shipheart_page { MouseArea { anchors.fill: parent onClicked: { - id_groupe_circuits = model.id; - for (var k = 0; k < modele_onglets_circuits.count; k++) { - modele_onglets_circuits.set(k, {"actif": (modele_onglets_circuits.get(k).id === model.id)}); + id_groupe = model.id; + for (var k = 0; k < modele_onglets.count; k++) { + modele_onglets.set(k, {"actif": (modele_onglets.get(k).id === model.id)}); } - dataProvider.get_elements("circuit", id_groupe_circuits); + dataProvider.get_elements(domaine_actif, id_groupe); } } } @@ -134,19 +196,19 @@ Shipheart_page { } //----------------------------------------------------------- - // Circuits de la famille sélectionnée : + // Éléments de la famille sélectionnée, dans le domaine actif : GridView { - id: circuits_grid + id: elements_grid Layout.fillWidth: true Layout.fillHeight: true - cellWidth: width / nombre_colonnes_affichage_circuits - cellHeight: height / nombre_circuits_affiches_sur_une_colonne - model: modele_circuits + cellWidth: width / 2 + cellHeight: height / 4 + model: modele_elements_actuel clip: true delegate: Rectangle { - width: circuits_grid.cellWidth - 4 - height: circuits_grid.cellHeight - 4 + width: elements_grid.cellWidth - 4 + height: elements_grid.cellHeight - 4 radius: 10 color: shipheart_couleur_fond_bouton border.color: (model.etat === 2) ? "red" : (model.etat === 1) ? "orange" : "transparent" @@ -163,19 +225,51 @@ Shipheart_page { font.bold: true anchors.horizontalCenter: parent.horizontalCenter } + Text { - text: model.tension.toFixed(1) + " V " + model.intensite.toFixed(2) + " A" + text: { + switch (domaine_actif) { + case "circuit": + return model.tension.toFixed(1) + " V " + model.intensite.toFixed(2) + " A"; + case "batterie": + return model.niveau.toFixed(0) + " % " + model.tension.toFixed(2) + " V"; + case "capteur": + return model.valeur + (model.unite ? (" " + model.unite) : ""); + case "reservoir": + return model.niveau.toFixed(0) + " % (" + model.capacite.toFixed(0) + " L)"; + default: + return ""; + } + } color: couleur_blanc_casse font.pixelSize: 12 anchors.horizontalCenter: parent.horizontalCenter } + + // Indication visuelle discrète que l'élément est cliquable (circuits) : + Text { + visible: (domaine_actif === "circuit") + text: (model.consigne > 0) ? qsTr("● Marche (toucher pour arrêter)") : qsTr("○ Arrêt (toucher pour démarrer)") + color: (model.consigne > 0) ? "lightgreen" : couleur_blanc_casse + font.pixelSize: 10 + anchors.horizontalCenter: parent.horizontalCenter + } + } + + MouseArea { + anchors.fill: parent + enabled: (domaine_actif === "circuit") + onClicked: { + var nouvelle_consigne = (model.consigne > 0) ? 0 : 100; + dataProvider.set_circuit(model.id, nouvelle_consigne); + } } } Label { anchors.centerIn: parent - visible: circuits_grid.count === 0 - text: qsTr("Aucun circuit dans ce groupe.") + visible: elements_grid.count === 0 + text: qsTr("Aucun élément dans ce groupe.") color: couleur_rouge_transparent } }