Maintained JS version of default ADR algorithm

Continuing the discussion from Creating and registering ADR plugins for v4:

@orne what about a maintained JS version of the default algorithm? I saw that @Alex9779 has made one but its already outdated and does not contain all parts of the current default ADR algorithm implementation? Probably it would be better if the plugin example contains the current example.

Here is our current translation as an example:

// This must return the name of the ADR algorithm.
export function name() {
  return "Default ADR algorithm (LoRa only)";
}

// This must return the id of the ADR algorithm.
export function id() {
  return "default-custom";
}

export function handle(req) {
  let resp = {
      dr: req.dr,
      tx_power_index: req.tx_power_index,
      nb_trans: req.nb_trans,
  };

  // If ADR is disabled, return with current values.
  if (!req.adr) {
    return resp;
  }

  // The max DR might be configured to a non LoRa (125kHz) data-rate.
  // As this algorithm works on LoRa (125kHz) data-rates only, we need to
  // find the max LoRa (125 kHz) data-rate.
  const region_conf = region.get(req.region_config_id).context("Get region config for region");
  let max_dr = req.max_dr;
  let max_lora_dr = region_conf
    .get_enabled_uplink_data_rates()
    .filter((dr) => {
      let dataRate = region_conf.get_data_rate(dr);
      if (dataRate.modulation === "Lora") {
          return dataRate.bandwidth === 125000;
      }
      return false;
    })
    .reduce((max, dr) => Math.max(max, dr), 0);

  // Reduce to max LoRa DR.
  if (max_dr > max_lora_dr) {
      max_dr = max_lora_dr;
  }

  // Lower the DR only if it exceeds the max. allowed DR.
  if (req.dr > max_dr) {
      resp.dr = max_dr;
  }

  // Set the new nb_trans;
  resp.nb_trans = get_nb_trans(req.nb_trans, get_packet_loss_percentage(req));

  // Calculate the number of steps.
  let snr_max = get_max_snr(req);
  let snr_margin = snr_max - req.required_snr_for_dr - req.installation_margin;
  let n_step = Math.trunc(snr_margin / 3);

  // In case of negative steps, the ADR algorithm will increase the TxPower
  // if possible. To avoid up / down / up / down TxPower changes, wait until
  // we have at least the required number of uplink history elements.
  if (n_step < 0 && get_history_count(req) !== required_history_count()) {
    return resp;
  }

  let [desired_tx_power_index, desired_dr] = get_ideal_tx_power_index_and_dr(
    n_step,
    resp.tx_power_index,
    resp.dr,
    req.max_tx_power_index,
    max_dr
  );

  resp.dr = desired_dr;
  resp.tx_power_index = desired_tx_power_index;

  return resp;
}

function get_ideal_tx_power_index_and_dr(
  nb_step,
  tx_power_index,
  dr,
  max_tx_power_index,
  max_dr
) {
  if (nb_step === 0) {
    return [tx_power_index, dr];
  }

  if (nb_step > 0) {
    if (dr < max_dr) {
      // Increase the DR.
      dr += 1;
    } else if (tx_power_index < max_tx_power_index) {
      // Decrease the tx-power.
      // (note that an increase in index decreases the tx-power)
      tx_power_index += 1;
    }
    nb_step -= 1;
  } else {
    // Increase the tx-power.
    // (note that a decrease in index increases the tx-power)
    // Subtract only if > 0
    tx_power_index = Math.max(0, tx_power_index - 1);
    nb_step += 1;
  }

  return get_ideal_tx_power_index_and_dr(
    nb_step,
    tx_power_index,
    dr,
    max_tx_power_index,
    max_dr
  )
}

function required_history_count() {
  return 20;
}

function get_history_count(req) {
  return req.uplink_history.filter((x) => x.tx_power_index === req.tx_power_index).length;
}

function get_max_snr(req) {
  let max_snr = -999.0;

  for (const uh of req.uplink_history) {
    if (uh.max_snr > max_snr) {
      max_snr = uh.max_snr;
    }
  }

  return max_snr;
}

