T4 API Asystom Advisor

Spec d'intégration LoRaWAN Asystom · Flux 02 + 03
✅ Intégration validée end-to-end — 2026-06-10

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

URLPOST <asystom_url>/lorawan
MéthodePOST
Content-Typeapplication/json
AuthAuthorization: Basic <base64(user:password)>

Headers obligatoires

HeaderSource canvas
Authorization'Basic ' .. global.asystom_auth_b64
Content-Typeapplication/json
X-Networkglobal.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

ChampSource canvas
endDevice.devEuimsg.DevEUI
endDevice.devAddrmsg.DevAddr
fPortmsg.fport
fCntUpmsg.FCntUp
payloadHex uppercase de msg.payload
encodingTypeHardcoded "HEXA"
recvTimeos.time() * 1000 (epoch ms)
clientglobal.asystom_client (Asystom client identifier, utilisé aussi pour le polling downlink)
dataRateHardcoded "SF7BW125" pour l'instant (TODO : extraire du msg)
gwInfo[0].rfRegionHardcoded "EU868"
gwInfo[0].rssimsg.rssi
gwInfo[0].snrmsg.snr

Codes de réponse

CodeStatutComportement canvas
201Frame processedglobal.last_asystom_ok_ts = os.time()
401Invalid credentialsAsystom = Basic auth (pas DAP). 401 = mauvais credentials → log + alerte. Ne déclenche pas le retry DAP.
500Server ErrorJournalisé. 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

CloudGate
Asystom Advisor
cron 60s · GET /getPendingMsgs?client=<id>
Basic auth
200 · {device_list, payload, cmd}
vide si rien à pousser
parse_asystom_dl
multi-msg → lora-app input
msg.app.queue[DevEUI]
downlink LoRa Class A
wait window après prochain uplink balise
📡 balise reçoit cmd 70 (set freq)
ou cmd 64 (reboot)

Endpoint

URLGET <asystom_url>/getPendingMsgs?client=<asystom_client>
MéthodeGET
Content-Typeapplication/x-www-form-urlencoded
AuthBasic <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.

Ce mécanisme était cassé jusqu'au 4 septembre 2026

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

MarqueurSignification
ASYSTOM_DL_EMPTYRien à pousser. C'est le cas de tous les jours.
ASYSTOM_DL_DISPATCHDes commandes sont mises en file ; le compte et les équipements concernés sont indiqués.
ASYSTOM_DL_SKIPDes équipements sont annoncés mais aucune charge utile exploitable — à distinguer d'une réponse vide, sinon le problème resterait invisible.
ASYSTOM_DL_KOLa plateforme a répondu autre chose qu'un succès.

FPort downlink connus

FPortAction AsystomFormat payload
64Reboot balise"00" ou TODO précis Asystom
70Set scheduling frequency054114XXXXXXXX6801 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

VariableDescriptionSource
global.asystom_urlURL base du serveur Asystom AdvisorAsystom
global.asystom_auth_b64Base64 de user:passwordAsystom
global.asystom_networkIdentifiant unique du réseau LoRaWAN (passé en header X-Network)fourni par Asystom
global.asystom_clientAsystom client identifier. Utilisé en body uplink ET en query param du polling DL.Asystom