1 contributor
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();