zigbee-on-host
Version:
Zigbee stack designed to run on a host and communicate with a radio co-processor (RCP)
460 lines • 16.9 kB
JavaScript
"use strict";
/**
* Save Type-Length-Value (TLV) binary serialization utilities.
*
* Performance-optimized for hot path state saving with extensibility.
* Format: [Tag: 1 byte][Length: 1-2 bytes][Value: N bytes]
*
* - Length < 128: single byte (most common case)
* - Length >= 128: two bytes with high bit set in first byte
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SAVE_FORMAT_VERSION = void 0;
exports.calculateTLVSize = calculateTLVSize;
exports.writeTLV = writeTLV;
exports.writeTLVUInt8 = writeTLVUInt8;
exports.writeTLVInt8 = writeTLVInt8;
exports.writeTLVUInt16LE = writeTLVUInt16LE;
exports.writeTLVUInt32LE = writeTLVUInt32LE;
exports.writeTLVBigUInt64LE = writeTLVBigUInt64LE;
exports.readTLVs = readTLVs;
exports.readAppLinkKeyTLV = readAppLinkKeyTLV;
exports.readDeviceTLVs = readDeviceTLVs;
exports.readSourceRouteTLVs = readSourceRouteTLVs;
exports.estimateTLVStateSize = estimateTLVStateSize;
exports.serializeSourceRouteEntry = serializeSourceRouteEntry;
exports.serializeDeviceEntry = serializeDeviceEntry;
exports.serializeAppLinkKeyEntry = serializeAppLinkKeyEntry;
const TLV_HEADER_SIZE_SHORT = 2; // tag (1) + length (1)
const TLV_HEADER_SIZE_LONG = 3; // tag (1) + length (2)
const LENGTH_THRESHOLD = 128;
exports.SAVE_FORMAT_VERSION = 1;
/**
* Calculate the required buffer size for a TLV entry.
*
* @param valueLength
* @returns
*/
function calculateTLVSize(valueLength) {
return (valueLength < LENGTH_THRESHOLD ? TLV_HEADER_SIZE_SHORT : TLV_HEADER_SIZE_LONG) + valueLength;
}
/**
* Write a TLV entry to buffer. Returns new offset.
* @param buffer
* @param offset
* @param tag
* @param value
* @returns
*/
function writeTLV(buffer, offset, tag, value) {
const length = value.length;
offset = buffer.writeUInt8(tag, offset);
if (length < LENGTH_THRESHOLD) {
offset = buffer.writeUInt8(length, offset);
}
else {
// Two-byte length with high bit set
offset = buffer.writeUInt8((length >> 8) | 0x80, offset);
offset = buffer.writeUInt8(length & 0xff, offset);
}
value.copy(buffer, offset);
return offset + length;
}
/**
* Write a single-byte TLV entry (optimized path).
* @param buffer
* @param offset
* @param tag
* @param value
* @returns
*/
function writeTLVUInt8(buffer, offset, tag, value) {
offset = buffer.writeUInt8(tag, offset);
offset = buffer.writeUInt8(1, offset);
offset = buffer.writeUInt8(value, offset);
return offset;
}
/**
* Write a signed single-byte TLV entry (optimized path).
* @param buffer
* @param offset
* @param tag
* @param value
* @returns
*/
function writeTLVInt8(buffer, offset, tag, value) {
offset = buffer.writeUInt8(tag, offset);
offset = buffer.writeUInt8(1, offset);
offset = buffer.writeInt8(value, offset);
return offset;
}
/**
* Write a 2-byte TLV entry (optimized path).
* @param buffer
* @param offset
* @param tag
* @param value
* @returns
*/
function writeTLVUInt16LE(buffer, offset, tag, value) {
offset = buffer.writeUInt8(tag, offset);
offset = buffer.writeUInt8(2, offset);
offset = buffer.writeUInt16LE(value, offset);
return offset;
}
/**
* Write a 4-byte TLV entry (optimized path).
* @param buffer
* @param offset
* @param tag
* @param value
* @returns
*/
function writeTLVUInt32LE(buffer, offset, tag, value) {
offset = buffer.writeUInt8(tag, offset);
offset = buffer.writeUInt8(4, offset);
offset = buffer.writeUInt32LE(value, offset);
return offset;
}
/**
* Write an 8-byte BigInt TLV entry (optimized path).
* @param buffer
* @param offset
* @param tag
* @param value
* @returns
*/
function writeTLVBigUInt64LE(buffer, offset, tag, value) {
offset = buffer.writeUInt8(tag, offset);
offset = buffer.writeUInt8(8, offset);
offset = buffer.writeBigUInt64LE(value, offset);
return offset;
}
/**
* Read and parse top-level state TLVs into typed structure.
* @param buffer State buffer
* @returns Strongly-typed parsed state with direct property access
*/
function readTLVs(buffer, startOffset = 0, endOffset) {
const state = {
deviceEntries: [],
appLinkKeys: [],
};
let offset = startOffset;
const limit = endOffset ?? buffer.length;
while (offset < limit) {
if (offset + 2 > limit) {
break;
}
const tag = buffer.readUInt8(offset++);
if (tag === 255 /* TLVTag.END_MARKER */) {
break;
}
const lengthByte = buffer.readUInt8(offset++);
let length;
if (lengthByte < LENGTH_THRESHOLD) {
length = lengthByte;
}
else {
if (offset >= limit) {
break;
}
length = ((lengthByte & 0x7f) << 8) | buffer.readUInt8(offset++);
}
if (offset + length > limit) {
break;
}
// Parse value directly to final type based on tag
switch (tag) {
case 240 /* TLVTag.VERSION */:
state.version = buffer.readUInt8(offset);
break;
case 1 /* TLVTag.EUI64 */:
state.eui64 = buffer.readBigUInt64LE(offset);
break;
case 2 /* TLVTag.PAN_ID */:
state.panId = buffer.readUInt16LE(offset);
break;
case 3 /* TLVTag.EXTENDED_PAN_ID */:
state.extendedPanId = buffer.readBigUInt64LE(offset);
break;
case 4 /* TLVTag.CHANNEL */:
state.channel = buffer.readUInt8(offset);
break;
case 5 /* TLVTag.NWK_UPDATE_ID */:
state.nwkUpdateId = buffer.readUInt8(offset);
break;
case 6 /* TLVTag.TX_POWER */:
state.txPower = buffer.readInt8(offset);
break;
case 7 /* TLVTag.NETWORK_KEY */:
state.networkKey = buffer.subarray(offset, offset + length);
break;
case 8 /* TLVTag.NETWORK_KEY_FRAME_COUNTER */:
state.networkKeyFrameCounter = buffer.readUInt32LE(offset);
break;
case 9 /* TLVTag.NETWORK_KEY_SEQUENCE_NUMBER */:
state.networkKeySequenceNumber = buffer.readUInt8(offset);
break;
case 10 /* TLVTag.TC_KEY */:
state.tcKey = buffer.subarray(offset, offset + length);
break;
case 11 /* TLVTag.TC_KEY_FRAME_COUNTER */:
state.tcKeyFrameCounter = buffer.readUInt32LE(offset);
break;
case 128 /* TLVTag.DEVICE_ENTRY */:
state.deviceEntries.push(readDeviceTLVs(buffer, offset, offset + length));
break;
case 12 /* TLVTag.APP_LINK_KEY_ENTRY */:
state.appLinkKeys.push(readAppLinkKeyTLV(buffer, offset));
break;
// Unknown tags ignored for forward compatibility
}
offset += length;
}
// Validate required fields
if (state.eui64 === undefined ||
state.panId === undefined ||
state.extendedPanId === undefined ||
state.channel === undefined ||
state.nwkUpdateId === undefined ||
state.txPower === undefined ||
!state.networkKey ||
state.networkKeyFrameCounter === undefined ||
state.networkKeySequenceNumber === undefined ||
!state.tcKey ||
state.tcKeyFrameCounter === undefined) {
throw new Error("Missing required network parameters in state file");
}
return state;
}
function readAppLinkKeyTLV(buffer, startOffset) {
const deviceA = buffer.readBigUInt64LE(startOffset);
const deviceB = buffer.readBigUInt64LE(startOffset + 8);
const key = Buffer.from(buffer.subarray(startOffset + 16, startOffset + 16 + 16 /* ZigbeeConsts.SEC_KEYSIZE */));
return { deviceA, deviceB, key };
}
/**
* Read and parse device entry TLVs into typed structure with final values.
* All values are parsed directly from buffers during reading.
* @param buffer Whole buffer
* @param startOffset Offset to start parsing TLVs from
* @param endOffset Offset to end parsing
* @returns Strongly-typed parsed device with final values ready to use
*/
function readDeviceTLVs(buffer, startOffset, endOffset) {
const device = {
sourceRouteEntries: [],
};
let offset = startOffset;
const limit = endOffset;
while (offset < limit) {
if (offset + 2 > limit) {
break;
}
const tag = buffer.readUInt8(offset++);
const lengthByte = buffer.readUInt8(offset++);
let length;
if (lengthByte < LENGTH_THRESHOLD) {
length = lengthByte;
}
else {
if (offset >= limit) {
break;
}
length = ((lengthByte & 0x7f) << 8) | buffer.readUInt8(offset++);
}
if (offset + length > limit) {
break;
}
// Parse value directly to final type based on tag
switch (tag) {
case 1 /* DeviceTLVTag.DEVICE_ADDRESS64 */:
device.address64 = buffer.readBigUInt64LE(offset);
break;
case 2 /* DeviceTLVTag.DEVICE_ADDRESS16 */:
device.address16 = buffer.readUInt16LE(offset);
break;
case 3 /* DeviceTLVTag.DEVICE_CAPABILITIES */:
device.capabilities = buffer.readUInt8(offset);
break;
case 4 /* DeviceTLVTag.DEVICE_AUTHORIZED */:
device.authorized = Boolean(buffer.readUInt8(offset));
break;
case 5 /* DeviceTLVTag.DEVICE_NEIGHBOR */:
device.neighbor = Boolean(buffer.readUInt8(offset));
break;
case 6 /* DeviceTLVTag.DEVICE_LAST_NWK_KEY_SEQ */:
device.lastTransportedNetworkKeySeq = buffer.readUInt8(offset);
break;
case 64 /* DeviceTLVTag.SOURCE_ROUTE_ENTRY */:
device.sourceRouteEntries.push(readSourceRouteTLVs(buffer, offset, offset + length));
break;
// Unknown tags ignored
}
offset += length;
}
// Validate required fields
if (device.address64 === undefined ||
device.address16 === undefined ||
device.capabilities === undefined ||
device.authorized === undefined ||
device.neighbor === undefined) {
throw new Error("Missing required device fields");
}
return device;
}
/**
* Read and parse source route entry TLVs into final values.
* All values are parsed directly from buffers during reading.
* @param buffer Whole buffer
* @param startOffset Offset to start parsing TLVs from
* @param endOffset Offset to end parsing
* @returns Parsed source route with final values
*/
function readSourceRouteTLVs(buffer, startOffset, endOffset) {
let pathCost;
const relayAddresses = [];
let lastUpdated;
let offset = startOffset;
const limit = endOffset;
while (offset < limit) {
if (offset + 2 > limit) {
break;
}
const tag = buffer.readUInt8(offset++);
const lengthByte = buffer.readUInt8(offset++);
let length;
if (lengthByte < LENGTH_THRESHOLD) {
length = lengthByte;
}
else {
if (offset >= limit) {
break;
}
length = ((lengthByte & 0x7f) << 8) | buffer.readUInt8(offset++);
}
if (offset + length > limit) {
break;
}
// Parse value directly to final type based on tag
switch (tag) {
case 1 /* SourceRouteTLVTag.PATH_COST */: {
pathCost = buffer.readUInt8(offset);
break;
}
case 2 /* SourceRouteTLVTag.RELAY_ADDRESSES */: {
// Parse relay addresses array
const relayCount = length / 2;
for (let i = 0; i < relayCount; i++) {
relayAddresses.push(buffer.readUInt16LE(offset + i * 2));
}
break;
}
case 3 /* SourceRouteTLVTag.LAST_UPDATED */:
lastUpdated = buffer.readUIntLE(offset, 6);
break;
// Unknown tags ignored
}
offset += length;
}
// Validate required fields
if (pathCost === undefined || lastUpdated === undefined) {
throw new Error("Missing required source route fields");
}
return { pathCost, relayAddresses, lastUpdated };
}
/**
* Calculate total size needed for network state with current device count.
* Provides an upper bound estimate for buffer allocation.
* @param deviceCount
* @returns
*/
function estimateTLVStateSize(deviceCount, appLinkKeyCount = 0) {
// version + network parameters
let size = 250;
// each device entry + source routes (to ~10% of network, min 5)
const avgDeviceSize = 50 + Math.max(Math.ceil(deviceCount * 0.1), 5) * 15;
size += deviceCount * calculateTLVSize(avgDeviceSize);
if (appLinkKeyCount > 0) {
const appLinkEntrySize = 8 + 8 + 1 + 16;
size += appLinkKeyCount * calculateTLVSize(appLinkEntrySize);
}
// end marker
size += 1;
return size;
}
/**
* Serialize a source route entry to TLV format.
* @param pathCost
* @param relayAddresses
* @param lastUpdated
* @returns Buffer containing the TLV-encoded source route entry.
*/
function serializeSourceRouteEntry(pathCost, relayAddresses, lastUpdated) {
// Calculate size: path cost (3) + relay addresses (2-3 + n*2) + lastUpdated (2-3 + 6)
const size = calculateTLVSize(1) + calculateTLVSize(relayAddresses.length * 2) + calculateTLVSize(6);
const buffer = Buffer.allocUnsafe(size);
let offset = 0;
offset = writeTLVUInt8(buffer, offset, 1 /* SourceRouteTLVTag.PATH_COST */, pathCost);
if (relayAddresses.length > 0) {
const relayBuf = Buffer.allocUnsafe(relayAddresses.length * 2);
let relayOffset = 0;
for (const address of relayAddresses) {
relayOffset = relayBuf.writeUInt16LE(address, relayOffset);
}
offset = writeTLV(buffer, offset, 2 /* SourceRouteTLVTag.RELAY_ADDRESSES */, relayBuf);
}
// Write lastUpdated as 48-bit timestamp (fits until year 2255)
const timestampBuf = Buffer.allocUnsafe(6);
timestampBuf.writeUIntLE(lastUpdated, 0, 6);
offset = writeTLV(buffer, offset, 3 /* SourceRouteTLVTag.LAST_UPDATED */, timestampBuf);
return buffer.subarray(0, offset);
}
/**
* Serialize device entry with source routes to TLV format.
* @param address64
* @param address16
* @param capabilities
* @param authorized
* @param neighbor
* @param sourceRouteEntries
* @returns Buffer containing the TLV-encoded device entry.
*/
function serializeDeviceEntry(address64, address16, capabilities, authorized, neighbor, lastTransportedNetworkKeySeq, sourceRouteEntries) {
// Estimate size generously
let estimatedSize = 100; // base fields with TLV overhead
if (sourceRouteEntries) {
for (const entry of sourceRouteEntries) {
estimatedSize += calculateTLVSize(50 + entry.relayAddresses.length * 2);
}
}
const buffer = Buffer.allocUnsafe(estimatedSize);
let offset = 0;
// Write device core fields
offset = writeTLVBigUInt64LE(buffer, offset, 1 /* DeviceTLVTag.DEVICE_ADDRESS64 */, address64);
offset = writeTLVUInt16LE(buffer, offset, 2 /* DeviceTLVTag.DEVICE_ADDRESS16 */, address16);
offset = writeTLVUInt8(buffer, offset, 3 /* DeviceTLVTag.DEVICE_CAPABILITIES */, capabilities);
offset = writeTLVUInt8(buffer, offset, 4 /* DeviceTLVTag.DEVICE_AUTHORIZED */, authorized ? 1 : 0);
offset = writeTLVUInt8(buffer, offset, 5 /* DeviceTLVTag.DEVICE_NEIGHBOR */, neighbor ? 1 : 0);
if (lastTransportedNetworkKeySeq !== undefined) {
offset = writeTLVUInt8(buffer, offset, 6 /* DeviceTLVTag.DEVICE_LAST_NWK_KEY_SEQ */, lastTransportedNetworkKeySeq);
}
// Write source route entries (if any)
if (sourceRouteEntries) {
for (const entry of sourceRouteEntries) {
const routeEntry = serializeSourceRouteEntry(entry.pathCost, entry.relayAddresses, entry.lastUpdated);
offset = writeTLV(buffer, offset, 64 /* DeviceTLVTag.SOURCE_ROUTE_ENTRY */, routeEntry);
}
}
return buffer.subarray(0, offset);
}
function serializeAppLinkKeyEntry(deviceA, deviceB, key) {
const payload = Buffer.allocUnsafe(16 + 16 /* ZigbeeConsts.SEC_KEYSIZE */);
let offset = 0;
offset = payload.writeBigUInt64LE(deviceA, offset);
offset = payload.writeBigUInt64LE(deviceB, offset);
key.copy(payload, offset);
return payload;
}
//# sourceMappingURL=save-serializer.js.map