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).
| Fichier | Rôle |
|---|---|
src/consigne_main.cpp, src/consigne_api.cpp | Processus dédié et API loopback sur 127.0.0.1:8081 |
include/consigne_manager.hpp, src/consigne_manager.cpp | Logique consigne, SQLite, thread de synchronisation |
include/saturn_client.hpp, src/saturn_client.cpp | Client sortant vers Saturn (gRPC générique + HTTP) |
src/local_http_client.cpp | IPC HTTP loopback et délégation des actionneurs |
src/local_api.cpp, src/grpc_server.cpp | Façades compatibles sur les ports historiques |
src/config.cpp | Sections 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éfaut | Description |
|---|---|---|
consigne.layout_path | /etc/leukos/consigne.json | Mur de casiers partagé avec l'IHM ; fournit les numéros si lockers est vide |
consigne.open_duration_ms | 5000 | Durée d'impulsion relais / ouverture serrure |
consigne.sync_interval_s | 30 | Synchronisation périodique vers Saturn (min. 5 s) |
consigne.status_log_interval_s | 300 | Log 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
kind | Niveau | Déclencheur |
|---|---|---|
mode.enabled / mode.disabled | info | Interrupteur IHM |
status | info | Changement de casier ou toutes les status_log_interval_s |
parcel.deposited | info | Dépôt reçu de Saturn |
parcel.picked_up | info | Scan valide |
scan.rejected | warn | Code inconnu ou déjà utilisé |
locker.open_failed | error | Échec relais / serrure |
API REST locale
| Méthode | Route | Corps | Ré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éthode | Requête | Réponse |
|---|---|---|
ConsigneDeposit | même JSON que POST /consigne/parcels | {"ok":true} / {"ok":true,"duplicate":true} |
ConsigneOpen | même JSON que POST /consigne/open | mê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