lora-decoder / maccommands.js /
toto@bin.kawi.fr first commit
39e89d1 4 months ago
1 contributor
207 lines | 7.073kb
const maxEirpDecoded = [8,10,12,13,14,16,18,20,21,24,26,27,29,30,33,36];
const timingSetupReqDelay = [1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
const deviceModeClass = ["Class-A", "RFU", "Class-C"];

const macCommands = [
  { cid: 0x02, name: "LinkCheckReq", size: 0, dir: 0 },
  { cid: 0x02, name: "LinkCheckAns", size: 2, dir: 1 },
  { cid: 0x03, name: "LinkADRReq", size: 4, dir: 1 },
  { cid: 0x03, name: "LinkADRAns", size: 1, dir: 0 },
  { cid: 0x04, name: "DutyCycleReq", size: 1, dir: 1 },
  { cid: 0x04, name: "DutyCycleAns", size: 0, dir: 0 },
  { cid: 0x05, name: "RXParamSetupReq", size: 4, dir: 1 },
  { cid: 0x05, name: "RXParamSetupAns", size: 1, dir: 0 },
  { cid: 0x06, name: "DevStatusReq", size: 0, dir: 1 },
  { cid: 0x06, name: "DevStatusAns", size: 2, dir: 0 },
  { cid: 0x07, name: "NewChannelReq", size: 5, dir: 1 },
  { cid: 0x07, name: "NewChannelAns", size: 1, dir: 0 },
  { cid: 0x08, name: "RXTimingSetupReq", size: 1, dir: 1 },
  { cid: 0x08, name: "RXTimingSetupAns", size: 0, dir: 0 },
  { cid: 0x09, name: "TxParamSetupReq", size: 1, dir: 1 },
  { cid: 0x09, name: "TxParamSetupAns", size: 0, dir: 0 },
  { cid: 0x0A, name: "DlChannelReq", size: 4, dir: 1 },
  { cid: 0x0A, name: "DlChannelAns", size: 1, dir: 0 },
  { cid: 0x0B, name: "RekeyConf", size: 1, dir: 1 },
  { cid: 0x0B, name: "RekeyInd", size: 1, dir: 0 },
  { cid: 0x0C, name: "ADRParamSetupReq", size: 1, dir: 1 },
  { cid: 0x0C, name: "ADRParamSetupAns", size: 0, dir: 0 },
  { cid: 0x0D, name: "DeviceTimeReq", size: 0, dir: 0 },
  { cid: 0x0D, name: "DeviceTimeAns", size: 5, dir: 1 },
  { cid: 0x0E, name: "ForceRejoinReq", size: 2, dir: 1 },
  { cid: 0x0F, name: "RejoinParamSetupReq", size: 1, dir: 1 },
  { cid: 0x0F, name: "RejoinParamSetupAns", size: 1, dir: 0 },
  { cid: 0x10, name: "PingSlotInfoReq", size: 1, dir: 0 },
  { cid: 0x10, name: "PingSlotInfoAns", size: 0, dir: 1 },
  { cid: 0x11, name: "PingSlotChannelReq", size: 4, dir: 1 },
  { cid: 0x11, name: "PingSlotChannelAns", size: 1, dir: 0 },
  { cid: 0x12, name: "BeaconTimingReq", size: 0, dir: 0 },
  { cid: 0x12, name: "BeaconTimingAns", size: 3, dir: 1 },
  { cid: 0x13, name: "BeaconFreqReq", size: 3, dir: 1 },
  { cid: 0x13, name: "BeaconFreqAns", size: 0, dir: 0 },
  { cid: 0x20, name: "DeviceModeConf", size: 1, dir: 1 },
  { cid: 0x20, name: "DeviceModeInd", size: 1, dir: 0 }
];

let ulCids = [], dlCids = [];

function macDirection() {
  ulCids = macCommands.filter(c => c.dir === 0);
  dlCids = macCommands.filter(c => c.dir === 1);
}

function macGetCid(cid, dir = 0) {
  const list = dir ? dlCids : ulCids;
  return list.find(c => c.cid === cid) || null;
}

function hexToBytes(hex) {
  return hex.match(/.{1,2}/g).map(h => parseInt(h, 16));
}

// === Decode helpers ===
const handlers = {
  NewChannelReq: c => ({
    ChIndex: c[0],
    Frequency: 100 * (c[1] + (c[2]<<8) + (c[3]<<16)),
    "DrRange.MinDR": c[4] & 0x0F,
    "DrRange.MaxDR": (c[4] >> 4) & 0x0F
  }),
  NewChannelAns: c => ({
    ChannelFrequencyOK: c[0] & 0x01,
    DataRateRangeOK: (c[0] >> 1) & 0x01
  }),
  DevStatusAns: c => {
    let battery = c[0];
    if (battery === 0) battery = "ExternalPower";
    else if (battery === 255) battery = "ErrorMeasuring";
    let rs = c[1] & 0x3F;
    if (rs > 0x1F) rs -= 64;
    return { Battery: battery, RadioStatus: rs };
  },
  LinkCheckAns: c => ({ Margin: c[0], GwCnt: c[1] }),
  LinkADRReq: c => ({
    TxPower: c[0] & 0x0F,
    DataRate: (c[0] >> 4) & 0x0F,
    ChMask: ((c[1]) + (c[2]<<8)).toString(16).padStart(4, "0"),
    Redundancy: c[3]
  }),
  LinkADRAns: c => ({
    ChannelMaskACK: c[0] & 0x01,
    DataRateACK: (c[0] >> 1) & 0x01,
    PowerACK: (c[0] >> 2) & 0x01
  }),
  DutyCycleReq: c => ({ MaxDutyCycle: c[0] & 0x0F }),
  RXParamSetupAns: c => ({
    ChannelACK: c[0] & 0x01,
    RX2DataRateACK: (c[0] >> 1) & 0x01,
    RX1DROffsetACK: (c[0] >> 2) & 0x01
  }),
  RXParamSetupReq: c => ({
    RX2DataRate: c[0] & 0x0F,
    RX1DRoffset: (c[0] >> 4) & 0x0F,
    Frequency: 100 * (c[1] + (c[2]<<8) + (c[3]<<16))
  }),
  DlChannelReq: c => ({
    ChIndex: c[0],
    Frequency: 100 * (c[1] + (c[2]<<8) + (c[3]<<16))
  }),
  DlChannelAns: c => ({
    ChannelFrequencyOK: c[0] & 0x01,
    UplinkFrequencyExists: (c[0] >> 1) & 0x01
  }),
  TxParamSetupReq: c => ({
    MaxEIRP: c[0] & 0x0F,
    MaxEIRPdBm: maxEirpDecoded[c[0] & 0x0F] + " dBm",
    UplinkDwellTime: (c[0] >> 4) & 0x01,
    DownlinkDwellTime: (c[0] >> 5) & 0x01
  }),
  RXTimingSetupReq: c => ({
    Delay: c[0] & 0x0F,
    DelayDec: timingSetupReqDelay[c[0] & 0x0F]
  }),
  DeviceTimeAns: c => ({
    Epoch: c[0] + (c[1]<<8) + (c[2]<<16) + (c[3]<<24),
    FractionalSeconds: c[4] * 0.00390625
  }),
  RekeyConf: c => ({ LoRaWanVersion: c[0] & 0x0F }),
  RekeyInd: c => ({ LoRaWanVersion: c[0] & 0x0F }),
  ForceRejoinReq: c => ({
    DataRate: c[0] & 0x0F,
    RejoinType: (c[0] >> 4) & 0x07,
    MaxRetries: c[1] & 0x07,
    MaxRetriesDecoded: (c[1] & 0x07) + 1,
    Period: (c[1] >> 3) & 0x07
  }),
  RejoinParamSetupReq: c => ({
    MaxCountN: c[0] & 0x0F,
    MaxCountNDecoded: 1 << (c[0] & 0x0F),
    MaxTimeN: (c[0] >> 4) & 0x0F,
    MaxTimeNDecoded: 1 << ((c[0] >> 4) + 10)
  }),
  RejoinParamSetupAns: c => ({ TimeOK: c[0] & 0x01 }),
  PingSlotInfoReq: c => ({ Periodicity: c[0] & 0x07 }),
  PingSlotChannelReq: c => ({
    DataRate: c[0] & 0x0F,
    Frequency: 100 * (c[1] + (c[2]<<8) + (c[3]<<16))
  }),
  PingSlotChannelAns: c => ({
    FrequencyOK: c[0] & 0x01,
    DataRateOK: (c[0] >> 1) & 0x01
  }),
  BeaconTimingAns: c => ({
    Channel: c[0],
    Delay: c[1] + (c[2]<<8),
    DelayDecoded: 30 * (c[1] + (c[2]<<8))
  }),
  BeaconFreqReq: c => ({
    Frequency: 100 * (c[0] + (c[1]<<8) + (c[2]<<16))
  }),
  DeviceModeInd: c => ({
    DeviceClass: c[0],
    DeviceClassDecoded: deviceModeClass[c[0]]
  }),
  DeviceModeConf: c => ({
    DeviceClass: c[0],
    DeviceClassDecoded: deviceModeClass[c[0]]
  })
};

// Main decoder
function macDecode(direction, hexStr) {
  const payload = hexToBytes(hexStr);
  const result = [];
  let i = 0;

  while (i < payload.length) {
    const cid = payload[i];
    const cmd = macGetCid(cid, direction);
    if (!cmd) {
      result.push({
        cid: `0x${cid.toString(16).padStart(2, "0")}`,
        name: "Unknown",
        raw: payload.slice(i + 1).map(b => b.toString(16).padStart(2, "0")).join('')
      });
      break;
    }

    const data = payload.slice(i + 1, i + 1 + cmd.size);
    const decoded = {
      cid: `0x${cid.toString(16).padStart(2, "0")}`,
      name: cmd.name,
      raw: data.map(b => b.toString(16).padStart(2, "0")).join(''),
      length: cmd.size
    };

    if (handlers[cmd.name]) decoded.data = handlers[cmd.name](data);
    else if (cmd.size > 0) decoded.data = { warning: "Not decoded currently" };

    result.push(decoded);
    i += 1 + cmd.size;
  }

  return result;
}

// Init
macDirection();