zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
1,273 lines (1,071 loc) • 88 kB
text/typescript
import Buffalo from "../../buffalo/buffalo";
import {logger} from "../../utils/logger";
import {DEFAULT_ENCRYPTION_KEY_SIZE, EUI64_SIZE, EXTENDED_PAN_ID_SIZE, PAN_ID_SIZE} from "../consts";
import type {ClusterId, Eui64, NodeId, ProfileId} from "../tstypes";
import * as ZSpecUtils from "../utils";
import {ClusterId as ZdoClusterId} from "./definition/clusters";
import {CHALLENGE_VALUE_SIZE, CURVE_PUBLIC_POINT_SIZE, MULTICAST_BINDING, UNICAST_BINDING, ZDO_MESSAGE_OVERHEAD} from "./definition/consts";
import {GlobalTLV, type LeaveRequestFlags, RoutingTableStatus} from "./definition/enums";
import {Status} from "./definition/status";
import type {
APSFrameCounterChallengeTLV,
APSFrameCounterResponseTLV,
ActiveEndpointsResponse,
AuthenticationTokenIdTLV,
BeaconAppendixEncapsulationGlobalTLV,
BeaconSurveyConfigurationTLV,
BeaconSurveyResultsTLV,
BindingTableEntry,
BindingTableResponse,
ChallengeResponse,
ClearAllBindingsReqEUI64TLV,
ConfigurationParametersGlobalTLV,
Curve25519PublicPointTLV,
DeviceAuthenticationLevelTLV,
DeviceCapabilityExtensionGlobalTLV,
DeviceEUI64ListTLV,
FragmentationParametersGlobalTLV,
GetAuthenticationLevelResponse,
GetConfigurationResponse,
IEEEAddressResponse,
JoinerEncapsulationGlobalTLV,
LQITableEntry,
LQITableResponse,
LocalTLVReader,
ManufacturerSpecificGlobalTLV,
MatchDescriptorsResponse,
NetworkAddressResponse,
NextChannelChangeGlobalTLV,
NextPanIdChangeGlobalTLV,
NodeDescriptorResponse,
NwkBeaconSurveyResponse,
NwkEnhancedUpdateResponse,
NwkIEEEJoiningListResponse,
NwkUnsolicitedEnhancedUpdateResponse,
NwkUpdateResponse,
PanIdConflictReportGlobalTLV,
ParentAnnounceResponse,
PotentialParentsTLV,
PowerDescriptorResponse,
ProcessingStatusTLV,
RequestMap,
ResponseMap,
RetrieveAuthenticationTokenResponse,
RouterInformationGlobalTLV,
RoutingTableEntry,
RoutingTableResponse,
SelectedKeyNegotiationMethodTLV,
ServerMask,
SetConfigurationResponse,
SimpleDescriptorResponse,
StartKeyNegotiationResponse,
SupportedKeyNegotiationMethodsGlobalTLV,
SymmetricPassphraseGlobalTLV,
SystemServerDiscoveryResponse,
TargetIEEEAddressTLV,
Tlv,
ValidResponseMap,
} from "./definition/tstypes";
import * as Utils from "./utils";
import {ZdoStatusError} from "./zdoStatusError";
const NS = "zh:zdo:buffalo";
const MAX_BUFFER_SIZE = 255;
export class BuffaloZdo extends Buffalo {
/**
* Set the position of the internal position tracker.
* TODO: move to base `Buffalo` class
* @param position
*/
public setPosition(position: number): void {
this.position = position;
}
/**
* Set the byte at given position without affecting the internal position tracker.
* TODO: move to base `Buffalo` class
* @param position
* @param value
*/
public setByte(position: number, value: number): void {
this.buffer.writeUInt8(value, position);
}
/**
* Get the byte at given position without affecting the internal position tracker.
* TODO: move to base `Buffalo` class
* @param position
* @returns
*/
public getByte(position: number): number {
return this.buffer.readUInt8(position);
}
/**
* Check if internal buffer has enough bytes to satisfy: (current position + given count).
* TODO: move to base `Buffalo` class
* @param count
* @returns True if has given more bytes
*/
public isMoreBy(count: number): boolean {
return this.position + count <= this.buffer.length;
}
//-- GLOBAL TLVS
private writeManufacturerSpecificGlobalTLV(tlv: ManufacturerSpecificGlobalTLV): void {
this.writeUInt16(tlv.zigbeeManufacturerId);
this.writeBuffer(tlv.additionalData, tlv.additionalData.length);
}
private readManufacturerSpecificGlobalTLV(length: number): ManufacturerSpecificGlobalTLV {
logger.debug(`readManufacturerSpecificGlobalTLV with length=${length}`, NS);
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const zigbeeManufacturerId = this.readUInt16();
const additionalData = this.readBuffer(length - 2);
return {
zigbeeManufacturerId,
additionalData,
};
}
private writeSupportedKeyNegotiationMethodsGlobalTLV(tlv: SupportedKeyNegotiationMethodsGlobalTLV): void {
this.writeUInt8(tlv.keyNegotiationProtocolsBitmask);
this.writeUInt8(tlv.preSharedSecretsBitmask);
if (tlv.sourceDeviceEui64) {
this.writeIeeeAddr(tlv.sourceDeviceEui64);
}
}
private readSupportedKeyNegotiationMethodsGlobalTLV(length: number): SupportedKeyNegotiationMethodsGlobalTLV {
logger.debug(`readSupportedKeyNegotiationMethodsGlobalTLV with length=${length}`, NS);
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const keyNegotiationProtocolsBitmask = this.readUInt8();
const preSharedSecretsBitmask = this.readUInt8();
let sourceDeviceEui64: SupportedKeyNegotiationMethodsGlobalTLV["sourceDeviceEui64"];
if (length >= 2 + EUI64_SIZE) {
sourceDeviceEui64 = this.readIeeeAddr();
}
return {
keyNegotiationProtocolsBitmask,
preSharedSecretsBitmask,
sourceDeviceEui64,
};
}
private writePanIdConflictReportGlobalTLV(tlv: PanIdConflictReportGlobalTLV): void {
this.writeUInt16(tlv.nwkPanIdConflictCount);
}
private readPanIdConflictReportGlobalTLV(length: number): PanIdConflictReportGlobalTLV {
logger.debug(`readPanIdConflictReportGlobalTLV with length=${length}`, NS);
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const nwkPanIdConflictCount = this.readUInt16();
return {
nwkPanIdConflictCount,
};
}
private writeNextPanIdChangeGlobalTLV(tlv: NextPanIdChangeGlobalTLV): void {
this.writeUInt16(tlv.panId);
}
private readNextPanIdChangeGlobalTLV(length: number): NextPanIdChangeGlobalTLV {
logger.debug(`readNextPanIdChangeGlobalTLV with length=${length}`, NS);
if (length < PAN_ID_SIZE) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least ${PAN_ID_SIZE}.`);
}
const panId = this.readUInt16();
return {
panId,
};
}
private writeNextChannelChangeGlobalTLV(tlv: NextChannelChangeGlobalTLV): void {
this.writeUInt32(tlv.channel);
}
private readNextChannelChangeGlobalTLV(length: number): NextChannelChangeGlobalTLV {
logger.debug(`readNextChannelChangeGlobalTLV with length=${length}`, NS);
if (length < 4) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 4.`);
}
const channel = this.readUInt32();
return {
channel,
};
}
private writeSymmetricPassphraseGlobalTLV(tlv: SymmetricPassphraseGlobalTLV): void {
this.writeBuffer(tlv.passphrase, DEFAULT_ENCRYPTION_KEY_SIZE);
}
private readSymmetricPassphraseGlobalTLV(length: number): SymmetricPassphraseGlobalTLV {
logger.debug(`readSymmetricPassphraseGlobalTLV with length=${length}`, NS);
if (length < DEFAULT_ENCRYPTION_KEY_SIZE) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least ${DEFAULT_ENCRYPTION_KEY_SIZE}.`);
}
const passphrase = this.readBuffer(DEFAULT_ENCRYPTION_KEY_SIZE);
return {
passphrase,
};
}
private writeRouterInformationGlobalTLV(tlv: RouterInformationGlobalTLV): void {
this.writeUInt16(tlv.bitmask);
}
private readRouterInformationGlobalTLV(length: number): RouterInformationGlobalTLV {
logger.debug(`readRouterInformationGlobalTLV with length=${length}`, NS);
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const bitmask = this.readUInt16();
return {
bitmask,
};
}
private writeFragmentationParametersGlobalTLV(tlv: FragmentationParametersGlobalTLV): void {
this.writeUInt16(tlv.nwkAddress);
if (tlv.fragmentationOptions !== undefined) {
this.writeUInt8(tlv.fragmentationOptions);
}
if (tlv.maxIncomingTransferUnit !== undefined) {
this.writeUInt16(tlv.maxIncomingTransferUnit);
}
}
private readFragmentationParametersGlobalTLV(length: number): FragmentationParametersGlobalTLV {
logger.debug(`readFragmentationParametersGlobalTLV with length=${length}`, NS);
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const nwkAddress = this.readUInt16();
let fragmentationOptions: FragmentationParametersGlobalTLV["fragmentationOptions"];
let maxIncomingTransferUnit: FragmentationParametersGlobalTLV["maxIncomingTransferUnit"];
if (length >= 3) {
fragmentationOptions = this.readUInt8();
}
if (length >= 5) {
maxIncomingTransferUnit = this.readUInt16();
}
return {
nwkAddress,
fragmentationOptions,
maxIncomingTransferUnit,
};
}
private writeJoinerEncapsulationGlobalTLV(encapsulationTLV: JoinerEncapsulationGlobalTLV): void {
this.writeGlobalTLVs(encapsulationTLV.additionalTLVs);
}
private readJoinerEncapsulationGlobalTLV(length: number): JoinerEncapsulationGlobalTLV {
logger.debug(`readJoinerEncapsulationGlobalTLV with length=${length}`, NS);
// at least the length of tagId+length for first encapsulated tlv, doesn't make sense otherwise
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const encapsulationBuffalo = new BuffaloZdo(this.readBuffer(length));
const additionalTLVs = encapsulationBuffalo.readTLVs(undefined, true);
return {
additionalTLVs,
};
}
private writeBeaconAppendixEncapsulationGlobalTLV(encapsulationTLV: BeaconAppendixEncapsulationGlobalTLV): void {
this.writeGlobalTLVs(encapsulationTLV.additionalTLVs);
}
private readBeaconAppendixEncapsulationGlobalTLV(length: number): BeaconAppendixEncapsulationGlobalTLV {
logger.debug(`readBeaconAppendixEncapsulationGlobalTLV with length=${length}`, NS);
// at least the length of tagId+length for first encapsulated tlv, doesn't make sense otherwise
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const encapsulationBuffalo = new BuffaloZdo(this.readBuffer(length));
// Global: SupportedKeyNegotiationMethodsGlobalTLV
// Global: FragmentationParametersGlobalTLV
const additionalTLVs = encapsulationBuffalo.readTLVs(undefined, true);
return {
additionalTLVs,
};
}
private writeConfigurationParametersGlobalTLV(configurationParameters: ConfigurationParametersGlobalTLV): void {
this.writeUInt16(configurationParameters.configurationParameters);
}
private readConfigurationParametersGlobalTLV(length: number): ConfigurationParametersGlobalTLV {
logger.debug(`readConfigurationParametersGlobalTLV with length=${length}`, NS);
if (length < 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`);
}
const configurationParameters = this.readUInt16();
return {
configurationParameters,
};
}
private writeDeviceCapabilityExtensionGlobalTLV(tlv: DeviceCapabilityExtensionGlobalTLV): void {
this.writeBuffer(tlv.data, tlv.data.length);
}
private readDeviceCapabilityExtensionGlobalTLV(length: number): DeviceCapabilityExtensionGlobalTLV {
logger.debug(`readDeviceCapabilityExtensionGlobalTLV with length=${length}`, NS);
const data = this.readBuffer(length);
return {
data,
};
}
public writeGlobalTLV(tlv: Tlv): void {
this.writeUInt8(tlv.tagId);
this.writeUInt8(tlv.length - 1); // remove offset (spec quirk...)
switch (tlv.tagId) {
case GlobalTLV.MANUFACTURER_SPECIFIC: {
this.writeManufacturerSpecificGlobalTLV(tlv.tlv as ManufacturerSpecificGlobalTLV);
break;
}
case GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS: {
this.writeSupportedKeyNegotiationMethodsGlobalTLV(tlv.tlv as SupportedKeyNegotiationMethodsGlobalTLV);
break;
}
case GlobalTLV.PAN_ID_CONFLICT_REPORT: {
this.writePanIdConflictReportGlobalTLV(tlv.tlv as PanIdConflictReportGlobalTLV);
break;
}
case GlobalTLV.NEXT_PAN_ID_CHANGE: {
this.writeNextPanIdChangeGlobalTLV(tlv.tlv as NextPanIdChangeGlobalTLV);
break;
}
case GlobalTLV.NEXT_CHANNEL_CHANGE: {
this.writeNextChannelChangeGlobalTLV(tlv.tlv as NextChannelChangeGlobalTLV);
break;
}
case GlobalTLV.SYMMETRIC_PASSPHRASE: {
this.writeSymmetricPassphraseGlobalTLV(tlv.tlv as SymmetricPassphraseGlobalTLV);
break;
}
case GlobalTLV.ROUTER_INFORMATION: {
this.writeRouterInformationGlobalTLV(tlv.tlv as RouterInformationGlobalTLV);
break;
}
case GlobalTLV.FRAGMENTATION_PARAMETERS: {
this.writeFragmentationParametersGlobalTLV(tlv.tlv as FragmentationParametersGlobalTLV);
break;
}
case GlobalTLV.JOINER_ENCAPSULATION: {
this.writeJoinerEncapsulationGlobalTLV(tlv.tlv as JoinerEncapsulationGlobalTLV);
break;
}
case GlobalTLV.BEACON_APPENDIX_ENCAPSULATION: {
this.writeBeaconAppendixEncapsulationGlobalTLV(tlv.tlv as BeaconAppendixEncapsulationGlobalTLV);
break;
}
case GlobalTLV.CONFIGURATION_PARAMETERS: {
this.writeConfigurationParametersGlobalTLV(tlv.tlv as ConfigurationParametersGlobalTLV);
break;
}
case GlobalTLV.DEVICE_CAPABILITY_EXTENSION: {
this.writeDeviceCapabilityExtensionGlobalTLV(tlv.tlv as DeviceCapabilityExtensionGlobalTLV);
break;
}
default: {
throw new ZdoStatusError(Status.NOT_SUPPORTED);
}
}
}
public readGlobalTLV(tagId: number, length: number): Tlv["tlv"] | undefined {
switch (tagId) {
case GlobalTLV.MANUFACTURER_SPECIFIC: {
return this.readManufacturerSpecificGlobalTLV(length);
}
case GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS: {
return this.readSupportedKeyNegotiationMethodsGlobalTLV(length);
}
case GlobalTLV.PAN_ID_CONFLICT_REPORT: {
return this.readPanIdConflictReportGlobalTLV(length);
}
case GlobalTLV.NEXT_PAN_ID_CHANGE: {
return this.readNextPanIdChangeGlobalTLV(length);
}
case GlobalTLV.NEXT_CHANNEL_CHANGE: {
return this.readNextChannelChangeGlobalTLV(length);
}
case GlobalTLV.SYMMETRIC_PASSPHRASE: {
return this.readSymmetricPassphraseGlobalTLV(length);
}
case GlobalTLV.ROUTER_INFORMATION: {
return this.readRouterInformationGlobalTLV(length);
}
case GlobalTLV.FRAGMENTATION_PARAMETERS: {
return this.readFragmentationParametersGlobalTLV(length);
}
case GlobalTLV.JOINER_ENCAPSULATION: {
return this.readJoinerEncapsulationGlobalTLV(length);
}
case GlobalTLV.BEACON_APPENDIX_ENCAPSULATION: {
return this.readBeaconAppendixEncapsulationGlobalTLV(length);
}
case GlobalTLV.CONFIGURATION_PARAMETERS: {
return this.readConfigurationParametersGlobalTLV(length);
}
case GlobalTLV.DEVICE_CAPABILITY_EXTENSION: {
return this.readDeviceCapabilityExtensionGlobalTLV(length);
}
default: {
// validation: unknown tag shall be ignored
return undefined;
}
}
}
public writeGlobalTLVs(tlvs: Tlv[]): void {
for (const tlv of tlvs) {
this.writeGlobalTLV(tlv);
}
}
//-- LOCAL TLVS
// write only
// private readBeaconSurveyConfigurationTLV(length: number): BeaconSurveyConfigurationTLV {
// logger.debug(`readBeaconSurveyConfigurationTLV with length=${length}`, NS);
// const count = this.readUInt8();
// if (length !== (1 + (count * 4) + 1)) {
// throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(1 + (count * 4) + 1)}.`);
// }
// const scanChannelList = this.readListUInt32(count);
// const configurationBitmask = this.readUInt8();
// return {
// scanChannelList,
// configurationBitmask,
// };
// }
private readCurve25519PublicPointTLV(length: number): Curve25519PublicPointTLV {
logger.debug(`readCurve25519PublicPointTLV with length=${length}`, NS);
if (length !== EUI64_SIZE + CURVE_PUBLIC_POINT_SIZE) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected ${EUI64_SIZE + CURVE_PUBLIC_POINT_SIZE}.`);
}
const eui64 = this.readIeeeAddr();
const publicPoint = this.readBuffer(CURVE_PUBLIC_POINT_SIZE);
return {
eui64,
publicPoint,
};
}
// write only
// private readTargetIEEEAddressTLV(length: number): TargetIEEEAddressTLV {
// logger.debug(`readTargetIEEEAddressTLV with length=${length}`, NS);
// if (length !== EUI64_SIZE) {
// throw new Error(`Malformed TLV. Invalid length '${length}', expected ${EUI64_SIZE}.`);
// }
// const ieee = this.readIeeeAddr();
// return {
// ieee,
// };
// }
// write only
// private readSelectedKeyNegotiationMethodTLV(length: number): SelectedKeyNegotiationMethodTLV {
// logger.debug(`readSelectedKeyNegotiationMethodTLV with length=${length}`, NS);
// if (length !== 10) {
// throw new Error(`Malformed TLV. Invalid length '${length}', expected 10.`);
// }
// const protocol = this.readUInt8();
// const presharedSecret = this.readUInt8();
// const sendingDeviceEui64 = this.readIeeeAddr();
// return {
// protocol,
// presharedSecret,
// sendingDeviceEui64,
// };
// }
// write only
// private readDeviceEUI64ListTLV(length: number): DeviceEUI64ListTLV {
// logger.debug(`readDeviceEUI64ListTLV with length=${length}`, NS);
// const count = this.readUInt8();
// if (length !== (1 + (count * EUI64_SIZE))) {
// throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(1 + (count * EUI64_SIZE))}.`);
// }
// const eui64List: DeviceEUI64ListTLV['eui64List'] = [];
// for (let i = 0; i < count; i++) {
// const eui64 = this.readIeeeAddr();
// eui64List.push(eui64);
// }
// return {
// eui64List,
// };
// }
private readAPSFrameCounterResponseTLV(length: number): APSFrameCounterResponseTLV {
logger.debug(`readAPSFrameCounterResponseTLV with length=${length}`, NS);
if (length !== 32) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected 32.`);
}
const responderEui64 = this.readIeeeAddr();
const receivedChallengeValue = this.readBuffer(CHALLENGE_VALUE_SIZE);
const apsFrameCounter = this.readUInt32();
const challengeSecurityFrameCounter = this.readUInt32();
const mic = this.readBuffer(8);
return {
responderEui64,
receivedChallengeValue,
apsFrameCounter,
challengeSecurityFrameCounter,
mic,
};
}
private readBeaconSurveyResultsTLV(length: number): BeaconSurveyResultsTLV {
logger.debug(`readBeaconSurveyResultsTLV with length=${length}`, NS);
if (length !== 4) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected 4.`);
}
const totalBeaconsReceived = this.readUInt8();
const onNetworkBeacons = this.readUInt8();
const potentialParentBeacons = this.readUInt8();
const otherNetworkBeacons = this.readUInt8();
return {
totalBeaconsReceived,
onNetworkBeacons,
potentialParentBeacons,
otherNetworkBeacons,
};
}
private readPotentialParentsTLV(length: number): PotentialParentsTLV {
logger.debug(`readPotentialParentsTLV with length=${length}`, NS);
if (length < 4) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 4.`);
}
const currentParentNwkAddress = this.readUInt16();
const currentParentLQA = this.readUInt8();
// [0x00 - 0x05]
const entryCount = this.readUInt8();
if (length !== 4 + entryCount * 3) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected ${4 + entryCount * 3}.`);
}
const potentialParents: PotentialParentsTLV["potentialParents"] = [];
for (let i = 0; i < entryCount; i++) {
const nwkAddress = this.readUInt16();
const lqa = this.readUInt8();
potentialParents.push({
nwkAddress,
lqa,
});
}
return {
currentParentNwkAddress,
currentParentLQA,
entryCount,
potentialParents,
};
}
private readDeviceAuthenticationLevelTLV(length: number): DeviceAuthenticationLevelTLV {
logger.debug(`readDeviceAuthenticationLevelTLV with length=${length}`, NS);
if (length !== 10) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected 10.`);
}
const remoteNodeIeee = this.readIeeeAddr();
const initialJoinMethod = this.readUInt8();
const activeLinkKeyType = this.readUInt8();
return {
remoteNodeIeee,
initialJoinMethod,
activeLinkKeyType,
};
}
private readProcessingStatusTLV(length: number): ProcessingStatusTLV {
logger.debug(`readProcessingStatusTLV with length=${length}`, NS);
const count = this.readUInt8();
if (length !== 1 + count * 2) {
throw new Error(`Malformed TLV. Invalid length '${length}', expected ${1 + count * 2}.`);
}
const tlvs: ProcessingStatusTLV["tlvs"] = [];
for (let i = 0; i < count; i++) {
const tagId = this.readUInt8();
const processingStatus = this.readUInt8();
tlvs.push({
tagId,
processingStatus,
});
}
return {
count,
tlvs,
};
}
/**
* ANNEX I ZIGBEE TLV DEFINITIONS AND FORMAT
*
* Unknown tags => TLV ignored
* Duplicate tags => reject message except for MANUFACTURER_SPECIFIC_GLOBAL_TLV
* Malformed TLVs => reject message
*
* @param localTLVReaders Mapping of tagID to local TLV reader function
* @param encapsulated Default false. If true, this is reading inside an encapsuled TLV (excludes further encapsulation)
* @returns
*/
public readTLVs(localTLVReaders?: Map<number, LocalTLVReader>, encapsulated = false): Tlv[] {
const tlvs: Tlv[] = [];
while (this.isMore()) {
const tagId = this.readUInt8();
// validation: cannot have duplicate tagId, except MANUFACTURER_SPECIFIC_GLOBAL_TLV
if (tagId !== GlobalTLV.MANUFACTURER_SPECIFIC && tlvs.findIndex((tlv) => tlv.tagId === tagId) !== -1) {
throw new Error(`Duplicate tag. Cannot have more than one of tagId=${tagId}.`);
}
// validation: encapsulation TLV cannot contain another encapsulation TLV, outer considered malformed, reject message
if (encapsulated && (tagId === GlobalTLV.BEACON_APPENDIX_ENCAPSULATION || tagId === GlobalTLV.JOINER_ENCAPSULATION)) {
throw new Error(`Invalid nested encapsulation for tagId=${tagId}.`);
}
const length = this.readUInt8() + 1; // add offset (spec quirk...)
// validation: invalid if not at least ${length} bytes to read
if (!this.isMoreBy(length)) {
throw new Error(`Malformed TLV. Invalid data length for tagId=${tagId}, expected ${length}.`);
}
const nextTLVStart = this.getPosition() + length;
// undefined == unknown tag
let tlv: Tlv["tlv"] | undefined;
if (tagId < GlobalTLV.MANUFACTURER_SPECIFIC) {
if (localTLVReaders) {
const localTLVReader = localTLVReaders.get(tagId);
if (localTLVReader) {
tlv = localTLVReader.call(this, length);
/* v8 ignore start */
} else {
logger.debug(`Local TLV found tagId=${tagId} but no reader given for it. Ignoring it.`, NS);
}
/* v8 ignore stop */
/* v8 ignore start */
} else {
logger.debug(`Local TLV found tagId=${tagId} but no reader available. Ignoring it.`, NS);
}
/* v8 ignore stop */
} else {
tlv = this.readGlobalTLV(tagId, length);
}
// validation: unknown tag shall be ignored
if (tlv) {
tlvs.push({
tagId,
length,
tlv,
});
} else {
logger.debug(`Unknown TLV tagId=${tagId}. Ignoring it.`, NS);
}
// ensure we're at the right position as dictated by the tlv length field, and not the tlv reader (should be the same if proper)
this.setPosition(nextTLVStart);
}
return tlvs;
}
//-- REQUESTS
public static buildRequest<K extends keyof RequestMap>(hasZdoMessageOverhead: boolean, clusterId: K, ...args: RequestMap[K]): Buffer {
const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), hasZdoMessageOverhead ? ZDO_MESSAGE_OVERHEAD : 0);
switch (clusterId) {
case ZdoClusterId.NETWORK_ADDRESS_REQUEST: {
return buffalo.buildNetworkAddressRequest(...(args as RequestMap[ZdoClusterId.NETWORK_ADDRESS_REQUEST]));
}
case ZdoClusterId.IEEE_ADDRESS_REQUEST: {
return buffalo.buildIeeeAddressRequest(...(args as RequestMap[ZdoClusterId.IEEE_ADDRESS_REQUEST]));
}
case ZdoClusterId.NODE_DESCRIPTOR_REQUEST: {
return buffalo.buildNodeDescriptorRequest(...(args as RequestMap[ZdoClusterId.NODE_DESCRIPTOR_REQUEST]));
}
case ZdoClusterId.POWER_DESCRIPTOR_REQUEST: {
return buffalo.buildPowerDescriptorRequest(...(args as RequestMap[ZdoClusterId.POWER_DESCRIPTOR_REQUEST]));
}
case ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST: {
return buffalo.buildSimpleDescriptorRequest(...(args as RequestMap[ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]));
}
case ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST: {
return buffalo.buildActiveEndpointsRequest(...(args as RequestMap[ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]));
}
case ZdoClusterId.MATCH_DESCRIPTORS_REQUEST: {
return buffalo.buildMatchDescriptorRequest(...(args as RequestMap[ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]));
}
case ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST: {
return buffalo.buildSystemServiceDiscoveryRequest(...(args as RequestMap[ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST]));
}
case ZdoClusterId.PARENT_ANNOUNCE: {
return buffalo.buildParentAnnounce(...(args as RequestMap[ZdoClusterId.PARENT_ANNOUNCE]));
}
case ZdoClusterId.BIND_REQUEST: {
return buffalo.buildBindRequest(...(args as RequestMap[ZdoClusterId.BIND_REQUEST]));
}
case ZdoClusterId.UNBIND_REQUEST: {
return buffalo.buildUnbindRequest(...(args as RequestMap[ZdoClusterId.UNBIND_REQUEST]));
}
case ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST: {
return buffalo.buildClearAllBindingsRequest(...(args as RequestMap[ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST]));
}
case ZdoClusterId.LQI_TABLE_REQUEST: {
return buffalo.buildLqiTableRequest(...(args as RequestMap[ZdoClusterId.LQI_TABLE_REQUEST]));
}
case ZdoClusterId.ROUTING_TABLE_REQUEST: {
return buffalo.buildRoutingTableRequest(...(args as RequestMap[ZdoClusterId.ROUTING_TABLE_REQUEST]));
}
case ZdoClusterId.BINDING_TABLE_REQUEST: {
return buffalo.buildBindingTableRequest(...(args as RequestMap[ZdoClusterId.BINDING_TABLE_REQUEST]));
}
case ZdoClusterId.LEAVE_REQUEST: {
return buffalo.buildLeaveRequest(...(args as RequestMap[ZdoClusterId.LEAVE_REQUEST]));
}
case ZdoClusterId.PERMIT_JOINING_REQUEST: {
return buffalo.buildPermitJoining(...(args as RequestMap[ZdoClusterId.PERMIT_JOINING_REQUEST]));
}
case ZdoClusterId.NWK_UPDATE_REQUEST: {
return buffalo.buildNwkUpdateRequest(...(args as RequestMap[ZdoClusterId.NWK_UPDATE_REQUEST]));
}
case ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST: {
return buffalo.buildNwkEnhancedUpdateRequest(...(args as RequestMap[ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST]));
}
case ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST: {
return buffalo.buildNwkIEEEJoiningListRequest(...(args as RequestMap[ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST]));
}
case ZdoClusterId.NWK_BEACON_SURVEY_REQUEST: {
return buffalo.buildNwkBeaconSurveyRequest(...(args as RequestMap[ZdoClusterId.NWK_BEACON_SURVEY_REQUEST]));
}
case ZdoClusterId.START_KEY_NEGOTIATION_REQUEST: {
return buffalo.buildStartKeyNegotiationRequest(...(args as RequestMap[ZdoClusterId.START_KEY_NEGOTIATION_REQUEST]));
}
case ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST: {
return buffalo.buildRetrieveAuthenticationTokenRequest(...(args as RequestMap[ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST]));
}
case ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST: {
return buffalo.buildGetAuthenticationLevelRequest(...(args as RequestMap[ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST]));
}
case ZdoClusterId.SET_CONFIGURATION_REQUEST: {
return buffalo.buildSetConfigurationRequest(...(args as RequestMap[ZdoClusterId.SET_CONFIGURATION_REQUEST]));
}
case ZdoClusterId.GET_CONFIGURATION_REQUEST: {
return buffalo.buildGetConfigurationRequest(...(args as RequestMap[ZdoClusterId.GET_CONFIGURATION_REQUEST]));
}
case ZdoClusterId.START_KEY_UPDATE_REQUEST: {
return buffalo.buildStartKeyUpdateRequest(...(args as RequestMap[ZdoClusterId.START_KEY_UPDATE_REQUEST]));
}
case ZdoClusterId.DECOMMISSION_REQUEST: {
return buffalo.buildDecommissionRequest(...(args as RequestMap[ZdoClusterId.DECOMMISSION_REQUEST]));
}
case ZdoClusterId.CHALLENGE_REQUEST: {
return buffalo.buildChallengeRequest(...(args as RequestMap[ZdoClusterId.CHALLENGE_REQUEST]));
}
default: {
throw new Error(`Unsupported request building for cluster ID '${clusterId}'.`);
}
}
}
/**
* @see ClusterId.NETWORK_ADDRESS_REQUEST
* @param target IEEE address for the request
* @param reportKids True to request that the target list their children in the response. [request type = 0x01]
* @param childStartIndex The index of the first child to list in the response. Ignored if reportKids is false.
*/
private buildNetworkAddressRequest(target: Eui64, reportKids: boolean, childStartIndex: number): Buffer {
this.writeIeeeAddr(target);
this.writeUInt8(reportKids ? 1 : 0);
this.writeUInt8(childStartIndex);
return this.getWritten();
}
/**
* @see ClusterId.IEEE_ADDRESS_REQUEST
* Can be sent to target, or to another node that will send to target.
* @param target NWK address for the request
* @param reportKids True to request that the target list their children in the response. [request type = 0x01]
* @param childStartIndex The index of the first child to list in the response. Ignored if reportKids is false.
*/
private buildIeeeAddressRequest(target: NodeId, reportKids: boolean, childStartIndex: number): Buffer {
this.writeUInt16(target);
this.writeUInt8(reportKids ? 1 : 0);
this.writeUInt8(childStartIndex);
return this.getWritten();
}
/**
* @see ClusterId.NODE_DESCRIPTOR_REQUEST
* @param target NWK address for the request
*/
private buildNodeDescriptorRequest(target: NodeId, fragmentationParameters?: FragmentationParametersGlobalTLV): Buffer {
this.writeUInt16(target);
if (fragmentationParameters) {
let length = 2;
if (fragmentationParameters.fragmentationOptions) {
length += 1;
}
if (fragmentationParameters.maxIncomingTransferUnit) {
length += 2;
}
this.writeGlobalTLV({tagId: GlobalTLV.FRAGMENTATION_PARAMETERS, length, tlv: fragmentationParameters});
}
return this.getWritten();
}
/**
* @see ClusterId.POWER_DESCRIPTOR_REQUEST
* @param target NWK address for the request
*/
private buildPowerDescriptorRequest(target: NodeId): Buffer {
this.writeUInt16(target);
return this.getWritten();
}
/**
* @see ClusterId.SIMPLE_DESCRIPTOR_REQUEST
* @param target NWK address for the request
* @param targetEndpoint The endpoint on the destination
*/
private buildSimpleDescriptorRequest(target: NodeId, targetEndpoint: number): Buffer {
this.writeUInt16(target);
this.writeUInt8(targetEndpoint);
return this.getWritten();
}
/**
* @see ClusterId.ACTIVE_ENDPOINTS_REQUEST
* @param target NWK address for the request
*/
private buildActiveEndpointsRequest(target: NodeId): Buffer {
this.writeUInt16(target);
return this.getWritten();
}
/**
* @see ClusterId.MATCH_DESCRIPTORS_REQUEST
* @param target NWK address for the request
* @param profileId Profile ID to be matched at the destination
* @param inClusterList List of Input ClusterIDs to be used for matching
* @param outClusterList List of Output ClusterIDs to be used for matching
*/
private buildMatchDescriptorRequest(target: NodeId, profileId: ProfileId, inClusterList: ClusterId[], outClusterList: ClusterId[]): Buffer {
this.writeUInt16(target);
this.writeUInt16(profileId);
this.writeUInt8(inClusterList.length);
this.writeListUInt16(inClusterList);
this.writeUInt8(outClusterList.length);
this.writeListUInt16(outClusterList);
return this.getWritten();
}
/**
* @see ClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST
* @param serverMask See Table 2-34 for bit assignments.
*/
private buildSystemServiceDiscoveryRequest(serverMask: ServerMask): Buffer {
this.writeUInt16(Utils.createServerMask(serverMask));
return this.getWritten();
}
/**
* @see ClusterId.PARENT_ANNOUNCE
* @param children The IEEE addresses of the children bound to the parent.
*/
private buildParentAnnounce(children: Eui64[]): Buffer {
this.writeUInt8(children.length);
for (const child of children) {
this.writeIeeeAddr(child);
}
return this.getWritten();
}
/**
* @see ClusterId.BIND_REQUEST
*
* @param source The IEEE address for the source.
* @param sourceEndpoint The source endpoint for the binding entry.
* @param clusterId The identifier of the cluster on the source device that is bound to the destination.
* @param type The addressing mode for the destination address used in this command, either ::UNICAST_BINDING, ::MULTICAST_BINDING.
* @param destination The destination address for the binding entry. IEEE for ::UNICAST_BINDING.
* @param groupAddress The destination address for the binding entry. Group ID for ::MULTICAST_BINDING.
* @param destinationEndpoint The destination endpoint for the binding entry. Only if ::UNICAST_BINDING.
*/
private buildBindRequest(
source: Eui64,
sourceEndpoint: number,
clusterId: ClusterId,
type: number,
destination: Eui64,
groupAddress: number,
destinationEndpoint: number,
): Buffer {
this.writeIeeeAddr(source);
this.writeUInt8(sourceEndpoint);
this.writeUInt16(clusterId);
this.writeUInt8(type);
switch (type) {
case UNICAST_BINDING: {
this.writeIeeeAddr(destination);
this.writeUInt8(destinationEndpoint);
break;
}
case MULTICAST_BINDING: {
this.writeUInt16(groupAddress);
break;
}
default:
throw new ZdoStatusError(Status.NOT_SUPPORTED);
}
return this.getWritten();
}
/**
* @see ClusterId.UNBIND_REQUEST
*
* @param source The IEEE address for the source.
* @param sourceEndpoint The source endpoint for the binding entry.
* @param clusterId The identifier of the cluster on the source device that is bound to the destination.
* @param type The addressing mode for the destination address used in this command, either ::UNICAST_BINDING, ::MULTICAST_BINDING.
* @param destination The destination address for the binding entry. IEEE for ::UNICAST_BINDING.
* @param groupAddress The destination address for the binding entry. Group ID for ::MULTICAST_BINDING.
* @param destinationEndpoint The destination endpoint for the binding entry. Only if ::UNICAST_BINDING.
*/
private buildUnbindRequest(
source: Eui64,
sourceEndpoint: number,
clusterId: ClusterId,
type: number,
destination: Eui64,
groupAddress: number,
destinationEndpoint: number,
): Buffer {
this.writeIeeeAddr(source);
this.writeUInt8(sourceEndpoint);
this.writeUInt16(clusterId);
this.writeUInt8(type);
switch (type) {
case UNICAST_BINDING: {
this.writeIeeeAddr(destination);
this.writeUInt8(destinationEndpoint);
break;
}
case MULTICAST_BINDING: {
this.writeUInt16(groupAddress);
break;
}
default:
throw new ZdoStatusError(Status.NOT_SUPPORTED);
}
return this.getWritten();
}
/**
* @see ClusterId.CLEAR_ALL_BINDINGS_REQUEST
*/
private buildClearAllBindingsRequest(tlv: ClearAllBindingsReqEUI64TLV): Buffer {
// ClearAllBindingsReqEUI64TLV: Local: ID: 0x00
this.writeUInt8(0x00);
this.writeUInt8(tlv.eui64List.length * EUI64_SIZE + 1 - 1);
this.writeUInt8(tlv.eui64List.length);
for (const entry of tlv.eui64List) {
this.writeIeeeAddr(entry);
}
return this.getWritten();
}
/**
* @see ClusterId.LQI_TABLE_REQUEST
* @param startIndex Starting Index for the requested elements of the Neighbor Table.
*/
private buildLqiTableRequest(startIndex: number): Buffer {
this.writeUInt8(startIndex);
return this.getWritten();
}
/**
* @see ClusterId.ROUTING_TABLE_REQUEST
* @param startIndex Starting Index for the requested elements of the Neighbor Table.
*/
private buildRoutingTableRequest(startIndex: number): Buffer {
this.writeUInt8(startIndex);
return this.getWritten();
}
/**
* @see ClusterId.BINDING_TABLE_REQUEST
* @param startIndex Starting Index for the requested elements of the Neighbor Table.
*/
private buildBindingTableRequest(startIndex: number): Buffer {
this.writeUInt8(startIndex);
return this.getWritten();
}
/**
* @see ClusterId.LEAVE_REQUEST
* @param deviceAddress All zeros if the target is to remove itself from the network or
* the EUI64 of a child of the target device to remove that child.
* @param leaveRequestFlags A bitmask of leave options. Include ::AND_REJOIN if the target is to rejoin the network immediately after leaving.
*/
private buildLeaveRequest(deviceAddress: Eui64, leaveRequestFlags: LeaveRequestFlags): Buffer {
this.writeIeeeAddr(deviceAddress);
this.writeUInt8(leaveRequestFlags);
return this.getWritten();
}
/**
* @see ClusterId.PERMIT_JOINING_REQUEST
* @param duration A value of 0x00 disables joining. A value of 0xFF enables joining. Any other value enables joining for that number of seconds.
* @param authentication Controls Trust Center authentication behavior.
* This field SHALL always have a value of 1, indicating a request to change the Trust Center policy.
* If a frame is received with a value of 0, it shall be treated as having a value of 1.
*/
private buildPermitJoining(duration: number, authentication: number, tlvs: Tlv[]): Buffer {
this.writeUInt8(duration);
this.writeUInt8(authentication);
// BeaconAppendixEncapsulationGlobalTLV
// - SupportedKeyNegotiationMethodsGlobalTLV
// - FragmentationParametersGlobalTLV
this.writeGlobalTLVs(tlvs);
return this.getWritten();
}
/**
* @see ClusterId.NWK_UPDATE_REQUEST
* @param channels See Table 3-7 for details on the 32-bit field structure..
* @param duration A value used to calculate the length of time to spend scanning each channel.
* The time spent scanning each channel is (aBaseSuperframeDuration * (2n + 1)) symbols, where n is the value of the duration parameter.
* If has a value of 0xfe this is a request for channel change.
* If has a value of 0xff this is a request to change the apsChannelMaskList and nwkManagerAddr attributes.
* @param count This field represents the number of energy scans to be conducted and reported.
* This field SHALL be present only if the duration is within the range of 0x00 to 0x05.
* @param nwkUpdateId The value of the nwkUpdateId contained in this request.
* This value is set by the Network Channel Manager prior to sending the message.
* This field SHALL only be present if the duration is 0xfe or 0xff.
* If the ScanDuration is 0xff, then the value in the nwkUpdateID SHALL be ignored.
* @param nwkManagerAddr This field SHALL be present only if the duration is set to 0xff, and, where present,
* indicates the NWK address for the device with the Network Manager bit set in its Node Descriptor.
*/
private buildNwkUpdateRequest(
channels: number[],
duration: number,
count: number | undefined,
nwkUpdateId: number | undefined,
nwkManagerAddr: number | undefined,
): Buffer {
this.writeUInt32(ZSpecUtils.channelsToUInt32Mask(channels));
this.writeUInt8(duration);
if (count !== undefined && duration >= 0x00 && duration <= 0x05) {
this.writeUInt8(count);
}
// TODO: What does "This value is set by the Network Channel Manager prior to sending the message." mean exactly??
// (isn't used/mentioned in EmberZNet, confirmed working if not set at all for channel change)
// for now, allow to bypass with undefined, otherwise should throw if undefined and duration passes below conditions (see NwkEnhancedUpdateRequest)
if (nwkUpdateId !== undefined && (duration === 0xfe || duration === 0xff)) {
this.writeUInt8(nwkUpdateId);
}
if (nwkManagerAddr !== undefined && duration === 0xff) {
this.writeUInt16(nwkManagerAddr);
}
return this.getWritten();
}
// /**
// * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest
// */
// private buildScanChannelsRequest(scanChannels: number[], duration: number, count: number): Buffer {
// return this.buildNwkUpdateRequest(scanChannels, duration, count, undefined, undefined);
// }
// /**
// * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest
// */
// private buildChannelChangeRequest(channel: number, nwkUpdateId: number | undefined): Buffer {
// return this.buildNwkUpdateRequest([channel], 0xfe, undefined, nwkUpdateId, undefined);
// }
// /**
// * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest
// */
// private buildSetActiveChannelsAndNwkManagerIdRequest(channels: number[], nwkUpdateId: number | undefined, nwkManagerAddr: NodeId): Buffer {
// return this.buildNwkUpdateRequest(channels, 0xff, undefined, nwkUpdateId, nwkManagerAddr);
// }
/**
* @see ClusterId.NWK_ENHANCED_UPDATE_REQUEST
* @param channelPages The set of channels (32-bit bitmap) for each channel page.
* The five most significant bits (b27,..., b31) represent the binary encoded Channel Page.
* The 27 least significant bits (b0, b1,... b26) indicate which channels are to be scanned
* (1 = scan, 0 = do not scan) for each of the 27 valid channels
* If duration is in the range 0x00 to 0x05, SHALL be restricted to a single page.
* @param duration A value used to calculate the length of time to spend scanning each channel.
* The time spent scanning each channel is (aBaseSuperframeDuration * (2n + 1)) symbols, where n is the value of the duration parameter.
* If has a value of 0xfe this is a request for channel change.
* If has a value of 0xff this is a request to change the apsChannelMaskList and nwkManagerAddr attributes.
* @param count This field represents the number of energy scans to be conducted and reported.
* This field SHALL be present only if the duration is within the range of 0x00 to 0x05.
* @param nwkUpdateId The value of the nwkUpdateId contained in this request.
* This value is set by the Network Channel Manager prior to sending the message.
* This field SHALL only be present if the duration is 0xfe or 0xff.
* If the ScanDuration is 0xff, then the value in the nwkUpdateID SHALL be ignored.
* @param nwkManagerAddr This field SHALL be present only if the duration is set to 0xff, and, where present,
* indicates the NWK address for the device with the Network Manager bit set in its Node Descriptor.
* @param configurationBitmask Defined in defined in section 2.4.3.3.12.
* The configurationBitmask must be added to the end of the list of parameters.
* This octet may or may not be present.
* If not present then assumption should be that it is enhanced active scan.
* Bit 0: This bit determines whether to do an Active Scan or Enhanced Active Scan.
* When the bit is set to 1 it indicates an Enhanced Active Scan.
* And in case of Enhanced Active scan EBR shall be sent with EPID filter instead of PJOIN filter.
* Bit 1-7: Reserved
*/
private buildNwkEnhancedUpdateRequest(
channelPages: number[],
duration: number,
count: number | undefined,
nwkUpdateId: number | undefined,
nwkManagerAddr: NodeId | undefined,
configurationBitmask: number | undefined,
): Buffer {
this.writeUInt8(channelPages.length);
for (const channelPage of channelPages) {
this.writeUInt32(ch