Aller au contenu principal

Daemon C++ : leukos-consigne

Le mode consigne tourne dans un processus séparé, leukos-consigne. leukos-node reste propriétaire des GPIO et des plugins, et conserve les ports historiques comme façade : il relaie les routes REST et méthodes gRPC consigne vers l'API loopback du processus dédié (127.0.0.1:8081). Les commandes d'actionneur reviennent au daemon global par son API locale (127.0.0.1:8080).

FichierRôle
src/consigne_main.cpp, src/consigne_api.cppProcessus dédié et API loopback sur 127.0.0.1:8081
include/consigne_manager.hpp, src/consigne_manager.cppLogique consigne, SQLite, thread de synchronisation
include/saturn_client.hpp, src/saturn_client.cppClient sortant vers Saturn (gRPC générique + HTTP)
src/local_http_client.cppIPC HTTP loopback et délégation des actionneurs
src/local_api.cpp, src/grpc_server.cppFaçades compatibles sur les ports historiques
src/config.cppSections consigne et central de node.json

Configuration node.json​

{
"node": { "address": "12 rue des Amandiers, 44000 Nantes" },
"grpc": { "enabled": true, "bind": "0.0.0.0", "port": 9080 },
"central": {
"saturn_grpc": "192.168.0.12:9130",
"saturn_http": "http://192.168.0.12:8130"
},
"consigne": {
"layout_path": "/etc/leukos/consigne.json",
"open_duration_ms": 5000,
"sync_interval_s": 30,
"status_log_interval_s": 300,
"lockers": [
{ "number": 1, "relay": "relay3" },
{ "number": 2, "lock_plugin": "arduino_lock" }
]
}
}
CléDéfautDescription
consigne.layout_path/etc/leukos/consigne.jsonMur de casiers partagé avec l'IHM ; fournit les numéros si lockers est vide
consigne.open_duration_ms5000Durée d'impulsion relais / ouverture serrure
consigne.sync_interval_s30Synchronisation périodique vers Saturn (min. 5 s)
consigne.status_log_interval_s300Log d'état périodique (min. 30 s)
node.address—Adresse statique envoyée à Saturn une fois à l'initialisation (et lors d'un sync-full)
consigne.lockers[].relay—Relais GPIO activé puis relâché après open_duration_ms
consigne.lockers[].lock_plugin—Plugin bridge série piloté par cmd_lock(open)
central.saturn_grpc—host:port du SaturnService
central.saturn_http—Base REST de secours

:::caution Casier sans actionneur Si aucun relay ni lock_plugin n'est défini pour un casier, le daemon tente d'abord un fallback relay<numero_casier> (ex: casier 1 -> relay1). Si ce relais n'existe pas / échoue, le scan valide est accepté avec "actuator":"none" : le casier est libéré manuellement par l'opérateur (fonctionnement sans serrure électrique). :::

Schéma SQLite​

Le manager ouvre sa propre connexion sur node.db (WAL, busy_timeout 3 s). Le fichier est partagé avec leukos-node, mais chaque processus écrit uniquement dans ses tables :

CREATE TABLE consigne_settings (key TEXT PRIMARY KEY, value TEXT);

CREATE TABLE consigne_lockers (
number INTEGER PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'empty', -- empty | occupied
parcel_id TEXT NOT NULL DEFAULT '',
updated_ts INTEGER NOT NULL DEFAULT (strftime('%s','now')));

CREATE TABLE consigne_parcels (
id TEXT PRIMARY KEY,
locker INTEGER NOT NULL,
recipient_name TEXT, recipient_email TEXT,
code_hash TEXT NOT NULL, -- sha256 hex, jamais le code clair
status TEXT NOT NULL DEFAULT 'deposited', -- deposited | picked_up
created_ts INTEGER, picked_up_ts INTEGER NOT NULL DEFAULT 0,
reported INTEGER NOT NULL DEFAULT 0); -- retrait acquitté par Saturn

CREATE TABLE consigne_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER, level TEXT, kind TEXT, message TEXT,
sent INTEGER NOT NULL DEFAULT 0); -- envoyé à Saturn

