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'appellentleukos-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. LocalApietGrpcServertournent chacun sur leur propre thread.MqttClientdé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::mutexdans 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_nodeest 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'appelerNode::stop()(async-signal : il ne fait qu'écrire un booléen atomique — voirNode::run()).
Déroulé de main() :
- Valeur par défaut
config_path = "/etc/leukos/node.json". - Boucle d'arguments :
--config/-c <path>(surcharge le chemin),--help/-h(afficheusage()et sort 0), tout autre argument → erreur (usage()+ code 2). Config::load(); en cas d'échec : logERRORet code 1.- Construction du
Node, publication dansg_node. - Installation des signaux :
SIGINTetSIGTERM→on_signal;SIGPIPEest ignoré (SIG_IGN) pour éviter que l'écriture sur un socket ou un port série fermé ne tue le process. node.start(); échec → logERROR+ code 1.node.run()(bloquant jusqu'austop()), 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 :
| Struct | Champs clés | Défaut notable |
|---|---|---|
RelayConfig | id, line, active_low | active_low = true |
ValveConfig | id, relay, default_seconds | default_seconds = 300 |
MeterConfig | id, line, unit, pulses_per_unit | pulses_per_unit = 1.0 |
ScheduleConfig | id, target, action, cron, seconds | seconds = 0 |
DevicePluginConfig | name, enabled, transport, port, baud, heartbeat_cmd, heartbeat_ack | baud = 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() :
- Pré-remplit
device_pluginsavecbuiltin_plugins()si vide. json_load_file(); erreur → message avecjerr.textet numéro de ligne,false.- 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. - Pour
device_bridge.plugins: si un tableau est fourni, il remplace entièrement la liste par défaut (clear()puisjson_array_foreach), et un plugin sansnameest ignoré. - Les tableaux
relays,valves,meters,schedulessont parcourus de même ; toute entrée sansidest ignorée (garde-fou contre les entrées vides). json_decref(root)puisreturn 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)etcmd_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
- Base :
db_.open()— échec ⇒ fatal (return false). - GPIO (si
gpio_enabled) :gpio_.open(), puisrelays_.init()etmeters_.init()(échecs fatals). Si GPIO désactivé, unWARNprévient que les configs relais/compteurs seront ignorées. valves_.init(),scheduler_.init(),watchdog_.start()(non fatals).- Device bridge (si activé) :
bridge_.start(); un échec est seulement loggé (WARN), pas fatal. - MQTT :
mqtt_.start(); échec non fatal (le node fonctionne hors-ligne). - API HTTP (si
api_enabled) :api_.start()— échec fatal (port occupé par exemple). - gRPC (si
grpc_enabled) :grpc_.start(); échec non fatal. - Journalise l'événement
starten base et logINFO.
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(), sinonfalse.cmd_valve:"open"→open(id, seconds),"close"→close(id), sinonfalse.
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 viabridge_.append_json().relays: mapid → bool.valves: mapid → bool(ouverte/fermée).meters: mapid → {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é (tspar 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 unWARNen cas d'erreur.log_event(source, kind, detail):INSERTpréparé et paramétré (SQLITE_TRANSIENTpour que SQLite copie les chaînes).save_relay_state/load_relay_state: upsert (ON CONFLICT ... DO UPDATE) et lecture.load_*renvoiefalsesi 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 ungpiod_line_request*(v2), soit ungpiod_line*(v1), avecoffsetetis_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, allouesettings/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 parRelayManager).
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 depuisLocalDb(défautoff), 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 ⇒errorrenseigné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 logINFO.toggle(id): lit l'état courant puisset(!cur).get(id, on): lecture protégée depuis la mapstate_.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 lesValveConfigpar 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 deadlinenow + durationdansdeadlines_. LogINFO.close(id): commande le relais àfalseet 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 lesclose()sont exécutés hors du verrou pour éviter tout inter-blocage avecRelayManager.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 gardeppu > 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/hourextraits, etlast_fired_min/last_fired_daypour 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 (avecWARN) 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 (comparaisonlast_fired_min/last_fired_dayavectm_yday), puis route l'action :open/close→cmd_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/watchdogen écriture. Absent ⇒WARNet fonctionnement « software heartbeat » (no-op réel). Si ouvert, tenteWDIOC_SETTIMEOUTviaioctl.feed(): écrit un octet\0pour réarmer (appelé à chaque tour de boucle).stop(): écrit le magic-close'V'avantclose()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), armenext_probe_à maintenant. Aucun plugin ⇒error.stop(): basculeenabled_àfalse.tick(): throttlé — ne fait rien tant quenow < next_probe_; sinon sonde tous les plugins et replanifienext_probe_.probe_plugin(plugin)(cœur) :- Ignore les plugins désactivés.
stat()du port : absent ⇒last_error = "device missing".- Transport ≠
serial⇒ non supporté. - Sinon
open_serial()(helper :opennon-bloquant,cfmakeraw, baud viabaud_to_termios,CLOCAL|CREAD) ; si unheartbeat_cmdest défini, l'écrit et, si unheartbeat_ackest attendu, lit la réponse avec unselect()de 300 ms et compare. Succès ⇒connected = true,last_seen_tsmis à jour,last_errorvidé. - Loggue tout changement d'état (connecté/déconnecté).
any_connected(): vrai si au moins un plugin activé est connecté.summary(): chaînebridge=enabled/disabled plugins=<name:up/down,...|none>(utilisée dans le log de connexions de la boucle principale).append_json(root): ajoute un objetbridge {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 callbackson_connect/on_message, arme un Last-Will<base>/<id>/status = "offline"(retained, QoS 1) pour que le superviseur détecte une chute, puisconnect_async+loop_start(boucle réseau sur thread mosquitto).stop(): publie explicitementstatus = "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, publiestatus = "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écoupedomain/target, l'action = payload.relay→cmd_relay.valve→ gère la formeopen:600(durée après:) puiscmd_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 viainet_pton, sinonINADDR_ANY),listen(8), lance le threadrun. Toute erreur socket renvoiefalseavecstrerror.stop():running_ = false(échange atomique idempotent),shutdown+closedu socket d'écoute pour débloqueraccept(),joindu thread.run(): boucleaccept()→recv(max 2047 octets) → parse la request-line (method path proto) →handle()→ réponse200 OKapplication/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éthode | Entrée | Sortie |
|---|---|---|
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 étapeskRequested → kRead → kWrite → kFinish, une transition de tag à la fois. Helpersbytebuffer_to_string/string_to_bytebufferpour convertir lesgrpc::ByteBuffer.start(bind, port, error):ServerBuilderavecInsecureServerCredentials, enregistre le service générique, crée la CQ,BuildAndStart. Échec ⇒false. Lance le threadrun.stop():Server::Shutdown()+CompletionQueue::Shutdown(), join, reset PIMPL.run(): amorce une premièreCallState, puis bouclecq->Next(&tag, &ok): àkRequestedré-amorce une nouvelleCallState(pipeline toujours prêt) et lanceRead; àkReadappelledispatch(method, body)etWrite; àkWriteFinish(OK); àkFinishlibère l'objet. Unok == falsedétruit la call.dispatch(method, body): isole le dernier segment de/leukos.AgentService/<Method>.Health/Statusrépondent directement ; sinon parse le JSON d'entrée (helpersfield/int_field) et route verscmd_relay/cmd_valve, renvoyant{"ok":true|false}ou{"ok":false,"error":"unknown method"}.
CMakeLists.txt — build & packaging
- C++17 strict (
CXX_EXTENSIONS OFF), buildReleasepar défaut. - Dépendances via
pkg-config:jansson,sqlite3,libgpiod,libmosquitto,grpc++(repli surfind_package(gRPC CONFIG)si le.pcest absent),Threads. - Si
libgpiod >= 2, définitLEUKOS_GPIOD_V2=1(bascule l'ABI dansgpio_manager.cpp). - Cible
leukos-node= tous lessrc/*.cpp, installée dans/usr/bin. - Option
LEUKOS_AGENT_ENABLE_SYSTEMD_PACKAGING(ON) : installe les wrapperspackaging/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_DEPENDSexplicites :systemd, python3, curl, ca-certificates, mosquitto.- Scripts mainteneur
postinst/prermviaCPACK_DEBIAN_PACKAGE_CONTROL_EXTRA.
packaging/ — intégration système
bin/leukos-agent
Wrapper shell : exec /usr/bin/leukos-node --config "$CONFIG" où 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 :
- Lit l'URL de l'API locale depuis
node.json(via un court script Python), en ramenant0.0.0.0/::à127.0.0.1. - Récupère l'état systemd (
systemctl is-active leukos-agent). - Interroge
GET /status(curl) et résume l'état (mqtt=... bridge=... plugins=...) pour journald (logger -t). - Construit un payload santé (Python) : choisit une IP non-loopback, calcule un
statussynthétique :downsi le service n'est pasactive;upsimqtt == upoubridge == up;degradedsinon.
- 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
| Besoin | Fichier(s) |
|---|---|
| Nouveau champ de config | include/config.hpp + src/config.cpp |
| Nouvelle action de commande | src/node.cpp (cmd_*) + surface concernée |
| Nouveau type de périphérique piloté | nouveau *_manager.{hpp,cpp} + câblage Node |
| Nouveau endpoint HTTP | src/local_api.cpp (handle) |
| Nouvelle méthode gRPC | src/grpc_server.cpp (dispatch) |
| Nouveau topic MQTT | src/mqtt_client.cpp (handle_command) |
| Champ de statut publié | src/node.cpp (state_json) |
| Comportement de santé flotte | packaging/bin/leukos-agent-heartbeat |