04 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_OUTASYSTOM_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 ErrorMis en queue offline (queueenabled=true), replay auto.

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

Pour chaque device, le parser construit :

msg.app = {
  queue = {
    [DevEUI] = {
      data      = payload_hex,
      fport     = tonumber(cmd_str),
      confirmed = false,
    },
  },
}

Et l'émet sur output 0 (multi-msg). La sortie est wirée vers le node lora-app input qui queue le downlink. Au prochain uplink de la balise (Class A), le downlink est envoyé pendant la wait window.

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.

Globals à configurer

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)convention déploiement (ex: cloudgate-XX)
global.asystom_clientAsystom client identifier. Utilisé en body uplink ET en query param du polling DL.Asystom