diff --git a/dataprovider.cpp b/dataprovider.cpp index e7c77de..577a5d1 100644 --- a/dataprovider.cpp +++ b/dataprovider.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -10,51 +9,90 @@ DataProvider::DataProvider(QObject *parent) : QObject(parent) { } +void DataProvider::signalerSucces() +{ + m_connecte = true; + m_derniereConnexionReussie = QDateTime::currentDateTime(); + emit etatConnexionChange(); +} + +void DataProvider::signalerEchec() +{ + if (m_connecte) { + m_connecte = false; + emit etatConnexionChange(); + } +} + void DataProvider::get_familles(const QString &type) { + if (m_replyFamillesEnCours) m_replyFamillesEnCours->abort(); + QUrl url(m_baseUrl + "/elements/familles"); QUrlQuery query; query.addQueryItem("type", type); url.setQuery(query); - auto reply = m_manager.get(QNetworkRequest(url)); + QNetworkRequest request(url); + request.setTransferTimeout(5000); + + auto reply = m_manager.get(request); + m_replyFamillesEnCours = reply; + connect(reply, &QNetworkReply::finished, this, [this, reply, type]() { reply->deleteLater(); + if (m_replyFamillesEnCours == reply) m_replyFamillesEnCours = nullptr; + if (reply->error() == QNetworkReply::OperationCanceledError) return; if (reply->error() != QNetworkReply::NoError) { + signalerEchec(); emit erreur(reply->errorString()); return; } const auto doc = QJsonDocument::fromJson(reply->readAll()); const auto obj = doc.object(); if (obj["status"].toString() != "ok") { + signalerEchec(); emit erreur(obj["message"].toString()); return; } + signalerSucces(); emit famillesRecues(type, obj["familles"].toArray()); }); } void DataProvider::get_elements(const QString &type, int famille) { + if (m_replyElementsEnCours) m_replyElementsEnCours->abort(); + QUrl url(m_baseUrl + "/elements"); QUrlQuery query; query.addQueryItem("type", type); query.addQueryItem("famille", QString::number(famille)); url.setQuery(query); - auto reply = m_manager.get(QNetworkRequest(url)); + QNetworkRequest request(url); + request.setTransferTimeout(5000); + + auto reply = m_manager.get(request); + m_replyElementsEnCours = reply; + connect(reply, &QNetworkReply::finished, this, [this, reply, type, famille]() { reply->deleteLater(); + if (m_replyElementsEnCours == reply) m_replyElementsEnCours = nullptr; + if (reply->error() == QNetworkReply::OperationCanceledError) return; if (reply->error() != QNetworkReply::NoError) { + signalerEchec(); emit erreur(reply->errorString()); return; } const auto doc = QJsonDocument::fromJson(reply->readAll()); const auto obj = doc.object(); if (obj["status"].toString() != "ok") { + signalerEchec(); emit erreur(obj["message"].toString()); return; } + signalerSucces(); emit elementsRecus(type, famille, obj["elements"].toArray()); }); } @@ -64,6 +102,7 @@ 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"); + request.setTransferTimeout(5000); QJsonObject body; body["consigne"] = consigne; @@ -73,7 +112,50 @@ void DataProvider::set_circuit(int id, int consigne) connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { reply->deleteLater(); const bool succes = (reply->error() == QNetworkReply::NoError); - if (!succes) emit erreur(reply->errorString()); + if (succes) signalerSucces(); + else { + signalerEchec(); + emit erreur(reply->errorString()); + } emit circuitModifie(id, succes); }); } + +void DataProvider::get_mesures(const QString &type, int id, int periode, const QString &variable) +{ + if (m_replyMesuresEnCours) m_replyMesuresEnCours->abort(); + + QUrl url(m_baseUrl + "/mesures"); + QUrlQuery query; + query.addQueryItem("type", type); + query.addQueryItem("id", QString::number(id)); + query.addQueryItem("periode", QString::number(periode)); + if (!variable.isEmpty()) query.addQueryItem("variable", variable); + url.setQuery(query); + + QNetworkRequest request(url); + request.setTransferTimeout(8000); // Requête potentiellement plus lourde (jusqu'à 200 points) qu'un simple /elements - délai un peu plus généreux. + + auto reply = m_manager.get(request); + m_replyMesuresEnCours = reply; + + connect(reply, &QNetworkReply::finished, this, [this, reply, type, id, periode, variable]() { + reply->deleteLater(); + if (m_replyMesuresEnCours == reply) m_replyMesuresEnCours = nullptr; + if (reply->error() == QNetworkReply::OperationCanceledError) return; + if (reply->error() != QNetworkReply::NoError) { + signalerEchec(); + emit erreur(reply->errorString()); + return; + } + const auto doc = QJsonDocument::fromJson(reply->readAll()); + const auto obj = doc.object(); + if (obj["status"].toString() != "ok") { + signalerEchec(); + emit erreur(obj["message"].toString()); + return; + } + signalerSucces(); + emit mesuresRecues(type, id, periode, variable, obj); + }); +} diff --git a/dataprovider.h b/dataprovider.h index 8c14f2e..5b086b7 100644 --- a/dataprovider.h +++ b/dataprovider.h @@ -2,6 +2,10 @@ #include #include #include +#include +#include + +class QNetworkReply; // ============================================================================ // DataProvider - couche d'accès aux données du client WASM (cas 2/3). @@ -11,13 +15,23 @@ // 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 : les 4 domaines (circuit, batterie, capteur, reservoir) -// en lecture, écriture (consigne/mode auto) sur circuit uniquement. +// Portée actuelle : les 4 domaines (circuits, batteries, capteurs, reservoirs) +// en lecture, écriture (consigne/mode auto) sur circuits uniquement, et +// désormais l'historique/courbe (capteurs uniquement pour l'instant, cf +// GET /mesures côté API). +// +// Suit également l'état de connexion (succès/échec de chaque requête) pour +// permettre à l'UI d'afficher un état "hors ligne" explicite plutôt que de +// laisser les listes silencieusement vides ou figées. // ============================================================================ class DataProvider : public QObject { Q_OBJECT + Q_PROPERTY(bool connecte READ connecte NOTIFY etatConnexionChange) + Q_PROPERTY(bool aDejaEteConnecte READ aDejaEteConnecte NOTIFY etatConnexionChange) + Q_PROPERTY(QDateTime derniereConnexionReussie READ derniereConnexionReussie NOTIFY etatConnexionChange) + public: explicit DataProvider(QObject *parent = nullptr); @@ -30,16 +44,34 @@ public: // Écriture rapide sur un circuit (bouton d'accueil) - PATCH /elements/circuit/. Q_INVOKABLE void set_circuit(int id, int consigne); + // Historique/courbe pour la page de détail - GET /mesures. + // periode : 0=1h, 1=24h, 2=7j, 3=30j (même convention que le natif). + // variable : requis pour circuits/batteries (sélecteur de variable côté + // client), ignoré pour capteurs/reservoirs (variable fixe côté serveur). + Q_INVOKABLE void get_mesures(const QString &type, int id, int periode, const QString &variable = QString()); + + bool connecte() const { return m_connecte; } + bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); } + QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; } + signals: - // 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 mesuresRecues(const QString &type, int id, int periode, const QString &variable, const QJsonObject &donnees); void erreur(const QString &message); + void etatConnexionChange(); private: + void signalerSucces(); + void signalerEchec(); + QNetworkAccessManager m_manager; QString m_baseUrl = QStringLiteral("https://api.ship2net.fr"); + bool m_connecte = false; + QDateTime m_derniereConnexionReussie; + + QNetworkReply *m_replyFamillesEnCours = nullptr; + QNetworkReply *m_replyElementsEnCours = nullptr; + QNetworkReply *m_replyMesuresEnCours = nullptr; }; diff --git a/dataprovider_OLD.cpp b/dataprovider_OLD.cpp new file mode 100644 index 0000000..7a93f5e --- /dev/null +++ b/dataprovider_OLD.cpp @@ -0,0 +1,139 @@ +#include "dataprovider.h" +#include +#include +#include +#include +#include +#include + +DataProvider::DataProvider(QObject *parent) : QObject(parent) +{ +} + +void DataProvider::signalerSucces() +{ + m_connecte = true; + m_derniereConnexionReussie = QDateTime::currentDateTime(); + emit etatConnexionChange(); +} + +void DataProvider::signalerEchec() +{ + // Ne touche JAMAIS m_derniereConnexionReussie ici : on veut garder la + // trace du dernier succès réel, pas la remplacer par l'instant de l'échec. + if (m_connecte) { + m_connecte = false; + emit etatConnexionChange(); + } +} + +void DataProvider::get_familles(const QString &type) +{ + if (m_replyFamillesEnCours) { + m_replyFamillesEnCours->abort(); // Requête précédente encore en attente : remplacée par celle-ci + } + + QUrl url(m_baseUrl + "/elements/familles"); + QUrlQuery query; + query.addQueryItem("type", type); + url.setQuery(query); + + QNetworkRequest request(url); + // Sans délai explicite, une requête vers un CM5 devenu injoignable peut + // rester en attente indéfiniment côté navigateur (ni succès ni échec) - + // 'finished' ne se déclencherait alors jamais, empêchant toute détection + // de perte de connexion. + request.setTransferTimeout(TIME_OUT_TRANSFER_DATA); + + auto reply = m_manager.get(request); + m_replyFamillesEnCours = reply; + + connect(reply, &QNetworkReply::finished, this, [this, reply, type]() { + reply->deleteLater(); + if (m_replyFamillesEnCours == reply) m_replyFamillesEnCours = nullptr; + + if (reply->error() == QNetworkReply::OperationCanceledError) { + return; // Annulée volontairement (remplacée par une requête plus récente) - pas un vrai échec réseau + } + if (reply->error() != QNetworkReply::NoError) { + signalerEchec(); + emit erreur(reply->errorString()); + return; + } + const auto doc = QJsonDocument::fromJson(reply->readAll()); + const auto obj = doc.object(); + if (obj["status"].toString() != "ok") { + signalerEchec(); + emit erreur(obj["message"].toString()); + return; + } + signalerSucces(); + emit famillesRecues(type, obj["familles"].toArray()); + }); +} + +void DataProvider::get_elements(const QString &type, int famille) +{ + if (m_replyElementsEnCours) { + m_replyElementsEnCours->abort(); + } + + QUrl url(m_baseUrl + "/elements"); + QUrlQuery query; + query.addQueryItem("type", type); + query.addQueryItem("famille", QString::number(famille)); + url.setQuery(query); + + QNetworkRequest request(url); + request.setTransferTimeout(TIME_OUT_TRANSFER_DATA); + + auto reply = m_manager.get(request); + m_replyElementsEnCours = reply; + + connect(reply, &QNetworkReply::finished, this, [this, reply, type, famille]() { + reply->deleteLater(); + if (m_replyElementsEnCours == reply) m_replyElementsEnCours = nullptr; + + if (reply->error() == QNetworkReply::OperationCanceledError) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + signalerEchec(); + emit erreur(reply->errorString()); + return; + } + const auto doc = QJsonDocument::fromJson(reply->readAll()); + const auto obj = doc.object(); + if (obj["status"].toString() != "ok") { + signalerEchec(); + emit erreur(obj["message"].toString()); + return; + } + signalerSucces(); + 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"); + request.setTransferTimeout(TIME_OUT_TRANSFER_DATA); + + 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) signalerSucces(); + else { + signalerEchec(); + emit erreur(reply->errorString()); + } + emit circuitModifie(id, succes); + }); +} diff --git a/dataprovider_OLD.h b/dataprovider_OLD.h new file mode 100644 index 0000000..73125cc --- /dev/null +++ b/dataprovider_OLD.h @@ -0,0 +1,79 @@ +#pragma once +#include +#include +#include +#include + +class QNetworkReply; + +// ============================================================================ +// DataProvider - couche d'accès aux données du client WASM (cas 2/3). +// ---------------------------------------------------------------------------- +// Remplace, pour les domaines déjà portés, les appels bdd.* directs du cas 1 +// (qui reste inchangé, cf décision §21.1 de la synthèse). Toute la logique +// 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 : les 4 domaines (circuit, batterie, capteur, reservoir) +// en lecture, écriture (consigne/mode auto) sur circuit uniquement. +// +// Suit également l'état de connexion (succès/échec de chaque requête) pour +// permettre à l'UI d'afficher un état "hors ligne" explicite plutôt que de +// laisser les listes silencieusement vides ou figées (cf synthèse, gestion +// du hors-ligne). +// ============================================================================ + +#define TIME_OUT_TRANSFER_DATA 5000 // En ms + + +class DataProvider : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool connecte READ connecte NOTIFY etatConnexionChange) + Q_PROPERTY(bool aDejaEteConnecte READ aDejaEteConnecte NOTIFY etatConnexionChange) + Q_PROPERTY(QDateTime derniereConnexionReussie READ derniereConnexionReussie NOTIFY etatConnexionChange) + +public: + explicit DataProvider(QObject *parent = nullptr); + + // 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 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); + + bool connecte() const { return m_connecte; } + bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); } + QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; } + +signals: + // 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); + void etatConnexionChange(); + +private: + void signalerSucces(); + void signalerEchec(); + + QNetworkAccessManager m_manager; + QString m_baseUrl = QStringLiteral("https://api.ship2net.fr"); + bool m_connecte = false; + QDateTime m_derniereConnexionReussie; // invalide (isValid()==false) tant qu'aucun succès n'a jamais eu lieu + + // Requête en cours par type d'appel (familles/éléments) - permet d'annuler + // une requête encore en vol avant d'en émettre une nouvelle, pour éviter + // qu'une réponse "en retard" (envoyée pendant une coupure, résolue après + // qu'une requête plus récente ait déjà réussi) ne fasse clignoter l'état + // de connexion après une reconnexion. + QNetworkReply *m_replyFamillesEnCours = nullptr; + QNetworkReply *m_replyElementsEnCours = nullptr; +}; diff --git a/deploy/deploy.bat b/deploy/deploy.bat new file mode 100644 index 0000000..3474c00 --- /dev/null +++ b/deploy/deploy.bat @@ -0,0 +1,3 @@ +@echo off +powershell -ExecutionPolicy Bypass -File "%~dp0deploy_wasm.ps1" + diff --git a/deploy/deploy_wasm.ps1 b/deploy/deploy_wasm.ps1 index 49d1fdb..73ce986 100644 --- a/deploy/deploy_wasm.ps1 +++ b/deploy/deploy_wasm.ps1 @@ -11,12 +11,10 @@ # ============================================================================ param( - [string]$BuildDir = "O:\Technique\Projets_Qt\koriolan\shipheart_wasm\build\WebAssembly_Qt_6_8_3_Release", - [string]$AssetsDir = "O:\Technique\Projets_Qt\koriolan\shipheart_wasm\deploy", # manifest.json, icônes, sw.js "modèle" (CACHE_NAME sans version) - [string]$CM5Host = "192.168.1.220", - [int]$CM5Port = 2022, - [string]$CM5User = "koriolan", - [string]$CM5Path = "/var/www/shipheart-poc-wasm/" + [string]$BuildDir = "C:\Users\pierr\KORIOLAN\Koriolan - Documents\Technique\Projets_Qt\koriolan\shipheart_wasm\build\WebAssembly_Qt_6_8_3_Release", + [string]$AssetsDir = "C:\Users\pierr\KORIOLAN\Koriolan - Documents\Technique\Projets_Qt\koriolan\shipheart_wasm\deploy", + [string]$CM5Alias = "cm5", # alias défini dans %USERPROFILE%\.ssh\config + [string]$CM5Path = "/var/www/shipheart-wasm/" ) $ErrorActionPreference = "Stop" @@ -44,6 +42,32 @@ if ('serviceWorker' in navigator) { "@ Set-Content "$BuildDir\index.html" $html -NoNewline +# --- 2bis. Remplacer l'écran de chargement Qt par défaut (logo Qt + texte +# technique "Qt for WebAssembly: ...") par un écran plus sobre, réutilisant +# l'icône de l'app déjà déployée. Fait ici plutôt que via une configuration +# CMake dédiée : plusieurs retours d'expérience Qt confirment que Qt Creator +# régénère de toute façon le HTML par défaut au build, quelle que soit la +# tentative de template personnalisé côté CMake - rester sur ce mécanisme de +# post-traitement, déjà fiable pour le manifest/service-worker, est plus sûr. +$html = Get-Content "$BuildDir\index.html" -Raw +$ancienSpinner = '(?s)]*id="qtspinner">.*?' +$nouveauSpinner = @" +
+
+ +

