4 pages de détail avec courbe (Canvas), navigation depuis home.qml, sélecteurs de variable pour circuits/batteries
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+239
-88
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user