Aller au contenu principal

Daemon C++ — référence détaillée du code

Cette page documente l'intégralité du code de l'agent (nodes/agent), fichier par fichier, classe par classe et fonction par fonction. Elle complète le survol du daemon qui, lui, reste volontairement synthétique.

Pour lire le source complet ligne par ligne directement dans la doc, voir Daemon C++ — code complet commenté.

  • Langage : C++17, namespace oa (« OpenAutomation », nom historique).
  • Binaire : leukos-node (le paquet et les services s'appellent leukos-agent).
  • Rôle : automate de terrain autonome pilotant relais, vannes et compteurs via GPIO, exposant son état via HTTP/gRPC/MQTT, et persistant tout localement en SQLite.
  • Dépendances natives : libgpiod, jansson (JSON), sqlite3, libmosquitto (MQTT), grpc++, plus la libc/POSIX (sockets, termios, watchdog).

Vue d'ensemble de l'architecture

La classe Node est l'orchestrateur : elle possède tous les gestionnaires et les câble entre eux par références. Les trois surfaces de commande (HTTP LocalApi, gRPC GrpcServer, MQTT MqttClient) ainsi que le Scheduler convergent vers deux méthodes uniques : Node::cmd_relay() et Node::cmd_valve().

Modèle de threads :

  • Le thread principal exécute la boucle de contrôle (Node::run()) : polling des compteurs, auto-fermeture des vannes, cron, sonde bridge, watchdog.
  • LocalApi et GrpcServer tournent chacun sur leur propre thread.
  • MqttClient délègue à la boucle réseau interne de mosquitto (loop_start), dont les callbacks s'exécutent sur un thread mosquitto.
  • La cohérence est assurée par des std::mutex dans chaque gestionnaire partagé (RelayManager, ValveManager, MeterManager, LocalDb, GpioManager, DeviceBridgeManager).

main.cpp — point d'entrée

Rôle : parser les arguments, charger la config, installer les gestionnaires de signaux, démarrer le node et lancer la boucle bloquante.

namespace {
oa::Node* g_node = nullptr;
void on_signal(int) { if (g_node) g_node->stop(); }
}
  • g_node est un pointeur global vers l'instance unique, nécessaire parce que les handlers de signaux POSIX ne prennent pas de contexte utilisateur.
  • on_signal() se contente d'appeler Node::stop() (async-signal : il ne fait qu'écrire un booléen atomique — voir Node::run()).