Le journal est borné : les événements déjà envoyés au-delà des 500 derniers sont purgés.

Types d'événements​

kindNiveauDéclencheur
mode.enabled / mode.disabledinfoInterrupteur IHM
statusinfoChangement de casier ou toutes les status_log_interval_s
parcel.depositedinfoDépôt reçu de Saturn
parcel.picked_upinfoScan valide
scan.rejectedwarnCode inconnu ou déjà utilisé
locker.open_failederrorÉchec relais / serrure

API REST locale​

MéthodeRouteCorpsRéponse
GET/consigne/status—état, casiers, dernière sync
POST/consigne/mode/on | /off—état mis à jour
POST/consigne/parcels{id, locker, recipient_name, recipient_email, code_hash}{"ok":true,"locker":3}
POST/consigne/open{"locker":3,"force":true,"focus_ui":true}{"ok":true,"locker":3,"actuator":"relay:relay3","events":[...]}
POST/system/ui-reset{}{"ok":true,"channel":"rest","command":"ui-reset","events":[...]}
POST/system/consigne-daemon-reset{}{"ok":true,"channel":"rest","command":"consigne-daemon-reset","events":[...]}
POST/system/full-reset{}{"ok":true,"channel":"rest","command":"full-reset","events":[...]}
POST/system/os-reboot{}{"ok":true,"channel":"rest","command":"os-reboot","events":[...]}
POST/consigne/scan{"code":"K7M3P9QX"}{"ok":true,"locker":3,"recipient_name":"…","actuator":"relay:relay3"}
POST/consigne/sync/full{}{"ok":true,"channel":"grpc/rest","error":"..."}

Ces routes restent publiées par leukos-node sur le port 8080; le daemon consigne les traite uniquement sur 127.0.0.1:8081.