+ ShipHeart™ +
+ by Koriolan +

+
+ +
+
+"@ +$html = $html -replace $ancienSpinner, $nouveauSpinner +Set-Content "$BuildDir\index.html" $html -NoNewline + # --- 3. Versionner sw.js (change le CACHE_NAME à chaque déploiement) --- # Le fichier "modèle" (CACHE_NAME générique) vit dans $AssetsDir, sous # contrôle de version - on ne le modifie jamais sur place, on écrit la @@ -60,7 +84,7 @@ Copy-Item "$AssetsDir\icon-512.png" "$BuildDir\icon-512.png" -Force $fichiers = "index.html","qtloader.js","qtlogo.svg","shipheart_wasm.js","shipheart_wasm.wasm","shipheart_wasm.worker.js","sw.js","manifest.json","icon-192.png","icon-512.png" foreach ($f in $fichiers) { - scp -P $CM5Port "$BuildDir\$f" "${CM5User}@${CM5Host}:${CM5Path}" + scp "$BuildDir\$f" "${CM5Alias}:${CM5Path}" } Write-Host "Déploiement terminé - version $version" diff --git a/deploy/icon-192.png b/deploy/icon-192.png index 0baffa4..9faab1a 100644 Binary files a/deploy/icon-192.png and b/deploy/icon-192.png differ diff --git a/deploy/icon-512.png b/deploy/icon-512.png index a502df5..2048119 100644 Binary files a/deploy/icon-512.png and b/deploy/icon-512.png differ diff --git a/deploy/manifest.json b/deploy/manifest.json index 83f1543..74a3f83 100644 --- a/deploy/manifest.json +++ b/deploy/manifest.json @@ -1,7 +1,7 @@ { - "name": "ShipHeart (POC WASM)", - "short_name": "ShipHeart POC", - "description": "POC d'accès à ShipHeart via WebAssembly - jusqu'à home_accueil.qml", + "name": "ShipHeart", + "short_name": "ShipHeart", + "description": "Accès à distance à ShipHeart", "start_url": "./index.html", "display": "fullscreen", "orientation": "landscape", diff --git a/images/4g_lte_off.png b/images/4g_lte_off.png new file mode 100644 index 0000000..01d9648 Binary files /dev/null and b/images/4g_lte_off.png differ diff --git a/images/4g_lte_on_1.png b/images/4g_lte_on_1.png new file mode 100644 index 0000000..d47051b Binary files /dev/null and b/images/4g_lte_on_1.png differ diff --git a/images/4g_lte_on_2.png b/images/4g_lte_on_2.png new file mode 100644 index 0000000..ae0aff0 Binary files /dev/null and b/images/4g_lte_on_2.png differ diff --git a/images/4g_lte_on_3.png b/images/4g_lte_on_3.png new file mode 100644 index 0000000..5126208 Binary files /dev/null and b/images/4g_lte_on_3.png differ diff --git a/images/4g_lte_on_4.png b/images/4g_lte_on_4.png new file mode 100644 index 0000000..30da103 Binary files /dev/null and b/images/4g_lte_on_4.png differ diff --git a/images/ak14.png b/images/ak14.png new file mode 100644 index 0000000..389e288 Binary files /dev/null and b/images/ak14.png differ diff --git a/images/ak5.png b/images/ak5.png new file mode 100644 index 0000000..f2fe322 Binary files /dev/null and b/images/ak5.png differ diff --git a/images/ampoule.png b/images/ampoule.png new file mode 100644 index 0000000..64a5de8 Binary files /dev/null and b/images/ampoule.png differ diff --git a/images/arrow-03-left.png b/images/arrow-03-left.png new file mode 100644 index 0000000..257d5c9 Binary files /dev/null and b/images/arrow-03-left.png differ diff --git a/images/attention.png b/images/attention.png new file mode 100644 index 0000000..5e1e8e8 Binary files /dev/null and b/images/attention.png differ diff --git a/images/backspace.png b/images/backspace.png new file mode 100644 index 0000000..3e3a18b Binary files /dev/null and b/images/backspace.png differ diff --git a/images/batterie.png b/images/batterie.png new file mode 100644 index 0000000..8e647da Binary files /dev/null and b/images/batterie.png differ diff --git a/images/batterie_12v.png b/images/batterie_12v.png new file mode 100644 index 0000000..63788fd Binary files /dev/null and b/images/batterie_12v.png differ diff --git a/images/batterie_24v.png b/images/batterie_24v.png new file mode 100644 index 0000000..c3ab950 Binary files /dev/null and b/images/batterie_24v.png differ diff --git a/images/batterie_moyen.png b/images/batterie_moyen.png new file mode 100644 index 0000000..6d738f2 Binary files /dev/null and b/images/batterie_moyen.png differ diff --git a/images/batterie_sombre.png b/images/batterie_sombre.png new file mode 100644 index 0000000..3aa80a9 Binary files /dev/null and b/images/batterie_sombre.png differ diff --git a/images/calendrier.png b/images/calendrier.png new file mode 100644 index 0000000..184fbbf Binary files /dev/null and b/images/calendrier.png differ diff --git a/images/chronometre.png b/images/chronometre.png new file mode 100644 index 0000000..c5a838f Binary files /dev/null and b/images/chronometre.png differ diff --git a/images/circuit_on.png b/images/circuit_on.png new file mode 100644 index 0000000..b9368cb Binary files /dev/null and b/images/circuit_on.png differ diff --git a/images/cloche.png b/images/cloche.png new file mode 100644 index 0000000..d63a669 Binary files /dev/null and b/images/cloche.png differ diff --git a/images/cloche_barree.png b/images/cloche_barree.png new file mode 100644 index 0000000..3ebd13e Binary files /dev/null and b/images/cloche_barree.png differ diff --git a/images/cloche_rouge.png b/images/cloche_rouge.png new file mode 100644 index 0000000..9488f88 Binary files /dev/null and b/images/cloche_rouge.png differ diff --git a/images/cloche_sombre.png b/images/cloche_sombre.png new file mode 100644 index 0000000..3dd2a60 Binary files /dev/null and b/images/cloche_sombre.png differ diff --git a/images/cloche_verte.png b/images/cloche_verte.png new file mode 100644 index 0000000..8721deb Binary files /dev/null and b/images/cloche_verte.png differ diff --git a/images/coin_bas_droite_bleu_koriolan.png b/images/coin_bas_droite_bleu_koriolan.png new file mode 100644 index 0000000..0c7b992 Binary files /dev/null and b/images/coin_bas_droite_bleu_koriolan.png differ diff --git a/images/coin_bas_droite_orange.png b/images/coin_bas_droite_orange.png new file mode 100644 index 0000000..64bee97 Binary files /dev/null and b/images/coin_bas_droite_orange.png differ diff --git a/images/coin_bas_droite_rouge.png b/images/coin_bas_droite_rouge.png new file mode 100644 index 0000000..5ddc7cd Binary files /dev/null and b/images/coin_bas_droite_rouge.png differ diff --git a/images/condition_horaire.pdn b/images/condition_horaire.pdn new file mode 100644 index 0000000..0355d56 Binary files /dev/null and b/images/condition_horaire.pdn differ diff --git a/images/condition_horaire.png b/images/condition_horaire.png new file mode 100644 index 0000000..484e8b1 Binary files /dev/null and b/images/condition_horaire.png differ diff --git a/images/cone_chantier.png b/images/cone_chantier.png new file mode 100644 index 0000000..c5088a4 Binary files /dev/null and b/images/cone_chantier.png differ diff --git a/images/cycle.png b/images/cycle.png new file mode 100644 index 0000000..87aaebb Binary files /dev/null and b/images/cycle.png differ diff --git a/images/cycle_100pc.png b/images/cycle_100pc.png new file mode 100644 index 0000000..fa87c01 Binary files /dev/null and b/images/cycle_100pc.png differ diff --git a/images/cycle_100pc_sombre.png b/images/cycle_100pc_sombre.png new file mode 100644 index 0000000..dad6bfd Binary files /dev/null and b/images/cycle_100pc_sombre.png differ diff --git a/images/cycle_clair.png b/images/cycle_clair.png new file mode 100644 index 0000000..609b2d9 Binary files /dev/null and b/images/cycle_clair.png differ diff --git a/images/cycle_sombre.png b/images/cycle_sombre.png new file mode 100644 index 0000000..3556aaa Binary files /dev/null and b/images/cycle_sombre.png differ diff --git a/images/double_fleche_gauche.png b/images/double_fleche_gauche.png new file mode 100644 index 0000000..a826842 Binary files /dev/null and b/images/double_fleche_gauche.png differ diff --git a/images/down.png b/images/down.png new file mode 100644 index 0000000..0660981 Binary files /dev/null and b/images/down.png differ diff --git a/images/ethernet_off.png b/images/ethernet_off.png new file mode 100644 index 0000000..e54a884 Binary files /dev/null and b/images/ethernet_off.png differ diff --git a/images/ethernet_off_old.png b/images/ethernet_off_old.png new file mode 100644 index 0000000..eb7bd1e Binary files /dev/null and b/images/ethernet_off_old.png differ diff --git a/images/ethernet_on.png b/images/ethernet_on.png new file mode 100644 index 0000000..7ba24e8 Binary files /dev/null and b/images/ethernet_on.png differ diff --git a/images/ethernet_on_old.png b/images/ethernet_on_old.png new file mode 100644 index 0000000..3cd2bc1 Binary files /dev/null and b/images/ethernet_on_old.png differ diff --git a/images/event.pdn b/images/event.pdn new file mode 100644 index 0000000..ac0dd00 Binary files /dev/null and b/images/event.pdn differ diff --git a/images/event.png b/images/event.png new file mode 100644 index 0000000..d538ec0 Binary files /dev/null and b/images/event.png differ diff --git a/images/factory_reset_actif.png b/images/factory_reset_actif.png new file mode 100644 index 0000000..9632879 Binary files /dev/null and b/images/factory_reset_actif.png differ diff --git a/images/factory_reset_sombre.png b/images/factory_reset_sombre.png new file mode 100644 index 0000000..4b21811 Binary files /dev/null and b/images/factory_reset_sombre.png differ diff --git a/images/faux.png b/images/faux.png new file mode 100644 index 0000000..ec02619 Binary files /dev/null and b/images/faux.png differ diff --git a/images/fibre_carbone.png b/images/fibre_carbone.png new file mode 100644 index 0000000..ad3431d Binary files /dev/null and b/images/fibre_carbone.png differ diff --git a/images/fleche_suite_bas.png b/images/fleche_suite_bas.png new file mode 100644 index 0000000..34664f6 Binary files /dev/null and b/images/fleche_suite_bas.png differ diff --git a/images/fleche_suite_droite.png b/images/fleche_suite_droite.png new file mode 100644 index 0000000..edcd0d4 Binary files /dev/null and b/images/fleche_suite_droite.png differ diff --git a/images/fleche_suite_droite_sombre.png b/images/fleche_suite_droite_sombre.png new file mode 100644 index 0000000..58b41bb Binary files /dev/null and b/images/fleche_suite_droite_sombre.png differ diff --git a/images/fleche_suite_gauche.png b/images/fleche_suite_gauche.png new file mode 100644 index 0000000..f76a0f3 Binary files /dev/null and b/images/fleche_suite_gauche.png differ diff --git a/images/fleche_suite_gauche_sombre.png b/images/fleche_suite_gauche_sombre.png new file mode 100644 index 0000000..8a4e7d3 Binary files /dev/null and b/images/fleche_suite_gauche_sombre.png differ diff --git a/images/fleche_suite_haut.png b/images/fleche_suite_haut.png new file mode 100644 index 0000000..4f8ce15 Binary files /dev/null and b/images/fleche_suite_haut.png differ diff --git a/images/fleche_tendance_haut_verte.png b/images/fleche_tendance_haut_verte.png new file mode 100644 index 0000000..275a802 Binary files /dev/null and b/images/fleche_tendance_haut_verte.png differ diff --git a/images/fond_2.jpg b/images/fond_2.jpg new file mode 100644 index 0000000..15b6b13 Binary files /dev/null and b/images/fond_2.jpg differ diff --git a/images/fond_3.jpg b/images/fond_3.jpg new file mode 100644 index 0000000..0afbcc6 Binary files /dev/null and b/images/fond_3.jpg differ diff --git a/images/gnss_off.png b/images/gnss_off.png new file mode 100644 index 0000000..331bde2 Binary files /dev/null and b/images/gnss_off.png differ diff --git a/images/gnss_on.png b/images/gnss_on.png new file mode 100644 index 0000000..b73dd30 Binary files /dev/null and b/images/gnss_on.png differ diff --git a/images/heure.png b/images/heure.png new file mode 100644 index 0000000..b75159a Binary files /dev/null and b/images/heure.png differ diff --git a/images/heure_minute.png b/images/heure_minute.png new file mode 100644 index 0000000..79ae6af Binary files /dev/null and b/images/heure_minute.png differ diff --git a/images/icone_auxiliaires.png b/images/icone_auxiliaires.png new file mode 100644 index 0000000..41532b8 Binary files /dev/null and b/images/icone_auxiliaires.png differ diff --git a/images/icone_barometre.png b/images/icone_barometre.png new file mode 100644 index 0000000..dc27082 Binary files /dev/null and b/images/icone_barometre.png differ diff --git a/images/icone_consommation_electrique.png b/images/icone_consommation_electrique.png new file mode 100644 index 0000000..4923bc6 Binary files /dev/null and b/images/icone_consommation_electrique.png differ diff --git a/images/icone_humidite.png b/images/icone_humidite.png new file mode 100644 index 0000000..299f86f Binary files /dev/null and b/images/icone_humidite.png differ diff --git a/images/icone_journal_de_bord.png b/images/icone_journal_de_bord.png new file mode 100644 index 0000000..04360cb Binary files /dev/null and b/images/icone_journal_de_bord.png differ diff --git a/images/icone_parametres.png b/images/icone_parametres.png new file mode 100644 index 0000000..1b181d4 Binary files /dev/null and b/images/icone_parametres.png differ diff --git a/images/icone_parametres_acces_autorise.png b/images/icone_parametres_acces_autorise.png new file mode 100644 index 0000000..3492de4 Binary files /dev/null and b/images/icone_parametres_acces_autorise.png differ diff --git a/images/icone_retour.png b/images/icone_retour.png new file mode 100644 index 0000000..2958364 Binary files /dev/null and b/images/icone_retour.png differ diff --git a/images/icone_retour_OLD.png b/images/icone_retour_OLD.png new file mode 100644 index 0000000..a5abf7d Binary files /dev/null and b/images/icone_retour_OLD.png differ diff --git a/images/icone_temperature.png b/images/icone_temperature.png new file mode 100644 index 0000000..f20d263 Binary files /dev/null and b/images/icone_temperature.png differ diff --git a/images/left.png b/images/left.png new file mode 100644 index 0000000..2b86a2a Binary files /dev/null and b/images/left.png differ diff --git a/images/logo_shipheart.png b/images/logo_shipheart.png new file mode 100644 index 0000000..51d0912 Binary files /dev/null and b/images/logo_shipheart.png differ diff --git a/images/logo_suncy_concept.png b/images/logo_suncy_concept.png new file mode 100644 index 0000000..b986f2e Binary files /dev/null and b/images/logo_suncy_concept.png differ diff --git a/images/lune_1.png b/images/lune_1.png new file mode 100644 index 0000000..f450280 Binary files /dev/null and b/images/lune_1.png differ diff --git a/images/lune_10.png b/images/lune_10.png new file mode 100644 index 0000000..bfbae73 Binary files /dev/null and b/images/lune_10.png differ diff --git a/images/lune_11.png b/images/lune_11.png new file mode 100644 index 0000000..c0fee65 Binary files /dev/null and b/images/lune_11.png differ diff --git a/images/lune_12.png b/images/lune_12.png new file mode 100644 index 0000000..d2e5ea2 Binary files /dev/null and b/images/lune_12.png differ diff --git a/images/lune_13.png b/images/lune_13.png new file mode 100644 index 0000000..f764c0a Binary files /dev/null and b/images/lune_13.png differ diff --git a/images/lune_14.png b/images/lune_14.png new file mode 100644 index 0000000..97bbd93 Binary files /dev/null and b/images/lune_14.png differ diff --git a/images/lune_15.png b/images/lune_15.png new file mode 100644 index 0000000..fa43ea5 Binary files /dev/null and b/images/lune_15.png differ diff --git a/images/lune_16.png b/images/lune_16.png new file mode 100644 index 0000000..fa43ea5 Binary files /dev/null and b/images/lune_16.png differ diff --git a/images/lune_2.png b/images/lune_2.png new file mode 100644 index 0000000..77ad83f Binary files /dev/null and b/images/lune_2.png differ diff --git a/images/lune_3.png b/images/lune_3.png new file mode 100644 index 0000000..79c845d Binary files /dev/null and b/images/lune_3.png differ diff --git a/images/lune_4.png b/images/lune_4.png new file mode 100644 index 0000000..c002267 Binary files /dev/null and b/images/lune_4.png differ diff --git a/images/lune_5.png b/images/lune_5.png new file mode 100644 index 0000000..f34f86f Binary files /dev/null and b/images/lune_5.png differ diff --git a/images/lune_6.png b/images/lune_6.png new file mode 100644 index 0000000..d4e27f9 Binary files /dev/null and b/images/lune_6.png differ diff --git a/images/lune_7.png b/images/lune_7.png new file mode 100644 index 0000000..6a3e52f Binary files /dev/null and b/images/lune_7.png differ diff --git a/images/lune_8.png b/images/lune_8.png new file mode 100644 index 0000000..58a6e1e Binary files /dev/null and b/images/lune_8.png differ diff --git a/images/lune_9.png b/images/lune_9.png new file mode 100644 index 0000000..c476aa2 Binary files /dev/null and b/images/lune_9.png differ diff --git a/images/minute.png b/images/minute.png new file mode 100644 index 0000000..410279f Binary files /dev/null and b/images/minute.png differ diff --git a/images/point_arc_en_ciel.png b/images/point_arc_en_ciel.png new file mode 100644 index 0000000..071afc0 Binary files /dev/null and b/images/point_arc_en_ciel.png differ diff --git a/images/point_blanc.png b/images/point_blanc.png new file mode 100644 index 0000000..3cc56d5 Binary files /dev/null and b/images/point_blanc.png differ diff --git a/images/point_bleu.png b/images/point_bleu.png new file mode 100644 index 0000000..94eb61d Binary files /dev/null and b/images/point_bleu.png differ diff --git a/images/point_jaune.png b/images/point_jaune.png new file mode 100644 index 0000000..a3072ec Binary files /dev/null and b/images/point_jaune.png differ diff --git a/images/point_orange.png b/images/point_orange.png new file mode 100644 index 0000000..6497a83 Binary files /dev/null and b/images/point_orange.png differ diff --git a/images/point_rouge.png b/images/point_rouge.png new file mode 100644 index 0000000..a13cacd Binary files /dev/null and b/images/point_rouge.png differ diff --git a/images/point_transparent.png b/images/point_transparent.png new file mode 100644 index 0000000..ebb945d Binary files /dev/null and b/images/point_transparent.png differ diff --git a/images/point_vert.png b/images/point_vert.png new file mode 100644 index 0000000..a6f0e68 Binary files /dev/null and b/images/point_vert.png differ diff --git a/images/reservoir.png b/images/reservoir.png new file mode 100644 index 0000000..29577c5 Binary files /dev/null and b/images/reservoir.png differ diff --git a/images/reservoir_eau.png b/images/reservoir_eau.png new file mode 100644 index 0000000..9d07ecf Binary files /dev/null and b/images/reservoir_eau.png differ diff --git a/images/reset.png b/images/reset.png new file mode 100644 index 0000000..b0fd6a0 Binary files /dev/null and b/images/reset.png differ diff --git a/images/reset_actif.png b/images/reset_actif.png new file mode 100644 index 0000000..c90f714 Binary files /dev/null and b/images/reset_actif.png differ diff --git a/images/reset_sombre.png b/images/reset_sombre.png new file mode 100644 index 0000000..01978d0 Binary files /dev/null and b/images/reset_sombre.png differ diff --git a/images/reveil.png b/images/reveil.png new file mode 100644 index 0000000..87eb85d Binary files /dev/null and b/images/reveil.png differ diff --git a/images/right.png b/images/right.png new file mode 100644 index 0000000..5d45d19 Binary files /dev/null and b/images/right.png differ diff --git a/images/right_sombre.png b/images/right_sombre.png new file mode 100644 index 0000000..253be02 Binary files /dev/null and b/images/right_sombre.png differ diff --git a/images/sablier.png b/images/sablier.png new file mode 100644 index 0000000..4a2cea6 Binary files /dev/null and b/images/sablier.png differ diff --git a/images/satellite.png b/images/satellite.png new file mode 100644 index 0000000..0568ae8 Binary files /dev/null and b/images/satellite.png differ diff --git a/images/shipbase.png b/images/shipbase.png new file mode 100644 index 0000000..a6fc621 Binary files /dev/null and b/images/shipbase.png differ diff --git a/images/shipbs.png b/images/shipbs.png new file mode 100644 index 0000000..92b3221 Binary files /dev/null and b/images/shipbs.png differ diff --git a/images/soh.png b/images/soh.png new file mode 100644 index 0000000..6968acd Binary files /dev/null and b/images/soh.png differ diff --git a/images/soh_sombre.png b/images/soh_sombre.png new file mode 100644 index 0000000..a5c1add Binary files /dev/null and b/images/soh_sombre.png differ diff --git a/images/sp4ac.png b/images/sp4ac.png new file mode 100644 index 0000000..9b93ecf Binary files /dev/null and b/images/sp4ac.png differ diff --git a/images/sp8dc.png b/images/sp8dc.png new file mode 100644 index 0000000..4b3b969 Binary files /dev/null and b/images/sp8dc.png differ diff --git a/images/tri.png b/images/tri.png new file mode 100644 index 0000000..34ad185 Binary files /dev/null and b/images/tri.png differ diff --git a/images/up.png b/images/up.png new file mode 100644 index 0000000..c37e529 Binary files /dev/null and b/images/up.png differ diff --git a/images/up_bleu.png b/images/up_bleu.png new file mode 100644 index 0000000..259b42d Binary files /dev/null and b/images/up_bleu.png differ diff --git a/images/vanne.png b/images/vanne.png new file mode 100644 index 0000000..66cc3f7 Binary files /dev/null and b/images/vanne.png differ diff --git a/images/vanne_fermee.png b/images/vanne_fermee.png new file mode 100644 index 0000000..e7adbb7 Binary files /dev/null and b/images/vanne_fermee.png differ diff --git a/images/vanne_ouverte.png b/images/vanne_ouverte.png new file mode 100644 index 0000000..8e6baee Binary files /dev/null and b/images/vanne_ouverte.png differ diff --git a/images/vanne_ouverte_2.png b/images/vanne_ouverte_2.png new file mode 100644 index 0000000..829b59f Binary files /dev/null and b/images/vanne_ouverte_2.png differ diff --git a/images/vrai.png b/images/vrai.png new file mode 100644 index 0000000..6bc6e9f Binary files /dev/null and b/images/vrai.png differ diff --git a/images/wifi_ap_off.png b/images/wifi_ap_off.png new file mode 100644 index 0000000..7773ade Binary files /dev/null and b/images/wifi_ap_off.png differ diff --git a/images/wifi_ap_on.png b/images/wifi_ap_on.png new file mode 100644 index 0000000..f3cc0ae Binary files /dev/null and b/images/wifi_ap_on.png differ diff --git a/images/wifi_sta_off.png b/images/wifi_sta_off.png new file mode 100644 index 0000000..e227b8b Binary files /dev/null and b/images/wifi_sta_off.png differ diff --git a/images/wifi_sta_on.png b/images/wifi_sta_on.png new file mode 100644 index 0000000..7677ee9 Binary files /dev/null and b/images/wifi_sta_on.png differ diff --git a/main.cpp b/main.cpp index 9a2c24f..b496562 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -10,12 +9,11 @@ #include "dataprovider.h" // ============================================================================ -// main.cpp - shipheart_wasm +// main.cpp - shipheart_wasm (CLIENT) // ---------------------------------------------------------------------------- -// Dérivé du main.cpp du POC (§19). Ajout : enregistrement de DataProvider, -// qui vient s'ajouter aux mocks existants (toujours nécessaires pour -// Shipheart_page.qml/main.qml, qui appellent encore bdd.*/systeme.* pour les -// fonctions non liées aux circuits - synchro, veille, etc., cf §19.4). +// À NE PAS CONFONDRE avec le main.cpp de shipheart_api (API, QHttpServer) - +// même nom de fichier, deux projets différents. Celui-ci utilise +// QGuiApplication (pas QApplication : pas de QtWidgets, app 100% QML/QtQuick). // ============================================================================ static QJSValue appVersionSingletonProvider(QQmlEngine *engine, QJSEngine *scriptEngine) @@ -34,7 +32,6 @@ static QJSValue appVersionSingletonProvider(QQmlEngine *engine, QJSEngine *scrip int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); -// app.setOverrideCursor(Qt::BlankCursor); QGuiApplication::setApplicationName("ShipHeart_Wasm"); QGuiApplication::setApplicationVersion("3.0.0-wasm"); diff --git a/mock/mock_bdd.cpp b/mock/mock_bdd.cpp index 0761e74..47aa45d 100644 --- a/mock/mock_bdd.cpp +++ b/mock/mock_bdd.cpp @@ -17,7 +17,7 @@ QVariant Bdd::requete_unique(const QString &req) return QVariantList{ "CM5" }; // Doit contenir "CM5" -> shipheart_generation_shipbase_v3 if (req.contains("Nom=\"Nom_du_bateau\"")) - return QVariantList{ "Bateau de test (POC WASM)" }; + return QVariantList{ "Bateau de test" }; if (req.contains("Validation_economiseur")) return QVariantList{ "0" }; // Économiseur désactivé par défaut pour ce POC diff --git a/qml.qrc b/qml.qrc index f03a00e..0555682 100644 --- a/qml.qrc +++ b/qml.qrc @@ -5,5 +5,135 @@ qml/home_attente.qml qml/home_accueil.qml qml/home.qml + qml/detail_capteur.qml + qml/detail_reservoir.qml + qml/detail_circuit.qml + qml/detail_batterie.qml + images/4g_lte_off.png + images/4g_lte_on_1.png + images/4g_lte_on_2.png + images/4g_lte_on_3.png + images/4g_lte_on_4.png + images/ak14.png + images/ak5.png + images/ampoule.png + images/arrow-03-left.png + images/attention.png + images/backspace.png + images/batterie.png + images/batterie_12v.png + images/batterie_24v.png + images/batterie_moyen.png + images/batterie_sombre.png + images/calendrier.png + images/chronometre.png + images/circuit_on.png + images/cloche.png + images/cloche_barree.png + images/cloche_rouge.png + images/cloche_sombre.png + images/cloche_verte.png + images/coin_bas_droite_bleu_koriolan.png + images/coin_bas_droite_orange.png + images/coin_bas_droite_rouge.png + images/condition_horaire.pdn + images/condition_horaire.png + images/cone_chantier.png + images/cycle.png + images/cycle_100pc.png + images/cycle_100pc_sombre.png + images/cycle_clair.png + images/cycle_sombre.png + images/double_fleche_gauche.png + images/down.png + images/ethernet_off.png + images/ethernet_off_old.png + images/ethernet_on.png + images/ethernet_on_old.png + images/event.pdn + images/event.png + images/factory_reset_actif.png + images/factory_reset_sombre.png + images/faux.png + images/fibre_carbone.png + images/fleche_suite_bas.png + images/fleche_suite_droite.png + images/fleche_suite_droite_sombre.png + images/fleche_suite_gauche.png + images/fleche_suite_gauche_sombre.png + images/fleche_suite_haut.png + images/fleche_tendance_haut_verte.png + images/fond_2.jpg + images/fond_3.jpg + images/gnss_off.png + images/gnss_on.png + images/heure.png + images/heure_minute.png + images/icone_auxiliaires.png + images/icone_barometre.png + images/icone_consommation_electrique.png + images/icone_humidite.png + images/icone_journal_de_bord.png + images/icone_parametres.png + images/icone_parametres_acces_autorise.png + images/icone_retour.png + images/icone_retour_OLD.png + images/icone_temperature.png + images/left.png + images/logo_shipheart.png + images/logo_suncy_concept.png + images/lune_1.png + images/lune_10.png + images/lune_11.png + images/lune_12.png + images/lune_13.png + images/lune_14.png + images/lune_15.png + images/lune_16.png + images/lune_2.png + images/lune_3.png + images/lune_4.png + images/lune_5.png + images/lune_6.png + images/lune_7.png + images/lune_8.png + images/lune_9.png + images/minute.png + images/point_arc_en_ciel.png + images/point_blanc.png + images/point_bleu.png + images/point_jaune.png + images/point_orange.png + images/point_rouge.png + images/point_transparent.png + images/point_vert.png + images/reservoir.png + images/reservoir_eau.png + images/reset.png + images/reset_actif.png + images/reset_sombre.png + images/reveil.png + images/right.png + images/right_sombre.png + images/sablier.png + images/satellite.png + images/shipbase.png + images/shipbs.png + images/soh.png + images/soh_sombre.png + images/sp4ac.png + images/sp8dc.png + images/tri.png + images/up.png + images/up_bleu.png + images/vanne.png + images/vanne_fermee.png + images/vanne_ouverte.png + images/vanne_ouverte_2.png + images/vrai.png + images/wifi_ap_off.png + images/wifi_ap_on.png + images/wifi_sta_off.png + images/wifi_sta_on.png diff --git a/qml/detail_batterie.qml b/qml/detail_batterie.qml new file mode 100644 index 0000000..a08b361 --- /dev/null +++ b/qml/detail_batterie.qml @@ -0,0 +1,310 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import qt.koriolan.shipheart 1.0 + +// ============================================================================ +// detail_batterie.qml - page de détail d'une batterie (valeur + historique). +// ---------------------------------------------------------------------------- +// Poussée sur le StackView depuis home.qml (clic court sur une batterie). +// Domaine batteries : sélecteur de variable (8 options), comme les circuits. +// Le vrai défaut natif est le comparatif d'énergie (id_variable=0), +// volontairement laissé de côté (Phase 4, énergie, cf synthèse) - "SOC" +// choisi comme vue par défaut à la place, sur décision explicite. +// +// Mêmes simplifications volontaires que detail_capteur.qml (Canvas plutôt que +// QtCharts, ligne continue plutôt qu'escalier, pas de zoom tactile). +// ============================================================================ + +Shipheart_page { + id: page_detail_batterie + + property int idElement: 0 + property string nomElement: "" + + property int periodeIndex: 1 // 24 heures par défaut + readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")] + + property int variableIndex: 0 // "soc" par défaut + readonly property var variablesCodes: ["soc", "tension", "temperature", "courant_differentiel", "courant_charge", "courant_decharge", "soh", "autonomie"] + readonly property var variablesLibelles: [qsTr("SOC"), qsTr("Tension"), qsTr("Température"), qsTr("I. différentiel"), qsTr("I. charge"), qsTr("I. décharge"), qsTr("SOH"), qsTr("Autonomie")] + + property var pointsCourbe: [] + property real valeurMin: 0 + property real valeurMax: 0 + property real valeurMoyenne: 0 + property double tempsMin: 0 + property double tempsMax: 0 + property string uniteAffichage: "" + property int precisionAffichage: 1 + + property real valeurActuelle: 0 + + property DataProvider dataProvider: DataProvider {} + + function charger_courbe() { + dataProvider.get_mesures("batteries", idElement, periodeIndex, variablesCodes[variableIndex]); + } + + function formater_date(timestampMs) { + return Qt.formatDateTime(new Date(timestampMs), "dd/MM HH:mm"); + } + + Connections { + target: dataProvider + + function onMesuresRecues(type, id, periode, variable, donnees) { + if (id !== idElement || periode !== periodeIndex || variable !== variablesCodes[variableIndex]) return; + + pointsCourbe = donnees.points; + uniteAffichage = donnees.unite; + precisionAffichage = donnees.precision; + valeurActuelle = donnees.valeur_actuelle; + if (donnees.points.length > 0) { + valeurMin = donnees.min.v; + tempsMin = donnees.min.t; + valeurMax = donnees.max.v; + tempsMax = donnees.max.t; + valeurMoyenne = donnees.moyenne; + } + graphique.requestPaint(); + } + + function onErreur(message) { + console.log("DataProvider erreur (détail batterie) : " + message); + } + } + + Component.onCompleted: charger_courbe() + + Timer { + interval: 5000 + running: true + repeat: true + onTriggered: charger_courbe() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 15 + + Text { + text: nomElement + font.pixelSize: 22 + font.bold: true + color: couleur_gris_pale + } + + // Sélecteur de variable (8 options - grille 4x2 plutôt qu'une seule + // ligne, pour rester lisible) : + GridLayout { + Layout.fillWidth: true + columns: 4 + columnSpacing: 8 + rowSpacing: 8 + + Repeater { + model: variablesLibelles + delegate: Rectangle { + Layout.fillWidth: true + height: 34 + radius: 8 + color: (index === variableIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: modelData + color: couleur_blanc_casse + font.pixelSize: 12 + font.bold: (index === variableIndex) + } + + MouseArea { + anchors.fill: parent + onClicked: { + variableIndex = index; + charger_courbe(); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 20 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: 4 + Text { + text: valeurActuelle.toFixed(precisionAffichage) + font.pixelSize: 64 + font.bold: true + color: couleur_blanc_casse + } + Text { + text: uniteAffichage + font.pixelSize: 20 + color: couleur_gris_pale + Layout.alignment: Qt.AlignTop + } + } + + Item { Layout.fillWidth: true } + + ColumnLayout { + spacing: 10 + RowLayout { + Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Moyen"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + } + RowLayout { + Text { text: qsTr("Min"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; Layout.preferredWidth: 160 } + Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 15 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + Canvas { + id: graphique + Layout.fillWidth: true + Layout.fillHeight: true + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + if (pointsCourbe.length < 2) { + ctx.fillStyle = couleur_rouge_transparent; + ctx.font = "14px sans-serif"; + ctx.fillText(qsTr("Pas assez de données sur cette période."), width / 2 - 110, height / 2); + return; + } + + var marge = 45; + var largeurUtile = width - 2 * marge; + var hauteurUtile = height - 2 * marge; + + var tMin = pointsCourbe[0].t; + var tMax = pointsCourbe[pointsCourbe.length - 1].t; + + var padding = (valeurMax - valeurMin) * 0.1 || 1; + var yMin = valeurMin - padding; + var yMax = valeurMax + padding; + + function px(t) { return marge + ((t - tMin) / (tMax - tMin)) * largeurUtile; } + function py(v) { return marge + hauteurUtile - ((v - yMin) / (yMax - yMin)) * hauteurUtile; } + + ctx.strokeStyle = "#333333"; + ctx.fillStyle = couleur_gris_pale; + ctx.font = "11px sans-serif"; + var nbLignesY = 5; + for (var i = 0; i <= nbLignesY; i++) { + var v = yMin + (i / nbLignesY) * (yMax - yMin); + var y = py(v); + ctx.beginPath(); + ctx.moveTo(marge, y); + ctx.lineTo(width - marge, y); + ctx.stroke(); + ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4); + } + + var nbLabelsX = 6; + for (var j = 0; j <= nbLabelsX; j++) { + var t = tMin + (j / nbLabelsX) * (tMax - tMin); + var x = px(t); + ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8); + } + + ctx.strokeStyle = couleur_bleu_koriolan; + ctx.lineWidth = 2; + ctx.beginPath(); + for (var k = 0; k < pointsCourbe.length; k++) { + var xx = px(pointsCourbe[k].t); + var yy = py(pointsCourbe[k].v); + if (k === 0) ctx.moveTo(xx, yy); + else ctx.lineTo(xx, yy); + } + ctx.stroke(); + + ctx.fillStyle = "lightgreen"; + ctx.beginPath(); + ctx.arc(px(tempsMin), py(valeurMin), 4, 0, 2 * Math.PI); + ctx.fill(); + ctx.beginPath(); + ctx.arc(px(tempsMax), py(valeurMax), 4, 0, 2 * Math.PI); + ctx.fill(); + } + } + + ColumnLayout { + spacing: 8 + Repeater { + model: periodesLibelles + delegate: Rectangle { + Layout.preferredWidth: 140 + Layout.preferredHeight: 40 + radius: 8 + color: (index === periodeIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: modelData + color: couleur_blanc_casse + font.bold: (index === periodeIndex) + } + + MouseArea { + anchors.fill: parent + onClicked: { + periodeIndex = index; + charger_courbe(); + } + } + } + } + } + } + + // Bouton retour : + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: 8 + color: shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: "↩" + font.pixelSize: 20 + color: couleur_blanc_casse + } + + MouseArea { + anchors.fill: parent + onClicked: stackView.pop() + } + } + } +} diff --git a/qml/detail_capteur.qml b/qml/detail_capteur.qml new file mode 100644 index 0000000..563d820 --- /dev/null +++ b/qml/detail_capteur.qml @@ -0,0 +1,288 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import qt.koriolan.shipheart 1.0 + +// ============================================================================ +// detail_capteur.qml - page de détail d'un capteur (valeur + historique). +// ---------------------------------------------------------------------------- +// Poussée sur le StackView depuis home.qml (clic court sur un capteur). +// Première page de détail du client wasm - domaine capteurs uniquement pour +// l'instant (Phase 2 du plan convenu), circuits/batteries/reservoirs à +// répliquer ensuite sur le même patron. +// +// Simplifications volontaires par rapport au natif (capteur.qml) : +// - Graphique dessiné "à la main" en Canvas QML (pas QtCharts, qui exigerait +// de repasser de QGuiApplication à QApplication - remise en cause +// structurelle écartée pour l'instant). +// - Tracé en ligne continue, pas en "escalier" comme le natif (détail +// esthétique, ajustable plus tard si besoin). +// - Bornes de l'axe Y avec une marge simple (10%), pas encore l'arrondi "à +// un nombre rond" du natif (Fonctions.ajustement_mini_maxi()). +// - Pas de zoom/glisser tactile (cf discussion - le sélecteur de période +// couvre l'essentiel du besoin pour cette V1). +// ============================================================================ + +Shipheart_page { + id: page_detail_capteur + + // Capturé ici, à la racine - cf home.qml pour l'explication du piège QML évité. + + property int idElement: 0 + property string nomElement: "" + + property int periodeIndex: 1 // 24 heures par défaut + readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")] + + property var pointsCourbe: [] + property real valeurMin: 0 + property real valeurMax: 0 + property real valeurMoyenne: 0 + property double tempsMin: 0 + property double tempsMax: 0 + property string uniteAffichage: "" + property int precisionAffichage: 1 + + property real valeurActuelle: 0 + + property DataProvider dataProvider: DataProvider {} + + function charger_courbe() { + dataProvider.get_mesures("capteurs", idElement, periodeIndex); + } + + function formater_date(timestampMs) { + return Qt.formatDateTime(new Date(timestampMs), "dd/MM HH:mm"); + } + + Connections { + target: dataProvider + + function onMesuresRecues(type, id, periode, variable, donnees) { + if (id !== idElement || periode !== periodeIndex) return; // Réponse obsolète (période changée entretemps) + + pointsCourbe = donnees.points; + uniteAffichage = donnees.unite; + precisionAffichage = donnees.precision; + valeurActuelle = donnees.valeur_actuelle; + if (donnees.points.length > 0) { + valeurMin = donnees.min.v; + tempsMin = donnees.min.t; + valeurMax = donnees.max.v; + tempsMax = donnees.max.t; + valeurMoyenne = donnees.moyenne; + } + graphique.requestPaint(); + } + + function onErreur(message) { + console.log("DataProvider erreur (détail capteur) : " + message); + } + } + + Component.onCompleted: charger_courbe() + + // Rafraîchissement périodique - moins fréquent que sur l'accueil, une + // courbe n'a pas besoin d'une résolution à la seconde : + Timer { + interval: 5000 + running: true + repeat: true + onTriggered: charger_courbe() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 15 + + Text { + text: nomElement + font.pixelSize: 22 + font.bold: true + color: couleur_gris_pale + } + + RowLayout { + Layout.fillWidth: true + spacing: 20 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: 4 + Text { + text: valeurActuelle.toFixed(precisionAffichage) + font.pixelSize: 64 + font.bold: true + color: couleur_blanc_casse + } + Text { + text: uniteAffichage + font.pixelSize: 20 + color: couleur_gris_pale + Layout.alignment: Qt.AlignTop + } + } + + Item { Layout.fillWidth: true } + + ColumnLayout { + spacing: 10 + RowLayout { + Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Moyen"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + } + RowLayout { + Text { text: qsTr("Min"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; Layout.preferredWidth: 160 } + Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 15 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + Canvas { + id: graphique + Layout.fillWidth: true + Layout.fillHeight: true + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + if (pointsCourbe.length < 2) { + ctx.fillStyle = couleur_rouge_transparent; + ctx.font = "14px sans-serif"; + ctx.fillText(qsTr("Pas assez de données sur cette période."), width / 2 - 110, height / 2); + return; + } + + var marge = 45; + var largeurUtile = width - 2 * marge; + var hauteurUtile = height - 2 * marge; + + var tMin = pointsCourbe[0].t; + var tMax = pointsCourbe[pointsCourbe.length - 1].t; + + var padding = (valeurMax - valeurMin) * 0.1 || 1; + var yMin = valeurMin - padding; + var yMax = valeurMax + padding; + + function px(t) { return marge + ((t - tMin) / (tMax - tMin)) * largeurUtile; } + function py(v) { return marge + hauteurUtile - ((v - yMin) / (yMax - yMin)) * hauteurUtile; } + + // Quadrillage horizontal + étiquettes Y : + ctx.strokeStyle = "#333333"; + ctx.fillStyle = couleur_gris_pale; + ctx.font = "11px sans-serif"; + var nbLignesY = 5; + for (var i = 0; i <= nbLignesY; i++) { + var v = yMin + (i / nbLignesY) * (yMax - yMin); + var y = py(v); + ctx.beginPath(); + ctx.moveTo(marge, y); + ctx.lineTo(width - marge, y); + ctx.stroke(); + ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4); + } + + // Étiquettes X : + var nbLabelsX = 6; + for (var j = 0; j <= nbLabelsX; j++) { + var t = tMin + (j / nbLabelsX) * (tMax - tMin); + var x = px(t); + ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8); + } + + // Ligne de la courbe : + ctx.strokeStyle = couleur_bleu_koriolan; + ctx.lineWidth = 2; + ctx.beginPath(); + for (var k = 0; k < pointsCourbe.length; k++) { + var xx = px(pointsCourbe[k].t); + var yy = py(pointsCourbe[k].v); + if (k === 0) ctx.moveTo(xx, yy); + else ctx.lineTo(xx, yy); + } + ctx.stroke(); + + // Points min/max : + ctx.fillStyle = "lightgreen"; + ctx.beginPath(); + ctx.arc(px(tempsMin), py(valeurMin), 4, 0, 2 * Math.PI); + ctx.fill(); + ctx.beginPath(); + ctx.arc(px(tempsMax), py(valeurMax), 4, 0, 2 * Math.PI); + ctx.fill(); + } + } + + ColumnLayout { + spacing: 8 + Repeater { + model: periodesLibelles + delegate: Rectangle { + Layout.preferredWidth: 140 + Layout.preferredHeight: 40 + radius: 8 + color: (index === periodeIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: modelData + color: couleur_blanc_casse + font.bold: (index === periodeIndex) + } + + MouseArea { + anchors.fill: parent + onClicked: { + periodeIndex = index; + charger_courbe(); + } + } + } + } + } + } + + // Bouton retour : + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: 8 + color: shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: "↩" + font.pixelSize: 20 + color: couleur_blanc_casse + } + + MouseArea { + anchors.fill: parent + onClicked: { + stackView.pop(); + } + } + } + } +} diff --git a/qml/detail_circuit.qml b/qml/detail_circuit.qml new file mode 100644 index 0000000..623daec --- /dev/null +++ b/qml/detail_circuit.qml @@ -0,0 +1,307 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import qt.koriolan.shipheart 1.0 + +// ============================================================================ +// detail_circuit.qml - page de détail d'un circuit (valeur + historique). +// ---------------------------------------------------------------------------- +// Poussée sur le StackView depuis home.qml (clic court sur un circuit). +// Domaine circuits : sélecteur de variable (consigne/tension/intensité/ +// fréquence) en plus du sélecteur de période - contrairement à capteurs/ +// réservoirs, à variable unique. Page en LECTURE SEULE : la commande du +// circuit (bascule marche/arrêt) reste sur l'appui long depuis home.qml, +// pas dupliquée ici. +// +// Mêmes simplifications volontaires que detail_capteur.qml (Canvas plutôt que +// QtCharts, ligne continue plutôt qu'escalier, pas de zoom tactile). +// ============================================================================ + +Shipheart_page { + id: page_detail_circuit + + property int idElement: 0 + property string nomElement: "" + + property int periodeIndex: 1 // 24 heures par défaut + readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")] + + property int variableIndex: 1 // "tension" par défaut + readonly property var variablesCodes: ["consigne", "tension", "intensite", "frequence"] + readonly property var variablesLibelles: [qsTr("Consigne"), qsTr("Tension"), qsTr("Intensité"), qsTr("Fréquence")] + + property var pointsCourbe: [] + property real valeurMin: 0 + property real valeurMax: 0 + property real valeurMoyenne: 0 + property double tempsMin: 0 + property double tempsMax: 0 + property string uniteAffichage: "" + property int precisionAffichage: 1 + + property real valeurActuelle: 0 + + property DataProvider dataProvider: DataProvider {} + + function charger_courbe() { + dataProvider.get_mesures("circuits", idElement, periodeIndex, variablesCodes[variableIndex]); + } + + function formater_date(timestampMs) { + return Qt.formatDateTime(new Date(timestampMs), "dd/MM HH:mm"); + } + + Connections { + target: dataProvider + + function onMesuresRecues(type, id, periode, variable, donnees) { + if (id !== idElement || periode !== periodeIndex || variable !== variablesCodes[variableIndex]) return; // Réponse obsolète + + pointsCourbe = donnees.points; + uniteAffichage = donnees.unite; + precisionAffichage = donnees.precision; + valeurActuelle = donnees.valeur_actuelle; + if (donnees.points.length > 0) { + valeurMin = donnees.min.v; + tempsMin = donnees.min.t; + valeurMax = donnees.max.v; + tempsMax = donnees.max.t; + valeurMoyenne = donnees.moyenne; + } + graphique.requestPaint(); + } + + function onErreur(message) { + console.log("DataProvider erreur (détail circuit) : " + message); + } + } + + Component.onCompleted: charger_courbe() + + Timer { + interval: 5000 + running: true + repeat: true + onTriggered: charger_courbe() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 15 + + Text { + text: nomElement + font.pixelSize: 22 + font.bold: true + color: couleur_gris_pale + } + + // Sélecteur de variable (propre aux circuits/batteries) : + Row { + Layout.fillWidth: true + spacing: 8 + Repeater { + model: variablesLibelles + delegate: Rectangle { + width: (page_detail_circuit.width - 40 - 3 * 8) / 4 + height: 36 + radius: 8 + color: (index === variableIndex) ? 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 === variableIndex) + } + + MouseArea { + anchors.fill: parent + onClicked: { + variableIndex = index; + charger_courbe(); + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 20 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: 4 + Text { + text: valeurActuelle.toFixed(precisionAffichage) + font.pixelSize: 64 + font.bold: true + color: couleur_blanc_casse + } + Text { + text: uniteAffichage + font.pixelSize: 20 + color: couleur_gris_pale + Layout.alignment: Qt.AlignTop + } + } + + Item { Layout.fillWidth: true } + + ColumnLayout { + spacing: 10 + RowLayout { + Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Moyen"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + } + RowLayout { + Text { text: qsTr("Min"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; Layout.preferredWidth: 160 } + Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 15 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + Canvas { + id: graphique + Layout.fillWidth: true + Layout.fillHeight: true + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + if (pointsCourbe.length < 2) { + ctx.fillStyle = couleur_rouge_transparent; + ctx.font = "14px sans-serif"; + ctx.fillText(qsTr("Pas assez de données sur cette période."), width / 2 - 110, height / 2); + return; + } + + var marge = 45; + var largeurUtile = width - 2 * marge; + var hauteurUtile = height - 2 * marge; + + var tMin = pointsCourbe[0].t; + var tMax = pointsCourbe[pointsCourbe.length - 1].t; + + var padding = (valeurMax - valeurMin) * 0.1 || 1; + var yMin = valeurMin - padding; + var yMax = valeurMax + padding; + + function px(t) { return marge + ((t - tMin) / (tMax - tMin)) * largeurUtile; } + function py(v) { return marge + hauteurUtile - ((v - yMin) / (yMax - yMin)) * hauteurUtile; } + + ctx.strokeStyle = "#333333"; + ctx.fillStyle = couleur_gris_pale; + ctx.font = "11px sans-serif"; + var nbLignesY = 5; + for (var i = 0; i <= nbLignesY; i++) { + var v = yMin + (i / nbLignesY) * (yMax - yMin); + var y = py(v); + ctx.beginPath(); + ctx.moveTo(marge, y); + ctx.lineTo(width - marge, y); + ctx.stroke(); + ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4); + } + + var nbLabelsX = 6; + for (var j = 0; j <= nbLabelsX; j++) { + var t = tMin + (j / nbLabelsX) * (tMax - tMin); + var x = px(t); + ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8); + } + + ctx.strokeStyle = couleur_bleu_koriolan; + ctx.lineWidth = 2; + ctx.beginPath(); + for (var k = 0; k < pointsCourbe.length; k++) { + var xx = px(pointsCourbe[k].t); + var yy = py(pointsCourbe[k].v); + if (k === 0) ctx.moveTo(xx, yy); + else ctx.lineTo(xx, yy); + } + ctx.stroke(); + + ctx.fillStyle = "lightgreen"; + ctx.beginPath(); + ctx.arc(px(tempsMin), py(valeurMin), 4, 0, 2 * Math.PI); + ctx.fill(); + ctx.beginPath(); + ctx.arc(px(tempsMax), py(valeurMax), 4, 0, 2 * Math.PI); + ctx.fill(); + } + } + + ColumnLayout { + spacing: 8 + Repeater { + model: periodesLibelles + delegate: Rectangle { + Layout.preferredWidth: 140 + Layout.preferredHeight: 40 + radius: 8 + color: (index === periodeIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: modelData + color: couleur_blanc_casse + font.bold: (index === periodeIndex) + } + + MouseArea { + anchors.fill: parent + onClicked: { + periodeIndex = index; + charger_courbe(); + } + } + } + } + } + } + + // Bouton retour : + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: 8 + color: shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: "↩" + font.pixelSize: 20 + color: couleur_blanc_casse + } + + MouseArea { + anchors.fill: parent + onClicked: stackView.pop() + } + } + } +} diff --git a/qml/detail_reservoir.qml b/qml/detail_reservoir.qml new file mode 100644 index 0000000..20a7c2d --- /dev/null +++ b/qml/detail_reservoir.qml @@ -0,0 +1,279 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import qt.koriolan.shipheart 1.0 + +// ============================================================================ +// detail_reservoir.qml - page de détail d'un réservoir (valeur + historique). +// ---------------------------------------------------------------------------- +// Poussée sur le StackView depuis home.qml (clic court sur un réservoir). +// Domaine réservoirs : variable unique et fixe ("niveau", résolue côté +// serveur) - pas de sélecteur de variable, contrairement à circuits/batteries. +// +// Mêmes simplifications volontaires que detail_capteur.qml (Canvas plutôt que +// QtCharts, ligne continue plutôt qu'escalier, pas de zoom tactile). +// ============================================================================ + +Shipheart_page { + id: page_detail_reservoir + + // Capturé ici, à la racine - cf home.qml pour l'explication du piège QML évité. + + property int idElement: 0 + property string nomElement: "" + + property int periodeIndex: 1 // 24 heures par défaut + readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")] + + property var pointsCourbe: [] + property real valeurMin: 0 + property real valeurMax: 0 + property real valeurMoyenne: 0 + property double tempsMin: 0 + property double tempsMax: 0 + property string uniteAffichage: "" + property int precisionAffichage: 1 + + property real valeurActuelle: 0 + + property DataProvider dataProvider: DataProvider {} + + function charger_courbe() { + dataProvider.get_mesures("reservoirs", idElement, periodeIndex); + } + + function formater_date(timestampMs) { + return Qt.formatDateTime(new Date(timestampMs), "dd/MM HH:mm"); + } + + Connections { + target: dataProvider + + function onMesuresRecues(type, id, periode, variable, donnees) { + if (id !== idElement || periode !== periodeIndex) return; // Réponse obsolète (période changée entretemps) + + pointsCourbe = donnees.points; + uniteAffichage = donnees.unite; + precisionAffichage = donnees.precision; + valeurActuelle = donnees.valeur_actuelle; + if (donnees.points.length > 0) { + valeurMin = donnees.min.v; + tempsMin = donnees.min.t; + valeurMax = donnees.max.v; + tempsMax = donnees.max.t; + valeurMoyenne = donnees.moyenne; + } + graphique.requestPaint(); + } + + function onErreur(message) { + console.log("DataProvider erreur (détail reservoir) : " + message); + } + } + + Component.onCompleted: charger_courbe() + + // Rafraîchissement périodique - moins fréquent que sur l'accueil, une + // courbe n'a pas besoin d'une résolution à la seconde : + Timer { + interval: 5000 + running: true + repeat: true + onTriggered: charger_courbe() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 15 + + Text { + text: nomElement + font.pixelSize: 22 + font.bold: true + color: couleur_gris_pale + } + + RowLayout { + Layout.fillWidth: true + spacing: 20 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: 4 + Text { + text: valeurActuelle.toFixed(precisionAffichage) + font.pixelSize: 64 + font.bold: true + color: couleur_blanc_casse + } + Text { + text: uniteAffichage + font.pixelSize: 20 + color: couleur_gris_pale + Layout.alignment: Qt.AlignTop + } + } + + Item { Layout.fillWidth: true } + + ColumnLayout { + spacing: 10 + RowLayout { + Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Moyen"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + } + RowLayout { + Text { text: qsTr("Min"); color: couleur_gris_pale; Layout.preferredWidth: 90 } + Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 } + Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen } + } + RowLayout { + Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; Layout.preferredWidth: 160 } + Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 15 + opacity: dataProvider.connecte ? 1.0 : 0.45 + + Canvas { + id: graphique + Layout.fillWidth: true + Layout.fillHeight: true + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + if (pointsCourbe.length < 2) { + ctx.fillStyle = couleur_rouge_transparent; + ctx.font = "14px sans-serif"; + ctx.fillText(qsTr("Pas assez de données sur cette période."), width / 2 - 110, height / 2); + return; + } + + var marge = 45; + var largeurUtile = width - 2 * marge; + var hauteurUtile = height - 2 * marge; + + var tMin = pointsCourbe[0].t; + var tMax = pointsCourbe[pointsCourbe.length - 1].t; + + var padding = (valeurMax - valeurMin) * 0.1 || 1; + var yMin = valeurMin - padding; + var yMax = valeurMax + padding; + + function px(t) { return marge + ((t - tMin) / (tMax - tMin)) * largeurUtile; } + function py(v) { return marge + hauteurUtile - ((v - yMin) / (yMax - yMin)) * hauteurUtile; } + + // Quadrillage horizontal + étiquettes Y : + ctx.strokeStyle = "#333333"; + ctx.fillStyle = couleur_gris_pale; + ctx.font = "11px sans-serif"; + var nbLignesY = 5; + for (var i = 0; i <= nbLignesY; i++) { + var v = yMin + (i / nbLignesY) * (yMax - yMin); + var y = py(v); + ctx.beginPath(); + ctx.moveTo(marge, y); + ctx.lineTo(width - marge, y); + ctx.stroke(); + ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4); + } + + // Étiquettes X : + var nbLabelsX = 6; + for (var j = 0; j <= nbLabelsX; j++) { + var t = tMin + (j / nbLabelsX) * (tMax - tMin); + var x = px(t); + ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8); + } + + // Ligne de la courbe : + ctx.strokeStyle = couleur_bleu_koriolan; + ctx.lineWidth = 2; + ctx.beginPath(); + for (var k = 0; k < pointsCourbe.length; k++) { + var xx = px(pointsCourbe[k].t); + var yy = py(pointsCourbe[k].v); + if (k === 0) ctx.moveTo(xx, yy); + else ctx.lineTo(xx, yy); + } + ctx.stroke(); + + // Points min/max : + ctx.fillStyle = "lightgreen"; + ctx.beginPath(); + ctx.arc(px(tempsMin), py(valeurMin), 4, 0, 2 * Math.PI); + ctx.fill(); + ctx.beginPath(); + ctx.arc(px(tempsMax), py(valeurMax), 4, 0, 2 * Math.PI); + ctx.fill(); + } + } + + ColumnLayout { + spacing: 8 + Repeater { + model: periodesLibelles + delegate: Rectangle { + Layout.preferredWidth: 140 + Layout.preferredHeight: 40 + radius: 8 + color: (index === periodeIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: modelData + color: couleur_blanc_casse + font.bold: (index === periodeIndex) + } + + MouseArea { + anchors.fill: parent + onClicked: { + periodeIndex = index; + charger_courbe(); + } + } + } + } + } + } + + // Bouton retour : + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: 8 + color: shipheart_couleur_fond_bouton + + Text { + anchors.centerIn: parent + text: "↩" + font.pixelSize: 20 + color: couleur_blanc_casse + } + + MouseArea { + anchors.fill: parent + onClicked: { + stackView.pop(); + } + } + } + } +} diff --git a/qml/home.qml b/qml/home.qml index 15c87d3..4f1accb 100644 --- a/qml/home.qml +++ b/qml/home.qml @@ -9,19 +9,21 @@ import qt.koriolan.shipheart 1.0 // reservoir), via l'API réelle (DataProvider). Écriture (consigne) sur // circuit uniquement pour l'instant. // -// 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'. +// Architecture : les ONGLETS et les ÉLÉMENTS ont chacun UN MODÈLE SÉPARÉ PAR +// DOMAINE (8 ListModel au total). Deux raisons distinctes : +// - Éléments : évite le piège des "rôles" QML figés (tension/intensite pour +// circuit, valeur/unite pour capteur... - cf notes historiques). +// - Onglets ET éléments : préserve les dernières données connues d'un +// domaine quand on le quitte puis qu'on y revient, y compris hors ligne - +// on ne vide JAMAIS un modèle avant d'avoir une nouvelle donnée réelle à +// afficher à la place (sinon : données qui "disparaissent" en changeant +// d'onglet, cf retours d'expérience). // ============================================================================ Shipheart_page { id: page_accueil - readonly property var domaines: ["circuit", "batterie", "capteur", "reservoir"] + readonly property var domaines: ["circuits", "batteries", "capteurs", "reservoirs"] 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] @@ -29,95 +31,194 @@ Shipheart_page { readonly property int espace_onglets: 10 readonly property int rayon_onglets: 20 - property int id_groupe: 0 + // Famille sélectionnée, conservée INDÉPENDAMMENT par domaine - permet de + // revenir sur un domaine déjà visité sans perdre sa sélection, et permet + // au minuteur de savoir, pour CHAQUE domaine, s'il a déjà une famille + // active ou doit encore en obtenir une. + property var id_groupe_par_domaine: ({"circuits": 0, "batteries": 0, "capteurs": 0, "reservoirs": 0}) + readonly property int id_groupe: id_groupe_par_domaine[domaine_actif] - ListModel { id: modele_onglets } + function definir_id_groupe(type, valeur) { + var copie = id_groupe_par_domaine; + copie[type] = valeur; + id_groupe_par_domaine = copie; // réassignation complète : nécessaire pour déclencher la notification QML sur un objet JS + } + + // Un ListModel par domaine pour les onglets ET pour les éléments : + ListModel { id: modele_onglets_circuit } + ListModel { id: modele_onglets_batterie } + ListModel { id: modele_onglets_capteur } + ListModel { id: modele_onglets_reservoir } + property var modele_onglets_actuel: modele_onglets_circuit - // 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 + //-------------------------------------------------------------------------------------------------- + // Fonction de recherche du modèle d'onglet pour un domaine donné : + // Reçoit : le nom du domaine ("circuits", "batteries", "capteurs" ou "reservoirs") + // Retour : le modèle d'onglets correspondant au domaine + //-------------------------------------------------------------------------------------------------- + function modele_onglets_pour(type) { + switch (type) { + case "circuits": return modele_onglets_circuit; + case "batteries": return modele_onglets_batterie; + case "capteurs": return modele_onglets_capteur; + case "reservoirs": return modele_onglets_reservoir; + } + return null; + } + + //-------------------------------------------------------------------------------------------------- + // Fonction de recherche du modèle d'éléments pour un domaine donné : + // Reçoit : le nom du domaine ("circuits", "batteries", "capteurs" ou "reservoirs") + // Retour : le modèle d'onglets correspondant au domaine + //-------------------------------------------------------------------------------------------------- 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; + case "circuits": return modele_elements_circuit; + case "batteries": return modele_elements_batterie; + case "capteurs": return modele_elements_capteur; + case "reservoirs": return modele_elements_reservoir; } return null; } property DataProvider dataProvider: DataProvider {} - function charger_domaine(index) { + //-------------------------------------------------------------------------------------------------- + // Fonction de chargement des données liées à un domaine donné : + // Reçoit : l'index du domaine + // Retour : rien + //-------------------------------------------------------------------------------------------------- + function charger_domaine(index) + { domaine_actif_index = index; - id_groupe = 0; - modele_onglets.clear(); + modele_onglets_actuel = modele_onglets_pour(domaine_actif); modele_elements_actuel = modele_elements_pour(domaine_actif); - modele_elements_actuel.clear(); + // Pas de clear() ici volontairement : si ce domaine a déjà été chargé + // avec succès plus tôt (avant une éventuelle coupure), ses dernières + // données restent affichées jusqu'à ce qu'une nouvelle récupération + // réussisse - jamais vidées juste parce qu'on regarde ailleurs puis + // qu'on revient. dataProvider.get_familles(domaine_actif); } + // Connexions (déclenchées sur apparition d'un signal de DataProvider) : Connections { target: dataProvider - 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.append({ - "id": f.id, - "libelle": f.nom, - "etat": f.etat, - "actif": (f.id === id_groupe) - }); - if (id_groupe === 0 && i === 0) { - id_groupe = f.id; - dataProvider.get_elements(domaine_actif, id_groupe); + //-------------------------------------------------------------------------------------------------- + // Réception du la iste des familles : + // La demande a été faite préalablement par un 'dataProvider.get_familles(domaine_actif)' + //-------------------------------------------------------------------------------------------------- + function onFamillesRecues(type, familles) + { + var m = modele_onglets_pour(type); + m.clear(); + for (var i = 0; i < familles.length; i++) + { + // Traitement de la première famille à afficher : + if (id_groupe_par_domaine[type] === 0) + { + definir_id_groupe(type, familles[i].id); // On sélectionne la première récupérée + dataProvider.get_elements(type, familles[i].id); // Au passage, on demande la récupération des éléments de cette famille } + + // Ajout de la famille au modèle d'onglets : + m.append({ + "id": familles[i].id, + "libelle": familles[i].nom, + "etat": familles[i].etat, + "actif": (familles[i].id === id_groupe_par_domaine[type]) + }); } } - function onElementsRecus(type, famille, elements) { - if (type !== domaine_actif || famille !== id_groupe) return; + //-------------------------------------------------------------------------------------------------- + // Réception de la iste des éléments d'une famille : + // La demande a été faite préalablement par un 'dataProvider.get_elements(domaine_actif, famille_active)' + //-------------------------------------------------------------------------------------------------- + function onElementsRecus(type, famille, elements) + { + if (famille !== id_groupe_par_domaine[type]) return; // Réponse obsolète (famille changée entretemps pour ce domaine) + var m = modele_elements_pour(type); m.clear(); for (var j = 0; j < elements.length; j++) m.append(elements[j]); } - function onCircuitModifie(id, succes) { + //-------------------------------------------------------------------------------------------------- + // Réception de la réponse à une commande de circuit : + // La demande a été faite préalablement par un 'dataProvider.set_circuit(id_circuit, nouvelle_consigne)' + //-------------------------------------------------------------------------------------------------- + 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); + + if ((domaine_actif === "circuits") && (id_groupe > 0)) dataProvider.get_elements("circuits", id_groupe); // Rafraichissement de l'affichage } + //-------------------------------------------------------------------------------------------------- + // Réception d'une erreur du DataProvider : + //-------------------------------------------------------------------------------------------------- function onErreur(message) { console.log("DataProvider erreur : " + message); } } - Component.onCompleted: charger_domaine(0) + Component.onCompleted: charger_domaine(0) // A l'ouverture, chargement du premier domaine dans la liste (par défaut) + // Rafraîchissement périodique du domaine actuellement affiché. + // get_familles() est TOUJOURS retentée, même sans famille encore + // sélectionnée pour ce domaine (id_groupe===0) - c'est ce qui permet de + // se rétablir tout seul après une reconnexion, sans action de + // l'utilisateur, même pour un domaine qui n'avait encore jamais réussi + // à charger quoi que ce soit. Timer { interval: 1000 running: true repeat: true onTriggered: { - if (id_groupe > 0) { - dataProvider.get_elements(domaine_actif, id_groupe); - dataProvider.get_familles(domaine_actif); - } + dataProvider.get_familles(domaine_actif); + if (id_groupe > 0) dataProvider.get_elements(domaine_actif, id_groupe); } } + + //==================================================================================== + // Affichage : + //==================================================================================== ColumnLayout { anchors.fill: parent spacing: 10 + //----------------------------------------------------------- + // Bandeau hors ligne - permanent, ne disparaît que sur reconnexion + // réelle (dataProvider.connecte redevient true après un succès) : + Rectangle { + Layout.fillWidth: true + height: visible ? 40 : 0 + visible: !dataProvider.connecte + color: couleur_rouge_transparent + border.color: couleur_orange + border.width: 1 + + Text { + anchors.centerIn: parent + color: couleur_orange + font.pixelSize: 13 + font.bold: true + text: dataProvider.aDejaEteConnecte + ? qsTr("⚠ Bateau non connecté depuis le %1").arg( + Qt.formatDateTime(dataProvider.derniereConnexionReussie, "dd/MM/yyyy à HH:mm:ss")) + : qsTr("⚠ Bateau jamais connecté durant cette session") + } + } + //----------------------------------------------------------- // Sélecteur de domaine : Row { @@ -143,7 +244,9 @@ Shipheart_page { MouseArea { anchors.fill: parent - onClicked: charger_domaine(index) + onClicked: { + charger_domaine(index) + } } } } @@ -155,14 +258,14 @@ Shipheart_page { Layout.fillWidth: true height: shipheart_hauteur_bandeau_haut color: "transparent" - visible: modele_onglets.count > 1 + visible: modele_onglets_actuel.count > 1 GridView { id: grille_onglets anchors.fill: parent - cellWidth: width / Math.max(1, Math.min(modele_onglets.count, 5)) + cellWidth: width / Math.max(1, Math.min(modele_onglets_actuel.count, 5)) cellHeight: shipheart_hauteur_bandeau_haut - model: modele_onglets + model: modele_onglets_actuel clip: true delegate: Rectangle { @@ -184,11 +287,11 @@ Shipheart_page { MouseArea { anchors.fill: parent onClicked: { - 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)}); + definir_id_groupe(domaine_actif, model.id); + for (var k = 0; k < modele_onglets_actuel.count; k++) { + modele_onglets_actuel.set(k, {"actif": (modele_onglets_actuel.get(k).id === model.id)}); } - dataProvider.get_elements(domaine_actif, id_groupe); + dataProvider.get_elements(domaine_actif, model.id); } } } @@ -197,6 +300,8 @@ Shipheart_page { //----------------------------------------------------------- // Éléments de la famille sélectionnée, dans le domaine actif : + // (opacité réduite quand hors ligne - les valeurs affichées sont + // les dernières connues, plus garanties à jour, cf bandeau ci-dessus) GridView { id: elements_grid Layout.fillWidth: true @@ -205,8 +310,10 @@ Shipheart_page { cellHeight: height / 4 model: modele_elements_actuel clip: true + opacity: dataProvider.connecte ? 1.0 : 0.45 delegate: Rectangle { + id: bouton_element width: elements_grid.cellWidth - 4 height: elements_grid.cellHeight - 4 radius: 10 @@ -216,52 +323,94 @@ Shipheart_page { Column { anchors.centerIn: parent - spacing: 4 - Text { - text: model.nom - color: couleur_blanc_casse - font.pixelSize: 14 - font.bold: true - anchors.horizontalCenter: parent.horizontalCenter - } + Rectangle { + width: bouton_element.width + height: bouton_element.height / 3 + radius: 5 + color: "transparent" - Text { - 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 ""; - } + Text { + text: model.nom + color: couleur_blanc_casse + font.pixelSize: 16 + font.bold: true + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter } - 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 + Rectangle { + width: bouton_element.width + height: bouton_element.height / 3 + radius: 5 + color: "transparent" + + Text { + text: { + switch (domaine_actif) { + case "circuits": + return model.tension.toFixed(1) + " V | " + model.intensite.toFixed(2) + " A"; + case "batteries": + return model.niveau.toFixed(0) + " % | " + model.tension.toFixed(2) + " V"; + case "capteurs": + return model.valeur + (model.unite ? (" " + model.unite) : ""); + case "reservoirs": + return model.niveau.toFixed(0) + " % (" + model.capacite.toFixed(0) + " L)"; + default: + return ""; + } + } + color: couleur_blanc_casse + font.pixelSize: 12 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: bouton_element.width + height: bouton_element.height / 3 + radius: 5 + color: (model.consigne > 0) ? couleur_green_yellow_transparent : "transparent" + visible: (domaine_actif === "circuits") + Text { + + text: (model.consigne > 0) ? qsTr("On") : qsTr("Off") + color: (model.consigne > 0) ? couleur_green_yellow : couleur_blanc_casse + font.pixelSize: 18 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } } } MouseArea { anchors.fill: parent - enabled: (domaine_actif === "circuit") onClicked: { - var nouvelle_consigne = (model.consigne > 0) ? 0 : 100; - dataProvider.set_circuit(model.id, nouvelle_consigne); + // Clic court : navigation vers la page de détail, + // une par domaine (toutes construites maintenant). + switch (domaine_actif) { + case "capteurs": + stackView.push("detail_capteur.qml", {"idElement": model.id, "nomElement": model.nom}); + break; + case "reservoirs": + stackView.push("detail_reservoir.qml", {"idElement": model.id, "nomElement": model.nom}); + break; + case "circuits": + stackView.push("detail_circuit.qml", {"idElement": model.id, "nomElement": model.nom}); + break; + case "batteries": + stackView.push("detail_batterie.qml", {"idElement": model.id, "nomElement": model.nom}); + break; + } + } + onPressAndHold: { + // Appui long : bascule marche/arrêt (circuits uniquement). + if (domaine_actif === "circuits" && dataProvider.connecte) { + var nouvelle_consigne = (model.consigne > 0) ? 0 : 100; + dataProvider.set_circuit(model.id, nouvelle_consigne); + } } } } @@ -269,8 +418,10 @@ Shipheart_page { Label { anchors.centerIn: parent visible: elements_grid.count === 0 - text: qsTr("Aucun élément dans ce groupe.") - color: couleur_rouge_transparent + text: dataProvider.connecte + ? qsTr("Aucun élément dans ce groupe.") + : qsTr("Hors ligne - dernières données indisponibles pour ce groupe.") + color: couleur_orange_transparent } } } diff --git a/qml/home_OLD.qml b/qml/home_OLD.qml new file mode 100644 index 0000000..03d9e73 --- /dev/null +++ b/qml/home_OLD.qml @@ -0,0 +1,408 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +import qt.koriolan.shipheart 1.0 + +// ============================================================================ +// 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. +// +// Architecture : les ONGLETS et les ÉLÉMENTS ont chacun UN MODÈLE SÉPARÉ PAR +// DOMAINE (8 ListModel au total). Deux raisons distinctes : +// - Éléments : évite le piège des "rôles" QML figés (tension/intensite pour +// circuit, valeur/unite pour capteur... - cf notes historiques). +// - Onglets ET éléments : préserve les dernières données connues d'un +// domaine quand on le quitte puis qu'on y revient, y compris hors ligne - +// on ne vide JAMAIS un modèle avant d'avoir une nouvelle donnée réelle à +// afficher à la place (sinon : données qui "disparaissent" en changeant +// d'onglet, cf retours d'expérience). +// ============================================================================ + +Shipheart_page { + id: page_accueil + + readonly property var domaines: ["circuits", "batteries", "capteurs", "reservoirs"] + 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] + + readonly property int espace_onglets: 10 + readonly property int rayon_onglets: 20 + + // Famille sélectionnée, conservée INDÉPENDAMMENT par domaine - permet de + // revenir sur un domaine déjà visité sans perdre sa sélection, et permet + // au minuteur de savoir, pour CHAQUE domaine, s'il a déjà une famille + // active ou doit encore en obtenir une. + property var id_groupe_par_domaine: ({"circuits": 0, "batteries": 0, "capteurs": 0, "reservoirs": 0}) + readonly property int id_groupe: id_groupe_par_domaine[domaine_actif] + + function definir_id_groupe(type, valeur) { + var copie = id_groupe_par_domaine; + copie[type] = valeur; + id_groupe_par_domaine = copie; // réassignation complète : nécessaire pour déclencher la notification QML sur un objet JS + } + + // Un ListModel par domaine pour les onglets ET pour les éléments : + ListModel { id: modele_onglets_circuit } + ListModel { id: modele_onglets_batterie } + ListModel { id: modele_onglets_capteur } + ListModel { id: modele_onglets_reservoir } + property var modele_onglets_actuel: modele_onglets_circuit + + 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 + + //-------------------------------------------------------------------------------------------------- + // Fonction de recherche du modèle d'onglet pour un domaine donné : + // Reçoit : le nom du domaine ("circuits", "batteries", "capteurs" ou "reservoirs") + // Retour : le modèle d'onglets correspondant au domaine + //-------------------------------------------------------------------------------------------------- + function modele_onglets_pour(type) { + switch (type) { + case "circuits": return modele_onglets_circuit; + case "batteries": return modele_onglets_batterie; + case "capteurs": return modele_onglets_capteur; + case "reservoirs": return modele_onglets_reservoir; + } + return null; + } + + //-------------------------------------------------------------------------------------------------- + // Fonction de recherche du modèle d'éléments pour un domaine donné : + // Reçoit : le nom du domaine ("circuits", "batteries", "capteurs" ou "reservoirs") + // Retour : le modèle d'onglets correspondant au domaine + //-------------------------------------------------------------------------------------------------- + function modele_elements_pour(type) { + switch (type) { + case "circuits": return modele_elements_circuit; + case "batteries": return modele_elements_batterie; + case "capteurs": return modele_elements_capteur; + case "reservoirs": return modele_elements_reservoir; + } + return null; + } + + property DataProvider dataProvider: DataProvider {} + + //-------------------------------------------------------------------------------------------------- + // Fonction de chargement des données liées à un domaine donné : + // Reçoit : l'index du domaine + // Retour : rien + //-------------------------------------------------------------------------------------------------- + function charger_domaine(index) + { + domaine_actif_index = index; + modele_onglets_actuel = modele_onglets_pour(domaine_actif); + modele_elements_actuel = modele_elements_pour(domaine_actif); + // Pas de clear() ici volontairement : si ce domaine a déjà été chargé + // avec succès plus tôt (avant une éventuelle coupure), ses dernières + // données restent affichées jusqu'à ce qu'une nouvelle récupération + // réussisse - jamais vidées juste parce qu'on regarde ailleurs puis + // qu'on revient. + dataProvider.get_familles(domaine_actif); + } + + // Connexions (déclenchées sur apparition d'un signal de DataProvider) : + Connections { + target: dataProvider + + //-------------------------------------------------------------------------------------------------- + // Réception du la iste des familles : + // La demande a été faite préalablement par un 'dataProvider.get_familles(domaine_actif)' + //-------------------------------------------------------------------------------------------------- + function onFamillesRecues(type, familles) + { + var m = modele_onglets_pour(type); + m.clear(); + for (var i = 0; i < familles.length; i++) + { + // Traitement de la première famille à afficher : + if (id_groupe_par_domaine[type] === 0) + { + definir_id_groupe(type, familles[i].id); // On sélectionne la première récupérée + dataProvider.get_elements(type, familles[i].id); // Au passage, on demande la récupération des éléments de cette famille + } + + // Ajout de la famille au modèle d'onglets : + m.append({ + "id": familles[i].id, + "libelle": familles[i].nom, + "etat": familles[i].etat, + "actif": (familles[i].id === id_groupe_par_domaine[type]) + }); + } + } + + //-------------------------------------------------------------------------------------------------- + // Réception de la iste des éléments d'une famille : + // La demande a été faite préalablement par un 'dataProvider.get_elements(domaine_actif, famille_active)' + //-------------------------------------------------------------------------------------------------- + function onElementsRecus(type, famille, elements) + { + if (famille !== id_groupe_par_domaine[type]) return; // Réponse obsolète (famille changée entretemps pour ce domaine) + + var m = modele_elements_pour(type); + m.clear(); + for (var j = 0; j < elements.length; j++) m.append(elements[j]); + } + + //-------------------------------------------------------------------------------------------------- + // Réception de la réponse à une commande de circuit : + // La demande a été faite préalablement par un 'dataProvider.set_circuit(id_circuit, nouvelle_consigne)' + //-------------------------------------------------------------------------------------------------- + function onCircuitModifie(id, succes) + { + if (!succes) console.log("Échec de la commande sur le circuit " + id); + + if ((domaine_actif === "circuits") && (id_groupe > 0)) dataProvider.get_elements("circuits", id_groupe); // Rafraichissement de l'affichage + } + + //-------------------------------------------------------------------------------------------------- + // Réception d'une erreur du DataProvider : + //-------------------------------------------------------------------------------------------------- + function onErreur(message) { + console.log("DataProvider erreur : " + message); + } + } + + Component.onCompleted: charger_domaine(0) // A l'ouverture, chargement du premier domaine dans la liste (par défaut) + + // Rafraîchissement périodique du domaine actuellement affiché. + // get_familles() est TOUJOURS retentée, même sans famille encore + // sélectionnée pour ce domaine (id_groupe===0) - c'est ce qui permet de + // se rétablir tout seul après une reconnexion, sans action de + // l'utilisateur, même pour un domaine qui n'avait encore jamais réussi + // à charger quoi que ce soit. + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + dataProvider.get_familles(domaine_actif); + if (id_groupe > 0) dataProvider.get_elements(domaine_actif, id_groupe); + } + } + + + //==================================================================================== + // Affichage : + //==================================================================================== + ColumnLayout { + anchors.fill: parent + spacing: 10 + + //----------------------------------------------------------- + // Bandeau hors ligne - permanent, ne disparaît que sur reconnexion + // réelle (dataProvider.connecte redevient true après un succès) : + Rectangle { + Layout.fillWidth: true + height: visible ? 40 : 0 + visible: !dataProvider.connecte + color: couleur_rouge_transparent + border.color: couleur_orange + border.width: 1 + + Text { + anchors.centerIn: parent + color: couleur_orange + font.pixelSize: 13 + font.bold: true + text: dataProvider.aDejaEteConnecte + ? qsTr("⚠ Bateau non connecté depuis le %1").arg( + Qt.formatDateTime(dataProvider.derniereConnexionReussie, "dd/MM/yyyy à HH:mm:ss")) + : qsTr("⚠ Bateau jamais connecté durant cette session") + } + } + + //----------------------------------------------------------- + // 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 { + Layout.fillWidth: true + height: shipheart_hauteur_bandeau_haut + color: "transparent" + visible: modele_onglets_actuel.count > 1 + + GridView { + id: grille_onglets + anchors.fill: parent + cellWidth: width / Math.max(1, Math.min(modele_onglets_actuel.count, 5)) + cellHeight: shipheart_hauteur_bandeau_haut + model: modele_onglets_actuel + clip: true + + delegate: Rectangle { + 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 + anchors.centerIn: parent + color: couleur_blanc_casse + font.pixelSize: 14 + font.bold: model.actif + } + + MouseArea { + anchors.fill: parent + onClicked: { + definir_id_groupe(domaine_actif, model.id); + for (var k = 0; k < modele_onglets_actuel.count; k++) { + modele_onglets_actuel.set(k, {"actif": (modele_onglets_actuel.get(k).id === model.id)}); + } + dataProvider.get_elements(domaine_actif, model.id); + } + } + } + } + } + + //----------------------------------------------------------- + // Éléments de la famille sélectionnée, dans le domaine actif : + // (opacité réduite quand hors ligne - les valeurs affichées sont + // les dernières connues, plus garanties à jour, cf bandeau ci-dessus) + GridView { + id: elements_grid + Layout.fillWidth: true + Layout.fillHeight: true + cellWidth: width / 2 + cellHeight: height / 4 + model: modele_elements_actuel + clip: true + opacity: dataProvider.connecte ? 1.0 : 0.45 + + delegate: Rectangle { + id: bouton_element + 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" + border.width: 2 + + Column { + anchors.centerIn: parent + + Rectangle { + width: bouton_element.width + height: bouton_element.height / 3 + radius: 5 + color: "transparent" + + Text { + text: model.nom + color: couleur_blanc_casse + font.pixelSize: 16 + font.bold: true + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: bouton_element.width + height: bouton_element.height / 3 + radius: 5 + color: "transparent" + + Text { + text: { + switch (domaine_actif) { + case "circuits": + return model.tension.toFixed(1) + " V | " + model.intensite.toFixed(2) + " A"; + case "batteries": + return model.niveau.toFixed(0) + " % | " + model.tension.toFixed(2) + " V"; + case "capteurs": + return model.valeur + (model.unite ? (" " + model.unite) : ""); + case "reservoirs": + return model.niveau.toFixed(0) + " % (" + model.capacite.toFixed(0) + " L)"; + default: + return ""; + } + } + color: couleur_blanc_casse + font.pixelSize: 12 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: bouton_element.width + height: bouton_element.height / 3 + radius: 5 + color: (model.consigne > 0) ? couleur_green_yellow_transparent : "transparent" + visible: (domaine_actif === "circuits") + Text { + + text: (model.consigne > 0) ? qsTr("On") : qsTr("Off") + color: (model.consigne > 0) ? couleur_green_yellow : couleur_blanc_casse + font.pixelSize: 18 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + + MouseArea { + anchors.fill: parent + enabled: (domaine_actif === "circuits") && dataProvider.connecte + onClicked: { + var nouvelle_consigne = (model.consigne > 0) ? 0 : 100; + dataProvider.set_circuit(model.id, nouvelle_consigne); + } + } + } + + Label { + anchors.centerIn: parent + visible: elements_grid.count === 0 + text: dataProvider.connecte + ? qsTr("Aucun élément dans ce groupe.") + : qsTr("Hors ligne - dernières données indisponibles pour ce groupe.") + color: couleur_orange_transparent + } + } + } +}