function get_nb_trans(current_nb_trans, pkt_loss_rate) {
  const pkt_loss_table = [[1, 1, 2], [1, 2, 3], [2, 3, 3], [3, 3, 3]];

  if (current_nb_trans < 1) {
    current_nb_trans = 1;
  }

  if (current_nb_trans > 3) {
    current_nb_trans = 3;
  }

  const nb_trans_index = current_nb_trans - 1;
  if (pkt_loss_rate < 5.0) {
    return pkt_loss_table[0][nb_trans_index];
  } else if (pkt_loss_rate < 10.0) {
    return pkt_loss_table[1][nb_trans_index];
  } else if (pkt_loss_rate < 30.0) {
    return pkt_loss_table[2][nb_trans_index];
  }

  return pkt_loss_table[3][nb_trans_index];
}

function get_packet_loss_percentage(req) {
  if (req.uplink_history.length < required_history_count()) {
    return 0.0;
  }

  let lost_packets = 0;
  let previous_f_cnt = 0;

  for (let i = 0; i < req.uplink_history.length; i++) {
    const h = req.uplink_history[i];

    if (i === 0) {
      previous_f_cnt = h.f_cnt;
      continue;
    }

    lost_packets += h.f_cnt - previous_f_cnt - 1; // there is always an expected difference of 1
    previous_f_cnt = h.f_cnt;
  }

  return (lost_packets / req.uplink_history.length) * 100.0;
}
2 Likes

Niceā€¦
Ya I didnā€˜t update because I am still on v3 because I was not able to get the custom algo to work though I think I transferred it right but it didnā€˜t behave as the built in algo.
Maybe Iā€˜ll give it a shot now with your implementation and the changes I wantā€¦

If one of you would be willing to make a pull-request, I agree it could be good to have a full example in the repository. Maybe we also keep the ā€œskeletonā€ example, just to document the interface without actual implementation + this one to also show people how a full implementation could look like?

May I ask in which configuration file should I put the plug-in for a custom adr?

You should add the custom ADR algorithms to the adr_plugins setting (I noticed this setting was not in the docs yet, I just pushed an update).

So I adapted your implementation but wasnā€™t able to try it out because of some problems migrating to v4 which I finally managed now.
But neither my adaption nor your code worked in my system.
After working out a method how to debug, I realized that you are using different styled variable names from the ā€œreqā€ object passed to the handle function. In the example the are camel cased without underscores. I assume this is one issue.

So I tried to find a solution how to debug (no realy experience with JS so far) so I built a bare HTML page with a debug JS which imports the functions from the ADR I want to debug an outputs the functions return values on that page, for the handle function with the input object for req from the example.
Yours is not working as expected but my old implementation worked without issues.

But initially when trying that implementation in the server it did not.
But now it does, no idea why but I do not get errors in the log when I switch back to my initial v4 implementation.

@Alex9779 youā€™re right and sorry for the late response

my code contained parts which are only working in rust environment because it uses classes/data which is not available within JS plugin.

So here is our version with support to reduce DR usage to DR 4 & 5 within a region, when you also want to support other DRs for other type of devices, when both type of devices uses the same gateway bridge:

// Based on official Plugin written in Rust: https://github.com/chirpstack/chirpstack/blob/master/chirpstack/src/adr/default.rs
// Only initial DR is set to a higher value
// Algorithm only increases DR if possible
// min-DR: 4
// max-DR: 5

// This must return the name of the ADR algorithm.
export function name() {
  return "Default ADR algorithm DR 4-5 (LoRa only)";
}

// This must return the id of the ADR algorithm.
export function id() {
  return "default-dr4-dr5";
}