:::warning Transport Saturn → node Le transport Saturn vers les nodes est désormais gRPC only. Les appels REST réseau depuis Saturn (/consigne/*, /relay/*, /system/*) sont supprimés. Si le daemon gRPC du node est indisponible, l'action échoue sans fallback HTTP. :::

:::info Transport Venus → Saturn (commandes système) consignes.executeSystemAction est maintenant relayé de Venus vers Saturn via /leukos.SaturnService/ConsigneSystemCommand (gRPC JSON), au lieu d'un POST REST direct sur /api/consignes/{node_id}/execution. :::

# depuis le node
curl -s localhost:8080/consigne/status
curl -s -X POST localhost:8080/consigne/mode/on
curl -s -X POST localhost:8080/consigne/scan \
-H 'Content-Type: application/json' -d '{"code":"K7M3P9QX"}'

:::info Lecture HTTP complète LocalApi lit désormais la requête jusqu'à Content-Length (64 Kio max, timeout 2 s) : Qt envoie souvent en-têtes et corps dans des segments TCP séparés. :::

Méthodes gRPC (leukos.AgentService, :9080)​

MéthodeRequêteRéponse
ConsigneDepositmême JSON que POST /consigne/parcels{"ok":true} / {"ok":true,"duplicate":true}
ConsigneOpenmême JSON que POST /consigne/openmême JSON que /consigne/open (events inclus)
ConsigneStatus{}même JSON que /consigne/status
ConsigneSyncFull{}idem POST /consigne/sync/full
ConsigneAuthentication{}policy {ok, qr, pin, facial, facial_available}
ConsigneSetAuthentication{qr, pin, facial, management_key}{"ok":true}
ConsigneEnrollIdentity{id, email, pin_salt, pin_hash, management_key}{"ok":true}
ConsigneSystemCommand{command}même JSON que POST /system/<command> (events inclus)

Les méthodes gRPC restent dans leukos-node pour compatibilité réseau, puis sont relayées vers ConsigneApi; Saturn et les nodes ne changent pas de protocole.

Trace d'exécution renvoyée (events)​

/consigne/open et ConsigneOpen renvoient un tableau events d'objets pour suivre les actions exécutées dans l'OS (validation, ouverture actionneur, signal UI, etc.).

Les routes /system/* suivent le même contrat events avec une granularité par étape OS exécutée (résolution de commande, commandes init.d, finalisation). Exemple :

{
"ok": true,
"channel": "rest",
"command": "full-reset",
"events": [
{"action": "decode_request", "ok": true, "detail": "system command received: full-reset"},
{"action": "resolve_command", "ok": true, "detail": "resolved 2 os step(s)"},
{"action": "restart_consigne_daemon", "ok": true, "detail": "/etc/init.d/S93leukos-consigne restart -> exit_code=0"},
{"action": "restart_ui_service", "ok": true, "detail": "/etc/init.d/S91leukos-ui restart -> exit_code=0"},
{"action": "finalize", "ok": true, "detail": "system command completed"}
]
}

Exemple :

{
"ok": true,
"locker": 2,
"actuator": "relay:relay2",
"focus_ui": true,
"events": [
{"action": "decode_request", "ok": true, "detail": "open command received"},
{"action": "validate_payload", "ok": true, "detail": "locker=2, force=true, focus_ui=true"},
{"action": "check_mode", "ok": true, "detail": "consigne mode enabled"},
{"action": "resolve_locker", "ok": true, "detail": "locker exists in layout/config"},
{"action": "open_locker", "ok": true, "detail": "actuator=relay:relay2"},
{"action": "open_page", "ok": true, "detail": "consigne layout page request queued"},
{"action": "refresh_parcel_displayer_svg", "ok": true, "detail": "locker highlight refreshed"},
{"action": "finalize", "ok": true, "detail": "locker open completed"}
]
}

En cas d'échec, ok=false, error est présent et events contient la séquence partielle avec l'étape en échec marquée ok=false.

Données statiques vs dynamiques​

  • Données dynamiques: casiers, retraits, événements. Elles sont envoyées en continu selon sync_interval_s.
  • Données statiques: bloc static (ex: address). Il est envoyé une seule fois après initialisation (consigne_settings.static_synced=1), puis renvoyé uniquement lors d'un déclenchement manuel sync-full.

Vérification du code au scan​

Le hash doit être identique à celui de Saturn (HashPickupCode) :

// Must match Saturn's HashPickupCode: trim + uppercase + sha256 hex.
std::string hash_code(const std::string& raw) {
std::string code;
for (char c : raw) {
const unsigned char uc = static_cast<unsigned char>(c);
if (!std::isspace(uc))
code.push_back(static_cast<char>(std::toupper(uc)));
}
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(code.data()), code.size(), digest);
// ... encodage hexadécimal minuscule
}

Toutes les requêtes SQLite utilisent des requêtes préparées (sqlite3_bind_*) ; dépôt et retrait sont exécutés en transaction (BEGIN / COMMIT).

Client Saturn : gRPC générique puis HTTP​

bool SaturnClient::via_grpc(const std::string& method, const std::string& json,
std::string& error) {
grpc::GenericStub stub(channel_);
grpc::ClientContext ctx;
ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(4));

grpc::CompletionQueue cq;
grpc::Slice slice(json);
grpc::ByteBuffer request(&slice, 1);
auto call = stub.PrepareUnaryCall(&ctx, method, request, &cq);
call->StartCall();

grpc::ByteBuffer response;
grpc::Status status;
call->Finish(&response, &status, reinterpret_cast<void*>(1));
// cq.Next(...) puis vérification {"ok":true}
}

Le fallback HTTP est un client socket minimal (POST /api/consignes/sync, HTTP/1.0, timeouts 4 s) : aucune dépendance supplémentaire dans l'image Buildroot.

Thread de synchronisation​

La logique consigne et ses appels réseau ne s'exécutent plus dans le processus ni la boucle de contrôle principale de leukos-node. Un crash du worker consigne est isolé et le script init peut redémarrer uniquement leukos-consigne.

Compilation​

make -C .build/buildroot BR2_EXTERNAL="$PWD/nodes/os" leukos-node-rebuild -j4
# binaires : .build/buildroot/output/target/usr/bin/leukos-node
# .build/buildroot/output/target/usr/bin/leukos-consigne
# init : nodes/os/board/leukos/<board>/rootfs-overlay/etc/init.d/S93leukos-consigne