Files
shipheart-api/main_OLD.cpp
T

657 lines
32 KiB
C++

#include <QCoreApplication>
#include <QHttpServer>
#include <QHttpHeaders>
#include <QTcpServer>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUrlQuery>
#include <QDebug>
// ============================================================================
// shipheart_api - Phase 4 : première tranche verticale, domaine "circuit"
// ----------------------------------------------------------------------------
// Deux endpoints réels, remplaçant le spike (/ping conservé comme sonde de vie
// générique, /test-db retiré car superflu désormais) :
// - GET /elements/familles?type=circuit
// - GET /elements?type=circuit&famille=<id>
// Logique et requêtes SQL reprises à l'identique de home.qml (init_circuits(),
// mise_a_jour_onglets_circuits(), mise_a_jour_circuits() - cf synthèse §20.4)
// et des constantes documentées dans le schéma (TB_Circuits.Etat, cf §20).
// ============================================================================
// Types de familles (cf TB_Familles.Type_source / TB_Events.Type_source) :
static const int TYPE_SOURCE_CIRCUIT = 1;
static const int TYPE_SOURCE_CAPTEUR = 2;
static const int TYPE_SOURCE_RESERVOIR = 3;
static const int TYPE_SOURCE_BATTERIE = 4;
// États circuit (cf commentaire colonne TB_Circuits.Etat dans le schéma) :
static const int ETAT_CIRCUIT_OK = 0;
static const int ETAT_CIRCUIT_LIMITATION = 1;
static const int ETAT_CIRCUIT_PROTECTION = 2;
static const int ETAT_CIRCUIT_DISJONCTE = 3;
static const int ETAT_CIRCUIT_ABSENCE_TERRE = 8;
static const int ETAT_CIRCUIT_ABSENCE_MASSE = 9;
static const int ETAT_COMMANDE_CARTE_OK = 0;
// Types d'entrées capteur (cf commentaire colonne TB_Capteurs.Type_entrees) :
static const int TYPE_ENTREES_AUX = 1;
static const int TYPE_ENTREES_NMEA = 7;
static const int TYPE_ENTREES_CALCULEE_BOOLEEN = 20;
// Types de conversion d'unité (cf commentaire colonne TB_Variables.Conversion_unite,
// confirmé identique à shipheart_type_unite_* dans main.qml) :
static const int TYPE_UNITE_VOLUME = 1;
static const int TYPE_UNITE_TEMPERATURE = 2;
static const int TYPE_UNITE_LATITUDE = 3;
static const int TYPE_UNITE_LONGITUDE = 4;
// Unités cibles (cf main.qml, shipheart_unite_volume_*/shipheart_unite_temperature_*) :
static const int UNITE_VOLUME_LITRES = 0;
static const int UNITE_VOLUME_US_GALLONS = 1;
static const int UNITE_VOLUME_IMP_GALLONS = 2;
static const int UNITE_TEMPERATURE_CELSIUS = 0;
static const int UNITE_TEMPERATURE_FAHRENHEIT = 1;
// ----------------------------------------------------------------------------
// Conversions - traduction fidèle de fonctions.js (conversion_volume(),
// conversion_temperature(), chaine_latitude(), chaine_longitude()) et de
// fonctions_localisation.js (symbole_unite_volume(), symbole_unite_temperature()).
// ----------------------------------------------------------------------------
static double conversion_volume(double val, int unite)
{
switch (unite) {
case UNITE_VOLUME_US_GALLONS: return val / 3.785411784;
case UNITE_VOLUME_IMP_GALLONS: return val / 4.546090;
case UNITE_VOLUME_LITRES:
default: return val;
}
}
static double conversion_temperature(double val, int unite)
{
switch (unite) {
case UNITE_TEMPERATURE_FAHRENHEIT: return (val * 1.8) + 32.0;
case UNITE_TEMPERATURE_CELSIUS:
default: return val;
}
}
static QString symbole_unite_volume(int unite)
{
switch (unite) {
case UNITE_VOLUME_US_GALLONS: return QStringLiteral("US Ga");
case UNITE_VOLUME_IMP_GALLONS: return QStringLiteral("Imp. Ga");
case UNITE_VOLUME_LITRES:
default: return QStringLiteral("lit.");
}
}
static QString symbole_unite_temperature(int unite)
{
return (unite == UNITE_TEMPERATURE_FAHRENHEIT) ? QStringLiteral("°F") : QStringLiteral("°C");
}
static QString chaine_latitude(double lat)
{
const int deg = static_cast<int>(lat);
const double minutes = (lat - deg) * 60.0;
const QString suffixe = (lat >= 0) ? QStringLiteral("N") : QStringLiteral("S");
return QString("%1°%2' %3").arg(deg).arg(minutes, 0, 'f', 3).arg(suffixe);
}
static QString chaine_longitude(double lon)
{
const int deg = static_cast<int>(lon);
const double minutes = (lon - deg) * 60.0;
const QString suffixe = (lon >= 0) ? QStringLiteral("E") : QStringLiteral("W");
return QString("%1°%2' %3").arg(deg).arg(minutes, 0, 'f', 3).arg(suffixe);
}
// ----------------------------------------------------------------------------
// Ajoute l'en-tête CORS à une réponse - indispensable puisque le client WASM
// (servi par nginx sur un port distinct) et cette API (port 8080) constituent
// deux origines différentes du point de vue du navigateur. Sans cet en-tête,
// les requêtes aboutissent bien côté serveur (visibles dans les logs) mais
// leur réponse est bloquée silencieusement côté client.
// TODO : restreindre à l'origine réelle du client en production plutôt que
// '*', une fois l'authentification (cf "Architecture cible") en place.
// ----------------------------------------------------------------------------
static QHttpServerResponse withCors(QHttpServerResponse &&resp)
{
QHttpHeaders headers = resp.headers();
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowOrigin, "*");
// Empêche le navigateur de mettre en cache les réponses - sans ça, des
// requêtes identiques répétées (rafraîchissement périodique) pourraient
// recevoir une réponse mise en cache plutôt que la valeur réellement à
// jour en base.
headers.append(QHttpHeaders::WellKnownHeader::CacheControl, "no-store");
resp.setHeaders(std::move(headers));
return std::move(resp);
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// --- Connexion MariaDB locale ---
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("localhost");
db.setUserName("koriolan");
db.setPassword("Mp7Hgf12bV9zm");
db.setDatabaseName("shipheart");
if (!db.open()) {
qCritical() << "Connexion MariaDB échouée :" << db.lastError().text();
return 1;
}
qInfo() << "Connexion MariaDB réussie.";
QHttpServer server;
// Sonde de vie générique, sans BDD :
server.route("/ping", []() {
QJsonObject obj;
obj["status"] = "ok";
return withCors(QHttpServerResponse(QJsonDocument(obj).toJson()));
});
// ------------------------------------------------------------------
// GET /elements/familles?type=circuit
// Liste des familles avec état d'alarme agrégé (fusionne les deux
// requêtes onglets de mise_a_jour_onglets_circuits()).
// ------------------------------------------------------------------
server.route("/elements/familles", [](const QHttpServerRequest &request) {
QUrlQuery query(request.query());
const QString type = query.queryItemValue("type");
int typeSource;
if (type == "circuits") typeSource = TYPE_SOURCE_CIRCUIT;
else if (type == "batteries") typeSource = TYPE_SOURCE_BATTERIE;
else if (type == "capteurs") typeSource = TYPE_SOURCE_CAPTEUR;
else if (type == "reservoirs") typeSource = TYPE_SOURCE_RESERVOIR;
else {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "type non supporté (circuit, batterie, capteur, reservoir)";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotImplemented));
}
QJsonArray familles;
QSqlQuery q1(QString("SELECT Cle_primaire, Nom FROM TB_Familles WHERE Type_source=%1 ORDER BY Priorite_affichage").arg(typeSource));
while (q1.next()) {
const int idFamille = q1.value(0).toInt();
const QString nom = q1.value(1).toString();
int etat = 0;
if (type == "circuits") {
// Alarmes (Events) liées à cette famille :
QSqlQuery q2;
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Circuits WHERE ID_Famille=:famille)");
q2.bindValue(":type", typeSource);
q2.bindValue(":famille", idFamille);
q2.exec();
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
// État agrégé des circuits de la famille :
QSqlQuery q3;
q3.prepare("SELECT Etat, Etat_commande_carte, Etat_systeme FROM TB_Circuits "
"WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0");
q3.bindValue(":famille", idFamille);
q3.exec();
while (q3.next()) {
const int etatCircuit = q3.value(0).toInt();
const int etatCommandeCarte = q3.value(1).toInt();
const int etatSysteme = q3.value(2).toInt();
if (etatSysteme != 0
|| etatCircuit == ETAT_CIRCUIT_LIMITATION
|| etatCircuit == ETAT_CIRCUIT_PROTECTION
|| etatCircuit == ETAT_CIRCUIT_DISJONCTE
|| etatCircuit == ETAT_CIRCUIT_ABSENCE_TERRE
|| etatCircuit == ETAT_CIRCUIT_ABSENCE_MASSE) {
etat = 2;
} else if (etat < 2 && (etatCommandeCarte != ETAT_COMMANDE_CARTE_OK || etatCircuit != ETAT_CIRCUIT_OK)) {
etat = 1;
}
}
} else if (type == "batteries") {
// Règle plus simple que les circuits (cf §20.4/§21.8 synthèse) :
// rouge si Etat_systeme en défaut sur une batterie de la famille,
// sinon orange si au moins une alarme (Events), sinon vert.
// Alarmes (Events) liées à cette famille :
QSqlQuery q2;
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Batteries WHERE ID_Famille=:famille)");
q2.bindValue(":type", typeSource);
q2.bindValue(":famille", idFamille);
q2.exec();
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
// Etat_systeme des batteries de la famille :
QSqlQuery q3;
q3.prepare("SELECT Etat_systeme FROM TB_Batteries WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0");
q3.bindValue(":famille", idFamille);
q3.exec();
while (q3.next()) {
if (q3.value(0).toInt() != 0) etat = 2;
}
} else if (type == "capteurs") {
// Période d'obsolescence NMEA (TB_Configuration) :
int periodeObsolescenceNmea = 0;
{
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Periode_obsolescence_capteur_nmea'");
if (qc.next()) periodeObsolescenceNmea = qc.value(0).toInt();
}
// Alarmes (Events) liées à cette famille :
QSqlQuery q2;
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Capteurs WHERE ID_Famille=:famille)");
q2.bindValue(":type", typeSource);
q2.bindValue(":famille", idFamille);
q2.exec();
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
QSqlQuery q3;
q3.prepare(
"SELECT Etat, Etat_systeme, "
"IF(Type_entrees=:typeNmea, (SELECT (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(Date_maj)) FROM TB_Nmea2000 "
"WHERE Pgn=SUBSTRING_INDEX(Entrees, ':', 1) AND Field=SUBSTRING_INDEX(Entrees, ':', -1)), 0) AS Age "
"FROM TB_Capteurs WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0"
);
q3.bindValue(":typeNmea", TYPE_ENTREES_NMEA);
q3.bindValue(":famille", idFamille);
q3.exec();
while (q3.next()) {
const int etatCapteur = q3.value(0).toInt();
const int etatSysteme = q3.value(1).toInt();
const int age = q3.value(2).toInt();
if (age > periodeObsolescenceNmea && etat < 1) etat = 1;
if ((etatCapteur > 0 || etatSysteme != 0) && etat < 2) etat = 2;
}
} else if (type == "reservoirs") {
// Alarmes (Events) liées à cette famille :
QSqlQuery q2;
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Reservoirs WHERE ID_Famille=:famille)");
q2.bindValue(":type", typeSource);
q2.bindValue(":famille", idFamille);
q2.exec();
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
// Défaut ordre des sondes AUX (Etat_entrees) ou Etat_systeme en défaut :
QSqlQuery q3;
q3.prepare("SELECT Etat_entrees, Etat_systeme FROM TB_Reservoirs "
"WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0");
q3.bindValue(":famille", idFamille);
q3.exec();
while (q3.next()) {
const int etatEntrees = q3.value(0).toInt();
const int etatSysteme = q3.value(1).toInt();
if ((etatEntrees > 0 || etatSysteme != 0) && etat < 2) etat = 2;
}
}
QJsonObject obj;
obj["id"] = idFamille;
obj["nom"] = nom;
obj["etat"] = etat;
familles.append(obj);
}
QJsonObject reponse;
reponse["status"] = "ok";
reponse["familles"] = familles;
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
});
// ------------------------------------------------------------------
// GET /elements?type=circuit&famille=<id>
// Liste des circuits de la famille - sert chargement initial ET
// rafraîchissement périodique (même requête que init_circuits() et
// mise_a_jour_circuits(), cf §20.4 : ce n'est pas deux besoins
// différents, mais un seul, appelé deux fois).
// ------------------------------------------------------------------
server.route("/elements", [](const QHttpServerRequest &request) {
QUrlQuery query(request.query());
const QString type = query.queryItemValue("type");
const QString familleStr = query.queryItemValue("famille");
if (type != "circuits" && type != "batteries" && type != "capteurs" && type != "reservoirs") {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "type non supporté (circuit, batterie, capteur, reservoir)";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotImplemented));
}
bool ok = false;
const int idFamille = familleStr.toInt(&ok);
if (!ok) {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "paramètre 'famille' manquant ou invalide";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
}
QJsonArray elements;
if (type == "circuits") {
QSqlQuery q;
q.prepare(
"SELECT Cle_primaire, Etat_systeme, Nom, Etat_commande_carte, Mode_commande, Etat, Consigne, Old_consigne, "
"Mode_test, Tension, Intensite, Frequence, "
"(SELECT Type FROM TB_Modules_config WHERE Adresse_k_bus=TB_Circuits.Adresse_module) AS type_circuit, "
"No_circuit, "
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Visibilite=1 AND "
"(Action_circuits LIKE CONCAT(TB_Circuits.Cle_primaire,'/%') OR Action_circuits LIKE CONCAT('%;', TB_Circuits.Cle_primaire,'/%'))) AS nb_events, "
"Validation_cdes_events, "
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:type AND Alarme_source>0 AND Etat_test=1 "
"AND ID_Source=TB_Circuits.Cle_primaire) AS Nb_alarmes "
"FROM TB_Circuits WHERE Priorite_affichage_accueil>0 AND ID_Famille=:famille "
"ORDER BY Priorite_affichage_accueil, Cle_primaire"
);
q.bindValue(":type", TYPE_SOURCE_CIRCUIT);
q.bindValue(":famille", idFamille);
q.exec();
while (q.next()) {
QJsonObject obj;
obj["id"] = q.value(0).toInt();
obj["etat_systeme"] = q.value(1).toInt();
obj["nom"] = q.value(2).toString();
obj["etat_commande_carte"] = q.value(3).toInt();
obj["mode_commande"] = q.value(4).toInt();
obj["etat"] = q.value(5).toInt();
obj["consigne"] = q.value(6).toInt();
obj["old_consigne"] = q.value(7).toInt();
obj["mode_test"] = q.value(8).toInt();
obj["tension"] = q.value(9).toDouble();
obj["intensite"] = q.value(10).toDouble();
obj["frequence"] = q.value(11).toDouble();
obj["type_circuit"] = q.value(12).toInt();
obj["no_circuit"] = q.value(13).toInt();
obj["validation_cdes_events"] = (q.value(14).toInt() == 0) ? -1 : q.value(15).toInt();
obj["etat_events"] = q.value(16).toInt();
elements.append(obj);
}
} else if (type == "batteries") {
// Rappel : le champ "etat" retourné ici est le NOMBRE D'ALARMES,
// pas un code d'état comme pour les circuits - fidèle au modèle
// natif existant (modele_batteries), pas une incohérence introduite ici.
QSqlQuery q;
q.prepare(
"SELECT Cle_primaire, Etat_systeme, Nom, Type, Soc, Tension, Tension_nominale, "
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:type AND Alarme_source>0 AND Etat_test=1 "
"AND ID_Source=TB_Batteries.Cle_primaire) AS Nb_alarmes "
"FROM TB_Batteries WHERE ID_Famille=:famille "
"ORDER BY Priorite_affichage_accueil"
);
q.bindValue(":type", TYPE_SOURCE_BATTERIE);
q.bindValue(":famille", idFamille);
q.exec();
while (q.next()) {
QJsonObject obj;
obj["id"] = q.value(0).toInt();
obj["etat_systeme"] = q.value(1).toInt();
obj["nom"] = q.value(2).toString();
obj["type_batterie"] = q.value(3).toInt();
obj["niveau"] = q.value(4).toDouble();
obj["tension"] = q.value(5).toDouble();
obj["tension_nominale"] = q.value(6).toDouble();
obj["etat"] = q.value(7).toInt();
elements.append(obj);
}
} else if (type == "capteurs") {
// Préférences d'unité du bateau (TB_Configuration) :
int uniteVolume = 0, uniteTemperature = 0, periodeObsolescenceNmea = 0;
{
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Unite_volume'");
if (qc.next()) uniteVolume = qc.value(0).toInt();
}
{
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Unite_temperature'");
if (qc.next()) uniteTemperature = qc.value(0).toInt();
}
{
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Periode_obsolescence_capteur_nmea'");
if (qc.next()) periodeObsolescenceNmea = qc.value(0).toInt();
}
QSqlQuery q;
q.prepare(
"SELECT Cle_primaire, Etat_systeme, Nom, Type_entrees, Entrees, ID_Variable, Etat, "
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:typeSource AND Alarme_source>0 AND Etat_test=1 "
"AND ID_Source=TB_Capteurs.Cle_primaire) AS Nb_alarmes, "
"Valeur, Precision_affichage_accueil, Liste_valeurs_textes, "
"IF(Type_entrees=:typeNmea, (SELECT (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(Date_maj)) FROM TB_Nmea2000 "
"WHERE Pgn=SUBSTRING_INDEX(Entrees, ':', 1) AND Field=SUBSTRING_INDEX(Entrees, ':', -1)), 0) AS Age "
"FROM TB_Capteurs WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0 "
"ORDER BY Priorite_affichage_accueil, Nom"
);
q.bindValue(":typeSource", TYPE_SOURCE_CAPTEUR);
q.bindValue(":typeNmea", TYPE_ENTREES_NMEA);
q.bindValue(":famille", idFamille);
q.exec();
while (q.next()) {
const int id = q.value(0).toInt();
const int etatSysteme = q.value(1).toInt();
const QString nom = q.value(2).toString();
const int typeEntrees = q.value(3).toInt();
const int idVariable = q.value(5).toInt();
const int etatCapteur = q.value(6).toInt();
const int nbAlarmes = q.value(7).toInt();
const double valeurBrute = q.value(8).toDouble();
const int precision = q.value(9).toInt();
const QString listeValeursTextes = q.value(10).toString();
const int age = q.value(11).toInt();
// Résolution de l'unité/conversion via TB_Variables :
QSqlQuery qv;
qv.prepare("SELECT Unite, Conversion_unite FROM TB_Variables WHERE Cle_primaire=:id");
qv.bindValue(":id", idVariable);
qv.exec();
QString uniteBrute;
int typeUnite = 0;
if (qv.next()) {
uniteBrute = qv.value(0).toString();
typeUnite = qv.value(1).toInt();
}
QJsonObject obj;
obj["id"] = id;
obj["etat_systeme"] = etatSysteme;
obj["nom"] = nom;
obj["etat"] = etatCapteur;
obj["etat_events"] = nbAlarmes;
if (typeUnite == TYPE_UNITE_LATITUDE) {
obj["valeur"] = chaine_latitude(valeurBrute);
obj["valeur_texte"] = true;
obj["unite"] = QString();
} else if (typeUnite == TYPE_UNITE_LONGITUDE) {
obj["valeur"] = chaine_longitude(valeurBrute);
obj["valeur_texte"] = true;
obj["unite"] = QString();
} else {
// Cas des listes de valeurs/textes (entrées AUX ou booléen calculé) :
QString texteListe;
if (typeEntrees == TYPE_ENTREES_AUX || typeEntrees == TYPE_ENTREES_CALCULEE_BOOLEEN) {
const auto paires = listeValeursTextes.split(';');
for (const auto &paire : paires) {
const auto champs = paire.split('/');
if (champs.size() == 2 && champs[0].toDouble() == valeurBrute) {
texteListe = champs[1];
break;
}
}
}
if (!texteListe.isEmpty()) {
obj["valeur"] = texteListe;
obj["valeur_texte"] = true;
obj["unite"] = QString();
} else {
double valeurConvertie = valeurBrute;
QString symboleUnite = uniteBrute;
if (typeUnite == TYPE_UNITE_VOLUME) {
valeurConvertie = conversion_volume(valeurBrute, uniteVolume);
symboleUnite = symbole_unite_volume(uniteVolume);
} else if (typeUnite == TYPE_UNITE_TEMPERATURE) {
valeurConvertie = conversion_temperature(valeurBrute, uniteTemperature);
symboleUnite = symbole_unite_temperature(uniteTemperature);
}
// Toujours une chaîne (même pour les nombres) - évite que QML
// ne fige le rôle 'valeur' en type numérique lors d'une famille
// visitée avant "Position" (latitude/longitude), qui tronquerait
// alors ces dernières à leur seul chiffre de tête.
obj["valeur"] = QString::number(valeurConvertie, 'f', precision);
obj["valeur_texte"] = false;
obj["unite"] = symboleUnite;
}
}
obj["obsolete"] = (typeEntrees == TYPE_ENTREES_NMEA && age > periodeObsolescenceNmea);
elements.append(obj);
}
} else if (type == "reservoirs") {
QSqlQuery q;
q.prepare(
"SELECT Cle_primaire, Nom, Etat_systeme, Niveau, Capacite, Logique, "
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:type AND Alarme_source>0 AND Etat_test=1 "
"AND ID_Source=TB_Reservoirs.Cle_primaire) AS Nb_alarmes, "
"Etat_entrees "
"FROM TB_Reservoirs WHERE ID_Famille=:famille "
"ORDER BY Priorite_affichage_accueil"
);
q.bindValue(":type", TYPE_SOURCE_RESERVOIR);
q.bindValue(":famille", idFamille);
q.exec();
while (q.next()) {
const int etatSysteme = q.value(2).toInt();
const int nbAlarmes = q.value(6).toInt();
const int etatEntrees = q.value(7).toInt();
// État agrégé, même règle que le natif (mise_a_jour_reservoirs()) :
// rouge si Etat_systeme en défaut ou défaut d'ordre des sondes AUX,
// sinon orange si au moins une alarme, sinon vert.
int etat = 0;
if (etatSysteme < 0 || etatEntrees > 0) etat = 2;
else if (nbAlarmes > 0) etat = 1;
QJsonObject obj;
obj["id"] = q.value(0).toInt();
obj["nom"] = q.value(1).toString();
obj["etat_systeme"] = etatSysteme;
obj["niveau"] = q.value(3).toDouble();
obj["capacite"] = q.value(4).toDouble();
obj["logique"] = q.value(5).toInt();
obj["etat_events"] = nbAlarmes;
obj["etat_entrees"] = etatEntrees;
obj["etat"] = etat;
elements.append(obj);
}
}
QJsonObject reponse;
reponse["status"] = "ok";
reponse["elements"] = elements;
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
});
// ------------------------------------------------------------------
// PATCH /elements/circuit/<id>
// Écriture rapide (bouton d'accueil) - PAS le formulaire de config
// complet (config_circuit.qml, avec orchestration esclave/maître,
// §20.7) qui reste un futur endpoint séparé. Reproduit fidèlement
// changement_consigne()/toggle_cdes_auto() (identiques dans circuit.qml
// et Shipheart_bouton_circuit.qml).
// ------------------------------------------------------------------
server.route("/elements/circuit/<arg>", QHttpServerRequest::Method::Patch,
[](int idCircuit, const QHttpServerRequest &request) {
const QJsonDocument body = QJsonDocument::fromJson(request.body());
if (!body.isObject()) {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "corps de requête JSON invalide";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
}
const QJsonObject obj = body.object();
bool ecritureEffectuee = false;
// Changement de consigne :
if (obj.contains("consigne")) {
int consigne = obj["consigne"].toInt();
if (consigne < 0) consigne = 0;
if (consigne > 100) consigne = 100;
QString req = QString("UPDATE TB_Circuits SET New_consigne=%1 ").arg(consigne);
if (obj.contains("old_consigne")) {
req += QString(", Old_consigne=%1 ").arg(obj["old_consigne"].toInt());
}
req += QString("WHERE Cle_primaire='%1'").arg(idCircuit);
QSqlQuery q(req);
ecritureEffectuee = true;
}
// Bascule du mode "commandes automatiques" (Events) :
if (obj.contains("validation_cdes_events")) {
const int mode = obj["validation_cdes_events"].toInt() != 0 ? 1 : 0;
QSqlQuery q(QString("UPDATE TB_Circuits SET Validation_cdes_events=%1 WHERE Cle_primaire='%2'").arg(mode).arg(idCircuit));
ecritureEffectuee = true;
}
if (!ecritureEffectuee) {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "aucun champ reconnu (attendu : consigne, validation_cdes_events)";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
}
// Signale à l'Appli qu'un changement est en attente, pour un
// traitement rapide (100ms maxi) plutôt que son cycle normal
// (cf synchro_bdd(shipheart_type_synchro_data) du natif) :
QSqlQuery("UPDATE TB_Systeme SET Valeur=1 WHERE Nom=\"Synchro\"");
QJsonObject reponse;
reponse["status"] = "ok";
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
});
// Réponse au "preflight" CORS (requête OPTIONS automatique du navigateur
// avant tout PATCH avec Content-Type: application/json) - sans elle, le
// navigateur abandonne silencieusement la vraie requête PATCH avant même
// de l'envoyer.
server.route("/elements/circuit/<arg>", QHttpServerRequest::Method::Options,
[](int) {
QHttpServerResponse resp(QHttpServerResponder::StatusCode::NoContent);
QHttpHeaders headers = resp.headers();
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowOrigin, "*");
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowMethods, "GET, PATCH, OPTIONS");
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowHeaders, "Content-Type");
resp.setHeaders(std::move(headers));
return resp;
});
auto tcpServer = new QTcpServer();
if (!tcpServer->listen(QHostAddress::Any, 8080) || !server.bind(tcpServer)) {
qCritical() << "Impossible de démarrer le serveur HTTP sur le port 8080.";
delete tcpServer;
return 1;
}
qInfo() << "shipheart_api en écoute sur le port" << tcpServer->serverPort();
return app.exec();
}