Déroulé de main() :

  1. Valeur par défaut config_path = "/etc/leukos/node.json".
  2. Boucle d'arguments : --config/-c <path> (surcharge le chemin), --help/-h (affiche usage() et sort 0), tout autre argument → erreur (usage() + code 2).
  3. Config::load() ; en cas d'échec : log ERROR et code 1.
  4. Construction du Node, publication dans g_node.
  5. Installation des signaux : SIGINT et SIGTERMon_signal ; SIGPIPE est ignoré (SIG_IGN) pour éviter que l'écriture sur un socket ou un port série fermé ne tue le process.
  6. node.start() ; échec → log ERROR + code 1.
  7. node.run() (bloquant jusqu'au stop()), puis retour 0.

config.hpp / config.cpp — configuration

Structures (config.hpp)

Chaque section de node.json a sa structure, avec des valeurs par défaut sûres directement dans les membres :

StructChamps clésDéfaut notable
RelayConfigid, line, active_lowactive_low = true
ValveConfigid, relay, default_secondsdefault_seconds = 300
MeterConfigid, line, unit, pulses_per_unitpulses_per_unit = 1.0
ScheduleConfigid, target, action, cron, secondsseconds = 0
DevicePluginConfigname, enabled, transport, port, baud, heartbeat_cmd, heartbeat_ackbaud = 115200, heartbeat = PING\n/PONG

La structure agrégée Config regroupe le node (id/nom/site/poll_interval_ms = 250), la base (db_path), le MQTT, l'API HTTP (activée par défaut, :8080), le gRPC (désactivé par défaut, :9080), le watchdog (activé, 15 s), le GPIO (gpiochip0), le device bridge, et les vecteurs de relais/vannes/compteurs/planifications.

static bool Config::load(path, out, error) : charge le JSON et retourne false uniquement sur erreur fatale (fichier illisible/non parsable).

Chargement (config.cpp)

Helpers anonymes typés et tolérants (retournent une valeur par défaut si la clé est absente ou du mauvais type) :

  • jstr(obj, key, def) — chaîne.
  • jint(obj, key, def) — entier, accepte aussi un réel (troncature).
  • jreal(obj, key, def) — réel, accepte aussi un entier.
  • jbool(obj, key, def) — booléen.

builtin_plugins() fournit la liste par défaut de 5 plugins MCU (arduino_atmega2560 activé, esp32/esp8266/stm32/rp2040 désactivés) : si aucun plugin n'est fourni par la config, cette liste est utilisée.

Config::load() :

  1. Pré-remplit device_plugins avec builtin_plugins() si vide.
  2. json_load_file() ; erreur → message avec jerr.text et numéro de ligne, false.
  3. Lecture section par section (node, database, mqtt, api, grpc, watchdog, gpio, device_bridge). Chaque section n'écrase un champ que si elle est présente, grâce aux helpers et à leurs défauts.
  4. Pour device_bridge.plugins : si un tableau est fourni, il remplace entièrement la liste par défaut (clear() puis json_array_foreach), et un plugin sans name est ignoré.
  5. Les tableaux relays, valves, meters, schedules sont parcourus de même ; toute entrée sans id est ignorée (garde-fou contre les entrées vides).
  6. json_decref(root) puis return true.

:::note Gestion mémoire jansson json_object_get() renvoie une référence empruntée (pas d'incrément de compteur), donc seul le root obtenu de json_load_file est libéré par json_decref. :::


node.hpp / node.cpp — l'orchestrateur

Déclaration (node.hpp)

Node possède par valeur tous les sous-systèmes (LocalDb, GpioManager, RelayManager, ValveManager, MeterManager, Watchdog, Scheduler, MqttClient, DeviceBridgeManager, LocalApi, GrpcServer) et un std::atomic<bool> running_.

Surface publique :

  • start(error) / run() / stop() — cycle de vie.
  • Accès aux gestionnaires (relays(), valves(), meters(), db()).
  • cmd_relay(id, action) et cmd_valve(id, action, seconds)surface de commande partagée par l'API, le gRPC, le MQTT et le scheduler.
  • state_json() — instantané complet du node en JSON.

Construction

L'ordre d'initialisation de la liste d'initialisation respecte les dépendances par référence : relays_(gpio_, db_), valves_(relays_), meters_(gpio_, db_), scheduler_(*this), mqtt_(*this), api_(*this), grpc_(*this). Les sous-systèmes reçoivent donc des références vers leurs collaborateurs, tous membres du même Node.

start() — séquence de démarrage

  1. Base : db_.open() — échec ⇒ fatal (return false).
  2. GPIO (si gpio_enabled) : gpio_.open(), puis relays_.init() et meters_.init() (échecs fatals). Si GPIO désactivé, un WARN prévient que les configs relais/compteurs seront ignorées.
  3. valves_.init(), scheduler_.init(), watchdog_.start() (non fatals).
  4. Device bridge (si activé) : bridge_.start() ; un échec est seulement loggé (WARN), pas fatal.
  5. MQTT : mqtt_.start() ; échec non fatal (le node fonctionne hors-ligne).
  6. API HTTP (si api_enabled) : api_.start()échec fatal (port occupé par exemple).
  7. gRPC (si grpc_enabled) : grpc_.start() ; échec non fatal.
  8. Journalise l'événement start en base et log INFO.

La philosophie : les liaisons réseau facultatives ne bloquent jamais le démarrage, seuls la base, le GPIO demandé et l'API HTTP demandée sont critiques.

run() — boucle de contrôle déterministe

while (running_.load()) {
meters_.tick(); // détection de fronts sur les entrées
valves_.tick(); // auto-fermeture des vannes expirées
scheduler_.tick(); // déclenchement cron
bridge_.tick(); // sonde MCU (throttlée en interne)
watchdog_.feed(); // réarme le watchdog matériel
// ... publication d'état toutes les 2 s, log connexions toutes les 30 s
std::this_thread::sleep_for(period); // period = poll_interval_ms
}
  • Publication MQTT d'état toutes les 2 s (publish_state()).
  • Journal de connexions toutes les 30 s : chaîne mqtt=up/down <résumé bridge>, loggée et enregistrée en base (connections/heartbeat).
  • running_ est atomique : c'est ce que le handler de signal bascule pour sortir.

stop()

Bascule running_ à false puis arrête proprement, dans l'ordre, le bridge, le gRPC, l'API, le MQTT et le watchdog, journalise l'événement stop. Chaque sous-système a un stop()/destructeur idempotent.

cmd_relay() / cmd_valve()

Simples routeurs d'action → gestionnaire :

  • cmd_relay : "on"set(true), "off"set(false), "toggle"toggle(), sinon false.
  • cmd_valve : "open"open(id, seconds), "close"close(id), sinon false.

state_json()

Construit avec jansson un objet contenant :

  • Métadonnées : node, name, site, ts (epoch secondes).
  • connections : mqtt.status (up/down) + injection du bridge via bridge_.append_json().
  • relays : map id → bool.
  • valves : map id → bool (ouverte/fermée).
  • meters : map id → {pulses, value, unit}.

Sérialisé en JSON_COMPACT. Les const_cast locaux permettent d'appeler les accesseurs (non-const car ils prennent un mutex) depuis une méthode const.


log.hpp — journalisation

En-tête header-only. enum class LogLevel { Debug, Info, Warn, Error } et log_write(level, msg) qui écrit sur stderr une ligne horodatée ISO-8601 local :

[2026-08-11T14:03:21] INFO leukos-node: <message>

Écrire sur stderr permet à systemd/journald de capturer nativement les logs. Macros pratiques : OA_LOG_DEBUG/INFO/WARN/ERROR.


local_db.hpp / local_db.cpp — persistance SQLite

Fin wrapper autour de sqlite3, protégé par un std::mutex.

  • open(path, error) : ouvre la base, active WAL (PRAGMA journal_mode=WAL) pour de meilleures performances/robustesse, et crée trois tables si absentes :
    • events(id, ts, source, kind, detail) — journal horodaté (ts par défaut = strftime('%s','now')).
    • relay_state(id PRIMARY KEY, on_state) — dernier état connu des relais.
    • meter_count(id PRIMARY KEY, count) — cumul d'impulsions par compteur.
  • exec(sql) : helper interne pour les DDL/PRAGMA, loggue un WARN en cas d'erreur.
  • log_event(source, kind, detail) : INSERT préparé et paramétré (SQLITE_TRANSIENT pour que SQLite copie les chaînes).
  • save_relay_state / load_relay_state : upsert (ON CONFLICT ... DO UPDATE) et lecture. load_* renvoie false si aucune ligne (le manager garde alors son défaut).
  • save_meter_count / load_meter_count : idem pour les compteurs (int64).

Toutes les requêtes utilisent sqlite3_prepare_v2 + bind + finalize : aucune concaténation de SQL, donc pas d'injection. Chaque méthode vérifie db_ != nullptr.


gpio_manager.hpp / gpio_manager.cpp — accès GPIO (libgpiod)

Encapsule la puce libgpiod et gère un ensemble de lignes réservées, mélangeant librement entrées et sorties. Le code supporte les deux ABIs de libgpiod via la macro LEUKOS_GPIOD_V2 (définie par le CMake selon la version détectée).

  • struct Line (privée) : détient soit un gpiod_line_request* (v2), soit un gpiod_line* (v1), avec offset et is_output.
  • Destructeur : libère toutes les requêtes (v2) puis gpiod_chip_close().
  • open(chip_name, error) : préfixe /dev/ si nécessaire, ouvre la puce.
  • add_output(tag, line, initial_high) : réserve une ligne en sortie avec une valeur initiale. En v2, alloue settings/line_config/request_config, positionne direction OUTPUT et la valeur, consumer = "leukos-node". En v1, gpiod_line_request_output.
  • add_input(tag, line) : réserve une ligne en entrée avec bias pull-up (GPIOD_LINE_BIAS_PULL_UP / ..._FLAG_BIAS_PULL_UP).
  • set_output(tag, high) / get_input(tag, high) : écrit/lit un niveau physique (la logique active-low est gérée plus haut par RelayManager).

Toutes les mutations sont protégées par mtx_. Les erreurs sont loggées et renvoient false plutôt que de lever.


relay_manager.hpp / relay_manager.cpp — relais

Fait le pont entre relais logiques et sorties GPIO physiques, en gérant le câblage active-low et la persistance.

  • init(relays, error) : pour chaque relais, restaure l'état depuis LocalDb (défaut off), calcule le niveau physique initial (off_high = active_low, initial_high = on ? !off_high : off_high) et réserve la ligne en sortie. Échec de réservation ⇒ error renseignée + false.
  • set(id, on) : convertit logique→physique (high = active_low ? !on : on), écrit la sortie, met à jour l'état mémoire, persiste (save_relay_state), journalise l'événement et log INFO.
  • toggle(id) : lit l'état courant puis set(!cur).
  • get(id, on) : lecture protégée depuis la map state_.
  • ids() : liste triée des identifiants (issue de la map ordonnée).

Le point clé est le mapping active-low : un relais active_low est « off » quand la ligne est HIGH, ce qui correspond aux cartes relais classiques à optocoupleur.


valve_manager.hpp / valve_manager.cpp — vannes (fail-safe)

Pilote des électrovannes à travers des relais, avec fermeture automatique de sécurité (irrigation qui ne peut pas rester ouverte indéfiniment).

  • init(valves) : indexe les ValveConfig par id.
  • open(id, seconds) : résout le relais associé et la durée (seconds > 0 ? seconds : default_seconds), commande le relais à true, puis enregistre une deadline now + duration dans deadlines_. Log INFO.
  • close(id) : commande le relais à false et retire la deadline.
  • is_open(id) : vrai si une deadline existe pour cet id.
  • tick() : appelé depuis la boucle principale ; ferme toutes les vannes dont la deadline est atteinte. Le balayage des expirées se fait sous verrou, mais les close() sont exécutés hors du verrou pour éviter tout inter-blocage avec RelayManager.
  • ids() : liste des identifiants.

C'est le sous-système garantissant qu'une vanne ouverte se referme même si le superviseur central disparaît.


meter_manager.hpp / meter_manager.cpp — compteurs à impulsions

Compte les impulsions (eau, énergie…) par détection de front montant pendant le polling, et convertit en unités d'ingénierie.

  • struct State { bool last; long long count; } — dernier niveau et cumul.
  • init(meters, error) : réserve chaque ligne en entrée, restaure le compteur depuis la base (load_meter_count) et lit le niveau initial.
  • tick() : pour chaque compteur, lit l'entrée ; sur front montant (level && !last) incrémente le cumul et le persiste immédiatement, puis mémorise le niveau. C'est du polling (pas d'interruption), suffisant pour des compteurs à ampoule reed/sortie optique aux fréquences typiques.
  • pulses(id) : cumul brut. value(id) : count / pulses_per_unit (avec garde ppu > 0). unit(id) : unité configurée.
  • ids() : liste des identifiants.

:::caution Fréquence de comptage La détection par polling est limitée par poll_interval_ms (250 ms par défaut) : elle convient aux compteurs lents. Pour des trains d'impulsions rapides, il faudrait des interruptions matérielles (hors périmètre actuel). :::


scheduler.hpp / scheduler.cpp — planification cron

Évalue des entrées cron journalières simplifiées et déclenche des actions.

  • struct Entry : la config, minute/hour extraits, et last_fired_min/ last_fired_day pour l'anti-rebond.
  • parse_cron(cron, minute, hour) : parse "min hour dom mon dow" mais n'honore que minute et heure ; "*" signifie « tout » (-1), toute autre valeur est fixe.
  • init(schedules) : construit les entrées, ignore (avec WARN) les crons invalides, log le nombre d'entrées chargées.
  • tick() : lit l'heure locale (localtime_r), pour chaque entrée vérifie que minute et heure correspondent (ou -1), tire au plus une fois par minute correspondante (comparaison last_fired_min/last_fired_day avec tm_yday), puis route l'action : open/closecmd_valve, sinon → cmd_relay.

L'anti-rebond via tm_yday évite de re-déclencher plusieurs fois dans la même minute tout en autorisant le déclenchement le lendemain à la même heure.


watchdog.hpp / watchdog.cpp — watchdog matériel

Réarme le watchdog Linux (/dev/watchdog) pour garantir un redémarrage si le daemon se fige ; dégrade en no-op si le device est absent.

  • start(enabled, timeout_s) : si activé, ouvre /dev/watchdog en écriture. Absent ⇒ WARN et fonctionnement « software heartbeat » (no-op réel). Si ouvert, tente WDIOC_SETTIMEOUT via ioctl.
  • feed() : écrit un octet \0 pour réarmer (appelé à chaque tour de boucle).
  • stop() : écrit le magic-close 'V' avant close() pour demander au noyau de ne pas redémarrer lors d'un arrêt propre, puis ferme le descripteur.

device_bridge.hpp / device_bridge.cpp — pont MCU (série)

Sonde des microcontrôleurs (Arduino/ESP32/STM32/RP2040…) reliés en série et suit leur état de connexion pour l'exposer dans les logs et le statut du node.

  • struct DevicePluginStatus : image runtime d'un plugin (nom, transport, port, baud, activé, connecté, last_seen_ts, last_error, heartbeat cmd/ack).
  • start(enabled, probe_interval_s, plugins, error) : copie les configs en statuts, fixe l'intervalle (défaut 15 s), arme next_probe_ à maintenant. Aucun plugin ⇒ error.
  • stop() : bascule enabled_ à false.
  • tick() : throttlé — ne fait rien tant que now < next_probe_ ; sinon sonde tous les plugins et replanifie next_probe_.
  • probe_plugin(plugin) (cœur) :
    1. Ignore les plugins désactivés.
    2. stat() du port : absent ⇒ last_error = "device missing".
    3. Transport ≠ serial ⇒ non supporté.
    4. Sinon open_serial() (helper : open non-bloquant, cfmakeraw, baud via baud_to_termios, CLOCAL|CREAD) ; si un heartbeat_cmd est défini, l'écrit et, si un heartbeat_ack est attendu, lit la réponse avec un select() de 300 ms et compare. Succès ⇒ connected = true, last_seen_ts mis à jour, last_error vidé.
    5. Loggue tout changement d'état (connecté/déconnecté).
  • any_connected() : vrai si au moins un plugin activé est connecté.
  • summary() : chaîne bridge=enabled/disabled plugins=<name:up/down,...|none> (utilisée dans le log de connexions de la boucle principale).
  • append_json(root) : ajoute un objet bridge {enabled, plugins:[{name, enabled, transport, port, baud, connected, last_seen_ts, last_error}]} au JSON d'état.

:::note Sonde par « présence de port » Si heartbeat_cmd/heartbeat_ack sont vides, la connexion est considérée établie dès que le port s'ouvre. C'est le profil utilisé sur appserver (ATmega sur /dev/ttyUSB0), où un simple test de présence suffit. :::


mqtt_client.hpp / mqtt_client.cpp — client MQTT

Relie le node au superviseur central : publie l'état sous <base>/<node_id>/... et s'abonne aux topics de commande.

  • start(host, port, keepalive, base, node_id, error) : init libmosquitto, mosquitto_new(node_id), enregistre les callbacks on_connect/on_message, arme un Last-Will <base>/<id>/status = "offline" (retained, QoS 1) pour que le superviseur détecte une chute, puis connect_async + loop_start (boucle réseau sur thread mosquitto).
  • stop() : publie explicitement status = "offline", arrête la boucle, détruit le client, lib_cleanup.
  • publish_state(json) : publie sur <base>/<id>/state (QoS 0, non retained) si connecté.
  • on_connect (statique) : sur succès, connected_ = true, publie status = "online" (retained, QoS 1) et s'abonne à <base>/<id>/cmd/# (QoS 1).
  • on_message (statique) : extrait topic + payload et délègue à handle_command.
  • handle_command(topic, payload) : vérifie le préfixe <base>/<id>/cmd/, découpe domain/target, l'action = payload. relaycmd_relay. valve → gère la forme open:600 (durée après :) puis cmd_valve.

Convention de topics : openautomation/node-0001/cmd/relay/relay1 avec payload on.


local_api.hpp / local_api.cpp — serveur HTTP embarqué

Minuscule serveur HTTP/1.1 en sockets POSIX bruts, sur son propre thread.

  • start(bind, port, error) : socket(), SO_REUSEADDR, bind() (adresse via inet_pton, sinon INADDR_ANY), listen(8), lance le thread run. Toute erreur socket renvoie false avec strerror.
  • stop() : running_ = false (échange atomique idempotent), shutdown+close du socket d'écoute pour débloquer accept(), join du thread.
  • run() : boucle accept()recv (max 2047 octets) → parse la request-line (method path proto) → handle() → réponse 200 OK application/json (Content-Length, Connection: close) avec écriture complète en boucle → close. Connexion par requête (pas de keep-alive).
  • handle(method, path) : découpe le chemin en segments (coupe sur ?), et route :
    • GET /status (ou chemin vide) → node_.state_json().
    • POST /relay/<id>/<on|off|toggle>cmd_relay, réponse {"ok":bool}.
    • POST /valve/<id>/<open|close>[/<seconds>]cmd_valve.
    • sinon {"error":"not found"}.

C'est l'API consommée par le heartbeat local et par Saturn en repli HTTP.


grpc_server.hpp / grpc_server.cpp — daemon gRPC (JSON, sans protoc)

Daemon gRPC embarqué exploitant le service générique (codec-less) de gRPC : les requêtes/réponses sont des octets JSON bruts, donc aucun protobuf/protoc n'est requis sur la cible. C'est la même convention JSON-over-gRPC que Saturn ↔ dnsdhcp (Saturn compose avec grpc.CallContentSubtype("json")).

Service leukos.AgentService :

MéthodeEntréeSortie
Health{"service":"up"}
Statusétat complet (state_json)
Relay{"id","action"}{"ok":bool}
Valve{"id","action","seconds"}{"ok":bool}
  • struct Impl (hors header) : AsyncGenericService, ServerCompletionQueue, Server — isole les en-têtes grpcpp du reste du daemon (PIMPL).
  • struct CallState : une requête en vol, pilotée par la completion queue à travers les étapes kRequested → kRead → kWrite → kFinish, une transition de tag à la fois. Helpers bytebuffer_to_string / string_to_bytebuffer pour convertir les grpc::ByteBuffer.
  • start(bind, port, error) : ServerBuilder avec InsecureServerCredentials, enregistre le service générique, crée la CQ, BuildAndStart. Échec ⇒ false. Lance le thread run.
  • stop() : Server::Shutdown() + CompletionQueue::Shutdown(), join, reset PIMPL.
  • run() : amorce une première CallState, puis boucle cq->Next(&tag, &ok) : à kRequested ré-amorce une nouvelle CallState (pipeline toujours prêt) et lance Read ; à kRead appelle dispatch(method, body) et Write ; à kWrite Finish(OK) ; à kFinish libère l'objet. Un ok == false détruit la call.
  • dispatch(method, body) : isole le dernier segment de /leukos.AgentService/<Method>. Health/Status répondent directement ; sinon parse le JSON d'entrée (helpers field/int_field) et route vers cmd_relay/ cmd_valve, renvoyant {"ok":true|false} ou {"ok":false,"error":"unknown method"}.

CMakeLists.txt — build & packaging

  • C++17 strict (CXX_EXTENSIONS OFF), build Release par défaut.
  • Dépendances via pkg-config : jansson, sqlite3, libgpiod, libmosquitto, grpc++ (repli sur find_package(gRPC CONFIG) si le .pc est absent), Threads.
  • Si libgpiod >= 2, définit LEUKOS_GPIOD_V2=1 (bascule l'ABI dans gpio_manager.cpp).
  • Cible leukos-node = tous les src/*.cpp, installée dans /usr/bin.
  • Option LEUKOS_AGENT_ENABLE_SYSTEMD_PACKAGING (ON) : installe les wrappers packaging/bin/*, les unités systemd, node.json (renommé node.json.example), crée /var/lib/leukos, et configure CPack/DEB :
    • CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON (deps ABI auto) + CPACK_DEBIAN_PACKAGE_DEPENDS explicites : systemd, python3, curl, ca-certificates, mosquitto.
    • Scripts mainteneur postinst/prerm via CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA.

packaging/ — intégration système

bin/leukos-agent

Wrapper shell : exec /usr/bin/leukos-node --config "$CONFIG"CONFIG provient de LEUKOS_AGENT_CONFIG (défaut /etc/leukos/node.json). C'est la commande lancée par le service systemd.

bin/leukos-agent-heartbeat

Script POSIX exécuté périodiquement par le timer. Il :

  1. Lit l'URL de l'API locale depuis node.json (via un court script Python), en ramenant 0.0.0.0/:: à 127.0.0.1.
  2. Récupère l'état systemd (systemctl is-active leukos-agent).
  3. Interroge GET /status (curl) et résume l'état (mqtt=... bridge=... plugins=...) pour journald (logger -t).
  4. Construit un payload santé (Python) : choisit une IP non-loopback, calcule un status synthétique :
    • down si le service n'est pas active ;
    • up si mqtt == up ou bridge == up ;
    • degraded sinon.
  5. POST ce payload vers MERCURE_HEALTH_URL (http://127.0.0.1:8140/health/state).

C'est la source unique alimentant la vue Network → Flotte de Venus.

systemd/

  • leukos-agent.service : Type=simple, ExecStart=/usr/bin/leukos-agent, Restart=always (2 s), After=network-online.target, RuntimeDirectory=leukos.
  • leukos-agent-heartbeat.service : Type=oneshot, dépend du service agent, pousse vers Mercure (MERCURE_HEALTH_URL).
  • leukos-agent-heartbeat.timer : déclenche toutes les 30 s (OnBootSec=30s, OnUnitActiveSec=30s).

debian/postinst

À la (re)configuration : crée /etc/leukos et /var/lib/leukos, copie node.json.example → node.json s'il est absent, daemon-reload, active/redémarre mosquitto uniquement si mqtt.host est local, puis active/redémarre leukos-agent.service et leukos-agent-heartbeat.timer.

debian/prerm

À la suppression : stoppe et désactive le timer et le service, daemon-reload.

config/node.json

Config d'exemple (installée comme node.json.example) : GPIO désactivé, device bridge activé avec 5 plugins (arduino_atmega2560 + esp32 activés), listes relais/vannes/compteurs/planifications vides. C'est le point de départ à adapter par machine.


Où intervenir selon le besoin

BesoinFichier(s)
Nouveau champ de configinclude/config.hpp + src/config.cpp
Nouvelle action de commandesrc/node.cpp (cmd_*) + surface concernée
Nouveau type de périphérique piloténouveau *_manager.{hpp,cpp} + câblage Node
Nouveau endpoint HTTPsrc/local_api.cpp (handle)
Nouvelle méthode gRPCsrc/grpc_server.cpp (dispatch)
Nouveau topic MQTTsrc/mqtt_client.cpp (handle_command)
Champ de statut publiésrc/node.cpp (state_json)
Comportement de santé flottepackaging/bin/leukos-agent-heartbeat