lora-packet
Version:
LoRa packet decoder/encoder
934 lines (834 loc) • 31.6 kB
text/typescript
import { reverseBuffer, asHexString } from "./util";
import { decrypt, decryptJoin, decryptFOpts } from "./crypto";
import { recalculateMIC } from "./mic";
enum MType {
JOIN_REQUEST = 0,
JOIN_ACCEPT = 1,
UNCONFIRMED_DATA_UP = 2,
UNCONFIRMED_DATA_DOWN = 3,
CONFIRMED_DATA_UP = 4,
CONFIRMED_DATA_DOWN = 5,
REJOIN_REQUEST = 6,
}
const MTYPE_DESCRIPTIONS: { [key: number]: string } = {
[MType.JOIN_REQUEST]: "Join Request",
[MType.JOIN_ACCEPT]: "Join Accept",
[MType.UNCONFIRMED_DATA_UP]: "Unconfirmed Data Up",
[MType.UNCONFIRMED_DATA_DOWN]: "Unconfirmed Data Down",
[MType.CONFIRMED_DATA_UP]: "Confirmed Data Up",
[MType.CONFIRMED_DATA_DOWN]: "Confirmed Data Down",
[MType.REJOIN_REQUEST]: "Rejoin Request",
};
const DESCRIPTIONS_MTYPE: { [description: string]: MType } = Object.keys(MTYPE_DESCRIPTIONS).reduce((acc, key) => {
const mTypeKey = (key as unknown) as MType; // Cast the key to MType
const description = MTYPE_DESCRIPTIONS[mTypeKey];
acc[description] = mTypeKey;
return acc;
}, {} as { [description: string]: MType });
type Range = {
start: number;
end: number;
};
type PacketStructures = {
[key: string]: {
[key: string]: Range;
};
};
const PACKET_STRUCTURES: PacketStructures = {
JOIN_REQUEST: {
AppEUI: { start: 1, end: 9 },
DevEUI: { start: 9, end: 17 },
DevNonce: { start: 17, end: 19 },
},
JOIN_ACCEPT: {
AppNonce: { start: 1, end: 4 },
NetID: { start: 4, end: 7 },
DevAddr: { start: 7, end: 11 },
DLSettings: { start: 11, end: 12 },
RxDelay: { start: 12, end: 13 },
},
REJOIN_TYPE_1: {
NetID: { start: 2, end: 5 },
DevEUI: { start: 5, end: 13 },
RJCount0: { start: 13, end: 15 },
},
REJOIN_TYPE_2: {
JoinEUI: { start: 2, end: 10 },
DevEUI: { start: 10, end: 18 },
RJCount1: { start: 13, end: 15 },
},
};
enum LorawanVersion {
V1_0 = "1.0",
V1_1 = "1.1",
}
enum Masks {
FCTRL_ADR = 0x80,
FCTRL_ADRACKREQ = 0x40,
FCTRL_ACK = 0x20,
FCTRL_FPENDING = 0x10,
DLSETTINGS_RXONEDROFFSET_MASK = 0x70,
DLSETTINGS_RXONEDROFFSET_POS = 4,
DLSETTINGS_RXTWODATARATE_MASK = 0x0f,
DLSETTINGS_RXTWODATARATE_POS = 0,
DLSETTINGS_OPTNEG_MASK = 0x80,
DLSETTINGS_OPTNEG_POS = 7,
RXDELAY_DEL_MASK = 0x0f,
RXDELAY_DEL_POS = 0,
}
export interface UserFields {
CFList?: Buffer;
RxDelay?: Buffer | number;
DLSettings?: Buffer | number;
NetID?: Buffer;
AppNonce?: Buffer;
DevNonce?: Buffer;
DevEUI?: Buffer;
AppEUI?: Buffer;
FPort?: number;
FOpts?: string | Buffer;
FCnt?: number | Buffer;
MType?: string | number;
DevAddr?: Buffer;
payload?: string | Buffer;
FCtrl?: {
ADR?: boolean;
ADRACKReq?: boolean;
ACK?: boolean;
FPending?: boolean;
};
JoinReqType?: Buffer | number;
}
function extractBytesFromBuffer(buffer: Buffer, start: number, end: number): Buffer {
return reverseBuffer(buffer.slice(start, end));
}
function extractStructuredBytesFromBuffer(buffer: Buffer, name: string): { [key: string]: Buffer } {
const structure = PACKET_STRUCTURES[name];
const ret: { [key: string]: Buffer } = {};
for (const key in structure) {
if (structure.hasOwnProperty(key)) {
ret[key] = extractBytesFromBuffer(buffer, structure[key].start, structure[key].end);
}
}
return ret;
}
class LoraPacket {
static fromWire(buffer: Buffer): LoraPacket {
const payload = new LoraPacket();
payload._initfromWire(buffer);
return payload;
}
static fromFields(
fields: UserFields,
AppSKey?: Buffer,
NwkSKey?: Buffer,
AppKey?: Buffer,
FCntMSBytes?: Buffer,
ConfFCntDownTxDrTxCh?: Buffer
): LoraPacket {
if (!FCntMSBytes) FCntMSBytes = Buffer.alloc(2, 0);
const payload = new LoraPacket();
payload._initFromFields(fields);
if (payload.isDataMessage()) {
// to encrypt, need NwkSKey if port=0, else AppSKey
const port = payload.getFPort();
if (port !== null && ((port === 0 && NwkSKey?.length === 16) || (port > 0 && AppSKey?.length === 16))) {
// crypto is reversible (just XORs FRMPayload), so we can
// just do "decrypt" on the plaintext to get ciphertext
let ciphertext: Buffer;
if (port === 0 && NwkSKey?.length === 16 && AppSKey?.length === 16 && AppKey?.length === 16) {
ciphertext = decrypt(payload, undefined, AppSKey, FCntMSBytes);
} else {
ciphertext = decrypt(payload, AppSKey, NwkSKey, FCntMSBytes);
}
// overwrite payload with ciphertext
payload.FRMPayload = ciphertext;
// recalculate buffers to be ready for MIC calc'n
payload._mergeGroupFields();
if (NwkSKey?.length === 16) {
recalculateMIC(payload, NwkSKey, AppKey, FCntMSBytes, ConfFCntDownTxDrTxCh);
payload._mergeGroupFields();
}
}
} else if (payload._getMType() === MType.JOIN_REQUEST) {
if (AppKey?.length === 16) {
recalculateMIC(payload, NwkSKey, AppKey, FCntMSBytes);
payload._mergeGroupFields();
}
} else if (payload._getMType() === MType.JOIN_ACCEPT) {
if (AppKey?.length === 16) {
recalculateMIC(payload, NwkSKey, AppKey, FCntMSBytes);
payload._mergeGroupFields();
const ciphertext = decryptJoin(payload, AppKey);
// overwrite payload with ciphertext
if (payload.MACPayloadWithMIC) ciphertext.copy(payload.MACPayloadWithMIC);
}
}
return payload;
}
private assignFromStructuredBuffer(buffer: Buffer, structure: string) {
const fields = extractStructuredBytesFromBuffer(buffer, structure);
Object.assign(this, fields);
}
private _initfromWire(contents: Buffer): void {
const incoming = Buffer.from(contents);
this.PHYPayload = incoming;
this.MHDR = incoming.slice(0, 1);
this.MACPayload = incoming.slice(1, incoming.length - 4);
this.MACPayloadWithMIC = incoming.slice(1, incoming.length);
this.MIC = incoming.slice(incoming.length - 4);
const mtype = this._getMType();
if (mtype == MType.JOIN_REQUEST) {
if (incoming.length < 5 + 18) {
throw new Error("contents too short for a Join Request");
}
this.assignFromStructuredBuffer(incoming, "JOIN_REQUEST");
} else if (mtype == MType.JOIN_ACCEPT) {
if (incoming.length < 5 + 12) {
throw new Error("contents too short for a Join Accept");
}
this.assignFromStructuredBuffer(incoming, "JOIN_ACCEPT");
this.JoinReqType = Buffer.from([0xff]);
if (incoming.length == 13 + 16 + 4) {
this.CFList = incoming.slice(13, 13 + 16);
} else {
this.CFList = Buffer.alloc(0);
}
} else if (mtype == MType.REJOIN_REQUEST) {
this.RejoinType = incoming.slice(1, 1 + 1);
if (this.RejoinType[0] === 0 || this.RejoinType[0] === 2) {
if (incoming.length < 5 + 14) {
throw new Error("contents too short for a Rejoin Request (Type 0/2)");
}
this.assignFromStructuredBuffer(incoming, "REJOIN_TYPE_1");
} else if (this.RejoinType[0] === 1) {
if (incoming.length < 5 + 19) {
throw new Error("contents too short for a Rejoin Request (Type 1)");
}
this.assignFromStructuredBuffer(incoming, "REJOIN_TYPE_2");
}
} else if (this.isDataMessage()) {
this.DevAddr = reverseBuffer(incoming.slice(1, 5));
this.FCtrl = reverseBuffer(incoming.slice(5, 6));
this.FCnt = reverseBuffer(incoming.slice(6, 8));
const FCtrl = this.FCtrl.readInt8(0);
const FOptsLen = FCtrl & 0x0f;
this.FOpts = incoming.slice(8, 8 + FOptsLen);
const FHDR_length = 7 + FOptsLen;
this.FHDR = incoming.slice(1, 1 + FHDR_length);
if (FHDR_length == this.MACPayload.length) {
this.FPort = Buffer.alloc(0);
this.FRMPayload = Buffer.alloc(0);
} else {
this.FPort = incoming.slice(FHDR_length + 1, FHDR_length + 2);
this.FRMPayload = incoming.slice(FHDR_length + 2, incoming.length - 4);
}
}
}
private _initFromFields(userFields: UserFields): void {
if (typeof userFields.MType !== "undefined") {
let MTypeNo;
if (typeof userFields.MType === "number") {
MTypeNo = userFields.MType;
} else if (typeof userFields.MType == "string") {
const mhdr_idx = DESCRIPTIONS_MTYPE[userFields.MType];
if (mhdr_idx >= 0) {
MTypeNo = mhdr_idx;
} else {
throw new Error("MType is unknown");
}
} else {
throw new Error("MType is required in a suitable format");
}
if (MTypeNo == MType.JOIN_REQUEST) {
this._initialiseJoinRequestPacketFromFields(userFields);
} else if (MTypeNo == MType.JOIN_ACCEPT) {
this._initialiseJoinAcceptPacketFromFields(userFields);
} else {
this._initialiseDataPacketFromFields(userFields);
}
} else {
if (userFields.DevAddr && typeof userFields.payload !== "undefined") {
this._initialiseDataPacketFromFields(userFields);
} else if (userFields.AppEUI && userFields.DevEUI && userFields.DevNonce) {
this._initialiseJoinRequestPacketFromFields(userFields);
} else if (userFields.AppNonce && userFields.NetID && userFields.DevAddr) {
this._initialiseJoinAcceptPacketFromFields(userFields);
} else {
throw new Error("No plausible packet");
}
}
}
private _mergeGroupFields(): void {
if (this.MHDR && this.MIC) {
if (this._getMType() === MType.JOIN_REQUEST && this.AppEUI && this.DevEUI && this.DevNonce) {
this.MACPayload = Buffer.concat([
reverseBuffer(this.AppEUI),
reverseBuffer(this.DevEUI),
reverseBuffer(this.DevNonce),
]);
this.PHYPayload = Buffer.concat([this.MHDR, this.MACPayload, this.MIC]);
this.MACPayloadWithMIC = this.PHYPayload.slice(this.MHDR.length, this.PHYPayload.length);
} else if (
this._getMType() === MType.JOIN_ACCEPT &&
this.AppNonce &&
this.NetID &&
this.DevAddr &&
this.DLSettings &&
this.RxDelay &&
this.CFList
) {
this.MACPayload = Buffer.concat([
reverseBuffer(this.AppNonce),
reverseBuffer(this.NetID),
reverseBuffer(this.DevAddr),
this.DLSettings,
this.RxDelay,
this.CFList,
]);
this.PHYPayload = Buffer.concat([this.MHDR, this.MACPayload, this.MIC]);
this.MACPayloadWithMIC = this.PHYPayload.slice(this.MHDR.length, this.PHYPayload.length);
} else if (this.FCtrl && this.DevAddr && this.FPort && this.FCnt && this.FRMPayload && this.FOpts) {
this.FHDR = Buffer.concat([reverseBuffer(this.DevAddr), this.FCtrl, reverseBuffer(this.FCnt), this.FOpts]);
this.MACPayload = Buffer.concat([this.FHDR, this.FPort, this.FRMPayload]);
this.PHYPayload = Buffer.concat([this.MHDR, this.MACPayload, this.MIC]);
this.MACPayloadWithMIC = this.PHYPayload.slice(this.MHDR.length, this.PHYPayload.length);
}
}
}
private _initialiseDataPacketFromFields(userFields: UserFields): void {
if (userFields.DevAddr && userFields.DevAddr.length == 4) {
this.DevAddr = Buffer.from(userFields.DevAddr);
} else {
throw new Error("DevAddr is required in a suitable format");
}
if (typeof userFields.payload === "string") {
this.FRMPayload = Buffer.from(userFields.payload);
} else if (userFields.payload instanceof Buffer) {
this.FRMPayload = Buffer.from(userFields.payload);
}
if (typeof userFields.MType !== "undefined") {
if (typeof userFields.MType === "number") {
this.MHDR = Buffer.alloc(1);
this.MHDR.writeUInt8(userFields.MType << 5, 0);
} else if (typeof userFields.MType === "string") {
const mhdr_idx = DESCRIPTIONS_MTYPE[userFields.MType];
if (mhdr_idx >= 0) {
this.MHDR = Buffer.alloc(1);
this.MHDR.writeUInt8(mhdr_idx << 5, 0);
} else {
throw new Error("MType is unknown");
}
} else {
throw new Error("MType is required in a suitable format");
}
}
if (userFields.FCnt) {
if (userFields.FCnt instanceof Buffer && userFields.FCnt.length == 2) {
this.FCnt = Buffer.from(userFields.FCnt);
} else if (typeof userFields.FCnt === "number") {
this.FCnt = Buffer.alloc(2);
this.FCnt.writeUInt16BE(userFields.FCnt, 0);
} else {
throw new Error("FCnt is required in a suitable format");
}
}
if (typeof userFields.FOpts !== "undefined") {
if (typeof userFields.FOpts === "string") {
this.FOpts = Buffer.from(userFields.FOpts, "hex");
} else if (userFields.FOpts instanceof Buffer) {
this.FOpts = Buffer.from(userFields.FOpts);
} else {
throw new Error("FOpts is required in a suitable format");
}
if (15 < this.FOpts.length) {
throw new Error("Too many options for piggybacking");
}
} else {
this.FOpts = Buffer.from("", "hex");
}
let fctrl = 0;
if (userFields.FCtrl?.ADR) {
fctrl |= Masks.FCTRL_ADR;
}
if (userFields.FCtrl?.ADRACKReq) {
fctrl |= Masks.FCTRL_ADRACKREQ;
}
if (userFields.FCtrl?.ACK) {
fctrl |= Masks.FCTRL_ACK;
}
if (userFields.FCtrl?.FPending) {
fctrl |= Masks.FCTRL_FPENDING;
}
fctrl |= this.FOpts.length & 0x0f;
this.FCtrl = Buffer.alloc(1);
this.FCtrl.writeUInt8(fctrl, 0);
if (!isNaN(userFields.FPort) && userFields.FPort >= 0 && userFields.FPort <= 255) {
this.FPort = Buffer.alloc(1);
this.FPort.writeUInt8(userFields.FPort, 0);
}
if (!this?.MHDR) {
this.MHDR = Buffer.alloc(1);
this.MHDR.writeUInt8(MType.UNCONFIRMED_DATA_UP << 5, 0);
}
if (this?.FPort == null) {
if (this?.FRMPayload && this.FRMPayload.length > 0) {
this.FPort = Buffer.from("01", "hex");
} else {
this.FPort = Buffer.alloc(0);
}
}
if (!this?.FPort == null) {
this.FPort = Buffer.from("01", "hex");
}
if (!this.FCnt) {
this.FCnt = Buffer.from("0000", "hex");
}
if (!this.MIC) {
this.MIC = Buffer.from("EEEEEEEE", "hex");
}
this._mergeGroupFields();
}
private _initialiseJoinRequestPacketFromFields(userFields: UserFields): void {
if (userFields.AppEUI && userFields.AppEUI.length == 8) {
this.AppEUI = Buffer.from(userFields.AppEUI);
} else {
throw new Error("AppEUI is required in a suitable format");
}
if (userFields.DevEUI && userFields.DevEUI.length == 8) {
this.DevEUI = Buffer.from(userFields.DevEUI);
} else {
throw new Error("DevEUI is required in a suitable format");
}
if (userFields.DevNonce && userFields.DevNonce.length == 2) {
this.DevNonce = Buffer.from(userFields.DevNonce);
} else {
throw new Error("DevNonce is required in a suitable format");
}
if (userFields.FCnt) {
if (userFields.FCnt instanceof Buffer && userFields.FCnt.length == 2) {
this.FCnt = Buffer.from(userFields.FCnt);
} else if (typeof userFields.FCnt === "number") {
this.FCnt = Buffer.alloc(2);
this.FCnt.writeUInt16BE(userFields.FCnt, 0);
} else {
throw new Error("FCnt is required in a suitable format");
}
}
this.MHDR = Buffer.alloc(1);
this.MHDR.writeUInt8(MType.JOIN_REQUEST << 5, 0);
if (!this.MIC) {
this.MIC = Buffer.from("EEEEEEEE", "hex");
}
this._mergeGroupFields();
}
private _initialiseJoinAcceptPacketFromFields(userFields: UserFields): void {
if (userFields.AppNonce && userFields.AppNonce.length == 3) {
this.AppNonce = Buffer.from(userFields.AppNonce);
} else {
throw new Error("AppNonce is required in a suitable format");
}
if (userFields.NetID && userFields.NetID.length == 3) {
this.NetID = Buffer.from(userFields.NetID);
} else {
throw new Error("NetID is required in a suitable format");
}
if (userFields.DevAddr && userFields.DevAddr.length == 4) {
this.DevAddr = Buffer.from(userFields.DevAddr);
} else {
throw new Error("DevAddr is required in a suitable format");
}
if (userFields.DLSettings) {
if (userFields.DLSettings instanceof Buffer && userFields.DLSettings.length == 1) {
this.DLSettings = Buffer.from(userFields.DLSettings);
} else if (typeof userFields.DLSettings === "number") {
this.DLSettings = Buffer.alloc(1);
this.DLSettings.writeUInt8(userFields.DLSettings, 0);
} else {
throw new Error("DLSettings is required in a suitable format");
}
}
if (userFields.RxDelay) {
if (userFields.RxDelay instanceof Buffer && userFields.RxDelay.length == 1) {
this.RxDelay = Buffer.from(userFields.RxDelay);
} else if (typeof userFields.RxDelay == "number") {
this.RxDelay = Buffer.alloc(1);
this.RxDelay.writeUInt8(userFields.RxDelay, 0);
} else {
throw new Error("RxDelay is required in a suitable format");
}
}
if (userFields.CFList) {
if (userFields.CFList instanceof Buffer && (userFields.CFList.length == 0 || userFields.CFList.length == 16)) {
this.CFList = Buffer.from(userFields.CFList);
} else {
throw new Error("CFList is required in a suitable format");
}
}
if (!userFields.JoinReqType) {
this.JoinReqType = Buffer.from("ff", "hex");
} else {
if (userFields.JoinReqType instanceof Buffer && userFields.JoinReqType.length == 1) {
this.JoinReqType = Buffer.from(userFields.JoinReqType);
} else if (typeof userFields.JoinReqType === "number") {
this.JoinReqType = Buffer.alloc(1);
this.JoinReqType.writeUInt8(userFields.JoinReqType, 0);
} else {
throw new Error("JoinReqType is required in a suitable format");
}
}
if (userFields.AppEUI && userFields.AppEUI.length == 8) {
this.AppEUI = Buffer.from(userFields.AppEUI);
} else if (this.getDLSettingsOptNeg()) {
throw new Error("AppEUI/JoinEUI is required in a suitable format");
}
if (userFields.DevNonce && userFields.DevNonce.length == 2) {
this.DevNonce = Buffer.from(userFields.DevNonce);
} else if (this.getDLSettingsOptNeg()) {
throw new Error("DevNonce is required in a suitable format");
}
if (!this.DLSettings) {
this.DLSettings = Buffer.from("00", "hex");
}
if (!this.RxDelay) {
this.RxDelay = Buffer.from("00", "hex");
}
if (!this.CFList) {
this.CFList = Buffer.from("", "hex");
}
this.MHDR = Buffer.alloc(1);
this.MHDR.writeUInt8(MType.JOIN_ACCEPT << 5, 0);
if (!this.MIC) {
this.MIC = Buffer.from("EEEEEEEE", "hex");
}
this._mergeGroupFields();
}
private _getMType(): number {
if (this.MHDR) return (this.MHDR.readUInt8(0) & 0xff) >> 5;
return -1;
}
public isDataMessage(): boolean {
const mtype = this._getMType();
return mtype >= MType.UNCONFIRMED_DATA_UP && mtype <= MType.CONFIRMED_DATA_DOWN;
}
public isConfirmed(): boolean {
const mtype = this._getMType();
return mtype === MType.CONFIRMED_DATA_DOWN || mtype === MType.CONFIRMED_DATA_UP;
}
/**
* Provide MType as a string
*/
public getMType(): string {
return MTYPE_DESCRIPTIONS[this._getMType()] || "Proprietary";
}
/**
* Provide Direction as a string
*/
public getDir(): string | null {
const mType = this._getMType();
if (mType > 5) return null;
if (mType % 2 == 0) return "up";
return "down";
}
/**
* Provide FPort as a number
*/
public getFPort(): number | null {
if (this.FPort && this.FPort.length) return this.FPort.readUInt8(0);
return null;
}
/**
* Provide FCnt as a number
*/
public getFCnt(): number | null {
if (this.FCnt) return this.FCnt.readUInt16BE(0);
return null;
}
/**
* Provide FCtrl.ACK as a flag
*/
public getFCtrlACK(): boolean | null {
if (!this.FCtrl) return null;
return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_ACK);
}
/**
* Provide FCtrl.ADR as a flag
*/
public getFCtrlADR(): boolean | null {
if (!this.FCtrl) return null;
return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_ADR);
}
/**
* Provide FCtrl.ADRACKReq as a flag
*/
public getFCtrlADRACKReq(): boolean | null {
if (!this.FCtrl) return null;
return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_ADRACKREQ);
}
/**
* Provide FCtrl.FPending as a flag
*/
public getFCtrlFPending(): boolean | null {
if (!this.FCtrl) return null;
return !!(this.FCtrl.readUInt8(0) & Masks.FCTRL_FPENDING);
}
/**
* Provide DLSettings.RX1DRoffset as integer
*/
public getDLSettingsRxOneDRoffset(): number | null {
if (!this.DLSettings) return null;
return (this.DLSettings.readUInt8(0) & Masks.DLSETTINGS_RXONEDROFFSET_MASK) >> Masks.DLSETTINGS_RXONEDROFFSET_POS;
}
/**
* Provide DLSettings.RX2DataRate as integer
*/
public getDLSettingsRxTwoDataRate(): number | null {
if (!this.DLSettings) return null;
return (this.DLSettings.readUInt8(0) & Masks.DLSETTINGS_RXTWODATARATE_MASK) >> Masks.DLSETTINGS_RXTWODATARATE_POS;
}
/**
* Provide DLSettings.OptNeg as boolean
*/
public getDLSettingsOptNeg(): boolean | null {
if (!this.DLSettings) return null;
return (this.DLSettings.readUInt8(0) & Masks.DLSETTINGS_OPTNEG_MASK) >> Masks.DLSETTINGS_OPTNEG_POS === 1;
}
/**
* Provide RxDelay.Del as integer
*/
public getRxDelayDel(): number | null {
if (!this.RxDelay) return null;
return (this.RxDelay.readUInt8(0) & Masks.RXDELAY_DEL_MASK) >> Masks.RXDELAY_DEL_POS;
}
/**
* Provide CFList.FreqChFour as buffer
*/
public getCFListFreqChFour(): Buffer {
if (this.CFList && this.CFList.length === 16) {
return reverseBuffer(this.CFList.slice(0, 0 + 3));
} else {
return Buffer.alloc(0);
}
}
/**
* Provide CFList.FreqChFive as buffer
*/
public getCFListFreqChFive(): Buffer {
if (this.CFList && this.CFList.length === 16) {
return reverseBuffer(this.CFList.slice(3, 3 + 3));
} else {
return Buffer.alloc(0);
}
}
/**
* Provide CFList.FreqChSix as buffer
*/
public getCFListFreqChSix(): Buffer {
if (this.CFList && this.CFList.length === 16) {
return reverseBuffer(this.CFList.slice(6, 6 + 3));
} else {
return Buffer.alloc(0);
}
}
/**
* Provide CFList.FreqChSeven as buffer
*/
public getCFListFreqChSeven(): Buffer {
if (this.CFList && this.CFList.length === 16) {
return reverseBuffer(this.CFList.slice(9, 9 + 3));
} else {
return Buffer.alloc(0);
}
}
/**
* Provide CFList.FreqChEight as buffer
*/
public getCFListFreqChEight(): Buffer {
if (this.CFList && this.CFList.length === 16) {
return reverseBuffer(this.CFList.slice(12, 12 + 3));
} else {
return Buffer.alloc(0);
}
}
public getBuffers() {
return this;
}
public decryptFOpts(
NwkSEncKey: Buffer,
NwkSKey?: Buffer,
FCntMSBytes?: Buffer,
ConfFCntDownTxDrTxCh?: Buffer
): Buffer {
return this.encryptFOpts(NwkSEncKey, NwkSKey, FCntMSBytes, ConfFCntDownTxDrTxCh);
}
public encryptFOpts(
NwkSEncKey: Buffer,
SNwkSIntKey?: Buffer,
FCntMSBytes?: Buffer,
ConfFCntDownTxDrTxCh?: Buffer
): Buffer {
if (!this.FOpts) return Buffer.alloc(0);
if (!NwkSEncKey || NwkSEncKey?.length !== 16) throw new Error("NwkSEncKey must be 16 bytes");
this.FOpts = decryptFOpts(this, NwkSEncKey, FCntMSBytes);
this._mergeGroupFields();
if (SNwkSIntKey?.length === 16) {
recalculateMIC(this, SNwkSIntKey, undefined, FCntMSBytes, ConfFCntDownTxDrTxCh);
this._mergeGroupFields();
}
return this.FOpts;
}
public getPHYPayload(): Buffer | void {
return this.PHYPayload;
}
public isJoinRequestMessage() {
return this._getMType() == MType.JOIN_REQUEST;
}
public isRejoinRequestMessage() {
return this._getMType() == MType.REJOIN_REQUEST;
}
// deprecated (bogus capitalisation)
public isReJoinRequestMessage() {
return this._getMType() == MType.REJOIN_REQUEST;
}
public isJoinAcceptMessage() {
return this._getMType() == MType.JOIN_ACCEPT;
}
public toString(): string {
let msg = "";
if (this.isJoinRequestMessage()) {
msg += " Message Type = Join Request" + "\n";
msg += " PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
msg += "\n";
msg += " ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
msg += " MHDR = " + asHexString(this.MHDR) + "\n";
msg += " MACPayload = " + asHexString(this.MACPayload) + "\n";
msg += " MIC = " + asHexString(this.MIC) + "\n";
msg += "\n";
msg += " ( MACPayload = AppEUI[8] | DevEUI[8] | DevNonce[2] )\n";
msg += " AppEUI = " + asHexString(this.AppEUI) + "\n";
msg += " DevEUI = " + asHexString(this.DevEUI) + "\n";
msg += " DevNonce = " + asHexString(this.DevNonce) + "\n";
} else if (this.isJoinAcceptMessage()) {
msg += " Message Type = Join Accept" + "\n";
msg += " PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
msg += "\n";
msg += " ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
msg += " MHDR = " + asHexString(this.MHDR) + "\n";
msg += " MACPayload = " + asHexString(this.MACPayload) + "\n";
msg += " MIC = " + asHexString(this.MIC) + "\n";
msg += "\n";
msg +=
" ( MACPayload = AppNonce[3] | NetID[3] | DevAddr[4] | DLSettings[1] | RxDelay[1] | CFList[0|15] )\n";
msg += " AppNonce = " + asHexString(this.AppNonce) + "\n";
msg += " NetID = " + asHexString(this.NetID) + "\n";
msg += " DevAddr = " + asHexString(this.DevAddr) + "\n";
msg += " DLSettings = " + asHexString(this.DLSettings) + "\n";
msg += " RxDelay = " + asHexString(this.RxDelay) + "\n";
msg += " CFList = " + asHexString(this.CFList) + "\n";
msg += "\n";
msg += "DLSettings.RX1DRoffset = " + this.getDLSettingsRxOneDRoffset() + "\n";
msg += "DLSettings.RX2DataRate = " + this.getDLSettingsRxTwoDataRate() + "\n";
msg += " RxDelay.Del = " + this.getRxDelayDel() + "\n";
msg += "\n";
if (this.CFList.length === 16) {
msg += " ( CFList = FreqCh4[3] | FreqCh5[3] | FreqCh6[3] | FreqCh7[3] | FreqCh8[3] )\n";
msg += " FreqCh4 = " + asHexString(this.getCFListFreqChFour()) + "\n";
msg += " FreqCh5 = " + asHexString(this.getCFListFreqChFive()) + "\n";
msg += " FreqCh6 = " + asHexString(this.getCFListFreqChSix()) + "\n";
msg += " FreqCh7 = " + asHexString(this.getCFListFreqChSeven()) + "\n";
msg += " FreqCh8 = " + asHexString(this.getCFListFreqChEight()) + "\n";
}
} else if (this.isRejoinRequestMessage()) {
msg += " Message Type = ReJoin Request" + "\n";
msg += " PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
msg += "\n";
msg += " ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
msg += " MHDR = " + asHexString(this.MHDR) + "\n";
msg += " MACPayload = " + asHexString(this.MACPayload) + "\n";
msg += " MIC = " + asHexString(this.MIC) + "\n";
msg += "\n";
if (this.RejoinType[0] === 0 || this.RejoinType[0] === 2) {
msg += " ( MACPayload = RejoinType[1] | NetID[3] | DevEUI[8] | RJCount0[2] )\n";
msg += " RejoinType = " + asHexString(this.RejoinType) + "\n";
msg += " NetID = " + asHexString(this.NetID) + "\n";
msg += " DevEUI = " + asHexString(this.DevEUI) + "\n";
msg += " RJCount0 = " + asHexString(this.RJCount0) + "\n";
} else if (this.RejoinType[0] === 1) {
msg += " ( MACPayload = RejoinType[1] | JoinEUI[8] | DevEUI[8] | RJCount0[2] )\n";
msg += " RejoinType = " + asHexString(this.RejoinType) + "\n";
msg += " JoinEUI = " + asHexString(this.JoinEUI) + "\n";
msg += " DevEUI = " + asHexString(this.DevEUI) + "\n";
msg += " RJCount0 = " + asHexString(this.RJCount0) + "\n";
}
} else if (this.isDataMessage()) {
msg += "Message Type = Data" + "\n";
msg += " PHYPayload = " + asHexString(this.PHYPayload).toUpperCase() + "\n";
msg += "\n";
msg += " ( PHYPayload = MHDR[1] | MACPayload[..] | MIC[4] )\n";
msg += " MHDR = " + asHexString(this.MHDR) + "\n";
msg += " MACPayload = " + asHexString(this.MACPayload) + "\n";
msg += " MIC = " + asHexString(this.MIC) + "\n";
msg += "\n";
msg += " ( MACPayload = FHDR | FPort | FRMPayload )\n";
msg += " FHDR = " + asHexString(this.FHDR) + "\n";
msg += " FPort = " + asHexString(this.FPort) + "\n";
msg += " FRMPayload = " + asHexString(this.FRMPayload) + "\n";
msg += "\n";
msg += " ( FHDR = DevAddr[4] | FCtrl[1] | FCnt[2] | FOpts[0..15] )\n";
msg += " DevAddr = " + asHexString(this.DevAddr) + " (Big Endian)\n";
msg += " FCtrl = " + asHexString(this.FCtrl) + "\n"; //TODO as binary?
msg += " FCnt = " + asHexString(this.FCnt) + " (Big Endian)\n";
msg += " FOpts = " + asHexString(this.FOpts) + "\n";
msg += "\n";
msg += " Message Type = " + this.getMType() + "\n";
msg += " Direction = " + this.getDir() + "\n";
msg += " FCnt = " + this.getFCnt() + "\n";
msg += " FCtrl.ACK = " + this.getFCtrlACK() + "\n";
msg += " FCtrl.ADR = " + this.getFCtrlADR() + "\n";
if (this._getMType() == MType.CONFIRMED_DATA_DOWN || this._getMType() == MType.UNCONFIRMED_DATA_DOWN) {
msg += " FCtrl.FPending = " + this.getFCtrlFPending() + "\n";
} else {
msg += " FCtrl.ADRACKReq = " + this.getFCtrlADRACKReq() + "\n";
}
}
return msg;
}
get JoinEUI(): Buffer {
return this.AppEUI;
}
set JoinEUI(v: Buffer) {
this.AppEUI = v;
}
get JoinNonce(): Buffer {
return this.AppNonce;
}
set JoinNonce(v: Buffer) {
this.AppNonce = v;
}
PHYPayload?: Buffer;
MHDR?: Buffer;
MACPayload?: Buffer;
MACPayloadWithMIC?: Buffer;
AppEUI?: Buffer;
DevEUI?: Buffer;
DevNonce?: Buffer;
MIC?: Buffer;
AppNonce?: Buffer;
NetID?: Buffer;
DevAddr?: Buffer;
DLSettings?: Buffer;
RxDelay?: Buffer;
CFList?: Buffer;
FCtrl?: Buffer;
FOpts?: Buffer;
FCnt?: Buffer;
FHDR?: Buffer;
FPort?: Buffer;
FRMPayload?: Buffer;
JoinReqType?: Buffer;
RejoinType?: Buffer;
RJCount0?: Buffer;
RJCount1?: Buffer;
}
export default LoraPacket;
export { LorawanVersion };