#include #include #include #include #include #include #include #include #include #include #include #include // ============================================================================ // 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= // 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(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(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 == "circuit") typeSource = TYPE_SOURCE_CIRCUIT; else if (type == "batterie") typeSource = TYPE_SOURCE_BATTERIE; else if (type == "capteur") typeSource = TYPE_SOURCE_CAPTEUR; else if (type == "reservoir") 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 == "circuit") { // 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 == "batterie") { // 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 == "capteur") { // 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 == "reservoir") { // 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= // 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 != "circuit" && type != "batterie" && type != "capteur" && type != "reservoir") { 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 == "circuit") { 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 == "batterie") { // 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 == "capteur") { // 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 == "reservoir") { 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/ // É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/", 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/", 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(); }