External ADR (mt-adr)
Model catalog
Search for these models in ThinkLink by name or ID.
| Model type | Name | id_name | Platform model ID |
|---|---|---|---|
| RPC | External ADR Config | mt_adr_config | 118745136434671616 |
| RPC | External ADR Engine | mt_adr_engine | 118745135679696896 |
| Trigger | External ADR Trigger (generic) | mt_adr_trigger | 118745137390972928 |
Operations Skill
Use mt-adr-operations to configure ADR, analyze trigger gates and SNR/DR decisions, or diagnose LinkADRReq downlinks. Confirm the regional band and ADR owner first, then verify the actual DR on later uplinks.
Supported platform version: ThinkLink ≥ 2.00.031 | Last updated: 2026-06-14
What it is
The ThinkLink platform's Network Server (NS) does not perform server-side ADR (Adaptive Data Rate). This capability externalizes an ADR engine at the application layer: each uplink fires a trigger → margin algorithm → MAC downlink (LinkADRReq) feedback loop that automatically adjusts the data rate (DR/SF) for LoRaWAN devices.
- Only DR/SF is adjusted. TX power and NbTrans are left unchanged.
- Multi-band support: CN470 / EU868 / US915 (see Notes for US915 caveats).
- Engine state is stored in
server_attrs.adr_ext.stateon the device — no persistent schema changes are required on the platform side.
Components
| Artifact | Identifier | Role |
|---|---|---|
| Trigger | mt_adr_trigger | Uplink hook + gate: filters invalid frames and invokes the engine RPC |
| Engine RPC | mt_adr_engine | Margin algorithm + sliding-window state + generates the MAC downlink |
| Config RPC | mt_adr_config | Writes server_attrs.adr_ext (enable flag + parameters) |
Enabling steps
Verify prerequisites: the thing model bound to the target device must expose
snr,adr, anddatrin its telemetry (see next section).Configure parameters: on the device detail page, execute the
mt_adr_configRPC and fill in the following parameters:Parameter Description Default / Recommended enabledEnable the ADR engine false(set totrueto activate)margin_dbSafety margin (dB) — conservative buffer before stepping up the DR 10window_sizeSliding window size (frames); max SNR is taken over this many frames 20min_samplesMinimum frames to collect before making any decision 8min_interval_framesMinimum frames between consecutive DR adjustments (debounce) 5bandFrequency band: CN470/EU868/US915Match deployment band (default CN470)chmaskChannel mask (16-bit; all-on = 65535) 65535chmask_cntlChMaskCntl (EU868/CN470=6 all-on; US915 125 kHz=6) 6Bind thing models (deferred to owner): add the target thing model IDs to the trigger's
thing_model_ids. Until bound, the trigger remains completely silent for that thing model.
Prerequisite: thing model must expose three fields
The payload_parser of the device's bound thing model must write snr, adr, and datr into the telemetry object.
- Setting
frameInfo.rssi = truealready injectssnr(andrssi) automatically. adranddatrmust be explicitly extracted from the uplinkuserdata.motetx.
Add the following snippet to the payload_parser function body of the relevant thing model:
if (this.msg?.userdata?.motetx) {
this.tdata.adr = this.msg.userdata.motetx.adr === true; // uplink ADR bit
this.tdata.datr = this.msg.userdata.motetx.datr; // e.g. "SF7BW125"
}After updating the thing model, push it to the platform and confirm that adr and datr appear in live telemetry before proceeding to step 2.
Gating (when it does nothing)
If any of the following conditions is true, the trigger returns null immediately and the engine does not run:
| Condition | Reason |
|---|---|
server_attrs.adr_ext.enabled !== true | ADR not enabled via the config RPC |
Uplink adr !== true | The device has not set its ADR bit — the NS requires device consent before sending ADR commands |
Telemetry missing snr or datr | The thing model prerequisite is not met; the algorithm cannot proceed |
Algorithm
On each valid uplink the engine updates the sliding window and makes a DR decision as follows:
Accumulate samples: collect SNR values for the last
window_sizeframes. No decision is made until at leastmin_samplesframes have been seen.Compute margin:
maxSNR = max(SNR over last window_size frames) margin = maxSNR − requiredSNR(currentDR) − margin_db drStep = floor(margin / 3)requiredSNR reference values (demodulation floor per SF, in dB):
SF / DR requiredSNR (dB) SF7 −7.5 SF8 −10.0 SF9 −12.5 SF10 −15.0 SF11 −17.5 SF12 −20.0 Debounce: if fewer than
min_interval_framesframes have elapsed since the last DR change, skip the current decision.Apply:
drStep > 0→ step DR up;drStep < 0→ step DR down;drStep == 0→ keep current DR.
MAC downlink
When a decision is made the engine RPC returns a thinkOne action that causes the platform to send a LinkADRReq FOpts frame to the device:
{
"type": "mac",
"userdata": {
"port": -1,
"payload": "0x03..."
}
}port: -1signals a MAC command (FOpts layer); no application port is consumed.payloadis the hex-encoded LinkADRReq byte string. The NS handles encryption and framing automatically — no additional handling is required at the application layer.
Notes
Channel mask overwrites the existing channel plan (CN470 / EU868 / US915)
Every LinkADRReq carries ChMask (a mandatory command field), and the engine sends all-on by default (ChMask=0xFFFF, ChMaskCntl=6). This means each DR adjustment resets the device's channel plan to all-on, overwriting any custom channel mask the operator previously pushed. If your deployment customizes channels (fixed sub-band, avoiding interfered channels), set chmask/chmask_cntl explicitly via mt_adr_config to match the live channel plan so the engine does not overwrite it with all-on. The config RPC already rejects chmask=0 (which would disable all channels and brick the device).
US915 channel mask
US915 channel-mask paging is non-trivial: ChMaskCntl values 0–5 each govern a different 125 kHz sub-band; ChMaskCntl=6 enables all 125 kHz channels; ChMaskCntl=7 enables all 500 kHz channels. The current v1 implementation treats US915 as "all 125 kHz channels enabled" (ChMaskCntl=6, ChMask=0xFFFF), which is correct when a standard 8-channel gateway covers every sub-band. Before deploying in a US915 environment, verify the channel plan with your gateway/NS operator to confirm the mask matches the actual channel configuration. An incorrect mask can cause devices to stop receiving downlinks. Note: the engine now resolves the current DR by exact SF + bandwidth match (BUG-035 fixed 2026-06-15), so US915 SF8BW500 (DR4) vs SF8BW125 (DR2) and EU868 SF7BW250 (DR6) vs SF7BW125 (DR5) are correctly distinguished — the earlier "SF-only mis-mapping" no longer occurs; still limit dr_min/dr_max to the DR range your gateway supports.
The engine does not track LinkADRAns (fire-and-forget)
After sending a LinkADRReq, the engine does not parse the device's LinkADRAns (PowerACK/DataRateACK/ChannelMaskACK status bits), so it has no explicit awareness of a device NACK. It currently relies on self-correction ("next frame datr unchanged → margin still satisfied → retry"), which is acceptable but not robust. A strict closed loop (echo the three ACK bits + NACK backoff/alert) is a future enhancement. state.last_dr records the last target DR the engine commanded and is kept for diagnostics only (it does not drive decisions — curDR is always computed fresh from the current frame's datr).
Do not hand-edit the state field
server_attrs.adr_ext.state is owned entirely by the engine (sliding window buffer, frame counter since last adjustment, etc.). Operators must not directly modify this field via the platform UI or any script. Doing so will corrupt the debounce logic or put the state machine out of sync. To reset the engine, re-execute mt_adr_config (omitting the state key); the engine will reinitialize with a clean state on the next uplink.
Binding deferred to owner
The trigger mt_adr_trigger is deployed with thing_model_ids intentionally left empty. The owner is responsible for binding the appropriate thing model IDs based on the actual deployment scope. Until a thing model is bound, the trigger is completely inert for devices using that model — no DR commands will be issued.