Download OpenAPI specification:
Serveur Toolkeez : broker MQTT 3.1.1 (port 1883, ou 8883 en TLS) + API HTTP
locale (port 3443) pour piloter des modules ESP32 — sorties GPIO, émission
Wiegand vers un lecteur de badge, et pont BLE central vers un équipement tiers
(FAMA). La plupart des commandes sont publiées vers le module via MQTT ; le
module applique la commande puis accuse réception (ACK binaire pour
GPIO/Wiegand, ble/result JSON pour BLE).
Réponse HTTP 202 = publication seule. Toutes les routes de commande
(PUT .../gpios/:gpio, POST .../wiegand/emit, POST .../ble/*) renvoient
202 dès que la commande est publiée sur MQTT — pas quand elle est réellement
appliquée par le module. Suivre GET /api/commands/{commandId} (ou le flux SSE
pour les notifications BLE) pour connaître le résultat réel.
⚠️ Le serveur écoute en HTTP en clair sur HTTP_PORT : le TLS public est
terminé par nginx (deploy/nginx/toolkeez.conf), qui reparle en clair avec ce
backend en local. Ne jamais exposer HTTP_PORT directement sans ce proxy — la
clé API circule en clair (Bearer token, pas de mTLS).
Les payloads MQTT pour GPIO/ACK/Wiegand (pas BLE, en JSON) sont des buffers
binaires bruts — jamais du JSON ni de l'hexadécimal texte — avec un en-tête
commun de 40 octets (magic TK, version, type, flags, taille payload,
séquence, timestamp, UUID de message). Détail complet octet par octet :
PROTOCOL.md, codec de référence : src/protocol.js.
Sur ce déploiement, un module Toolkeez pilote 3 sorties GPIO reliées au FAMA.
Binding fixé au niveau applicatif (pas dans le protocole Toolkeez lui-même —
n'importe quel GPIO 0-255 reste utilisable), propriété de l'agent hopper
pour les tests e2e (agents/hopper/tools/toolkeez/) :
| Action | GPIO | Effet métier |
|---|---|---|
| USE | 6 | Passer le FAMA en mode "use" |
| SHUNT | 5 | Shunter le FAMA |
| WORKING | 4 | Passer le FAMA en mode "working" |
Les 3 pins ne sont pas mutuellement exclusifs côté API : un scénario qui a
besoin d'un seul état actif à la fois doit explicitement éteindre les autres
pins avant/après. CLI internalisée : node agents/hopper/tools/toolkeez/toolkeez-cli.js action use on.
Consigne persistante et retenue côté broker (QoS 1, retain: true) :
acceptée même si le module est hors ligne, transmise automatiquement à sa
prochaine connexion.
Modules connectés ou déjà vus. État conservé uniquement en mémoire —
redémarrer le serveur efface la liste. desiredState = dernière consigne
envoyée par gpio · state = dernier état confirmé par le module (peut
différer temporairement de desiredState pendant la propagation).
[- {
- "deviceId": "TK-001",
- "clientId": "toolkeez-TK-001",
- "connected": true,
- "connectedAt": "2019-08-24T14:15:22Z",
- "disconnectedAt": "2019-08-24T14:15:22Z",
- "lastSeenAt": "2019-08-24T14:15:22Z",
- "status": {
- "firmware": "string",
- "mac": "string",
- "ip": "string",
- "model": "string",
- "gpio": [
- 0
]
}, - "desiredState": {
- "property1": "on",
- "property2": "on"
}, - "state": {
- "property1": "on",
- "property2": "on"
}, - "lastAck": {
- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "status": "applied",
- "errorCode": 0,
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}
}
]| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
{- "deviceId": "TK-001",
- "clientId": "toolkeez-TK-001",
- "connected": true,
- "connectedAt": "2019-08-24T14:15:22Z",
- "disconnectedAt": "2019-08-24T14:15:22Z",
- "lastSeenAt": "2019-08-24T14:15:22Z",
- "status": {
- "firmware": "string",
- "mac": "string",
- "ip": "string",
- "model": "string",
- "gpio": [
- 0
]
}, - "desiredState": {
- "property1": "on",
- "property2": "on"
}, - "state": {
- "property1": "on",
- "property2": "on"
}, - "lastAck": {
- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "status": "applied",
- "errorCode": 0,
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}
}La consigne est persistante et retenue côté broker (QoS 1,
retain: true) : acceptée même si le module est hors ligne, transmise
automatiquement à sa prochaine connexion.
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
| gpio required | integer [ 0 .. 255 ] |
required | string or boolean
|
{- "state": "on"
}{- "protocolVersion": 1,
- "commandId": "af774cbc-df78-4e79-9be1-f0d761b5753b",
- "type": "gpio.set",
- "deviceId": "TK-001",
- "gpio": 4,
- "state": "on",
- "sequence": 43,
- "issuedAt": "2026-09-15T09:46:44.988Z",
- "status": "published",
- "acknowledgedAt": null
}Suit l'exécution d'une commande émise par n'importe laquelle des routes
de ce document (GPIO, Wiegand, BLE). status passe de "published" à un
statut d'ACK (GPIO/Wiegand) ou "completed"/"failed" (BLE, voir
ble/result) une fois le module répondu. Les commandes sont purgées
automatiquement 1h après émission.
| commandId required | string <uuid> |
{- "commandId": "af774cbc-df78-4e79-9be1-f0d761b5753b",
- "deviceId": "TK-001",
- "gpio": 4,
- "state": "on",
- "status": "applied",
- "acknowledgedAt": "2026-09-15T09:46:45.201Z",
- "ack": {
- "commandId": "af774cbc-df78-4e79-9be1-f0d761b5753b",
- "status": "applied",
- "errorCode": 0,
- "acknowledgedAt": "2026-09-15T09:46:45.201Z"
}
}Émission ponctuelle d'une trame Wiegand vers un lecteur de badge piloté par
le module. Non persistante : contrairement au GPIO, si le module est
hors ligne au moment de l'appel, la commande n'est pas rejouée à sa
prochaine connexion (retain: false).
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
| format | integer Default: 26 Enum: 26 36 26 = HID 26-bit, 36 = brut 36-bit |
| facilityCode | integer [ 0 .. 255 ] Requis si format=26, ignoré sinon |
| cardNumber required | integer 0..65535 si format=26, 0..68719476735 (36 bits) si format=36 |
{- "facilityCode": 12,
- "cardNumber": 34567
}{- "protocolVersion": 1,
- "commandId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
- "type": "wiegand.emit",
- "deviceId": "TK-001",
- "format": 26,
- "facilityCode": 12,
- "cardNumber": 34567,
- "sequence": 7,
- "issuedAt": "2026-09-15T09:50:00.000Z",
- "status": "published",
- "acknowledgedAt": null
}Contrairement à GPIO/Wiegand, ce canal ne passe pas par l'enveloppe
binaire — commandes et réponses en JSON brut, comme le channel status.
Il pilote le rôle BLE central du module vers un équipement tiers qui
expose un service GATT (le FAMA), sans configuration figée côté firmware :
chaque commande transporte les UUID et valeurs cibles directement.
famaUid (UID 32 bits) est décodé par le module depuis les données
constructeur (manufacturerData) de l'advertisement BLE de l'équipement —
même layout que celui déjà décodé côté lhc-mobile/osha-list-mobile-app (ID
constructeur Adveez, marqueur "ADV", octet de type, puis cet UID). Le
module scanne en continu, décode les advertisements et se connecte au
premier qui correspond s'il n'est pas déjà connecté, puis reste connecté en
permanence — reconnexion automatique sauf après un ble/disconnect explicite.
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
| famaUid required | integer <int64> [ 0 .. 4294967295 ] UID 32 bits de l'équipement |
| password required | string non-empty |
| connection_priority | integer Enum: 0 1 2 0=balanced, 1=high, 2=low power (façon Android BluetoothGatt) |
| password_service_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
| password_characteristic_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
{- "famaUid": 305419896,
- "password": "<secret>",
- "connection_priority": 1,
- "password_service_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7",
- "password_characteristic_uuid": "47182909-fb23-4324-a48e-37ba930d7911"
}{- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "type": "string",
- "deviceId": "string",
- "status": "string",
- "issuedAt": "2019-08-24T14:15:22Z",
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}Aucun champ requis. Termine la liaison BLE en cours et désactive la
reconnexion automatique jusqu'au prochain ble/connect — utile pour
laisser temporairement un autre appareil se connecter à l'équipement.
Sans effet si rien n'est connecté (ble/result renvoie
error: "ESP_ERR_INVALID_STATE").
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
{- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "type": "string",
- "deviceId": "string",
- "status": "string",
- "issuedAt": "2019-08-24T14:15:22Z",
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}Résultat sur ble/result — value en hexadécimal si succès.
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
| service_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
| characteristic_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
{- "service_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7",
- "characteristic_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7"
}{- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "type": "string",
- "deviceId": "string",
- "status": "string",
- "issuedAt": "2019-08-24T14:15:22Z",
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
| service_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
| characteristic_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
| value required | string non-empty |
{- "service_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7",
- "characteristic_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7",
- "value": "string"
}{- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "type": "string",
- "deviceId": "string",
- "status": "string",
- "issuedAt": "2019-08-24T14:15:22Z",
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}Active les notifications sur cette caractéristique (écriture du
descripteur CCCD). Une seule souscription à la fois — une nouvelle
commande remplace la précédente (désabonnement côté équipement avant
réabonnement). Les valeurs notifiées arrivent en continu sur le flux SSE
(GET .../ble/notifications/stream), indépendamment de cette commande
ponctuelle.
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
| service_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
| characteristic_uuid required | string (Uuid) ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]... |
{- "service_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7",
- "characteristic_uuid": "ca340dd3-5e62-42d5-af47-8b4fd43b93e7"
}{- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "type": "string",
- "deviceId": "string",
- "status": "string",
- "issuedAt": "2019-08-24T14:15:22Z",
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}Aucun champ requis. Désactive la souscription en cours.
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
{- "commandId": "9e2dd63c-3478-489f-86d3-8c292a65a0aa",
- "type": "string",
- "deviceId": "string",
- "status": "string",
- "issuedAt": "2019-08-24T14:15:22Z",
- "acknowledgedAt": "2019-08-24T14:15:22Z"
}Flux text/event-stream des valeurs notifiées par la caractéristique
actuellement souscrite ({ service_uuid, characteristic_uuid, value, receivedAt } par événement, value en hexadécimal). Indépendant de
ble/notifications/subscribe : peut être ouvert avant, pendant ou après,
et par plusieurs clients HTTP à la fois — le serveur republie chaque
notification vers tous les flux ouverts pour ce module.
| deviceId required | string^[A-Za-z0-9_-]{1,64}$ Example: TK-001 |
{- "error": "string"
}