UNPKG

lora-packet

Version:
934 lines (834 loc) 31.6 kB
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 };