// This handles the ADR request.
//
// Input object example:
// {
//  regionName: "eu868",
//  regionCommonName: "EU868",
//  devEui: "0102030405060708",
//  macVersion: "1.0.3",
//  regParamsRevision: "A",
//  adr: true,
//  dr: 1,
//  txPowerIndex: 0,
//  nbTrans: 1,
//  maxTxPowerIndex: 15,
//  requiredSnrForDr: -17.5,
//  installationMargin: 10,
//  minDr: 0,
//  maxDr: 5,
//  uplinkHistory: [
//    {
//      "fCnt": 10,
//      "maxSnr": 7.5,
//      "maxRssi": -110,
//      "txPowerIndex": 0,
//      "gatewayCount": 3
//    }
//  ]
// }
//
// This function must return an object, example:
// {
//  dr: 2,
//  txPowerIndex: 1,
//  nbTrans: 1
// }
export function handle(req) {
  //  using DR 4 as minimum DR
  let resp = {
      dr: Math.max(4, req.dr),
      txPowerIndex: req.txPowerIndex,
      nbTrans: req.nbTrans,
  };

  // If ADR is disabled, return with current values.
  if (!req.adr) {
    return resp;
  }

  // Lower the DR only if it exceeds the max. allowed DR.
  if (resp.dr > (req.maxDr || req.maxRr)) {
    resp.dr = (req.maxDr || req.maxRr);
  }

  // Set the new nbTrans;
  resp.nbTrans = getNbTrans(req.nbTrans, getPacketLossPercentage(req));

  // Calculate the number of steps.
  let snrMax = getMaxSnr(req);
  let snrMargin = snrMax - req.requiredSnrForDr - req.installationMargin;
  let nStep = Math.trunc(snrMargin / 3);

  // In case of negative steps, the ADR algorithm will increase the TxPower
  // if possible. To avoid up / down / up / down TxPower changes, wait until
  // we have at least the required number of uplink history elements.
  if (nStep < 0 && getHistoryCount(req) < requiredHistoryCount()) {
    return resp;
  }

  let idealValues = getIdealTxPowerIndexAndDr(nStep, req);

  resp.dr = idealValues.desiredDr;
  resp.txPowerIndex = idealValues.desiredTxPowerIndex;

  return resp;
}

function getIdealTxPowerIndexAndDr(nStep, req) {
  if (nStep === 0) {
    return {
      desiredTxPowerIndex: req.txPowerIndex,
      desiredDr: req.dr
    };
  }

  if (nStep > 0) {
    if (req.dr < (req.maxDr || req.maxRr)) {
      // Increase the DR.
      req.dr++;
    } else if (req.txPowerIndex < req.maxTxPowerIndex) {
      // Decrease the tx-power.
      // (note that an increase in index decreases the tx-power)
      req.txPowerIndex++;
    }
    nStep--;
  } else {
    // Increase the tx-power.
    // (note that a decrease in index increases the tx-power)
    // Subtract only if > 0
    req.txPowerIndex = Math.max(0, txPowerIndex - 1);
    nStep++;
  }

  return getIdealTxPowerIndexAndDr(nStep, req)
}

function requiredHistoryCount() {
  return 20;
}

function getHistoryCount(req) {
  return req.uplinkHistory.filter((x) => x.txPowerIndex === req.txPowerIndex).length;
}

function getMaxSnr(req) {
  let maxSnr = -999.0;

  for (const uh of req.uplinkHistory) {
    if (uh.maxSnr > maxSnr) {
      maxSnr = uh.maxSnr;
    }
  }

  return maxSnr;
}

function getNbTrans(currentNbTrans, pktLossRate) {
  const pktLossTable = [[1, 1, 2], [1, 2, 3], [2, 3, 3], [3, 3, 3]];

  if (currentNbTrans < 1) {
    currentNbTrans = 1;
  }

  if (currentNbTrans > 3) {
    currentNbTrans = 3;
  }

  const nbTransIndex = currentNbTrans - 1;
  if (pktLossRate < 5.0) {
    return pktLossTable[0][nbTransIndex];
  } else if (pktLossRate < 10.0) {
    return pktLossTable[1][nbTransIndex];
  } else if (pktLossRate < 30.0) {
    return pktLossTable[2][nbTransIndex];
  }

  return pktLossTable[3][nbTransIndex];
}

function getPacketLossPercentage(req) {
  if (req.uplinkHistory.length < requiredHistoryCount()) {
    return 0.0;
  }

  let lostPackets = 0;
  let previousFCnt = 0;

  for (let i = 0; i < req.uplinkHistory.length; i++) {
    const h = req.uplinkHistory[i];

    if (i === 0) {
      previousFCnt = h.fCnt;
      continue;
    }

    lostPackets += h.fCnt - previousFCnt - 1; // there is always an expected difference of 1
    previousFCnt = h.fCnt;
  }

  return (lostPackets / req.uplinkHistory.length) * 100.0;
}This text will be hidden

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.