UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

789 lines (718 loc) 22.3 kB
import { CommandClasses, encodeBitMask, MAX_NODES, parseBitMask, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core/safe"; import { num2hex, pick, sum } from "@zwave-js/shared/safe"; import { SUC_MAX_UPDATES } from "../consts"; import { nodeHasInfo } from "../convert"; import { encodeRoute, encodeSUCUpdateEntry, parseRoute, parseSUCUpdateEntry, Route, SUCUpdateEntry, } from "../files"; import { encodeNVM500NodeInfo, encodeNVMDescriptor, encodeNVMModuleDescriptor, NVM500NodeInfo, NVMDescriptor, NVMModuleDescriptor, parseNVM500NodeInfo, parseNVMDescriptor, parseNVMModuleDescriptor, } from "./EntryParsers"; import { Bridge_6_6x } from "./parsers/Bridge_6_6x"; import { Bridge_6_7x } from "./parsers/Bridge_6_7x"; import { Bridge_6_8x } from "./parsers/Bridge_6_8x"; import { Static_6_6x } from "./parsers/Static_6_6x"; import { Static_6_7x } from "./parsers/Static_6_7x"; import { Static_6_8x } from "./parsers/Static_6_8x"; import { APPL_NODEPARM_MAX, CONFIGURATION_VALID_0, CONFIGURATION_VALID_1, MAGIC_VALUE, NVMData, NVMEntryName, NVMEntrySizes, NVMEntryType, NVMLayout, NVMModuleType, NVM_SERIALAPI_HOST_SIZE, ParsedNVMEntry, ROUTECACHE_VALID, } from "./shared"; export interface NVM500Details { name: string; library: "static" | "bridge"; protocolVersions: string[]; layout: NVMLayout; } export const nmvDetails500 = [ Bridge_6_6x, Bridge_6_7x, Bridge_6_8x, Static_6_6x, Static_6_7x, Static_6_8x, ] as const; /** Detects which parser is able to parse the given NVM */ export function createParser(nvm: Buffer): NVMParser | undefined { for (const impl of nmvDetails500) { try { const parser = new NVMParser(impl, nvm); return parser; } catch { continue; } } } export class NVMParser { public constructor(private readonly impl: NVM500Details, nvm: Buffer) { this.parse(nvm); if (!this.isValid()) throw new ZWaveError( "Invalid NVM!", ZWaveErrorCodes.NVM_InvalidFormat, ); } /** Tests if the given NVM is a valid NVM for this parser version */ private isValid(): boolean { // Checking if an NVM is valid requires checking multiple bytes at different locations const eeoffset_magic = this.cache.get("EEOFFSET_MAGIC_far") ?.data[0] as number; const configuration_valid_0 = this.cache.get( "NVM_CONFIGURATION_VALID_far", )?.data[0] as number; const configuration_valid_1 = this.cache.get( "NVM_CONFIGURATION_REALLYVALID_far", )?.data[0] as number; const routecache_valid = this.cache.get("EX_NVM_ROUTECACHE_MAGIC_far") ?.data[0] as number; const nvm = this.cache.get("nvmDescriptor")?.data[0] as NVMDescriptor; const endMarker = this.cache.get("nvmModuleSizeEndMarker") ?.data[0] as number; return ( eeoffset_magic === MAGIC_VALUE && configuration_valid_0 === CONFIGURATION_VALID_0 && configuration_valid_1 === CONFIGURATION_VALID_1 && routecache_valid === ROUTECACHE_VALID && this.impl.protocolVersions.includes(nvm.protocolVersion) && endMarker === 0 ); } private cache = new Map<NVMEntryName, ParsedNVMEntry>(); private parse(nvm: Buffer): void { let offset = 0; let moduleStart = -1; let moduleSize = -1; const nvmEnd = nvm.readUInt16BE(0); for (const entry of this.impl.layout) { const size = entry.size ?? NVMEntrySizes[entry.type]; if (entry.type === NVMEntryType.NVMModuleSize) { if (moduleStart !== -1) { // All following NVM modules must start at the last module's end offset = moduleStart + moduleSize; } moduleStart = offset; moduleSize = nvm.readUInt16BE(offset); } else if (entry.type === NVMEntryType.NVMModuleDescriptor) { // The module descriptor is always at the end of the module offset = moduleStart + moduleSize - size; } if (entry.offset != undefined && entry.offset !== offset) { // The entry has a defined offset but is at the wrong location throw new ZWaveError( `${entry.name} is at wrong location in NVM buffer!`, ZWaveErrorCodes.NVM_InvalidFormat, ); } const data: Buffer[] = []; for (let i = 0; i < entry.count; i++) { data.push( nvm.slice(offset + i * size, offset + (i + 1) * size), ); } const converted = data.map((buffer) => { switch (entry.type) { case NVMEntryType.Byte: return buffer.readUInt8(0); case NVMEntryType.Word: case NVMEntryType.NVMModuleSize: return buffer.readUInt16BE(0); case NVMEntryType.DWord: return buffer.readUInt32BE(0); case NVMEntryType.NodeInfo: if (buffer.every((byte) => byte === 0)) return undefined; return parseNVM500NodeInfo(buffer, 0); case NVMEntryType.NodeMask: return parseBitMask(buffer); case NVMEntryType.SUCUpdateEntry: if (buffer.every((byte) => byte === 0)) return undefined; return parseSUCUpdateEntry(buffer, 0); case NVMEntryType.Route: if (buffer.every((byte) => byte === 0)) return undefined; return parseRoute(buffer, 0); case NVMEntryType.NVMModuleDescriptor: { const ret = parseNVMModuleDescriptor(buffer); if (ret.size !== moduleSize) { throw new ZWaveError( "NVM module descriptor size does not match module size!", ZWaveErrorCodes.NVM_InvalidFormat, ); } return ret; } case NVMEntryType.NVMDescriptor: return parseNVMDescriptor(buffer); default: // This includes NVMEntryType.BUFFER return buffer; } }); this.cache.set(entry.name, { ...entry, data: converted, }); // Skip forward offset += size * entry.count; if (offset >= nvmEnd) return; } } private getOne<T extends NVMData>(key: NVMEntryName): T { return this.cache.get(key)?.data[0] as T; } private getAll<T extends NVMData>( key: NVMEntryName, ): T extends Buffer ? T : T[] { return this.cache.get(key)?.data as any; } public toJSON(): Required<NVM500JSON> { const nvmDescriptor = this.getOne<NVMDescriptor>("nvmDescriptor"); const ownHomeId = this.getOne<number>("EX_NVM_HOME_ID_far"); const learnedHomeId = this.getOne<number>("NVM_HOMEID_far"); const lastNodeId = this.getOne<number>( "EX_NVM_LAST_USED_NODE_ID_START_far", ); const maxNodeId = this.getOne<number>("EX_NVM_MAX_NODE_ID_far"); const nodeInfos = this.getAll<NVM500NodeInfo>( "EX_NVM_NODE_TABLE_START_far", ); const sucUpdateIndizes = this.getAll<number>( "EX_NVM_SUC_CONTROLLER_LIST_START_far", ); const appRouteLock = new Set( this.getOne<number[]>("EX_NVM_ROUTECACHE_APP_LOCK_far"), ); const routeSlaveSUC = new Set( this.getOne<number[]>("EX_NVM_SUC_ROUTING_SLAVE_LIST_START_far"), ); const pendingDiscovery = new Set( this.getOne<number[]>("NVM_PENDING_DISCOVERY_far"), ); const sucPendingUpdate = new Set( this.getOne<number[]>("EX_NVM_PENDING_UPDATE_far"), ); const virtualNodes = new Set( this.getOne<number[]>("EX_NVM_BRIDGE_NODEPOOL_START_far") ?? [], ); const lwr = this.getAll<Route>("EX_NVM_ROUTECACHE_START_far"); const nlwr = this.getAll<Route>("EX_NVM_ROUTECACHE_NLWR_SR_START_far"); const neighbors = this.getAll<number[]>( "EX_NVM_ROUTING_TABLE_START_far", ); const numCCs = this.getOne<number>("EEOFFSET_CMDCLASS_LEN_far"); const commandClasses = this.getAll<CommandClasses>( "EEOFFSET_CMDCLASS_far", ).slice(0, numCCs); const nodes: Record<number, NVM500JSONNode> = {}; for (let nodeId = 1; nodeId <= MAX_NODES; nodeId++) { const nodeInfo = nodeInfos[nodeId - 1]; const isVirtual = virtualNodes.has(nodeId); if (!nodeInfo) { if (isVirtual) { nodes[nodeId] = { isVirtual: true }; } continue; } nodes[nodeId] = { ...nodeInfo, isVirtual, neighbors: neighbors[nodeId - 1] ?? [], sucUpdateIndex: sucUpdateIndizes[nodeId - 1], appRouteLock: appRouteLock.has(nodeId), routeSlaveSUC: routeSlaveSUC.has(nodeId), sucPendingUpdate: sucPendingUpdate.has(nodeId), pendingDiscovery: pendingDiscovery.has(nodeId), lwr: lwr[nodeId - 1] ?? null, nlwr: nlwr[nodeId - 1] ?? null, }; } return { format: 500, meta: { library: this.impl.library, ...pick(nvmDescriptor, [ "manufacturerID", "firmwareID", "productType", "productID", ]), }, controller: { protocolVersion: nvmDescriptor.protocolVersion, applicationVersion: nvmDescriptor.firmwareVersion, ownHomeId: num2hex(ownHomeId), learnedHomeId: learnedHomeId ? num2hex(learnedHomeId) : null, nodeId: this.getOne<number>("NVM_NODEID_far"), lastNodeId, staticControllerNodeId: this.getOne<number>( "EX_NVM_STATIC_CONTROLLER_NODE_ID_START_far", ), sucLastIndex: this.getOne<number>( "EX_NVM_SUC_LAST_INDEX_START_far", ), controllerConfiguration: this.getOne<number>( "EX_NVM_CONTROLLER_CONFIGURATION_far", ), sucUpdateEntries: this.getAll<SUCUpdateEntry>( "EX_NVM_SUC_NODE_LIST_START_far", ).filter(Boolean), maxNodeId, reservedId: this.getOne<number>("EX_NVM_RESERVED_ID_far"), systemState: this.getOne<number>("NVM_SYSTEM_STATE"), watchdogStarted: this.getOne<number>( "EEOFFSET_WATCHDOG_STARTED_far", ), rfConfig: { powerLevelNormal: this.getAll<number>( "EEOFFSET_POWERLEVEL_NORMAL_far", ), powerLevelLow: this.getAll<number>( "EEOFFSET_POWERLEVEL_LOW_far", ), powerMode: this.getOne<number>( "EEOFFSET_MODULE_POWER_MODE_far", ), powerModeExtintEnable: this.getOne<number>( "EEOFFSET_MODULE_POWER_MODE_EXTINT_ENABLE_far", ), powerModeWutTimeout: this.getOne<number>( "EEOFFSET_MODULE_POWER_MODE_WUT_TIMEOUT_far", ), }, preferredRepeaters: this.getOne<number[]>( "NVM_PREFERRED_REPEATERS_far", ), commandClasses, applicationData: this.getOne<Buffer>( "EEOFFSET_HOST_OFFSET_START_far", ).toString("hex"), }, nodes, }; } } export class NVMSerializer { public constructor(private readonly impl: NVM500Details) {} public readonly entries = new Map<NVMEntryName, ParsedNVMEntry>(); private nvmSize: number = 0; private setOne<T extends NVMData>(key: NVMEntryName, value: T) { const entry = this.impl.layout.find((e) => e.name === key); // Skip entries not present in this layout if (!entry) return; this.entries.set(key, { ...entry, data: [value], }); } private setMany<T extends NVMData>(key: NVMEntryName, value: T[]) { const entry = this.impl.layout.find((e) => e.name === key); // Skip entries not present in this layout if (!entry) return; this.entries.set(key, { ...entry, data: value, }); } private setFromNodeMap<T extends NVMData>( key: NVMEntryName, map: Map<number, T>, fill?: number, ) { const entry = this.impl.layout.find((e) => e.name === key); // Skip entries not present in this layout if (!entry) return; const data: (T | undefined)[] = new Array(MAX_NODES).fill(fill); for (const [nodeId, value] of map) { data[nodeId - 1] = value; } this.entries.set(key, { ...entry, data, }); } private fill(key: NVMEntryName, value: number) { const entry = this.impl.layout.find((e) => e.name === key); // Skip entries not present in this layout if (!entry) return; const size = entry.size ?? NVMEntrySizes[entry.type]; const data: any[] = []; for (let i = 1; i <= entry.count; i++) { switch (entry.type) { case NVMEntryType.Byte: case NVMEntryType.Word: case NVMEntryType.DWord: data.push(value); break; case NVMEntryType.Buffer: data.push(Buffer.alloc(size, value)); break; case NVMEntryType.NodeMask: // This ignores the fill value data.push(new Array(size).fill(0)); break; default: throw new Error( `Cannot fill entry of type ${NVMEntryType[entry.type]}`, ); } } this.entries.set(key, { ...entry, data, }); } public parseJSON( json: Required<NVM500JSON>, protocolVersion: string, ): void { this.entries.clear(); // Set controller infos const c = json.controller; this.setOne( "EX_NVM_HOME_ID_far", parseInt(c.ownHomeId.replace(/^0x/, ""), 16), ); if (c.learnedHomeId) { this.setOne( "NVM_HOMEID_far", parseInt(c.learnedHomeId.replace(/^0x/, ""), 16), ); } else { this.setOne("NVM_HOMEID_far", 0); } this.setOne("EX_NVM_LAST_USED_NODE_ID_START_far", c.lastNodeId); this.setOne("NVM_NODEID_far", c.nodeId); this.setOne( "EX_NVM_STATIC_CONTROLLER_NODE_ID_START_far", c.staticControllerNodeId, ); this.setOne("EX_NVM_SUC_LAST_INDEX_START_far", c.sucLastIndex); this.setOne( "EX_NVM_CONTROLLER_CONFIGURATION_far", c.controllerConfiguration, ); const sucUpdateEntries = new Array(SUC_MAX_UPDATES).fill(undefined); for (let i = 0; i < c.sucUpdateEntries.length; i++) { if (i < SUC_MAX_UPDATES) { sucUpdateEntries[i] = c.sucUpdateEntries[i]; } } this.setMany("EX_NVM_SUC_NODE_LIST_START_far", sucUpdateEntries); this.setOne("EX_NVM_MAX_NODE_ID_far", c.maxNodeId); this.setOne("EX_NVM_RESERVED_ID_far", c.reservedId); this.setOne("NVM_SYSTEM_STATE", c.systemState); this.setOne("EEOFFSET_WATCHDOG_STARTED_far", c.watchdogStarted); this.setMany( "EEOFFSET_POWERLEVEL_NORMAL_far", c.rfConfig.powerLevelNormal, ); this.setMany("EEOFFSET_POWERLEVEL_LOW_far", c.rfConfig.powerLevelLow); this.setOne("EEOFFSET_MODULE_POWER_MODE_far", c.rfConfig.powerMode); this.setOne( "EEOFFSET_MODULE_POWER_MODE_EXTINT_ENABLE_far", c.rfConfig.powerModeExtintEnable, ); this.setOne( "EEOFFSET_MODULE_POWER_MODE_WUT_TIMEOUT_far", c.rfConfig.powerModeWutTimeout, ); this.setOne("NVM_PREFERRED_REPEATERS_far", c.preferredRepeaters); this.setOne("EEOFFSET_CMDCLASS_LEN_far", c.commandClasses.length); const CCs = new Array(APPL_NODEPARM_MAX).fill(0xff); for (let i = 0; i < c.commandClasses.length; i++) { if (i < APPL_NODEPARM_MAX) { CCs[i] = c.commandClasses[i]; } } this.setMany("EEOFFSET_CMDCLASS_far", CCs); if (c.applicationData) { this.setOne<Buffer>( "EEOFFSET_HOST_OFFSET_START_far", Buffer.from(c.applicationData, "hex"), ); } else { this.setOne<Buffer>( "EEOFFSET_HOST_OFFSET_START_far", Buffer.alloc(NVM_SERIALAPI_HOST_SIZE, 0xff), ); } // Set node infos const nodeInfos = new Map<number, NVM500NodeInfo>(); const sucUpdateIndizes = new Map<number, number>(); const appRouteLock: number[] = []; const routeSlaveSUC: number[] = []; const pendingDiscovery: number[] = []; const sucPendingUpdate: number[] = []; const virtualNodes: number[] = []; const lwr = new Map<number, Route>(); const nlwr = new Map<number, Route>(); const neighbors = new Map<number, number[]>(); for (const [id, node] of Object.entries(json.nodes)) { const nodeId = parseInt(id); if (!nodeHasInfo(node)) { virtualNodes.push(nodeId); continue; } nodeInfos.set( nodeId, pick(node, [ "isListening", "isFrequentListening", "isRouting", "supportedDataRates", "protocolVersion", "optionalFunctionality", "nodeType", "supportsSecurity", "supportsBeaming", "genericDeviceClass", "specificDeviceClass", ]), ); sucUpdateIndizes.set(nodeId, node.sucUpdateIndex); if (node.appRouteLock) appRouteLock.push(nodeId); if (node.routeSlaveSUC) routeSlaveSUC.push(nodeId); if (node.pendingDiscovery) pendingDiscovery.push(nodeId); if (node.sucPendingUpdate) sucPendingUpdate.push(nodeId); if (node.lwr) lwr.set(nodeId, node.lwr); if (node.nlwr) nlwr.set(nodeId, node.nlwr); neighbors.set(nodeId, node.neighbors); } this.setFromNodeMap<NVM500NodeInfo>( "EX_NVM_NODE_TABLE_START_far", nodeInfos, ); this.setFromNodeMap( "EX_NVM_SUC_CONTROLLER_LIST_START_far", sucUpdateIndizes, 0xfe, ); this.setOne<number[]>("EX_NVM_ROUTECACHE_APP_LOCK_far", appRouteLock); this.setOne<number[]>( "EX_NVM_SUC_ROUTING_SLAVE_LIST_START_far", routeSlaveSUC, ); this.setOne<number[]>("NVM_PENDING_DISCOVERY_far", pendingDiscovery); this.setOne<number[]>("EX_NVM_PENDING_UPDATE_far", sucPendingUpdate); this.setOne<number[]>("EX_NVM_BRIDGE_NODEPOOL_START_far", virtualNodes); this.setFromNodeMap("EX_NVM_ROUTECACHE_START_far", lwr); this.setFromNodeMap("EX_NVM_ROUTECACHE_NLWR_SR_START_far", nlwr); this.setFromNodeMap("EX_NVM_ROUTING_TABLE_START_far", neighbors); // Set some entries that are always identical this.setOne("NVM_CONFIGURATION_VALID_far", CONFIGURATION_VALID_0); this.setOne("NVM_CONFIGURATION_REALLYVALID_far", CONFIGURATION_VALID_1); this.setOne("EEOFFSET_MAGIC_far", MAGIC_VALUE); this.setOne("EX_NVM_ROUTECACHE_MAGIC_far", ROUTECACHE_VALID); this.setOne("nvmModuleSizeEndMarker", 0); // Set NVM descriptor this.setOne<NVMDescriptor>("nvmDescriptor", { ...pick(json.meta, [ "manufacturerID", "productType", "productID", "firmwareID", ]), // Override the protocol version with the specified one protocolVersion, firmwareVersion: c.applicationVersion, }); // Set dummy entries we're never going to fill this.fill("NVM_INTERNAL_RESERVED_1_far", 0); this.fill("NVM_INTERNAL_RESERVED_2_far", 0xff); this.fill("NVM_INTERNAL_RESERVED_3_far", 0); this.fill("NVM_RTC_TIMERS_far", 0); this.fill("EX_NVM_SUC_ACTIVE_START_far", 0); this.fill("EX_NVM_ZENSOR_TABLE_START_far", 0); this.fill("NVM_SECURITY0_KEY_far", 0); // Auto-compute some fields const entrySizes = this.impl.layout.map( (e) => e.count * (e.size ?? NVMEntrySizes[e.type]), ); this.nvmSize = sum(entrySizes); this.setOne("nvmTotalEnd", this.nvmSize - 1); // the value points to the last byte let moduleSize = 0; let moduleKey: NVMEntryName; for (let i = 0; i < this.impl.layout.length; i++) { const entry = this.impl.layout[i]; if (entry.type === NVMEntryType.NVMModuleSize) { // Start of NVM module moduleSize = 0; moduleKey = entry.name; } moduleSize += entrySizes[i]; if (entry.type === NVMEntryType.NVMModuleDescriptor) { // End of NVM module // set size at the start this.setOne<number>(moduleKey!, moduleSize); // and descriptor at the end const moduleType = entry.name === "nvmZWlibraryDescriptor" ? NVMModuleType.ZW_LIBRARY : entry.name === "nvmApplicationDescriptor" ? NVMModuleType.APPLICATION : entry.name === "nvmHostApplicationDescriptor" ? NVMModuleType.HOST_APPLICATION : entry.name === "nvmDescriptorDescriptor" ? NVMModuleType.NVM_DESCRIPTOR : 0; this.setOne<NVMModuleDescriptor>(entry.name, { size: moduleSize, type: moduleType, version: entry.name === "nvmZWlibraryDescriptor" ? c.protocolVersion : c.applicationVersion, }); } } } public serialize(): Buffer { const ret = Buffer.alloc(this.nvmSize, 0xff); let offset = 0; for (const entry of this.impl.layout) { // In 500 NVMs there are no optional entries. Make sure they all exist const value = this.entries.get(entry.name); if (value == undefined) { throw new Error(`Required entry ${entry.name} is missing`); } const size = entry.size ?? NVMEntrySizes[entry.type]; const converted: Buffer[] = value.data.map((data) => { switch (entry.type) { case NVMEntryType.Byte: return Buffer.from([data as number]); case NVMEntryType.Word: case NVMEntryType.NVMModuleSize: { const ret = Buffer.allocUnsafe(2); ret.writeUInt16BE(data as number, 0); return ret; } case NVMEntryType.DWord: { const ret = Buffer.allocUnsafe(4); ret.writeUInt32BE(data as number, 0); return ret; } case NVMEntryType.NodeInfo: return data ? encodeNVM500NodeInfo(data as NVM500NodeInfo) : Buffer.alloc(size, 0); case NVMEntryType.NodeMask: { const ret = Buffer.alloc(size, 0); if (data) { encodeBitMask(data as number[], MAX_NODES, 1).copy( ret, 0, ); } return ret; } case NVMEntryType.SUCUpdateEntry: return encodeSUCUpdateEntry(data as SUCUpdateEntry); case NVMEntryType.Route: return encodeRoute(data as Route); case NVMEntryType.NVMModuleDescriptor: return encodeNVMModuleDescriptor( data as NVMModuleDescriptor, ); case NVMEntryType.NVMDescriptor: return encodeNVMDescriptor(data as NVMDescriptor); case NVMEntryType.Buffer: return data as Buffer; } }); for (const buf of converted) { buf.copy(ret, offset); offset += size; // Not all entries have the same size as the raw buffer } } return ret; } } export interface NVM500JSON { // To distinguish between 700 and 500 series JSONs better format: 500; meta?: NVM500Meta; controller: NVM500JSONController; nodes: Record<number, NVM500JSONNode>; } export interface NVM500Meta { manufacturerID: number; firmwareID: number; productType: number; productID: number; library: NVM500Details["library"]; } export interface NVM500JSONController { protocolVersion: string; applicationVersion: string; ownHomeId: string; learnedHomeId?: string | null; nodeId: number; lastNodeId: number; staticControllerNodeId: number; sucLastIndex: number; controllerConfiguration: number; sucUpdateEntries: SUCUpdateEntry[]; maxNodeId: number; reservedId: number; systemState: number; watchdogStarted: number; rfConfig: NVM500JSONControllerRFConfig; preferredRepeaters: number[]; // These are only the insecure ones commandClasses: CommandClasses[]; applicationData?: string | null; } export interface NVM500JSONControllerRFConfig { powerLevelNormal: number[]; powerLevelLow: number[]; powerMode: number; powerModeExtintEnable: number; powerModeWutTimeout: number; } export interface NVM500JSONNodeWithInfo extends NVM500NodeInfo { isVirtual: boolean; neighbors: number[]; sucUpdateIndex: number; appRouteLock: boolean; routeSlaveSUC: boolean; sucPendingUpdate: boolean; pendingDiscovery: boolean; lwr?: Route | null; nlwr?: Route | null; } export interface NVM500JSONVirtualNode { isVirtual: true; } export type NVM500JSONNode = NVM500JSONNodeWithInfo | NVM500JSONVirtualNode;