T4 API Asystom Advisor
Uplink push (Flux 02) et downlink polling (Flux 03) opérationnels en réel depuis la CloudGate vers le serveur Advisor (environnement de staging). Les 4 globals (asystom_url, asystom_auth_b64, asystom_network, asystom_client) sont configurés dans startup_init et persistés. Les trames des balises sont routées et le serveur accuse réception (HTTP 200). Validation confirmée par les logs canvas (ASYSTOM_OUT → ASYSTOM_RESP status 200) et le polling downlink (ASYSTOM_DL).
Référence
Document : Lorawan_Network_Integration.pdf publié par Asystom (Third-party LoRaWAN network integration with Asystom Services).
Flux 02 — Uplink push
Endpoint
| URL | POST <asystom_url>/lorawan |
|---|---|
| Méthode | POST |
| Content-Type | application/json |
| Auth | Authorization: Basic <base64(user:password)> |
Headers obligatoires
| Header | Source canvas |
|---|---|
Authorization | 'Basic ' .. global.asystom_auth_b64 |
Content-Type | application/json |
X-Network | global.asystom_network (network identifier passé en header HTTP, recommandation Asystom plutôt qu'en body) |
Body JSON (Loriot-like)
Le format de body dépend du LNS source (Actility, Kerlink, Loriot, Multitech). Asystom fournit un exemple Loriot complet dans la doc PDF que le déserializer Advisor comprend déjà. La chaîne canvas envoie un format Loriot-compatible :
{
"endDevice": {
"devEui": "0004A30B010593C1",
"devAddr": "00DA5139"
},
"fPort": 4,
"fCntUp": 1182,
"payload": "00A2070E4C210239360BE184F67DD87C417CC87A...",
"encodingType": "HEXA",
"recvTime": 1714056772000,
"client": "<asystom_client>",
"dataRate": "SF7BW125",
"gwInfo": [
{
"rfRegion": "EU868",
"rssi": -85,
"snr": 10.5
}
]
}
Champs et leur source
| Champ | Source canvas |
|---|---|
endDevice.devEui | msg.DevEUI |
endDevice.devAddr | msg.DevAddr |
fPort | msg.fport |
fCntUp | msg.FCntUp |
payload | Hex uppercase de msg.payload |
encodingType | Hardcoded "HEXA" |
recvTime | os.time() * 1000 (epoch ms) |
client | global.asystom_client (Asystom client identifier, utilisé aussi pour le polling downlink) |
dataRate | Hardcoded "SF7BW125" pour l'instant (TODO : extraire du msg) |
gwInfo[0].rfRegion | Hardcoded "EU868" |
gwInfo[0].rssi | msg.rssi |
gwInfo[0].snr | msg.snr |
Codes de réponse
| Code | Statut | Comportement canvas |
|---|---|---|
| 201 | Frame processed | global.last_asystom_ok_ts = os.time() |
| 401 | Invalid credentials | Asystom = Basic auth (pas DAP). 401 = mauvais credentials → log + alerte. Ne déclenche pas le retry DAP. |
| 500 | Server Error | Journalisé. La trame n'est pas rejouée — il n'existe pas de file sur cette passerelle. |
Observation 2026-06-10 (env staging) : l'uplink reçoit actuellement HTTP 200 (frame parsée et accusée) plutôt que le 201 spec. Comportement constant, traité comme succès côté canvas. À reconfirmer avec Asystom lors du passage en environnement de production.
Flux 03 — Downlink polling
Basic auth
vide si rien à pousser
multi-msg → lora-app input
msg.app.queue[DevEUI]
wait window après prochain uplink balise
ou cmd 64 (reboot)
Endpoint
| URL | GET <asystom_url>/getPendingMsgs?client=<asystom_client> |
|---|---|
| Méthode | GET |
| Content-Type | application/x-www-form-urlencoded |
| Auth | Basic <asystom_auth_b64> |
Cadence
Inject cron asystom downlink poll avec repeat=60 (60s) et onceDelay=30. Donc 1 poll toutes les 60 secondes.
Réponse
{
"device_list": {
"0": "00-04-A3-0B-01-05-93-C1",
"1": "00-04-A3-0B-01-05-93-C2"
},
"payload": {
"0": "054114003C003C006801",
"1": "00"
},
"cmd": {
"0": "70",
"1": "64"
}
}
Si device_list est vide, aucun downlink à émettre. Sinon, pour chaque index i, la balise device_list[i] doit recevoir un downlink avec payload[i] sur FPort cmd[i].
Parsing canvas
Le node parse_asystom_dl extrait les 3 sections via regex Lua (le sandbox LuvitRED n'a pas json.decode garanti) :
local section_devices = body:match('"device_list"%s*:%s*(%b{})')
local section_payloads = body:match('"payload"%s*:%s*(%b{})')
local section_cmds = body:match('"cmd"%s*:%s*(%b{})')
-- Pour chaque section, extraire les paires "i":"value"
for k, v in section:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do t[k] = v end
-- Recompacter le DevEUI : "AA-BB-CC-DD-EE-FF-00-11" → "AABBCCDDEEFF0011"
local deveui = mac:gsub('-', ''):upper()
Émission du downlink LoRa
Le parser construit un seul message, quel que soit le nombre d'équipements concernés :
msg.app = {
queue = {
[DevEUI] = {
data = payload_hex,
fport = tonumber(cmd_str),
confirmed = false,
},
},
}
La file du moteur LoRa est indexée par DevEUI : elle porte donc plusieurs équipements à la
fois. Le message part vers le node lora app, qui met chaque commande en attente. Au
prochain réveil de la balise, la commande est émise pendant la fenêtre d'écoute — en classe A, la
passerelle ne peut pas parler quand elle veut, seulement répondre.
Le parser émettait auparavant une liste de messages, un par équipement. Le moteur de la passerelle n'accepte pas cette forme : il lit chaque élément du retour comme le message d'une sortie, et une liste n'est pas un message. C'est ce motif qui avait interrompu le renouvellement des jetons pendant 48 heures fin août.
Sur le chemin du downlink, il n'avait jamais explosé pour une seule raison : Asystom répond toujours à vide. La première commande réelle l'aurait déclenché. La correction ne demande aucun mécanisme nouveau — une seule file portant tous les équipements suffit.
Ce que dit le journal
| Marqueur | Signification |
|---|---|
ASYSTOM_DL_EMPTY | Rien à pousser. C'est le cas de tous les jours. |
ASYSTOM_DL_DISPATCH | Des commandes sont mises en file ; le compte et les équipements concernés sont indiqués. |
ASYSTOM_DL_SKIP | Des équipements sont annoncés mais aucune charge utile exploitable — à distinguer d'une réponse vide, sinon le problème resterait invisible. |
ASYSTOM_DL_KO | La plateforme a répondu autre chose qu'un succès. |
FPort downlink connus
| FPort | Action Asystom | Format payload |
|---|---|---|
| 64 | Reboot balise | "00" ou TODO précis Asystom |
| 70 | Set scheduling frequency | 054114XXXXXXXX6801 avec X = secondes/10 little-endian |
Helper Lua main_flow.build_frequency_payload(seconds) et build_frequency_command(seconds) dans main_flow.lua pour générer ces payloads.
Capteurs exclus de cette chaîne
Un capteur déclaré generic — d'une autre marque qu'Asystom — n'emprunte
pas cette chaîne. Ses mesures partent vers IoBase seul, qui se charge du décodage, et il
ne reçoit aucune commande. Le journal affiche alors ASYSTOM_SKIP_GENERIC : c'est le
comportement attendu, pas une anomalie. Voir Ajouter un capteur.
Variables à renseigner
| Variable | Description | Source |
|---|---|---|
global.asystom_url | URL base du serveur Asystom Advisor | Asystom |
global.asystom_auth_b64 | Base64 de user:password | Asystom |
global.asystom_network | Identifiant unique du réseau LoRaWAN (passé en header X-Network) | fourni par Asystom |
global.asystom_client | Asystom client identifier. Utilisé en body uplink ET en query param du polling DL. | Asystom |